Skip to content

Adding support for .yarnrc.yml files. #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules
.store
dist
lib
.npmrc
.npmrc
.vscode
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@
"lage": "^2.7.16",
"typescript": "^5.4.2",
"prettier": "^3.2.5"
},
"engines": {
"node": ">=16",
"pnpm": "8"
}
}
4 changes: 3 additions & 1 deletion packages/ado-npm-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
"dependencies": {
"@npmcli/config": "^4.0.1",
"azureauth": "^0.4.5",
"workspace-tools": "^0.26.3"
"workspace-tools": "^0.26.3",
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@types/js-yaml": "4.0.9",
"@types/node": "^20.5.9",
"@types/npmcli__config": "^6.0.0",
"eslint": "^8.30.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { toBase64 } from "../utils/encoding.js";
import { makeRequest } from "../utils/request.js";

/**
Expand All @@ -14,7 +15,7 @@ export const makeADORequest = async ({ password, organization }: {
password: string;
organization: string;
}) => {
const auth = `Basic ${Buffer.from(`.:${password}`).toString("base64")}`;
const auth = `Basic ${toBase64(`.:${password}`)}`;

const options = {
hostname: "feeds.dev.azure.com",
Expand Down
15 changes: 15 additions & 0 deletions packages/ado-npm-auth/src/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface Args {
doValidCheck: boolean;
skipAuth: boolean;
configFile?: string;
}

export function parseArgs(args: string[]) : Args {
const doValidCheck = !args.includes("--skip-check");
const skipAuth = args.includes("--skip-auth");

return {
doValidCheck,
skipAuth
}
}
79 changes: 68 additions & 11 deletions packages/ado-npm-auth/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,87 @@ import { isSupportedPlatformAndArchitecture } from "./azureauth/is-supported-pla
import { isCodespaces } from "./utils/is-codespaces.js";
import { logTelemetry } from "./telemetry/index.js";
import { arch, platform } from "os";
import { isValidPat } from "./npmrc/is-valid-pat.js";
import { setNpmrcPat } from "./npmrc/set-npmrc-pat.js";
import { Args, parseArgs } from "./args.js";
import { NpmrcFileProvider } from "./npmrc/npmrcFileProvider.js";
import { defaultEmail, defaultUser, ValidatedFeed } from "./fileProvider.js";
import { generateNpmrcPat } from "./npmrc/generate-npmrc-pat.js";
import { partition } from "./utils/partition.js";
import { YarnRcFileProvider } from "./yarnrc/yarnrcFileProvider.js";

export const run = async (): Promise<null | boolean> => {
const doValidCheck = !process.argv.includes("--skip-check");
const skipAuth = process.argv.includes("--skip-auth");
const fileProviders = [new NpmrcFileProvider(), new YarnRcFileProvider()]

export const run = async (args: Args): Promise<null | boolean> => {

const validatedFeeds: ValidatedFeed[] = [];
if (args.doValidCheck || args.skipAuth) {
for (const fileProvider of fileProviders) {
if (await fileProvider.isSupportedInRepo()) {
validatedFeeds.push(...await fileProvider.validateAllUsedFeeds());
}
}
}

if (doValidCheck && (await isValidPat())) {
const invalidFeeds = validatedFeeds.filter(feed => !feed.isValid);
const invalidFeedCount = invalidFeeds.length;

if (args.doValidCheck && invalidFeedCount == 0) {
return null;
}

if (skipAuth && !(await isValidPat())) {
if (args.skipAuth && invalidFeedCount != 0) {
logTelemetry(
{ success: false, automaticSuccess: false, error: "invalid token" },
{ success: false, automaticSuccess: false, error: "invalid token(s)" },
true
);
console.log(
"❌ Your token is invalid."
invalidFeedCount == 1
? "❌ Your token is invalid."
: `❌ ${invalidFeedCount} tokens are invalid.`
);
return false;
}

try {
console.log("🔑 Authenticating to package feed...")
await setNpmrcPat();

const adoOrgs = new Set<string>();
for (const adoOrg of invalidFeeds.map(feed => feed.feed.adoOrganization))
{
adoOrgs.add(adoOrg);
}

// get a token for each feed
const organizationPatMap: Record<string, string> = {};
for (const adoOrg of adoOrgs) {
organizationPatMap[adoOrg] = await generateNpmrcPat(adoOrg, false);
}

// Update the pat in the invalid feeds.
for (const invalidFeed of invalidFeeds) {
const feed = invalidFeed.feed;

const authToken = organizationPatMap[feed.adoOrganization];
if (!authToken) {
console.log(`❌ Failed to obtain pat for ${feed.registry} via ${invalidFeed.fileProvider.id}`);
return false;
}
feed.authToken = authToken;
if (!feed.email) {
feed.email = defaultEmail;
}
if (!feed.userName) {
feed.userName = defaultUser;
}
}


const invalidFeedsByProvider = partition(invalidFeeds, feed => feed.fileProvider);
for (const [fileProvider, updatedFeeds] of invalidFeedsByProvider) {
await fileProvider.writeWorspaceRegistries(updatedFeeds.map(updatedFeed => updatedFeed.feed));
}

return true;

} catch (error) {
logTelemetry(
{
Expand All @@ -54,7 +109,9 @@ if (!isSupportedPlatformAndArchitecture()) {
process.exit(0);
}

const result = await run();
const args = parseArgs(process.argv)

const result = await run(args);

if (result === null) {
// current auth is valid, do nothing
Expand Down
98 changes: 98 additions & 0 deletions packages/ado-npm-auth/src/fileProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { getWorkspaceRoot } from "workspace-tools";
import { join } from "node:path";
import fs from "node:fs/promises";
import { homedir } from "node:os";
import { getOrganizationFromFeedUrl } from "./utils/get-organization-from-feed-url.js";
import { makeADORequest } from "./ado/make-ado-request.js";


/**
* Default user to be used in the .npmrc
*/
export const defaultUser = "me";

/**
* Default email to be used in the .npmrc
*/
export const defaultEmail = "[email protected]";

export interface Feed {
registry: string;
adoOrganization: string;
userName?: string;
email?: string;
authToken?: string;
}

export type ValidatedFeed = { feed: Feed; isValid: boolean, fileProvider: FileProvider }

export abstract class FileProvider {
public workspaceFilePath: string;
public userFilePath: string;

public feeds: Map<string, Feed>;

constructor(public id: string, public workspaceFileName: string) {
const workspaceRoot = getWorkspaceRoot(process.cwd()) || "";
this.workspaceFilePath = join(workspaceRoot, this.workspaceFileName);

const userHome =
process.env["HOME"] || process.env["USERPROFILE"] || homedir() || "";
this.userFilePath = join(userHome, workspaceFileName);
this.feeds = new Map<string, Feed>();
}

public async isSupportedInRepo(): Promise<boolean> {
try {
await fs.access(this.workspaceFilePath);
} catch (error) {
return false;
}

return true;
}

public async validateAllUsedFeeds(): Promise<ValidatedFeed[]> {
await this.prepUserFile();

const result: ValidatedFeed[] = [];

const workspaceRegistries = await this.getWorkspaceRegistries();
const userFeeds = await this.getUserFeeds();

// check each feed for validity
for (const registry of workspaceRegistries) {
const feed = userFeeds.get(registry);

if (feed) {
let feedIsValid = true;
try {
await makeADORequest({
password: feed.authToken || "",
organization: feed.adoOrganization,
});
} catch (e) {
feedIsValid = false;
}
result.push({ feed: feed, isValid: feedIsValid, fileProvider: this });
} else {
// No representation of the token in the users config file.
result.push({
feed: {
registry: registry,
adoOrganization: getOrganizationFromFeedUrl(registry),
},
isValid: false,
fileProvider: this,
});
}
}

return result;
}

abstract prepUserFile(): Promise<void>;
abstract getUserFeeds(): Promise<Map<string, Feed>>;
abstract getWorkspaceRegistries(): Promise<string[]>;
abstract writeWorspaceRegistries(feedsToPatch: Iterable<Feed>): Promise<void>;
}
7 changes: 0 additions & 7 deletions packages/ado-npm-auth/src/npmrc/base64.ts

This file was deleted.

37 changes: 0 additions & 37 deletions packages/ado-npm-auth/src/npmrc/check-tokens.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/ado-npm-auth/src/npmrc/generate-npmrc-pat.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { hostname } from "os";
import { AdoPatResponse, adoPat } from "../azureauth/ado.js";
import { toBase64 } from "../utils/encoding.js";

/**
* Generates a valid ADO PAT, scoped for vso.packaging in the given ado organization, 30 minute timeout
Expand All @@ -22,8 +23,7 @@ export const generateNpmrcPat = async (
const rawToken = (pat as AdoPatResponse).token;

if (encode) {
// base64 encode the token
return Buffer.from(rawToken).toString("base64");
return toBase64(rawToken);
}

return rawToken;
Expand Down
63 changes: 0 additions & 63 deletions packages/ado-npm-auth/src/npmrc/get-repo-npmrc-ado-orgs.ts

This file was deleted.

Loading
Loading