@shipfox/api-integration-jira 10.2.0 → 12.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 (65) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +1 -1
  4. package/dist/api/client.d.ts +15 -0
  5. package/dist/api/client.d.ts.map +1 -1
  6. package/dist/api/client.js +53 -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/install.d.ts +22 -0
  13. package/dist/core/install.d.ts.map +1 -1
  14. package/dist/core/install.js +68 -20
  15. package/dist/core/install.js.map +1 -1
  16. package/dist/core/webhook-processor.d.ts +15 -0
  17. package/dist/core/webhook-processor.d.ts.map +1 -0
  18. package/dist/core/webhook-processor.js +137 -0
  19. package/dist/core/webhook-processor.js.map +1 -0
  20. package/dist/core/webhook-registration.d.ts +26 -0
  21. package/dist/core/webhook-registration.d.ts.map +1 -0
  22. package/dist/core/webhook-registration.js +165 -0
  23. package/dist/core/webhook-registration.js.map +1 -0
  24. package/dist/core/webhook.d.ts +18 -0
  25. package/dist/core/webhook.d.ts.map +1 -0
  26. package/dist/core/webhook.js +38 -0
  27. package/dist/core/webhook.js.map +1 -0
  28. package/dist/db/installations.d.ts +11 -0
  29. package/dist/db/installations.d.ts.map +1 -1
  30. package/dist/db/installations.js +54 -5
  31. package/dist/db/installations.js.map +1 -1
  32. package/dist/index.d.ts +17 -5
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +61 -5
  35. package/dist/index.js.map +1 -1
  36. package/dist/presentation/routes/install.d.ts +22 -0
  37. package/dist/presentation/routes/install.d.ts.map +1 -1
  38. package/dist/presentation/routes/install.js.map +1 -1
  39. package/dist/presentation/routes/webhooks.d.ts +8 -0
  40. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  41. package/dist/presentation/routes/webhooks.js +108 -0
  42. package/dist/presentation/routes/webhooks.js.map +1 -0
  43. package/dist/tsconfig.test.tsbuildinfo +1 -1
  44. package/package.json +9 -7
  45. package/src/api/client.test.ts +106 -0
  46. package/src/api/client.ts +85 -0
  47. package/src/config.test.ts +0 -1
  48. package/src/config.ts +1 -4
  49. package/src/core/install.test.ts +179 -0
  50. package/src/core/install.ts +85 -19
  51. package/src/core/tokens-refresh.test.ts +2 -0
  52. package/src/core/webhook-processor.test.ts +418 -0
  53. package/src/core/webhook-processor.ts +166 -0
  54. package/src/core/webhook-registration.test.ts +375 -0
  55. package/src/core/webhook-registration.ts +216 -0
  56. package/src/core/webhook.ts +69 -0
  57. package/src/db/installations.test.ts +64 -0
  58. package/src/db/installations.ts +71 -5
  59. package/src/index.test.ts +8 -0
  60. package/src/index.ts +102 -3
  61. package/src/presentation/routes/install.ts +12 -0
  62. package/src/presentation/routes/webhooks.test.ts +132 -0
  63. package/src/presentation/routes/webhooks.ts +115 -0
  64. package/test/env.ts +0 -1
  65. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/installations.ts"],"sourcesContent":["import {isUniqueViolation} from '@shipfox/node-drizzle';\nimport {pgClient} from '@shipfox/node-postgres';\nimport {eq, sql} from 'drizzle-orm';\nimport {\n JiraConnectionAlreadyLinkedError,\n JiraInstallationAlreadyLinkedError,\n JiraInstallationSiteMismatchError,\n} from '#core/errors.js';\nimport {db} from './db.js';\nimport {jiraInstallations, toJiraInstallation} from './schema/installations.js';\n\nexport type JiraInstallationStatus = 'installed' | 'revoked';\n\nexport interface JiraInstallation {\n id: string;\n connectionId: string;\n cloudId: string;\n siteUrl: string;\n siteName: string;\n authorizingAccountId: string;\n scopes: string[];\n webhookIds: number[];\n webhookExpiresAt: Date | null;\n status: JiraInstallationStatus;\n tokenExpiresAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UpsertJiraInstallationParams {\n connectionId: string;\n cloudId: string;\n siteUrl: string;\n siteName: string;\n authorizingAccountId: string;\n scopes: string[];\n webhookIds?: number[] | undefined;\n webhookExpiresAt?: Date | null | undefined;\n status: JiraInstallationStatus;\n tokenExpiresAt?: Date | null | undefined;\n}\n\nexport interface UpdateJiraInstallationTokenExpiryParams {\n connectionId: string;\n tokenExpiresAt: Date | null;\n scopes?: string[] | undefined;\n}\n\ntype JiraDb = ReturnType<typeof db>;\ntype JiraTx = Parameters<Parameters<JiraDb['transaction']>[0]>[0];\n\nexport async function upsertJiraInstallation(\n params: UpsertJiraInstallationParams,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const now = new Date();\n const webhookIds = params.webhookIds ?? [];\n let row: typeof jiraInstallations.$inferSelect | undefined;\n try {\n [row] = await executor\n .insert(jiraInstallations)\n .values({\n connectionId: params.connectionId,\n cloudId: params.cloudId,\n siteUrl: params.siteUrl,\n siteName: params.siteName,\n authorizingAccountId: params.authorizingAccountId,\n scopes: params.scopes,\n webhookIds,\n webhookExpiresAt: params.webhookExpiresAt ?? null,\n status: params.status,\n tokenExpiresAt: params.tokenExpiresAt ?? null,\n })\n .onConflictDoUpdate({\n target: jiraInstallations.connectionId,\n setWhere: eq(jiraInstallations.cloudId, params.cloudId),\n set: {\n cloudId: params.cloudId,\n siteUrl: params.siteUrl,\n siteName: params.siteName,\n authorizingAccountId: params.authorizingAccountId,\n scopes: params.scopes,\n webhookIds,\n webhookExpiresAt: params.webhookExpiresAt ?? null,\n status: params.status,\n tokenExpiresAt: params.tokenExpiresAt ?? null,\n updatedAt: now,\n },\n })\n .returning();\n } catch (error) {\n if (isUniqueViolation(error, 'integrations_jira_installations_connection_unique')) {\n throw new JiraConnectionAlreadyLinkedError(params.connectionId);\n }\n if (isUniqueViolation(error, 'integrations_jira_installations_cloud_id_unique')) {\n throw new JiraInstallationAlreadyLinkedError(params.cloudId);\n }\n throw error;\n }\n\n if (!row) throw new JiraInstallationSiteMismatchError(params.connectionId, params.cloudId);\n return toJiraInstallation(row);\n}\n\nexport async function getJiraInstallationByCloudId(\n cloudId: string,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const rows = await executor\n .select()\n .from(jiraInstallations)\n .where(eq(jiraInstallations.cloudId, cloudId))\n .limit(1);\n const row = rows[0];\n return row ? toJiraInstallation(row) : undefined;\n}\n\nexport async function updateJiraInstallationTokenExpiry(\n params: UpdateJiraInstallationTokenExpiryParams,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const [row] = await executor\n .update(jiraInstallations)\n .set({\n tokenExpiresAt: params.tokenExpiresAt,\n ...(params.scopes === undefined ? {} : {scopes: params.scopes}),\n updatedAt: new Date(),\n })\n .where(eq(jiraInstallations.connectionId, params.connectionId))\n .returning();\n return row ? toJiraInstallation(row) : undefined;\n}\n\nexport async function deleteJiraInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<boolean> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const result = await executor\n .delete(jiraInstallations)\n .where(eq(jiraInstallations.connectionId, connectionId));\n return (result.rowCount ?? 0) > 0;\n}\n\nexport type JiraRefreshLockResult<T> = {acquired: true; value: T} | {acquired: false};\n\nexport function withJiraRefreshLock<T>(\n connectionId: string,\n fn: () => Promise<T>,\n): Promise<JiraRefreshLockResult<T>> {\n return withJiraRefreshLockClient(connectionId, fn);\n}\n\nasync function withJiraRefreshLockClient<T>(\n connectionId: string,\n fn: () => Promise<T>,\n): Promise<JiraRefreshLockResult<T>> {\n const client = await pgClient().connect();\n let acquired = false;\n try {\n const lock = await client.query<{acquired: boolean}>(\n 'SELECT pg_try_advisory_lock(hashtext($1)) AS acquired',\n [connectionId],\n );\n acquired = lock.rows[0]?.acquired === true;\n if (!acquired) return {acquired: false};\n return {acquired: true, value: await fn()};\n } finally {\n try {\n if (acquired) await client.query('SELECT pg_advisory_unlock(hashtext($1))', [connectionId]);\n } finally {\n client.release();\n }\n }\n}\n\nexport async function getJiraInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const rows = await executor\n .select()\n .from(jiraInstallations)\n .where(eq(jiraInstallations.connectionId, connectionId))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toJiraInstallation(row);\n}\n\nexport async function getJiraInstallationByWebhookId(\n webhookId: number,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const rows = await executor\n .select()\n .from(jiraInstallations)\n .where(sql`${jiraInstallations.webhookIds} @> ${JSON.stringify([webhookId])}::jsonb`)\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toJiraInstallation(row);\n}\n\nexport async function markJiraInstallationRevoked(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const [row] = await executor\n .update(jiraInstallations)\n .set({status: 'revoked', updatedAt: new Date()})\n .where(eq(jiraInstallations.connectionId, connectionId))\n .returning();\n if (!row) return undefined;\n return toJiraInstallation(row);\n}\n"],"names":["isUniqueViolation","pgClient","eq","sql","JiraConnectionAlreadyLinkedError","JiraInstallationAlreadyLinkedError","JiraInstallationSiteMismatchError","db","jiraInstallations","toJiraInstallation","upsertJiraInstallation","params","options","executor","tx","now","Date","webhookIds","row","insert","values","connectionId","cloudId","siteUrl","siteName","authorizingAccountId","scopes","webhookExpiresAt","status","tokenExpiresAt","onConflictDoUpdate","target","setWhere","set","updatedAt","returning","error","getJiraInstallationByCloudId","rows","select","from","where","limit","undefined","updateJiraInstallationTokenExpiry","update","deleteJiraInstallationByConnectionId","result","delete","rowCount","withJiraRefreshLock","fn","withJiraRefreshLockClient","client","connect","acquired","lock","query","value","release","getJiraInstallationByConnectionId","getJiraInstallationByWebhookId","webhookId","JSON","stringify","markJiraInstallationRevoked"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/db/installations.ts"],"sourcesContent":["import {isUniqueViolation} from '@shipfox/node-drizzle';\nimport {pgClient, withPostgresSession} from '@shipfox/node-postgres';\nimport {eq, sql} from 'drizzle-orm';\nimport {\n JiraConnectionAlreadyLinkedError,\n JiraInstallationAlreadyLinkedError,\n JiraInstallationSiteMismatchError,\n} from '#core/errors.js';\nimport {db} from './db.js';\nimport {jiraInstallations, toJiraInstallation} from './schema/installations.js';\n\nexport type JiraInstallationLock = <T>(lockKey: string, fn: () => Promise<T>) => Promise<T>;\n\nconst JIRA_INSTALLATION_LOCK_RETRY_DELAY_MS = 100;\nconst JIRA_INSTALLATION_LOCK_MAX_RETRY_DELAY_MS = 1_000;\nconst JIRA_INSTALLATION_LOCK_TIMEOUT_MS = 30_000;\n\nexport async function withJiraInstallationLock<T>(lockKey: string, fn: () => Promise<T>) {\n const advisoryKey = `jira-installation:${lockKey}`;\n const deadline = Date.now() + JIRA_INSTALLATION_LOCK_TIMEOUT_MS;\n let retryDelayMs = JIRA_INSTALLATION_LOCK_RETRY_DELAY_MS;\n\n while (true) {\n const attempt = await withPostgresSession(async (client) => {\n const lock = await client.query<{acquired: boolean}>(\n 'SELECT pg_try_advisory_lock(hashtext($1)) AS acquired',\n [advisoryKey],\n );\n if (lock.rows[0]?.acquired !== true) return {acquired: false as const};\n\n try {\n return {acquired: true as const, value: await fn()};\n } finally {\n await client.query('SELECT pg_advisory_unlock(hashtext($1))', [advisoryKey]);\n }\n });\n\n if (attempt.acquired) return attempt.value;\n if (Date.now() >= deadline) {\n throw new Error(`Timed out waiting for Jira installation lock: ${lockKey}`);\n }\n await new Promise((resolve) => setTimeout(resolve, retryDelayMs));\n retryDelayMs = Math.min(retryDelayMs * 2, JIRA_INSTALLATION_LOCK_MAX_RETRY_DELAY_MS);\n }\n}\n\nexport type JiraInstallationStatus = 'installed' | 'revoked';\n\nexport interface JiraInstallation {\n id: string;\n connectionId: string;\n cloudId: string;\n siteUrl: string;\n siteName: string;\n authorizingAccountId: string;\n scopes: string[];\n webhookIds: number[];\n webhookExpiresAt: Date | null;\n status: JiraInstallationStatus;\n tokenExpiresAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface UpsertJiraInstallationParams {\n connectionId: string;\n cloudId: string;\n siteUrl: string;\n siteName: string;\n authorizingAccountId: string;\n scopes: string[];\n webhookIds?: number[] | undefined;\n webhookExpiresAt?: Date | null | undefined;\n status: JiraInstallationStatus;\n tokenExpiresAt?: Date | null | undefined;\n}\n\nexport interface UpdateJiraInstallationTokenExpiryParams {\n connectionId: string;\n tokenExpiresAt: Date | null;\n scopes?: string[] | undefined;\n}\n\nexport interface UpdateJiraInstallationWebhookParams {\n connectionId: string;\n webhookIds: number[];\n webhookExpiresAt: Date | null;\n}\n\ntype JiraDb = ReturnType<typeof db>;\ntype JiraTx = Parameters<Parameters<JiraDb['transaction']>[0]>[0];\n\nexport async function upsertJiraInstallation(\n params: UpsertJiraInstallationParams,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const now = new Date();\n let row: typeof jiraInstallations.$inferSelect | undefined;\n try {\n [row] = await executor\n .insert(jiraInstallations)\n .values({\n connectionId: params.connectionId,\n cloudId: params.cloudId,\n siteUrl: params.siteUrl,\n siteName: params.siteName,\n authorizingAccountId: params.authorizingAccountId,\n scopes: params.scopes,\n webhookIds: params.webhookIds ?? [],\n webhookExpiresAt: params.webhookExpiresAt ?? null,\n status: params.status,\n tokenExpiresAt: params.tokenExpiresAt ?? null,\n })\n .onConflictDoUpdate({\n target: jiraInstallations.connectionId,\n setWhere: eq(jiraInstallations.cloudId, params.cloudId),\n set: {\n cloudId: params.cloudId,\n siteUrl: params.siteUrl,\n siteName: params.siteName,\n authorizingAccountId: params.authorizingAccountId,\n scopes: params.scopes,\n ...(params.webhookIds === undefined ? {} : {webhookIds: params.webhookIds}),\n ...(params.webhookExpiresAt === undefined\n ? {}\n : {webhookExpiresAt: params.webhookExpiresAt}),\n status: params.status,\n tokenExpiresAt: params.tokenExpiresAt ?? null,\n updatedAt: now,\n },\n })\n .returning();\n } catch (error) {\n if (isUniqueViolation(error, 'integrations_jira_installations_connection_unique')) {\n throw new JiraConnectionAlreadyLinkedError(params.connectionId);\n }\n if (isUniqueViolation(error, 'integrations_jira_installations_cloud_id_unique')) {\n throw new JiraInstallationAlreadyLinkedError(params.cloudId);\n }\n throw error;\n }\n\n if (!row) throw new JiraInstallationSiteMismatchError(params.connectionId, params.cloudId);\n return toJiraInstallation(row);\n}\n\nexport async function getJiraInstallationByCloudId(\n cloudId: string,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const rows = await executor\n .select()\n .from(jiraInstallations)\n .where(eq(jiraInstallations.cloudId, cloudId))\n .limit(1);\n const row = rows[0];\n return row ? toJiraInstallation(row) : undefined;\n}\n\nexport async function updateJiraInstallationTokenExpiry(\n params: UpdateJiraInstallationTokenExpiryParams,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const [row] = await executor\n .update(jiraInstallations)\n .set({\n tokenExpiresAt: params.tokenExpiresAt,\n ...(params.scopes === undefined ? {} : {scopes: params.scopes}),\n updatedAt: new Date(),\n })\n .where(eq(jiraInstallations.connectionId, params.connectionId))\n .returning();\n return row ? toJiraInstallation(row) : undefined;\n}\n\nexport async function updateJiraInstallationWebhook(\n params: UpdateJiraInstallationWebhookParams,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const [row] = await executor\n .update(jiraInstallations)\n .set({\n webhookIds: params.webhookIds,\n webhookExpiresAt: params.webhookExpiresAt,\n updatedAt: new Date(),\n })\n .where(eq(jiraInstallations.connectionId, params.connectionId))\n .returning();\n return row ? toJiraInstallation(row) : undefined;\n}\n\nexport async function deleteJiraInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<boolean> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const result = await executor\n .delete(jiraInstallations)\n .where(eq(jiraInstallations.connectionId, connectionId));\n return (result.rowCount ?? 0) > 0;\n}\n\nexport type JiraRefreshLockResult<T> = {acquired: true; value: T} | {acquired: false};\n\nexport function withJiraRefreshLock<T>(\n connectionId: string,\n fn: () => Promise<T>,\n): Promise<JiraRefreshLockResult<T>> {\n return withJiraRefreshLockClient(connectionId, fn);\n}\n\nexport function withJiraWebhookRegistrationLock<T>(\n lockKey: string,\n fn: (tx?: unknown) => Promise<T>,\n): Promise<T> {\n return withJiraInstallationLock(lockKey, () => fn());\n}\n\nasync function withJiraRefreshLockClient<T>(\n connectionId: string,\n fn: () => Promise<T>,\n): Promise<JiraRefreshLockResult<T>> {\n const client = await pgClient().connect();\n let acquired = false;\n try {\n const lock = await client.query<{acquired: boolean}>(\n 'SELECT pg_try_advisory_lock(hashtext($1)) AS acquired',\n [connectionId],\n );\n acquired = lock.rows[0]?.acquired === true;\n if (!acquired) return {acquired: false};\n return {acquired: true, value: await fn()};\n } finally {\n try {\n if (acquired) await client.query('SELECT pg_advisory_unlock(hashtext($1))', [connectionId]);\n } finally {\n client.release();\n }\n }\n}\n\nexport async function getJiraInstallationByConnectionId(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const rows = await executor\n .select()\n .from(jiraInstallations)\n .where(eq(jiraInstallations.connectionId, connectionId))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toJiraInstallation(row);\n}\n\nexport async function getJiraInstallationByWebhookId(\n webhookId: number,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const rows = await executor\n .select()\n .from(jiraInstallations)\n .where(sql`${jiraInstallations.webhookIds} @> ${JSON.stringify([webhookId])}::jsonb`)\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toJiraInstallation(row);\n}\n\nexport async function markJiraInstallationRevoked(\n connectionId: string,\n options: {tx?: unknown} = {},\n): Promise<JiraInstallation | undefined> {\n const executor = (options.tx ?? db()) as JiraDb | JiraTx;\n const [row] = await executor\n .update(jiraInstallations)\n .set({status: 'revoked', updatedAt: new Date()})\n .where(eq(jiraInstallations.connectionId, connectionId))\n .returning();\n if (!row) return undefined;\n return toJiraInstallation(row);\n}\n"],"names":["isUniqueViolation","pgClient","withPostgresSession","eq","sql","JiraConnectionAlreadyLinkedError","JiraInstallationAlreadyLinkedError","JiraInstallationSiteMismatchError","db","jiraInstallations","toJiraInstallation","JIRA_INSTALLATION_LOCK_RETRY_DELAY_MS","JIRA_INSTALLATION_LOCK_MAX_RETRY_DELAY_MS","JIRA_INSTALLATION_LOCK_TIMEOUT_MS","withJiraInstallationLock","lockKey","fn","advisoryKey","deadline","Date","now","retryDelayMs","attempt","client","lock","query","rows","acquired","value","Error","Promise","resolve","setTimeout","Math","min","upsertJiraInstallation","params","options","executor","tx","row","insert","values","connectionId","cloudId","siteUrl","siteName","authorizingAccountId","scopes","webhookIds","webhookExpiresAt","status","tokenExpiresAt","onConflictDoUpdate","target","setWhere","set","undefined","updatedAt","returning","error","getJiraInstallationByCloudId","select","from","where","limit","updateJiraInstallationTokenExpiry","update","updateJiraInstallationWebhook","deleteJiraInstallationByConnectionId","result","delete","rowCount","withJiraRefreshLock","withJiraRefreshLockClient","withJiraWebhookRegistrationLock","connect","release","getJiraInstallationByConnectionId","getJiraInstallationByWebhookId","webhookId","JSON","stringify","markJiraInstallationRevoked"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -4,9 +4,10 @@ import { closeDb, db } from '#db/db.js';
4
4
  import { getJiraInstallationByConnectionId } from '#db/installations.js';
5
5
  import { migrationsPath } from '#db/migrations.js';
6
6
  import { type CreateJiraIntegrationRoutesOptions } from '#presentation/routes/install.js';
7
+ import { type CreateJiraWebhookRoutesOptions } from '#presentation/routes/webhooks.js';
7
8
  export type { JiraProvider } from '@shipfox/api-integration-jira-dto';
8
- export type { JiraAccessibleResource, JiraApiClient, JiraAuthorization, JiraIdentity, } from '#api/client.js';
9
- export { createJiraApiClient, mapJiraError } from '#api/client.js';
9
+ export type { JiraAccessibleResource, JiraApiClient, JiraAuthorization, JiraDynamicWebhookRegistration, JiraIdentity, } from '#api/client.js';
10
+ export { createJiraApiClient, JIRA_DYNAMIC_WEBHOOK_EVENTS, JIRA_DYNAMIC_WEBHOOK_JQL, mapJiraError, } from '#api/client.js';
10
11
  export type { DisconnectJiraInstallationParams } from '#core/disconnect.js';
11
12
  export { disconnectJiraInstallation } from '#core/disconnect.js';
12
13
  export { JiraAccessTokenMissingError, JiraAuthorizationScopeMismatchError, JiraConnectionAlreadyLinkedError, JiraConnectionNotFoundError, JiraInstallationAlreadyLinkedError, JiraInstallationSiteMismatchError, JiraInstallStateActorMismatchError, JiraInstallStateError, JiraIntegrationProviderError, JiraOAuthCallbackError, JiraOfflineAccessNotGrantedError, JiraPendingSelectionNotFoundError, JiraSiteSelectionMismatchError, JiraTokenUnrefreshableError, } from '#core/errors.js';
@@ -18,14 +19,21 @@ export { assertJiraAuthorizationScopes, formatJiraOAuthScopes, JIRA_OAUTH_SCOPES
18
19
  export { signJiraInstallState, verifyJiraInstallState } from '#core/state.js';
19
20
  export type { CreateJiraTokenStoreParams, GetJiraAccessTokenParams, JiraConnectionResolverResult, JiraSecretsStore, JiraTokenStore, StoreJiraTokensParams, } from '#core/tokens.js';
20
21
  export { createJiraTokenStore, jiraSecretsNamespace } from '#core/tokens.js';
21
- export type { JiraInstallation, JiraInstallationStatus, UpsertJiraInstallationParams, } from '#db/installations.js';
22
- export { deleteJiraInstallationByConnectionId, getJiraInstallationByCloudId, getJiraInstallationByConnectionId, getJiraInstallationByWebhookId, markJiraInstallationRevoked, updateJiraInstallationTokenExpiry, upsertJiraInstallation, withJiraRefreshLock, } from '#db/installations.js';
22
+ export type { CreateJiraWebhookProcessorOptions, JiraWebhookProcessor, } from '#core/webhook-processor.js';
23
+ export { createJiraWebhookProcessor } from '#core/webhook-processor.js';
24
+ export type { RegisterJiraWebhookParams } from '#core/webhook-registration.js';
25
+ export { JIRA_WEBHOOK_TTL_MS, registerJiraWebhook } from '#core/webhook-registration.js';
26
+ export type { JiraInstallation, JiraInstallationLock, JiraInstallationStatus, UpdateJiraInstallationWebhookParams, UpsertJiraInstallationParams, } from '#db/installations.js';
27
+ export { deleteJiraInstallationByConnectionId, getJiraInstallationByCloudId, getJiraInstallationByConnectionId, getJiraInstallationByWebhookId, markJiraInstallationRevoked, updateJiraInstallationTokenExpiry, updateJiraInstallationWebhook, upsertJiraInstallation, withJiraRefreshLock, withJiraWebhookRegistrationLock, } from '#db/installations.js';
28
+ export type { CreateJiraWebhookRoutesOptions } from '#presentation/routes/webhooks.js';
29
+ export { createJiraWebhookRoutes } from '#presentation/routes/webhooks.js';
23
30
  export { closeDb, config, db, migrationsPath };
24
31
  export interface CreateJiraIntegrationProviderOptions {
25
32
  jira?: JiraApiClient | undefined;
26
33
  getJiraInstallationByConnectionId?: typeof getJiraInstallationByConnectionId | undefined;
27
- routes?: Omit<CreateJiraIntegrationRoutesOptions, 'jira' | 'connectionCapabilities'> | undefined;
34
+ routes?: JiraIntegrationProviderRoutesOptions | undefined;
28
35
  }
36
+ type JiraIntegrationProviderRoutesOptions = Omit<CreateJiraIntegrationRoutesOptions, 'jira' | 'connectionCapabilities' | 'registerJiraWebhook'> & Omit<CreateJiraWebhookRoutesOptions, 'processor'>;
29
37
  export declare function createJiraIntegrationProvider(options?: CreateJiraIntegrationProviderOptions): {
30
38
  provider: string;
31
39
  displayName: string;
@@ -34,5 +42,9 @@ export declare function createJiraIntegrationProvider(options?: CreateJiraIntegr
34
42
  id: string;
35
43
  }): Promise<string | undefined>;
36
44
  routes: import("@shipfox/node-fastify").RouteGroup[];
45
+ webhookProcessors: {
46
+ routeIds: readonly ["jira"];
47
+ processor: import("#core/webhook-processor.js").JiraWebhookProcessor;
48
+ }[] | undefined;
37
49
  };
38
50
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,aAAa,EAAC,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAC,MAAM,EAAC,MAAM,YAAY,CAAC;AAClC,OAAO,EAAC,OAAO,EAAE,EAAE,EAAC,MAAM,WAAW,CAAC;AACtC,OAAO,EAAC,iCAAiC,EAAC,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAC,cAAc,EAAC,MAAM,mBAAmB,CAAC;AACjD,OAAO,EACL,KAAK,kCAAkC,EAExC,MAAM,iCAAiC,CAAC;AAEzC,YAAY,EAAC,YAAY,EAAC,MAAM,mCAAmC,CAAC;AACpE,YAAY,EACV,sBAAsB,EACtB,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAC,mBAAmB,EAAE,YAAY,EAAC,MAAM,gBAAgB,CAAC;AACjE,YAAY,EAAC,gCAAgC,EAAC,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAC,0BAA0B,EAAC,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EACL,2BAA2B,EAC3B,mCAAmC,EACnC,gCAAgC,EAChC,2BAA2B,EAC3B,kCAAkC,EAClC,iCAAiC,EACjC,kCAAkC,EAClC,qBAAqB,EACrB,4BAA4B,EAC5B,sBAAsB,EACtB,gCAAgC,EAChC,iCAAiC,EACjC,8BAA8B,EAC9B,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAC,4BAA4B,EAAE,wBAAwB,EAAC,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EACL,kBAAkB,EAClB,4BAA4B,EAC5B,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAC,gCAAgC,EAAE,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAClG,OAAO,EAAC,+BAA+B,EAAE,2BAA2B,EAAC,MAAM,kBAAkB,CAAC;AAC9F,OAAO,EACL,6BAA6B,EAC7B,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAC,oBAAoB,EAAE,sBAAsB,EAAC,MAAM,gBAAgB,CAAC;AAC5E,YAAY,EACV,0BAA0B,EAC1B,wBAAwB,EACxB,4BAA4B,EAC5B,gBAAgB,EAChB,cAAc,EACd,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAC,oBAAoB,EAAE,oBAAoB,EAAC,MAAM,iBAAiB,CAAC;AAC3E,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oCAAoC,EACpC,4BAA4B,EAC5B,iCAAiC,EACjC,8BAA8B,EAC9B,2BAA2B,EAC3B,iCAAiC,EACjC,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,cAAc,EAAC,CAAC;AAE7C,MAAM,WAAW,oCAAoC;IACnD,IAAI,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACjC,iCAAiC,CAAC,EAAE,OAAO,iCAAiC,GAAG,SAAS,CAAC;IACzF,MAAM,CAAC,EAAE,IAAI,CAAC,kCAAkC,EAAE,MAAM,GAAG,wBAAwB,CAAC,GAAG,SAAS,CAAC;CAClG;AAED,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,oCAAyC;;;;sCAWpD;QAAC,EAAE,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;;EAKrF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,11 +1,15 @@
1
1
  import { JIRA_PROVIDER } from '@shipfox/api-integration-jira-dto';
2
2
  import { createJiraApiClient } from '#api/client.js';
3
3
  import { config } from '#config.js';
4
+ import { createJiraWebhookProcessor } from '#core/webhook-processor.js';
5
+ import { registerJiraWebhook } from '#core/webhook-registration.js';
4
6
  import { closeDb, db } from '#db/db.js';
5
7
  import { getJiraInstallationByConnectionId } from '#db/installations.js';
6
8
  import { migrationsPath } from '#db/migrations.js';
7
9
  import { createJiraIntegrationRoutes } from '#presentation/routes/install.js';
8
- export { createJiraApiClient, mapJiraError } from '#api/client.js';
10
+ import { createJiraWebhookRoutes, JIRA_WEBHOOK_ROUTE_PREFIX } from '#presentation/routes/webhooks.js';
11
+ const TRAILING_SLASHES_RE = /\/+$/;
12
+ export { createJiraApiClient, JIRA_DYNAMIC_WEBHOOK_EVENTS, JIRA_DYNAMIC_WEBHOOK_JQL, mapJiraError } from '#api/client.js';
9
13
  export { disconnectJiraInstallation } from '#core/disconnect.js';
10
14
  export { JiraAccessTokenMissingError, JiraAuthorizationScopeMismatchError, JiraConnectionAlreadyLinkedError, JiraConnectionNotFoundError, JiraInstallationAlreadyLinkedError, JiraInstallationSiteMismatchError, JiraInstallStateActorMismatchError, JiraInstallStateError, JiraIntegrationProviderError, JiraOAuthCallbackError, JiraOfflineAccessNotGrantedError, JiraPendingSelectionNotFoundError, JiraSiteSelectionMismatchError, JiraTokenUnrefreshableError } from '#core/errors.js';
11
15
  export { handleJiraCallback, handleJiraOAuthCallbackError, handleJiraSiteSelection } from '#core/install.js';
@@ -13,17 +17,48 @@ export { createJiraPendingSelectionStore, jiraPendingSecretsNamespace } from '#c
13
17
  export { assertJiraAuthorizationScopes, formatJiraOAuthScopes, JIRA_OAUTH_SCOPES } from '#core/scopes.js';
14
18
  export { signJiraInstallState, verifyJiraInstallState } from '#core/state.js';
15
19
  export { createJiraTokenStore, jiraSecretsNamespace } from '#core/tokens.js';
16
- export { deleteJiraInstallationByConnectionId, getJiraInstallationByCloudId, getJiraInstallationByConnectionId, getJiraInstallationByWebhookId, markJiraInstallationRevoked, updateJiraInstallationTokenExpiry, upsertJiraInstallation, withJiraRefreshLock } from '#db/installations.js';
20
+ export { createJiraWebhookProcessor } from '#core/webhook-processor.js';
21
+ export { JIRA_WEBHOOK_TTL_MS, registerJiraWebhook } from '#core/webhook-registration.js';
22
+ export { deleteJiraInstallationByConnectionId, getJiraInstallationByCloudId, getJiraInstallationByConnectionId, getJiraInstallationByWebhookId, markJiraInstallationRevoked, updateJiraInstallationTokenExpiry, updateJiraInstallationWebhook, upsertJiraInstallation, withJiraRefreshLock, withJiraWebhookRegistrationLock } from '#db/installations.js';
23
+ export { createJiraWebhookRoutes } from '#presentation/routes/webhooks.js';
17
24
  export { closeDb, config, db, migrationsPath };
18
25
  export function createJiraIntegrationProvider(options = {}) {
19
26
  const jira = options.jira ?? createJiraApiClient();
20
27
  const getInstallationByConnectionId = options.getJiraInstallationByConnectionId ?? getJiraInstallationByConnectionId;
28
+ const webhookOptions = toJiraWebhookOptions(options.routes);
29
+ const webhookProcessor = webhookOptions ? createJiraWebhookProcessor(webhookOptions) : undefined;
30
+ const webhookRoutes = webhookOptions ? [
31
+ createJiraWebhookRoutes({
32
+ coreDb: webhookOptions.coreDb,
33
+ publishIntegrationEventReceived: webhookOptions.publishIntegrationEventReceived,
34
+ recordDeliveryOnly: webhookOptions.recordDeliveryOnly,
35
+ getIntegrationConnectionById: webhookOptions.getIntegrationConnectionById,
36
+ processor: webhookProcessor
37
+ })
38
+ ] : [];
21
39
  const routes = options.routes ? [
22
40
  createJiraIntegrationRoutes({
23
41
  jira,
24
42
  connectionCapabilities: [],
25
- ...options.routes
26
- })
43
+ ...options.routes,
44
+ registerJiraWebhook: (input)=>registerJiraWebhook({
45
+ jira,
46
+ connectionId: input.connectionId,
47
+ cloudId: input.cloudId,
48
+ accessToken: input.accessToken,
49
+ webhookUrl: jiraWebhookUrl(input.connectionId),
50
+ ...input.withRegistrationLock ? {
51
+ withRegistrationLock: input.withRegistrationLock
52
+ } : {},
53
+ ...input.onRegistrationSuccess ? {
54
+ onRegistrationSuccess: input.onRegistrationSuccess
55
+ } : {},
56
+ ...input.onRegistrationFailure ? {
57
+ onRegistrationFailure: input.onRegistrationFailure
58
+ } : {}
59
+ }).then(()=>undefined)
60
+ }),
61
+ ...webhookRoutes
27
62
  ] : [];
