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:
Родитель
a57b852e31
Коммит
681164aaaa
|
@ -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'
|
||||
}
|
||||
}));
|
||||
}
|
||||
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_(?<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 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<Release> {
|
||||
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<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'
|
||||
},
|
||||
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<ReleaseDetailsResult> {
|
||||
const inputPath = this.tmp.tmpNameSync();
|
||||
fs.writeFileSync(inputPath, JSON.stringify({
|
||||
Version: '1.0.0',
|
||||
OperationIds: [releaseId]
|
||||
}));
|
||||
private async getReleaseStatus(releaseId: string): Promise<ReleaseResultMessage> {
|
||||
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<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 ReleaseResultMessage;
|
||||
}
|
||||
|
||||
const tmp = new Temp();
|
||||
process.on('exit', () => tmp.dispose());
|
||||
private async getReleaseDetails(releaseId: string): Promise<ReleaseDetailsMessage> {
|
||||
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<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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
Загрузка…
Ссылка в новой задаче