Skip to content

Commit

Permalink
feat: allow manual supplying pat (#9)
Browse files Browse the repository at this point in the history
<!-- 👋 Hi, thanks for sending a PR to ado-npm-auth-lite! 💖.
Please fill out all fields below and make sure each item is true and [x]
checked.
Otherwise we may not be able to review your PR. -->

## PR Checklist

- [ ] Addresses an existing open issue: fixes #8 
- [ ] That issue was marked as [`status: accepting
prs`](https://github.com/johnnyreilly/ado-npm-auth-lite/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [ ] Steps in
[CONTRIBUTING.md](https://github.com/johnnyreilly/ado-npm-auth-lite/blob/main/.github/CONTRIBUTING.md)
were taken

## Overview

<!-- Description of what is changed and how the code change does that.
-->
  • Loading branch information
johnnyreilly authored Nov 9, 2024
1 parent 8cde07e commit 1d7ab17
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 87 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ With the above `preinstall` script in place, when the user performs `npm i` or s

## Prerequisites

`ado-npm-auth-lite` requires that you are authenticated with Azure to acquire an Azure DevOps Personal Access Token. To authenticate, run `az login`. [If you need to install the Azure CLI, follow these instructions](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli). It is not necessary to run `az login` if you are already authenticated with Azure.
If you would like `ado-npm-auth-lite` to acquire a token on your behalf, then it requires that your [Azure DevOps organisation is connected with your Azure account / Microsoft Entra ID](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/connect-organization-to-azure-ad?view=azure-devops). Then, assuming you are authenticated with Azure, it can acquire an Azure DevOps Personal Access Token on your behalf. To authenticate, run `az login`. [If you need to install the Azure CLI, follow these instructions](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli). It is not necessary to run `az login` if you are already authenticated with Azure.

You might be worried about `ado-npm-auth-lite` trying to create user `.npmrc` files when running CI builds. Happily this does not happen; it detects whether it is running in a CI environment and does **not** create a user `.npmrc` file in that case.

Expand Down Expand Up @@ -84,7 +84,9 @@ There is an official package named [`ado-npm-auth`](https://github.com/microsoft

`-c` | `--config` (`string`): The location of the .npmrc file. Defaults to current directory

`-e` | `--email` (`string`): Allows users to supply an explicit email - if not supplied, will be inferred from git user.config
`-e` | `--email` (`string`): Allows users to supply an explicit email - if not supplied, the example ADO value will be used

`-p` | `--pat` (`string`): Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI

`-h` | `--help`: Show help

Expand Down
6 changes: 5 additions & 1 deletion src/bin/help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ describe("logHelpText", () => {
],
[
"
-e | --email (string): Allows users to supply an explicit email - if not supplied, will be inferred from git user.config",
-e | --email (string): Allows users to supply an explicit email - if not supplied, the example ADO value will be used",
],
[
"
-p | --pat (string): Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI",
],
[],
]
Expand Down
94 changes: 39 additions & 55 deletions src/bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export async function bin(args: string[]) {
logLine();

const mappedOptions = {
pat: values.pat,
config: values.config,
email: values.email,
};
Expand All @@ -71,7 +72,7 @@ export async function bin(args: string[]) {
return StatusCodes.Failure;
}

const { config, email } = optionsParseResult.data;
const { config, email, pat } = optionsParseResult.data;

// TODO: this will prevent this file from running tests on the server after this - create an override parameter
if (ci.isCI) {
Expand All @@ -84,6 +85,7 @@ export async function bin(args: string[]) {
}

prompts.log.info(`options:
- pat: ${pat ? "supplied" : "[NONE SUPPLIED - WILL ACQUIRE FROM AZURE API]"}
- config: ${config ?? "[NONE SUPPLIED - WILL USE DEFAULT]"}
- email: ${email ?? "[NONE SUPPLIED - WILL USE DEFAULT]"}`);

Expand All @@ -92,70 +94,52 @@ export async function bin(args: string[]) {
error: prompts.log.error,
};

const parsedProjectNpmrc = await withSpinner(`Parsing project .npmrc`, () =>
parseProjectNpmrc({
config,
logger,
}),
);

if (!parsedProjectNpmrc) {
prompts.cancel(operationMessage("failed"));
prompts.outro(outroPrompts);

return StatusCodes.Failure;
}

const pat = await withSpinner(
`Creating Personal Access Token with vso.packaging scope`,
() => createPat({ logger, organisation: parsedProjectNpmrc.organisation }),
);
// const pat = {
// patToken: {
// token: "123456",
// },
// };

if (!pat) {
prompts.cancel(operationMessage("failed"));
prompts.outro(outroPrompts);
try {
const parsedProjectNpmrc = await withSpinner(`Parsing project .npmrc`, () =>
parseProjectNpmrc({
config,
logger,
}),
);

return StatusCodes.Failure;
}
const personalAccessToken = pat
? {
patToken: {
token: pat,
},
}
: await withSpinner(`Creating Personal Access Token`, () =>
createPat({ logger, organisation: parsedProjectNpmrc.organisation }),
);

const npmrc = await withSpinner(`Constructing user .npmrc`, () =>
Promise.resolve(
createUserNpmrc({
parsedProjectNpmrc,
email,
logger,
pat: personalAccessToken.patToken.token,
}),
),
);

const npmrc = await withSpinner(`Constructing user .npmrc`, () =>
Promise.resolve(
createUserNpmrc({
parsedProjectNpmrc,
email,
await withSpinner(`Writing user .npmrc`, () =>
writeNpmrc({
npmrc,
logger,
pat: pat.patToken.token,
}),
),
);
);

if (!npmrc) {
prompts.cancel(operationMessage("failed"));
prompts.outro(outroPrompts);

return StatusCodes.Failure;
}

const succeeded = await withSpinner(`Writing user .npmrc`, () =>
writeNpmrc({
npmrc,
logger,
}),
);

if (!succeeded) {
return StatusCodes.Success;
} catch (error) {
prompts.log.error(
`Error: ${error instanceof Error && error.cause instanceof Error ? error.cause.message : ""}`,
);
prompts.cancel(operationMessage("failed"));
prompts.outro(outroPrompts);

return StatusCodes.Failure;
}

prompts.outro(outroPrompts);

return StatusCodes.Success;
}
35 changes: 21 additions & 14 deletions src/createPat.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AzureCliCredential } from "@azure/identity";
import chalk from "chalk";
import { fromZodError } from "zod-validation-error";

import type { TokenResult } from "./types.js";
Expand All @@ -13,8 +12,10 @@ export async function createPat({
}: {
logger?: Logger;
organisation: string;
}): Promise<TokenResult | undefined> {
}): Promise<TokenResult> {
// const credential = new InteractiveBrowserCredential({});
logger.info(`Creating Azure CLI Token`);

const credential = new AzureCliCredential();

// get a token that can be used to authenticate to Azure DevOps
Expand All @@ -24,6 +25,8 @@ export async function createPat({
"499b84ac-1321-427f-aa17-267ca6975798",
]);

logger.info(`Created Azure CLI Token`);

// Get the current date
const currentDate = new Date();

Expand All @@ -32,6 +35,8 @@ export async function createPat({
futureDate.setDate(currentDate.getDate() + 30);

try {
logger.info(`Creating Personal Access Token with scope: vso.packaging`);

// https://learn.microsoft.com/en-us/rest/api/azure/devops/tokens/pats/create?view=azure-devops-rest-7.1&tabs=HTTP
const url = `https://vssps.dev.azure.com/${organisation}/_apis/tokens/pats?api-version=7.1-preview.1`;
const data = {
Expand All @@ -53,28 +58,30 @@ export async function createPat({
});

if (!response.ok) {
logger.error(`HTTP error! status: ${response.status.toString()}`);
return;
const responseText = await response.text();
const errorMessage = `HTTP error! status: ${response.status.toString()} - ${responseText}`;
throw new Error(errorMessage);
}

const tokenParseResult = tokenResultSchema.safeParse(await response.json());

if (!tokenParseResult.success) {
logger.error(
chalk.red(
fromZodError(tokenParseResult.error, {
issueSeparator: "\n - ",
}),
),
);
const errorMessage = `Error parsing the token result: ${fromZodError(tokenParseResult.error).message}`;
throw new Error(errorMessage);
}

logger.info(`Created Personal Access Token`);

return tokenParseResult.data;
} catch (error) {
logger.error(
`Error creating Personal Access Token: ${error instanceof Error ? error.message : ""}`,
);
const errorMessage = `Error creating Personal Access Token:
${error instanceof Error ? error.message : JSON.stringify(error)}
Please ensure that:
1. Your Azure DevOps organisation is connected with your Azure account / Microsoft Entra ID
2. You are logged into the Azure CLI (use \`az login\` to log in)
If you continue to have issues, consider creating a Personal Access Token with the Packaging read and write scopes manually in Azure DevOps and providing it to \`ado-npm-auth-lite\` using the \`--pat\` option.`;
throw new Error(errorMessage);
}
}
2 changes: 1 addition & 1 deletion src/createUserNpmrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function createUserNpmrc({
parsedProjectNpmrc: ParsedProjectNpmrc;
logger?: Logger;
pat: string;
}): string | undefined {
}): string {
const base64EncodedPAT = Buffer.from(pat).toString("base64");

const { urlWithoutRegistryAtEnd, urlWithoutRegistryAtStart, organisation } =
Expand Down
15 changes: 9 additions & 6 deletions src/parseProjectNpmrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function parseProjectNpmrc({
}: {
config?: string | undefined;
logger?: Logger;
}): Promise<ParsedProjectNpmrc | undefined> {
}): Promise<ParsedProjectNpmrc> {
const npmrcPath = config
? path.resolve(config)
: path.resolve(process.cwd(), ".npmrc");
Expand All @@ -28,29 +28,32 @@ export async function parseProjectNpmrc({
const npmrcContents = await readFileSafe(npmrcPath, "");

if (!npmrcContents) {
logger.error(`No .npmrc found at: ${npmrcPath}`);
return;
throw new Error(`No .npmrc found at: ${npmrcPath}`);
}

const regex = /^registry=.*$/gm;
const match = npmrcContents.match(regex);

if (!match || match.length === 0) {
logger.error(`Unable to extract information from project .npmrc`);
return;
throw new Error(`Unable to extract information from project .npmrc`);
}

const urlWithoutRegistryAtStart = match[0]
.replace("registry=https:", "")
.trim();
const urlWithoutRegistryAtEnd = urlWithoutRegistryAtStart.replace(
/\/registry\/$/,
/registry\/$/,
"",
);
// extract the organisation which we will use as the username
// not sure why this is the case, but this is the behaviour
// defined in ADO
const organisation = urlWithoutRegistryAtEnd.split("/")[3];

logger.info(`Parsed:
- organisation: ${organisation}
- urlWithoutRegistryAtStart: ${urlWithoutRegistryAtStart}
- urlWithoutRegistryAtEnd: ${urlWithoutRegistryAtEnd}`);

return { urlWithoutRegistryAtStart, urlWithoutRegistryAtEnd, organisation };
}
14 changes: 13 additions & 1 deletion src/shared/options/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export const options = {
type: "string",
},

pat: {
short: "p",
type: "string",
},

help: {
short: "h",
type: "boolean",
Expand Down Expand Up @@ -43,7 +48,14 @@ export const allArgOptions: Record<ValidOption, DocOption> = {
email: {
...options.email,
description:
"Allows users to supply an explicit email - if not supplied, will be inferred from git user.config",
"Allows users to supply an explicit email - if not supplied, the example ADO value will be used",
docsSection: "optional",
},

pat: {
...options.pat,
description:
"Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI",
docsSection: "optional",
},

Expand Down
1 change: 1 addition & 0 deletions src/shared/options/optionsSchema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from "zod";

export const optionsSchema = z.object({
pat: z.string().optional(),
config: z.string().optional(),
email: z.string().optional(),
});
10 changes: 3 additions & 7 deletions src/writeNpmrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function writeNpmrc({
}: {
npmrc: string;
logger?: Logger;
}): Promise<boolean> {
}): Promise<void> {
// Get the home directory
const homeDirectory = os.homedir();

Expand All @@ -23,11 +23,7 @@ export async function writeNpmrc({
// Write the content to the .npmrc file
await fs.writeFile(userNpmrcPath, npmrc);
} catch (error) {
logger.error(
`Error writing users .npmrc to ${userNpmrcPath}: ${error instanceof Error ? error.message : ""}`,
);
return false;
const errorMessage = `Error writing users .npmrc to ${userNpmrcPath}: ${error instanceof Error ? error.message : ""}`;
throw new Error(errorMessage);
}

return true;
}

0 comments on commit 1d7ab17

Please sign in to comment.