28
63
  return {
29
64
  provider: JIRA_PROVIDER,
@@ -32,7 +67,28 @@ export function createJiraIntegrationProvider(options = {}) {
32
67
  async connectionExternalUrl (connection) {
33
68
  return (await getInstallationByConnectionId(connection.id))?.siteUrl;
34
69
  },
35
- routes
70
+ routes,
71
+ webhookProcessors: webhookProcessor ? [
72
+ {
73
+ routeIds: [
74
+ 'jira'
75
+ ],
76
+ processor: webhookProcessor
77
+ }
78
+ ] : undefined
79
+ };
80
+ }
81
+ function jiraWebhookUrl(connectionId) {
82
+ return `${config.JIRA_WEBHOOK_BASE_URL.replace(TRAILING_SLASHES_RE, '')}${JIRA_WEBHOOK_ROUTE_PREFIX}/${connectionId}`;
83
+ }
84
+ function toJiraWebhookOptions(routes) {
85
+ if (!routes) return undefined;
86
+ if (routes.coreDb === undefined || routes.publishIntegrationEventReceived === undefined || routes.recordDeliveryOnly === undefined || routes.getIntegrationConnectionById === undefined) throw new Error('Jira integration provider requires all webhook receiver dependencies: coreDb, publishIntegrationEventReceived, recordDeliveryOnly, and getIntegrationConnectionById');
87
+ return {
88
+ coreDb: routes.coreDb,
89
+ publishIntegrationEventReceived: routes.publishIntegrationEventReceived,
90
+ recordDeliveryOnly: routes.recordDeliveryOnly,
91
+ getIntegrationConnectionById: routes.getIntegrationConnectionById
36
92
  };
37
93
  }
