@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.
- package/.turbo/turbo-build.log +17 -2
- package/CHANGELOG.md +36 -0
- package/README.md +58 -2
- package/dist/api/client.d.ts +34 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +131 -0
- package/dist/api/client.js.map +1 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -4
- package/dist/config.js.map +1 -1
- package/dist/core/agent-tools-provider.d.ts +26 -0
- package/dist/core/agent-tools-provider.d.ts.map +1 -0
- package/dist/core/agent-tools-provider.js +130 -0
- package/dist/core/agent-tools-provider.js.map +1 -0
- package/dist/core/agent-tools.d.ts +16 -0
- package/dist/core/agent-tools.d.ts.map +1 -0
- package/dist/core/agent-tools.js +441 -0
- package/dist/core/agent-tools.js.map +1 -0
- package/dist/core/install.d.ts +22 -0
- package/dist/core/install.d.ts.map +1 -1
- package/dist/core/install.js +68 -20
- package/dist/core/install.js.map +1 -1
- package/dist/core/tokens.d.ts +2 -0
- package/dist/core/tokens.d.ts.map +1 -1
- package/dist/core/tokens.js +129 -37
- package/dist/core/tokens.js.map +1 -1
- package/dist/core/webhook-processor.d.ts +15 -0
- package/dist/core/webhook-processor.d.ts.map +1 -0
- package/dist/core/webhook-processor.js +137 -0
- package/dist/core/webhook-processor.js.map +1 -0
- package/dist/core/webhook-registration.d.ts +26 -0
- package/dist/core/webhook-registration.d.ts.map +1 -0
- package/dist/core/webhook-registration.js +165 -0
- package/dist/core/webhook-registration.js.map +1 -0
- package/dist/core/webhook.d.ts +18 -0
- package/dist/core/webhook.d.ts.map +1 -0
- package/dist/core/webhook.js +38 -0
- package/dist/core/webhook.js.map +1 -0
- package/dist/db/db.d.ts +68 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/installations.d.ts +33 -6
- package/dist/db/installations.d.ts.map +1 -1
- package/dist/db/installations.js +79 -7
- package/dist/db/installations.js.map +1 -1
- package/dist/db/schema/installations.d.ts +34 -0
- package/dist/db/schema/installations.d.ts.map +1 -1
- package/dist/db/schema/installations.js +12 -2
- package/dist/db/schema/installations.js.map +1 -1
- package/dist/index.d.ts +34 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +76 -8
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/install.d.ts +22 -0
- package/dist/presentation/routes/install.d.ts.map +1 -1
- package/dist/presentation/routes/install.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts +8 -0
- package/dist/presentation/routes/webhooks.d.ts.map +1 -0
- package/dist/presentation/routes/webhooks.js +108 -0
- package/dist/presentation/routes/webhooks.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +2 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +3 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/refresh-jira-tokens.d.ts +20 -0
- package/dist/temporal/activities/refresh-jira-tokens.d.ts.map +1 -0
- package/dist/temporal/activities/refresh-jira-tokens.js +58 -0
- package/dist/temporal/activities/refresh-jira-tokens.js.map +1 -0
- package/dist/temporal/constants.d.ts +6 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +8 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/worker.d.ts +9 -0
- package/dist/temporal/worker.d.ts.map +1 -0
- package/dist/temporal/worker.js +26 -0
- package/dist/temporal/worker.js.map +1 -0
- package/dist/temporal/workflows/index.bundle.js +25225 -0
- package/dist/temporal/workflows/index.bundle.meta.json +1 -0
- package/dist/temporal/workflows/index.d.ts +2 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +3 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts +2 -0
- package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/refresh-jira-tokens-cron.js +20 -0
- package/dist/temporal/workflows/refresh-jira-tokens-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_jira-token-refresh-state.sql +12 -0
- package/drizzle/meta/0001_snapshot.json +245 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +17 -8
- package/src/api/client.test.ts +236 -9
- package/src/api/client.ts +192 -0
- package/src/config.test.ts +0 -1
- package/src/config.ts +1 -4
- package/src/core/agent-tools-provider.test.ts +299 -0
- package/src/core/agent-tools-provider.ts +175 -0
- package/src/core/agent-tools.ts +450 -0
- package/src/core/install.test.ts +179 -0
- package/src/core/install.ts +85 -19
- package/src/core/tokens-refresh.test.ts +223 -4
- package/src/core/tokens.test.ts +1 -1
- package/src/core/tokens.ts +153 -33
- package/src/core/webhook-processor.test.ts +418 -0
- package/src/core/webhook-processor.ts +166 -0
- package/src/core/webhook-registration.test.ts +375 -0
- package/src/core/webhook-registration.ts +216 -0
- package/src/core/webhook.ts +69 -0
- package/src/db/installations.test.ts +148 -2
- package/src/db/installations.ts +133 -7
- package/src/db/schema/installations.ts +21 -1
- package/src/index.test.ts +42 -0
- package/src/index.ts +148 -5
- package/src/presentation/routes/install.ts +12 -0
- package/src/presentation/routes/webhooks.test.ts +132 -0
- package/src/presentation/routes/webhooks.ts +115 -0
- package/src/temporal/activities/index.ts +5 -0
- package/src/temporal/activities/refresh-jira-tokens.test.ts +156 -0
- package/src/temporal/activities/refresh-jira-tokens.ts +88 -0
- package/src/temporal/constants.ts +8 -0
- package/src/temporal/worker.ts +39 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/src/temporal/workflows/refresh-jira-tokens-cron.ts +19 -0
- package/test/env.ts +0 -1
- package/test/factories/jira-installation.ts +4 -0
- 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":"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"}
|
package/dist/core/install.d.ts
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/core/install.ts"],"names":[],"mappings":"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"}
|
package/dist/core/install.js
CHANGED
|
@@ -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
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
}
|
|
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
|
package/dist/core/install.js.map
CHANGED
|
@@ -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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"}
|
package/dist/core/tokens.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/core/tokens.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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), ()=>
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
108
|
-
await
|
|
109
|
-
|
|
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
|