@shipfox/api-integration-jira 11.0.0 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.turbo/turbo-build.log +17 -2
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +58 -2
  4. package/dist/api/client.d.ts +34 -0
  5. package/dist/api/client.d.ts.map +1 -1
  6. package/dist/api/client.js +131 -0
  7. package/dist/api/client.js.map +1 -1
  8. package/dist/config.d.ts +0 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +1 -4
  11. package/dist/config.js.map +1 -1
  12. package/dist/core/agent-tools-provider.d.ts +26 -0
  13. package/dist/core/agent-tools-provider.d.ts.map +1 -0
  14. package/dist/core/agent-tools-provider.js +130 -0
  15. package/dist/core/agent-tools-provider.js.map +1 -0
  16. package/dist/core/agent-tools.d.ts +16 -0
  17. package/dist/core/agent-tools.d.ts.map +1 -0
  18. package/dist/core/agent-tools.js +441 -0
  19. package/dist/core/agent-tools.js.map +1 -0
  20. package/dist/core/install.d.ts +22 -0
  21. package/dist/core/install.d.ts.map +1 -1
  22. package/dist/core/install.js +68 -20
  23. package/dist/core/install.js.map +1 -1
  24. package/dist/core/tokens.d.ts +2 -0
  25. package/dist/core/tokens.d.ts.map +1 -1
  26. package/dist/core/tokens.js +129 -37
  27. package/dist/core/tokens.js.map +1 -1
  28. package/dist/core/webhook-processor.d.ts +15 -0
  29. package/dist/core/webhook-processor.d.ts.map +1 -0
  30. package/dist/core/webhook-processor.js +137 -0
  31. package/dist/core/webhook-processor.js.map +1 -0
  32. package/dist/core/webhook-registration.d.ts +26 -0
  33. package/dist/core/webhook-registration.d.ts.map +1 -0
  34. package/dist/core/webhook-registration.js +165 -0
  35. package/dist/core/webhook-registration.js.map +1 -0
  36. package/dist/core/webhook.d.ts +18 -0
  37. package/dist/core/webhook.d.ts.map +1 -0
  38. package/dist/core/webhook.js +38 -0
  39. package/dist/core/webhook.js.map +1 -0
  40. package/dist/db/db.d.ts +68 -0
  41. package/dist/db/db.d.ts.map +1 -1
  42. package/dist/db/installations.d.ts +33 -6
  43. package/dist/db/installations.d.ts.map +1 -1
  44. package/dist/db/installations.js +79 -7
  45. package/dist/db/installations.js.map +1 -1
  46. package/dist/db/schema/installations.d.ts +34 -0
  47. package/dist/db/schema/installations.d.ts.map +1 -1
  48. package/dist/db/schema/installations.js +12 -2
  49. package/dist/db/schema/installations.js.map +1 -1
  50. package/dist/index.d.ts +34 -7
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +76 -8
  53. package/dist/index.js.map +1 -1
  54. package/dist/presentation/routes/install.d.ts +22 -0
  55. package/dist/presentation/routes/install.d.ts.map +1 -1
  56. package/dist/presentation/routes/install.js.map +1 -1
  57. package/dist/presentation/routes/webhooks.d.ts +8 -0
  58. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  59. package/dist/presentation/routes/webhooks.js +108 -0
  60. package/dist/presentation/routes/webhooks.js.map +1 -0
  61. package/dist/temporal/activities/index.d.ts +2 -0
  62. package/dist/temporal/activities/index.d.ts.map +1 -0
  63. package/dist/temporal/activities/index.js +3 -0
  64. package/dist/temporal/activities/index.js.map +1 -0
  65. package/dist/temporal/activities/refresh-jira-tokens.d.ts +20 -0
  66. package/dist/temporal/activities/refresh-jira-tokens.d.ts.map +1 -0
  67. package/dist/temporal/activities/refresh-jira-tokens.js +58 -0
  68. package/dist/temporal/activities/refresh-jira-tokens.js.map +1 -0
  69. package/dist/temporal/constants.d.ts +6 -0
  70. package/dist/temporal/constants.d.ts.map +1 -0
  71. package/dist/temporal/constants.js +8 -0
  72. package/dist/temporal/constants.js.map +1 -0
  73. package/dist/temporal/worker.d.ts +9 -0
  74. package/dist/temporal/worker.d.ts.map +1 -0
  75. package/dist/temporal/worker.js +26 -0
  76. package/dist/temporal/worker.js.map +1 -0
  77. package/dist/temporal/workflows/index.bundle.js +25225 -0
  78. package/dist/temporal/workflows/index.bundle.meta.json +1 -0
  79. package/dist/temporal/workflows/index.d.ts +2 -0
  80. package/dist/temporal/workflows/index.d.ts.map +1 -0
  81. package/dist/temporal/workflows/index.js +3 -0
  82. package/dist/temporal/workflows/index.js.map +1 -0
  83. package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts +2 -0
  84. package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts.map +1 -0
  85. package/dist/temporal/workflows/refresh-jira-tokens-cron.js +20 -0
  86. package/dist/temporal/workflows/refresh-jira-tokens-cron.js.map +1 -0
  87. package/dist/tsconfig.test.tsbuildinfo +1 -1
  88. package/drizzle/0001_jira-token-refresh-state.sql +12 -0
  89. package/drizzle/meta/0001_snapshot.json +245 -0
  90. package/drizzle/meta/_journal.json +7 -0
  91. package/package.json +17 -8
  92. package/src/api/client.test.ts +236 -9
  93. package/src/api/client.ts +192 -0
  94. package/src/config.test.ts +0 -1
  95. package/src/config.ts +1 -4
  96. package/src/core/agent-tools-provider.test.ts +299 -0
  97. package/src/core/agent-tools-provider.ts +175 -0
  98. package/src/core/agent-tools.ts +450 -0
  99. package/src/core/install.test.ts +179 -0
  100. package/src/core/install.ts +85 -19
  101. package/src/core/tokens-refresh.test.ts +223 -4
  102. package/src/core/tokens.test.ts +1 -1
  103. package/src/core/tokens.ts +153 -33
  104. package/src/core/webhook-processor.test.ts +418 -0
  105. package/src/core/webhook-processor.ts +166 -0
  106. package/src/core/webhook-registration.test.ts +375 -0
  107. package/src/core/webhook-registration.ts +216 -0
  108. package/src/core/webhook.ts +69 -0
  109. package/src/db/installations.test.ts +148 -2
  110. package/src/db/installations.ts +133 -7
  111. package/src/db/schema/installations.ts +21 -1
  112. package/src/index.test.ts +42 -0
  113. package/src/index.ts +148 -5
  114. package/src/presentation/routes/install.ts +12 -0
  115. package/src/presentation/routes/webhooks.test.ts +132 -0
  116. package/src/presentation/routes/webhooks.ts +115 -0
  117. package/src/temporal/activities/index.ts +5 -0
  118. package/src/temporal/activities/refresh-jira-tokens.test.ts +156 -0
  119. package/src/temporal/activities/refresh-jira-tokens.ts +88 -0
  120. package/src/temporal/constants.ts +8 -0
  121. package/src/temporal/worker.ts +39 -0
  122. package/src/temporal/workflows/index.ts +1 -0
  123. package/src/temporal/workflows/refresh-jira-tokens-cron.ts +19 -0
  124. package/test/env.ts +0 -1
  125. package/test/factories/jira-installation.ts +4 -0
  126. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/agent-tools.ts"],"sourcesContent":["import type {\n JiraAgentToolId,\n JiraAgentToolRequiredScope,\n JiraAgentToolCatalogEntry as JiraDtoAgentToolCatalogEntry,\n} from '@shipfox/api-integration-jira-dto';\nimport type {\n AgentToolCatalogEntry,\n AgentToolJsonSchema,\n AgentToolSelectionCatalog,\n AgentToolSelector,\n} from '@shipfox/api-integration-spi';\nimport type {JiraAgentToolHttpMethod, JiraAgentToolQueryValue} from '#api/client.js';\n\nexport type JiraAgentToolCatalogEntry = JiraDtoAgentToolCatalogEntry;\nexport type {JiraAgentToolId, JiraAgentToolRequiredScope};\n\ninterface JiraAgentToolCatalogInput {\n id: JiraAgentToolId;\n description: string;\n sensitivity: 'read' | 'write';\n sensitive: boolean;\n requiredScope: JiraAgentToolRequiredScope;\n inputSchema: AgentToolJsonSchema;\n}\n\nconst issueIdOrKeySchema = stringSchema('Jira issue ID or key, such as ENG-123');\nconst projectIdOrKeySchema = stringSchema('Jira project ID or key, such as ENG');\nconst fieldsSchema = recordSchema('Jira issue fields keyed by their REST API field name');\nconst updateSchema = recordSchema('Jira issue field update operations');\nconst fieldNamesSchema = arraySchema(stringSchema('Jira field name'));\nconst propertiesSchema = arraySchema(stringSchema('Jira issue property key'));\n\nexport const jiraAgentToolCatalog = [\n tool({\n id: 'get_issue',\n description: 'Retrieve a Jira issue by its ID or key.',\n sensitivity: 'read',\n sensitive: false,\n requiredScope: 'read',\n inputSchema: objectSchema(\n {\n idOrKey: issueIdOrKeySchema,\n fields: fieldNamesSchema,\n fieldsByKeys: booleanSchema('Interpret fields by their keys instead of IDs'),\n expand: stringSchema('Comma-separated Jira issue expansions'),\n properties: propertiesSchema,\n updateHistory: booleanSchema('Record this issue as viewed by the authorizing user'),\n },\n ['idOrKey'],\n ),\n }),\n tool({\n id: 'search_issues',\n description: 'Search Jira issues with a JQL query.',\n sensitivity: 'read',\n sensitive: false,\n requiredScope: 'read',\n inputSchema: objectSchema(\n {\n jql: stringSchema('Jira Query Language expression'),\n maxResults: integerSchema('Maximum number of issues to return'),\n nextPageToken: stringSchema('Pagination token from a previous search'),\n fields: fieldNamesSchema,\n fieldsByKeys: booleanSchema('Interpret fields by their keys instead of IDs'),\n expand: stringSchema('Comma-separated Jira issue expansions'),\n properties: propertiesSchema,\n reconcileIssues: arraySchema(integerSchema('Issue ID to reconcile for read-after-write')),\n },\n ['jql'],\n ),\n }),\n tool({\n id: 'get_issue_comments',\n description: 'List the comments on a Jira issue.',\n sensitivity: 'read',\n sensitive: false,\n requiredScope: 'read',\n inputSchema: objectSchema(\n {\n idOrKey: issueIdOrKeySchema,\n startAt: integerSchema('Zero-based comment offset'),\n maxResults: integerSchema('Maximum number of comments to return'),\n orderBy: stringSchema('Comment ordering expression'),\n expand: stringSchema('Comma-separated Jira comment expansions'),\n },\n ['idOrKey'],\n ),\n }),\n tool({\n id: 'get_issue_transitions',\n description: 'List the workflow transitions available for a Jira issue.',\n sensitivity: 'read',\n sensitive: false,\n requiredScope: 'read',\n inputSchema: objectSchema(\n {\n idOrKey: issueIdOrKeySchema,\n expand: stringSchema('Comma-separated transition expansions, such as transitions.fields'),\n },\n ['idOrKey'],\n ),\n }),\n tool({\n id: 'get_project',\n description: 'Retrieve a Jira project by its ID or key.',\n sensitivity: 'read',\n sensitive: false,\n requiredScope: 'read',\n inputSchema: objectSchema(\n {\n idOrKey: projectIdOrKeySchema,\n expand: stringSchema('Comma-separated Jira project expansions'),\n properties: propertiesSchema,\n },\n ['idOrKey'],\n ),\n }),\n tool({\n id: 'get_user',\n description: 'Retrieve a Jira user by Atlassian account ID.',\n sensitivity: 'read',\n sensitive: false,\n requiredScope: 'read',\n inputSchema: objectSchema(\n {\n accountId: stringSchema('Atlassian account ID'),\n expand: stringSchema('Comma-separated Jira user expansions'),\n },\n ['accountId'],\n ),\n }),\n tool({\n id: 'create_issue',\n description:\n 'Create a Jira issue using Jira REST issue fields. If both a project key and project ID or both an issue type name and issue type ID are supplied, the ID takes precedence.',\n sensitivity: 'write',\n sensitive: false,\n requiredScope: 'write',\n inputSchema: objectSchema(\n {\n projectKey: stringSchema(\n 'Jira project key; projectId takes precedence if both are supplied',\n ),\n projectId: stringSchema('Jira project ID; takes precedence over projectKey'),\n summary: stringSchema('Issue summary'),\n issueType: stringSchema(\n 'Issue type name; issueTypeId takes precedence if both are supplied',\n ),\n issueTypeId: stringSchema('Issue type ID; takes precedence over issueType'),\n assigneeAccountId: stringSchema('Atlassian account ID to assign'),\n priority: stringSchema('Jira priority name'),\n labels: arraySchema(stringSchema('Jira label')),\n body: stringSchema('Plain-text issue description'),\n fields: fieldsSchema,\n update: updateSchema,\n },\n ['summary'],\n ),\n }),\n tool({\n id: 'update_issue',\n description: 'Update a Jira issue using Jira REST issue fields.',\n sensitivity: 'write',\n sensitive: false,\n requiredScope: 'write',\n inputSchema: objectSchema(\n {\n idOrKey: issueIdOrKeySchema,\n summary: stringSchema('Replacement issue summary'),\n issueType: stringSchema(\n 'Replacement issue type name; issueTypeId takes precedence if both are supplied',\n ),\n issueTypeId: stringSchema('Replacement issue type ID; takes precedence over issueType'),\n assigneeAccountId: stringSchema('Atlassian account ID to assign'),\n priority: stringSchema('Replacement Jira priority name'),\n labels: arraySchema(stringSchema('Replacement Jira labels')),\n body: stringSchema('Plain-text replacement issue description'),\n fields: fieldsSchema,\n update: updateSchema,\n },\n ['idOrKey'],\n ),\n }),\n tool({\n id: 'add_comment',\n description: 'Add a plain-text comment to a Jira issue.',\n sensitivity: 'write',\n sensitive: false,\n requiredScope: 'write',\n inputSchema: objectSchema(\n {\n idOrKey: issueIdOrKeySchema,\n body: stringSchema('Plain-text comment body'),\n },\n ['idOrKey', 'body'],\n ),\n }),\n tool({\n id: 'transition_issue',\n description: 'Move a Jira issue through a workflow transition.',\n sensitivity: 'write',\n sensitive: false,\n requiredScope: 'write',\n inputSchema: objectSchema(\n {\n idOrKey: issueIdOrKeySchema,\n transitionId: stringSchema('Jira workflow transition ID'),\n fields: fieldsSchema,\n update: updateSchema,\n },\n ['idOrKey', 'transitionId'],\n ),\n }),\n tool({\n id: 'assign_issue',\n description: 'Assign or unassign a Jira issue by Atlassian account ID.',\n sensitivity: 'write',\n sensitive: false,\n requiredScope: 'write',\n inputSchema: objectSchema(\n {\n idOrKey: issueIdOrKeySchema,\n accountId: nullableStringSchema('Atlassian account ID, or null to unassign'),\n },\n ['idOrKey', 'accountId'],\n ),\n }),\n] as const satisfies readonly JiraAgentToolCatalogEntry[];\n\nexport const jiraAgentToolSelectionCatalog =\n buildJiraAgentToolSelectionCatalog(jiraAgentToolCatalog);\n\nexport interface JiraToolOperation {\n method: JiraAgentToolHttpMethod;\n path: (args: Record<string, unknown>) => string;\n query?: ((args: Record<string, unknown>) => Record<string, JiraAgentToolQueryValue>) | undefined;\n body?: ((args: Record<string, unknown>) => unknown) | undefined;\n}\n\nexport const JIRA_TOOL_OPERATIONS: Record<JiraAgentToolId, JiraToolOperation> = {\n get_issue: {\n method: 'GET',\n path: (args) => issuePath(args),\n query: (args) =>\n definedArguments(args, ['fields', 'fieldsByKeys', 'expand', 'properties', 'updateHistory']),\n },\n search_issues: {\n method: 'POST',\n path: () => '/search/jql',\n body: (args) =>\n definedArguments(args, [\n 'jql',\n 'maxResults',\n 'nextPageToken',\n 'fields',\n 'fieldsByKeys',\n 'expand',\n 'properties',\n 'reconcileIssues',\n ]),\n },\n get_issue_comments: {\n method: 'GET',\n path: (args) => issuePath(args, 'comment'),\n query: (args) => definedArguments(args, ['startAt', 'maxResults', 'orderBy', 'expand']),\n },\n get_issue_transitions: {\n method: 'GET',\n path: (args) => issuePath(args, 'transitions'),\n query: (args) => definedArguments(args, ['expand']),\n },\n get_project: {\n method: 'GET',\n path: (args) => `/project/${encodeURIComponent(stringArgument(args, 'idOrKey'))}`,\n query: (args) => definedArguments(args, ['expand', 'properties']),\n },\n get_user: {\n method: 'GET',\n path: () => '/user',\n query: (args) => definedArguments(args, ['accountId', 'expand']),\n },\n create_issue: {\n method: 'POST',\n path: () => '/issue',\n body: (args) => issueWriteBody(args),\n },\n update_issue: {\n method: 'PUT',\n path: (args) => issuePath(args),\n body: (args) => issueWriteBody(args),\n },\n add_comment: {\n method: 'POST',\n path: (args) => issuePath(args, 'comment'),\n body: (args) => ({body: jiraPlainTextToAdf(stringArgument(args, 'body'))}),\n },\n transition_issue: {\n method: 'POST',\n path: (args) => issuePath(args, 'transitions'),\n body: (args) => transitionBody(args),\n },\n assign_issue: {\n method: 'PUT',\n path: (args) => issuePath(args, 'assignee'),\n body: (args) => ({accountId: args.accountId}),\n },\n};\n\nexport function jiraPlainTextToAdf(body: string): Record<string, unknown> {\n return {\n type: 'doc',\n version: 1,\n content: [\n {\n type: 'paragraph',\n content: [{type: 'text', text: body}],\n },\n ],\n };\n}\n\nfunction buildJiraAgentToolSelectionCatalog(\n catalog: readonly AgentToolCatalogEntry<JiraAgentToolRequiredScope>[],\n): AgentToolSelectionCatalog {\n return {\n selectors: catalog.map(\n (entry): AgentToolSelector => ({\n token: entry.id,\n kind: 'standalone',\n sensitivity: entry.sensitivity,\n sensitive: entry.sensitive,\n }),\n ),\n };\n}\n\nfunction issuePath(args: Record<string, unknown>, suffix = ''): string {\n return `/issue/${encodeURIComponent(stringArgument(args, 'idOrKey'))}${suffix ? `/${suffix}` : ''}`;\n}\n\nfunction issueWriteBody(args: Record<string, unknown>): Record<string, unknown> {\n const body: Record<string, unknown> = {};\n const fields = issueFields(args);\n if (Object.keys(fields).length > 0) body.fields = fields;\n if (isRecord(args.update)) body.update = args.update;\n return body;\n}\n\nfunction issueFields(args: Record<string, unknown>): Record<string, unknown> {\n const fields = isRecord(args.fields) ? {...args.fields} : {};\n const projectKey = stringArgumentOrUndefined(args, 'projectKey');\n const projectId = stringArgumentOrUndefined(args, 'projectId');\n const summary = stringArgumentOrUndefined(args, 'summary');\n const issueType = stringArgumentOrUndefined(args, 'issueType');\n const issueTypeId = stringArgumentOrUndefined(args, 'issueTypeId');\n const assigneeAccountId = args.assigneeAccountId;\n const priority = stringArgumentOrUndefined(args, 'priority');\n\n if (projectKey !== undefined) fields.project = {key: projectKey};\n if (projectId !== undefined) fields.project = {id: projectId};\n if (summary !== undefined) fields.summary = summary;\n if (issueType !== undefined) fields.issuetype = {name: issueType};\n if (issueTypeId !== undefined) fields.issuetype = {id: issueTypeId};\n if (typeof assigneeAccountId === 'string') fields.assignee = {accountId: assigneeAccountId};\n if (priority !== undefined) fields.priority = {name: priority};\n if (Array.isArray(args.labels)) fields.labels = args.labels;\n if (typeof args.body === 'string') fields.description = jiraPlainTextToAdf(args.body);\n\n return fields;\n}\n\nfunction transitionBody(args: Record<string, unknown>): Record<string, unknown> {\n const body: Record<string, unknown> = {\n transition: {id: stringArgument(args, 'transitionId')},\n };\n const fields = isRecord(args.fields) ? args.fields : undefined;\n if (fields !== undefined) body.fields = fields;\n if (isRecord(args.update)) body.update = args.update;\n return body;\n}\n\nfunction definedArguments(\n args: Record<string, unknown>,\n names: readonly string[],\n): Record<string, JiraAgentToolQueryValue> {\n return Object.fromEntries(\n names.map((name) => [name, args[name]] as const).filter(([, value]) => value !== undefined),\n ) as Record<string, JiraAgentToolQueryValue>;\n}\n\nfunction stringArgument(args: Record<string, unknown>, name: string): string {\n const value = args[name];\n return typeof value === 'string' ? value : String(value ?? '');\n}\n\nfunction stringArgumentOrUndefined(\n args: Record<string, unknown>,\n name: string,\n): string | undefined {\n const value = args[name];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction tool(input: JiraAgentToolCatalogInput): JiraAgentToolCatalogEntry {\n return input;\n}\n\nfunction objectSchema(\n properties: Record<string, AgentToolJsonSchema>,\n required: string[] = [],\n): AgentToolJsonSchema {\n return {\n type: 'object',\n additionalProperties: false,\n properties,\n ...(required.length > 0 ? {required} : {}),\n };\n}\n\nfunction recordSchema(description?: string): AgentToolJsonSchema {\n return {\n type: 'object',\n additionalProperties: true,\n ...(description === undefined ? {} : {description}),\n };\n}\n\nfunction stringSchema(description?: string): AgentToolJsonSchema {\n return {type: 'string', ...(description === undefined ? {} : {description})};\n}\n\nfunction nullableStringSchema(description?: string): AgentToolJsonSchema {\n return {type: ['string', 'null'], ...(description === undefined ? {} : {description})};\n}\n\nfunction integerSchema(description?: string): AgentToolJsonSchema {\n return {type: 'integer', ...(description === undefined ? {} : {description})};\n}\n\nfunction booleanSchema(description?: string): AgentToolJsonSchema {\n return {type: 'boolean', ...(description === undefined ? {} : {description})};\n}\n\nfunction arraySchema(items: AgentToolJsonSchema): AgentToolJsonSchema {\n return {type: 'array', items};\n}\n"],"names":["issueIdOrKeySchema","stringSchema","projectIdOrKeySchema","fieldsSchema","recordSchema","updateSchema","fieldNamesSchema","arraySchema","propertiesSchema","jiraAgentToolCatalog","tool","id","description","sensitivity","sensitive","requiredScope","inputSchema","objectSchema","idOrKey","fields","fieldsByKeys","booleanSchema","expand","properties","updateHistory","jql","maxResults","integerSchema","nextPageToken","reconcileIssues","startAt","orderBy","accountId","projectKey","projectId","summary","issueType","issueTypeId","assigneeAccountId","priority","labels","body","update","transitionId","nullableStringSchema","jiraAgentToolSelectionCatalog","buildJiraAgentToolSelectionCatalog","JIRA_TOOL_OPERATIONS","get_issue","method","path","args","issuePath","query","definedArguments","search_issues","get_issue_comments","get_issue_transitions","get_project","encodeURIComponent","stringArgument","get_user","create_issue","issueWriteBody","update_issue","add_comment","jiraPlainTextToAdf","transition_issue","transitionBody","assign_issue","type","version","content","text","catalog","selectors","map","entry","token","kind","suffix","issueFields","Object","keys","length","isRecord","stringArgumentOrUndefined","undefined","project","key","issuetype","name","assignee","Array","isArray","transition","names","fromEntries","filter","value","String","input","required","additionalProperties","items"],"mappings":"AAyBA,MAAMA,qBAAqBC,aAAa;AACxC,MAAMC,uBAAuBD,aAAa;AAC1C,MAAME,eAAeC,aAAa;AAClC,MAAMC,eAAeD,aAAa;AAClC,MAAME,mBAAmBC,YAAYN,aAAa;AAClD,MAAMO,mBAAmBD,YAAYN,aAAa;AAElD,OAAO,MAAMQ,uBAAuB;IAClCC,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEC,SAASlB;YACTmB,QAAQb;YACRc,cAAcC,cAAc;YAC5BC,QAAQrB,aAAa;YACrBsB,YAAYf;YACZgB,eAAeH,cAAc;QAC/B,GACA;YAAC;SAAU;IAEf;IACAX,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEQ,KAAKxB,aAAa;YAClByB,YAAYC,cAAc;YAC1BC,eAAe3B,aAAa;YAC5BkB,QAAQb;YACRc,cAAcC,cAAc;YAC5BC,QAAQrB,aAAa;YACrBsB,YAAYf;YACZqB,iBAAiBtB,YAAYoB,cAAc;QAC7C,GACA;YAAC;SAAM;IAEX;IACAjB,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEC,SAASlB;YACT8B,SAASH,cAAc;YACvBD,YAAYC,cAAc;YAC1BI,SAAS9B,aAAa;YACtBqB,QAAQrB,aAAa;QACvB,GACA;YAAC;SAAU;IAEf;IACAS,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEC,SAASlB;YACTsB,QAAQrB,aAAa;QACvB,GACA;YAAC;SAAU;IAEf;IACAS,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEC,SAAShB;YACToB,QAAQrB,aAAa;YACrBsB,YAAYf;QACd,GACA;YAAC;SAAU;IAEf;IACAE,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEe,WAAW/B,aAAa;YACxBqB,QAAQrB,aAAa;QACvB,GACA;YAAC;SAAY;IAEjB;IACAS,KAAK;QACHC,IAAI;QACJC,aACE;QACFC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEgB,YAAYhC,aACV;YAEFiC,WAAWjC,aAAa;YACxBkC,SAASlC,aAAa;YACtBmC,WAAWnC,aACT;YAEFoC,aAAapC,aAAa;YAC1BqC,mBAAmBrC,aAAa;YAChCsC,UAAUtC,aAAa;YACvBuC,QAAQjC,YAAYN,aAAa;YACjCwC,MAAMxC,aAAa;YACnBkB,QAAQhB;YACRuC,QAAQrC;QACV,GACA;YAAC;SAAU;IAEf;IACAK,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEC,SAASlB;YACTmC,SAASlC,aAAa;YACtBmC,WAAWnC,aACT;YAEFoC,aAAapC,aAAa;YAC1BqC,mBAAmBrC,aAAa;YAChCsC,UAAUtC,aAAa;YACvBuC,QAAQjC,YAAYN,aAAa;YACjCwC,MAAMxC,aAAa;YACnBkB,QAAQhB;YACRuC,QAAQrC;QACV,GACA;YAAC;SAAU;IAEf;IACAK,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEC,SAASlB;YACTyC,MAAMxC,aAAa;QACrB,GACA;YAAC;YAAW;SAAO;IAEvB;IACAS,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEC,SAASlB;YACT2C,cAAc1C,aAAa;YAC3BkB,QAAQhB;YACRuC,QAAQrC;QACV,GACA;YAAC;YAAW;SAAe;IAE/B;IACAK,KAAK;QACHC,IAAI;QACJC,aAAa;QACbC,aAAa;QACbC,WAAW;QACXC,eAAe;QACfC,aAAaC,aACX;YACEC,SAASlB;YACTgC,WAAWY,qBAAqB;QAClC,GACA;YAAC;YAAW;SAAY;IAE5B;CACD,CAAyD;AAE1D,OAAO,MAAMC,gCACXC,mCAAmCrC,sBAAsB;AAS3D,OAAO,MAAMsC,uBAAmE;IAC9EC,WAAW;QACTC,QAAQ;QACRC,MAAM,CAACC,OAASC,UAAUD;QAC1BE,OAAO,CAACF,OACNG,iBAAiBH,MAAM;gBAAC;gBAAU;gBAAgB;gBAAU;gBAAc;aAAgB;IAC9F;IACAI,eAAe;QACbN,QAAQ;QACRC,MAAM,IAAM;QACZT,MAAM,CAACU,OACLG,iBAAiBH,MAAM;gBACrB;gBACA;gBACA;gBACA;gBACA;gBACA;gBACA;gBACA;aACD;IACL;IACAK,oBAAoB;QAClBP,QAAQ;QACRC,MAAM,CAACC,OAASC,UAAUD,MAAM;QAChCE,OAAO,CAACF,OAASG,iBAAiBH,MAAM;gBAAC;gBAAW;gBAAc;gBAAW;aAAS;IACxF;IACAM,uBAAuB;QACrBR,QAAQ;QACRC,MAAM,CAACC,OAASC,UAAUD,MAAM;QAChCE,OAAO,CAACF,OAASG,iBAAiBH,MAAM;gBAAC;aAAS;IACpD;IACAO,aAAa;QACXT,QAAQ;QACRC,MAAM,CAACC,OAAS,CAAC,SAAS,EAAEQ,mBAAmBC,eAAeT,MAAM,aAAa;QACjFE,OAAO,CAACF,OAASG,iBAAiBH,MAAM;gBAAC;gBAAU;aAAa;IAClE;IACAU,UAAU;QACRZ,QAAQ;QACRC,MAAM,IAAM;QACZG,OAAO,CAACF,OAASG,iBAAiBH,MAAM;gBAAC;gBAAa;aAAS;IACjE;IACAW,cAAc;QACZb,QAAQ;QACRC,MAAM,IAAM;QACZT,MAAM,CAACU,OAASY,eAAeZ;IACjC;IACAa,cAAc;QACZf,QAAQ;QACRC,MAAM,CAACC,OAASC,UAAUD;QAC1BV,MAAM,CAACU,OAASY,eAAeZ;IACjC;IACAc,aAAa;QACXhB,QAAQ;QACRC,MAAM,CAACC,OAASC,UAAUD,MAAM;QAChCV,MAAM,CAACU,OAAU,CAAA;gBAACV,MAAMyB,mBAAmBN,eAAeT,MAAM;YAAQ,CAAA;IAC1E;IACAgB,kBAAkB;QAChBlB,QAAQ;QACRC,MAAM,CAACC,OAASC,UAAUD,MAAM;QAChCV,MAAM,CAACU,OAASiB,eAAejB;IACjC;IACAkB,cAAc;QACZpB,QAAQ;QACRC,MAAM,CAACC,OAASC,UAAUD,MAAM;QAChCV,MAAM,CAACU,OAAU,CAAA;gBAACnB,WAAWmB,KAAKnB,SAAS;YAAA,CAAA;IAC7C;AACF,EAAE;AAEF,OAAO,SAASkC,mBAAmBzB,IAAY;IAC7C,OAAO;QACL6B,MAAM;QACNC,SAAS;QACTC,SAAS;YACP;gBACEF,MAAM;gBACNE,SAAS;oBAAC;wBAACF,MAAM;wBAAQG,MAAMhC;oBAAI;iBAAE;YACvC;SACD;IACH;AACF;AAEA,SAASK,mCACP4B,OAAqE;IAErE,OAAO;QACLC,WAAWD,QAAQE,GAAG,CACpB,CAACC,QAA8B,CAAA;gBAC7BC,OAAOD,MAAMlE,EAAE;gBACfoE,MAAM;gBACNlE,aAAagE,MAAMhE,WAAW;gBAC9BC,WAAW+D,MAAM/D,SAAS;YAC5B,CAAA;IAEJ;AACF;AAEA,SAASsC,UAAUD,IAA6B,EAAE6B,SAAS,EAAE;IAC3D,OAAO,CAAC,OAAO,EAAErB,mBAAmBC,eAAeT,MAAM,cAAc6B,SAAS,CAAC,CAAC,EAAEA,QAAQ,GAAG,IAAI;AACrG;AAEA,SAASjB,eAAeZ,IAA6B;IACnD,MAAMV,OAAgC,CAAC;IACvC,MAAMtB,SAAS8D,YAAY9B;IAC3B,IAAI+B,OAAOC,IAAI,CAAChE,QAAQiE,MAAM,GAAG,GAAG3C,KAAKtB,MAAM,GAAGA;IAClD,IAAIkE,SAASlC,KAAKT,MAAM,GAAGD,KAAKC,MAAM,GAAGS,KAAKT,MAAM;IACpD,OAAOD;AACT;AAEA,SAASwC,YAAY9B,IAA6B;IAChD,MAAMhC,SAASkE,SAASlC,KAAKhC,MAAM,IAAI;QAAC,GAAGgC,KAAKhC,MAAM;IAAA,IAAI,CAAC;IAC3D,MAAMc,aAAaqD,0BAA0BnC,MAAM;IACnD,MAAMjB,YAAYoD,0BAA0BnC,MAAM;IAClD,MAAMhB,UAAUmD,0BAA0BnC,MAAM;IAChD,MAAMf,YAAYkD,0BAA0BnC,MAAM;IAClD,MAAMd,cAAciD,0BAA0BnC,MAAM;IACpD,MAAMb,oBAAoBa,KAAKb,iBAAiB;IAChD,MAAMC,WAAW+C,0BAA0BnC,MAAM;IAEjD,IAAIlB,eAAesD,WAAWpE,OAAOqE,OAAO,GAAG;QAACC,KAAKxD;IAAU;IAC/D,IAAIC,cAAcqD,WAAWpE,OAAOqE,OAAO,GAAG;QAAC7E,IAAIuB;IAAS;IAC5D,IAAIC,YAAYoD,WAAWpE,OAAOgB,OAAO,GAAGA;IAC5C,IAAIC,cAAcmD,WAAWpE,OAAOuE,SAAS,GAAG;QAACC,MAAMvD;IAAS;IAChE,IAAIC,gBAAgBkD,WAAWpE,OAAOuE,SAAS,GAAG;QAAC/E,IAAI0B;IAAW;IAClE,IAAI,OAAOC,sBAAsB,UAAUnB,OAAOyE,QAAQ,GAAG;QAAC5D,WAAWM;IAAiB;IAC1F,IAAIC,aAAagD,WAAWpE,OAAOoB,QAAQ,GAAG;QAACoD,MAAMpD;IAAQ;IAC7D,IAAIsD,MAAMC,OAAO,CAAC3C,KAAKX,MAAM,GAAGrB,OAAOqB,MAAM,GAAGW,KAAKX,MAAM;IAC3D,IAAI,OAAOW,KAAKV,IAAI,KAAK,UAAUtB,OAAOP,WAAW,GAAGsD,mBAAmBf,KAAKV,IAAI;IAEpF,OAAOtB;AACT;AAEA,SAASiD,eAAejB,IAA6B;IACnD,MAAMV,OAAgC;QACpCsD,YAAY;YAACpF,IAAIiD,eAAeT,MAAM;QAAe;IACvD;IACA,MAAMhC,SAASkE,SAASlC,KAAKhC,MAAM,IAAIgC,KAAKhC,MAAM,GAAGoE;IACrD,IAAIpE,WAAWoE,WAAW9C,KAAKtB,MAAM,GAAGA;IACxC,IAAIkE,SAASlC,KAAKT,MAAM,GAAGD,KAAKC,MAAM,GAAGS,KAAKT,MAAM;IACpD,OAAOD;AACT;AAEA,SAASa,iBACPH,IAA6B,EAC7B6C,KAAwB;IAExB,OAAOd,OAAOe,WAAW,CACvBD,MAAMpB,GAAG,CAAC,CAACe,OAAS;YAACA;YAAMxC,IAAI,CAACwC,KAAK;SAAC,EAAWO,MAAM,CAAC,CAAC,GAAGC,MAAM,GAAKA,UAAUZ;AAErF;AAEA,SAAS3B,eAAeT,IAA6B,EAAEwC,IAAY;IACjE,MAAMQ,QAAQhD,IAAI,CAACwC,KAAK;IACxB,OAAO,OAAOQ,UAAU,WAAWA,QAAQC,OAAOD,SAAS;AAC7D;AAEA,SAASb,0BACPnC,IAA6B,EAC7BwC,IAAY;IAEZ,MAAMQ,QAAQhD,IAAI,CAACwC,KAAK;IACxB,OAAO,OAAOQ,UAAU,WAAWA,QAAQZ;AAC7C;AAEA,SAASF,SAASc,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACN,MAAMC,OAAO,CAACK;AACvE;AAEA,SAASzF,KAAK2F,KAAgC;IAC5C,OAAOA;AACT;AAEA,SAASpF,aACPM,UAA+C,EAC/C+E,WAAqB,EAAE;IAEvB,OAAO;QACLhC,MAAM;QACNiC,sBAAsB;QACtBhF;QACA,GAAI+E,SAASlB,MAAM,GAAG,IAAI;YAACkB;QAAQ,IAAI,CAAC,CAAC;IAC3C;AACF;AAEA,SAASlG,aAAaQ,WAAoB;IACxC,OAAO;QACL0D,MAAM;QACNiC,sBAAsB;QACtB,GAAI3F,gBAAgB2E,YAAY,CAAC,IAAI;YAAC3E;QAAW,CAAC;IACpD;AACF;AAEA,SAASX,aAAaW,WAAoB;IACxC,OAAO;QAAC0D,MAAM;QAAU,GAAI1D,gBAAgB2E,YAAY,CAAC,IAAI;YAAC3E;QAAW,CAAC;IAAC;AAC7E;AAEA,SAASgC,qBAAqBhC,WAAoB;IAChD,OAAO;QAAC0D,MAAM;YAAC;YAAU;SAAO;QAAE,GAAI1D,gBAAgB2E,YAAY,CAAC,IAAI;YAAC3E;QAAW,CAAC;IAAC;AACvF;AAEA,SAASe,cAAcf,WAAoB;IACzC,OAAO;QAAC0D,MAAM;QAAW,GAAI1D,gBAAgB2E,YAAY,CAAC,IAAI;YAAC3E;QAAW,CAAC;IAAC;AAC9E;AAEA,SAASS,cAAcT,WAAoB;IACzC,OAAO;QAAC0D,MAAM;QAAW,GAAI1D,gBAAgB2E,YAAY,CAAC,IAAI;YAAC3E;QAAW,CAAC;IAAC;AAC9E;AAEA,SAASL,YAAYiG,KAA0B;IAC7C,OAAO;QAAClC,MAAM;QAASkC;IAAK;AAC9B"}
@@ -3,6 +3,7 @@ import type { IntegrationConnection } from '@shipfox/api-integration-spi';
3
3
  import type { JiraAccessibleResource, JiraApiClient } from '#api/client.js';
4
4
  import type { JiraPendingSelectionStore } from '#core/pending.js';
5
5
  import type { JiraTokenStore } from '#core/tokens.js';
6
+ import { type JiraInstallationLock } from '#db/installations.js';
6
7
  export interface ConnectJiraInstallationInput {
7
8
  workspaceId: string;
8
9
  cloudId: string;
@@ -30,6 +31,27 @@ export interface HandleJiraCallbackParams {
30
31
  cloudId: string;
31
32
  }): Promise<IntegrationConnection<'jira'> | undefined>;
32
33
  connectJiraInstallation(input: ConnectJiraInstallationInput): Promise<IntegrationConnection<'jira'>>;
34
+ registerJiraWebhook(input: {
35
+ connectionId: string;
36
+ cloudId: string;
37
+ accessToken: string;
38
+ withRegistrationLock?: JiraInstallationLock;
39
+ onRegistrationSuccess: (input: {
40
+ tx?: unknown;
41
+ }) => Promise<void>;
42
+ onRegistrationFailure: (input: {
43
+ tx?: unknown;
44
+ }) => Promise<void>;
45
+ }): Promise<void>;
46
+ withJiraInstallationLock?: JiraInstallationLock;
47
+ markConnectionActive(input: {
48
+ connectionId: string;
49
+ tx?: unknown;
50
+ }): Promise<void>;
51
+ markConnectionError(input: {
52
+ connectionId: string;
53
+ tx?: unknown;
54
+ }): Promise<void>;
33
55
  disconnectJiraInstallation(input: {
34
56
  connectionId: string;
35
57
  }): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/core/install.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,8BAA8B,CAAC;AAExE,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAoB,MAAM,gBAAgB,CAAC;AAC7F,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,iBAAiB,CAAC;AAapD,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,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,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACzD,0BAA0B,CAAC,KAAK,EAAE;QAChC,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;KACnD,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrB,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;CAC1E;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,GAAG;IAAC,KAAK,EAAE,sBAAsB,EAAE,CAAA;CAAC,CAAC,CA4B5E;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,IAAI,CAAC,wBAAwB,EAAE,MAAM,CAAC,GAAG;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACjE,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAkBxC;AAED,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACzD,0BAA0B,CAAC,KAAK,EAAE;QAChC,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;KACnD,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,GAAG,OAAO,CAAC,KAAK,CAAC,CAGjB"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/core/install.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { logger } from '@shipfox/node-opentelemetry';
2
+ import { withJiraInstallationLock } from '#db/installations.js';
2
3
  import { JiraInstallationAlreadyLinkedError, JiraInstallStateActorMismatchError, JiraIntegrationProviderError, JiraOAuthCallbackError, JiraOfflineAccessNotGrantedError, JiraPendingSelectionNotFoundError, JiraSiteSelectionMismatchError } from './errors.js';
3
4
  import { assertJiraAuthorizationScopes } from './scopes.js';
4
5
  import { verifyJiraInstallState } from './state.js';
@@ -71,18 +72,18 @@ async function verifyClaims(params, state) {
71
72
  });