38
94
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {JIRA_PROVIDER} from '@shipfox/api-integration-jira-dto';\nimport {createJiraApiClient, type JiraApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {closeDb, db} from '#db/db.js';\nimport {getJiraInstallationByConnectionId} from '#db/installations.js';\nimport {migrationsPath} from '#db/migrations.js';\nimport {\n type CreateJiraIntegrationRoutesOptions,\n createJiraIntegrationRoutes,\n} from '#presentation/routes/install.js';\n\nexport type {JiraProvider} from '@shipfox/api-integration-jira-dto';\nexport type {\n JiraAccessibleResource,\n JiraApiClient,\n JiraAuthorization,\n JiraIdentity,\n} from '#api/client.js';\nexport {createJiraApiClient, mapJiraError} from '#api/client.js';\nexport type {DisconnectJiraInstallationParams} from '#core/disconnect.js';\nexport {disconnectJiraInstallation} from '#core/disconnect.js';\nexport {\n JiraAccessTokenMissingError,\n JiraAuthorizationScopeMismatchError,\n JiraConnectionAlreadyLinkedError,\n JiraConnectionNotFoundError,\n JiraInstallationAlreadyLinkedError,\n JiraInstallationSiteMismatchError,\n JiraInstallStateActorMismatchError,\n JiraInstallStateError,\n JiraIntegrationProviderError,\n JiraOAuthCallbackError,\n JiraOfflineAccessNotGrantedError,\n JiraPendingSelectionNotFoundError,\n JiraSiteSelectionMismatchError,\n JiraTokenUnrefreshableError,\n} from '#core/errors.js';\nexport type {ConnectJiraInstallationInput, HandleJiraCallbackParams} from '#core/install.js';\nexport {\n handleJiraCallback,\n handleJiraOAuthCallbackError,\n handleJiraSiteSelection,\n} from '#core/install.js';\nexport type {JiraPendingSelectionSecretsStore, JiraPendingSelectionStore} from '#core/pending.js';\nexport {createJiraPendingSelectionStore, jiraPendingSecretsNamespace} from '#core/pending.js';\nexport {\n assertJiraAuthorizationScopes,\n formatJiraOAuthScopes,\n JIRA_OAUTH_SCOPES,\n} from '#core/scopes.js';\nexport {signJiraInstallState, verifyJiraInstallState} from '#core/state.js';\nexport type {\n CreateJiraTokenStoreParams,\n GetJiraAccessTokenParams,\n JiraConnectionResolverResult,\n JiraSecretsStore,\n JiraTokenStore,\n StoreJiraTokensParams,\n} from '#core/tokens.js';\nexport {createJiraTokenStore, jiraSecretsNamespace} from '#core/tokens.js';\nexport type {\n JiraInstallation,\n JiraInstallationStatus,\n UpsertJiraInstallationParams,\n} from '#db/installations.js';\nexport {\n deleteJiraInstallationByConnectionId,\n getJiraInstallationByCloudId,\n getJiraInstallationByConnectionId,\n getJiraInstallationByWebhookId,\n markJiraInstallationRevoked,\n updateJiraInstallationTokenExpiry,\n upsertJiraInstallation,\n withJiraRefreshLock,\n} from '#db/installations.js';\nexport {closeDb, config, db, migrationsPath};\n\nexport interface CreateJiraIntegrationProviderOptions {\n jira?: JiraApiClient | undefined;\n getJiraInstallationByConnectionId?: typeof getJiraInstallationByConnectionId | undefined;\n routes?: Omit<CreateJiraIntegrationRoutesOptions, 'jira' | 'connectionCapabilities'> | undefined;\n}\n\nexport function createJiraIntegrationProvider(options: CreateJiraIntegrationProviderOptions = {}) {\n const jira = options.jira ?? createJiraApiClient();\n const getInstallationByConnectionId =\n options.getJiraInstallationByConnectionId ?? getJiraInstallationByConnectionId;\n const routes = options.routes\n ? [createJiraIntegrationRoutes({jira, connectionCapabilities: [], ...options.routes})]\n : [];\n return {\n provider: JIRA_PROVIDER,\n displayName: 'Jira',\n adapters: {},\n async connectionExternalUrl(connection: {id: string}): Promise<string | undefined> {\n return (await getInstallationByConnectionId(connection.id))?.siteUrl;\n },\n routes,\n };\n}\n"],"names":["JIRA_PROVIDER","createJiraApiClient","config","closeDb","db","getJiraInstallationByConnectionId","migrationsPath","createJiraIntegrationRoutes","mapJiraError","disconnectJiraInstallation","JiraAccessTokenMissingError","JiraAuthorizationScopeMismatchError","JiraConnectionAlreadyLinkedError","JiraConnectionNotFoundError","JiraInstallationAlreadyLinkedError","JiraInstallationSiteMismatchError","JiraInstallStateActorMismatchError","JiraInstallStateError","JiraIntegrationProviderError","JiraOAuthCallbackError","JiraOfflineAccessNotGrantedError","JiraPendingSelectionNotFoundError","JiraSiteSelectionMismatchError","JiraTokenUnrefreshableError","handleJiraCallback","handleJiraOAuthCallbackError","handleJiraSiteSelection","createJiraPendingSelectionStore","jiraPendingSecretsNamespace","assertJiraAuthorizationScopes","formatJiraOAuthScopes","JIRA_OAUTH_SCOPES","signJiraInstallState","verifyJiraInstallState","createJiraTokenStore","jiraSecretsNamespace","deleteJiraInstallationByConnectionId","getJiraInstallationByCloudId","getJiraInstallationByWebhookId","markJiraInstallationRevoked","updateJiraInstallationTokenExpiry","upsertJiraInstallation","withJiraRefreshLock","createJiraIntegrationProvider","options","jira","getInstallationByConnectionId","routes","connectionCapabilities","provider","displayName","adapters","connectionExternalUrl","connection","id","siteUrl"],"mappings":"AAAA,SAAQA,aAAa,QAAO,oCAAoC;AAChE,SAAQC,mBAAmB,QAA2B,iBAAiB;AACvE,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,OAAO,EAAEC,EAAE,QAAO,YAAY;AACtC,SAAQC,iCAAiC,QAAO,uBAAuB;AACvE,SAAQC,cAAc,QAAO,oBAAoB;AACjD,SAEEC,2BAA2B,QACtB,kCAAkC;AASzC,SAAQN,mBAAmB,EAAEO,YAAY,QAAO,iBAAiB;AAEjE,SAAQC,0BAA0B,QAAO,sBAAsB;AAC/D,SACEC,2BAA2B,EAC3BC,mCAAmC,EACnCC,gCAAgC,EAChCC,2BAA2B,EAC3BC,kCAAkC,EAClCC,iCAAiC,EACjCC,kCAAkC,EAClCC,qBAAqB,EACrBC,4BAA4B,EAC5BC,sBAAsB,EACtBC,gCAAgC,EAChCC,iCAAiC,EACjCC,8BAA8B,EAC9BC,2BAA2B,QACtB,kBAAkB;AAEzB,SACEC,kBAAkB,EAClBC,4BAA4B,EAC5BC,uBAAuB,QAClB,mBAAmB;AAE1B,SAAQC,+BAA+B,EAAEC,2BAA2B,QAAO,mBAAmB;AAC9F,SACEC,6BAA6B,EAC7BC,qBAAqB,EACrBC,iBAAiB,QACZ,kBAAkB;AACzB,SAAQC,oBAAoB,EAAEC,sBAAsB,QAAO,iBAAiB;AAS5E,SAAQC,oBAAoB,EAAEC,oBAAoB,QAAO,kBAAkB;AAM3E,SACEC,oCAAoC,EACpCC,4BAA4B,EAC5BhC,iCAAiC,EACjCiC,8BAA8B,EAC9BC,2BAA2B,EAC3BC,iCAAiC,EACjCC,sBAAsB,EACtBC,mBAAmB,QACd,uBAAuB;AAC9B,SAAQvC,OAAO,EAAED,MAAM,EAAEE,EAAE,EAAEE,cAAc,GAAE;AAQ7C,OAAO,SAASqC,8BAA8BC,UAAgD,CAAC,CAAC;IAC9F,MAAMC,OAAOD,QAAQC,IAAI,IAAI5C;IAC7B,MAAM6C,gCACJF,QAAQvC,iCAAiC,IAAIA;IAC/C,MAAM0C,SAASH,QAAQG,MAAM,GACzB;QAACxC,4BAA4B;YAACsC;YAAMG,wBAAwB,EAAE;YAAE,GAAGJ,QAAQG,MAAM;QAAA;KAAG,GACpF,EAAE;IACN,OAAO;QACLE,UAAUjD;QACVkD,aAAa;QACbC,UAAU,CAAC;QACX,MAAMC,uBAAsBC,UAAwB;YAClD,OAAQ,CAAA,MAAMP,8BAA8BO,WAAWC,EAAE,CAAA,GAAIC;QAC/D;QACAR;IACF;AACF"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {JIRA_PROVIDER} from '@shipfox/api-integration-jira-dto';\nimport {createJiraApiClient, type JiraApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {createJiraWebhookProcessor} from '#core/webhook-processor.js';\nimport {registerJiraWebhook} from '#core/webhook-registration.js';\nimport {closeDb, db} from '#db/db.js';\nimport {getJiraInstallationByConnectionId} from '#db/installations.js';\nimport {migrationsPath} from '#db/migrations.js';\nimport {\n type CreateJiraIntegrationRoutesOptions,\n createJiraIntegrationRoutes,\n} from '#presentation/routes/install.js';\nimport {\n type CreateJiraWebhookRoutesOptions,\n createJiraWebhookRoutes,\n JIRA_WEBHOOK_ROUTE_PREFIX,\n} from '#presentation/routes/webhooks.js';\n\nconst TRAILING_SLASHES_RE = /\\/+$/;\n\nexport type {JiraProvider} from '@shipfox/api-integration-jira-dto';\nexport type {\n JiraAccessibleResource,\n JiraApiClient,\n JiraAuthorization,\n JiraDynamicWebhookRegistration,\n JiraIdentity,\n} from '#api/client.js';\nexport {\n createJiraApiClient,\n JIRA_DYNAMIC_WEBHOOK_EVENTS,\n JIRA_DYNAMIC_WEBHOOK_JQL,\n mapJiraError,\n} from '#api/client.js';\nexport type {DisconnectJiraInstallationParams} from '#core/disconnect.js';\nexport {disconnectJiraInstallation} from '#core/disconnect.js';\nexport {\n JiraAccessTokenMissingError,\n JiraAuthorizationScopeMismatchError,\n JiraConnectionAlreadyLinkedError,\n JiraConnectionNotFoundError,\n JiraInstallationAlreadyLinkedError,\n JiraInstallationSiteMismatchError,\n JiraInstallStateActorMismatchError,\n JiraInstallStateError,\n JiraIntegrationProviderError,\n JiraOAuthCallbackError,\n JiraOfflineAccessNotGrantedError,\n JiraPendingSelectionNotFoundError,\n JiraSiteSelectionMismatchError,\n JiraTokenUnrefreshableError,\n} from '#core/errors.js';\nexport type {ConnectJiraInstallationInput, HandleJiraCallbackParams} from '#core/install.js';\nexport {\n handleJiraCallback,\n handleJiraOAuthCallbackError,\n handleJiraSiteSelection,\n} from '#core/install.js';\nexport type {JiraPendingSelectionSecretsStore, JiraPendingSelectionStore} from '#core/pending.js';\nexport {createJiraPendingSelectionStore, jiraPendingSecretsNamespace} from '#core/pending.js';\nexport {\n assertJiraAuthorizationScopes,\n formatJiraOAuthScopes,\n JIRA_OAUTH_SCOPES,\n} from '#core/scopes.js';\nexport {signJiraInstallState, verifyJiraInstallState} from '#core/state.js';\nexport type {\n CreateJiraTokenStoreParams,\n GetJiraAccessTokenParams,\n JiraConnectionResolverResult,\n JiraSecretsStore,\n JiraTokenStore,\n StoreJiraTokensParams,\n} from '#core/tokens.js';\nexport {createJiraTokenStore, jiraSecretsNamespace} from '#core/tokens.js';\nexport type {\n CreateJiraWebhookProcessorOptions,\n JiraWebhookProcessor,\n} from '#core/webhook-processor.js';\nexport {createJiraWebhookProcessor} from '#core/webhook-processor.js';\nexport type {RegisterJiraWebhookParams} from '#core/webhook-registration.js';\nexport {JIRA_WEBHOOK_TTL_MS, registerJiraWebhook} from '#core/webhook-registration.js';\nexport type {\n JiraInstallation,\n JiraInstallationLock,\n JiraInstallationStatus,\n UpdateJiraInstallationWebhookParams,\n UpsertJiraInstallationParams,\n} from '#db/installations.js';\nexport {\n deleteJiraInstallationByConnectionId,\n getJiraInstallationByCloudId,\n getJiraInstallationByConnectionId,\n getJiraInstallationByWebhookId,\n markJiraInstallationRevoked,\n updateJiraInstallationTokenExpiry,\n updateJiraInstallationWebhook,\n upsertJiraInstallation,\n withJiraRefreshLock,\n withJiraWebhookRegistrationLock,\n} from '#db/installations.js';\nexport type {CreateJiraWebhookRoutesOptions} from '#presentation/routes/webhooks.js';\nexport {createJiraWebhookRoutes} from '#presentation/routes/webhooks.js';\nexport {closeDb, config, db, migrationsPath};\n\nexport interface CreateJiraIntegrationProviderOptions {\n jira?: JiraApiClient | undefined;\n getJiraInstallationByConnectionId?: typeof getJiraInstallationByConnectionId | undefined;\n routes?: JiraIntegrationProviderRoutesOptions | undefined;\n}\n\ntype JiraIntegrationProviderRoutesOptions = Omit<\n CreateJiraIntegrationRoutesOptions,\n 'jira' | 'connectionCapabilities' | 'registerJiraWebhook'\n> &\n Omit<CreateJiraWebhookRoutesOptions, 'processor'>;\n\nexport function createJiraIntegrationProvider(options: CreateJiraIntegrationProviderOptions = {}) {\n const jira = options.jira ?? createJiraApiClient();\n const getInstallationByConnectionId =\n options.getJiraInstallationByConnectionId ?? getJiraInstallationByConnectionId;\n const webhookOptions = toJiraWebhookOptions(options.routes);\n const webhookProcessor = webhookOptions ? createJiraWebhookProcessor(webhookOptions) : undefined;\n const webhookRoutes = webhookOptions\n ? [\n createJiraWebhookRoutes({\n coreDb: webhookOptions.coreDb,\n publishIntegrationEventReceived: webhookOptions.publishIntegrationEventReceived,\n recordDeliveryOnly: webhookOptions.recordDeliveryOnly,\n getIntegrationConnectionById: webhookOptions.getIntegrationConnectionById,\n processor: webhookProcessor,\n }),\n ]\n : [];\n const routes = options.routes\n ? [\n createJiraIntegrationRoutes({\n jira,\n connectionCapabilities: [],\n ...options.routes,\n registerJiraWebhook: (input) =>\n registerJiraWebhook({\n jira,\n connectionId: input.connectionId,\n cloudId: input.cloudId,\n accessToken: input.accessToken,\n webhookUrl: jiraWebhookUrl(input.connectionId),\n ...(input.withRegistrationLock\n ? {withRegistrationLock: input.withRegistrationLock}\n : {}),\n ...(input.onRegistrationSuccess\n ? {onRegistrationSuccess: input.onRegistrationSuccess}\n : {}),\n ...(input.onRegistrationFailure\n ? {onRegistrationFailure: input.onRegistrationFailure}\n : {}),\n }).then(() => undefined),\n }),\n ...webhookRoutes,\n ]\n : [];\n return {\n provider: JIRA_PROVIDER,\n displayName: 'Jira',\n adapters: {},\n async connectionExternalUrl(connection: {id: string}): Promise<string | undefined> {\n return (await getInstallationByConnectionId(connection.id))?.siteUrl;\n },\n routes,\n webhookProcessors: webhookProcessor\n ? [{routeIds: ['jira'] as const, processor: webhookProcessor}]\n : undefined,\n };\n}\n\nfunction jiraWebhookUrl(connectionId: string): string {\n return `${config.JIRA_WEBHOOK_BASE_URL.replace(TRAILING_SLASHES_RE, '')}${JIRA_WEBHOOK_ROUTE_PREFIX}/${connectionId}`;\n}\n\nfunction toJiraWebhookOptions(\n routes: CreateJiraIntegrationProviderOptions['routes'],\n): CreateJiraWebhookRoutesOptions | undefined {\n if (!routes) return undefined;\n if (\n routes.coreDb === undefined ||\n routes.publishIntegrationEventReceived === undefined ||\n routes.recordDeliveryOnly === undefined ||\n routes.getIntegrationConnectionById === undefined\n )\n throw new Error(\n 'Jira integration provider requires all webhook receiver dependencies: coreDb, publishIntegrationEventReceived, recordDeliveryOnly, and getIntegrationConnectionById',\n );\n return {\n coreDb: routes.coreDb,\n publishIntegrationEventReceived: routes.publishIntegrationEventReceived,\n recordDeliveryOnly: routes.recordDeliveryOnly,\n getIntegrationConnectionById: routes.getIntegrationConnectionById,\n };\n}\n"],"names":["JIRA_PROVIDER","createJiraApiClient","config","createJiraWebhookProcessor","registerJiraWebhook","closeDb","db","getJiraInstallationByConnectionId","migrationsPath","createJiraIntegrationRoutes","createJiraWebhookRoutes","JIRA_WEBHOOK_ROUTE_PREFIX","TRAILING_SLASHES_RE","JIRA_DYNAMIC_WEBHOOK_EVENTS","JIRA_DYNAMIC_WEBHOOK_JQL","mapJiraError","disconnectJiraInstallation","JiraAccessTokenMissingError","JiraAuthorizationScopeMismatchError","JiraConnectionAlreadyLinkedError","JiraConnectionNotFoundError","JiraInstallationAlreadyLinkedError","JiraInstallationSiteMismatchError","JiraInstallStateActorMismatchError","JiraInstallStateError","JiraIntegrationProviderError","JiraOAuthCallbackError","JiraOfflineAccessNotGrantedError","JiraPendingSelectionNotFoundError","JiraSiteSelectionMismatchError","JiraTokenUnrefreshableError","handleJiraCallback","handleJiraOAuthCallbackError","handleJiraSiteSelection","createJiraPendingSelectionStore","jiraPendingSecretsNamespace","assertJiraAuthorizationScopes","formatJiraOAuthScopes","JIRA_OAUTH_SCOPES","signJiraInstallState","verifyJiraInstallState","createJiraTokenStore","jiraSecretsNamespace","JIRA_WEBHOOK_TTL_MS","deleteJiraInstallationByConnectionId","getJiraInstallationByCloudId","getJiraInstallationByWebhookId","markJiraInstallationRevoked","updateJiraInstallationTokenExpiry","updateJiraInstallationWebhook","upsertJiraInstallation","withJiraRefreshLock","withJiraWebhookRegistrationLock","createJiraIntegrationProvider","options","jira","getInstallationByConnectionId","webhookOptions","toJiraWebhookOptions","routes","webhookProcessor","undefined","webhookRoutes","coreDb","publishIntegrationEventReceived","recordDeliveryOnly","getIntegrationConnectionById","processor","connectionCapabilities","input","connectionId","cloudId","accessToken","webhookUrl","jiraWebhookUrl","withRegistrationLock","onRegistrationSuccess","onRegistrationFailure","then","provider","displayName","adapters","connectionExternalUrl","connection","id","siteUrl","webhookProcessors","routeIds","JIRA_WEBHOOK_BASE_URL","replace","Error"],"mappings":"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"}
@@ -4,6 +4,7 @@ import type { JiraApiClient } from '#api/client.js';
4
4
  import { type ConnectJiraInstallationInput } from '#core/install.js';
