From 681164aaaa75ab8602921911b6fc4bed89fbe6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Fri, 15 Nov 2024 11:54:43 +0100 Subject: [PATCH] Move to auth cert for Release service (#233841) * wip * move to auto provisioning, only cert based auth * k * missing compilation * remove console logs * extract get publish auth tokens, wait 5 seconds before polling for release --- .../common/getPublishAuthTokens.js | 47 + .../common/getPublishAuthTokens.ts | 54 ++ build/azure-pipelines/common/publish.js | 511 ++++++----- build/azure-pipelines/common/publish.ts | 817 ++++++++++-------- build/azure-pipelines/product-build.yml | 4 +- build/azure-pipelines/product-publish.yml | 33 +- build/package-lock.json | 447 ++++++---- build/package.json | 5 + 8 files changed, 1120 insertions(+), 798 deletions(-) create mode 100644 build/azure-pipelines/common/getPublishAuthTokens.js create mode 100644 build/azure-pipelines/common/getPublishAuthTokens.ts diff --git a/build/azure-pipelines/common/getPublishAuthTokens.js b/build/azure-pipelines/common/getPublishAuthTokens.js new file mode 100644 index 00000000000..9c22e9ad94b --- /dev/null +++ b/build/azure-pipelines/common/getPublishAuthTokens.js @@ -0,0 +1,47 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAccessToken = getAccessToken; +const msal_node_1 = require("@azure/msal-node"); +function e(name) { + const result = process.env[name]; + if (typeof result !== 'string') { + throw new Error(`Missing env: ${name}`); + } + return result; +} +async function getAccessToken(endpoint, tenantId, clientId, idToken) { + const app = new msal_node_1.ConfidentialClientApplication({ + auth: { + clientId, + authority: `https://login.microsoftonline.com/${tenantId}`, + clientAssertion: idToken + } + }); + const result = await app.acquireTokenByClientCredential({ scopes: [`${endpoint}.default`] }); + if (!result) { + throw new Error('Failed to get access token'); + } + return { + token: result.accessToken, + expiresOnTimestamp: result.expiresOn.getTime(), + refreshAfterTimestamp: result.refreshOn?.getTime() + }; +} +async function main() { + const cosmosDBAccessToken = await getAccessToken(e('AZURE_DOCUMENTDB_ENDPOINT'), e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_ID_TOKEN')); + const blobServiceAccessToken = await getAccessToken(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_ID_TOKEN']); + console.log(JSON.stringify({ cosmosDBAccessToken, blobServiceAccessToken })); +} +if (require.main === module) { + main().then(() => { + process.exit(0); + }, err => { + console.error(err); + process.exit(1); + }); +} +//# sourceMappingURL=getPublishAuthTokens.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/getPublishAuthTokens.ts b/build/azure-pipelines/common/getPublishAuthTokens.ts new file mode 100644 index 00000000000..68e76de1a83 --- /dev/null +++ b/build/azure-pipelines/common/getPublishAuthTokens.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AccessToken } from '@azure/core-auth'; +import { ConfidentialClientApplication } from '@azure/msal-node'; + +function e(name: string): string { + const result = process.env[name]; + + if (typeof result !== 'string') { + throw new Error(`Missing env: ${name}`); + } + + return result; +} + +export async function getAccessToken(endpoint: string, tenantId: string, clientId: string, idToken: string): Promise { + const app = new ConfidentialClientApplication({ + auth: { + clientId, + authority: `https://login.microsoftonline.com/${tenantId}`, + clientAssertion: idToken + } + }); + + const result = await app.acquireTokenByClientCredential({ scopes: [`${endpoint}.default`] }); + + if (!result) { + throw new Error('Failed to get access token'); + } + + return { + token: result.accessToken, + expiresOnTimestamp: result.expiresOn!.getTime(), + refreshAfterTimestamp: result.refreshOn?.getTime() + }; +} + +async function main() { + const cosmosDBAccessToken = await getAccessToken(e('AZURE_DOCUMENTDB_ENDPOINT')!, e('AZURE_TENANT_ID')!, e('AZURE_CLIENT_ID')!, e('AZURE_ID_TOKEN')!); + const blobServiceAccessToken = await getAccessToken(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, process.env['AZURE_ID_TOKEN']!); + console.log(JSON.stringify({ cosmosDBAccessToken, blobServiceAccessToken })); +} + +if (require.main === module) { + main().then(() => { + process.exit(0); + }, err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index 5b7acc20001..3816db385a0 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -4,7 +4,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.getAccessToken = getAccessToken; const fs = require("fs"); const path = require("path"); const stream_1 = require("stream"); @@ -13,10 +12,12 @@ const yauzl = require("yauzl"); const crypto = require("crypto"); const retry_1 = require("./retry"); const cosmos_1 = require("@azure/cosmos"); -const identity_1 = require("@azure/identity"); const cp = require("child_process"); const os = require("os"); const node_worker_threads_1 = require("node:worker_threads"); +const msal_node_1 = require("@azure/msal-node"); +const storage_blob_1 = require("@azure/storage-blob"); +const jws = require("jws"); function e(name) { const result = process.env[name]; if (typeof result !== 'string') { @@ -24,267 +25,236 @@ function e(name) { } return result; } -class Temp { - _files = []; - tmpNameSync() { - const file = path.join(os.tmpdir(), crypto.randomBytes(20).toString('hex')); - this._files.push(file); - return file; - } - dispose() { - for (const file of this._files) { - try { - fs.unlinkSync(file); - } - catch (err) { - // noop - } - } - } -} -/** - * Gets an access token converted from a WIF/OIDC id token. - * We need this since this build job takes a while to run and while id tokens live for 10 minutes only, access tokens live for 24 hours. - * Source: https://goodworkaround.com/2021/12/21/another-deep-dive-into-azure-ad-workload-identity-federation-using-github-actions/ - */ -async function getAccessToken(endpoint, tenantId, clientId, idToken) { - const body = new URLSearchParams({ - scope: `${endpoint}.default`, - client_id: clientId, - grant_type: 'client_credentials', - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - client_assertion: encodeURIComponent(idToken) - }); - const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: body.toString() - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const aadToken = await response.json(); - return aadToken.access_token; -} -function isCreateProvisionedFilesErrorResponse(response) { - return response?.ErrorDetails?.Code !== undefined; -} -class ProvisionService { - log; - accessToken; - constructor(log, accessToken) { - this.log = log; - this.accessToken = accessToken; - } - async provision(releaseId, fileId, fileName) { - const body = JSON.stringify({ - ReleaseId: releaseId, - PortalName: 'VSCode', - PublisherCode: 'VSCode', - ProvisionedFilesCollection: [{ - PublisherKey: fileId, - IsStaticFriendlyFileName: true, - FriendlyFileName: fileName, - MaxTTL: '1440', - CdnMappings: ['ECN'] - }] - }); - this.log(`Provisioning ${fileName} (releaseId: ${releaseId}, fileId: ${fileId})...`); - const res = await (0, retry_1.retry)(() => this.request('POST', '/api/v2/ProvisionedFiles/CreateProvisionedFiles', { body })); - if (isCreateProvisionedFilesErrorResponse(res) && res.ErrorDetails.Code === 'FriendlyFileNameAlreadyProvisioned') { - this.log(`File already provisioned (most likley due to a re-run), skipping: ${fileName}`); - return; - } - if (!res.IsSuccess) { - throw new Error(`Failed to submit provisioning request: ${JSON.stringify(res.ErrorDetails)}`); - } - this.log(`Successfully provisioned ${fileName}`); - } - async request(method, url, options) { - const opts = { - method, - body: options?.body, - headers: { - Authorization: `Bearer ${this.accessToken}`, - 'Content-Type': 'application/json' - } - }; - const res = await fetch(`https://dsprovisionapi.microsoft.com${url}`, opts); - // 400 normally means the request is bad or something is already provisioned, so we will return as retries are useless - // Otherwise log the text body and headers. We do text because some responses are not JSON. - if ((!res.ok || res.status < 200 || res.status >= 500) && res.status !== 400) { - throw new Error(`Unexpected status code: ${res.status}\nResponse Headers: ${JSON.stringify(res.headers)}\nBody Text: ${await res.text()}`); - } - return await res.json(); - } -} function hashStream(hashName, stream) { return new Promise((c, e) => { const shasum = crypto.createHash(hashName); stream .on('data', shasum.update.bind(shasum)) .on('error', e) - .on('close', () => c(shasum.digest('hex'))); + .on('close', () => c(shasum.digest())); }); } -class ESRPClient { - log; - tmp; - authPath; - constructor(log, tmp, tenantId, clientId, authCertSubjectName, requestSigningCertSubjectName) { - this.log = log; - this.tmp = tmp; - this.authPath = this.tmp.tmpNameSync(); - fs.writeFileSync(this.authPath, JSON.stringify({ - Version: '1.0.0', - AuthenticationType: 'AAD_CERT', - TenantId: tenantId, - ClientId: clientId, - AuthCert: { - SubjectName: authCertSubjectName, - StoreLocation: 'LocalMachine', - StoreName: 'My', - SendX5c: 'true' - }, - RequestSigningCert: { - SubjectName: requestSigningCertSubjectName, - StoreLocation: 'LocalMachine', - StoreName: 'My' - } - })); - } - async release(version, filePath) { - this.log(`Submitting release for ${version}: ${filePath}`); - const submitReleaseResult = await this.SubmitRelease(version, filePath); - if (submitReleaseResult.submissionResponse.statusCode !== 'pass') { - throw new Error(`Unexpected status code: ${submitReleaseResult.submissionResponse.statusCode}`); - } - const releaseId = submitReleaseResult.submissionResponse.operationId; - this.log(`Successfully submitted release ${releaseId}. Polling for completion...`); - let details; - // Poll every 5 seconds, wait 60 minutes max -> poll 60/5*60=720 times - for (let i = 0; i < 720; i++) { - details = await this.ReleaseDetails(releaseId); - if (details.releaseDetails[0].statusCode === 'pass') { - break; - } - else if (details.releaseDetails[0].statusCode !== 'inprogress') { - throw new Error(`Failed to submit release: ${JSON.stringify(details)}`); - } - await new Promise(c => setTimeout(c, 5000)); - } - if (details.releaseDetails[0].statusCode !== 'pass') { - throw new Error(`Timed out waiting for release ${releaseId}: ${JSON.stringify(details)}`); - } - const fileId = details.releaseDetails[0].fileDetails[0].publisherKey; - this.log('Release completed successfully with fileId: ', fileId); - return { releaseId, fileId }; - } - async SubmitRelease(version, filePath) { - const policyPath = this.tmp.tmpNameSync(); - fs.writeFileSync(policyPath, JSON.stringify({ - Version: '1.0.0', - Audience: 'InternalLimited', - Intent: 'distribution', - ContentType: 'InstallPackage' - })); - const inputPath = this.tmp.tmpNameSync(); - const size = fs.statSync(filePath).size; - const istream = fs.createReadStream(filePath); - const sha256 = await hashStream('sha256', istream); - fs.writeFileSync(inputPath, JSON.stringify({ - Version: '1.0.0', - ReleaseInfo: { - ReleaseMetadata: { - Title: 'VS Code', - Properties: { - ReleaseContentType: 'InstallPackage' - }, - MinimumNumberOfApprovers: 1 - }, - ProductInfo: { - Name: 'VS Code', - Version: version, - Description: path.basename(filePath, path.extname(filePath)), - }, - Owners: [ - { - Owner: { - UserPrincipalName: 'jomo@microsoft.com' - } - } - ], - Approvers: [ - { - Approver: { - UserPrincipalName: 'jomo@microsoft.com' - }, - IsAutoApproved: true, - IsMandatory: false - } - ], - AccessPermissions: { - MainPublisher: 'VSCode', - ChannelDownloadEntityDetails: { - Consumer: ['VSCode'] - } - }, - CreatedBy: { - UserPrincipalName: 'jomo@microsoft.com' - } - }, - ReleaseBatches: [ - { - ReleaseRequestFiles: [ - { - SizeInBytes: size, - SourceHash: sha256, - HashType: 'SHA256', - SourceLocation: path.basename(filePath) - } - ], - SourceLocationType: 'UNC', - SourceRootDirectory: path.dirname(filePath), - DestinationLocationType: 'AzureBlob' - } - ] - })); - const outputPath = this.tmp.tmpNameSync(); - cp.execSync(`ESRPClient SubmitRelease -a ${this.authPath} -p ${policyPath} -i ${inputPath} -o ${outputPath}`, { stdio: 'inherit' }); - const output = fs.readFileSync(outputPath, 'utf8'); - return JSON.parse(output); - } - async ReleaseDetails(releaseId) { - const inputPath = this.tmp.tmpNameSync(); - fs.writeFileSync(inputPath, JSON.stringify({ - Version: '1.0.0', - OperationIds: [releaseId] - })); - const outputPath = this.tmp.tmpNameSync(); - cp.execSync(`ESRPClient ReleaseDetails -a ${this.authPath} -i ${inputPath} -o ${outputPath}`, { stdio: 'inherit' }); - const output = fs.readFileSync(outputPath, 'utf8'); - return JSON.parse(output); - } +var StatusCode; +(function (StatusCode) { + StatusCode["Pass"] = "pass"; + StatusCode["Inprogress"] = "inprogress"; + StatusCode["FailCanRetry"] = "failCanRetry"; + StatusCode["FailDoNotRetry"] = "failDoNotRetry"; + StatusCode["PendingAnalysis"] = "pendingAnalysis"; + StatusCode["Cancelled"] = "cancelled"; +})(StatusCode || (StatusCode = {})); +function getCertificateBuffer(input) { + return Buffer.from(input.replace(/-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----|\n/g, ''), 'base64'); } -async function releaseAndProvision(log, releaseTenantId, releaseClientId, releaseAuthCertSubjectName, releaseRequestSigningCertSubjectName, provisionTenantId, provisionAADUsername, provisionAADPassword, version, quality, filePath) { - const fileName = `${quality}/${version}/${path.basename(filePath)}`; - const result = `${e('PRSS_CDN_URL')}/${fileName}`; - const res = await (0, retry_1.retry)(() => fetch(result)); - if (res.status === 200) { - log(`Already released and provisioned: ${result}`); +function getThumbprint(input, algorithm) { + const buffer = getCertificateBuffer(input); + return crypto.createHash(algorithm).update(buffer).digest(); +} +function getKeyFromPFX(pfx) { + const pfxCertificatePath = path.join(os.tmpdir(), 'cert.pfx'); + const pemKeyPath = path.join(os.tmpdir(), 'key.pem'); + try { + const pfxCertificate = Buffer.from(pfx, 'base64'); + fs.writeFileSync(pfxCertificatePath, pfxCertificate); + cp.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nocerts -nodes -out "${pemKeyPath}" -passin pass:`); + const raw = fs.readFileSync(pemKeyPath, 'utf-8'); + const result = raw.match(/-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g)[0]; return result; } - const tmp = new Temp(); - process.on('exit', () => tmp.dispose()); - const esrpclient = new ESRPClient(log, tmp, releaseTenantId, releaseClientId, releaseAuthCertSubjectName, releaseRequestSigningCertSubjectName); - const release = await esrpclient.release(version, filePath); - const credential = new identity_1.ClientSecretCredential(provisionTenantId, provisionAADUsername, provisionAADPassword); - const accessToken = await credential.getToken(['https://microsoft.onmicrosoft.com/DS.Provisioning.WebApi/.default']); - const service = new ProvisionService(log, accessToken.token); - await service.provision(release.releaseId, release.fileId, fileName); - return result; + finally { + fs.rmSync(pfxCertificatePath, { force: true }); + fs.rmSync(pemKeyPath, { force: true }); + } +} +function getCertificatesFromPFX(pfx) { + const pfxCertificatePath = path.join(os.tmpdir(), 'cert.pfx'); + const pemCertificatePath = path.join(os.tmpdir(), 'cert.pem'); + try { + const pfxCertificate = Buffer.from(pfx, 'base64'); + fs.writeFileSync(pfxCertificatePath, pfxCertificate); + cp.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nokeys -out "${pemCertificatePath}" -passin pass:`); + const raw = fs.readFileSync(pemCertificatePath, 'utf-8'); + const matches = raw.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g); + return matches ? matches.reverse() : []; + } + finally { + fs.rmSync(pfxCertificatePath, { force: true }); + fs.rmSync(pemCertificatePath, { force: true }); + } +} +class ESRPReleaseService { + log; + clientId; + accessToken; + requestSigningCertificates; + requestSigningKey; + containerClient; + static async create(log, tenantId, clientId, authCertificatePfx, requestSigningCertificatePfx, containerClient) { + const authKey = getKeyFromPFX(authCertificatePfx); + const authCertificate = getCertificatesFromPFX(authCertificatePfx)[0]; + const requestSigningKey = getKeyFromPFX(requestSigningCertificatePfx); + const requestSigningCertificates = getCertificatesFromPFX(requestSigningCertificatePfx); + const app = new msal_node_1.ConfidentialClientApplication({ + auth: { + clientId, + authority: `https://login.microsoftonline.com/${tenantId}`, + clientCertificate: { + thumbprintSha256: getThumbprint(authCertificate, 'sha256').toString('hex'), + privateKey: authKey, + x5c: authCertificate + } + } + }); + const response = await app.acquireTokenByClientCredential({ + scopes: ['https://api.esrp.microsoft.com/.default'] + }); + return new ESRPReleaseService(log, clientId, response.accessToken, requestSigningCertificates, requestSigningKey, containerClient); + } + static API_URL = 'https://api.esrp.microsoft.com/api/v3/releaseservices/clients/'; + constructor(log, clientId, accessToken, requestSigningCertificates, requestSigningKey, containerClient) { + this.log = log; + this.clientId = clientId; + this.accessToken = accessToken; + this.requestSigningCertificates = requestSigningCertificates; + this.requestSigningKey = requestSigningKey; + this.containerClient = containerClient; + } + async createRelease(version, filePath, friendlyFileName) { + const correlationId = crypto.randomUUID(); + const blobClient = this.containerClient.getBlockBlobClient(correlationId); + this.log(`Uploading ${filePath} to ${blobClient.url}`); + await blobClient.uploadFile(filePath); + this.log('Uploaded blob successfully'); + try { + this.log(`Submitting release for ${version}: ${filePath}`); + const submitReleaseResult = await this.submitRelease(version, filePath, friendlyFileName, correlationId, blobClient); + this.log(`Successfully submitted release ${submitReleaseResult.operationId}. Polling for completion...`); + // Poll every 5 seconds, wait 60 minutes max -> poll 60/5*60=720 times + for (let i = 0; i < 720; i++) { + await new Promise(c => setTimeout(c, 5000)); + const releaseStatus = await this.getReleaseStatus(submitReleaseResult.operationId); + if (releaseStatus.status === 'pass') { + break; + } + else if (releaseStatus.status !== 'inprogress') { + throw new Error(`Failed to submit release: ${JSON.stringify(releaseStatus)}`); + } + } + const releaseDetails = await this.getReleaseDetails(submitReleaseResult.operationId); + if (releaseDetails.status !== 'pass') { + throw new Error(`Timed out waiting for release: ${JSON.stringify(releaseDetails)}`); + } + this.log('Successfully created release:', releaseDetails.files[0].fileDownloadDetails[0].downloadUrl); + return releaseDetails.files[0].fileDownloadDetails[0].downloadUrl; + } + finally { + this.log(`Deleting blob ${blobClient.url}`); + await blobClient.delete(); + this.log('Deleted blob successfully'); + } + } + async submitRelease(version, filePath, friendlyFileName, correlationId, blobClient) { + const size = fs.statSync(filePath).size; + const hash = await hashStream('sha256', fs.createReadStream(filePath)); + const message = { + customerCorrelationId: correlationId, + esrpCorrelationId: correlationId, + driEmail: ['joao.moreno@microsoft.com'], + createdBy: { userPrincipalName: 'jomo@microsoft.com' }, + owners: [{ owner: { userPrincipalName: 'jomo@microsoft.com' } }], + approvers: [{ approver: { userPrincipalName: 'jomo@microsoft.com' }, isAutoApproved: true, isMandatory: false }], + releaseInfo: { + title: 'VS Code', + properties: { + 'ReleaseContentType': 'InstallPackage' + }, + minimumNumberOfApprovers: 1 + }, + productInfo: { + name: 'VS Code', + version, + description: 'VS Code' + }, + accessPermissionsInfo: { + mainPublisher: 'VSCode', + channelDownloadEntityDetails: { + AllDownloadEntities: ['VSCode'] + } + }, + routingInfo: { + intent: 'filedownloadlinkgeneration' + }, + files: [{ + name: path.basename(filePath), + friendlyFileName, + tenantFileLocation: blobClient.url, + tenantFileLocationType: 'AzureBlob', + sourceLocation: { + type: 'azureBlob', + blobUrl: blobClient.url + }, + hashType: 'sha256', + hash: Array.from(hash), + sizeInBytes: size + }] + }; + message.jwsToken = await this.generateJwsToken(message); + const res = await fetch(`${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.accessToken}` + }, + body: JSON.stringify(message) + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to submit release: ${res.statusText}\n${text}`); + } + return await res.json(); + } + async getReleaseStatus(releaseId) { + const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grs/${releaseId}`; + const res = await fetch(url, { + headers: { + 'Authorization': `Bearer ${this.accessToken}` + } + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to get release status: ${res.statusText}\n${text}`); + } + return await res.json(); + } + async getReleaseDetails(releaseId) { + const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grd/${releaseId}`; + const res = await fetch(url, { + headers: { + 'Authorization': `Bearer ${this.accessToken}` + } + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to get release status: ${res.statusText}\n${text}`); + } + return await res.json(); + } + async generateJwsToken(message) { + return jws.sign({ + header: { + alg: 'RS256', + crit: ['exp', 'x5t'], + // Release service uses ticks, not seconds :roll_eyes: (https://stackoverflow.com/a/7968483) + exp: ((Date.now() + (6 * 60 * 1000)) * 10000) + 621355968000000000, + // Release service uses hex format, not base64url :roll_eyes: + x5t: getThumbprint(this.requestSigningCertificates[0], 'sha1').toString('hex'), + // Release service uses a '.' separated string, not an array of strings :roll_eyes: + x5c: this.requestSigningCertificates.map(c => getCertificateBuffer(c).toString('base64url')).join('.'), + }, + payload: message, + privateKey: this.requestSigningKey, + }); + } } class State { statePath; @@ -500,30 +470,42 @@ function getRealType(type) { return type; } } -async function processArtifact(artifact, artifactFilePath, cosmosDBAccessToken) { - const log = (...args) => console.log(`[${artifact.name}]`, ...args); +async function processArtifact(artifact, filePath) { const match = /^vscode_(?[^_]+)_(?[^_]+)(?:_legacy)?_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); if (!match) { throw new Error(`Invalid artifact name: ${artifact.name}`); } // getPlatform needs the unprocessedType + const { cosmosDBAccessToken, blobServiceAccessToken } = JSON.parse(e('PUBLISH_AUTH_TOKENS')); const quality = e('VSCODE_QUALITY'); - const commit = e('BUILD_SOURCEVERSION'); + const version = e('BUILD_SOURCEVERSION'); const { product, os, arch, unprocessedType } = match.groups; const isLegacy = artifact.name.includes('_legacy'); const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); const type = getRealType(unprocessedType); - const size = fs.statSync(artifactFilePath).size; - const stream = fs.createReadStream(artifactFilePath); + const size = fs.statSync(filePath).size; + const stream = fs.createReadStream(filePath); const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 - const url = await releaseAndProvision(log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), e('RELEASE_AUTH_CERT_SUBJECT_NAME'), e('RELEASE_REQUEST_SIGNING_CERT_SUBJECT_NAME'), e('PROVISION_TENANT_ID'), e('PROVISION_AAD_USERNAME'), e('PROVISION_AAD_PASSWORD'), commit, quality, artifactFilePath); - const asset = { platform, type, url, hash, sha256hash, size, supportsFastUpdate: true }; + const log = (...args) => console.log(`[${artifact.name}]`, ...args); + const blobServiceClient = new storage_blob_1.BlobServiceClient(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, { getToken: async () => blobServiceAccessToken }); + const containerClient = blobServiceClient.getContainerClient('staging'); + const releaseService = await ESRPReleaseService.create(log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), e('RELEASE_AUTH_CERT'), e('RELEASE_REQUEST_SIGNING_CERT'), containerClient); + const friendlyFileName = `${quality}/${version}/${path.basename(filePath)}`; + const url = `${e('PRSS_CDN_URL')}/${friendlyFileName}`; + const res = await (0, retry_1.retry)(() => fetch(url)); + if (res.status === 200) { + log(`Already released and provisioned: ${url}`); + } + else { + await releaseService.createRelease(version, filePath, friendlyFileName); + } + const asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; log('Creating asset...', JSON.stringify(asset, undefined, 2)); await (0, retry_1.retry)(async (attempt) => { log(`Creating asset in Cosmos DB (attempt ${attempt})...`); - const client = new cosmos_1.CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken}`) }); + const client = new cosmos_1.CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); const scripts = client.database('builds').container(quality).scripts; - await scripts.storedProcedure('createAsset').execute('', [commit, asset, true]); + await scripts.storedProcedure('createAsset').execute('', [version, asset, true]); }); log('Asset successfully created'); } @@ -535,8 +517,8 @@ async function processArtifact(artifact, artifactFilePath, cosmosDBAccessToken) // the CDN and finally update the build in Cosmos DB. async function main() { if (!node_worker_threads_1.isMainThread) { - const { artifact, artifactFilePath, cosmosDBAccessToken } = node_worker_threads_1.workerData; - await processArtifact(artifact, artifactFilePath, cosmosDBAccessToken); + const { artifact, artifactFilePath } = node_worker_threads_1.workerData; + await processArtifact(artifact, artifactFilePath); return; } const done = new State(); @@ -565,7 +547,6 @@ async function main() { } let resultPromise = Promise.resolve([]); const operations = []; - const cosmosDBAccessToken = await getAccessToken(e('AZURE_DOCUMENTDB_ENDPOINT'), e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_ID_TOKEN')); while (true) { const [timeline, artifacts] = await Promise.all([(0, retry_1.retry)(() => getPipelineTimeline()), (0, retry_1.retry)(() => getPipelineArtifacts())]); const stagesCompleted = new Set(timeline.records.filter(r => r.type === 'Stage' && r.state === 'completed' && stages.has(r.name)).map(r => r.name)); @@ -602,7 +583,7 @@ async function main() { const artifactFilePath = artifactFilePaths.filter(p => !/_manifest/.test(p))[0]; processing.add(artifact.name); const promise = new Promise((resolve, reject) => { - const worker = new node_worker_threads_1.Worker(__filename, { workerData: { artifact, artifactFilePath, cosmosDBAccessToken } }); + const worker = new node_worker_threads_1.Worker(__filename, { workerData: { artifact, artifactFilePath } }); worker.on('error', reject); worker.on('exit', code => { if (code === 0) { diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index 37ed4232f38..a3760c03434 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -12,10 +12,12 @@ import * as yauzl from 'yauzl'; import * as crypto from 'crypto'; import { retry } from './retry'; import { CosmosClient } from '@azure/cosmos'; -import { ClientSecretCredential } from '@azure/identity'; import * as cp from 'child_process'; import * as os from 'os'; import { Worker, isMainThread, workerData } from 'node:worker_threads'; +import { ConfidentialClientApplication } from '@azure/msal-node'; +import { BlobClient, BlobServiceClient, ContainerClient } from '@azure/storage-blob'; +import * as jws from 'jws'; function e(name: string): string { const result = process.env[name]; @@ -27,375 +29,495 @@ function e(name: string): string { return result; } -class Temp { - private _files: string[] = []; - - tmpNameSync(): string { - const file = path.join(os.tmpdir(), crypto.randomBytes(20).toString('hex')); - this._files.push(file); - return file; - } - - dispose(): void { - for (const file of this._files) { - try { - fs.unlinkSync(file); - } catch (err) { - // noop - } - } - } -} - -/** - * Gets an access token converted from a WIF/OIDC id token. - * We need this since this build job takes a while to run and while id tokens live for 10 minutes only, access tokens live for 24 hours. - * Source: https://goodworkaround.com/2021/12/21/another-deep-dive-into-azure-ad-workload-identity-federation-using-github-actions/ - */ -export async function getAccessToken(endpoint: string, tenantId: string, clientId: string, idToken: string): Promise { - const body = new URLSearchParams({ - scope: `${endpoint}.default`, - client_id: clientId, - grant_type: 'client_credentials', - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - client_assertion: encodeURIComponent(idToken) - }); - - const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: body.toString() - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const aadToken = await response.json(); - return aadToken.access_token; -} - -interface RequestOptions { - readonly body?: string; -} - -interface CreateProvisionedFilesSuccessResponse { - IsSuccess: true; - ErrorDetails: null; -} - -interface CreateProvisionedFilesErrorResponse { - IsSuccess: false; - ErrorDetails: { - Code: string; - Category: string; - Message: string; - CanRetry: boolean; - AdditionalProperties: Record; - }; -} - -type CreateProvisionedFilesResponse = CreateProvisionedFilesSuccessResponse | CreateProvisionedFilesErrorResponse; - -function isCreateProvisionedFilesErrorResponse(response: unknown): response is CreateProvisionedFilesErrorResponse { - return (response as CreateProvisionedFilesErrorResponse)?.ErrorDetails?.Code !== undefined; -} - -class ProvisionService { - - constructor( - private readonly log: (...args: any[]) => void, - private readonly accessToken: string - ) { } - - async provision(releaseId: string, fileId: string, fileName: string) { - const body = JSON.stringify({ - ReleaseId: releaseId, - PortalName: 'VSCode', - PublisherCode: 'VSCode', - ProvisionedFilesCollection: [{ - PublisherKey: fileId, - IsStaticFriendlyFileName: true, - FriendlyFileName: fileName, - MaxTTL: '1440', - CdnMappings: ['ECN'] - }] - }); - - this.log(`Provisioning ${fileName} (releaseId: ${releaseId}, fileId: ${fileId})...`); - const res = await retry(() => this.request('POST', '/api/v2/ProvisionedFiles/CreateProvisionedFiles', { body })); - - if (isCreateProvisionedFilesErrorResponse(res) && res.ErrorDetails.Code === 'FriendlyFileNameAlreadyProvisioned') { - this.log(`File already provisioned (most likley due to a re-run), skipping: ${fileName}`); - return; - } - - if (!res.IsSuccess) { - throw new Error(`Failed to submit provisioning request: ${JSON.stringify(res.ErrorDetails)}`); - } - - this.log(`Successfully provisioned ${fileName}`); - } - - private async request(method: string, url: string, options?: RequestOptions): Promise { - const opts: RequestInit = { - method, - body: options?.body, - headers: { - Authorization: `Bearer ${this.accessToken}`, - 'Content-Type': 'application/json' - } - }; - - const res = await fetch(`https://dsprovisionapi.microsoft.com${url}`, opts); - - - // 400 normally means the request is bad or something is already provisioned, so we will return as retries are useless - // Otherwise log the text body and headers. We do text because some responses are not JSON. - if ((!res.ok || res.status < 200 || res.status >= 500) && res.status !== 400) { - throw new Error(`Unexpected status code: ${res.status}\nResponse Headers: ${JSON.stringify(res.headers)}\nBody Text: ${await res.text()}`); - } - - return await res.json(); - } -} - -function hashStream(hashName: string, stream: Readable): Promise { - return new Promise((c, e) => { +function hashStream(hashName: string, stream: Readable): Promise { + return new Promise((c, e) => { const shasum = crypto.createHash(hashName); stream .on('data', shasum.update.bind(shasum)) .on('error', e) - .on('close', () => c(shasum.digest('hex'))); + .on('close', () => c(shasum.digest())); }); } -interface Release { - readonly releaseId: string; - readonly fileId: string; +interface ReleaseSubmitResponse { + operationId: string; + esrpCorrelationId: string; + code?: string; + message?: string; + target?: string; + innerError?: any; } -interface SubmitReleaseResult { - submissionResponse: { - operationId: string; - statusCode: string; - }; +interface ReleaseActivityInfo { + activityId: string; + activityType: string; + name: string; + status: string; + errorCode: number; + errorMessages: string[]; + beginTime?: Date; + endTime?: Date; + lastModifiedAt?: Date; } -interface ReleaseDetailsResult { - releaseDetails: [{ - fileDetails: [{ publisherKey: string }]; - statusCode: 'inprogress' | 'pass'; - }]; +interface InnerServiceError { + code: string; + details: { [key: string]: string }; + innerError?: InnerServiceError; } -class ESRPClient { +interface ReleaseError { + errorCode: number; + errorMessages: string[]; +} - private readonly authPath: string; +const enum StatusCode { + Pass = 'pass', + Inprogress = 'inprogress', + FailCanRetry = 'failCanRetry', + FailDoNotRetry = 'failDoNotRetry', + PendingAnalysis = 'pendingAnalysis', + Cancelled = 'cancelled' +} - constructor( - private readonly log: (...args: any[]) => void, - private readonly tmp: Temp, +interface ReleaseResultMessage { + activities: ReleaseActivityInfo[]; + childWorkflowType: string; + clientId: string; + customerCorrelationId: string; + errorInfo: InnerServiceError; + groupId: string; + lastModifiedAt: Date; + operationId: string; + releaseError: ReleaseError; + requestSubmittedAt: Date; + routedRegion: string; + status: StatusCode; + totalFileCount: number; + totalReleaseSize: number; + version: string; +} + +interface ReleaseFileInfo { + name?: string; + hash?: number[]; + sourceLocation?: FileLocation; + sizeInBytes?: number; + hashType?: FileHashType; + fileId?: any; + distributionRelativePath?: string; + partNumber?: string; + friendlyFileName?: string; + tenantFileLocationType?: string; + tenantFileLocation?: string; + signedEngineeringCopyLocation?: string; + encryptedDistributionBlobLocation?: string; + preEncryptedDistributionBlobLocation?: string; + secondaryDistributionHashRequired?: boolean; + secondaryDistributionHashType?: FileHashType; + lastModifiedAt?: Date; + cultureCodes?: string[]; + displayFileInDownloadCenter?: boolean; + isPrimaryFileInDownloadCenter?: boolean; + fileDownloadDetails?: FileDownloadDetails[]; +} + +interface ReleaseDetailsFileInfo extends ReleaseFileInfo { } + +interface ReleaseDetailsMessage extends ReleaseResultMessage { + clusterRegion: string; + correlationVector: string; + releaseCompletedAt?: Date; + releaseInfo: ReleaseInfo; + productInfo: ProductInfo; + createdBy: UserInfo; + owners: OwnerInfo[]; + accessPermissionsInfo: AccessPermissionsInfo; + files: ReleaseDetailsFileInfo[]; + comments: string[]; + cancellationReason: string; + downloadCenterInfo: DownloadCenterInfo; +} + + +interface ProductInfo { + name?: string; + version?: string; + description?: string; +} + +interface ReleaseInfo { + title?: string; + minimumNumberOfApprovers: number; + properties?: { [key: string]: string }; + isRevision?: boolean; + revisionNumber?: string; +} + +type FileLocationType = 'azureBlob'; + +interface FileLocation { + type: FileLocationType; + blobUrl: string; + uncPath?: string; + url?: string; +} + +type FileHashType = 'sha256' | 'sha1'; + +interface FileDownloadDetails { + portalName: string; + downloadUrl: string; +} + +interface RoutingInfo { + intent?: string; + contentType?: string; + contentOrigin?: string; + productState?: string; + audience?: string; +} + +interface ReleaseFileInfo { + name?: string; + hash?: number[]; + sourceLocation?: FileLocation; + sizeInBytes?: number; + hashType?: FileHashType; + fileId?: any; + distributionRelativePath?: string; + partNumber?: string; + friendlyFileName?: string; + tenantFileLocationType?: string; + tenantFileLocation?: string; + signedEngineeringCopyLocation?: string; + encryptedDistributionBlobLocation?: string; + preEncryptedDistributionBlobLocation?: string; + secondaryDistributionHashRequired?: boolean; + secondaryDistributionHashType?: FileHashType; + lastModifiedAt?: Date; + cultureCodes?: string[]; + displayFileInDownloadCenter?: boolean; + isPrimaryFileInDownloadCenter?: boolean; + fileDownloadDetails?: FileDownloadDetails[]; +} + +interface UserInfo { + userPrincipalName?: string; +} + +interface OwnerInfo { + owner: UserInfo; +} + +interface ApproverInfo { + approver: UserInfo; + isAutoApproved: boolean; + isMandatory: boolean; +} + +interface AccessPermissionsInfo { + mainPublisher?: string; + releasePublishers?: string[]; + channelDownloadEntityDetails?: { [key: string]: string[] }; +} + +interface DownloadCenterLocaleInfo { + cultureCode?: string; + downloadTitle?: string; + shortName?: string; + shortDescription?: string; + longDescription?: string; + instructions?: string; + additionalInfo?: string; + keywords?: string[]; + version?: string; + relatedLinks?: { [key: string]: URL }; +} + +interface DownloadCenterInfo { + downloadCenterId: number; + publishToDownloadCenter?: boolean; + publishingGroup?: string; + operatingSystems?: string[]; + relatedReleases?: string[]; + kbNumbers?: string[]; + sbNumbers?: string[]; + locales?: DownloadCenterLocaleInfo[]; + additionalProperties?: { [key: string]: string }; +} + +interface ReleaseRequestMessage { + driEmail: string[]; + groupId?: string; + customerCorrelationId: string; + esrpCorrelationId: string; + contextData?: { [key: string]: string }; + releaseInfo: ReleaseInfo; + productInfo: ProductInfo; + files: ReleaseFileInfo[]; + routingInfo?: RoutingInfo; + createdBy: UserInfo; + owners: OwnerInfo[]; + approvers: ApproverInfo[]; + accessPermissionsInfo: AccessPermissionsInfo; + jwsToken?: string; + publisherId?: string; + downloadCenterInfo?: DownloadCenterInfo; +} + +function getCertificateBuffer(input: string) { + return Buffer.from(input.replace(/-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----|\n/g, ''), 'base64'); +} + +function getThumbprint(input: string, algorithm: string): Buffer { + const buffer = getCertificateBuffer(input); + return crypto.createHash(algorithm).update(buffer).digest(); +} + +function getKeyFromPFX(pfx: string): string { + const pfxCertificatePath = path.join(os.tmpdir(), 'cert.pfx'); + const pemKeyPath = path.join(os.tmpdir(), 'key.pem'); + + try { + const pfxCertificate = Buffer.from(pfx, 'base64'); + fs.writeFileSync(pfxCertificatePath, pfxCertificate); + cp.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nocerts -nodes -out "${pemKeyPath}" -passin pass:`); + const raw = fs.readFileSync(pemKeyPath, 'utf-8'); + const result = raw.match(/-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g)![0]; + return result; + } finally { + fs.rmSync(pfxCertificatePath, { force: true }); + fs.rmSync(pemKeyPath, { force: true }); + } +} + +function getCertificatesFromPFX(pfx: string): string[] { + const pfxCertificatePath = path.join(os.tmpdir(), 'cert.pfx'); + const pemCertificatePath = path.join(os.tmpdir(), 'cert.pem'); + + try { + const pfxCertificate = Buffer.from(pfx, 'base64'); + fs.writeFileSync(pfxCertificatePath, pfxCertificate); + cp.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nokeys -out "${pemCertificatePath}" -passin pass:`); + const raw = fs.readFileSync(pemCertificatePath, 'utf-8'); + const matches = raw.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g); + return matches ? matches.reverse() : []; + } finally { + fs.rmSync(pfxCertificatePath, { force: true }); + fs.rmSync(pemCertificatePath, { force: true }); + } +} + +class ESRPReleaseService { + + static async create( + log: (...args: any[]) => void, tenantId: string, clientId: string, - authCertSubjectName: string, - requestSigningCertSubjectName: string, + authCertificatePfx: string, + requestSigningCertificatePfx: string, + containerClient: ContainerClient ) { - this.authPath = this.tmp.tmpNameSync(); - fs.writeFileSync(this.authPath, JSON.stringify({ - Version: '1.0.0', - AuthenticationType: 'AAD_CERT', - TenantId: tenantId, - ClientId: clientId, - AuthCert: { - SubjectName: authCertSubjectName, - StoreLocation: 'LocalMachine', - StoreName: 'My', - SendX5c: 'true' - }, - RequestSigningCert: { - SubjectName: requestSigningCertSubjectName, - StoreLocation: 'LocalMachine', - StoreName: 'My' + const authKey = getKeyFromPFX(authCertificatePfx); + const authCertificate = getCertificatesFromPFX(authCertificatePfx)[0]; + const requestSigningKey = getKeyFromPFX(requestSigningCertificatePfx); + const requestSigningCertificates = getCertificatesFromPFX(requestSigningCertificatePfx); + + const app = new ConfidentialClientApplication({ + auth: { + clientId, + authority: `https://login.microsoftonline.com/${tenantId}`, + clientCertificate: { + thumbprintSha256: getThumbprint(authCertificate, 'sha256').toString('hex'), + privateKey: authKey, + x5c: authCertificate + } } - })); + }); + + const response = await app.acquireTokenByClientCredential({ + scopes: ['https://api.esrp.microsoft.com/.default'] + }); + + return new ESRPReleaseService(log, clientId, response!.accessToken, requestSigningCertificates, requestSigningKey, containerClient); } - async release( - version: string, - filePath: string - ): Promise { - this.log(`Submitting release for ${version}: ${filePath}`); - const submitReleaseResult = await this.SubmitRelease(version, filePath); + private static API_URL = 'https://api.esrp.microsoft.com/api/v3/releaseservices/clients/'; - if (submitReleaseResult.submissionResponse.statusCode !== 'pass') { - throw new Error(`Unexpected status code: ${submitReleaseResult.submissionResponse.statusCode}`); - } + private constructor( + private readonly log: (...args: any[]) => void, + private readonly clientId: string, + private readonly accessToken: string, + private readonly requestSigningCertificates: string[], + private readonly requestSigningKey: string, + private readonly containerClient: ContainerClient + ) { } - const releaseId = submitReleaseResult.submissionResponse.operationId; - this.log(`Successfully submitted release ${releaseId}. Polling for completion...`); + async createRelease(version: string, filePath: string, friendlyFileName: string) { + const correlationId = crypto.randomUUID(); + const blobClient = this.containerClient.getBlockBlobClient(correlationId); - let details!: ReleaseDetailsResult; + this.log(`Uploading ${filePath} to ${blobClient.url}`); + await blobClient.uploadFile(filePath); + this.log('Uploaded blob successfully'); - // Poll every 5 seconds, wait 60 minutes max -> poll 60/5*60=720 times - for (let i = 0; i < 720; i++) { - details = await this.ReleaseDetails(releaseId); + try { + this.log(`Submitting release for ${version}: ${filePath}`); + const submitReleaseResult = await this.submitRelease(version, filePath, friendlyFileName, correlationId, blobClient); - if (details.releaseDetails[0].statusCode === 'pass') { - break; - } else if (details.releaseDetails[0].statusCode !== 'inprogress') { - throw new Error(`Failed to submit release: ${JSON.stringify(details)}`); + this.log(`Successfully submitted release ${submitReleaseResult.operationId}. Polling for completion...`); + + // Poll every 5 seconds, wait 60 minutes max -> poll 60/5*60=720 times + for (let i = 0; i < 720; i++) { + await new Promise(c => setTimeout(c, 5000)); + const releaseStatus = await this.getReleaseStatus(submitReleaseResult.operationId); + + if (releaseStatus.status === 'pass') { + break; + } else if (releaseStatus.status !== 'inprogress') { + throw new Error(`Failed to submit release: ${JSON.stringify(releaseStatus)}`); + } } - await new Promise(c => setTimeout(c, 5000)); + const releaseDetails = await this.getReleaseDetails(submitReleaseResult.operationId); + + if (releaseDetails.status !== 'pass') { + throw new Error(`Timed out waiting for release: ${JSON.stringify(releaseDetails)}`); + } + + this.log('Successfully created release:', releaseDetails.files[0].fileDownloadDetails![0].downloadUrl); + return releaseDetails.files[0].fileDownloadDetails![0].downloadUrl; + } finally { + this.log(`Deleting blob ${blobClient.url}`); + await blobClient.delete(); + this.log('Deleted blob successfully'); } - - if (details.releaseDetails[0].statusCode !== 'pass') { - throw new Error(`Timed out waiting for release ${releaseId}: ${JSON.stringify(details)}`); - } - - const fileId = details.releaseDetails[0].fileDetails[0].publisherKey; - this.log('Release completed successfully with fileId: ', fileId); - - return { releaseId, fileId }; } - private async SubmitRelease( + private async submitRelease( version: string, - filePath: string - ): Promise { - const policyPath = this.tmp.tmpNameSync(); - fs.writeFileSync(policyPath, JSON.stringify({ - Version: '1.0.0', - Audience: 'InternalLimited', - Intent: 'distribution', - ContentType: 'InstallPackage' - })); - - const inputPath = this.tmp.tmpNameSync(); + filePath: string, + friendlyFileName: string, + correlationId: string, + blobClient: BlobClient + ): Promise { const size = fs.statSync(filePath).size; - const istream = fs.createReadStream(filePath); - const sha256 = await hashStream('sha256', istream); - fs.writeFileSync(inputPath, JSON.stringify({ - Version: '1.0.0', - ReleaseInfo: { - ReleaseMetadata: { - Title: 'VS Code', - Properties: { - ReleaseContentType: 'InstallPackage' - }, - MinimumNumberOfApprovers: 1 + const hash = await hashStream('sha256', fs.createReadStream(filePath)); + + const message: ReleaseRequestMessage = { + customerCorrelationId: correlationId, + esrpCorrelationId: correlationId, + driEmail: ['joao.moreno@microsoft.com'], + createdBy: { userPrincipalName: 'jomo@microsoft.com' }, + owners: [{ owner: { userPrincipalName: 'jomo@microsoft.com' } }], + approvers: [{ approver: { userPrincipalName: 'jomo@microsoft.com' }, isAutoApproved: true, isMandatory: false }], + releaseInfo: { + title: 'VS Code', + properties: { + 'ReleaseContentType': 'InstallPackage' }, - ProductInfo: { - Name: 'VS Code', - Version: version, - Description: path.basename(filePath, path.extname(filePath)), - }, - Owners: [ - { - Owner: { - UserPrincipalName: 'jomo@microsoft.com' - } - } - ], - Approvers: [ - { - Approver: { - UserPrincipalName: 'jomo@microsoft.com' - }, - IsAutoApproved: true, - IsMandatory: false - } - ], - AccessPermissions: { - MainPublisher: 'VSCode', - ChannelDownloadEntityDetails: { - Consumer: ['VSCode'] - } - }, - CreatedBy: { - UserPrincipalName: 'jomo@microsoft.com' + minimumNumberOfApprovers: 1 + }, + productInfo: { + name: 'VS Code', + version, + description: 'VS Code' + }, + accessPermissionsInfo: { + mainPublisher: 'VSCode', + channelDownloadEntityDetails: { + AllDownloadEntities: ['VSCode'] } }, - ReleaseBatches: [ - { - ReleaseRequestFiles: [ - { - SizeInBytes: size, - SourceHash: sha256, - HashType: 'SHA256', - SourceLocation: path.basename(filePath) - } - ], - SourceLocationType: 'UNC', - SourceRootDirectory: path.dirname(filePath), - DestinationLocationType: 'AzureBlob' - } - ] - })); + routingInfo: { + intent: 'filedownloadlinkgeneration' + }, + files: [{ + name: path.basename(filePath), + friendlyFileName, + tenantFileLocation: blobClient.url, + tenantFileLocationType: 'AzureBlob', + sourceLocation: { + type: 'azureBlob', + blobUrl: blobClient.url + }, + hashType: 'sha256', + hash: Array.from(hash), + sizeInBytes: size + }] + }; - const outputPath = this.tmp.tmpNameSync(); - cp.execSync(`ESRPClient SubmitRelease -a ${this.authPath} -p ${policyPath} -i ${inputPath} -o ${outputPath}`, { stdio: 'inherit' }); + message.jwsToken = await this.generateJwsToken(message); - const output = fs.readFileSync(outputPath, 'utf8'); - return JSON.parse(output) as SubmitReleaseResult; + const res = await fetch(`${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.accessToken}` + }, + body: JSON.stringify(message) + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to submit release: ${res.statusText}\n${text}`); + } + + return await res.json() as ReleaseSubmitResponse; } - private async ReleaseDetails( - releaseId: string - ): Promise { - const inputPath = this.tmp.tmpNameSync(); - fs.writeFileSync(inputPath, JSON.stringify({ - Version: '1.0.0', - OperationIds: [releaseId] - })); + private async getReleaseStatus(releaseId: string): Promise { + const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grs/${releaseId}`; - const outputPath = this.tmp.tmpNameSync(); - cp.execSync(`ESRPClient ReleaseDetails -a ${this.authPath} -i ${inputPath} -o ${outputPath}`, { stdio: 'inherit' }); + const res = await fetch(url, { + headers: { + 'Authorization': `Bearer ${this.accessToken}` + } + }); - const output = fs.readFileSync(outputPath, 'utf8'); - return JSON.parse(output) as ReleaseDetailsResult; - } -} + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to get release status: ${res.statusText}\n${text}`); + } -async function releaseAndProvision( - log: (...args: any[]) => void, - releaseTenantId: string, - releaseClientId: string, - releaseAuthCertSubjectName: string, - releaseRequestSigningCertSubjectName: string, - provisionTenantId: string, - provisionAADUsername: string, - provisionAADPassword: string, - version: string, - quality: string, - filePath: string -): Promise { - const fileName = `${quality}/${version}/${path.basename(filePath)}`; - const result = `${e('PRSS_CDN_URL')}/${fileName}`; - - const res = await retry(() => fetch(result)); - - if (res.status === 200) { - log(`Already released and provisioned: ${result}`); - return result; + return await res.json() as ReleaseResultMessage; } - const tmp = new Temp(); - process.on('exit', () => tmp.dispose()); + private async getReleaseDetails(releaseId: string): Promise { + const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grd/${releaseId}`; - const esrpclient = new ESRPClient(log, tmp, releaseTenantId, releaseClientId, releaseAuthCertSubjectName, releaseRequestSigningCertSubjectName); - const release = await esrpclient.release(version, filePath); + const res = await fetch(url, { + headers: { + 'Authorization': `Bearer ${this.accessToken}` + } + }); - const credential = new ClientSecretCredential(provisionTenantId, provisionAADUsername, provisionAADPassword); - const accessToken = await credential.getToken(['https://microsoft.onmicrosoft.com/DS.Provisioning.WebApi/.default']); - const service = new ProvisionService(log, accessToken.token); - await service.provision(release.releaseId, release.fileId, fileName); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to get release status: ${res.statusText}\n${text}`); + } - return result; + return await res.json() as ReleaseDetailsMessage; + } + + private async generateJwsToken(message: ReleaseRequestMessage): Promise { + return jws.sign({ + header: { + alg: 'RS256', + crit: ['exp', 'x5t'], + // Release service uses ticks, not seconds :roll_eyes: (https://stackoverflow.com/a/7968483) + exp: ((Date.now() + (6 * 60 * 1000)) * 10000) + 621355968000000000, + // Release service uses hex format, not base64url :roll_eyes: + x5t: getThumbprint(this.requestSigningCertificates[0], 'sha1').toString('hex'), + // Release service uses a '.' separated string, not an array of strings :roll_eyes: + x5c: this.requestSigningCertificates.map(c => getCertificateBuffer(c).toString('base64url')).join('.') as any, + }, + payload: message, + privateKey: this.requestSigningKey, + }); + } } class State { @@ -666,8 +788,10 @@ function getRealType(type: string) { } } -async function processArtifact(artifact: Artifact, artifactFilePath: string, cosmosDBAccessToken: string): Promise { - const log = (...args: any[]) => console.log(`[${artifact.name}]`, ...args); +async function processArtifact( + artifact: Artifact, + filePath: string +) { const match = /^vscode_(?[^_]+)_(?[^_]+)(?:_legacy)?_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); if (!match) { @@ -675,38 +799,48 @@ async function processArtifact(artifact: Artifact, artifactFilePath: string, cos } // getPlatform needs the unprocessedType + const { cosmosDBAccessToken, blobServiceAccessToken } = JSON.parse(e('PUBLISH_AUTH_TOKENS')); const quality = e('VSCODE_QUALITY'); - const commit = e('BUILD_SOURCEVERSION'); + const version = e('BUILD_SOURCEVERSION'); const { product, os, arch, unprocessedType } = match.groups!; const isLegacy = artifact.name.includes('_legacy'); const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); const type = getRealType(unprocessedType); - const size = fs.statSync(artifactFilePath).size; - const stream = fs.createReadStream(artifactFilePath); + const size = fs.statSync(filePath).size; + const stream = fs.createReadStream(filePath); const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 - const url = await releaseAndProvision( + const log = (...args: any[]) => console.log(`[${artifact.name}]`, ...args); + const blobServiceClient = new BlobServiceClient(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, { getToken: async () => blobServiceAccessToken }); + const containerClient = blobServiceClient.getContainerClient('staging'); + + const releaseService = await ESRPReleaseService.create( log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), - e('RELEASE_AUTH_CERT_SUBJECT_NAME'), - e('RELEASE_REQUEST_SIGNING_CERT_SUBJECT_NAME'), - e('PROVISION_TENANT_ID'), - e('PROVISION_AAD_USERNAME'), - e('PROVISION_AAD_PASSWORD'), - commit, - quality, - artifactFilePath + e('RELEASE_AUTH_CERT'), + e('RELEASE_REQUEST_SIGNING_CERT'), + containerClient ); - const asset: Asset = { platform, type, url, hash, sha256hash, size, supportsFastUpdate: true }; + const friendlyFileName = `${quality}/${version}/${path.basename(filePath)}`; + const url = `${e('PRSS_CDN_URL')}/${friendlyFileName}`; + const res = await retry(() => fetch(url)); + + if (res.status === 200) { + log(`Already released and provisioned: ${url}`); + } else { + await releaseService.createRelease(version, filePath, friendlyFileName); + } + + const asset: Asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; log('Creating asset...', JSON.stringify(asset, undefined, 2)); await retry(async (attempt) => { log(`Creating asset in Cosmos DB (attempt ${attempt})...`); - const client = new CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT')!, tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken}`) }); + const client = new CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT')!, tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); const scripts = client.database('builds').container(quality).scripts; - await scripts.storedProcedure('createAsset').execute('', [commit, asset, true]); + await scripts.storedProcedure('createAsset').execute('', [version, asset, true]); }); log('Asset successfully created'); @@ -720,8 +854,8 @@ async function processArtifact(artifact: Artifact, artifactFilePath: string, cos // the CDN and finally update the build in Cosmos DB. async function main() { if (!isMainThread) { - const { artifact, artifactFilePath, cosmosDBAccessToken } = workerData; - await processArtifact(artifact, artifactFilePath, cosmosDBAccessToken); + const { artifact, artifactFilePath } = workerData; + await processArtifact(artifact, artifactFilePath); return; } @@ -742,7 +876,6 @@ async function main() { let resultPromise = Promise.resolve[]>([]); const operations: { name: string; operation: Promise }[] = []; - const cosmosDBAccessToken = await getAccessToken(e('AZURE_DOCUMENTDB_ENDPOINT')!, e('AZURE_TENANT_ID')!, e('AZURE_CLIENT_ID')!, e('AZURE_ID_TOKEN')!); while (true) { const [timeline, artifacts] = await Promise.all([retry(() => getPipelineTimeline()), retry(() => getPipelineArtifacts())]); @@ -784,7 +917,7 @@ async function main() { processing.add(artifact.name); const promise = new Promise((resolve, reject) => { - const worker = new Worker(__filename, { workerData: { artifact, artifactFilePath, cosmosDBAccessToken } }); + const worker = new Worker(__filename, { workerData: { artifact, artifactFilePath } }); worker.on('error', reject); worker.on('exit', code => { if (code === 0) { diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index e52274ae1f2..5aad3d4ffb0 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -134,14 +134,14 @@ variables: value: ${{ eq(parameters.VSCODE_STEP_ON_IT, true) }} - name: VSCODE_BUILD_MACOS_UNIVERSAL value: ${{ and(eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true), eq(parameters.VSCODE_BUILD_MACOS_UNIVERSAL, true)) }} + - name: VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME + value: vscodeesrp - name: PRSS_CDN_URL value: https://vscode.download.prss.microsoft.com/dbazure/download - name: PRSS_RELEASE_TENANT_ID value: 975f013f-7f24-47e8-a7d3-abc4752bf346 - name: PRSS_RELEASE_CLIENT_ID value: c24324f7-e65f-4c45-8702-ed2d4c35df99 - - name: PRSS_PROVISION_TENANT_ID - value: 72f988bf-86f1-41af-91ab-2d7cd011db47 - name: AZURE_DOCUMENTDB_ENDPOINT value: https://vscode.documents.azure.com/ - name: VSCODE_MIXIN_REPO diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index a175cf9571c..55cf61dd844 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -20,7 +20,7 @@ steps: inputs: azureSubscription: vscode-esrp KeyVaultName: vscode-esrp - SecretsFilter: "esrp-auth,esrp-sign,esrp-aad-username,esrp-aad-password" + SecretsFilter: esrp-auth,esrp-sign # allow-any-unicode-next-line - pwsh: Write-Host "##vso[build.addbuildtag]🚀" @@ -65,22 +65,13 @@ steps: displayName: Create build if it hasn't been created before - pwsh: | - $ErrorActionPreference = "Stop" - $CertCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection - $AuthCertBytes = [System.Convert]::FromBase64String("$(esrp-auth)") - $CertCollection.Import($AuthCertBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bxor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet) - $RequestSigningCertIndex = $CertCollection.Count - $RequestSigningCertBytes = [System.Convert]::FromBase64String("$(esrp-sign)") - $CertCollection.Import($RequestSigningCertBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bxor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet) - $CertStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine") - $CertStore.Open("ReadWrite") - $CertStore.AddRange($CertCollection) - $CertStore.Close() - $AuthCertSubjectName = $CertCollection[0].Subject - $RequestSigningCertSubjectName = $CertCollection[$RequestSigningCertIndex].Subject - Write-Host "##vso[task.setvariable variable=RELEASE_AUTH_CERT_SUBJECT_NAME]$AuthCertSubjectName" - Write-Host "##vso[task.setvariable variable=RELEASE_REQUEST_SIGNING_CERT_SUBJECT_NAME]$RequestSigningCertSubjectName" - displayName: Import certificates + $publishAuthTokens = (node build/azure-pipelines/common/getPublishAuthTokens) + Write-Host "##vso[task.setvariable variable=PUBLISH_AUTH_TOKENS;issecret=true]$publishAuthTokens" + env: + AZURE_TENANT_ID: "$(AZURE_TENANT_ID)" + AZURE_CLIENT_ID: "$(AZURE_CLIENT_ID)" + AZURE_ID_TOKEN: "$(AZURE_ID_TOKEN)" + displayName: Get publish auth tokens - pwsh: node build/azure-pipelines/common/publish.js env: @@ -89,13 +80,11 @@ steps: AZURE_CLIENT_ID: "$(AZURE_CLIENT_ID)" AZURE_ID_TOKEN: "$(AZURE_ID_TOKEN)" SYSTEM_ACCESSTOKEN: $(System.AccessToken) + PUBLISH_AUTH_TOKENS: "$(PUBLISH_AUTH_TOKENS)" RELEASE_TENANT_ID: "$(PRSS_RELEASE_TENANT_ID)" RELEASE_CLIENT_ID: "$(PRSS_RELEASE_CLIENT_ID)" - RELEASE_AUTH_CERT_SUBJECT_NAME: "$(RELEASE_AUTH_CERT_SUBJECT_NAME)" - RELEASE_REQUEST_SIGNING_CERT_SUBJECT_NAME: "$(RELEASE_REQUEST_SIGNING_CERT_SUBJECT_NAME)" - PROVISION_TENANT_ID: "$(PRSS_PROVISION_TENANT_ID)" - PROVISION_AAD_USERNAME: "$(esrp-aad-username)" - PROVISION_AAD_PASSWORD: "$(esrp-aad-password)" + RELEASE_AUTH_CERT: "$(esrp-auth)" + RELEASE_REQUEST_SIGNING_CERT: "$(esrp-sign)" displayName: Process artifacts retryCountOnTaskFailure: 3 diff --git a/build/package-lock.json b/build/package-lock.json index 1e373f2e68f..e8f5ce67f24 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -9,8 +9,11 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@azure/core-auth": "^1.9.0", "@azure/cosmos": "^3", "@azure/identity": "^4.2.1", + "@azure/msal-node": "^2.16.1", + "@azure/storage-blob": "^12.25.0", "@electron/get": "^2.0.0", "@types/ansi-colors": "^3.2.0", "@types/byline": "^4.2.32", @@ -26,6 +29,7 @@ "@types/gulp-rename": "^0.0.33", "@types/gulp-sort": "^2.0.4", "@types/gulp-sourcemaps": "^0.0.32", + "@types/jws": "^3.2.10", "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", "@types/minimist": "^1.2.1", @@ -47,6 +51,7 @@ "gulp-merge-json": "^2.1.1", "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", + "jws": "^4.0.0", "mime": "^1.4.1", "source-map": "0.6.1", "ternary-stream": "^3.0.0", @@ -73,107 +78,188 @@ "node": ">=8.0.0" } }, - "node_modules/@azure/core-asynciterator-polyfill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.0.tgz", - "integrity": "sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==", - "dev": true - }, "node_modules/@azure/core-auth": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", - "integrity": "sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-util": "^1.1.0", - "tslib": "^2.2.0" + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@azure/core-client": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.5.0.tgz", - "integrity": "sha512-YNk8i9LT6YcFdFO+RRU0E4Ef+A8Y5lhXo6lz61rwbG8Uo7kSqh0YqK04OexiilM43xd6n3Y9yBhLnb1NFNI9dA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-asynciterator-polyfill": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-rest-pipeline": "^1.5.0", - "@azure/core-tracing": "1.0.0-preview.13", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", "@azure/logger": "^1.0.0", - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-client/node_modules/@azure/core-tracing": { - "version": "1.0.0-preview.13", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", - "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, + "license": "MIT", "dependencies": { - "@opentelemetry/api": "^1.0.1", - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", + "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.7.0.tgz", - "integrity": "sha512-e2awPzwMKHrmvYgZ0qIKNkqnCM1QoDs7A0rOiS3OSAlOQOz/kL7PPKHXwFMuBeaRvS8i7fgobJn79q2Cji5f+Q==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz", + "integrity": "sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-tracing": "1.0.0-preview.13", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", - "form-data": "^4.0.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "tslib": "^2.2.0", - "uuid": "^8.3.0" + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-rest-pipeline/node_modules/@azure/core-tracing": { - "version": "1.0.0-preview.13", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", - "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, + "license": "MIT", "dependencies": { - "@opentelemetry/api": "^1.0.1", - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/core-tracing": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", - "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", + "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.2.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/@azure/core-util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", - "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", "dev": true, + "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", "tslib": "^2.6.2" @@ -194,6 +280,20 @@ "node": ">=18.0.0" } }, + "node_modules/@azure/core-xml": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.4.4.tgz", + "integrity": "sha512-J4FYAqakGXcbfeZjwjMzjNcpcH4E+JtEBv+xcV1yL0Ydn/6wbQfeFKTCHh9wttAi0lmajHw7yBbHPRG+YHckZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@azure/cosmos": { "version": "3.17.3", "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.17.3.tgz", @@ -277,12 +377,13 @@ } }, "node_modules/@azure/msal-node": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", - "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.1.tgz", + "integrity": "sha512-1NEFpTmMMT2A7RnZuvRl/hUmJU+GLPjh+ShyIqPktG2PvSd2yvPnzGd/BxIBAAvJG5nr9lH4oYcQXepDbaE7fg==", "dev": true, + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.12.0", + "@azure/msal-common": "14.16.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -290,6 +391,54 @@ "node": ">=16" } }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "14.16.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", + "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.25.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.25.0.tgz", + "integrity": "sha512-oodouhA3nCCIh843tMMbxty3WqfNT+Vgzj3Xo5jqR9UPnzq3d7mzLjlHAYz7lW+b4km3SIgz+NAgztvhm7Z6kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.4.0", + "@azure/core-client": "^1.6.2", + "@azure/core-http-compat": "^2.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.10.1", + "@azure/core-tracing": "^1.1.2", + "@azure/core-util": "^1.6.1", + "@azure/core-xml": "^1.4.3", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-blob/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@electron/asar": { "version": "3.2.10", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.10.tgz", @@ -743,15 +892,6 @@ "node": ">= 12.13.0" } }, - "node_modules/@opentelemetry/api": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.3.tgz", - "integrity": "sha512-puWxACExDe9nxbBB3lOymQFrLYml2dVOrd7USiVRnSbgXE+KwBu+HxFvxrzfqsiSda9IWsXJG1ef7C1O2/GmKQ==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -776,15 +916,6 @@ "node": ">=10" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/@types/ansi-colors": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@types/ansi-colors/-/ansi-colors-3.2.0.tgz", @@ -969,6 +1100,16 @@ "integrity": "sha512-/siF86XrwDKLuHe8l7h6NhrAWgLdgqbxmjZv9NvGWmgYRZoTipkjKiWb0SQHy/jcR+ee0GvbG6uGd+LEBMGNvA==", "dev": true }, + "node_modules/@types/jws": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@types/jws/-/jws-3.2.10.tgz", + "integrity": "sha512-cOevhttJmssERB88/+XvZXvsq5m9JLKZNUiGfgjUb5lcPRdV2ZQciU6dU76D/qXXFYpSqkP3PrSg4hMTiafTZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -1197,6 +1338,16 @@ "node": ">=10" } }, + "node_modules/@vscode/vsce/node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -1207,15 +1358,16 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, + "license": "MIT", "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ansi-colors": { @@ -1324,12 +1476,6 @@ "node": ">= 0.10" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, "node_modules/azure-devops-node-api": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", @@ -1705,18 +1851,6 @@ "color-support": "bin.js" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", @@ -1881,15 +2015,6 @@ "node": ">= 0.4" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-libc": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", @@ -2207,6 +2332,29 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -2261,20 +2409,6 @@ "integrity": "sha512-Pqq5NnT78ehvUnAk/We/Jr22vSvanRlFTpAmQ88xBY/M1TlHe+P0ILuEyXS595ysdGfaj22634LBkGMA2GTcpA==", "dev": true }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -2631,17 +2765,17 @@ "dev": true }, "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, + "license": "MIT", "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/http2-wrapper": { @@ -2658,16 +2792,17 @@ } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/ieee754": { @@ -2959,6 +3094,7 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "dev": true, + "license": "MIT", "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" @@ -3111,27 +3247,6 @@ "node": ">=4" } }, - "node_modules/mime-db": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", - "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.28", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", - "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", - "dev": true, - "dependencies": { - "mime-db": "1.45.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -3904,6 +4019,13 @@ "node": ">=0.10.0" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true, + "license": "MIT" + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -4371,15 +4493,6 @@ "fd-slicer": "~1.1.0" } }, - "node_modules/yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/build/package.json b/build/package.json index aa94a210e9c..c85b600983f 100644 --- a/build/package.json +++ b/build/package.json @@ -3,8 +3,11 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@azure/core-auth": "^1.9.0", "@azure/cosmos": "^3", "@azure/identity": "^4.2.1", + "@azure/msal-node": "^2.16.1", + "@azure/storage-blob": "^12.25.0", "@electron/get": "^2.0.0", "@types/ansi-colors": "^3.2.0", "@types/byline": "^4.2.32", @@ -20,6 +23,7 @@ "@types/gulp-rename": "^0.0.33", "@types/gulp-sort": "^2.0.4", "@types/gulp-sourcemaps": "^0.0.32", + "@types/jws": "^3.2.10", "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", "@types/minimist": "^1.2.1", @@ -41,6 +45,7 @@ "gulp-merge-json": "^2.1.1", "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", + "jws": "^4.0.0", "mime": "^1.4.1", "source-map": "0.6.1", "ternary-stream": "^3.0.0",