72
73
  return claims;
73
74
  }
74
- async function resolveJiraSite(params) {
75
- const existing = await params.getExistingJiraConnection({
76
- cloudId: params.site.cloudId
77
- });
78
- if (existing && existing.workspaceId !== params.claims.workspaceId) throw new JiraInstallationAlreadyLinkedError(params.site.cloudId);
79
- let connectionId;
80
- try {
75
+ function resolveJiraSite(params) {
76
+ const withInstallationLock = params.withJiraInstallationLock ?? withJiraInstallationLock;
77
+ return withInstallationLock(params.site.cloudId, async ()=>{
78
+ const existing = await params.getExistingJiraConnection({
79
+ cloudId: params.site.cloudId
80
+ });
81
+ if (existing && existing.workspaceId !== params.claims.workspaceId) throw new JiraInstallationAlreadyLinkedError(params.site.cloudId);
81
82
  const identity = await params.jira.getMyself({
82
83
  accessToken: params.authorization.accessToken,
83
84
  cloudId: params.site.cloudId
84
85
  });
85
- const connection = await params.connectJiraInstallation({
86
+ const installationInput = {
86
87
  workspaceId: params.claims.workspaceId,
87
88
  cloudId: params.site.cloudId,
88
89
  siteUrl: params.site.url,
@@ -91,19 +92,46 @@ async function resolveJiraSite(params) {
91
92
  scopes: params.site.scopes,
92
93
  tokenExpiresAt: params.authorization.expiresAt ?? null,
93
94
  displayName: `Jira ${params.site.name}`
94
- });
95
- connectionId = connection.id;
96
- await params.tokenStore.storeTokens({
97
- connectionId: connection.id,
98
- accessToken: params.authorization.accessToken,
99
- refreshToken: params.authorization.refreshToken,
100
- editedBy: params.claims.userId
101
- });
95
+ };
96
+ let connection = existing;
97
+ try {
98
+ if (!connection) connection = await params.connectJiraInstallation(installationInput);
99
+ await params.tokenStore.storeTokens({
100
+ connectionId: connection.id,
101
+ accessToken: params.authorization.accessToken,
102
+ refreshToken: params.authorization.refreshToken,
103
+ editedBy: params.claims.userId
104
+ });
105
+ if (existing) connection = await params.connectJiraInstallation(installationInput);
106
+ } catch (error) {
107
+ if (existing) {
108
+ const markedError = await bestEffortMarkConnectionError(params, existing.id);
109
+ if (!markedError) await bestEffortMarkConnectionError(params, existing.id);
110
+ } else if (connection) {
111
+ await bestEffortDisconnect(params, connection.id);
112
+ }
113
+ throw error;
114
+ }
115
+ let registrationFailureHandled = false;
116
+ try {
117
+ await params.registerJiraWebhook({
118
+ connectionId: connection.id,
119
+ cloudId: params.site.cloudId,
120
+ accessToken: params.authorization.accessToken,
121
+ withRegistrationLock: async (_lockKey, fn)=>fn(),
122
+ onRegistrationSuccess: async ({ tx })=>{
123
+ await params.markConnectionActive(connectionLifecycleInput(connection.id, tx));
124
+ },
125
+ onRegistrationFailure: async ({ tx })=>{
126
+ registrationFailureHandled = await bestEffortMarkConnectionError(params, connection.id, tx);
127
+ }
128
+ });
129
+ } catch (error) {
130
+ if (!registrationFailureHandled) await bestEffortMarkConnectionError(params, connection.id);
131
+ throw error;
132
+ }
102
133
  return connection;
103
- } catch (error) {
104
- if (connectionId && !existing) await bestEffortDisconnect(params, connectionId);
105
- throw error;
106
- }
134
+ });
107
135
  }
108
136
  async function bestEffortDisconnect(params, connectionId) {
109
137
  try {
@@ -117,5 +145,25 @@ async function bestEffortDisconnect(params, connectionId) {
117
145
  }, 'Jira connect compensation failed after token storage rejection');
118
146
  }
119
147
  }
148
+ async function bestEffortMarkConnectionError(params, connectionId, tx) {
149
+ try {
150
+ await params.markConnectionError(connectionLifecycleInput(connectionId, tx));
151
+ return true;
152
+ } catch (error) {
153
+ logger().warn({
154
+ err: error,
155
+ connectionId
156
+ }, 'Jira connection error-state update failed after webhook registration rejection');
157
+ return false;
158
+ }
159
+ }
160
+ function connectionLifecycleInput(connectionId, tx) {
161
+ return tx === undefined ? {
162
+ connectionId
163
+ } : {
164
+ connectionId,
165
+ tx
166
+ };
167
+ }
120
168
 
121
169
  //# sourceMappingURL=install.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/install.ts"],"sourcesContent":["import type {UserContextMembership} from '@shipfox/api-auth-context';\nimport type {IntegrationConnection} from '@shipfox/api-integration-spi';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {JiraAccessibleResource, JiraApiClient, JiraAuthorization} from '#api/client.js';\nimport type {JiraPendingSelectionStore} from '#core/pending.js';\nimport type {JiraTokenStore} from '#core/tokens.js';\nimport {\n JiraInstallationAlreadyLinkedError,\n JiraInstallStateActorMismatchError,\n JiraIntegrationProviderError,\n JiraOAuthCallbackError,\n JiraOfflineAccessNotGrantedError,\n JiraPendingSelectionNotFoundError,\n JiraSiteSelectionMismatchError,\n} from './errors.js';\nimport {assertJiraAuthorizationScopes} from './scopes.js';\nimport {verifyJiraInstallState} from './state.js';\n\nexport interface ConnectJiraInstallationInput {\n workspaceId: string;\n cloudId: string;\n siteUrl: string;\n siteName: string;\n authorizingAccountId: string;\n scopes: string[];\n tokenExpiresAt: Date | null;\n displayName: string;\n}\n\nexport interface HandleJiraCallbackParams {\n jira: JiraApiClient;\n tokenStore: Pick<JiraTokenStore, 'storeTokens'>;\n pendingStore: Pick<JiraPendingSelectionStore, 'save' | 'load' | 'clear'>;\n code: string;\n state: string;\n sessionUserId: string;\n sessionMemberships: ReadonlyArray<UserContextMembership>;\n requireWorkspaceMembership(input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<UserContextMembership>;\n }): Promise<unknown>;\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}\n\nexport async function handleJiraCallback(\n params: HandleJiraCallbackParams,\n): Promise<IntegrationConnection<'jira'> | {sites: JiraAccessibleResource[]}> {\n const claims = await verifyClaims(params, params.state);\n const authorization = await params.jira.exchangeAuthorizationCode({code: params.code});\n if (!authorization.refreshToken) throw new JiraOfflineAccessNotGrantedError();\n const sites = await params.jira.getAccessibleResources({accessToken: authorization.accessToken});\n if (sites.length === 0)\n throw new JiraIntegrationProviderError(\n 'access-denied',\n 'Jira authorization did not grant access to a site',\n );\n if (sites.length > 1) {\n await params.pendingStore.save({\n workspaceId: claims.workspaceId,\n state: params.state,\n authorization,\n sites,\n });\n return {sites};\n }\n const site = sites[0];\n if (!site) {\n throw new JiraIntegrationProviderError(\n 'malformed-provider-response',\n 'Jira accessible-resources response did not contain a selected site',\n );\n }\n assertJiraAuthorizationScopes(site.scopes);\n return resolveJiraSite({...params, authorization, site, claims});\n}\n\nexport async function handleJiraSiteSelection(\n params: Omit<HandleJiraCallbackParams, 'code'> & {cloudId: string},\n): Promise<IntegrationConnection<'jira'>> {\n const claims = await verifyClaims(params, params.state);\n const pending = await params.pendingStore.load({\n workspaceId: claims.workspaceId,\n state: params.state,\n });\n if (!pending) throw new JiraPendingSelectionNotFoundError();\n const site = pending.sites.find((candidate) => candidate.cloudId === params.cloudId);\n if (!site) throw new JiraSiteSelectionMismatchError(params.cloudId);\n assertJiraAuthorizationScopes(site.scopes);\n const connection = await resolveJiraSite({\n ...params,\n authorization: pending.authorization,\n site,\n claims,\n });\n await params.pendingStore.clear({workspaceId: claims.workspaceId, state: params.state});\n return connection;\n}\n\nexport async function handleJiraOAuthCallbackError(params: {\n state: string;\n error: string;\n errorDescription?: string | undefined;\n sessionUserId: string;\n sessionMemberships: ReadonlyArray<UserContextMembership>;\n requireWorkspaceMembership(input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<UserContextMembership>;\n }): Promise<unknown>;\n}): Promise<never> {\n await verifyClaims(params, params.state);\n throw new JiraOAuthCallbackError(params.error, params.errorDescription);\n}\n\nasync function verifyClaims(\n params: Pick<\n HandleJiraCallbackParams,\n 'sessionUserId' | 'sessionMemberships' | 'requireWorkspaceMembership'\n >,\n state: string,\n) {\n const claims = verifyJiraInstallState(state);\n if (claims.userId !== params.sessionUserId) throw new JiraInstallStateActorMismatchError();\n await params.requireWorkspaceMembership({\n workspaceId: claims.workspaceId,\n userId: claims.userId,\n memberships: params.sessionMemberships,\n });\n return claims;\n}\n\nasync function resolveJiraSite(\n params: Omit<HandleJiraCallbackParams, 'code'> & {\n authorization: JiraAuthorization;\n site: JiraAccessibleResource;\n claims: {workspaceId: string; userId: string};\n },\n): Promise<IntegrationConnection<'jira'>> {\n const existing = await params.getExistingJiraConnection({cloudId: params.site.cloudId});\n if (existing && existing.workspaceId !== params.claims.workspaceId)\n throw new JiraInstallationAlreadyLinkedError(params.site.cloudId);\n let connectionId: string | undefined;\n try {\n const identity = await params.jira.getMyself({\n accessToken: params.authorization.accessToken,\n cloudId: params.site.cloudId,\n });\n const connection = await params.connectJiraInstallation({\n workspaceId: params.claims.workspaceId,\n cloudId: params.site.cloudId,\n siteUrl: params.site.url,\n siteName: params.site.name,\n authorizingAccountId: identity.accountId,\n scopes: params.site.scopes,\n tokenExpiresAt: params.authorization.expiresAt ?? null,\n displayName: `Jira ${params.site.name}`,\n });\n connectionId = connection.id;\n await params.tokenStore.storeTokens({\n connectionId: connection.id,\n accessToken: params.authorization.accessToken,\n refreshToken: params.authorization.refreshToken,\n editedBy: params.claims.userId,\n });\n return connection;\n } catch (error) {\n if (connectionId && !existing) await bestEffortDisconnect(params, connectionId);\n throw error;\n }\n}\n\nasync function bestEffortDisconnect(\n params: Pick<HandleJiraCallbackParams, 'disconnectJiraInstallation'>,\n connectionId: string,\n): Promise<void> {\n try {\n await params.disconnectJiraInstallation({connectionId});\n } catch (error) {\n logger().warn(\n {err: error, connectionId},\n 'Jira connect compensation failed after token storage rejection',\n );\n }\n}\n"],"names":["logger","JiraInstallationAlreadyLinkedError","JiraInstallStateActorMismatchError","JiraIntegrationProviderError","JiraOAuthCallbackError","JiraOfflineAccessNotGrantedError","JiraPendingSelectionNotFoundError","JiraSiteSelectionMismatchError","assertJiraAuthorizationScopes","verifyJiraInstallState","handleJiraCallback","params","claims","verifyClaims","state","authorization","jira","exchangeAuthorizationCode","code","refreshToken","sites","getAccessibleResources","accessToken","length","pendingStore","save","workspaceId","site","scopes","resolveJiraSite","handleJiraSiteSelection","pending","load","find","candidate","cloudId","connection","clear","handleJiraOAuthCallbackError","error","errorDescription","userId","sessionUserId","requireWorkspaceMembership","memberships","sessionMemberships","existing","getExistingJiraConnection","connectionId","identity","getMyself","connectJiraInstallation","siteUrl","url","siteName","name","authorizingAccountId","accountId","tokenExpiresAt","expiresAt","displayName","id","tokenStore","storeTokens","editedBy","bestEffortDisconnect","disconnectJiraInstallation","warn","err"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/install.ts"],"sourcesContent":["import type {UserContextMembership} from '@shipfox/api-auth-context';\nimport type {IntegrationConnection} from '@shipfox/api-integration-spi';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport type {JiraAccessibleResource, JiraApiClient, JiraAuthorization} from '#api/client.js';\nimport type {JiraPendingSelectionStore} from '#core/pending.js';\nimport type {JiraTokenStore} from '#core/tokens.js';\nimport {type JiraInstallationLock, withJiraInstallationLock} from '#db/installations.js';\nimport {\n JiraInstallationAlreadyLinkedError,\n JiraInstallStateActorMismatchError,\n JiraIntegrationProviderError,\n JiraOAuthCallbackError,\n JiraOfflineAccessNotGrantedError,\n JiraPendingSelectionNotFoundError,\n JiraSiteSelectionMismatchError,\n} from './errors.js';\nimport {assertJiraAuthorizationScopes} from './scopes.js';\nimport {verifyJiraInstallState} from './state.js';\n\nexport interface ConnectJiraInstallationInput {\n workspaceId: string;\n cloudId: string;\n siteUrl: string;\n siteName: string;\n authorizingAccountId: string;\n scopes: string[];\n tokenExpiresAt: Date | null;\n displayName: string;\n}\n\nexport interface HandleJiraCallbackParams {\n jira: JiraApiClient;\n tokenStore: Pick<JiraTokenStore, 'storeTokens'>;\n pendingStore: Pick<JiraPendingSelectionStore, 'save' | 'load' | 'clear'>;\n code: string;\n state: string;\n sessionUserId: string;\n sessionMemberships: ReadonlyArray<UserContextMembership>;\n requireWorkspaceMembership(input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<UserContextMembership>;\n }): Promise<unknown>;\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}\n\nexport async function handleJiraCallback(\n params: HandleJiraCallbackParams,\n): Promise<IntegrationConnection<'jira'> | {sites: JiraAccessibleResource[]}> {\n const claims = await verifyClaims(params, params.state);\n const authorization = await params.jira.exchangeAuthorizationCode({code: params.code});\n if (!authorization.refreshToken) throw new JiraOfflineAccessNotGrantedError();\n const sites = await params.jira.getAccessibleResources({accessToken: authorization.accessToken});\n if (sites.length === 0)\n throw new JiraIntegrationProviderError(\n 'access-denied',\n 'Jira authorization did not grant access to a site',\n );\n if (sites.length > 1) {\n await params.pendingStore.save({\n workspaceId: claims.workspaceId,\n state: params.state,\n authorization,\n sites,\n });\n return {sites};\n }\n const site = sites[0];\n if (!site) {\n throw new JiraIntegrationProviderError(\n 'malformed-provider-response',\n 'Jira accessible-resources response did not contain a selected site',\n );\n }\n assertJiraAuthorizationScopes(site.scopes);\n return resolveJiraSite({...params, authorization, site, claims});\n}\n\nexport async function handleJiraSiteSelection(\n params: Omit<HandleJiraCallbackParams, 'code'> & {cloudId: string},\n): Promise<IntegrationConnection<'jira'>> {\n const claims = await verifyClaims(params, params.state);\n const pending = await params.pendingStore.load({\n workspaceId: claims.workspaceId,\n state: params.state,\n });\n if (!pending) throw new JiraPendingSelectionNotFoundError();\n const site = pending.sites.find((candidate) => candidate.cloudId === params.cloudId);\n if (!site) throw new JiraSiteSelectionMismatchError(params.cloudId);\n assertJiraAuthorizationScopes(site.scopes);\n const connection = await resolveJiraSite({\n ...params,\n authorization: pending.authorization,\n site,\n claims,\n });\n await params.pendingStore.clear({workspaceId: claims.workspaceId, state: params.state});\n return connection;\n}\n\nexport async function handleJiraOAuthCallbackError(params: {\n state: string;\n error: string;\n errorDescription?: string | undefined;\n sessionUserId: string;\n sessionMemberships: ReadonlyArray<UserContextMembership>;\n requireWorkspaceMembership(input: {\n workspaceId: string;\n userId: string;\n memberships: ReadonlyArray<UserContextMembership>;\n }): Promise<unknown>;\n}): Promise<never> {\n await verifyClaims(params, params.state);\n throw new JiraOAuthCallbackError(params.error, params.errorDescription);\n}\n\nasync function verifyClaims(\n params: Pick<\n HandleJiraCallbackParams,\n 'sessionUserId' | 'sessionMemberships' | 'requireWorkspaceMembership'\n >,\n state: string,\n) {\n const claims = verifyJiraInstallState(state);\n if (claims.userId !== params.sessionUserId) throw new JiraInstallStateActorMismatchError();\n await params.requireWorkspaceMembership({\n workspaceId: claims.workspaceId,\n userId: claims.userId,\n memberships: params.sessionMemberships,\n });\n return claims;\n}\n\nfunction resolveJiraSite(\n params: Omit<HandleJiraCallbackParams, 'code'> & {\n authorization: JiraAuthorization;\n site: JiraAccessibleResource;\n claims: {workspaceId: string; userId: string};\n },\n): Promise<IntegrationConnection<'jira'>> {\n const withInstallationLock = params.withJiraInstallationLock ?? withJiraInstallationLock;\n return withInstallationLock(params.site.cloudId, async () => {\n const existing = await params.getExistingJiraConnection({cloudId: params.site.cloudId});\n if (existing && existing.workspaceId !== params.claims.workspaceId)\n throw new JiraInstallationAlreadyLinkedError(params.site.cloudId);\n const identity = await params.jira.getMyself({\n accessToken: params.authorization.accessToken,\n cloudId: params.site.cloudId,\n });\n const installationInput = {\n workspaceId: params.claims.workspaceId,\n cloudId: params.site.cloudId,\n siteUrl: params.site.url,\n siteName: params.site.name,\n authorizingAccountId: identity.accountId,\n scopes: params.site.scopes,\n tokenExpiresAt: params.authorization.expiresAt ?? null,\n displayName: `Jira ${params.site.name}`,\n };\n let connection = existing;\n try {\n if (!connection) connection = await params.connectJiraInstallation(installationInput);\n await params.tokenStore.storeTokens({\n connectionId: connection.id,\n accessToken: params.authorization.accessToken,\n refreshToken: params.authorization.refreshToken,\n editedBy: params.claims.userId,\n });\n if (existing) connection = await params.connectJiraInstallation(installationInput);\n } catch (error) {\n if (existing) {\n const markedError = await bestEffortMarkConnectionError(params, existing.id);\n if (!markedError) await bestEffortMarkConnectionError(params, existing.id);\n } else if (connection) {\n await bestEffortDisconnect(params, connection.id);\n }\n throw error;\n }\n\n let registrationFailureHandled = false;\n try {\n await params.registerJiraWebhook({\n connectionId: connection.id,\n cloudId: params.site.cloudId,\n accessToken: params.authorization.accessToken,\n withRegistrationLock: async (_lockKey, fn) => fn(),\n onRegistrationSuccess: async ({tx}) => {\n await params.markConnectionActive(connectionLifecycleInput(connection.id, tx));\n },\n onRegistrationFailure: async ({tx}) => {\n registrationFailureHandled = await bestEffortMarkConnectionError(\n params,\n connection.id,\n tx,\n );\n },\n });\n } catch (error) {\n if (!registrationFailureHandled) await bestEffortMarkConnectionError(params, connection.id);\n throw error;\n }\n\n return connection;\n });\n}\n\nasync function bestEffortDisconnect(\n params: Pick<HandleJiraCallbackParams, 'disconnectJiraInstallation'>,\n connectionId: string,\n): Promise<void> {\n try {\n await params.disconnectJiraInstallation({connectionId});\n } catch (error) {\n logger().warn(\n {err: error, connectionId},\n 'Jira connect compensation failed after token storage rejection',\n );\n }\n}\n\nasync function bestEffortMarkConnectionError(\n params: Pick<HandleJiraCallbackParams, 'markConnectionError'>,\n connectionId: string,\n tx?: unknown,\n): Promise<boolean> {\n try {\n await params.markConnectionError(connectionLifecycleInput(connectionId, tx));\n return true;\n } catch (error) {\n logger().warn(\n {err: error, connectionId},\n 'Jira connection error-state update failed after webhook registration rejection',\n );\n return false;\n }\n}\n\nfunction connectionLifecycleInput(connectionId: string, tx?: unknown) {\n return tx === undefined ? {connectionId} : {connectionId, tx};\n}\n"],"names":["logger","withJiraInstallationLock","JiraInstallationAlreadyLinkedError","JiraInstallStateActorMismatchError","JiraIntegrationProviderError","JiraOAuthCallbackError","JiraOfflineAccessNotGrantedError","JiraPendingSelectionNotFoundError","JiraSiteSelectionMismatchError","assertJiraAuthorizationScopes","verifyJiraInstallState","handleJiraCallback","params","claims","verifyClaims","state","authorization","jira","exchangeAuthorizationCode","code","refreshToken","sites","getAccessibleResources","accessToken","length","pendingStore","save","workspaceId","site","scopes","resolveJiraSite","handleJiraSiteSelection","pending","load","find","candidate","cloudId","connection","clear","handleJiraOAuthCallbackError","error","errorDescription","userId","sessionUserId","requireWorkspaceMembership","memberships","sessionMemberships","withInstallationLock","existing","getExistingJiraConnection","identity","getMyself","installationInput","siteUrl","url","siteName","name","authorizingAccountId","accountId","tokenExpiresAt","expiresAt","displayName","connectJiraInstallation","tokenStore","storeTokens","connectionId","id","editedBy","markedError","bestEffortMarkConnectionError","bestEffortDisconnect","registrationFailureHandled","registerJiraWebhook","withRegistrationLock","_lockKey","fn","onRegistrationSuccess","tx","markConnectionActive","connectionLifecycleInput","onRegistrationFailure","disconnectJiraInstallation","warn","err","markConnectionError","undefined"],"mappings":"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"}
@@ -1,6 +1,8 @@
1
+ import type { IntegrationConnectionLifecycleStatus } from '@shipfox/api-integration-spi';
1
2
  import { type JiraApiClient } from '#api/client.js';
2
3
  export interface JiraConnectionResolverResult {
3
4
  workspaceId: string;
5
+ lifecycleStatus: IntegrationConnectionLifecycleStatus;
4
6
  }
5
7
  export interface JiraSecretsStore {
6
8
  getSecret(params: {
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/core/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAkBvE,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,MAAM,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACjG,UAAU,CAAC,MAAM,EAAE;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;KACtC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC,CAAC;IAC3F,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACnC,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,cAAc,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACnE;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,0BAA0B,GAAG,cAAc,CA6DvF"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/core/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,oCAAoC,EAAC,MAAM,8BAA8B,CAAC;AAEvF,OAAO,EAAsB,KAAK,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAkBvE,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,oCAAoC,CAAC;CACvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,MAAM,EAAE;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACjG,UAAU,CAAC,MAAM,EAAE;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;KACtC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC,CAAC;IAC3F,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACnC,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE;QAAC,YAAY,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,cAAc,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACnE;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,0BAA0B,GAAG,cAAc,CAgIvF"}
@@ -1,19 +1,49 @@
1
+ import { logger } from '@shipfox/node-opentelemetry';
1
2
  import { createJiraApiClient } from '#api/client.js';
2
3
  import { JiraAccessTokenMissingError, JiraConnectionNotFoundError, JiraIntegrationProviderError, JiraTokenUnrefreshableError } from '#core/errors.js';
3
- import { getJiraInstallationByConnectionId, updateJiraInstallationTokenExpiry, withJiraRefreshLock } from '#db/installations.js';
4
+ import { getJiraInstallationByConnectionId, updateJiraInstallationTokenExpiry, withJiraRefreshLock, withJiraRefreshLockAndWait } from '#db/installations.js';
4
5
  const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN';
5
6
  const REFRESH_TOKEN_KEY = 'REFRESH_TOKEN';
6
7
  const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
7
- const tokenRefreshes = new Map();
8
8
  export function jiraSecretsNamespace(connectionId) {
9
9
  return `system/integrations/jira/${connectionId}`;
10
10
  }
11
11
  export function createJiraTokenStore(params) {
12
12
  const client = params.client ?? createJiraApiClient();
13
- async function resolveWorkspaceId(connectionId) {
13
+ const tokenRefreshes = new Map();
14
+ const refreshStateUnknownConnections = new Set();
15
+ const credentialGenerations = new Map();
16
+ const credentialWriteQueues = new Map();
17
+ const currentCredentialGeneration = (connectionId)=>credentialGenerations.get(connectionId) ?? 0;
18
+ const advanceCredentialGeneration = (connectionId)=>{
19
+ const generation = currentCredentialGeneration(connectionId) + 1;
20
+ credentialGenerations.set(connectionId, generation);
21
+ return generation;
22
+ };
23
+ function withCredentialWrite(connectionId, operation) {
24
+ const previous = credentialWriteQueues.get(connectionId) ?? Promise.resolve();
25
+ const operationResult = previous.then(operation);
26
+ const completion = operationResult.then(()=>undefined, ()=>undefined);
27
+ credentialWriteQueues.set(connectionId, completion);
28
+ return operationResult.finally(()=>{
29
+ if (credentialWriteQueues.get(connectionId) === completion) {
30
+ credentialWriteQueues.delete(connectionId);
31
+ }
32
+ });
33
+ }
34
+ async function waitForCredentialWrites(connectionId) {
35
+ await credentialWriteQueues.get(connectionId);
36
+ }
37
+ async function resolveConnection(connectionId) {
14
38
  const connection = await params.resolveConnection(connectionId);
15
39
  if (!connection) throw new JiraConnectionNotFoundError(connectionId);
16
- return connection.workspaceId;
40
+ return connection;
41
+ }
42
+ async function resolveWorkspaceId(connectionId) {
43
+ return (await resolveConnection(connectionId)).workspaceId;
44
+ }
45
+ function clearTokenRefresh(connectionId, refresh) {
46
+ if (tokenRefreshes.get(connectionId) === refresh) tokenRefreshes.delete(connectionId);
17
47
  }
18
48
  const readSecretToken = (workspaceId, connectionId, key)=>params.secrets.getSecret({
19
49
  workspaceId,
@@ -27,20 +57,39 @@ export function createJiraTokenStore(params) {
27
57
  }
28
58
  return {
29
59
  async storeTokens (input) {
30
- const workspaceId = await resolveWorkspaceId(input.connectionId);
31
- const values = {
32
- [ACCESS_TOKEN_KEY]: input.accessToken
33
- };
34
- if (input.refreshToken) values[REFRESH_TOKEN_KEY] = input.refreshToken;
35
- await params.secrets.setSecrets({
36
- workspaceId,
37
- namespace: jiraSecretsNamespace(input.connectionId),
38
- values,
39
- editedBy: input.editedBy
40
- });
60
+ const credentialGeneration = advanceCredentialGeneration(input.connectionId);
61
+ tokenRefreshes.delete(input.connectionId);
62
+ await withJiraRefreshLockAndWait(input.connectionId, ()=>withCredentialWrite(input.connectionId, async ()=>{
63
+ if (currentCredentialGeneration(input.connectionId) !== credentialGeneration) return;
64
+ const workspaceId = await resolveWorkspaceId(input.connectionId);
65
+ if (currentCredentialGeneration(input.connectionId) !== credentialGeneration) return;
66
+ const values = {
67
+ [ACCESS_TOKEN_KEY]: input.accessToken
68
+ };
69
+ if (input.refreshToken) values[REFRESH_TOKEN_KEY] = input.refreshToken;
70
+ await params.secrets.setSecrets({
71
+ workspaceId,
72
+ namespace: jiraSecretsNamespace(input.connectionId),
73
+ values,
74
+ editedBy: input.editedBy
75
+ });
76
+ if (currentCredentialGeneration(input.connectionId) === credentialGeneration) {
77
+ refreshStateUnknownConnections.delete(input.connectionId);
78
+ }
79
+ }));
41
80
  },
42
81
  async getAccessToken (input) {
43
- const workspaceId = await resolveWorkspaceId(input.connectionId);
82
+ await waitForCredentialWrites(input.connectionId);
83
+ const connection = await resolveConnection(input.connectionId);
84
+ if (connection.lifecycleStatus !== 'active') {
85
+ throw new JiraTokenUnrefreshableError(input.connectionId);
86
+ }
87
+ await waitForCredentialWrites(input.connectionId);
88
+ if (refreshStateUnknownConnections.has(input.connectionId)) {
89
+ throw new JiraTokenUnrefreshableError(input.connectionId);
90
+ }
91
+ const credentialGeneration = currentCredentialGeneration(input.connectionId);
92
+ const workspaceId = connection.workspaceId;
44
93
  const accessToken = await readAccessToken(workspaceId, input.connectionId);
45
94
  if (!input.forceRefresh && !shouldRefresh((await getJiraInstallationByConnectionId(input.connectionId))?.tokenExpiresAt ?? null)) return accessToken;
46
95
  const inFlight = tokenRefreshes.get(input.connectionId);
@@ -54,10 +103,18 @@ export function createJiraTokenStore(params) {
54
103
  secrets: params.secrets,
55
104
  readAccessToken,
56
105
  readSecretToken,
57
- markConnectionError: params.markConnectionError
106
+ markConnectionError: params.markConnectionError,
107
+ withCredentialWrite,
108
+ waitForCredentialWrites,
109
+ isCurrentCredentialGeneration: ()=>currentCredentialGeneration(input.connectionId) === credentialGeneration
58
110
  });
59
111
  tokenRefreshes.set(input.connectionId, refresh);
60
- void refresh.then(()=>clearTokenRefresh(input.connectionId, refresh), ()=>clearTokenRefresh(input.connectionId, refresh));
112
+ void refresh.then(()=>clearTokenRefresh(input.connectionId, refresh), (error)=>{
113
+ if (isTerminalRefreshFailure(error) && currentCredentialGeneration(input.connectionId) === credentialGeneration) {
114
+ refreshStateUnknownConnections.add(input.connectionId);
115
+ }
116
+ clearTokenRefresh(input.connectionId, refresh);
117
+ });
61
118
  return refresh;
62
119
  }
63
120
  };
@@ -65,17 +122,20 @@ export function createJiraTokenStore(params) {
65
122
  function shouldRefresh(expiresAt) {
66
123
  return expiresAt !== null && expiresAt.getTime() <= Date.now() + TOKEN_REFRESH_MARGIN_MS;
67
124
  }
68
- function clearTokenRefresh(connectionId, refresh) {
69
- if (tokenRefreshes.get(connectionId) === refresh) tokenRefreshes.delete(connectionId);
70
- }
71
125
  async function refreshAccessTokenWithLock(params) {
72
126
  const lock = await withJiraRefreshLock(params.connectionId, ()=>refreshAccessTokenForConnection(params));
73
127
  if (lock.acquired) return lock.value;
128
+ if (!params.isCurrentCredentialGeneration()) {
129
+ return params.readAccessToken(params.workspaceId, params.connectionId);
130
+ }
74
131
  const reread = await params.readAccessToken(params.workspaceId, params.connectionId);
75
132
  if (reread !== params.originalAccessToken) return reread;
76
133
  throw new JiraIntegrationProviderError('provider-unavailable', 'Jira token refresh is already in progress');
77
134
  }
78
135
  async function refreshAccessTokenForConnection(params) {
136
+ if (!params.isCurrentCredentialGeneration()) {
137
+ return params.readAccessToken(params.workspaceId, params.connectionId);
138
+ }
79
139
  const current = await params.readAccessToken(params.workspaceId, params.connectionId);
80
140
  const installation = await getJiraInstallationByConnectionId(params.connectionId);
81
141
  if (current !== params.originalAccessToken || !params.forceRefresh && !shouldRefresh(installation?.tokenExpiresAt ?? null)) return current;
@@ -88,29 +148,61 @@ async function refreshAccessTokenForConnection(params) {
88
148
  const refreshed = await params.client.refreshAccessToken({
89
149
  refreshToken
90
150
  });
91
- if (!refreshed.refreshToken) throw new JiraTokenUnrefreshableError(params.connectionId);
92
- await params.secrets.setSecrets({
93
- workspaceId: params.workspaceId,
94
- namespace: jiraSecretsNamespace(params.connectionId),
95
- values: {
96
- [ACCESS_TOKEN_KEY]: refreshed.accessToken,
97
- [REFRESH_TOKEN_KEY]: refreshed.refreshToken
98
- }
99
- });
100
- await updateJiraInstallationTokenExpiry({
101
- connectionId: params.connectionId,
102
- tokenExpiresAt: refreshed.expiresAt ?? null,
103
- scopes: refreshed.scopes.length > 0 ? refreshed.scopes : undefined
151
+ const refreshedRefreshToken = refreshed.refreshToken;
152
+ if (!refreshedRefreshToken) throw new JiraTokenUnrefreshableError(params.connectionId);
153
+ const persisted = await params.withCredentialWrite(params.connectionId, async ()=>{
154
+ if (!params.isCurrentCredentialGeneration()) return false;
155
+ await params.secrets.setSecrets({
156
+ workspaceId: params.workspaceId,
157
+ namespace: jiraSecretsNamespace(params.connectionId),
158
+ values: {
159
+ [ACCESS_TOKEN_KEY]: refreshed.accessToken,
160
+ [REFRESH_TOKEN_KEY]: refreshedRefreshToken
161
+ }
162
+ });
163
+ if (!params.isCurrentCredentialGeneration()) return false;
164
+ await updateJiraInstallationTokenExpiry({
165
+ connectionId: params.connectionId,
166
+ tokenExpiresAt: refreshed.expiresAt ?? null,
167
+ scopes: refreshed.scopes.length > 0 ? refreshed.scopes : undefined
168
+ });
169
+ return true;
104
170
  });
171
+ if (!persisted || !params.isCurrentCredentialGeneration()) {
172
+ await params.waitForCredentialWrites(params.connectionId);
173
+ return params.readAccessToken(params.workspaceId, params.connectionId);
174
+ }
105
175
  return refreshed.accessToken;
106
176
  } catch (error) {
107
- if (error instanceof JiraIntegrationProviderError && error.reason === 'access-denied') {
108
- await params.markConnectionError?.({
109
- connectionId: params.connectionId
177
+ if (isTerminalRefreshFailure(error)) {
178
+ await markConnectionError({
179
+ callback: params.markConnectionError,
180
+ connectionId: params.connectionId,
181
+ isCurrentCredentialGeneration: params.isCurrentCredentialGeneration,
182
+ withCredentialWrite: params.withCredentialWrite
110
183
  });
111
184
  }
112
185
  throw error;
113
186
  }
114
187
  }
188
+ function isTerminalRefreshFailure(error) {
189
+ return error instanceof JiraIntegrationProviderError && (error.reason === 'access-denied' || error.reason === 'timeout');
190
+ }
191
+ async function markConnectionError(params) {
192
+ if (!params.isCurrentCredentialGeneration()) return;
193
+ try {
194
+ await params.withCredentialWrite(params.connectionId, async ()=>{
195
+ if (!params.isCurrentCredentialGeneration()) return;
196
+ await params.callback?.({
197
+ connectionId: params.connectionId
198
+ });
199
+ });
200
+ } catch (error) {
201
+ logger().warn({
202
+ err: error,
203
+ connectionId: params.connectionId
204
+ }, 'Jira connection error-state update failed after token refresh failure');
205
+ }
206
+ }
115
207
 
116
208
  //# sourceMappingURL=tokens.js.map