5
5
  import type { JiraPendingSelectionStore } from '#core/pending.js';
6
6
  import type { JiraTokenStore } from '#core/tokens.js';
7
+ import type { JiraInstallationLock } from '#db/installations.js';
7
8
  export interface CreateJiraIntegrationRoutesOptions {
8
9
  jira: JiraApiClient;
9
10
  tokenStore: Pick<JiraTokenStore, 'storeTokens'>;
@@ -12,6 +13,27 @@ export interface CreateJiraIntegrationRoutesOptions {
12
13
  cloudId: string;
13
14
  }): Promise<IntegrationConnection<'jira'> | undefined>;
14
15
  connectJiraInstallation(input: ConnectJiraInstallationInput): Promise<IntegrationConnection<'jira'>>;
16
+ registerJiraWebhook(input: {
17
+ connectionId: string;
18
+ cloudId: string;
19
+ accessToken: string;
20
+ withRegistrationLock?: JiraInstallationLock;
21
+ onRegistrationSuccess: (input: {
22
+ tx?: unknown;
23
+ }) => Promise<void>;
24
+ onRegistrationFailure: (input: {
25
+ tx?: unknown;
26
+ }) => Promise<void>;
27
+ }): Promise<void>;
28
+ withJiraInstallationLock?: JiraInstallationLock;
29
+ markConnectionActive(input: {
30
+ connectionId: string;
31
+ tx?: unknown;
32
+ }): Promise<void>;
33
+ markConnectionError(input: {
34
+ connectionId: string;
35
+ tx?: unknown;
36
+ }): Promise<void>;
15
37
  disconnectJiraInstallation(input: {
16
38
  connectionId: string;
17
39
  }): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/install.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAE3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAc,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAElD,OAAO,EACL,KAAK,4BAA4B,EAIlC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAGhE,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAIpD,MAAM,WAAW,kCAAkC;IACjD,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;IAChD,YAAY,EAAE,IAAI,CAAC,yBAAyB,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACzE,yBAAyB,CAAC,KAAK,EAAE;QAC/B,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;IACvD,uBAAuB,CACrB,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,0BAA0B,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,sBAAsB,EAAE,qBAAqB,EAAE,CAAC;IAChD,gCAAgC,CAAC,EAAE,CAAC,KAAK,EAAE;QACzC,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,aAAa,CAAC,OAAO,2BAA2B,EAAE,qBAAqB,CAAC,CAAC;KACvF,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxB;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kCAAkC,GAC1C,UAAU,CA6FZ"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/install.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAE3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAc,KAAK,UAAU,EAAC,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAElD,OAAO,EACL,KAAK,4BAA4B,EAIlC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAGhE,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,sBAAsB,CAAC;AAI/D,MAAM,WAAW,kCAAkC;IACjD,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;IAChD,YAAY,EAAE,IAAI,CAAC,yBAAyB,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACzE,yBAAyB,CAAC,KAAK,EAAE;QAC/B,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;IACvD,uBAAuB,CACrB,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,mBAAmB,CAAC,KAAK,EAAE;QACzB,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;QAC5C,qBAAqB,EAAE,CAAC,KAAK,EAAE;YAAC,EAAE,CAAC,EAAE,OAAO,CAAA;SAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAChE,qBAAqB,EAAE,CAAC,KAAK,EAAE;YAAC,EAAE,CAAC,EAAE,OAAO,CAAA;SAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACjE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,wBAAwB,CAAC,EAAE,oBAAoB,CAAC;IAChD,oBAAoB,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,OAAO,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,mBAAmB,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,OAAO,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChF,0BAA0B,CAAC,KAAK,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,sBAAsB,EAAE,qBAAqB,EAAE,CAAC;IAChD,gCAAgC,CAAC,EAAE,CAAC,KAAK,EAAE;QACzC,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,aAAa,CAAC,OAAO,2BAA2B,EAAE,qBAAqB,CAAC,CAAC;KACvF,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxB;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,kCAAkC,GAC1C,UAAU,CA6FZ"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/presentation/routes/install.ts"],"sourcesContent":["import {AUTH_USER, requireUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';\nimport {\n completeJiraSiteSelectionBodySchema,\n createJiraInstallBodySchema,\n createJiraInstallResponseSchema,\n type JiraCallbackQueryDto,\n jiraCallbackQuerySchema,\n jiraCallbackResponseSchema,\n} from '@shipfox/api-integration-jira-dto';\nimport {\n type IntegrationCapability,\n type IntegrationConnection,\n integrationConnectionDtoSchema,\n} from '@shipfox/api-integration-spi';\nimport {defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {JiraApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {\n type ConnectJiraInstallationInput,\n handleJiraCallback,\n handleJiraOAuthCallbackError,\n handleJiraSiteSelection,\n} from '#core/install.js';\nimport type {JiraPendingSelectionStore} from '#core/pending.js';\nimport {formatJiraOAuthScopes} from '#core/scopes.js';\nimport {signJiraInstallState} from '#core/state.js';\nimport type {JiraTokenStore} from '#core/tokens.js';\nimport {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';\nimport {jiraRouteErrorHandler} from './errors.js';\n\nexport interface CreateJiraIntegrationRoutesOptions {\n jira: JiraApiClient;\n tokenStore: Pick<JiraTokenStore, 'storeTokens'>;\n pendingStore: Pick<JiraPendingSelectionStore, 'save' | 'load' | 'clear'>;\n getExistingJiraConnection(input: {\n cloudId: string;\n }): Promise<IntegrationConnection<'jira'> | undefined>;\n connectJiraInstallation(\n input: ConnectJiraInstallationInput,\n ): Promise<IntegrationConnection<'jira'>>;\n disconnectJiraInstallation(input: {connectionId: string}): Promise<void>;\n connectionCapabilities: IntegrationCapability[];\n requireActiveWorkspaceMembership?: (input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;\n }) => Promise<unknown>;\n}\n\nexport function createJiraIntegrationRoutes(\n options: CreateJiraIntegrationRoutesOptions,\n): RouteGroup {\n const requireMembership =\n options.requireActiveWorkspaceMembership ?? unavailableWorkspaceMembershipCheck;\n const installRoute = defineRoute({\n method: 'POST',\n path: '/install',\n auth: AUTH_USER,\n description: 'Create a Jira OAuth authorization URL for a workspace.',\n schema: {body: createJiraInstallBodySchema, response: {200: createJiraInstallResponseSchema}},\n handler: (request) => {\n const {workspace_id: workspaceId} = request.body;\n const actor = requireUserContext(request);\n requireWorkspaceAccess({request, workspaceId});\n const installUrl = new URL(`${config.JIRA_AUTH_BASE_URL}/authorize`);\n installUrl.searchParams.set('audience', 'api.atlassian.com');\n installUrl.searchParams.set('client_id', config.JIRA_OAUTH_CLIENT_ID);\n installUrl.searchParams.set('scope', formatJiraOAuthScopes());\n installUrl.searchParams.set('redirect_uri', config.JIRA_OAUTH_REDIRECT_URL);\n installUrl.searchParams.set(\n 'state',\n signJiraInstallState({workspaceId, userId: actor.userId}),\n );\n installUrl.searchParams.set('response_type', 'code');\n installUrl.searchParams.set('prompt', 'consent');\n return {install_url: installUrl.toString()};\n },\n });\n const callbackApiRoute = defineRoute({\n method: 'GET',\n path: '/callback/api',\n auth: AUTH_USER,\n description: 'Handle the Jira OAuth callback.',\n schema: {querystring: jiraCallbackQuerySchema, response: {200: jiraCallbackResponseSchema}},\n errorHandler: jiraRouteErrorHandler,\n handler: async (request) => {\n const actor = requireUserContext(request);\n const query = request.query;\n if (isJiraOAuthErrorCallback(query))\n return await handleJiraOAuthCallbackError({\n state: query.state,\n error: query.error,\n errorDescription: query.error_description,\n sessionUserId: actor.userId,\n sessionMemberships: actor.memberships,\n requireWorkspaceMembership: requireMembership,\n });\n const result = await handleJiraCallback({\n ...options,\n code: query.code,\n state: query.state,\n sessionUserId: actor.userId,\n sessionMemberships: actor.memberships,\n requireWorkspaceMembership: requireMembership,\n });\n return 'sites' in result\n ? {\n sites: result.sites.map((site) => ({\n cloud_id: site.cloudId,\n name: site.name,\n url: site.url,\n scopes: site.scopes,\n })),\n }\n : toIntegrationConnectionDto(result, {capabilities: options.connectionCapabilities});\n },\n });\n const siteSelectionRoute = defineRoute({\n method: 'POST',\n path: '/callback/site',\n auth: AUTH_USER,\n description: 'Complete Jira OAuth site selection.',\n schema: {\n body: completeJiraSiteSelectionBodySchema,\n response: {200: integrationConnectionDtoSchema},\n },\n errorHandler: jiraRouteErrorHandler,\n handler: async (request) => {\n const actor = requireUserContext(request);\n const connection = await handleJiraSiteSelection({\n ...options,\n cloudId: request.body.cloud_id,\n state: request.body.state,\n sessionUserId: actor.userId,\n sessionMemberships: actor.memberships,\n requireWorkspaceMembership: requireMembership,\n });\n return toIntegrationConnectionDto(connection, {capabilities: options.connectionCapabilities});\n },\n });\n return {\n prefix: '/integrations/jira',\n routes: [installRoute, callbackApiRoute, siteSelectionRoute],\n };\n}\n\nfunction unavailableWorkspaceMembershipCheck(_input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;\n}): Promise<never> {\n return Promise.reject(new Error('Workspaces inter-module client is not configured'));\n}\n\nfunction isJiraOAuthErrorCallback(query: JiraCallbackQueryDto): query is JiraCallbackQueryDto & {\n error: string;\n error_description?: string | undefined;\n state: string;\n} {\n return 'error' in query && typeof query.error === 'string';\n}\n"],"names":["AUTH_USER","requireUserContext","requireWorkspaceAccess","completeJiraSiteSelectionBodySchema","createJiraInstallBodySchema","createJiraInstallResponseSchema","jiraCallbackQuerySchema","jiraCallbackResponseSchema","integrationConnectionDtoSchema","defineRoute","config","handleJiraCallback","handleJiraOAuthCallbackError","handleJiraSiteSelection","formatJiraOAuthScopes","signJiraInstallState","toIntegrationConnectionDto","jiraRouteErrorHandler","createJiraIntegrationRoutes","options","requireMembership","requireActiveWorkspaceMembership","unavailableWorkspaceMembershipCheck","installRoute","method","path","auth","description","schema","body","response","handler","request","workspace_id","workspaceId","actor","installUrl","URL","JIRA_AUTH_BASE_URL","searchParams","set","JIRA_OAUTH_CLIENT_ID","JIRA_OAUTH_REDIRECT_URL","userId","install_url","toString","callbackApiRoute","querystring","errorHandler","query","isJiraOAuthErrorCallback","state","error","errorDescription","error_description","sessionUserId","sessionMemberships","memberships","requireWorkspaceMembership","result","code","sites","map","site","cloud_id","cloudId","name","url","scopes","capabilities","connectionCapabilities","siteSelectionRoute","connection","prefix","routes","_input","Promise","reject","Error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/presentation/routes/install.ts"],"sourcesContent":["import {AUTH_USER, requireUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';\nimport {\n completeJiraSiteSelectionBodySchema,\n createJiraInstallBodySchema,\n createJiraInstallResponseSchema,\n type JiraCallbackQueryDto,\n jiraCallbackQuerySchema,\n jiraCallbackResponseSchema,\n} from '@shipfox/api-integration-jira-dto';\nimport {\n type IntegrationCapability,\n type IntegrationConnection,\n integrationConnectionDtoSchema,\n} from '@shipfox/api-integration-spi';\nimport {defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport type {JiraApiClient} from '#api/client.js';\nimport {config} from '#config.js';\nimport {\n type ConnectJiraInstallationInput,\n handleJiraCallback,\n handleJiraOAuthCallbackError,\n handleJiraSiteSelection,\n} from '#core/install.js';\nimport type {JiraPendingSelectionStore} from '#core/pending.js';\nimport {formatJiraOAuthScopes} from '#core/scopes.js';\nimport {signJiraInstallState} from '#core/state.js';\nimport type {JiraTokenStore} from '#core/tokens.js';\nimport type {JiraInstallationLock} from '#db/installations.js';\nimport {toIntegrationConnectionDto} from '#presentation/dto/integrations.js';\nimport {jiraRouteErrorHandler} from './errors.js';\n\nexport interface CreateJiraIntegrationRoutesOptions {\n jira: JiraApiClient;\n tokenStore: Pick<JiraTokenStore, 'storeTokens'>;\n pendingStore: Pick<JiraPendingSelectionStore, 'save' | 'load' | 'clear'>;\n getExistingJiraConnection(input: {\n cloudId: string;\n }): Promise<IntegrationConnection<'jira'> | undefined>;\n connectJiraInstallation(\n input: ConnectJiraInstallationInput,\n ): Promise<IntegrationConnection<'jira'>>;\n registerJiraWebhook(input: {\n connectionId: string;\n cloudId: string;\n accessToken: string;\n withRegistrationLock?: JiraInstallationLock;\n onRegistrationSuccess: (input: {tx?: unknown}) => Promise<void>;\n onRegistrationFailure: (input: {tx?: unknown}) => Promise<void>;\n }): Promise<void>;\n withJiraInstallationLock?: JiraInstallationLock;\n markConnectionActive(input: {connectionId: string; tx?: unknown}): Promise<void>;\n markConnectionError(input: {connectionId: string; tx?: unknown}): Promise<void>;\n disconnectJiraInstallation(input: {connectionId: string}): Promise<void>;\n connectionCapabilities: IntegrationCapability[];\n requireActiveWorkspaceMembership?: (input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;\n }) => Promise<unknown>;\n}\n\nexport function createJiraIntegrationRoutes(\n options: CreateJiraIntegrationRoutesOptions,\n): RouteGroup {\n const requireMembership =\n options.requireActiveWorkspaceMembership ?? unavailableWorkspaceMembershipCheck;\n const installRoute = defineRoute({\n method: 'POST',\n path: '/install',\n auth: AUTH_USER,\n description: 'Create a Jira OAuth authorization URL for a workspace.',\n schema: {body: createJiraInstallBodySchema, response: {200: createJiraInstallResponseSchema}},\n handler: (request) => {\n const {workspace_id: workspaceId} = request.body;\n const actor = requireUserContext(request);\n requireWorkspaceAccess({request, workspaceId});\n const installUrl = new URL(`${config.JIRA_AUTH_BASE_URL}/authorize`);\n installUrl.searchParams.set('audience', 'api.atlassian.com');\n installUrl.searchParams.set('client_id', config.JIRA_OAUTH_CLIENT_ID);\n installUrl.searchParams.set('scope', formatJiraOAuthScopes());\n installUrl.searchParams.set('redirect_uri', config.JIRA_OAUTH_REDIRECT_URL);\n installUrl.searchParams.set(\n 'state',\n signJiraInstallState({workspaceId, userId: actor.userId}),\n );\n installUrl.searchParams.set('response_type', 'code');\n installUrl.searchParams.set('prompt', 'consent');\n return {install_url: installUrl.toString()};\n },\n });\n const callbackApiRoute = defineRoute({\n method: 'GET',\n path: '/callback/api',\n auth: AUTH_USER,\n description: 'Handle the Jira OAuth callback.',\n schema: {querystring: jiraCallbackQuerySchema, response: {200: jiraCallbackResponseSchema}},\n errorHandler: jiraRouteErrorHandler,\n handler: async (request) => {\n const actor = requireUserContext(request);\n const query = request.query;\n if (isJiraOAuthErrorCallback(query))\n return await handleJiraOAuthCallbackError({\n state: query.state,\n error: query.error,\n errorDescription: query.error_description,\n sessionUserId: actor.userId,\n sessionMemberships: actor.memberships,\n requireWorkspaceMembership: requireMembership,\n });\n const result = await handleJiraCallback({\n ...options,\n code: query.code,\n state: query.state,\n sessionUserId: actor.userId,\n sessionMemberships: actor.memberships,\n requireWorkspaceMembership: requireMembership,\n });\n return 'sites' in result\n ? {\n sites: result.sites.map((site) => ({\n cloud_id: site.cloudId,\n name: site.name,\n url: site.url,\n scopes: site.scopes,\n })),\n }\n : toIntegrationConnectionDto(result, {capabilities: options.connectionCapabilities});\n },\n });\n const siteSelectionRoute = defineRoute({\n method: 'POST',\n path: '/callback/site',\n auth: AUTH_USER,\n description: 'Complete Jira OAuth site selection.',\n schema: {\n body: completeJiraSiteSelectionBodySchema,\n response: {200: integrationConnectionDtoSchema},\n },\n errorHandler: jiraRouteErrorHandler,\n handler: async (request) => {\n const actor = requireUserContext(request);\n const connection = await handleJiraSiteSelection({\n ...options,\n cloudId: request.body.cloud_id,\n state: request.body.state,\n sessionUserId: actor.userId,\n sessionMemberships: actor.memberships,\n requireWorkspaceMembership: requireMembership,\n });\n return toIntegrationConnectionDto(connection, {capabilities: options.connectionCapabilities});\n },\n });\n return {\n prefix: '/integrations/jira',\n routes: [installRoute, callbackApiRoute, siteSelectionRoute],\n };\n}\n\nfunction unavailableWorkspaceMembershipCheck(_input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<import('@shipfox/api-auth-context').UserContextMembership>;\n}): Promise<never> {\n return Promise.reject(new Error('Workspaces inter-module client is not configured'));\n}\n\nfunction isJiraOAuthErrorCallback(query: JiraCallbackQueryDto): query is JiraCallbackQueryDto & {\n error: string;\n error_description?: string | undefined;\n state: string;\n} {\n return 'error' in query && typeof query.error === 'string';\n}\n"],"names":["AUTH_USER","requireUserContext","requireWorkspaceAccess","completeJiraSiteSelectionBodySchema","createJiraInstallBodySchema","createJiraInstallResponseSchema","jiraCallbackQuerySchema","jiraCallbackResponseSchema","integrationConnectionDtoSchema","defineRoute","config","handleJiraCallback","handleJiraOAuthCallbackError","handleJiraSiteSelection","formatJiraOAuthScopes","signJiraInstallState","toIntegrationConnectionDto","jiraRouteErrorHandler","createJiraIntegrationRoutes","options","requireMembership","requireActiveWorkspaceMembership","unavailableWorkspaceMembershipCheck","installRoute","method","path","auth","description","schema","body","response","handler","request","workspace_id","workspaceId","actor","installUrl","URL","JIRA_AUTH_BASE_URL","searchParams","set","JIRA_OAUTH_CLIENT_ID","JIRA_OAUTH_REDIRECT_URL","userId","install_url","toString","callbackApiRoute","querystring","errorHandler","query","isJiraOAuthErrorCallback","state","error","errorDescription","error_description","sessionUserId","sessionMemberships","memberships","requireWorkspaceMembership","result","code","sites","map","site","cloud_id","cloudId","name","url","scopes","capabilities","connectionCapabilities","siteSelectionRoute","connection","prefix","routes","_input","Promise","reject","Error"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { type RouteGroup } from '@shipfox/node-fastify';
2
+ import { type CreateJiraWebhookProcessorOptions, type JiraWebhookProcessor } from '#core/webhook-processor.js';
3
+ export declare const JIRA_WEBHOOK_ROUTE_PREFIX = "/webhooks/integrations/jira";
4
+ export interface CreateJiraWebhookRoutesOptions extends Omit<CreateJiraWebhookProcessorOptions, 'getJiraInstallationByConnectionId'> {
5
+ processor?: JiraWebhookProcessor | undefined;
6
+ }
7
+ export declare function createJiraWebhookRoutes(options: CreateJiraWebhookRoutesOptions): RouteGroup;
8
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/webhooks.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,KAAK,iCAAiC,EAEtC,KAAK,oBAAoB,EAC1B,MAAM,4BAA4B,CAAC;AAGpC,eAAO,MAAM,yBAAyB,gCAAgC,CAAC;AAEvE,MAAM,WAAW,8BACf,SAAQ,IAAI,CAAC,iCAAiC,EAAE,mCAAmC,CAAC;IACpF,SAAS,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;CAC9C;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,8BAA8B,GAAG,UAAU,CAgC3F"}
@@ -0,0 +1,108 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createStoredWebhookRequest, WEBHOOK_MAX_RAW_BODY_BYTES } from '@shipfox/api-integration-spi';
3
+ import { ClientError, defineRoute, rawBodyPlugin, WEBHOOK_BODY_LIMIT } from '@shipfox/node-fastify';
4
+ import { z } from 'zod';
5
+ import { createJiraWebhookProcessor } from '#core/webhook-processor.js';
6
+ const jiraWebhookParamsSchema = z.object({
7
+ connectionId: z.string().uuid()
8
+ });
9
+ export const JIRA_WEBHOOK_ROUTE_PREFIX = '/webhooks/integrations/jira';
10
+ export function createJiraWebhookRoutes(options) {
11
+ const processor = options.processor ?? createJiraWebhookProcessor(options);
12
+ const route = defineRoute({
13
+ method: 'POST',
14
+ path: '/:connectionId',
15
+ auth: [],
16
+ description: 'Jira dynamic webhook receiver.',
17
+ options: {
18
+ bodyLimit: WEBHOOK_BODY_LIMIT
19
+ },
20
+ schema: {
21
+ params: jiraWebhookParamsSchema
22
+ },
23
+ handler: async (request, reply)=>{
24
+ const body = request.body;
25
+ if (!(body instanceof Uint8Array)) {
26
+ throw new ClientError('Expected raw JSON body', 'invalid-webhook-request', {
27
+ status: 400
28
+ });
29
+ }
30
+ const result = await processor.process(createJiraStoredWebhookRequest({
31
+ body,
32
+ connectionId: request.params.connectionId,
33
+ headers: request.headers,
34
+ rawQueryString: request.raw.url?.split('?')[1] ?? ''
35
+ }));
36
+ return sendJiraWebhookResponse(reply, result);
37
+ }
38
+ });
39
+ return {
40
+ prefix: JIRA_WEBHOOK_ROUTE_PREFIX,
41
+ auth: [],
42
+ plugins: [
43
+ rawBodyPlugin
44
+ ],
45
+ routes: [
46
+ route
47
+ ]
48
+ };
49
+ }
50
+ function createJiraStoredWebhookRequest(input) {
51
+ if (input.body.byteLength > WEBHOOK_MAX_RAW_BODY_BYTES) {
52
+ throw new ClientError('Webhook request body is too large', 'body-too-large', {
53
+ status: 413
54
+ });
55
+ }
56
+ try {
57
+ return createStoredWebhookRequest({
58
+ requestId: randomUUID(),
59
+ routeId: 'jira',
60
+ receivedAt: new Date().toISOString(),
61
+ rawQueryString: input.rawQueryString,
62
+ headers: jiraWebhookHeaders(input.headers),
63
+ body: input.body,
64
+ connectionId: input.connectionId
65
+ });
66
+ } catch (error) {
67
+ throw new ClientError('Webhook request metadata is invalid', 'invalid-webhook-request', {
68
+ cause: error
69
+ });
70
+ }
71
+ }
72
+ function jiraWebhookHeaders(headers) {
73
+ return Object.fromEntries([
74
+ 'authorization',
75
+ 'content-type',
76
+ 'x-atlassian-webhook-identifier'
77
+ ].flatMap((name)=>{
78
+ const value = headers[name];
79
+ return typeof value === 'string' ? [
80
+ [
81
+ name,
82
+ value
83
+ ]
84
+ ] : [];
85
+ }));
86
+ }
87
+ function sendJiraWebhookResponse(reply, result) {
88
+ if (result.outcome !== 'discarded') {
89
+ reply.code(200);
90
+ return null;
91
+ }
92
+ if (result.reason === 'invalid_signature' || result.reason === 'missing_required_input') {
93
+ reply.code(401);
94
+ return {
95
+ error: 'invalid authorization'
96
+ };
97
+ }
98
+ if (result.reason === 'malformed_payload') {
99
+ reply.code(400);
100
+ return {
101
+ error: 'malformed JSON'
102
+ };
103
+ }
104
+ reply.code(200);
105
+ return null;
106
+ }
107
+
108
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/presentation/routes/webhooks.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport type {StoredWebhookRequest, WebhookProcessingResult} from '@shipfox/api-integration-spi';\nimport {createStoredWebhookRequest, WEBHOOK_MAX_RAW_BODY_BYTES} from '@shipfox/api-integration-spi';\nimport {\n ClientError,\n defineRoute,\n type RouteGroup,\n rawBodyPlugin,\n WEBHOOK_BODY_LIMIT,\n} from '@shipfox/node-fastify';\nimport {z} from 'zod';\nimport {\n type CreateJiraWebhookProcessorOptions,\n createJiraWebhookProcessor,\n type JiraWebhookProcessor,\n} from '#core/webhook-processor.js';\n\nconst jiraWebhookParamsSchema = z.object({connectionId: z.string().uuid()});\nexport const JIRA_WEBHOOK_ROUTE_PREFIX = '/webhooks/integrations/jira';\n\nexport interface CreateJiraWebhookRoutesOptions\n extends Omit<CreateJiraWebhookProcessorOptions, 'getJiraInstallationByConnectionId'> {\n processor?: JiraWebhookProcessor | undefined;\n}\n\nexport function createJiraWebhookRoutes(options: CreateJiraWebhookRoutesOptions): RouteGroup {\n const processor = options.processor ?? createJiraWebhookProcessor(options);\n const route = defineRoute({\n method: 'POST',\n path: '/:connectionId',\n auth: [],\n description: 'Jira dynamic webhook receiver.',\n options: {bodyLimit: WEBHOOK_BODY_LIMIT},\n schema: {params: jiraWebhookParamsSchema},\n handler: async (request, reply) => {\n const body = request.body;\n if (!(body instanceof Uint8Array)) {\n throw new ClientError('Expected raw JSON body', 'invalid-webhook-request', {status: 400});\n }\n const result = await processor.process(\n createJiraStoredWebhookRequest({\n body,\n connectionId: request.params.connectionId,\n headers: request.headers,\n rawQueryString: request.raw.url?.split('?')[1] ?? '',\n }),\n );\n return sendJiraWebhookResponse(reply, result);\n },\n });\n\n return {\n prefix: JIRA_WEBHOOK_ROUTE_PREFIX,\n auth: [],\n plugins: [rawBodyPlugin],\n routes: [route],\n };\n}\n\nfunction createJiraStoredWebhookRequest(input: {\n body: Uint8Array;\n connectionId: string;\n headers: Record<string, string | string[] | undefined>;\n rawQueryString: string;\n}): StoredWebhookRequest {\n if (input.body.byteLength > WEBHOOK_MAX_RAW_BODY_BYTES) {\n throw new ClientError('Webhook request body is too large', 'body-too-large', {status: 413});\n }\n try {\n return createStoredWebhookRequest({\n requestId: randomUUID(),\n routeId: 'jira',\n receivedAt: new Date().toISOString(),\n rawQueryString: input.rawQueryString,\n headers: jiraWebhookHeaders(input.headers),\n body: input.body,\n connectionId: input.connectionId,\n });\n } catch (error) {\n throw new ClientError('Webhook request metadata is invalid', 'invalid-webhook-request', {\n cause: error,\n });\n }\n}\n\nfunction jiraWebhookHeaders(\n headers: Record<string, string | string[] | undefined>,\n): Record<string, string> {\n return Object.fromEntries(\n ['authorization', 'content-type', 'x-atlassian-webhook-identifier'].flatMap((name) => {\n const value = headers[name];\n return typeof value === 'string' ? [[name, value]] : [];\n }),\n );\n}\n\nfunction sendJiraWebhookResponse(\n reply: {code(statusCode: number): void},\n result: WebhookProcessingResult,\n) {\n if (result.outcome !== 'discarded') {\n reply.code(200);\n return null;\n }\n if (result.reason === 'invalid_signature' || result.reason === 'missing_required_input') {\n reply.code(401);\n return {error: 'invalid authorization'};\n }\n if (result.reason === 'malformed_payload') {\n reply.code(400);\n return {error: 'malformed JSON'};\n }\n reply.code(200);\n return null;\n}\n"],"names":["randomUUID","createStoredWebhookRequest","WEBHOOK_MAX_RAW_BODY_BYTES","ClientError","defineRoute","rawBodyPlugin","WEBHOOK_BODY_LIMIT","z","createJiraWebhookProcessor","jiraWebhookParamsSchema","object","connectionId","string","uuid","JIRA_WEBHOOK_ROUTE_PREFIX","createJiraWebhookRoutes","options","processor","route","method","path","auth","description","bodyLimit","schema","params","handler","request","reply","body","Uint8Array","status","result","process","createJiraStoredWebhookRequest","headers","rawQueryString","raw","url","split","sendJiraWebhookResponse","prefix","plugins","routes","input","byteLength","requestId","routeId","receivedAt","Date","toISOString","jiraWebhookHeaders","error","cause","Object","fromEntries","flatMap","name","value","outcome","code","reason"],"mappings":"AAAA,SAAQA,UAAU,QAAO,cAAc;AAEvC,SAAQC,0BAA0B,EAAEC,0BAA0B,QAAO,+BAA+B;AACpG,SACEC,WAAW,EACXC,WAAW,EAEXC,aAAa,EACbC,kBAAkB,QACb,wBAAwB;AAC/B,SAAQC,CAAC,QAAO,MAAM;AACtB,SAEEC,0BAA0B,QAErB,6BAA6B;AAEpC,MAAMC,0BAA0BF,EAAEG,MAAM,CAAC;IAACC,cAAcJ,EAAEK,MAAM,GAAGC,IAAI;AAAE;AACzE,OAAO,MAAMC,4BAA4B,8BAA8B;AAOvE,OAAO,SAASC,wBAAwBC,OAAuC;IAC7E,MAAMC,YAAYD,QAAQC,SAAS,IAAIT,2BAA2BQ;IAClE,MAAME,QAAQd,YAAY;QACxBe,QAAQ;QACRC,MAAM;QACNC,MAAM,EAAE;QACRC,aAAa;QACbN,SAAS;YAACO,WAAWjB;QAAkB;QACvCkB,QAAQ;YAACC,QAAQhB;QAAuB;QACxCiB,SAAS,OAAOC,SAASC;YACvB,MAAMC,OAAOF,QAAQE,IAAI;YACzB,IAAI,CAAEA,CAAAA,gBAAgBC,UAAS,GAAI;gBACjC,MAAM,IAAI3B,YAAY,0BAA0B,2BAA2B;oBAAC4B,QAAQ;gBAAG;YACzF;YACA,MAAMC,SAAS,MAAMf,UAAUgB,OAAO,CACpCC,+BAA+B;gBAC7BL;gBACAlB,cAAcgB,QAAQF,MAAM,CAACd,YAAY;gBACzCwB,SAASR,QAAQQ,OAAO;gBACxBC,gBAAgBT,QAAQU,GAAG,CAACC,GAAG,EAAEC,MAAM,IAAI,CAAC,EAAE,IAAI;YACpD;YAEF,OAAOC,wBAAwBZ,OAAOI;QACxC;IACF;IAEA,OAAO;QACLS,QAAQ3B;QACRO,MAAM,EAAE;QACRqB,SAAS;YAACrC;SAAc;QACxBsC,QAAQ;YAACzB;SAAM;IACjB;AACF;AAEA,SAASgB,+BAA+BU,KAKvC;IACC,IAAIA,MAAMf,IAAI,CAACgB,UAAU,GAAG3C,4BAA4B;QACtD,MAAM,IAAIC,YAAY,qCAAqC,kBAAkB;YAAC4B,QAAQ;QAAG;IAC3F;IACA,IAAI;QACF,OAAO9B,2BAA2B;YAChC6C,WAAW9C;YACX+C,SAAS;YACTC,YAAY,IAAIC,OAAOC,WAAW;YAClCd,gBAAgBQ,MAAMR,cAAc;YACpCD,SAASgB,mBAAmBP,MAAMT,OAAO;YACzCN,MAAMe,MAAMf,IAAI;YAChBlB,cAAciC,MAAMjC,YAAY;QAClC;IACF,EAAE,OAAOyC,OAAO;QACd,MAAM,IAAIjD,YAAY,uCAAuC,2BAA2B;YACtFkD,OAAOD;QACT;IACF;AACF;AAEA,SAASD,mBACPhB,OAAsD;IAEtD,OAAOmB,OAAOC,WAAW,CACvB;QAAC;QAAiB;QAAgB;KAAiC,CAACC,OAAO,CAAC,CAACC;QAC3E,MAAMC,QAAQvB,OAAO,CAACsB,KAAK;QAC3B,OAAO,OAAOC,UAAU,WAAW;YAAC;gBAACD;gBAAMC;aAAM;SAAC,GAAG,EAAE;IACzD;AAEJ;AAEA,SAASlB,wBACPZ,KAAuC,EACvCI,MAA+B;IAE/B,IAAIA,OAAO2B,OAAO,KAAK,aAAa;QAClC/B,MAAMgC,IAAI,CAAC;QACX,OAAO;IACT;IACA,IAAI5B,OAAO6B,MAAM,KAAK,uBAAuB7B,OAAO6B,MAAM,KAAK,0BAA0B;QACvFjC,MAAMgC,IAAI,CAAC;QACX,OAAO;YAACR,OAAO;QAAuB;IACxC;IACA,IAAIpB,OAAO6B,MAAM,KAAK,qBAAqB;QACzCjC,MAAMgC,IAAI,CAAC;QACX,OAAO;YAACR,OAAO;QAAgB;IACjC;IACAxB,MAAMgC,IAAI,CAAC;IACX,OAAO;AACT"}