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
This commit is contained in:
João Moreno 2024-11-15 11:54:43 +01:00 коммит произвёл GitHub
Родитель a57b852e31
Коммит 681164aaaa
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
8 изменённых файлов: 1120 добавлений и 798 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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<AccessToken> {
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);
});
}

Просмотреть файл

@ -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'
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');
}
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;
}
}));
}
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);
finally {
fs.rmSync(pfxCertificatePath, { force: true });
fs.rmSync(pemKeyPath, { force: true });
}
}
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}`);
return result;
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,
});
}
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;
}
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_(?<product>[^_]+)_(?<os>[^_]+)(?:_legacy)?_(?<arch>[^_]+)_(?<unprocessedType>[^_]+)$/.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) {

Просмотреть файл

@ -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<string> {
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<string, string>;
};
}
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<CreateProvisionedFilesResponse>('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<T>(method: string, url: string, options?: RequestOptions): Promise<T> {
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<string> {
return new Promise<string>((c, e) => {
function hashStream(hashName: string, stream: Readable): Promise<Buffer> {
return new Promise<Buffer>((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 SubmitReleaseResult {
submissionResponse: {
interface ReleaseSubmitResponse {
operationId: string;
statusCode: string;
};
esrpCorrelationId: string;
code?: string;
message?: string;
target?: string;
innerError?: any;
}
interface ReleaseDetailsResult {
releaseDetails: [{
fileDetails: [{ publisherKey: string }];
statusCode: 'inprogress' | 'pass';
}];
interface ReleaseActivityInfo {
activityId: string;
activityType: string;
name: string;
status: string;
errorCode: number;
errorMessages: string[];
beginTime?: Date;
endTime?: Date;
lastModifiedAt?: Date;
}
class ESRPClient {
interface InnerServiceError {
code: string;
details: { [key: string]: string };
innerError?: InnerServiceError;
}
private readonly authPath: string;
interface ReleaseError {
errorCode: number;
errorMessages: string[];
}
constructor(
private readonly log: (...args: any[]) => void,
private readonly tmp: Temp,
const enum StatusCode {
Pass = 'pass',
Inprogress = 'inprogress',
FailCanRetry = 'failCanRetry',
FailDoNotRetry = 'failDoNotRetry',
PendingAnalysis = 'pendingAnalysis',
Cancelled = 'cancelled'
}
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<Release> {
private static API_URL = 'https://api.esrp.microsoft.com/api/v3/releaseservices/clients/';
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
) { }
async createRelease(version: string, filePath: string, friendlyFileName: string) {
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);
const submitReleaseResult = await this.submitRelease(version, filePath, friendlyFileName, correlationId, blobClient);
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!: ReleaseDetailsResult;
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++) {
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));
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)}`);
}
}
if (details.releaseDetails[0].statusCode !== 'pass') {
throw new Error(`Timed out waiting for release ${releaseId}: ${JSON.stringify(details)}`);
const releaseDetails = await this.getReleaseDetails(submitReleaseResult.operationId);
if (releaseDetails.status !== 'pass') {
throw new Error(`Timed out waiting for release: ${JSON.stringify(releaseDetails)}`);
}
const fileId = details.releaseDetails[0].fileDetails[0].publisherKey;
this.log('Release completed successfully with fileId: ', fileId);
return { releaseId, fileId };
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');
}
}
private async SubmitRelease(
private async submitRelease(
version: string,
filePath: string
): Promise<SubmitReleaseResult> {
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<ReleaseSubmitResponse> {
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'
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'
},
MinimumNumberOfApprovers: 1
minimumNumberOfApprovers: 1
},
ProductInfo: {
Name: 'VS Code',
Version: version,
Description: path.basename(filePath, path.extname(filePath)),
productInfo: {
name: 'VS Code',
version,
description: 'VS Code'
},
Owners: [
{
Owner: {
UserPrincipalName: 'jomo@microsoft.com'
}
}
],
Approvers: [
{
Approver: {
UserPrincipalName: 'jomo@microsoft.com'
},
IsAutoApproved: true,
IsMandatory: false
}
],
AccessPermissions: {
MainPublisher: 'VSCode',
ChannelDownloadEntityDetails: {
Consumer: ['VSCode']
accessPermissionsInfo: {
mainPublisher: 'VSCode',
channelDownloadEntityDetails: {
AllDownloadEntities: ['VSCode']
}
},
CreatedBy: {
UserPrincipalName: 'jomo@microsoft.com'
}
routingInfo: {
intent: 'filedownloadlinkgeneration'
},
ReleaseBatches: [
{
ReleaseRequestFiles: [
{
SizeInBytes: size,
SourceHash: sha256,
HashType: 'SHA256',
SourceLocation: path.basename(filePath)
}
],
SourceLocationType: 'UNC',
SourceRootDirectory: path.dirname(filePath),
DestinationLocationType: 'AzureBlob'
}
]
}));
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}`);
}
private async ReleaseDetails(
releaseId: string
): Promise<ReleaseDetailsResult> {
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) as ReleaseDetailsResult;
}
}
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<string> {
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 ReleaseSubmitResponse;
}
const tmp = new Temp();
process.on('exit', () => tmp.dispose());
private async getReleaseStatus(releaseId: string): Promise<ReleaseResultMessage> {
const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grs/${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 ReleaseResultMessage;
}
private async getReleaseDetails(releaseId: string): Promise<ReleaseDetailsMessage> {
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() as ReleaseDetailsMessage;
}
private async generateJwsToken(message: ReleaseRequestMessage): Promise<string> {
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<void> {
const log = (...args: any[]) => console.log(`[${artifact.name}]`, ...args);
async function processArtifact(
artifact: Artifact,
filePath: string
) {
const match = /^vscode_(?<product>[^_]+)_(?<os>[^_]+)(?:_legacy)?_(?<arch>[^_]+)_(?<unprocessedType>[^_]+)$/.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<PromiseSettledResult<void>[]>([]);
const operations: { name: string; operation: Promise<void> }[] = [];
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<void>((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) {

Просмотреть файл

@ -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

Просмотреть файл

@ -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

447
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",

Просмотреть файл

@ -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",