From bbd57b8f8e8a88feef9c51c4293d5fa989715bc2 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:03:55 -0700 Subject: [PATCH] Switch from bot PAT to GitHub App token via Azure Key Vault --- .github/workflows/daily.yml | 17 +- .github/workflows/deploy.yml | 8 +- .github/workflows/update-ts-version-tags.yml | 3 +- .github/workflows/version-or-publish.yml | 34 +- packages/mergebot/README.md | 2 +- packages/mergebot/package.json | 2 + packages/mergebot/src/execute-pr-actions.ts | 31 +- packages/mergebot/src/functions/api.ts | 3 +- .../src/functions/discussions-trigger.ts | 6 +- packages/mergebot/src/functions/pr-trigger.ts | 5 +- packages/mergebot/src/github-auth.ts | 77 ++++ packages/mergebot/src/graphql-client.ts | 29 +- packages/mergebot/src/pr-info.ts | 6 +- packages/mergebot/src/util/github-app-auth.ts | 370 ++++++++++++++++ packages/mergebot/src/util/util.ts | 17 +- pnpm-lock.yaml | 397 +++++++++++++++++- 16 files changed, 946 insertions(+), 61 deletions(-) create mode 100644 packages/mergebot/src/github-auth.ts create mode 100644 packages/mergebot/src/util/github-app-auth.ts diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 6813225dde..bbad81df22 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -5,9 +5,16 @@ on: schedule: - cron: '37 */6 * * *' +permissions: + contents: read + id-token: write + jobs: build: runs-on: ubuntu-latest + environment: + name: azure + deployment: false steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -17,8 +24,12 @@ jobs: - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - run: pnpm install - run: pnpm run build - - # Go through all open PRs and run the bot over them + - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - run: node packages/mergebot/dist/run.js env: - BOT_AUTH_TOKEN: ${{ secrets.TYPESCRIPT_BOT_TOKEN }} + GITHUB_APP_CLIENT_ID: ${{ vars.TYPESCRIPT_AUTOMATION_GITHUB_APP_CLIENT_ID }} + GITHUB_APP_KEY_VAULT_KEY_ID: ${{ vars.TYPESCRIPT_AUTOMATION_GITHUB_APP_KEY_ID }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3f85e24107..3cb03fe5e7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -59,7 +59,7 @@ jobs: - "1ES.ImageOverride=azure-linux-3" needs: build environment: - name: 'Production' + name: azure permissions: contents: read id-token: write @@ -72,9 +72,9 @@ jobs: - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - name: Upload blob run: az storage blob upload -f ${{ env.FUNCTION_ZIP_NAME }} --account-name ${{ env.STORAGE_ACCOUNT_NAME }} -c ${{ env.STORAGE_CONTAINER_NAME }} -n ${{ env.FUNCTION_ZIP_NAME }} --overwrite true --auth-mode login diff --git a/.github/workflows/update-ts-version-tags.yml b/.github/workflows/update-ts-version-tags.yml index 4d0bd59b0f..3a56535b9f 100644 --- a/.github/workflows/update-ts-version-tags.yml +++ b/.github/workflows/update-ts-version-tags.yml @@ -46,5 +46,4 @@ jobs: run: node packages/retag/dist/index.js --path ../DefinitelyTyped env: NPM_TOKEN: ${{ secrets.NPM_RETAG_TOKEN }} - GH_API_TOKEN: ${{ secrets.GH_API_TOKEN }} - \ No newline at end of file + diff --git a/.github/workflows/version-or-publish.yml b/.github/workflows/version-or-publish.yml index d1ea4d3dc8..09ed128192 100644 --- a/.github/workflows/version-or-publish.yml +++ b/.github/workflows/version-or-publish.yml @@ -11,7 +11,8 @@ on: - main permissions: - pull-requests: write + contents: read + id-token: write concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -22,11 +23,14 @@ env: jobs: publish: runs-on: ubuntu-latest + environment: + name: azure + deployment: false steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - token: ${{ secrets.TYPESCRIPT_BOT_TOKEN }} + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 20 @@ -35,9 +39,33 @@ jobs: - run: pnpm install - run: pnpm build - run: pnpm test + - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + - name: Create GitHub App token + id: app-token + uses: microsoft/create-github-app-token-via-key-vault@5ba0d436e9c3cac52feff4d1f2f66f9698ce4a2d # v1 + with: + client-id: ${{ vars.TYPESCRIPT_AUTOMATION_GITHUB_APP_CLIENT_ID }} + key-id: ${{ vars.TYPESCRIPT_AUTOMATION_GITHUB_APP_KEY_ID }} + owner: microsoft + repositories: DefinitelyTyped-tools + permission-contents: write + permission-pull-requests: write + - name: Configure git for GitHub App token + shell: bash + env: + GITHUB_APP_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -euo pipefail + basic_auth="$(node -e 'process.stdout.write(Buffer.from("x-access-token:" + process.env.GITHUB_APP_TOKEN).toString("base64"))')" + echo "::add-mask::$basic_auth" + git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}" - uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0 with: publish: pnpm ci:publish env: - GITHUB_TOKEN: ${{ secrets.TYPESCRIPT_BOT_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/packages/mergebot/README.md b/packages/mergebot/README.md index 503c784349..e9f1c4bb7e 100644 --- a/packages/mergebot/README.md +++ b/packages/mergebot/README.md @@ -42,7 +42,7 @@ There is an Azure function in `PR-Trigger` that receives webhooks; this function You _probably_ don't need to do this. Use test to validate any change inside the src dir against integration tests. -However, you need to have a GitHub API access key in either: `DT_BOT_AUTH_TOKEN`, `BOT_AUTH_TOKEN` or `AUTH_TOKEN`. +However, you need to have a GitHub API access key in either: `DT_BOT_AUTH_TOKEN`, `BOT_AUTH_TOKEN` or `AUTH_TOKEN`. These token env vars are for local development; production uses GitHub App tokens minted through Azure Key Vault with `GITHUB_APP_CLIENT_ID` and `GITHUB_APP_KEY_VAULT_KEY_ID`. Ask Ryan for the bot's auth token (TypeScript team members: Look in the team OneNote). Don't run the bot under your own auth token as this will generate a bunch of spam from duplicate comments. diff --git a/packages/mergebot/package.json b/packages/mergebot/package.json index d4c2bc81a7..9913784ab2 100644 --- a/packages/mergebot/package.json +++ b/packages/mergebot/package.json @@ -20,6 +20,8 @@ }, "dependencies": { "@apollo/client": "^4.1.6", + "@azure/identity": "^4.13.0", + "@azure/keyvault-keys": "^4.10.0", "@azure/functions": "^4.11.2", "@definitelytyped/old-header-parser": "npm:@definitelytyped/header-parser@0.0.178", "@definitelytyped/utils": "workspace:*", diff --git a/packages/mergebot/src/execute-pr-actions.ts b/packages/mergebot/src/execute-pr-actions.ts index 7373f74fea..4a954d9066 100644 --- a/packages/mergebot/src/execute-pr-actions.ts +++ b/packages/mergebot/src/execute-pr-actions.ts @@ -5,10 +5,10 @@ import type { PrQuery } from "./queries/schema/graphql"; import { Actions } from "./compute-pr-actions"; import { createMutation, client } from "./graphql-client"; import { getProjectBoardColumns, getLabels } from "./util/cachedQueries"; -import { noNullish, flatten } from "./util/util"; +import { noNullish, flatten, isTypeScriptBot } from "./util/util"; import { tagsToDeleteIfNotPosted } from "./comments"; import * as comment from "./util/comment"; -import { request } from "https"; +import { getGitHubAuthToken } from "./github-auth"; import { assertDefined } from "@definitelytyped/utils"; type PR_repository_pullRequest = NonNullable["pullRequest"]>; @@ -153,7 +153,7 @@ interface ParsedComment { function getBotComments(pr: PR_repository_pullRequest): ParsedComment[] { return noNullish( (pr.comments.nodes ?? []) - .filter((comment) => comment?.author?.login === "typescript-bot") + .filter((comment) => isTypeScriptBot(comment?.author?.login)) .map((c) => { const { id, body } = c!, parsed = comment.parse(body); @@ -236,22 +236,19 @@ interface RestMutation { op: string; } -function doRestCall(call: RestMutation): Promise { +async function doRestCall(call: RestMutation): Promise { + const token = await getGitHubAuthToken(); const url = `https://api.github.com/repos/DefinitelyTyped/DefinitelyTyped/${call.op}`; - const headers = { - accept: "application/vnd.github.v3+json", - authorization: `token ${process.env.BOT_AUTH_TOKEN}`, - "user-agent": "mergebot", - }; - return new Promise((resolve, reject) => { - const req = request(url, { method: call.method, headers }, (reply) => { - const bad = !reply.statusCode || reply.statusCode < 200 || reply.statusCode >= 300; - if (bad) return reject(`doRestCall failed with a status of ${reply.statusCode}`); - return resolve(); - }); - req.on("error", reject); - req.end(); + const response = await fetch(url, { + method: call.method, + headers: { + accept: "application/vnd.github.v3+json", + authorization: `token ${token}`, + }, }); + if (!response.ok) { + throw new Error(`doRestCall failed with a status of ${response.status}`); + } } function getMutationsForReRunningCI(actions: Actions) { diff --git a/packages/mergebot/src/functions/api.ts b/packages/mergebot/src/functions/api.ts index 3f54caab85..cc4be8609c 100644 --- a/packages/mergebot/src/functions/api.ts +++ b/packages/mergebot/src/functions/api.ts @@ -1,5 +1,6 @@ import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; import { getPRInfo } from "../queries/pr-query"; +import { isTypeScriptBot } from "../util/util"; const headers = { "Content-Type": "text/json", "Access-Control-Allow-Methods": "GET", @@ -23,7 +24,7 @@ export async function httpTrigger(request: HttpRequest, context: InvocationConte if (!prInfo) return notFound("No PR metadata"); const welcomeComment = prInfo.comments.nodes!.find( - (c) => c && c.author?.login === "typescript-bot" && c.body.endsWith(""), + (c) => c && isTypeScriptBot(c.author?.login) && c.body.endsWith(""), ); if (!welcomeComment || !welcomeComment.body || !welcomeComment.body.includes("```json")) return notFound("PR comment with JSON not found"); diff --git a/packages/mergebot/src/functions/discussions-trigger.ts b/packages/mergebot/src/functions/discussions-trigger.ts index e7469fb116..7889e60c8b 100644 --- a/packages/mergebot/src/functions/discussions-trigger.ts +++ b/packages/mergebot/src/functions/discussions-trigger.ts @@ -6,7 +6,7 @@ import { createMutation, client } from "../graphql-client"; import { getLabelByName, getCommentsForDiscussionNumber } from "../queries/discussion-queries"; import { reply } from "../util/reply"; import { httpLog, shouldRunRequest } from "../util/verify"; -import { txt } from "../util/util"; +import { isTypeScriptBot, txt } from "../util/util"; import { getOwnersOfPackage } from "../pr-info"; import { fetchFile } from "../util/fetchFile"; @@ -88,7 +88,7 @@ async function pingAuthorsAndSetUpDiscussion(discussion: Discussion) { const message = gotAReferenceMessage(aboutNPMRef, owners); await updateOrCreateMainComment(discussion, message); // Only create a label once we've confirmed the package actually exists on DT -- - // otherwise an unprivileged user could make typescript-bot create arbitrarily-named + // otherwise an unprivileged user could make the TypeScript bot create arbitrarily-named // repository labels by editing the discussion title. await addLabel(discussion, "Pkg: " + aboutNPMRef, `Discussions related to ${aboutNPMRef}`); } @@ -115,7 +115,7 @@ async function updateDiscordWithRequest(discussion: Discussion) { async function updateOrCreateMainComment(discussion: Discussion, message: string) { const discussionComments = await getCommentsForDiscussionNumber(discussion.number); - const previousComment = discussionComments.find((c) => c?.author?.login === "typescript-bot"); + const previousComment = discussionComments.find((c) => isTypeScriptBot(c?.author?.login)); if (previousComment) { await client.mutate( createMutation("updateDiscussionComment" as any, { body: message, commentId: previousComment.id }), diff --git a/packages/mergebot/src/functions/pr-trigger.ts b/packages/mergebot/src/functions/pr-trigger.ts index 7eb896e70a..4c4ef8f1be 100644 --- a/packages/mergebot/src/functions/pr-trigger.ts +++ b/packages/mergebot/src/functions/pr-trigger.ts @@ -17,6 +17,7 @@ import type { PullRequestReviewEvent, } from "@octokit/webhooks-types"; import { runQueryToGetPRForCardId } from "../queries/card-id-to-pr-query"; +import { isTypeScriptBot } from "../util/util"; app.http("PR-Trigger", { methods: ["GET", "POST"], authLevel: "anonymous", handler: httpTrigger }); const eventNames = [ @@ -81,8 +82,8 @@ async function httpTrigger(req: HttpRequest, context: InvocationContext) { const handleTrigger = async (context: InvocationContext, event: PrEvent) => { const fullName = event.name + "." + event.payload.action; context.log(`Handling event: ${fullName}`); - if (event.payload.sender.login === "typescript-bot" && fullName !== "check_suite.completed") - return reply(context, 200, "Skipped webhook because it was triggered by typescript-bot"); + if (isTypeScriptBot(event.payload.sender.login) && fullName !== "check_suite.completed") + return reply(context, 200, "Skipped webhook because it was triggered by the TypeScript bot"); // Allow the bot to run side-effects that are not the 'core' function // of the review cycle, but are related to keeping DT running smoothly diff --git a/packages/mergebot/src/github-auth.ts b/packages/mergebot/src/github-auth.ts new file mode 100644 index 0000000000..07972a48a7 --- /dev/null +++ b/packages/mergebot/src/github-auth.ts @@ -0,0 +1,77 @@ +import { DefaultAzureCredential } from "@azure/identity"; +import { CryptographyClient } from "@azure/keyvault-keys"; +import { createGitHubAppAuth } from "./util/github-app-auth"; + +type PermissionLevel = "read" | "write" | "admin"; +type GitHubAppAuth = ReturnType; + +let githubAuth: GitHubAppAuth | undefined; + +function explicitToken() { + if (process.env.JEST_WORKER_ID) return "FAKE_TOKEN"; + return process.env.BOT_AUTH_TOKEN || process.env.DT_BOT_AUTH_TOKEN || process.env.AUTH_TOKEN; +} + +function requiredEnv(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`${name} must be set`); + } + return value; +} + +function getGitHubAuth() { + if (!githubAuth) { + const cryptographyClient = new CryptographyClient( + requiredEnv("GITHUB_APP_KEY_VAULT_KEY_ID"), + new DefaultAzureCredential(), + ); + githubAuth = createGitHubAppAuth({ + appClientId: requiredEnv("GITHUB_APP_CLIENT_ID"), + signer: async (signingInput) => { + const signature = await cryptographyClient.signData("RS256", Buffer.from(signingInput)); + return Buffer.from(signature.result).toString("base64url"); + }, + defaultOwner: process.env.GITHUB_APP_INSTALLATION_OWNER || "DefinitelyTyped", + }); + } + return githubAuth; +} + +function permissions() { + const raw = process.env.GITHUB_APP_PERMISSIONS; + if (!raw) { + return undefined; + } + const parsed: unknown = JSON.parse(raw); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("GITHUB_APP_PERMISSIONS must be a JSON object"); + } + const result: Record = {}; + for (const [permission, level] of Object.entries(parsed)) { + if (level !== "read" && level !== "write" && level !== "admin") { + throw new Error(`Invalid GitHub App permission level for ${permission}: ${level}`); + } + result[permission] = level; + } + return result; +} + +export async function getGitHubAuthToken() { + const token = explicitToken(); + if (token) { + return token.trim(); + } + + return getGitHubAuth().getToken({ + repositories: [process.env.GITHUB_APP_INSTALLATION_REPO || "DefinitelyTyped"], + permissions: permissions() ?? { + checks: "write", + contents: "read", + discussions: "write", + issues: "write", + organization_projects: "write", + pull_requests: "write", + }, + }); +} diff --git a/packages/mergebot/src/graphql-client.ts b/packages/mergebot/src/graphql-client.ts index c3bcb2dfdf..f84c388fc4 100644 --- a/packages/mergebot/src/graphql-client.ts +++ b/packages/mergebot/src/graphql-client.ts @@ -1,15 +1,24 @@ import { ApolloClient, gql, HttpLink, InMemoryCache, MutationOptions, TypedDocumentNode } from "@apollo/client/core"; import { print } from "graphql"; import * as schema from "@octokit/graphql-schema"; +import { getGitHubAuthToken } from "./github-auth"; const uri = "https://api.github.com/graphql"; -const headers = { - authorization: `Bearer ${getAuthToken()}`, - accept: "application/vnd.github.starfox-preview+json, application/vnd.github.bane-preview+json", -}; +const accept = "application/vnd.github.starfox-preview+json, application/vnd.github.bane-preview+json"; const cache = new InMemoryCache(); -const link = new HttpLink({ uri, headers, fetch }); +const link = new HttpLink({ + uri, + fetch: async (input, init) => + fetch(input, { + ...init, + headers: { + ...init?.headers, + authorization: `Bearer ${await getGitHubAuthToken()}`, + accept, + }, + }), +}); export const client = new ApolloClient({ cache, link }); @@ -29,13 +38,3 @@ export function createMutation( }; return { mutation, variables: { input } }; } - -function getAuthToken() { - if (process.env.JEST_WORKER_ID) return "FAKE_TOKEN"; - - const result = process.env.BOT_AUTH_TOKEN || process.env.AUTH_TOKEN || process.env.DT_BOT_AUTH_TOKEN; - if (typeof result !== "string") { - throw new Error("Set BOT_AUTH_TOKEN or AUTH_TOKEN to a valid auth token"); - } - return result.trim(); -} diff --git a/packages/mergebot/src/pr-info.ts b/packages/mergebot/src/pr-info.ts index 7fcb766572..6c06d4ded9 100644 --- a/packages/mergebot/src/pr-info.ts +++ b/packages/mergebot/src/pr-info.ts @@ -9,7 +9,7 @@ import { import type { PrQuery } from "./queries/schema/graphql"; import { getMonthlyDownloadCount } from "./util/npm"; import { fetchFile as defaultFetchFile } from "./util/fetchFile"; -import { noNullish, someLast, sameUser, authorNotBot, max, abbrOid } from "./util/util"; +import { noNullish, someLast, sameUser, authorNotBot, max, abbrOid, isTypeScriptBot } from "./util/util"; import { fileLimit, getPRInfo } from "./queries/pr-query"; import * as comment from "./util/comment"; import * as urls from "./urls"; @@ -606,7 +606,7 @@ function getMergeOfferDate(comments: PR_repository_pullRequest_comments_nodes[], const offer = latestComment( comments.filter( (c) => - sameUser("typescript-bot", c.author?.login || "-") && + isTypeScriptBot(c.author?.login) && comment.parse(c.body)?.tag === "merge-offer" && c.body.includes(`(at ${abbrOid(headOid)})`), ), @@ -664,7 +664,7 @@ function isMaintainerComment( ) { return ( (comment.authorAssociation === "MEMBER" || comment.authorAssociation === "OWNER") && - !sameUser("typescript-bot", comment.author?.login || "-") + !isTypeScriptBot(comment.author?.login) ); } diff --git a/packages/mergebot/src/util/github-app-auth.ts b/packages/mergebot/src/util/github-app-auth.ts new file mode 100644 index 0000000000..d5bffb44b6 --- /dev/null +++ b/packages/mergebot/src/util/github-app-auth.ts @@ -0,0 +1,370 @@ +// Copied from https://github.com/microsoft/create-github-app-token-via-key-vault/blob/main/src/api.ts +const defaultRefreshWindowMs = 5 * 60 * 1000; +const defaultGitHubApiUrl = "https://api.github.com"; +const transientRetryCount = 3; + +export type PermissionLevel = "read" | "write" | "admin"; +export type Permissions = Record; + +/** + * Signs the JWT signing input and returns the base64url-encoded raw RSA signature. + */ +export type GitHubAppJwtSigner = (signingInput: string) => Promise; + +export interface GitHubAppAuthOptions { + appClientId: string; + signer: GitHubAppJwtSigner; + defaultOwner?: string; + refreshWindowMs?: number; + githubApiUrl?: string; +} + +export interface GetInstallationTokenOptions { + owner?: string; + repositories?: string[] | string; + repositoryNames?: string[] | string; + enterprise?: string; + permissions?: Permissions; +} + +export interface InstallationToken { + token: string; + expiresAt: string; + installationId: number; + appSlug: string; + repositories: string[]; + permissions: Permissions | Record; +} + +export interface GitHubAppAuth { + getInstallationToken(options: GetInstallationTokenOptions): Promise; + getToken(options: GetInstallationTokenOptions): Promise; + revokeToken(token: string): Promise; +} + +class GitHubRequestError extends Error { + status: number; + constructor(message: string, status: number) { + super(message); + this.status = status; + } +} + +function assertValue(value: T | null | undefined, message: string): T { + if (!value) { + throw new Error(message); + } + return value; +} + +function base64url(value: string | Uint8Array): string { + return Buffer.from(value).toString("base64url"); +} + +async function sleep(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); +} + +function isRetryableError(error: unknown): boolean { + return error instanceof GitHubRequestError ? error.status >= 500 : error instanceof TypeError; +} + +async function retryTransient(operation: () => Promise): Promise { + for (let attempt = 0; ; attempt++) { + try { + return await operation(); + } catch (error) { + if (attempt >= transientRetryCount || !isRetryableError(error)) { + throw error; + } + await sleep(2 ** attempt * 1000); + } + } +} + +export function splitRepositoryNames(repositories: string[] | string | undefined): string[] { + if (Array.isArray(repositories)) { + return repositories.map((repo) => `${repo}`.trim()).filter(Boolean); + } + if (typeof repositories === "string") { + return repositories + .split(/[,\n]/) + .map((repo) => repo.trim()) + .filter(Boolean); + } + return []; +} + +function stableObject(value: unknown): unknown { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return value; + } + + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => [key, stableObject(entry)]), + ); +} + +function githubHeaders(token: string, json = false): Record { + return { + Accept: "application/vnd.github+json", + Authorization: `Bearer ${token}`, + ...(json ? { "Content-Type": "application/json" } : {}), + "X-GitHub-Api-Version": "2022-11-28", + }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function requiredIntegerProperty(value: unknown, property: string, failureMessage: string): number { + const propertyValue = isRecord(value) ? value[property] : undefined; + if (typeof propertyValue !== "number" || !Number.isInteger(propertyValue)) { + throw new Error(failureMessage); + } + return propertyValue; +} + +function requiredStringProperty(value: unknown, property: string, failureMessage: string): string { + const propertyValue = isRecord(value) ? value[property] : undefined; + if (typeof propertyValue !== "string" || !propertyValue) { + throw new Error(failureMessage); + } + return propertyValue; +} + +function validatePermissionName(key: string): void { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { + throw new Error(`Invalid permission name: ${key}`); + } +} + +function validatePermissionLevel(key: string, level: unknown): PermissionLevel { + if (level !== "read" && level !== "write" && level !== "admin") { + throw new Error(`Invalid permission level for ${key}: ${level}`); + } + return level; +} + +function validatePermissions(value: unknown): Permissions | undefined { + if (value === undefined) { + return undefined; + } + if (!isRecord(value)) { + throw new Error("permissions must be an object"); + } + + const permissions: Permissions = {}; + for (const [key, level] of Object.entries(value)) { + validatePermissionName(key); + permissions[key] = validatePermissionLevel(key, level); + } + + return Object.keys(permissions).length === 0 ? undefined : permissions; +} + +async function requestJson(url: string | URL, init: RequestInit, failureMessage: string): Promise { + const response = await fetch(url, init); + const body = await response.text(); + if (!response.ok) { + throw new GitHubRequestError( + `${failureMessage}: ${response.status} ${response.statusText}: ${body}`, + response.status, + ); + } + try { + return JSON.parse(body); + } catch { + throw new Error(`${failureMessage}: GitHub returned invalid JSON`); + } +} + +async function requestNoContent(url: string | URL, init: RequestInit, failureMessage: string): Promise { + const response = await fetch(url, init); + if (!response.ok) { + const body = await response.text(); + throw new GitHubRequestError( + `${failureMessage}: ${response.status} ${response.statusText}: ${body}`, + response.status, + ); + } +} + +type InstallationTarget = + | { type: "enterprise"; enterprise: string } + | { type: "owner"; owner: string } + | { type: "repository"; owner: string; repositories: string[] }; + +function resolveInstallationTarget( + options: GetInstallationTokenOptions, + defaultOwner: string | undefined, +): InstallationTarget { + const repositories = splitRepositoryNames(options.repositories ?? options.repositoryNames); + + if (options.enterprise) { + if (options.owner || repositories.length > 0) { + throw new Error("Cannot use 'enterprise' with 'owner' or 'repositories'"); + } + return { type: "enterprise", enterprise: options.enterprise }; + } + + const owner = assertValue(options.owner ?? defaultOwner, "owner is required to discover installation ID"); + + if (repositories.length === 0) { + return { type: "owner", owner }; + } + + return { type: "repository", owner, repositories }; +} + +export function createGitHubAppAuth(options: GitHubAppAuthOptions): GitHubAppAuth { + assertValue(options.appClientId, "appClientId is required"); + assertValue(options.signer, "signer is required"); + + const appClientId = options.appClientId; + const signer = options.signer; + const defaultOwner = options.defaultOwner; + const refreshWindowMs = options.refreshWindowMs ?? defaultRefreshWindowMs; + const githubApiUrl = options.githubApiUrl ?? defaultGitHubApiUrl; + const installationCache = new Map(); + const tokenCache = new Map(); + + async function createJwt(): Promise { + const now = Math.floor(Date.now() / 1000); + const iat = now - 60; + const exp = now + 9 * 60; + const header = base64url(JSON.stringify({ typ: "JWT", alg: "RS256" })); + const payload = base64url(JSON.stringify({ iat, exp, iss: appClientId })); + const signingInput = `${header}.${payload}`; + const signature = await signer(signingInput); + return `${signingInput}.${signature}`; + } + + async function discoverInstallation(target: InstallationTarget): Promise<{ id: number; appSlug: string }> { + const cacheKey = JSON.stringify(target); + const cached = installationCache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + + const jwt = await createJwt(); + let installation: unknown; + switch (target.type) { + case "enterprise": + installation = await requestJson( + `${githubApiUrl}/enterprises/${target.enterprise}/installation`, + { headers: githubHeaders(jwt) }, + "Could not discover GitHub App installation ID", + ); + break; + case "owner": + try { + installation = await requestJson( + `${githubApiUrl}/orgs/${target.owner}/installation`, + { headers: githubHeaders(jwt) }, + "Could not discover GitHub App installation ID", + ); + } catch (error) { + if (!(error instanceof GitHubRequestError) || error.status !== 404) { + throw error; + } + + installation = await requestJson( + `${githubApiUrl}/users/${target.owner}/installation`, + { headers: githubHeaders(jwt) }, + "Could not discover GitHub App installation ID", + ); + } + break; + case "repository": + installation = await requestJson( + `${githubApiUrl}/repos/${target.owner}/${assertValue( + target.repositories[0], + "repository is required", + )}/installation`, + { headers: githubHeaders(jwt) }, + "Could not discover GitHub App installation ID", + ); + break; + } + + const result = { + id: requiredIntegerProperty(installation, "id", "GitHub did not return an installation ID"), + appSlug: requiredStringProperty(installation, "app_slug", "GitHub did not return an App slug"), + }; + installationCache.set(cacheKey, result); + return result; + } + + async function getInstallationToken(options: GetInstallationTokenOptions): Promise { + const target = resolveInstallationTarget(options, defaultOwner); + const permissions = validatePermissions(options.permissions); + return retryTransient(async () => { + const installation = await discoverInstallation(target); + const repositories = target.type === "repository" ? target.repositories : []; + const cacheKey = JSON.stringify({ + installationId: installation.id, + repositories: [...repositories].sort(), + permissions: stableObject(permissions), + }); + const cached = tokenCache.get(cacheKey); + if (cached && Date.now() < new Date(cached.expiresAt).getTime() - refreshWindowMs) { + return cached; + } + + const jwt = await createJwt(); + const body = { + ...(repositories.length > 0 ? { repositories } : {}), + ...(permissions ? { permissions } : {}), + }; + const token = await requestJson( + `${githubApiUrl}/app/installations/${installation.id}/access_tokens`, + { + method: "POST", + headers: githubHeaders(jwt, true), + body: JSON.stringify(body), + }, + "Could not create GitHub App installation token", + ); + + const result: InstallationToken = { + token: requiredStringProperty(token, "token", "GitHub did not return an installation token"), + expiresAt: requiredStringProperty( + token, + "expires_at", + "GitHub did not return an installation token expiration", + ), + installationId: installation.id, + appSlug: installation.appSlug, + repositories, + permissions: isRecord(token) && isRecord(token.permissions) ? token.permissions : (permissions ?? {}), + }; + tokenCache.set(cacheKey, result); + return result; + }); + } + + async function getToken(options: GetInstallationTokenOptions): Promise { + return (await getInstallationToken(options)).token; + } + + async function revokeToken(token: string): Promise { + await requestNoContent( + `${githubApiUrl}/installation/token`, + { + method: "DELETE", + headers: githubHeaders(token), + }, + "Could not revoke GitHub App installation token", + ); + } + + return { + getInstallationToken, + getToken, + revokeToken, + }; +} diff --git a/packages/mergebot/src/util/util.ts b/packages/mergebot/src/util/util.ts index 42c9e2bf41..1f03a8a736 100644 --- a/packages/mergebot/src/util/util.ts +++ b/packages/mergebot/src/util/util.ts @@ -42,15 +42,24 @@ export function sameUser(u1: string, u2: string) { return u1.toLowerCase() === u2.toLowerCase(); } -const knownBots = new Set(["typescript-bot", "copilot-pull-request-reviewer"]); +const typeScriptBotLogins = new Set(["typescript-bot", "typescript-automation[bot]"]); +const knownBots = new Set([...typeScriptBotLogins, "copilot-pull-request-reviewer"]); + +export function isTypeScriptBot(login: string | null | undefined) { + return !!login && typeScriptBotLogins.has(login.toLowerCase()); +} + +function isKnownBot(login: string | null | undefined) { + return !!login && knownBots.has(login.toLowerCase()); +} export function authorNotBot( node: { login: string } | { author?: { login: string } | null } | { actor?: { login: string } | null }, ): boolean { return ( - ("author" in node && !knownBots.has(node.author!.login)) || - ("actor" in node && !knownBots.has(node.actor!.login)) || - ("login" in node && !knownBots.has(node.login)) + ("author" in node && !isKnownBot(node.author!.login)) || + ("actor" in node && !isKnownBot(node.actor!.login)) || + ("login" in node && !isKnownBot(node.login)) ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8660492263..80b0ddfea6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -290,6 +290,12 @@ importers: '@azure/functions': specifier: ^4.11.2 version: 4.11.2 + '@azure/identity': + specifier: ^4.13.0 + version: 4.13.1 + '@azure/keyvault-keys': + specifier: ^4.10.0 + version: 4.10.0(@azure/core-client@1.10.2) '@definitelytyped/old-header-parser': specifier: npm:@definitelytyped/header-parser@0.0.178 version: '@definitelytyped/header-parser@0.0.178' @@ -546,6 +552,49 @@ packages: resolution: {integrity: sha512-sWBB/tdIktaT5xMq0Dz6CJyqcf6oMNdmiKiuPU1lWoJLTL6gjRSsksBuSgqot21hylkklBQY1wiSu+PkZhW7sw==} engines: {node: '>=20'} + '@azure-rest/core-client@2.6.1': + resolution: {integrity: sha512-KzI10qnkWTsVS2yRBUdc8NLUJ1rOm+292mYs7Pe9wqAj/jv4bRskVm1l8XkKeVTN0OCQtrU5RG0Yhjbz1Wmg7g==} + engines: {node: '>=20.0.0'} + + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.2': + resolution: {integrity: sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-http-compat@2.4.0': + resolution: {integrity: sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 + + '@azure/core-lro@2.7.2': + resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} + engines: {node: '>=18.0.0'} + + '@azure/core-paging@1.6.2': + resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} + engines: {node: '>=18.0.0'} + + '@azure/core-rest-pipeline@1.23.0': + resolution: {integrity: sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + '@azure/functions-extensions-base@0.2.0': resolution: {integrity: sha512-ncCkHBNQYJa93dBIh+toH0v1iSgCzSo9tr94s6SMBe7DPWREkaWh8cq33A5P4rPSFX1g5W+3SPvIzDr/6/VOWQ==} engines: {node: '>=18.0'} @@ -554,6 +603,34 @@ packages: resolution: {integrity: sha512-U7qpPo0pUxDfdP3Q8gO5GLtust94nh8+RtIUvEKE4qU9yuDhL2vU1zzanuzkaV2j/TFv+EEmN8QDtchAgpeffw==} engines: {node: '>=20.0'} + '@azure/identity@4.13.1': + resolution: {integrity: sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==} + engines: {node: '>=20.0.0'} + + '@azure/keyvault-common@2.1.0': + resolution: {integrity: sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==} + engines: {node: '>=20.0.0'} + + '@azure/keyvault-keys@4.10.0': + resolution: {integrity: sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==} + engines: {node: '>=18.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/msal-browser@5.11.0': + resolution: {integrity: sha512-zkGNYS3TwY8lUpPIafAmsFCYZbgFixY9y/LZB9GUg0IILoHTqpN26j5OrkL1AQThh/YdZsawe4iWXfp85lFVxg==} + engines: {node: '>=0.8.0'} + + '@azure/msal-common@16.6.2': + resolution: {integrity: sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==} + engines: {node: '>=0.8.0'} + + '@azure/msal-node@5.2.2': + resolution: {integrity: sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==} + engines: {node: '>=20'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -2023,6 +2100,10 @@ packages: resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typespec/ts-http-runtime@0.3.6': + resolution: {integrity: sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==} + engines: {node: '>=20.0.0'} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -2396,9 +2477,16 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + cacache@20.0.3: resolution: {integrity: sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -2651,10 +2739,22 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -2711,6 +2811,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ecurve@1.0.6: resolution: {integrity: sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w==} @@ -3333,6 +3436,11 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3361,6 +3469,11 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-lower-case@2.0.2: resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} @@ -3447,6 +3560,10 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -3692,6 +3809,16 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3730,12 +3857,33 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} @@ -4049,6 +4197,10 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + optimism@0.18.1: resolution: {integrity: sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==} @@ -4335,6 +4487,10 @@ packages: engines: {node: 20 || >=22} hasBin: true + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4969,6 +5125,10 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5070,6 +5230,84 @@ snapshots: typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 + '@azure-rest/core-client@2.6.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-http-compat@2.4.0(@azure/core-client@1.10.2)(@azure/core-rest-pipeline@1.23.0)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.2 + '@azure/core-rest-pipeline': 1.23.0 + + '@azure/core-lro@2.7.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-paging@1.6.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-rest-pipeline@1.23.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@azure/functions-extensions-base@0.2.0': {} '@azure/functions@4.11.2': @@ -5077,6 +5315,71 @@ snapshots: '@azure/functions-extensions-base': 0.2.0 cookie: 0.7.2 + '@azure/identity@4.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.2 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@azure/msal-browser': 5.11.0 + '@azure/msal-node': 5.2.2 + open: 10.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/keyvault-common@2.1.0': + dependencies: + '@azure-rest/core-client': 2.6.1 + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/keyvault-keys@4.10.0(@azure/core-client@1.10.2)': + dependencies: + '@azure-rest/core-client': 2.6.1 + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-http-compat': 2.4.0(@azure/core-client@1.10.2)(@azure/core-rest-pipeline@1.23.0) + '@azure/core-lro': 2.7.2 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/keyvault-common': 2.1.0 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/msal-browser@5.11.0': + dependencies: + '@azure/msal-common': 16.6.2 + + '@azure/msal-common@16.6.2': {} + + '@azure/msal-node@5.2.2': + dependencies: + '@azure/msal-common': 16.6.2 + jsonwebtoken: 9.0.3 + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5809,7 +6112,7 @@ snapshots: dependencies: graphql: 16.13.0 lodash.sortby: 4.7.0 - tslib: 2.6.3 + tslib: 2.8.1 '@graphql-tools/executor-common@0.0.4(graphql@16.13.0)': dependencies: @@ -5998,14 +6301,14 @@ snapshots: '@graphql-tools/optimize@2.0.0(graphql@16.13.0)': dependencies: graphql: 16.13.0 - tslib: 2.6.3 + tslib: 2.8.1 '@graphql-tools/relay-operation-optimizer@7.0.27(graphql@16.13.0)': dependencies: '@ardatan/relay-compiler': 12.0.3(graphql@16.13.0) '@graphql-tools/utils': 11.0.0(graphql@16.13.0) graphql: 16.13.0 - tslib: 2.6.3 + tslib: 2.8.1 transitivePeerDependencies: - encoding @@ -6985,6 +7288,14 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 + '@typespec/ts-http-runtime@0.3.6': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -7335,8 +7646,14 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + cacache@20.0.3: dependencies: '@npmcli/fs': 5.0.0 @@ -7592,12 +7909,21 @@ snapshots: deepmerge@4.3.1: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@3.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -7645,6 +7971,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ecurve@1.0.6: dependencies: bigi: 1.4.2 @@ -8427,6 +8757,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -8453,6 +8785,10 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-lower-case@2.0.2: dependencies: tslib: 2.6.3 @@ -8531,6 +8867,10 @@ snapshots: is-windows@1.0.2: {} + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isarray@2.0.5: {} isexe@2.0.0: {} @@ -8955,6 +9295,30 @@ snapshots: jsonparse@1.3.1: {} + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -9015,10 +9379,24 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.sortby@4.7.0: {} lodash.startcase@4.4.0: {} @@ -9346,6 +9724,13 @@ snapshots: dependencies: mimic-function: 5.0.1 + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + optimism@0.18.1: dependencies: '@wry/caches': 1.0.1 @@ -9659,6 +10044,8 @@ snapshots: glob: 13.0.6 package-json-from-dist: 1.0.1 + run-applescript@7.1.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -10365,6 +10752,10 @@ snapshots: ws@8.19.0: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + y18n@5.0.8: {} yallist@3.1.1: {}