diff --git a/package-lock.json b/package-lock.json index c8e4375a90..09904bd5b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8504,16 +8504,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "dev": true, @@ -10295,6 +10285,16 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "dev": true, diff --git a/package.json b/package.json index e72d6bcfbd..fd14965b7c 100644 --- a/package.json +++ b/package.json @@ -4175,9 +4175,9 @@ }, "overrides": { "mocha": { - "diff": "7.0.0", - "serialize-javascript": "6.0.1" + "diff": "7.0.0" }, + "serialize-javascript": "7.0.4", "elliptic": "6.6.1" }, "resolutions": { diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index 3992526ad4..96adb30967 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -100,7 +100,7 @@ declare module 'vscode' { readonly command?: string; }; - readonly sessionOptions: ReadonlyArray<{ optionId: string; value: ChatSessionProviderOptionItem }>; + readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>; } /** @@ -116,7 +116,7 @@ declare module 'vscode' { * * @param sessionResource The resource of the chat session being forked. * @param request The request turn that marks the fork point. The forked session includes all turns - * upto this request turn and includes this request turn itself. If undefined, fork the full session. + * upto this request turn and excludes this request turn itself. If undefined, fork the full session. * @param token A cancellation token. * @returns The forked session item. */ @@ -396,9 +396,12 @@ declare module 'vscode' { /** * Options configured for this session as key-value pairs. * Keys correspond to option group IDs (e.g., 'models', 'subagents'). + * Values can be either: + * - A string (the option item ID) for backwards compatibility + * - A ChatSessionProviderOptionItem object to include metadata like locked state * TODO: Strongly type the keys */ - readonly options?: Record; + readonly options?: Record; /** * Callback invoked by the editor for a currently running response. This allows the session to push items for the @@ -429,7 +432,7 @@ declare module 'vscode' { * * @param sessionResource The resource of the chat session being forked. * @param request The request turn that marks the fork point. The forked session includes all turns - * until this request turn and includes this request turn itself. If undefined, fork the full session. + * upto this request turn and excludes this request turn itself. If undefined, fork the full session. * @param token A cancellation token. * @returns The forked session item. */ @@ -456,7 +459,7 @@ declare module 'vscode' { /** * The new value assigned to the option. When `undefined`, the option is cleared. */ - readonly value: ChatSessionProviderOptionItem; + readonly value: string | ChatSessionProviderOptionItem; }>; } @@ -489,7 +492,7 @@ declare module 'vscode' { * @return The {@link ChatSession chat session} associated with the given URI. */ provideChatSessionContent(resource: Uri, token: CancellationToken, context: { - readonly sessionOptions: ReadonlyArray<{ optionId: string; value: ChatSessionProviderOptionItem }>; + readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>; }): Thenable | ChatSession; /** @@ -514,7 +517,7 @@ declare module 'vscode' { /** * The new value assigned to the option. When `undefined`, the option is cleared. */ - readonly value: ChatSessionProviderOptionItem | undefined; + readonly value: string | undefined; } export namespace chat { @@ -544,7 +547,7 @@ declare module 'vscode' { * The initial option selections for the session, provided with the first request. * Contains the options the user selected (or defaults) before the session was created. */ - readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: ChatSessionProviderOptionItem }>; + readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>; } export interface ChatSessionCapabilities { @@ -667,6 +670,6 @@ declare module 'vscode' { * * Keys correspond to option group IDs (e.g., 'models', 'subagents'). */ - readonly newSessionOptions?: Record; + readonly newSessionOptions?: Record; } } diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index 7b0c608142..e09e458a89 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -1430,9 +1430,9 @@ export class GitHubRepository extends Disposable { } ret.push( - ...result.data.repository.mentionableUsers.nodes.map(node => { + ...await Promise.all(result.data.repository.mentionableUsers.nodes.map(node => { return parseAccount(node, this); - }), + })), ); hasNextPage = result.data.repository.mentionableUsers.pageInfo.hasNextPage; @@ -1457,7 +1457,7 @@ export class GitHubRepository extends Disposable { login, }, }); - return parseGraphQLUser(data, this); + return await parseGraphQLUser(data, this); } catch (e) { // Ignore cases where the user doesn't exist if (!(e.message as (string | undefined))?.startsWith('GraphQL error: Could not resolve to a User with the login of')) { @@ -1518,9 +1518,9 @@ export class GitHubRepository extends Disposable { const users = (result.data as AssignableUsersResponse).repository?.assignableUsers ?? (result.data as SuggestedActorsResponse).repository?.suggestedActors; ret.push( - ...(users?.nodes.map(node => { + ...(await Promise.all(users?.nodes.map(node => { return parseAccount(node, this); - }) || []), + }) || [])), ); hasNextPage = users?.pageInfo.hasNextPage; @@ -1616,9 +1616,9 @@ export class GitHubRepository extends Disposable { }, }); - result.data.organization.teams.nodes.forEach(node => { + for (const node of result.data.organization.teams.nodes) { const team: ITeam = { - avatarUrl: getAvatarWithEnterpriseFallback(node.avatarUrl, undefined, this.remote.isEnterprise), + avatarUrl: await getAvatarWithEnterpriseFallback(node.avatarUrl, undefined, this.remote.isEnterprise), name: node.name, url: node.url, slug: node.slug, @@ -1626,7 +1626,7 @@ export class GitHubRepository extends Disposable { org: remote.owner }; orgTeams.push({ ...team, repositoryNames: node.repositories.nodes.map(repo => repo.name) }); - }); + } hasNextPage = result.data.organization.teams.pageInfo.hasNextPage; after = result.data.organization.teams.pageInfo.endCursor; @@ -1671,9 +1671,9 @@ export class GitHubRepository extends Disposable { } ret.push( - ...result.data.repository.pullRequest.participants.nodes.map(node => { + ...await Promise.all(result.data.repository.pullRequest.participants.nodes.map(node => { return parseAccount(node, this); - }), + })), ); } catch (e) { Logger.debug(`Unable to fetch participants from a PullRequest: ${e}`, this.id); @@ -1767,7 +1767,7 @@ export class GitHubRepository extends Disposable { statuses: [] }; } else { - const dedupedStatuses = this.deduplicateStatusChecks(statusCheckRollup.contexts.nodes.map(context => { + const dedupedStatuses = this.deduplicateStatusChecks(await Promise.all(statusCheckRollup.contexts.nodes.map(async context => { if (isCheckRun(context)) { return { id: context.id, @@ -1775,7 +1775,7 @@ export class GitHubRepository extends Disposable { url: context.checkSuite?.app?.url, avatarUrl: context.checkSuite?.app?.logoUrl && - getAvatarWithEnterpriseFallback( + await getAvatarWithEnterpriseFallback( context.checkSuite.app.logoUrl, undefined, this.remote.isEnterprise, @@ -1795,7 +1795,7 @@ export class GitHubRepository extends Disposable { databaseId: undefined, url: context.targetUrl ?? undefined, avatarUrl: context.avatarUrl - ? getAvatarWithEnterpriseFallback(context.avatarUrl, undefined, this.remote.isEnterprise) + ? await getAvatarWithEnterpriseFallback(context.avatarUrl, undefined, this.remote.isEnterprise) : undefined, state: this.mapStateAsCheckState(context.state), description: context.description, @@ -1807,7 +1807,7 @@ export class GitHubRepository extends Disposable { isCheckRun: false, }; } - })); + }))); checks = { state: this.computeOverallCheckState(dedupedStatuses), diff --git a/src/github/issueModel.ts b/src/github/issueModel.ts index 7fb5c830b1..b4d8bd8941 100644 --- a/src/github/issueModel.ts +++ b/src/github/issueModel.ts @@ -281,7 +281,7 @@ export class IssueModel extends Disposable { }); this._onDidChange.fire({ timeline: true }); - return parseGraphQlIssueComment(data!.addComment.commentEdge.node, this.githubRepository); + return await parseGraphQlIssueComment(data!.addComment.commentEdge.node, this.githubRepository); } async editIssueComment(comment: IComment, text: string): Promise { @@ -299,7 +299,7 @@ export class IssueModel extends Disposable { }); this._onDidChange.fire({ timeline: true }); - return parseGraphQlIssueComment(data!.updateIssueComment.issueComment, this.githubRepository); + return await parseGraphQlIssueComment(data!.updateIssueComment.issueComment, this.githubRepository); } catch (e) { throw new Error(formatError(e)); } diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index f37f2feda8..2875a72ab0 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -564,7 +564,7 @@ export class PullRequestModel extends IssueModel implements IPullRe this.hasPendingReview = false; await this.updateDraftModeContext(); - const reviewEvent = parseGraphQLReviewEvent(data!.submitPullRequestReview.pullRequestReview, this.githubRepository); + const reviewEvent = await parseGraphQLReviewEvent(data!.submitPullRequestReview.pullRequestReview, this.githubRepository); const threadWithComment = (this._reviewThreadsCache ?? []).find(thread => thread.comments.length ? (thread.comments[0].pullRequestReviewId === reviewEvent.id) : undefined, @@ -649,7 +649,7 @@ export class PullRequestModel extends IssueModel implements IPullRe }); const { comments, databaseId } = data!.deletePullRequestReview.pullRequestReview; - const deletedReviewComments = comments.nodes.map(comment => parseGraphQLComment(comment, false, false, this.githubRepository)); + const deletedReviewComments = await Promise.all(comments.nodes.map(comment => parseGraphQLComment(comment, false, false, this.githubRepository))); // Update local state: remove all draft comments (and their threads if emptied) that belonged to the deleted review const deletedCommentIds = new Set(deletedReviewComments.map(c => c.id)); @@ -772,7 +772,7 @@ export class PullRequestModel extends IssueModel implements IPullRe } const thread = data.addPullRequestReviewThread.thread; - const newThread = parseGraphQLReviewThread(thread, this.githubRepository); + const newThread = await parseGraphQLReviewThread(thread, this.githubRepository); if (!this._reviewThreadsCache) { this._reviewThreadsCache = []; } @@ -824,7 +824,7 @@ export class PullRequestModel extends IssueModel implements IPullRe } const { comment } = data.addPullRequestReviewComment; - const newComment = parseGraphQLComment(comment, false, false, this.githubRepository); + const newComment = await parseGraphQLComment(comment, false, false, this.githubRepository); if (isSingleComment) { newComment.isDraft = false; @@ -1023,7 +1023,7 @@ export class PullRequestModel extends IssueModel implements IPullRe throw new Error('Editing review comment failed.'); } - const newComment = parseGraphQLComment( + const newComment = await parseGraphQLComment( data.updatePullRequestReviewComment.pullRequestReviewComment, !!comment.isResolved, !!comment.isOutdated, @@ -1341,7 +1341,7 @@ export class PullRequestModel extends IssueModel implements IPullRe return []; } - const reviewers: (IAccount | ITeam)[] = parseGraphQLReviewers(data, githubRepository); + const reviewers: (IAccount | ITeam)[] = await parseGraphQLReviewers(data, githubRepository); if (this.reviewers?.length !== reviewers.length || (this.reviewers.some(r => !reviewers.some(rr => rr.id === r.id)))) { this.reviewers = reviewers; this._onDidChange.fire({ reviewers: true }); @@ -1452,8 +1452,8 @@ export class PullRequestModel extends IssueModel implements IPullRe }); } - private setReviewThreadCacheFromRaw(raw: ReviewThread[]): IReviewThread[] { - const reviewThreads: IReviewThread[] = raw.map(thread => parseGraphQLReviewThread(thread, this.githubRepository)); + private async setReviewThreadCacheFromRaw(raw: ReviewThread[]): Promise { + const reviewThreads: IReviewThread[] = await Promise.all(raw.map(thread => parseGraphQLReviewThread(thread, this.githubRepository))); const oldReviewThreads = this._reviewThreadsCache ?? []; this._reviewThreadsCache = reviewThreads; this.diffThreads(oldReviewThreads, reviewThreads); @@ -1556,7 +1556,7 @@ export class PullRequestModel extends IssueModel implements IPullRe per_page: 100 }); const workStartedInitiator = (timeline.data.find(event => event.event === 'copilot_work_started') as { actor: RestAccount } | undefined)?.actor; - return workStartedInitiator ? [parseAccount(workStartedInitiator, this.githubRepository)] : []; + return workStartedInitiator ? [await parseAccount(workStartedInitiator, this.githubRepository)] : []; } protected override getUpdatesQuery(schema: any): any { @@ -2105,7 +2105,7 @@ export class PullRequestModel extends IssueModel implements IPullRe const index = this._reviewThreadsCache?.findIndex(thread => thread.id === threadId) ?? -1; if (index > -1) { - const thread = parseGraphQLReviewThread(data.resolveReviewThread.thread, this.githubRepository); + const thread = await parseGraphQLReviewThread(data.resolveReviewThread.thread, this.githubRepository); this._reviewThreadsCache?.splice(index, 1, thread); this._onDidChangeReviewThreads.fire({ added: [], changed: [thread], removed: [] }); } @@ -2148,7 +2148,7 @@ export class PullRequestModel extends IssueModel implements IPullRe const index = this._reviewThreadsCache?.findIndex(thread => thread.id === threadId) ?? -1; if (index > -1) { - const thread = parseGraphQLReviewThread(data.unresolveReviewThread.thread, this.githubRepository); + const thread = await parseGraphQLReviewThread(data.unresolveReviewThread.thread, this.githubRepository); this._reviewThreadsCache?.splice(index, 1, thread); this._onDidChangeReviewThreads.fire({ added: [], changed: [thread], removed: [] }); } diff --git a/src/github/utils.ts b/src/github/utils.ts index 24c42a4508..b09f680881 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as crypto from 'crypto'; import * as OctokitTypes from '@octokit/types'; import * as vscode from 'vscode'; import { OctokitCommon } from './common'; @@ -510,7 +509,7 @@ export function convertGraphQLEventType(text: string) { } } -export function parseGraphQLReviewThread(thread: GraphQL.ReviewThread, githubRepository: GitHubRepository): IReviewThread { +export async function parseGraphQLReviewThread(thread: GraphQL.ReviewThread, githubRepository: GitHubRepository): Promise { return { id: thread.id, prReviewDatabaseId: thread.comments.edges && thread.comments.edges.length ? @@ -526,12 +525,12 @@ export function parseGraphQLReviewThread(thread: GraphQL.ReviewThread, githubRep originalEndLine: thread.originalLine, diffSide: thread.diffSide, isOutdated: thread.isOutdated, - comments: thread.comments.nodes.map(comment => parseGraphQLComment(comment, thread.isResolved, thread.isOutdated, githubRepository)), + comments: await Promise.all(thread.comments.nodes.map(comment => parseGraphQLComment(comment, thread.isResolved, thread.isOutdated, githubRepository))), subjectType: thread.subjectType ?? SubjectType.LINE }; } -export function parseGraphQLComment(comment: GraphQL.ReviewComment, isResolved: boolean, isOutdated: boolean, githubRepository: GitHubRepository): IComment { +export async function parseGraphQLComment(comment: GraphQL.ReviewComment, isResolved: boolean, isOutdated: boolean, githubRepository: GitHubRepository): Promise { const specialAuthor = COPILOT_ACCOUNTS[comment.author?.login ?? '']; const c: IComment = { id: comment.databaseId, @@ -548,7 +547,7 @@ export function parseGraphQLComment(comment: GraphQL.ReviewComment, isResolved: commitId: comment.commit.oid, originalPosition: comment.originalPosition, originalCommitId: comment.originalCommit && comment.originalCommit.oid, - user: comment.author ? parseAccount(comment.author, githubRepository) : undefined, + user: comment.author ? await parseAccount(comment.author, githubRepository) : undefined, createdAt: comment.createdAt, htmlUrl: comment.url, graphNodeId: comment.id, @@ -565,7 +564,7 @@ export function parseGraphQLComment(comment: GraphQL.ReviewComment, isResolved: return c; } -export function parseGraphQlIssueComment(comment: GraphQL.IssueComment, githubRepository: GitHubRepository): IComment { +export async function parseGraphQlIssueComment(comment: GraphQL.IssueComment, githubRepository: GitHubRepository): Promise { return { id: comment.databaseId, url: comment.url, @@ -574,7 +573,7 @@ export function parseGraphQlIssueComment(comment: GraphQL.IssueComment, githubRe bodyHTML: comment.bodyHTML, canEdit: comment.viewerCanDelete, canDelete: comment.viewerCanDelete, - user: parseAccount(comment.author, githubRepository), + user: await parseAccount(comment.author, githubRepository), createdAt: comment.createdAt, htmlUrl: comment.url, graphNodeId: comment.id, @@ -644,10 +643,10 @@ export interface GraphQLAccount { __typename: string; } -export function parseAccount( +export async function parseAccount( author: GraphQLAccount | RestAccount | null, githubRepository?: GitHubRepository, -): IAccount { +): Promise { if (author) { let avatarUrl: string; let id: string; @@ -669,7 +668,7 @@ export function parseAccount( // In some places, Copilot comes in as a user, and in others as a bot - const finalAvatarUrl = githubRepository ? getAvatarWithEnterpriseFallback(avatarUrl, undefined, githubRepository.remote.isEnterprise) : avatarUrl; + const finalAvatarUrl = githubRepository ? await getAvatarWithEnterpriseFallback(avatarUrl, undefined, githubRepository.remote.isEnterprise) : avatarUrl; return { login: author.login, @@ -691,43 +690,43 @@ export function parseAccount( } } -function parseTeam(team: GraphQL.Team, githubRepository: GitHubRepository): ITeam { +async function parseTeam(team: GraphQL.Team, githubRepository: GitHubRepository): Promise { return { name: team.name, url: team.url, - avatarUrl: getAvatarWithEnterpriseFallback(team.avatarUrl, undefined, githubRepository.remote.isEnterprise), + avatarUrl: await getAvatarWithEnterpriseFallback(team.avatarUrl, undefined, githubRepository.remote.isEnterprise), id: team.id, org: githubRepository.remote.owner, slug: team.slug }; } -export function parseGraphQLReviewers(data: GraphQL.GetReviewRequestsResponse, repository: GitHubRepository): (IAccount | ITeam)[] { +export async function parseGraphQLReviewers(data: GraphQL.GetReviewRequestsResponse, repository: GitHubRepository): Promise<(IAccount | ITeam)[]> { if (!data.repository) { return []; } const reviewers: (IAccount | ITeam)[] = []; for (const reviewer of data.repository.pullRequest.reviewRequests.nodes) { if (GraphQL.isTeam(reviewer.requestedReviewer)) { - const team: ITeam = parseTeam(reviewer.requestedReviewer, repository); + const team: ITeam = await parseTeam(reviewer.requestedReviewer, repository); reviewers.push(team); } else if (GraphQL.isAccount(reviewer.requestedReviewer) || GraphQL.isBot(reviewer.requestedReviewer)) { - const account: IAccount = parseAccount(reviewer.requestedReviewer, repository); + const account: IAccount = await parseAccount(reviewer.requestedReviewer, repository); reviewers.push(account); } } return reviewers; } -function parseActor( +async function parseActor( author: { login: string; url: string; avatarUrl: string; } | null, githubRepository: GitHubRepository, -): IActor { +): Promise { if (author) { return { login: author.login, url: author.url, - avatarUrl: getAvatarWithEnterpriseFallback(author.avatarUrl, undefined, githubRepository.remote.isEnterprise), + avatarUrl: await getAvatarWithEnterpriseFallback(author.avatarUrl, undefined, githubRepository.remote.isEnterprise), }; } else { return { @@ -847,7 +846,7 @@ export async function parseGraphQLPullRequest( head: parseRef(graphQLPullRequest.headRef?.name ?? graphQLPullRequest.headRefName, graphQLPullRequest.headRefOid, graphQLPullRequest.headRepository), isRemoteBaseDeleted: !graphQLPullRequest.baseRef, base: parseRef(graphQLPullRequest.baseRef?.name ?? graphQLPullRequest.baseRefName, graphQLPullRequest.baseRefOid, graphQLPullRequest.baseRepository), - user: parseAccount(graphQLPullRequest.author, githubRepository), + user: await parseAccount(graphQLPullRequest.author, githubRepository), merged: graphQLPullRequest.merged, mergeable: parseMergeability(graphQLPullRequest.mergeable, graphQLPullRequest.mergeStateStatus), mergeQueueEntry: parseMergeQueueEntry(graphQLPullRequest.mergeQueueEntry), @@ -858,11 +857,11 @@ export async function parseGraphQLPullRequest( viewerCanUpdate: graphQLPullRequest.viewerCanUpdate, labels: graphQLPullRequest.labels.nodes, isDraft: graphQLPullRequest.isDraft, - suggestedReviewers: parseSuggestedReviewers(graphQLPullRequest.suggestedReviewers), - comments: parseComments(graphQLPullRequest.comments?.nodes, githubRepository), + suggestedReviewers: await parseSuggestedReviewers(graphQLPullRequest.suggestedReviewers), + comments: await parseComments(graphQLPullRequest.comments?.nodes, githubRepository), projectItems: parseProjectItems(graphQLPullRequest.projectItems?.nodes), milestone: parseMilestone(graphQLPullRequest.milestone), - assignees: graphQLPullRequest.assignees?.nodes.map(assignee => parseAccount(assignee, githubRepository)), + assignees: graphQLPullRequest.assignees?.nodes ? await Promise.all(graphQLPullRequest.assignees.nodes.map(assignee => parseAccount(assignee, githubRepository))) : undefined, commits: parseCommits(graphQLPullRequest.commits.nodes), reactionCount: graphQLPullRequest.reactions.totalCount, reactions: parseGraphQLReaction(graphQLPullRequest.reactionGroups), @@ -932,7 +931,7 @@ function parseCommits(commits: { commit: { message: string; }; }[]): { message: }); } -function parseComments(comments: GraphQL.AbbreviatedIssueComment[] | undefined, githubRepository: GitHubRepository) { +async function parseComments(comments: GraphQL.AbbreviatedIssueComment[] | undefined, githubRepository: GitHubRepository) { if (!comments) { return; } @@ -945,7 +944,7 @@ function parseComments(comments: GraphQL.AbbreviatedIssueComment[] | undefined, }[] = []; for (const comment of comments) { parsedComments.push({ - author: parseAccount(comment.author, githubRepository), + author: await parseAccount(comment.author, githubRepository), body: comment.body, databaseId: comment.databaseId, reactionCount: comment.reactions.totalCount, @@ -970,24 +969,24 @@ export async function parseGraphQLIssue(issue: GraphQL.Issue, githubRepository: titleHTML: issue.titleHTML, createdAt: issue.createdAt, updatedAt: issue.updatedAt, - assignees: issue.assignees?.nodes.map(assignee => parseAccount(assignee, githubRepository)), - user: parseAccount(issue.author, githubRepository), + assignees: issue.assignees?.nodes ? await Promise.all(issue.assignees.nodes.map(assignee => parseAccount(assignee, githubRepository))) : undefined, + user: await parseAccount(issue.author, githubRepository), labels: issue.labels.nodes, milestone: parseMilestone(issue.milestone), repositoryName: issue.repository?.name ?? githubRepository.remote.repositoryName, repositoryOwner: issue.repository?.owner.login ?? githubRepository.remote.owner, repositoryUrl: issue.repository?.url ?? githubRepository.remote.url, projectItems: parseProjectItems(issue.projectItems?.nodes), - comments: issue.comments.nodes?.map(comment => parseIssueComment(comment, githubRepository)), + comments: issue.comments.nodes ? await Promise.all(issue.comments.nodes.map(comment => parseIssueComment(comment, githubRepository))) : undefined, reactionCount: issue.reactions.totalCount, reactions: parseGraphQLReaction(issue.reactionGroups), commentCount: issue.comments.totalCount }; } -function parseIssueComment(comment: GraphQL.AbbreviatedIssueComment, githubRepository: GitHubRepository): IIssueComment { +async function parseIssueComment(comment: GraphQL.AbbreviatedIssueComment, githubRepository: GitHubRepository): Promise { return { - author: parseAccount(comment.author, githubRepository), + author: await parseAccount(comment.author, githubRepository), body: comment.body, databaseId: comment.databaseId, reactionCount: comment.reactions.totalCount, @@ -995,20 +994,20 @@ function parseIssueComment(comment: GraphQL.AbbreviatedIssueComment, githubRepos }; } -function parseSuggestedReviewers( +async function parseSuggestedReviewers( suggestedReviewers: GraphQL.SuggestedReviewerResponse[] | undefined, -): ISuggestedReviewer[] { +): Promise { if (!suggestedReviewers) { return []; } - const ret: ISuggestedReviewer[] = suggestedReviewers.map(suggestedReviewer => { - const account = parseAccount(suggestedReviewer.reviewer, undefined); + const ret: ISuggestedReviewer[] = await Promise.all(suggestedReviewers.map(async suggestedReviewer => { + const account = await parseAccount(suggestedReviewer.reviewer, undefined); return { ...account, isAuthor: suggestedReviewer.isAuthor, isCommenter: suggestedReviewer.isCommenter }; - }); + })); return ret.sort(loginComparator); } @@ -1030,18 +1029,18 @@ export function teamComparator(a: ITeam, b: ITeam) { return aKey.localeCompare(bKey, 'en', { sensitivity: 'accent' }); } -export function parseGraphQLReviewEvent( +export async function parseGraphQLReviewEvent( review: GraphQL.SubmittedReview, githubRepository: GitHubRepository, -): Common.ReviewEvent { +): Promise { return { event: Common.EventType.Reviewed, - comments: review.comments.nodes.map(comment => parseGraphQLComment(comment, false, false, githubRepository)).filter(c => !c.inReplyToId), + comments: (await Promise.all(review.comments.nodes.map(comment => parseGraphQLComment(comment, false, false, githubRepository)))).filter(c => !c.inReplyToId), submittedAt: review.submittedAt, body: review.body, bodyHTML: review.bodyHTML, htmlUrl: review.url, - user: parseAccount(review.author, githubRepository), + user: await parseAccount(review.author, githubRepository), authorAssociation: review.authorAssociation, state: review.state, id: review.databaseId, @@ -1186,7 +1185,7 @@ export async function parseCombinedTimelineEvents( htmlUrl: commentEvent.url, body: commentEvent.body, bodyHTML: commentEvent.bodyHTML, - user: parseAccount(commentEvent.author, githubRepository), + user: await parseAccount(commentEvent.author, githubRepository), event: type, canEdit: commentEvent.viewerCanUpdate, canDelete: commentEvent.viewerCanDelete, @@ -1205,7 +1204,7 @@ export async function parseCombinedTimelineEvents( body: reviewEvent.body, bodyHTML: reviewEvent.bodyHTML, htmlUrl: reviewEvent.url, - user: parseAccount(reviewEvent.author, githubRepository), + user: await parseAccount(reviewEvent.author, githubRepository), authorAssociation: reviewEvent.authorAssociation, state: reviewEvent.state, id: reviewEvent.databaseId, @@ -1219,7 +1218,7 @@ export async function parseCombinedTimelineEvents( event: type, sha: commitEv.commit.oid, author: commitEv.commit.author.user - ? parseAccount(commitEv.commit.author.user, githubRepository) + ? await parseAccount(commitEv.commit.author.user, githubRepository) : { login: commitEv.commit.committer.name }, htmlUrl: commitEv.url, message: commitEv.commit.message, @@ -1233,7 +1232,7 @@ export async function parseCombinedTimelineEvents( addTimelineEvent({ id: mergeEv.id, event: type, - user: parseActor(mergeEv.actor, githubRepository), + user: await parseActor(mergeEv.actor, githubRepository), createdAt: mergeEv.createdAt, mergeRef: mergeEv.mergeRef.name, sha: mergeEv.commit.oid, @@ -1248,8 +1247,8 @@ export async function parseCombinedTimelineEvents( addTimelineEvent({ id: assignEv.id, event: type, - assignees: [parseAccount(assignEv.user, githubRepository)], - actor: parseAccount(assignEv.actor), + assignees: [await parseAccount(assignEv.user, githubRepository)], + actor: await parseAccount(assignEv.actor), createdAt: assignEv.createdAt, }); break; @@ -1259,8 +1258,8 @@ export async function parseCombinedTimelineEvents( normalizedEvents.push({ id: unassignEv.id, event: type, - unassignees: [parseAccount(unassignEv.user, githubRepository)], - actor: parseAccount(unassignEv.actor), + unassignees: [await parseAccount(unassignEv.user, githubRepository)], + actor: await parseAccount(unassignEv.actor), createdAt: unassignEv.createdAt, }); break; @@ -1270,7 +1269,7 @@ export async function parseCombinedTimelineEvents( addTimelineEvent({ id: deletedEv.id, event: type, - actor: parseAccount(deletedEv.actor, githubRepository), + actor: await parseAccount(deletedEv.actor, githubRepository), createdAt: deletedEv.createdAt, headRef: deletedEv.headRefName, }); @@ -1287,7 +1286,7 @@ export async function parseCombinedTimelineEvents( addTimelineEvent({ id: crossRefEv.id, event: type, - actor: parseAccount(crossRefEv.actor, githubRepository), + actor: await parseAccount(crossRefEv.actor, githubRepository), createdAt: crossRefEv.createdAt, source: { url: crossRefEv.source.url, @@ -1307,7 +1306,7 @@ export async function parseCombinedTimelineEvents( addTimelineEvent({ id: closedEv.id, event: type, - actor: parseAccount(closedEv.actor, githubRepository), + actor: await parseAccount(closedEv.actor, githubRepository), createdAt: closedEv.createdAt, }); break; @@ -1317,7 +1316,7 @@ export async function parseCombinedTimelineEvents( addTimelineEvent({ id: reopenedEv.id, event: type, - actor: parseAccount(reopenedEv.actor, githubRepository), + actor: await parseAccount(reopenedEv.actor, githubRepository), createdAt: reopenedEv.createdAt, }); break; @@ -1327,7 +1326,7 @@ export async function parseCombinedTimelineEvents( addTimelineEvent({ id: baseRefChangedEv.id, event: type, - actor: parseAccount(baseRefChangedEv.actor, githubRepository), + actor: await parseAccount(baseRefChangedEv.actor, githubRepository), createdAt: baseRefChangedEv.createdAt, currentRefName: baseRefChangedEv.currentRefName, previousRefName: baseRefChangedEv.previousRefName, @@ -1346,11 +1345,11 @@ export async function parseCombinedTimelineEvents( return normalizedEvents; } -export function parseGraphQLUser(user: GraphQL.UserResponse, githubRepository: GitHubRepository): User { +export async function parseGraphQLUser(user: GraphQL.UserResponse, githubRepository: GitHubRepository): Promise { return { login: user.user.login, name: user.user.name, - avatarUrl: getAvatarWithEnterpriseFallback(user.user.avatarUrl ?? '', undefined, githubRepository.remote.isEnterprise), + avatarUrl: await getAvatarWithEnterpriseFallback(user.user.avatarUrl ?? '', undefined, githubRepository.remote.isEnterprise), url: user.user.url, bio: user.user.bio, company: user.user.company, @@ -1697,7 +1696,22 @@ export function generateGravatarUrl(gravatarId: string | undefined, size: number return !!gravatarId ? `https://www.gravatar.com/avatar/${gravatarId}?s=${size}&d=retro` : undefined; } -export function getAvatarWithEnterpriseFallback(avatarUrl: string, email: string | undefined, isEnterpriseRemote: boolean): string | undefined { +// Use the Node.js built-in crypto module (not the browserify polyfill) to avoid md5.js/hash.js +// bundled dependencies. In browser/webworker contexts Node.js crypto is unavailable, so we +// fall back to SubtleCrypto. +async function sha256Hex(data: string): Promise { + try { + return (require(/* webpackIgnore: true */ 'crypto') as typeof import('crypto')).createHash('sha256').update(data).digest('hex'); + } catch { + // Browser/webworker context: use SubtleCrypto + const msgBuffer = new TextEncoder().encode(data); + const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + } +} + +export async function getAvatarWithEnterpriseFallback(avatarUrl: string, email: string | undefined, isEnterpriseRemote: boolean): Promise { // For non-enterprise, always use the provided avatarUrl if (!isEnterpriseRemote) { @@ -1710,8 +1724,7 @@ export function getAvatarWithEnterpriseFallback(avatarUrl: string, email: string } // Only fallback to Gravatar if no avatarUrl is available and email is provided - const gravatarUrl = email ? generateGravatarUrl( - crypto.createHash('sha256').update(email.trim().toLowerCase()).digest('hex')) : undefined; + const gravatarUrl = email ? generateGravatarUrl(await sha256Hex(email.trim().toLowerCase())) : undefined; return gravatarUrl; } diff --git a/webpack.config.js b/webpack.config.js index 074334d616..32adea8b9b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -141,7 +141,7 @@ async function getWebviewConfig(mode, env, entry) { resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.svg'], fallback: { - crypto: require.resolve("crypto-browserify"), + crypto: false, path: require.resolve('path-browserify'), stream: require.resolve("stream-browserify"), http: require.resolve("stream-http") @@ -365,7 +365,7 @@ async function getExtensionConfig(target, mode, env) { fallback: target === 'webworker' ? { - crypto: require.resolve("crypto-browserify"), + crypto: false, path: require.resolve('path-browserify'), stream: require.resolve("stream-browserify"), url: false,