├── .ado ├── prep-node.yml └── publish.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── dependabot.yml └── workflows │ └── pull_request.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── beachball.config.js ├── change ├── change-2d40247e-8290-4b1a-9eef-269c0b92b327.json ├── change-72abf428-c4ea-439b-bc1e-4f77dc37ce7d.json ├── change-dc308d65-6222-40d9-b345-3ed61cb5d0e7.json └── change-fce6a77e-840b-4275-9d47-cd58df605f1a.json ├── lage.config.js ├── package.json ├── packages ├── ado-npm-auth │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── LICENSE.txt │ ├── README.md │ ├── bin │ │ └── index.js │ ├── package.json │ ├── src │ │ ├── ado │ │ │ └── make-ado-request.ts │ │ ├── args.ts │ │ ├── azureauth │ │ │ ├── ado.test.ts │ │ │ ├── ado.ts │ │ │ ├── azureauth-command.ts │ │ │ ├── index.ts │ │ │ ├── is-azureauth-installed.test.ts │ │ │ ├── is-azureauth-installed.ts │ │ │ └── is-supported-platform-and-architecture.ts │ │ ├── cli.ts │ │ ├── fileProvider.ts │ │ ├── index.ts │ │ ├── npmrc │ │ │ ├── generate-npmrc-pat.ts │ │ │ ├── index.ts │ │ │ └── npmrcFileProvider.ts │ │ ├── telemetry │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── encoding.ts │ │ │ ├── exec.ts │ │ │ ├── get-feed-without-protocol.test.ts │ │ │ ├── get-feed-without-protocol.ts │ │ │ ├── get-organization-from-feed-url.test.ts │ │ │ ├── get-organization-from-feed-url.ts │ │ │ ├── is-codespaces.ts │ │ │ ├── is-wsl.ts │ │ │ ├── partition.ts │ │ │ └── request.ts │ │ └── yarnrc │ │ │ └── yarnrcFileProvider.ts │ ├── static │ │ ├── image.png │ │ └── preinstall.png │ ├── tsconfig.json │ └── webpack.config.js └── node-azureauth │ ├── .gitignore │ ├── .npmignore │ ├── .prettierrc │ ├── CHANGELOG.json │ ├── CHANGELOG.md │ ├── LICENSE.txt │ ├── README.md │ ├── cli.js │ ├── examples │ └── pat.js │ ├── package.json │ ├── scripts │ ├── azureauth.cjs │ └── install.cjs │ ├── src │ ├── azure-auth-command.ts │ ├── index.ts │ ├── install.ts │ ├── pat.test.ts │ └── pat.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js └── publish.npmrc /.ado/prep-node.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | name: nodeVersion 3 | type: string 4 | default: 20 5 | 6 | steps: 7 | - task: UseNode@1 8 | displayName: Install Node ${{ parameters.nodeVersion }} 9 | inputs: 10 | version: ${{ parameters.nodeVersion }} 11 | 12 | 13 | - bash: | 14 | mv publish.npmrc .npmrc 15 | displayName: "Configure .npmrc to use CFS" 16 | 17 | - task: npmAuthenticate@0 18 | inputs: 19 | workingFile: '.npmrc' 20 | 21 | - script: | 22 | npm install -g yarn 23 | displayName: "Setup yarn" 24 | 25 | - script: | 26 | corepack enable pnpm 27 | corepack use pnpm@latest-8 28 | displayName: "Setup pnpm" 29 | -------------------------------------------------------------------------------- /.ado/publish.yml: -------------------------------------------------------------------------------- 1 | name: ado-npm-auth-$(Rev:rrr) 2 | 3 | pr: none 4 | trigger: 5 | branches: 6 | include: 7 | - main 8 | - 'release/*' 9 | 10 | resources: 11 | repositories: 12 | - repository: OfficePipelineTemplates 13 | type: git 14 | name: 1ESPipelineTemplates/OfficePipelineTemplates 15 | ref: refs/tags/release 16 | 17 | variables: 18 | - name: tags 19 | value: production,externalfacing 20 | 21 | parameters: 22 | - name: pools 23 | type: object 24 | default: 25 | - name: Azure-Pipelines-1ESPT-ExDShared 26 | image: windows-latest 27 | os: windows 28 | - name: Azure-Pipelines-1ESPT-ExDShared 29 | image: ubuntu-latest 30 | os: linux 31 | - name: Azure Pipelines 32 | vmImage: macos-13 33 | os: macOS 34 | - name: nodeVersions 35 | type: object 36 | default: 37 | #- 16.x Pnpm v8 using corepack does not work on linux nor macos with node 16.x 38 | - 18.x 39 | - 20.x 40 | - name: skipNpmPublish 41 | displayName: Skip Npm Publish 42 | type: boolean 43 | default: false 44 | - name: skipGitPush 45 | displayName: Skip Git Push 46 | type: boolean 47 | default: false 48 | 49 | extends: 50 | template: v1/Office.Official.PipelineTemplate.yml@OfficePipelineTemplates 51 | parameters: 52 | pool: 53 | name: Azure-Pipelines-1ESPT-ExDShared 54 | image: windows-latest 55 | os: windows 56 | 57 | sdl: 58 | codeql: 59 | compiled: 60 | enabled: true 61 | runSourceLanguagesInSourceAnalysis: true 62 | eslint: 63 | configuration: "recommended" 64 | parser: "@typescript-eslint/parser" 65 | parserOptions: "" 66 | 67 | stages: 68 | - stage: build 69 | jobs: 70 | - ${{ each pool in parameters.pools }}: 71 | - ${{ each nodeVersion in parameters.nodeVersions }}: 72 | - job: build_${{ pool.os }}_${{ replace(nodeVersion, '.', '_') }} 73 | displayName: Build on ${{ pool.os }} - node ${{ nodeVersion }} 74 | pool: ${{ pool }} 75 | templateContext: 76 | outputs: 77 | # declare the outputs for compliance. 78 | - output: pipelineArtifact 79 | targetPath: $(System.DefaultWorkingDirectory) 80 | artifactName: publish-${{ pool.os }}_${{ replace(nodeVersion, '.', '_') }} 81 | steps: 82 | - template: /.ado/prep-node.yml@self 83 | parameters: 84 | nodeVersion: ${{ nodeVersion }} 85 | 86 | - script: pnpm install 87 | displayName: Install dependencies (pnpm) 88 | 89 | - script: npx lage build test lint bundle --reporter=azureDevops 90 | displayName: Build all 91 | 92 | - stage: publish 93 | dependsOn: build # Make sure we build and pass tests on all platforms and node versions before publishing 94 | jobs: 95 | - job: Publish 96 | pool: 97 | name: Azure-Pipelines-1ESPT-ExDShared 98 | image: ubuntu-latest 99 | os: linux 100 | steps: 101 | - template: /.ado/prep-node.yml@self 102 | parameters: 103 | nodeVersion: 20.x 104 | 105 | - script: pnpm install 106 | displayName: Install dependencies (pnpm) 107 | 108 | - script: npx lage build bundle --reporter=azureDevops 109 | displayName: Build & Bundle 110 | 111 | - script: | 112 | echo "##vso[task.setvariable variable=SkipNpmPublishArgs]--no-publish" 113 | displayName: Enable No-Publish (npm) 114 | condition: ${{ parameters.skipNpmPublish }} 115 | 116 | - script: | 117 | echo "##vso[task.setvariable variable=SkipGitPushPublishArgs]--no-push" 118 | displayName: Enable No-Publish (git) 119 | condition: ${{ parameters.skipGitPush }} 120 | 121 | - task: AzureKeyVault@2 122 | inputs: 123 | azureSubscription: 'CXE - RNW CI & Anomaly-Detection (8735cced-c5b3-4da5-867b-2c3f680273c8)' 124 | KeyVaultName: rnw-keyvault 125 | SecretsFilter: 'githubAuthToken,npmAuthToken' # string. Required. Secrets filter. Default: *. 126 | displayName: Obtaining secrets for publish 127 | 128 | - task: CmdLine@2 129 | displayName: Configure git auth for publishing to github 130 | inputs: 131 | script: | 132 | git config --global user.email "53619745+rnbot@users.noreply.github.com" 133 | git config --global user.name "React-Native Bot" 134 | git remote set-url origin https://rnbot:$(githubAuthToken)@github.com/microsoft/ado-npm-auth 135 | 136 | - script: | 137 | npm run publish:beachball -- $(SkipNpmPublishArgs) $(SkipGitPushPublishArgs) --access public --token $(npmAuthToken) -y 138 | displayName: Beachball Publish 139 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary of the Pull Request 2 | 3 | ## References and Relevant Issues 4 | 5 | ## Detailed Description of the Pull Request / Additional comments 6 | 7 | ## Validation Steps Performed 8 | 9 | ## PR Checklist 10 | - [ ] Closes #xxx 11 | - [ ] Tests added/passed 12 | - [ ] Documentation updated 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: 'github-actions' 7 | directory: '/' 8 | schedule: 9 | interval: 'weekly' 10 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main" ] 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | node-version: [16.x, 18.x, 20.x, 22.x] 17 | os: [ubuntu-latest, windows-latest, macos-latest] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 22 | 23 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v3 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'pnpm' 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Check for change files 35 | run: pnpm run checkchange 36 | 37 | - name: Build, test & lint 38 | run: pnpm exec lage build test lint bundle 39 | 40 | - name: Check if files updated 41 | run: git diff --exit-code 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .store 3 | dist 4 | lib 5 | .npmrc 6 | .vscode -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ado-npm-auth repository 2 | 3 | This monorepo contains packages for helping authenticate against the Azure DevOps NPM Artifacts feed. 4 | 5 | ## Packages 6 | 7 | Below are the packages being published from this repo. 8 | 9 | ### ado-npm-auth 10 | 11 | The `ado-npm-auth` package can automatically use the `azureauth` CLI to fetch tokens and update a user's `.npmrc` file for authenticating to ADO package feeds. 12 | 13 | View the [README.md](packages/ado-npm-auth/README.md) for more. 14 | 15 | ### node-azureauth 16 | 17 | This package wraps the https://github.com/AzureAD/microsoft-authentication-cli with a node.js exec wrapper. 18 | 19 | View the [README.md](packages/node-azureauth/README.md) for more. 20 | 21 | ## Contributing 22 | 23 | Make sure to have [pnpm](https://pnpm.io/) installed. 24 | 25 | Then you can install the repo's dependencies with... 26 | 27 | ```bash 28 | pnpm install 29 | ``` 30 | 31 | And build the packages with... 32 | 33 | ```bash 34 | > pnpm exec lage build 35 | ``` 36 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /beachball.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** @type {import("beachball").BeachballConfig} */ 3 | const config = { 4 | changehint: 'Run "pnpm change" to generate a change file', 5 | branch: "main", 6 | groupChanges: true, 7 | ignorePatterns: ["**/*.test.ts"], 8 | disallowedChangeTypes: [ 9 | "prerelease", 10 | // If a major release is needed, temporarily remove this line. 11 | "major" 12 | ] 13 | }; 14 | module.exports = config; -------------------------------------------------------------------------------- /change/change-2d40247e-8290-4b1a-9eef-269c0b92b327.json: -------------------------------------------------------------------------------- 1 | { 2 | "changes": [ 3 | { 4 | "type": "none", 5 | "comment": "Bump vite from 5.3.4 to 5.3.6", 6 | "packageName": "ado-npm-auth", 7 | "email": "elcraig@microsoft.com", 8 | "dependentChangeType": "none" 9 | }, 10 | { 11 | "type": "none", 12 | "comment": "Bump vite from 5.3.4 to 5.3.6", 13 | "packageName": "azureauth", 14 | "email": "elcraig@microsoft.com", 15 | "dependentChangeType": "none" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /change/change-72abf428-c4ea-439b-bc1e-4f77dc37ce7d.json: -------------------------------------------------------------------------------- 1 | { 2 | "changes": [ 3 | { 4 | "type": "minor", 5 | "comment": "Add new flag to return a specific exit code when authentication ran", 6 | "packageName": "ado-npm-auth", 7 | "email": "dannyvv@microsoft.com", 8 | "dependentChangeType": "patch" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /change/change-dc308d65-6222-40d9-b345-3ed61cb5d0e7.json: -------------------------------------------------------------------------------- 1 | { 2 | "changes": [ 3 | { 4 | "type": "patch", 5 | "comment": "Fix exit code check in WSL environments", 6 | "packageName": "ado-npm-auth", 7 | "email": "mantiquillal@gmail.com", 8 | "dependentChangeType": "patch" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /change/change-fce6a77e-840b-4275-9d47-cd58df605f1a.json: -------------------------------------------------------------------------------- 1 | { 2 | "changes": [ 3 | { 4 | "type": "patch", 5 | "comment": "Update bundled devDependencies", 6 | "packageName": "ado-npm-auth", 7 | "email": "elcraig@microsoft.com", 8 | "dependentChangeType": "patch" 9 | }, 10 | { 11 | "type": "none", 12 | "comment": "Update devDependencies", 13 | "packageName": "azureauth", 14 | "email": "elcraig@microsoft.com", 15 | "dependentChangeType": "none" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /lage.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "pipeline": { 3 | "build": ["^build"], 4 | "bundle": ["build"], 5 | "test": [], 6 | "lint": [], 7 | } 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "private": true, 5 | "repository": "https://github.com/microsoft/ado-npm-auth", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "lage build", 11 | "bundle": "lage bundle", 12 | "lint": "lage lint", 13 | "test": "lage test", 14 | "format": "lage --no-cache lint --write", 15 | "change": "beachball change", 16 | "checkchange": "beachball check", 17 | "ci": "lage build test lint bundle", 18 | "publish:beachball": "beachball publish --bump-deps -m\"📦 applying package updates ***NO_CI***\" --verbose" 19 | }, 20 | "devDependencies": { 21 | "lage": "^2.13.0", 22 | "prettier": "^3.5.3", 23 | "typescript": "^5.8.2" 24 | }, 25 | "engines": { 26 | "node": ">=16", 27 | "pnpm": "8" 28 | }, 29 | "dependencies": { 30 | "beachball": "^2.51.1" 31 | }, 32 | "packageManager": "pnpm@8.15.9+sha512.499434c9d8fdd1a2794ebf4552b3b25c0a633abcee5bb15e7b5de90f32f47b513aca98cd5cfd001c31f0db454bc3804edccd578501e4ca293a6816166bbd9f81" 33 | } 34 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [], 3 | parserOptions: { 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | tsconfigRootDir: __dirname, 8 | sourceType: "module", 9 | project: "./tsconfig.json", 10 | }, 11 | overrides: [require("./overrides.json")], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib -------------------------------------------------------------------------------- /packages/ado-npm-auth/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src -------------------------------------------------------------------------------- /packages/ado-npm-auth/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ado-npm-auth", 3 | "entries": [ 4 | { 5 | "date": "Fri, 27 Dec 2024 22:13:14 GMT", 6 | "version": "0.3.2", 7 | "tag": "ado-npm-auth_v0.3.2", 8 | "comments": { 9 | "none": [ 10 | { 11 | "author": "dannyvv@microsoft.com", 12 | "package": "ado-npm-auth", 13 | "commit": "7eb20d3ff80b2f8673d78219dc2ba5150acaa7b4", 14 | "comment": "Format fixes" 15 | } 16 | ], 17 | "patch": [ 18 | { 19 | "author": "dannyvv@microsoft.com", 20 | "package": "ado-npm-auth", 21 | "commit": "581e48518aab31bf8b439d1ac50e29d677021a59", 22 | "comment": "Bump @npmcli/config to 10.0.0 to fix #52" 23 | }, 24 | { 25 | "author": "dsfsdf@microsoft.com", 26 | "package": "ado-npm-auth", 27 | "commit": "cb8e0e113e7bfa2768cddaa2f62f06a10d3cb2ba", 28 | "comment": "Add support to pass an override for azureAuthLocation on the commandline" 29 | }, 30 | { 31 | "author": "dannyvv@microsoft.com", 32 | "package": "ado-npm-auth", 33 | "commit": "7d7a4ff39c15735bb2c95eca2d75acbc405ebf55", 34 | "comment": "Allow non azure devops feeds to be present in the configuration." 35 | } 36 | ] 37 | } 38 | }, 39 | { 40 | "date": "Fri, 27 Dec 2024 20:26:44 GMT", 41 | "version": "0.3.1", 42 | "tag": "ado-npm-auth_v0.3.1", 43 | "comments": { 44 | "none": [ 45 | { 46 | "author": "dannyvv@microsoft.com", 47 | "package": "ado-npm-auth", 48 | "commit": "760870e1f16885663cbc202e64db4ca3114db3ca", 49 | "comment": "fix formatting of accidentally merged pr" 50 | } 51 | ] 52 | } 53 | }, 54 | { 55 | "date": "Wed, 30 Oct 2024 20:31:48 GMT", 56 | "version": "0.3.1", 57 | "tag": "ado-npm-auth_v0.3.1", 58 | "comments": { 59 | "patch": [ 60 | { 61 | "author": "beachball", 62 | "package": "ado-npm-auth", 63 | "comment": "Bump azureauth to v0.12.1", 64 | "commit": "not available" 65 | } 66 | ] 67 | } 68 | }, 69 | { 70 | "date": "Tue, 08 Oct 2024 17:07:15 GMT", 71 | "version": "0.3.0", 72 | "tag": "ado-npm-auth_v0.3.0", 73 | "comments": { 74 | "minor": [ 75 | { 76 | "author": "dannyvv@microsoft.com", 77 | "package": "ado-npm-auth", 78 | "commit": "1375d8cc4db409405ca3016232e40879e3642cfe", 79 | "comment": "Ensure all types are included in the package" 80 | } 81 | ] 82 | } 83 | }, 84 | { 85 | "date": "Mon, 07 Oct 2024 17:28:12 GMT", 86 | "version": "0.2.0", 87 | "tag": "ado-npm-auth_v0.2.0", 88 | "comments": { 89 | "minor": [ 90 | { 91 | "author": "dannyvv@microsoft.com", 92 | "package": "ado-npm-auth", 93 | "commit": "1731be2186e3f48b58dd6d114fc607a2686f40c0", 94 | "comment": "Expose functionality as a library" 95 | }, 96 | { 97 | "author": "beachball", 98 | "package": "ado-npm-auth", 99 | "comment": "Bump azureauth to v0.12.0", 100 | "commit": "not available" 101 | } 102 | ] 103 | } 104 | }, 105 | { 106 | "date": "Fri, 13 Sep 2024 19:47:53 GMT", 107 | "version": "0.1.11", 108 | "tag": "ado-npm-auth_v0.1.11", 109 | "comments": { 110 | "patch": [ 111 | { 112 | "author": "beachball", 113 | "package": "ado-npm-auth", 114 | "comment": "Bump azureauth to v0.11.0", 115 | "commit": "not available" 116 | } 117 | ] 118 | } 119 | }, 120 | { 121 | "date": "Fri, 13 Sep 2024 18:59:07 GMT", 122 | "version": "0.1.10", 123 | "tag": "ado-npm-auth_v0.1.10", 124 | "comments": { 125 | "patch": [ 126 | { 127 | "author": "beachball", 128 | "package": "ado-npm-auth", 129 | "comment": "Bump azureauth to v0.10.0", 130 | "commit": "not available" 131 | } 132 | ] 133 | } 134 | }, 135 | { 136 | "date": "Fri, 13 Sep 2024 18:42:55 GMT", 137 | "version": "0.1.9", 138 | "tag": "ado-npm-auth_v0.1.9", 139 | "comments": { 140 | "patch": [ 141 | { 142 | "author": "beachball", 143 | "package": "ado-npm-auth", 144 | "comment": "Bump azureauth to v0.9.0", 145 | "commit": "not available" 146 | } 147 | ] 148 | } 149 | }, 150 | { 151 | "date": "Fri, 13 Sep 2024 18:15:05 GMT", 152 | "version": "0.1.8", 153 | "tag": "ado-npm-auth_v0.1.8", 154 | "comments": { 155 | "patch": [ 156 | { 157 | "author": "beachball", 158 | "package": "ado-npm-auth", 159 | "comment": "Bump azureauth to v0.8.0", 160 | "commit": "not available" 161 | } 162 | ] 163 | } 164 | }, 165 | { 166 | "date": "Fri, 13 Sep 2024 17:50:14 GMT", 167 | "version": "0.1.7", 168 | "tag": "ado-npm-auth_v0.1.7", 169 | "comments": { 170 | "patch": [ 171 | { 172 | "author": "beachball", 173 | "package": "ado-npm-auth", 174 | "comment": "Bump azureauth to v0.7.0", 175 | "commit": "not available" 176 | } 177 | ] 178 | } 179 | }, 180 | { 181 | "date": "Thu, 12 Sep 2024 21:56:08 GMT", 182 | "version": "0.1.6", 183 | "tag": "ado-npm-auth_v0.1.6", 184 | "comments": { 185 | "patch": [ 186 | { 187 | "author": "beachball", 188 | "package": "ado-npm-auth", 189 | "comment": "Bump azureauth to v0.6.0", 190 | "commit": "not available" 191 | } 192 | ] 193 | } 194 | }, 195 | { 196 | "date": "Wed, 11 Sep 2024 20:04:18 GMT", 197 | "version": "0.1.5", 198 | "tag": "ado-npm-auth_v0.1.5", 199 | "comments": { 200 | "patch": [ 201 | { 202 | "author": "dannyvv@microsoft.com", 203 | "package": "ado-npm-auth", 204 | "commit": "bc3533c7489857c75a8deec785397943990fec6f", 205 | "comment": "Linting fixes" 206 | }, 207 | { 208 | "author": "beachball", 209 | "package": "ado-npm-auth", 210 | "comment": "Bump azureauth to v0.5.1", 211 | "commit": "not available" 212 | } 213 | ] 214 | } 215 | }, 216 | { 217 | "date": "Wed, 11 Sep 2024 16:57:41 GMT", 218 | "version": "0.1.4", 219 | "tag": "ado-npm-auth_v0.1.4", 220 | "comments": { 221 | "patch": [ 222 | { 223 | "author": "dannyvv@microsoft.com", 224 | "package": "ado-npm-auth", 225 | "commit": "5d5c5cdc2b98bfbcbd29fcad19054255544c30c0", 226 | "comment": "Make ado-npm-auth use bundles to avoid pulling in a bunch of packages for a build tool" 227 | }, 228 | { 229 | "author": "beachball", 230 | "package": "ado-npm-auth", 231 | "comment": "Bump azureauth to v0.5.0", 232 | "commit": "not available" 233 | } 234 | ] 235 | } 236 | }, 237 | { 238 | "date": "Mon, 09 Sep 2024 17:27:01 GMT", 239 | "version": "0.1.3", 240 | "tag": "ado-npm-auth_v0.1.3", 241 | "comments": { 242 | "patch": [ 243 | { 244 | "author": "dannyvv@microsoft.com", 245 | "package": "ado-npm-auth", 246 | "commit": "bb5431e37d0b40ace73c9f06e736219c5412f32c", 247 | "comment": "Initial official release" 248 | }, 249 | { 250 | "author": "beachball", 251 | "package": "ado-npm-auth", 252 | "comment": "Bump azureauth to v0.4.7", 253 | "commit": "not available" 254 | } 255 | ] 256 | } 257 | } 258 | ] 259 | } 260 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - ado-npm-auth 2 | 3 | This log was last generated on Fri, 27 Dec 2024 22:13:14 GMT and should not be manually modified. 4 | 5 | 6 | 7 | ## 0.3.2 8 | 9 | Fri, 27 Dec 2024 22:13:14 GMT 10 | 11 | ### Patches 12 | 13 | - Bump @npmcli/config to 10.0.0 to fix #52 (dannyvv@microsoft.com) 14 | - Add support to pass an override for azureAuthLocation on the commandline (dsfsdf@microsoft.com) 15 | - Allow non azure devops feeds to be present in the configuration. (dannyvv@microsoft.com) 16 | 17 | ## 0.3.1 18 | 19 | Wed, 30 Oct 2024 20:31:48 GMT 20 | 21 | ### Patches 22 | 23 | - Bump azureauth to v0.12.1 24 | 25 | ## 0.3.0 26 | 27 | Tue, 08 Oct 2024 17:07:15 GMT 28 | 29 | ### Minor changes 30 | 31 | - Ensure all types are included in the package (dannyvv@microsoft.com) 32 | 33 | ## 0.2.0 34 | 35 | Mon, 07 Oct 2024 17:28:12 GMT 36 | 37 | ### Minor changes 38 | 39 | - Expose functionality as a library (dannyvv@microsoft.com) 40 | - Bump azureauth to v0.12.0 41 | 42 | ## 0.1.11 43 | 44 | Fri, 13 Sep 2024 19:47:53 GMT 45 | 46 | ### Patches 47 | 48 | - Bump azureauth to v0.11.0 49 | 50 | ## 0.1.10 51 | 52 | Fri, 13 Sep 2024 18:59:07 GMT 53 | 54 | ### Patches 55 | 56 | - Bump azureauth to v0.10.0 57 | 58 | ## 0.1.9 59 | 60 | Fri, 13 Sep 2024 18:42:55 GMT 61 | 62 | ### Patches 63 | 64 | - Bump azureauth to v0.9.0 65 | 66 | ## 0.1.8 67 | 68 | Fri, 13 Sep 2024 18:15:05 GMT 69 | 70 | ### Patches 71 | 72 | - Bump azureauth to v0.8.0 73 | 74 | ## 0.1.7 75 | 76 | Fri, 13 Sep 2024 17:50:14 GMT 77 | 78 | ### Patches 79 | 80 | - Bump azureauth to v0.7.0 81 | 82 | ## 0.1.6 83 | 84 | Thu, 12 Sep 2024 21:56:08 GMT 85 | 86 | ### Patches 87 | 88 | - Bump azureauth to v0.6.0 89 | 90 | ## 0.1.5 91 | 92 | Wed, 11 Sep 2024 20:04:18 GMT 93 | 94 | ### Patches 95 | 96 | - Linting fixes (dannyvv@microsoft.com) 97 | - Bump azureauth to v0.5.1 98 | 99 | ## 0.1.4 100 | 101 | Wed, 11 Sep 2024 16:57:41 GMT 102 | 103 | ### Patches 104 | 105 | - Make ado-npm-auth use bundles to avoid pulling in a bunch of packages for a build tool (dannyvv@microsoft.com) 106 | - Bump azureauth to v0.5.0 107 | 108 | ## 0.1.3 109 | 110 | Mon, 09 Sep 2024 17:27:01 GMT 111 | 112 | ### Patches 113 | 114 | - Initial official release (dannyvv@microsoft.com) 115 | - Bump azureauth to v0.4.7 116 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/README.md: -------------------------------------------------------------------------------- 1 | # Azure Devops NPM Auth 2 | 3 | The `ado-npm-auth` package can automatically use the `azureauth` CLI to fetch tokens and update a user's `.npmrc` file for authenticating to ADO package feeds. 4 | 5 | You'll first need an `.npmrc` in your project such as... 6 | 7 | ```text 8 | registry=https://pkgs.dev.azure.com/org/project/_packaging/feedname/npm/registry/ 9 | ``` 10 | 11 | You can run the binary `"ado-npm-auth"` via `yarn ado-npm-auth` or `npm exec ado-npm-auth`. 12 | 13 | It will then shell out to the `azureauth` package on [npm](https://www.npmjs.com/package/azureauth), retrieve a token, and update your `~/.npmrc`. 14 | 15 | ## ado-npm-auth vs vsts-npm-auth 16 | 17 | The main difference between the two is how they function, and where they can run. The `vsts-npm-auth` tool is Windows only, and uses MSAL authentication. 18 | 19 | `ado-npm-auth` uses the `node-azureauth` library, to wrap the [azureauth-cli](https://github.com/AzureAD/microsoft-authentication-cli), which itself is a cross platform MSAL wrapper. 20 | 21 | ![screenshot of tool running](https://github.com/microsoft/ado-npm-auth/raw/main/packages/ado-npm-auth/static/image.png) 22 | 23 | Since the `azureauth-cli` is cross-platform, `ado-npm-auth` will also run cross-platform as well! 24 | 25 | One of the easiest ways to use the tool is to add it to your `"preinstall"` script in your repo like this... 26 | 27 | ```json 28 | "scripts": { 29 | "preinstall": "npm exec ado-npm-auth" 30 | }, 31 | ``` 32 | 33 | It will then perform a quick "pre-flight" check to assess if the token is valid, and generate a new one if it has expired. 34 | 35 | ![screenshot of tool running via preinstall](https://github.com/microsoft/ado-npm-auth/raw/main/packages/ado-npm-auth/static/preinstall.png) 36 | 37 | ### Beware the chicken and egg problem 38 | 39 | You may need to set the registry to the public NPM feed when running `npm exec` or `npx`. 40 | 41 | There are 2 options to address this case: 42 | 43 | ### 1: Explictly pass the config file. 44 | You can hop one directory up, or run it from an arbitrary path and pass the configuration. 45 | ```cmd 46 | pushd .. 47 | npx ado-npm-auth -c \.npmrc 48 | popd 49 | ``` 50 | 51 | ### 2: configure registry explicilty 52 | If that's the case, set the environment variable `npm_config_registry=https://registry.npmjs.org`. 53 | 54 | That will ensure that `npx` or `npm exec` grabs from the public NPM feed, bypassing the soon-to-be authenticated ADO feed. 55 | 56 | ```json 57 | "scripts": { 58 | "preinstall": "npm_config_registry=https://registry.npmjs.org npm exec ado-npm-auth" 59 | }, 60 | ``` 61 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import("../dist/ado-npm-auth.cjs"); 4 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ado-npm-auth", 3 | "version": "0.3.2", 4 | "description": "The ado-npm-auth package can automatically use the azureauth CLI to fetch tokens and update a user's .npmrc file for authenticating to ADO package feeds.", 5 | "repository": "https://github.com/microsoft/ado-npm-auth", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "./dist/index.js", 9 | "typings": "./lib/index.d.ts", 10 | "bin": { 11 | "ado-npm-auth": "./bin/index.js" 12 | }, 13 | "files": [ 14 | "dist/index.js", 15 | "dist/ado-npm-auth.cjs", 16 | "lib/**/*.d.ts", 17 | "bin", 18 | "static", 19 | "LICENCSE.txt", 20 | "README.md" 21 | ], 22 | "scripts": { 23 | "build": "tsc", 24 | "bundle": "npm run bundleBin && npm run bundleIndex", 25 | "bundleBin": "esbuild --sourcemap=external --bundle --minify --platform=node src/cli.ts --outfile=dist/ado-npm-auth.cjs", 26 | "bundleIndex": "esbuild --sourcemap=external --bundle --minify --platform=node --format=esm src/index.ts --outfile=dist/index.js", 27 | "lint": "prettier --check src/**/*.ts", 28 | "performance-test": "node lib/tests/performance.test.js", 29 | "test": "vitest run src" 30 | }, 31 | "dependencies": { 32 | "azureauth": "workspace:^" 33 | }, 34 | "devDependencies": { 35 | "@npmcli/config": "^10.1.0", 36 | "@types/js-yaml": "4.0.9", 37 | "@types/node": "^20.17.28", 38 | "@types/npmcli__config": "^6.0.3", 39 | "@types/yargs": "^17.0.33", 40 | "esbuild": "^0.23.1", 41 | "eslint": "^8.57.1", 42 | "js-yaml": "^4.1.0", 43 | "prettier": "^3.5.3", 44 | "tslib": "^2.8.1", 45 | "typescript": "^5.8.2", 46 | "vite": "~5.3.6", 47 | "vitest": "^2.1.9", 48 | "workspace-tools": "^0.38.0", 49 | "yargs": "^17.7.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/ado/make-ado-request.ts: -------------------------------------------------------------------------------- 1 | import { toBase64 } from "../utils/encoding.js"; 2 | import { makeRequest } from "../utils/request.js"; 3 | 4 | /** 5 | * Check if the user is authenticated to the ADO API. 6 | * This function performs a network request to retrieve an ADO feed for the given organization 7 | * Since that request is usually slow, and we don't actually know the ID of a valid feed, we just use the "invalid" ID of "1" 8 | * If the network request returns 401, then the PAT is invalid. If the request returns 404 (or somehow 200) then the PAT is valid 9 | * @param {Object} options 10 | * @param {string} options.password The password to use for the login 11 | * @param {string} options.organization The organization to check against 12 | * @returns 13 | */ 14 | export const makeADORequest = async ({ 15 | password, 16 | organization, 17 | }: { 18 | password: string; 19 | organization: string; 20 | }) => { 21 | const auth = `Basic ${toBase64(`.:${password}`)}`; 22 | 23 | const options = { 24 | hostname: "feeds.dev.azure.com", 25 | port: 443, 26 | path: `/${organization}/_apis/packaging/feeds/1?api-version=6.1-preview.1`, 27 | method: "GET", 28 | headers: { 29 | Accept: "application/json", 30 | Authorization: auth, 31 | }, 32 | }; 33 | 34 | try { 35 | await makeRequest(options); 36 | } catch (/** @type {any} */ error) { 37 | if ((error as any).statusCode === 404) { 38 | // this is the good case, because we use a non-existant feed ID 39 | // but if you get a 404 it means you can even determine that with a valid token 40 | return; 41 | } 42 | 43 | throw error; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/args.ts: -------------------------------------------------------------------------------- 1 | import yargs from "yargs"; 2 | import { hideBin } from "yargs/helpers"; 3 | 4 | export interface Args { 5 | doValidCheck: boolean; 6 | skipAuth: boolean; 7 | configFile?: string; 8 | azureAuthLocation?: string; 9 | exitCodeOnReAuthenticate?: number; 10 | } 11 | 12 | export function parseArgs(args: string[]): Args { 13 | const argv = yargs(hideBin(args)) 14 | .option({ 15 | skipCheck: { 16 | type: "boolean", 17 | description: "Skip checking the validity of the feeds", 18 | }, 19 | skipAuth: { 20 | type: "boolean", 21 | description: "Skip authenticating the feeds", 22 | }, 23 | configFile: { 24 | alias: "c", 25 | type: "string", 26 | description: "Skip checking the validity of the feeds", 27 | }, 28 | azureAuthLocation: { 29 | type: "string", 30 | description: "Allow specifying alternate location to azureauth", 31 | }, 32 | exitCodeOnReAuthenticate: { 33 | type: "number", 34 | description: "Exit when re-authentication occurs", 35 | }, 36 | }) 37 | .help() 38 | .parseSync(); 39 | 40 | return { 41 | skipAuth: argv.skipAuth || false, 42 | doValidCheck: !argv.skipCheck, 43 | configFile: argv.configFile, 44 | azureAuthLocation: argv.azureAuthLocation, 45 | exitCodeOnReAuthenticate: argv.exitCodeOnReAuthenticate, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/azureauth/ado.test.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | import { beforeEach, expect, test, vi } from "vitest"; 3 | import { exec } from "../utils/exec.js"; 4 | import * as utils from "../utils/is-wsl.js"; 5 | import { AdoPatResponse, adoPat } from "./ado.js"; 6 | 7 | vi.mock("child_process", async () => { 8 | return { 9 | spawnSync: vi.fn(() => { 10 | error: { 11 | } 12 | }), 13 | }; 14 | }); 15 | 16 | vi.mock("./is-azureauth-installed.js", async () => { 17 | return { 18 | isAzureAuthInstalled: vi.fn(() => true), 19 | }; 20 | }); 21 | 22 | vi.mock("../utils/exec.js", async () => { 23 | return { 24 | exec: vi.fn(), 25 | }; 26 | }); 27 | 28 | vi.mock("../utils/is-wsl.js", async () => { 29 | return { 30 | isWsl: vi.fn(), 31 | }; 32 | }); 33 | 34 | vi.mock("./is-supported-platform-and-architecture.js", async () => { 35 | return { 36 | isSupportedPlatformAndArchitecture: vi.fn(() => true), 37 | }; 38 | }); 39 | 40 | beforeEach(() => { 41 | vi.clearAllMocks(); 42 | }); 43 | 44 | test("it should spawn azureauth", async () => { 45 | vi.mocked(utils.isWsl).mockReturnValue(false); 46 | vi.mocked(exec).mockReturnValue( 47 | Promise.resolve({ 48 | stdout: '{ "token": "foobarabc123" }', 49 | stderr: "", 50 | }) as any, 51 | ); 52 | 53 | const results = (await adoPat({ 54 | promptHint: "hint", 55 | organization: "org", 56 | displayName: "test display", 57 | scope: ["foobar"], 58 | output: "json", 59 | domain: "baz.com", 60 | timeout: "200", 61 | })) as AdoPatResponse; 62 | 63 | expect(exec).toHaveBeenCalledWith( 64 | 'npm exec --silent --yes azureauth -- ado pat --prompt-hint "hint" --organization org --display-name test display --scope foobar --output json --domain baz.com --timeout 200', 65 | expect.anything(), 66 | ); 67 | expect(results.token).toBe("foobarabc123"); 68 | }); 69 | 70 | test("it should spawnSync azureauth on wsl", async () => { 71 | vi.mocked(spawnSync).mockReturnValue({ 72 | status: 0, 73 | stdout: '{ "token": "foobarabc123" }', 74 | } as any); 75 | vi.mocked(utils.isWsl).mockReturnValue(true); 76 | 77 | const results = (await adoPat({ 78 | promptHint: "hint", 79 | organization: "org", 80 | displayName: "test display", 81 | scope: ["foobar"], 82 | output: "json", 83 | domain: "baz.com", 84 | timeout: "200", 85 | })) as AdoPatResponse; 86 | 87 | expect(spawnSync).toHaveBeenCalledWith( 88 | "npm", 89 | [ 90 | "exec", 91 | "--silent", 92 | "--yes", 93 | "azureauth", 94 | "--", 95 | "ado", 96 | "pat", 97 | "--prompt-hint hint", 98 | "--organization org", 99 | "--display-name test display", 100 | "--scope foobar", 101 | "--output json", 102 | "--domain baz.com", 103 | "--timeout 200", 104 | ], 105 | expect.anything(), 106 | ); 107 | expect(results.token).toBe("foobarabc123"); 108 | }); 109 | 110 | test("it should handle errors on wsl if azureauth exit code is not 0", async () => { 111 | vi.mocked(utils.isWsl).mockReturnValue(true); 112 | vi.mocked(spawnSync).mockReturnValue({ 113 | status: 1, 114 | stdout: "", 115 | stderr: "an error", 116 | } as any); 117 | 118 | await expect( 119 | adoPat({ 120 | promptHint: "hint", 121 | organization: "org", 122 | displayName: "test display", 123 | scope: ["foobar"], 124 | output: "json", 125 | domain: "baz.com", 126 | timeout: "200", 127 | }), 128 | ).rejects.toThrowError( 129 | "Failed to get Ado Pat from system AzureAuth: Azure Auth failed with exit code 1: an error", 130 | ); 131 | }); 132 | 133 | test("it should handle errors on wsl if azureauth has stderr output", async () => { 134 | vi.mocked(utils.isWsl).mockReturnValue(true); 135 | vi.mocked(spawnSync).mockReturnValue({ 136 | status: 0, 137 | stdout: "", 138 | stderr: "an error", 139 | } as any); 140 | 141 | await expect( 142 | adoPat({ 143 | promptHint: "hint", 144 | organization: "org", 145 | displayName: "test display", 146 | scope: ["foobar"], 147 | output: "json", 148 | domain: "baz.com", 149 | timeout: "200", 150 | }), 151 | ).rejects.toThrowError( 152 | "Failed to get Ado Pat from system AzureAuth: Azure Auth failed with exit code 0: an error", 153 | ); 154 | }); 155 | 156 | test("it should handle json errors", async () => { 157 | vi.mocked(utils.isWsl).mockReturnValue(false); 158 | vi.mocked(exec).mockReturnValue( 159 | Promise.resolve({ 160 | stdout: "an error", 161 | stderr: "", 162 | }) as any, 163 | ); 164 | 165 | await expect( 166 | adoPat({ 167 | promptHint: "hint", 168 | organization: "org", 169 | displayName: "test display", 170 | scope: ["foobar"], 171 | output: "json", 172 | domain: "baz.com", 173 | timeout: "200", 174 | }), 175 | ).rejects.toThrowError("Failed to parse JSON output: an error"); 176 | }); 177 | 178 | test("it should handle errors from azureauth-cli", async () => { 179 | vi.mocked(utils.isWsl).mockReturnValue(false); 180 | vi.mocked(exec).mockReturnValue( 181 | Promise.resolve({ 182 | stdout: "", 183 | stderr: "an error", 184 | }) as any, 185 | ); 186 | 187 | await expect( 188 | adoPat({ 189 | promptHint: "hint", 190 | organization: "org", 191 | displayName: "test display", 192 | scope: ["foobar"], 193 | output: "json", 194 | domain: "baz.com", 195 | timeout: "200", 196 | }), 197 | ).rejects.toThrowError("Failed to get Ado Pat from npx AzureAuth: an error"); 198 | }); 199 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/azureauth/ado.ts: -------------------------------------------------------------------------------- 1 | import { arch, platform } from "os"; 2 | import { exec } from "../utils/exec.js"; 3 | import { isSupportedPlatformAndArchitecture } from "./is-supported-platform-and-architecture.js"; 4 | import { azureAuthCommand } from "./azureauth-command.js"; 5 | import { isWsl } from "../utils/is-wsl.js"; 6 | import { spawnSync } from "child_process"; 7 | import { isAzureAuthInstalled } from "./is-azureauth-installed.js"; 8 | 9 | export type AdoPatOptions = { 10 | promptHint: string; 11 | organization: string; 12 | displayName: string; 13 | scope: string[]; 14 | output?: string; 15 | mode?: string; 16 | domain?: string; 17 | timeout?: string; 18 | }; 19 | 20 | export type AdoPatResponse = { 21 | displayName: string; 22 | validTo: string; 23 | scope: string[]; 24 | targetAccounts: string[]; 25 | validFrom: string; 26 | authorizationId: string; 27 | token: string; 28 | }; 29 | 30 | /** 31 | * Wrapper for `azureauth ado pat`. Please run `azureauth ado pat --help` for full command options and description 32 | * @param options Options for PAT generation 33 | * @returns ADO PAT details 34 | */ 35 | export const adoPat = async ( 36 | options: AdoPatOptions, 37 | azureAuthLocation?: string, 38 | ): Promise => { 39 | if (!isSupportedPlatformAndArchitecture()) { 40 | throw new Error( 41 | `AzureAuth is not supported for platform ${platform()} and architecture ${arch()}`, 42 | ); 43 | } 44 | 45 | const { command: authCommand, env } = azureAuthLocation 46 | ? { 47 | command: [azureAuthLocation], 48 | env: process.env, 49 | } 50 | : azureAuthCommand(); 51 | 52 | const command = [ 53 | ...authCommand, 54 | `ado`, 55 | `pat`, 56 | `--prompt-hint ${isWsl() ? options.promptHint : `"${options.promptHint}"`}`, // We only use spawn for WSL. spawn does not does not require prompt hint to be wrapped in quotes. exec does. 57 | `--organization ${options.organization}`, 58 | `--display-name ${options.displayName}`, 59 | ...options.scope.map((scope) => `--scope ${scope}`), 60 | ]; 61 | 62 | if (options.output) { 63 | command.push(`--output ${options.output}`); 64 | } 65 | 66 | if (options.mode) { 67 | command.push(`--mode ${options.mode}`); 68 | } 69 | 70 | if (options.domain) { 71 | command.push(`--domain ${options.domain}`); 72 | } 73 | 74 | if (options.timeout) { 75 | command.push(`--timeout ${options.timeout}`); 76 | } 77 | 78 | try { 79 | let result; 80 | if (isWsl()) { 81 | try { 82 | result = spawnSync(command[0], command.slice(1), { encoding: "utf-8" }); 83 | 84 | if (result.status !== 0 || (result.stderr && !result.stdout)) { 85 | throw new Error( 86 | `Azure Auth failed with exit code ${result.status}: ${result.stderr}`, 87 | ); 88 | } 89 | } catch (error: any) { 90 | throw new Error( 91 | `Failed to get Ado Pat from system AzureAuth: ${error.message}`, 92 | ); 93 | } 94 | } else { 95 | try { 96 | result = await exec(command.join(" "), { env }); 97 | 98 | if (result.stderr && !result.stdout) { 99 | throw new Error(result.stderr); 100 | } 101 | } catch (error: any) { 102 | throw new Error( 103 | `Failed to get Ado Pat from npx AzureAuth: ${error.message}`, 104 | ); 105 | } 106 | } 107 | 108 | if (options.output === "json") { 109 | try { 110 | return JSON.parse(result.stdout) as AdoPatResponse; 111 | } catch (error: any) { 112 | throw new Error(`Failed to parse JSON output: ${result.stdout}`); 113 | } 114 | } 115 | 116 | return result.stdout; 117 | } catch (error: any) { 118 | if (!(await isAzureAuthInstalled())) { 119 | throw new Error(`AzureAuth is not installed: ${error}`); 120 | } 121 | 122 | throw new Error(error.message); 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/azureauth/azureauth-command.ts: -------------------------------------------------------------------------------- 1 | import { isWsl } from "../utils/is-wsl.js"; 2 | 3 | let memo: string[] | undefined = undefined; 4 | 5 | export const clearMemo = () => { 6 | memo = void 0; 7 | }; 8 | 9 | const npxAzureAuthCommand: string[] = [ 10 | "npm", 11 | "exec", 12 | "--silent", 13 | "--yes", 14 | "azureauth", 15 | "--", 16 | ]; 17 | const npxEnv = { 18 | ...process.env, 19 | // Use the version from the public registry to avoid a cycle 20 | npm_config_registry: "https://registry.npmjs.org", 21 | }; 22 | 23 | /** 24 | * Get the executable path of azureauth command 25 | * @returns the string of the executable command to run azureauth, and any 26 | * necessary environment variables if using npx 27 | */ 28 | export const azureAuthCommand = (): { 29 | command: string[]; 30 | env: Record; 31 | } => { 32 | if (!memo) { 33 | memo = isWsl() ? ["azureauth.exe"] : npxAzureAuthCommand; 34 | } 35 | 36 | return { command: memo, env: npxEnv }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/azureauth/index.ts: -------------------------------------------------------------------------------- 1 | export { adoPat } from "./ado.js"; 2 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/azureauth/is-azureauth-installed.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, vi, beforeEach } from "vitest"; 2 | import { isAzureAuthInstalled, clearMemo } from "./is-azureauth-installed.js"; 3 | import { clearMemo as clearAuthMemo } from "./azureauth-command.js"; 4 | import { exec } from "../utils/exec.js"; 5 | import * as utils from "../utils/is-wsl.js"; 6 | 7 | vi.mock("../utils/is-wsl.js", async () => { 8 | return { 9 | isWsl: vi.fn(), 10 | }; 11 | }); 12 | 13 | vi.mock("../utils/exec.js", async () => { 14 | return { 15 | exec: vi.fn(), 16 | }; 17 | }); 18 | 19 | beforeEach(() => { 20 | vi.clearAllMocks(); 21 | clearAuthMemo(); 22 | clearMemo(); 23 | }); 24 | 25 | test("when azure auth is not installed", async () => { 26 | vi.mocked(exec).mockReturnValue( 27 | Promise.resolve({ 28 | stdout: "", 29 | stderr: "", 30 | }) as any, 31 | ); 32 | vi.mocked(utils.isWsl).mockReturnValue(false); 33 | 34 | const azureAuthInstalled = await isAzureAuthInstalled(); 35 | 36 | expect(vi.mocked(exec)).toBeCalled(); 37 | 38 | expect(azureAuthInstalled).toBe(false); 39 | }); 40 | 41 | test("when azure auth is installed", async () => { 42 | vi.mocked(exec).mockReturnValue( 43 | Promise.resolve({ 44 | stdout: "0.8.5", 45 | stderr: "", 46 | }) as any, 47 | ); 48 | vi.mocked(utils.isWsl).mockReturnValue(false); 49 | 50 | const azureAuthInstalled = await isAzureAuthInstalled(); 51 | 52 | expect(vi.mocked(exec)).toBeCalled(); 53 | 54 | expect(azureAuthInstalled).toBe(true); 55 | }); 56 | 57 | test("when azure auth is installed on windows", async () => { 58 | vi.mocked(exec).mockReturnValue( 59 | Promise.resolve({ 60 | stdout: "0.8.5", 61 | stderr: "", 62 | }) as any, 63 | ); 64 | vi.mocked(utils.isWsl).mockReturnValue(true); 65 | 66 | const azureAuthInstalled = await isAzureAuthInstalled(); 67 | 68 | expect(vi.mocked(exec)).toBeCalled(); 69 | expect(vi.mocked(exec)).toBeCalledWith("azureauth.exe --version", { 70 | env: expect.any(Object), 71 | }); 72 | 73 | expect(azureAuthInstalled).toBe(true); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/azureauth/is-azureauth-installed.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "../utils/exec.js"; 2 | import { azureAuthCommand } from "./azureauth-command.js"; 3 | 4 | let memo: boolean | undefined = undefined; 5 | 6 | export const clearMemo = () => { 7 | memo = void 0; 8 | }; 9 | 10 | /** 11 | * Determine if a valid version (>=0.8.0.0) is installed 12 | * @returns { boolean } Whether a valid version of azureauth is installed 13 | */ 14 | export const isAzureAuthInstalled = async (): Promise => { 15 | if (memo === undefined) { 16 | const { command: authCommand, env } = azureAuthCommand(); 17 | const command = `${authCommand.join(" ")} --version`; 18 | 19 | try { 20 | const result = await exec(command, { env }); 21 | // version must be >=0.8.0.0 22 | const [, minor] = result.stdout.split("."); 23 | memo = parseInt(minor) >= 8; 24 | } catch (error) { 25 | // azureauth not installed 26 | memo = false; 27 | } 28 | } 29 | 30 | return memo; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/azureauth/is-supported-platform-and-architecture.ts: -------------------------------------------------------------------------------- 1 | import { arch, platform } from "os"; 2 | import { isWsl } from "../utils/is-wsl.js"; 3 | 4 | /** 5 | * Determines if the currently running platform is supported by azureauth. Currently, supported platforms are Windows, Mac & WSL 6 | * @returns { boolean } if the current platform is supported by azureauth 7 | */ 8 | export const isSupportedPlatformAndArchitecture = (): boolean => { 9 | const supportedPlatformsAndArchitectures: Record = { 10 | win32: ["x64"], 11 | darwin: ["x64", "arm64"], 12 | }; 13 | 14 | return ( 15 | isWsl() || 16 | (supportedPlatformsAndArchitectures[platform()] && 17 | supportedPlatformsAndArchitectures[platform()].includes(arch())) 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { isSupportedPlatformAndArchitecture } from "./azureauth/is-supported-platform-and-architecture.js"; 2 | import { isCodespaces } from "./utils/is-codespaces.js"; 3 | import { logTelemetry } from "./telemetry/index.js"; 4 | import { arch, platform } from "os"; 5 | import { Args, parseArgs } from "./args.js"; 6 | import { NpmrcFileProvider } from "./npmrc/npmrcFileProvider.js"; 7 | import { defaultEmail, defaultUser, ValidatedFeed } from "./fileProvider.js"; 8 | import { generateNpmrcPat } from "./npmrc/generate-npmrc-pat.js"; 9 | import { partition } from "./utils/partition.js"; 10 | import { YarnRcFileProvider } from "./yarnrc/yarnrcFileProvider.js"; 11 | 12 | export const run = async (args: Args): Promise => { 13 | const fileProviders = [ 14 | new NpmrcFileProvider(args.configFile), 15 | new YarnRcFileProvider(args.configFile), 16 | ]; 17 | 18 | const validatedFeeds: ValidatedFeed[] = []; 19 | if (args.doValidCheck || args.skipAuth) { 20 | for (const fileProvider of fileProviders) { 21 | if (await fileProvider.isSupportedInRepo()) { 22 | validatedFeeds.push(...(await fileProvider.validateAllUsedFeeds())); 23 | } 24 | } 25 | } 26 | 27 | // Filter to feeds to only feeds that were not authenticated and are actually 28 | // azure devops feeds by checking if we discovered the adoOrganization for it. 29 | const invalidFeeds = validatedFeeds.filter( 30 | (feed) => !feed.isValid && feed.feed.adoOrganization, 31 | ); 32 | const invalidFeedCount = invalidFeeds.length; 33 | 34 | if (args.doValidCheck && invalidFeedCount == 0) { 35 | return null; 36 | } 37 | 38 | if (args.skipAuth && invalidFeedCount != 0) { 39 | logTelemetry( 40 | { success: false, automaticSuccess: false, error: "invalid token(s)" }, 41 | true, 42 | ); 43 | console.log( 44 | invalidFeedCount == 1 45 | ? "❌ Your token is invalid." 46 | : `❌ ${invalidFeedCount} tokens are invalid.`, 47 | ); 48 | return false; 49 | } 50 | 51 | try { 52 | console.log("🔑 Authenticating to package feed..."); 53 | 54 | const adoOrgs = new Set(); 55 | for (const adoOrg of invalidFeeds.map( 56 | (feed) => feed.feed.adoOrganization, 57 | )) { 58 | adoOrgs.add(adoOrg); 59 | } 60 | 61 | // get a token for each feed 62 | const organizationPatMap: Record = {}; 63 | for (const adoOrg of adoOrgs) { 64 | organizationPatMap[adoOrg] = await generateNpmrcPat( 65 | adoOrg, 66 | false, 67 | args.azureAuthLocation, 68 | ); 69 | } 70 | 71 | // Update the pat in the invalid feeds. 72 | for (const invalidFeed of invalidFeeds) { 73 | const feed = invalidFeed.feed; 74 | 75 | const authToken = organizationPatMap[feed.adoOrganization]; 76 | if (!authToken) { 77 | console.log( 78 | `❌ Failed to obtain pat for ${feed.registry} via ${invalidFeed.fileProvider.id}`, 79 | ); 80 | return false; 81 | } 82 | feed.authToken = authToken; 83 | if (!feed.email) { 84 | feed.email = defaultEmail; 85 | } 86 | if (!feed.userName) { 87 | feed.userName = defaultUser; 88 | } 89 | } 90 | 91 | const invalidFeedsByProvider = partition( 92 | invalidFeeds, 93 | (feed) => feed.fileProvider, 94 | ); 95 | for (const [fileProvider, updatedFeeds] of invalidFeedsByProvider) { 96 | await fileProvider.writeWorkspaceRegistries( 97 | updatedFeeds.map((updatedFeed) => updatedFeed.feed), 98 | ); 99 | } 100 | 101 | return true; 102 | } catch (error) { 103 | logTelemetry( 104 | { 105 | success: false, 106 | automaticSuccess: false, 107 | error: (error as Error).message, 108 | }, 109 | true, 110 | ); 111 | console.log("Encountered error while performing auth", error); 112 | return false; 113 | } 114 | }; 115 | 116 | if (isCodespaces()) { 117 | // ignore codespaces setups 118 | process.exit(0); 119 | } 120 | 121 | if (!isSupportedPlatformAndArchitecture()) { 122 | const errorMessage = `Platform ${platform()} and architecture ${arch()} not supported for automatic authentication.`; 123 | console.log(errorMessage); 124 | logTelemetry({ success: false, error: errorMessage }, true); 125 | process.exit(0); 126 | } 127 | 128 | const args = parseArgs(process.argv); 129 | 130 | run(args) 131 | .then((result) => { 132 | if (result === null) { 133 | // current auth is valid, do nothing 134 | logTelemetry({ success: true }); 135 | console.log("✅ Current authentication is valid"); 136 | } else if (result) { 137 | // automatic auth was performed 138 | // advertise success 139 | logTelemetry({ success: true, automaticSuccess: true }); 140 | console.log("✅ Automatic authentication successful"); 141 | // if the user specified an exit code for reauthenticate, exit 142 | if (args.exitCodeOnReAuthenticate !== undefined) { 143 | process.exit(args.exitCodeOnReAuthenticate); 144 | } 145 | } else { 146 | // automatic auth failed (for some reason) 147 | // advertise failure and link wiki to fix 148 | console.log("❌ Authentication to package feed failed."); 149 | 150 | process.exitCode = 1; 151 | } 152 | }) 153 | .catch((error) => { 154 | console.error(error); 155 | console.log("❌ Authentication to package feed failed."); 156 | 157 | process.exitCode = 1; 158 | }); 159 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/fileProvider.ts: -------------------------------------------------------------------------------- 1 | import { getWorkspaceRoot } from "workspace-tools"; 2 | import { join } from "node:path"; 3 | import fs from "node:fs/promises"; 4 | import path from "node:path"; 5 | import { homedir } from "node:os"; 6 | import { getOrganizationFromFeedUrl } from "./utils/get-organization-from-feed-url.js"; 7 | import { makeADORequest } from "./ado/make-ado-request.js"; 8 | 9 | /** 10 | * Default user to be used in the .npmrc 11 | */ 12 | export const defaultUser = "me"; 13 | 14 | /** 15 | * Default email to be used in the .npmrc 16 | */ 17 | export const defaultEmail = "me@example.com"; 18 | 19 | export interface Feed { 20 | registry: string; 21 | adoOrganization: string; 22 | userName?: string; 23 | email?: string; 24 | authToken?: string; 25 | } 26 | 27 | export type ValidatedFeed = { 28 | feed: Feed; 29 | isValid: boolean; 30 | fileProvider: FileProvider; 31 | }; 32 | 33 | export abstract class FileProvider { 34 | public workspaceFilePath: string; 35 | public userFilePath: string; 36 | 37 | public feeds: Map; 38 | 39 | constructor( 40 | public id: string, 41 | public workspaceFileName: string, 42 | configFile?: string, 43 | ) { 44 | let workspaceFilePath = undefined; 45 | if (configFile && path.basename(configFile) === this.workspaceFileName) { 46 | workspaceFilePath = path.resolve(configFile); 47 | } else { 48 | const workspaceRoot = getWorkspaceRoot(process.cwd()) || ""; 49 | workspaceFilePath = join(workspaceRoot, this.workspaceFileName); 50 | } 51 | this.workspaceFilePath = workspaceFilePath; 52 | 53 | const userHome = 54 | process.env["HOME"] || process.env["USERPROFILE"] || homedir() || ""; 55 | this.userFilePath = join(userHome, workspaceFileName); 56 | this.feeds = new Map(); 57 | } 58 | 59 | public async isSupportedInRepo(): Promise { 60 | try { 61 | await fs.access(this.workspaceFilePath); 62 | } catch (error) { 63 | return false; 64 | } 65 | 66 | return true; 67 | } 68 | 69 | public async validateAllUsedFeeds(): Promise { 70 | await this.prepUserFile(); 71 | 72 | const result: ValidatedFeed[] = []; 73 | 74 | const workspaceRegistries = await this.getWorkspaceRegistries(); 75 | const userFeeds = await this.getUserFeeds(); 76 | 77 | // check each feed for validity 78 | for (const registry of workspaceRegistries) { 79 | const feed = userFeeds.get(registry); 80 | 81 | if (feed) { 82 | let feedIsValid = true; 83 | try { 84 | await makeADORequest({ 85 | password: feed.authToken || "", 86 | organization: feed.adoOrganization, 87 | }); 88 | } catch (e) { 89 | feedIsValid = false; 90 | } 91 | result.push({ feed: feed, isValid: feedIsValid, fileProvider: this }); 92 | } else { 93 | // No representation of the token in the users config file. 94 | result.push({ 95 | feed: { 96 | registry: registry, 97 | adoOrganization: getOrganizationFromFeedUrl(registry), 98 | }, 99 | isValid: false, 100 | fileProvider: this, 101 | }); 102 | } 103 | } 104 | 105 | return result; 106 | } 107 | 108 | abstract prepUserFile(): Promise; 109 | abstract getUserFeeds(): Promise>; 110 | abstract getWorkspaceRegistries(): Promise; 111 | abstract writeWorkspaceRegistries( 112 | feedsToPatch: Iterable, 113 | ): Promise; 114 | } 115 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./yarnrc/yarnrcFileProvider.js"; 2 | export * from "./npmrc/npmrcFileProvider.js"; 3 | export * from "./fileProvider.js"; 4 | 5 | export * from "./npmrc/generate-npmrc-pat.js"; 6 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/npmrc/generate-npmrc-pat.ts: -------------------------------------------------------------------------------- 1 | import { hostname } from "os"; 2 | import { AdoPatResponse, adoPat } from "../azureauth/ado.js"; 3 | import { toBase64 } from "../utils/encoding.js"; 4 | 5 | /** 6 | * Generates a valid ADO PAT, scoped for vso.packaging in the given ado organization, 30 minute timeout 7 | * @returns { string } a valid ADO PAT 8 | */ 9 | export const generateNpmrcPat = async ( 10 | organization: string, 11 | encode = false, 12 | azureAuthLocation?: string, 13 | ): Promise => { 14 | const name = `${hostname()}-${organization}`; 15 | const pat = await adoPat( 16 | { 17 | promptHint: `${name} .npmrc PAT`, 18 | organization, 19 | displayName: `${name}-npmrc-pat`, 20 | scope: ["vso.packaging"], 21 | timeout: "30", 22 | output: "json", 23 | }, 24 | azureAuthLocation, 25 | ); 26 | 27 | const rawToken = (pat as AdoPatResponse).token; 28 | 29 | if (encode) { 30 | return toBase64(rawToken); 31 | } 32 | 33 | return rawToken; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/npmrc/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ado-npm-auth/9429a12f00ef63aa0384abcfaf69e16ae1d4504f/packages/ado-npm-auth/src/npmrc/index.ts -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/npmrc/npmrcFileProvider.ts: -------------------------------------------------------------------------------- 1 | import Config from "@npmcli/config"; 2 | import { 3 | defaultEmail, 4 | defaultUser, 5 | Feed, 6 | FileProvider, 7 | } from "../fileProvider.js"; 8 | import fs from "node:fs/promises"; 9 | import { EOL } from "node:os"; 10 | import { getOrganizationFromFeedUrl } from "../utils/get-organization-from-feed-url.js"; 11 | import { getFeedWithoutProtocol } from "../utils/get-feed-without-protocol.js"; 12 | import { fromBase64, toBase64 } from "../utils/encoding.js"; 13 | import path from "node:path"; 14 | 15 | export class NpmrcFileProvider extends FileProvider { 16 | constructor(configFile?: string) { 17 | super("NpmRc", ".npmrc", configFile); 18 | } 19 | 20 | override async prepUserFile(): Promise { 21 | try { 22 | const npmrcContent = await fs.readFile(this.userFilePath, "utf-8"); 23 | 24 | // remove the entry for registries in the user-level .npmrc 25 | const updatedNpmrcContent = npmrcContent 26 | .split(EOL) 27 | .filter((line) => !line.includes("registry=")) 28 | .join(EOL); 29 | await fs.writeFile(this.userFilePath, updatedNpmrcContent); 30 | } catch (error) { 31 | // user npmrc does not exist so make an empty one 32 | await fs.writeFile(this.userFilePath, ""); 33 | } 34 | } 35 | 36 | override async getWorkspaceRegistries(): Promise { 37 | let config!: Config; 38 | 39 | try { 40 | config = new Config({ 41 | npmPath: this.workspaceFilePath, 42 | argv: [ 43 | "", // pnpm code always slices the first two arv arguments 44 | "", // pnpm code always slices the first two arv arguments 45 | 46 | // This is to ensure that the parser picks up the selected .npmrc file 47 | // as the built-in logic for finding the .npmrc file is not resiliant 48 | // for repo's that are of mixed languages i.e. js + cpp + C# etc. 49 | // The assumption is that the .npmrc file is next to a node_modules or package.json 50 | // file which in large mono-repo's at is not always the case. 51 | `--prefix=${path.dirname(this.workspaceFilePath)}`, 52 | ], 53 | shorthands: {}, 54 | definitions: {} as any, // needed so we can access random feed names 55 | }); 56 | await config.load(); 57 | } catch (error) { 58 | if (error instanceof TypeError && error.message.includes("Invalid URL")) { 59 | throw new Error("Registry URL missing or invalid"); 60 | } 61 | throw new Error("Error loading .npmrc"); 62 | } 63 | 64 | // @npmcli/config does not have a normal way to display all keys 65 | // so we use this ugly access instead 66 | const projectNpmrcKeys = Object.keys( 67 | (config.data?.get("project") || {})["data"] || {}, 68 | ); 69 | 70 | // find any and all keys which are a registry 71 | const registries = projectNpmrcKeys.filter((key) => 72 | key.includes("registry"), 73 | ); 74 | 75 | return registries 76 | .map((registry) => config.get(registry, "project") as string) 77 | .map((feed) => getFeedWithoutProtocol(feed)); 78 | } 79 | 80 | override async getUserFeeds(): Promise> { 81 | const result = new Map(); 82 | 83 | await this.processNpmRcFile( 84 | this.userFilePath, 85 | (_: string, registry: string, field: string, value: string) => { 86 | let feed = result.get(registry); 87 | if (!feed) { 88 | feed = { 89 | registry: registry, 90 | adoOrganization: getOrganizationFromFeedUrl(registry), 91 | }; 92 | result.set(feed.registry, feed); 93 | } 94 | switch (field) { 95 | case "_password": 96 | feed.authToken = fromBase64(value).trim(); 97 | break; 98 | case "username": 99 | feed.userName = value; 100 | break; 101 | case "email": 102 | feed.email = value; 103 | break; 104 | } 105 | }, 106 | ); 107 | 108 | return result; 109 | } 110 | 111 | async patchUserNpmRcFile( 112 | newLinesByRegistryAndField: Map, 113 | ): Promise { 114 | const linesToAdd = new Set(newLinesByRegistryAndField.values()); 115 | 116 | const npmrcLines = await this.processNpmRcFile( 117 | this.userFilePath, 118 | (line, registry, field, _value) => { 119 | const newLine = newLinesByRegistryAndField.get( 120 | this.toRegistryAndFunctionKey(registry, field), 121 | ); 122 | if (newLine !== undefined) { 123 | linesToAdd.delete(newLine); 124 | return newLine; 125 | } 126 | 127 | return line; 128 | }, 129 | (line) => line, 130 | ); 131 | 132 | for (const lineToAdd of linesToAdd) { 133 | npmrcLines.push(lineToAdd); 134 | } 135 | 136 | return npmrcLines; 137 | } 138 | 139 | toRegistryAndFunctionKey(registry: string, field: string): string { 140 | return `//${registry}:${field}=`; 141 | } 142 | 143 | override async writeWorkspaceRegistries( 144 | feedsToPatch: Iterable, 145 | ): Promise { 146 | const newLinesByRegistryAndField = new Map(); 147 | 148 | // Build a map with registry and feed with the updated line for value. 149 | for (var feedToPatch of feedsToPatch) { 150 | newLinesByRegistryAndField.set( 151 | this.toRegistryAndFunctionKey(feedToPatch.registry, "username"), 152 | `//${feedToPatch.registry}:username=${feedToPatch.userName || defaultUser}`, 153 | ); 154 | newLinesByRegistryAndField.set( 155 | this.toRegistryAndFunctionKey(feedToPatch.registry, "email"), 156 | `//${feedToPatch.registry}:email=${feedToPatch.email || defaultEmail}`, 157 | ); 158 | newLinesByRegistryAndField.set( 159 | this.toRegistryAndFunctionKey(feedToPatch.registry, "_password"), 160 | `//${feedToPatch.registry}:_password=${toBase64(feedToPatch.authToken)}`, 161 | ); 162 | } 163 | 164 | const npmrcLines = await this.patchUserNpmRcFile( 165 | newLinesByRegistryAndField, 166 | ); 167 | 168 | await fs.writeFile(this.userFilePath, npmrcLines.join(EOL), { 169 | encoding: "utf-8", 170 | }); 171 | } 172 | 173 | async processNpmRcFile( 174 | npmrcFilePath: string, 175 | handleFeedConfig: ( 176 | line: string, 177 | registry: string, 178 | field: string, 179 | value: string, 180 | ) => string | void, 181 | handleOtherLine?: (line: string) => string | void, 182 | ): Promise { 183 | const npmrc = await fs.readFile(npmrcFilePath, { 184 | encoding: "utf8", 185 | }); 186 | const npmrcLines = npmrc.split("\n").map((line: string) => line.trim()); 187 | 188 | const resultLines: string[] = []; 189 | for (const line of npmrcLines) { 190 | const slashColonIndex = line.indexOf("/:"); 191 | if (line.startsWith("//") && slashColonIndex >= 2) { 192 | const registry = line.substring(2, slashColonIndex + 1); 193 | const remainder = line.substring(slashColonIndex + 2); 194 | const field = remainder.substring(0, remainder.indexOf("=")); 195 | const value = remainder.substring(remainder.indexOf("=") + 1); 196 | 197 | const newLine = handleFeedConfig(line, registry, field, value); 198 | if (newLine) { 199 | resultLines.push(newLine); 200 | } 201 | } else if (handleOtherLine) { 202 | const newLine = handleOtherLine(line); 203 | if (newLine) { 204 | resultLines.push(newLine); 205 | } 206 | } 207 | } 208 | 209 | return resultLines; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/telemetry/index.ts: -------------------------------------------------------------------------------- 1 | import { platform, arch } from "os"; 2 | import { isWsl } from "../utils/is-wsl.js"; 3 | 4 | export type TelemetryProperties = { 5 | success: boolean; 6 | automaticSuccess: boolean; 7 | platform: string; 8 | arch: string; 9 | error: string; 10 | }; 11 | 12 | export interface TelemetryClient { 13 | LogEvent: (eventName: string, properties: Map) => void; 14 | flush(): void; 15 | } 16 | 17 | /** 18 | * Logs an node-azure-auth event to telemetry. 19 | */ 20 | export const logTelemetry = ( 21 | inputProperties: { 22 | success?: boolean; 23 | automaticSuccess?: boolean; 24 | error?: string; 25 | }, 26 | flush?: boolean, 27 | client?: TelemetryClient, 28 | ) => { 29 | const outputProperties = new Map(); 30 | outputProperties.set("success", inputProperties.success ? "true" : "false"); 31 | outputProperties.set( 32 | "automaticSuccess", 33 | inputProperties.automaticSuccess ? "true" : "false", 34 | ); 35 | outputProperties.set("error", inputProperties.error || ""); 36 | outputProperties.set("platform", isWsl() ? "wsl" : platform()); 37 | outputProperties.set("arch", arch()); 38 | 39 | if (client) { 40 | client.LogEvent("node-azure-auth", outputProperties); 41 | 42 | if (flush) { 43 | client.flush(); 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/encoding.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a base64 encoded string from a string 3 | * @param {string} input 4 | * @returns {string} 5 | */ 6 | export function toBase64(input: string | undefined): string { 7 | return Buffer.from(input || "").toString("base64"); 8 | } 9 | 10 | /** 11 | * Decode a base64 encoded string 12 | * @param {string} base64string 13 | * @returns 14 | */ 15 | export const fromBase64 = (base64string: string) => 16 | Buffer.from(base64string, "base64").toString("utf8"); 17 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/exec.ts: -------------------------------------------------------------------------------- 1 | import { exec as _exec } from "node:child_process"; 2 | import { promisify } from "node:util"; 3 | 4 | export const exec = promisify(_exec); 5 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/get-feed-without-protocol.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { getFeedWithoutProtocol } from "./get-feed-without-protocol.js"; 3 | 4 | test("getFeedWithoutProtocol with a normal feed", () => { 5 | const feedWithoutProtocol = getFeedWithoutProtocol( 6 | "https://foo.bar.baz/org/proj/_packaging/repo/npm/registry/", 7 | ); 8 | expect(feedWithoutProtocol).toBe( 9 | "foo.bar.baz/org/proj/_packaging/repo/npm/registry/", 10 | ); 11 | }); 12 | 13 | test("getFeedWithoutProtocol throws error with an empty feed", () => { 14 | expect(() => getFeedWithoutProtocol("")).toThrowError("Invalid URL"); 15 | }); 16 | 17 | test("getFeedWithoutProtocol with a port specified", () => { 18 | // hint: if you use the default port for the protocol (ex https/443) then it will be stripped as it is not necessary 19 | const feedHasPortWithoutProtocol = getFeedWithoutProtocol( 20 | "https://foo.bar:444/baz", 21 | ); 22 | expect(feedHasPortWithoutProtocol).toBe("foo.bar:444/baz"); 23 | }); 24 | 25 | test("getFeedWithoutProtocol with a query argument specified", () => { 26 | const feedHasQueryArgWithoutProtocol = getFeedWithoutProtocol( 27 | "https://foo.bar/baz?qux=qaz", 28 | ); 29 | expect(feedHasQueryArgWithoutProtocol).toBe("foo.bar/baz?qux=qaz"); 30 | }); 31 | 32 | test("getFeedWithoutProtocol with many query arguments specified", () => { 33 | const feedHasQueryArgsWithoutProtocol = getFeedWithoutProtocol( 34 | "https://foo.bar/baz?qux=qaz&type=multi&env=test", 35 | ); 36 | expect(feedHasQueryArgsWithoutProtocol).toBe( 37 | "foo.bar/baz?qux=qaz&type=multi&env=test", 38 | ); 39 | }); 40 | 41 | test("getFeedWithoutProtocol with a fragment specified", () => { 42 | const feedHasFragmentWithoutProtocol = getFeedWithoutProtocol( 43 | "https://foo.bar/baz#frag", 44 | ); 45 | expect(feedHasFragmentWithoutProtocol).toBe("foo.bar/baz#frag"); 46 | }); 47 | 48 | test("getFeedWithoutProtocol with a port, many query arguments and fragment specified", () => { 49 | const feedHasQueryArgsWithoutProtocol = getFeedWithoutProtocol( 50 | "https://foo.bar.com:440/baz/pop?qux=qaz&type=multi&env=test#frag", 51 | ); 52 | expect(feedHasQueryArgsWithoutProtocol).toBe( 53 | "foo.bar.com:440/baz/pop?qux=qaz&type=multi&env=test#frag", 54 | ); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/get-feed-without-protocol.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Given the feed url, get the name of the feed without the protocol ("https") at the beginning 3 | */ 4 | export const getFeedWithoutProtocol = (feed: string): string => { 5 | const feedUrl = new URL(feed); 6 | const protocol = feedUrl.protocol; // will be something like "http:" 7 | const protocolLength = protocol.length + 2; // we want to strip out the protocol, colon, and double slash 8 | const feedWithoutProtocol = feedUrl.toString().slice(protocolLength); 9 | return feedWithoutProtocol; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/get-organization-from-feed-url.test.ts: -------------------------------------------------------------------------------- 1 | import { getOrganizationFromFeedUrl } from "./get-organization-from-feed-url.js"; 2 | import { expect, test } from "vitest"; 3 | 4 | test("should get the correct projects from the feed url", () => { 5 | const stuff = getOrganizationFromFeedUrl( 6 | "https://pkgs.dev.azure.com/org/proj/_packaging/feedname/npm/registry/", 7 | ); 8 | 9 | expect(stuff).toBe("org"); 10 | }); 11 | 12 | test("should get the correct projects from the feed url with only a project name", () => { 13 | const stuff = getOrganizationFromFeedUrl( 14 | "https://pkgs.dev.azure.com/org/proj/_packaging/feedname/npm/registry/", 15 | ); 16 | 17 | expect(stuff).toBe("org"); 18 | }); 19 | 20 | test("should get the correct projects from the feed url with only a project name but without _packaging", () => { 21 | const stuff = getOrganizationFromFeedUrl( 22 | "https://org.dev.azure.com/org/proj/feedname/npm/registry/", 23 | ); 24 | 25 | expect(stuff).toBe("org"); 26 | }); 27 | 28 | test("should work for legacy vs urls", () => { 29 | const stuff = getOrganizationFromFeedUrl( 30 | "https://org.pkgs.visualstudio.com/proj/_packaging/feed/npm/registry/", 31 | ); 32 | 33 | expect(stuff).toBe("org"); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/get-organization-from-feed-url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracts the organization and project details from a given Azure DevOps URL. 3 | * The function differentiates between the "new style" URLs that use 'dev.azure.com' 4 | * and "old style" URLs that use a subdomain of 'visualstudio.com'. 5 | * 6 | * @param {string} url - The Azure DevOps URL from which to extract details. 7 | * @returns {Object} An object containing the `organization` and `project` extracted from the URL. 8 | * @throws {Error} Throws an error if the URL is invalid, not in the expected format, 9 | * or does not contain the necessary information for extraction. 10 | * 11 | * @example 12 | * // New style URL 13 | * extractAdoDetails("https://dev.azure.com/contoso/WebsiteRedesign"); 14 | * // returns { organization: "contoso", project: "WebsiteRedesign" } 15 | * 16 | * // Old style URL 17 | * extractAdoDetails("https://contoso.visualstudio.com/WebsiteRedesign"); 18 | * // returns { organization: "contoso", project: "WebsiteRedesign" } 19 | * 20 | * // Invalid URL 21 | * extractAdoDetails("https://invalid.url.com"); 22 | * // throws Error 23 | */ 24 | const extractAdoDetails = (url: string) => { 25 | try { 26 | if (!url.startsWith("https://")) { 27 | url = "https://" + url; 28 | } 29 | const parsedUrl = new URL(url); 30 | const hostname = parsedUrl.hostname; 31 | const pathname = parsedUrl.pathname; 32 | 33 | // Check for new style URLs (dev.azure.com) 34 | if (hostname.endsWith("dev.azure.com")) { 35 | const pathSegments = pathname.split("/").filter(Boolean); // Remove empty strings from the split result 36 | if (pathSegments.length >= 2) { 37 | return { 38 | organization: pathSegments[0], 39 | project: pathSegments[1], 40 | }; 41 | } else { 42 | throw new Error( 43 | "Not enough segments in path for a valid organization and project extraction.", 44 | ); 45 | } 46 | } 47 | 48 | // Check for old style URLs (visualstudio.com) 49 | if (hostname.endsWith("visualstudio.com")) { 50 | const subdomain = hostname.split(".")[0]; 51 | const pathSegments = pathname.split("/").filter(Boolean); 52 | if (subdomain && pathSegments.length >= 1) { 53 | return { 54 | organization: subdomain, 55 | project: pathSegments[0], 56 | }; 57 | } else { 58 | throw new Error( 59 | "Not enough segments in path or missing subdomain for a valid organization and project extraction.", 60 | ); 61 | } 62 | } 63 | 64 | // If the URL does not match expected formats 65 | throw new Error( 66 | "URL format not recognized or does not contain enough information.", 67 | ); 68 | } catch (error) { 69 | throw new Error("Invalid URL or unsupported format"); 70 | } 71 | }; 72 | 73 | /** 74 | * Get the ADO Org for a npm feed 75 | * @param {string} feedUrl URL of the feed to get the ADO organization from 76 | * @param {string} [defaultOrg] Backup org in case it cannot be determined from the feed url 77 | * @returns ADO Organization for the feed 78 | */ 79 | export const getOrganizationFromFeedUrl = ( 80 | feedUrl: string, 81 | defaultOrg = "", 82 | ) => { 83 | try { 84 | const { organization } = extractAdoDetails(feedUrl); 85 | return organization; 86 | } catch (error) { 87 | return defaultOrg; 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/is-codespaces.ts: -------------------------------------------------------------------------------- 1 | const truthy = [true, "true", "TRUE", 1]; 2 | 3 | /** 4 | * Determines if the current machine's setup is codespaces 5 | * @returns { boolean } if the current machine is in codespaces 6 | */ 7 | export const isCodespaces = () => { 8 | const codespaces = process.env["CODESPACES"] || ""; 9 | return truthy.includes(codespaces); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/is-wsl.ts: -------------------------------------------------------------------------------- 1 | import { release, platform } from "os"; 2 | 3 | /** 4 | * Determine if the current machine's platform is WSL 5 | * @returns { boolean } if the current platform is WSL 6 | */ 7 | export const isWsl = () => { 8 | return platform() === "linux" && release().toLowerCase().includes("wsl"); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/partition.ts: -------------------------------------------------------------------------------- 1 | export function partition( 2 | items: TValue[], 3 | keySelector: (item: TValue) => TKey, 4 | ): Map { 5 | const result = new Map(); 6 | for (var item of items) { 7 | const key = keySelector(item); 8 | const existing = result.get(key); 9 | if (existing) { 10 | existing.push(item); 11 | } else { 12 | result.set(key, [item]); 13 | } 14 | } 15 | 16 | return result; 17 | } 18 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import https, { RequestOptions } from "https"; 2 | 3 | const defaultOptions: RequestOptions = { 4 | port: 443, 5 | method: "GET", 6 | }; 7 | 8 | /** 9 | * 10 | * @param {import("http").RequestOptions} options 11 | * @returns 12 | */ 13 | export const makeRequest = async (options: RequestOptions) => { 14 | return new Promise((resolve, reject) => { 15 | const mergedOptions = { 16 | ...defaultOptions, 17 | ...options, 18 | }; 19 | 20 | const req = https.request(mergedOptions, (res) => { 21 | let data = ""; 22 | let dataJson = {}; 23 | let ok = res.statusCode === 200; 24 | 25 | res.on("data", (d) => { 26 | data += d; 27 | }); 28 | 29 | res.on("end", () => { 30 | if (data && mergedOptions?.headers?.Accept === "application/json") { 31 | dataJson = JSON.parse(data.toString().trim()); 32 | } 33 | 34 | if (ok) { 35 | resolve(dataJson || data); 36 | } else { 37 | if (dataJson) { 38 | dataJson = { ...dataJson, statusCode: res.statusCode }; 39 | } 40 | reject( 41 | dataJson || data || new Error(`Error code: ${res.statusCode}.`), 42 | ); 43 | } 44 | }); 45 | 46 | res.on("error", (/** @type {string} */ error) => { 47 | reject(new Error(error as any)); 48 | }); 49 | }); 50 | 51 | req.end(); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/src/yarnrc/yarnrcFileProvider.ts: -------------------------------------------------------------------------------- 1 | import { Feed, FileProvider } from "../fileProvider.js"; 2 | import yaml from "js-yaml"; 3 | import fs from "node:fs/promises"; 4 | import { fromBase64, toBase64 } from "../utils/encoding.js"; 5 | import { getOrganizationFromFeedUrl } from "../utils/get-organization-from-feed-url.js"; 6 | import { getFeedWithoutProtocol } from "../utils/get-feed-without-protocol.js"; 7 | 8 | export class YarnRcFileProvider extends FileProvider { 9 | constructor(configFile?: string) { 10 | super("YarnRc", ".yarnrc.yml", configFile); 11 | } 12 | 13 | override async prepUserFile(): Promise { 14 | try { 15 | const yarnrc = await this.paseYarnRc(this.userFilePath); 16 | 17 | // remove the entry for registries in the user-level .npmrc 18 | if (yarnrc && yarnrc.npmRegistryServer) { 19 | delete yarnrc.npmRegistryServer; 20 | await this.writeYarnRc(this.userFilePath, yarnrc); 21 | } 22 | } catch (error) { 23 | // user .yarnrc file does not exist so make an empty one 24 | await fs.writeFile(this.userFilePath, ""); 25 | } 26 | } 27 | 28 | override async getUserFeeds(): Promise> { 29 | const result = new Map(); 30 | const yarnrc = await this.paseYarnRc(this.userFilePath); 31 | if (!yarnrc) { 32 | // No content 33 | return result; 34 | } 35 | const npmRegistries = yarnrc.npmRegistries || {}; 36 | for (var registry of Object.keys(npmRegistries)) { 37 | const registryData = npmRegistries[registry] || {}; 38 | const registryWithoutProtocol = registry.startsWith("//") 39 | ? registry.substring(2) 40 | : registry; 41 | 42 | const feed: Feed = { 43 | registry: registryWithoutProtocol, 44 | adoOrganization: getOrganizationFromFeedUrl(registryWithoutProtocol), 45 | }; 46 | 47 | const authToken = fromBase64(registryData.npmAuthIdent || ""); 48 | const userPasswordIndex = authToken.indexOf(":"); 49 | if (userPasswordIndex > 0) { 50 | feed.userName = authToken.substring(0, userPasswordIndex); 51 | feed.authToken = authToken.substring(userPasswordIndex + 1); 52 | } 53 | 54 | result.set(registryWithoutProtocol, feed); 55 | } 56 | 57 | return result; 58 | } 59 | 60 | override async getWorkspaceRegistries(): Promise { 61 | const registries: string[] = []; 62 | const yarnrc = await this.paseYarnRc(this.workspaceFilePath); 63 | 64 | if (yarnrc.npmRegistryServer) { 65 | registries.push(getFeedWithoutProtocol(yarnrc.npmRegistryServer)); 66 | } 67 | 68 | if (yarnrc.npmScopes) { 69 | for (var scope of Object.keys(yarnrc.npmScopes)) { 70 | const scopeRegistry = yarnrc.npmScopes[scope]?.npmRegistryServer; 71 | if (scopeRegistry) { 72 | registries.push(scopeRegistry); 73 | } 74 | } 75 | } 76 | 77 | return registries; 78 | } 79 | 80 | override async writeWorkspaceRegistries( 81 | feedsToPatch: Iterable, 82 | ): Promise { 83 | const yarnrc = (await this.paseYarnRc(this.userFilePath)) || {}; 84 | 85 | if (!yarnrc.npmRegistries) { 86 | yarnrc.npmRegistries = {}; 87 | } 88 | 89 | for (var feed of feedsToPatch) { 90 | const yarnRcYamlKey = "//" + feed.registry; 91 | const entry = yarnrc.npmRegistries[yarnRcYamlKey] || {}; 92 | // Make sure alwaysAuth is the default 93 | if (entry.npmAlwaysAuth === undefined) { 94 | entry.npmAlwaysAuth = true; 95 | } 96 | entry.npmAuthIdent = toBase64(`${feed.userName}:${feed.authToken}`); 97 | yarnrc.npmRegistries[yarnRcYamlKey] = entry; 98 | } 99 | 100 | await this.writeYarnRc(this.userFilePath, yarnrc); 101 | } 102 | 103 | async writeYarnRc(filePath: string, yarnrc: YarnRc) { 104 | const yarnrcContent = yaml.dump(yarnrc); 105 | await fs.writeFile(filePath, yarnrcContent, "utf8"); 106 | } 107 | 108 | async paseYarnRc(filePath: string): Promise { 109 | const content = await fs.readFile(filePath, "utf8"); 110 | return yaml.load(content, { filename: filePath }) as YarnRc; 111 | } 112 | } 113 | 114 | interface YarnRc { 115 | npmRegistryServer?: string; 116 | npmScopes?: { 117 | [org: string]: { 118 | npmRegistryServer?: string; 119 | npmAlwaysAuth?: boolean; 120 | npmAuthIdent?: string; 121 | npmAuthToken?: string; 122 | }; 123 | }; 124 | npmRegistries?: { 125 | [registry: string]: { 126 | npmAlwaysAuth?: boolean; 127 | npmAuthIdent?: string; 128 | npmAuthToken?: string; 129 | }; 130 | }; 131 | } 132 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/static/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ado-npm-auth/9429a12f00ef63aa0384abcfaf69e16ae1d4504f/packages/ado-npm-auth/static/image.png -------------------------------------------------------------------------------- /packages/ado-npm-auth/static/preinstall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ado-npm-auth/9429a12f00ef63aa0384abcfaf69e16ae1d4504f/packages/ado-npm-auth/static/preinstall.png -------------------------------------------------------------------------------- /packages/ado-npm-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "composite": true, 5 | "target": "ES2019", 6 | "rootDir": "src", 7 | "outDir": "lib", 8 | "incremental": true, 9 | "tsBuildInfoFile": "lib/.tsbuildinfo", 10 | "lib": ["ES2019", "dom"], 11 | "jsx": "react", 12 | "declaration": true, 13 | "declarationMap": true, 14 | "sourceMap": true, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "Node16", 17 | "strict": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noImplicitReturns": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "importHelpers": true, 24 | "esModuleInterop": true, 25 | "downlevelIteration": true, 26 | "types": ["node", "npmcli__config"] 27 | }, 28 | "include": ["./src"] 29 | } 30 | -------------------------------------------------------------------------------- /packages/ado-npm-auth/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import webpack from "webpack"; 3 | import { fileURLToPath } from "url"; 4 | import TerserPlugin from "terser-webpack-plugin"; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | 8 | export default { 9 | mode: "production", 10 | entry: { 11 | cli: path.join(__dirname, "./lib/cli.js"), 12 | }, 13 | output: { 14 | path: path.join(__dirname, "dist"), 15 | filename: "ado-npm-auth.cjs", 16 | }, 17 | optimization: { 18 | minimize: true, 19 | minimizer: [new TerserPlugin()], 20 | }, 21 | target: "node", 22 | devtool: false, 23 | plugins: [ 24 | // Keeps webpack from doing lots of chunks 25 | new webpack.optimize.LimitChunkCountPlugin({ 26 | maxChunks: 1, 27 | }), 28 | ], 29 | experiments: { 30 | topLevelAwait: true, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /packages/node-azureauth/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | lib -------------------------------------------------------------------------------- /packages/node-azureauth/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | src -------------------------------------------------------------------------------- /packages/node-azureauth/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2 3 | } -------------------------------------------------------------------------------- /packages/node-azureauth/CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azureauth", 3 | "entries": [ 4 | { 5 | "date": "Wed, 30 Oct 2024 20:31:48 GMT", 6 | "version": "0.12.1", 7 | "tag": "azureauth_v0.12.1", 8 | "comments": { 9 | "patch": [ 10 | { 11 | "author": "jcreamer@microsoft.com", 12 | "package": "azureauth", 13 | "commit": "04b908d952dc8bdc95713c1009b54cfefa9cf6aa", 14 | "comment": "fix the types for node-azureauth" 15 | } 16 | ] 17 | } 18 | }, 19 | { 20 | "date": "Mon, 07 Oct 2024 17:28:12 GMT", 21 | "version": "0.12.0", 22 | "tag": "azureauth_v0.12.0", 23 | "comments": { 24 | "minor": [ 25 | { 26 | "author": "dannyvv@microsoft.com", 27 | "package": "azureauth", 28 | "commit": "1731be2186e3f48b58dd6d114fc607a2686f40c0", 29 | "comment": "Expose functionality as a library" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "date": "Fri, 13 Sep 2024 19:47:53 GMT", 36 | "version": "0.11.0", 37 | "tag": "azureauth_v0.11.0", 38 | "comments": { 39 | "minor": [ 40 | { 41 | "author": "dannyvv@microsoft.com", 42 | "package": "azureauth", 43 | "commit": "5dc4767777f07a20589e3c98f4f0674d06f42cf2", 44 | "comment": "fix install script" 45 | } 46 | ] 47 | } 48 | }, 49 | { 50 | "date": "Fri, 13 Sep 2024 18:59:07 GMT", 51 | "version": "0.10.0", 52 | "tag": "azureauth_v0.10.0", 53 | "comments": { 54 | "minor": [ 55 | { 56 | "author": "dannyvv@microsoft.com", 57 | "package": "azureauth", 58 | "commit": "6c64e0b603f618d5604b5ade6fe4ad6802e63219", 59 | "comment": "files section lacked updated .cjs extension" 60 | } 61 | ] 62 | } 63 | }, 64 | { 65 | "date": "Fri, 13 Sep 2024 18:42:55 GMT", 66 | "version": "0.9.0", 67 | "tag": "azureauth_v0.9.0", 68 | "comments": { 69 | "minor": [ 70 | { 71 | "author": "dannyvv@microsoft.com", 72 | "package": "azureauth", 73 | "commit": "bc9d2cda630f75971b8c7679ef9030ce097daad6", 74 | "comment": "Migrate bin script to cjs" 75 | } 76 | ] 77 | } 78 | }, 79 | { 80 | "date": "Fri, 13 Sep 2024 18:15:05 GMT", 81 | "version": "0.8.0", 82 | "tag": "azureauth_v0.8.0", 83 | "comments": { 84 | "minor": [ 85 | { 86 | "author": "dannyvv@microsoft.com", 87 | "package": "azureauth", 88 | "commit": "e02ce4f37bd48a1bf01979c4a9c3b59dff5991ae", 89 | "comment": "Make bin script work form shell" 90 | } 91 | ] 92 | } 93 | }, 94 | { 95 | "date": "Fri, 13 Sep 2024 17:50:14 GMT", 96 | "version": "0.7.0", 97 | "tag": "azureauth_v0.7.0", 98 | "comments": { 99 | "minor": [ 100 | { 101 | "author": "dannyvv@microsoft.com", 102 | "package": "azureauth", 103 | "commit": "cab5dd80067b6add44ee5cb0ca33685ecbb6ee9b", 104 | "comment": "fix bin section" 105 | } 106 | ] 107 | } 108 | }, 109 | { 110 | "date": "Thu, 12 Sep 2024 21:56:08 GMT", 111 | "version": "0.6.0", 112 | "tag": "azureauth_v0.6.0", 113 | "comments": { 114 | "minor": [ 115 | { 116 | "author": "dannyvv@microsoft.com", 117 | "package": "azureauth", 118 | "commit": "9c68d1dd5344a9129a61e04e8b8dac09c1a27a9b", 119 | "comment": "Fix #42 include bin\\cli.js in package" 120 | } 121 | ] 122 | } 123 | }, 124 | { 125 | "date": "Wed, 11 Sep 2024 20:04:18 GMT", 126 | "version": "0.5.1", 127 | "tag": "azureauth_v0.5.1", 128 | "comments": { 129 | "patch": [ 130 | { 131 | "author": "dannyvv@microsoft.com", 132 | "package": "azureauth", 133 | "commit": "bc3533c7489857c75a8deec785397943990fec6f", 134 | "comment": "Fix missing file" 135 | } 136 | ] 137 | } 138 | }, 139 | { 140 | "date": "Wed, 11 Sep 2024 16:57:41 GMT", 141 | "version": "0.5.0", 142 | "tag": "azureauth_v0.5.0", 143 | "comments": { 144 | "minor": [ 145 | { 146 | "author": "dannyvv@microsoft.com", 147 | "package": "azureauth", 148 | "commit": "5d5c5cdc2b98bfbcbd29fcad19054255544c30c0", 149 | "comment": "Make node-azureauth use bundles to avoid pulling in 70 packages for a build tool" 150 | } 151 | ] 152 | } 153 | }, 154 | { 155 | "date": "Mon, 09 Sep 2024 17:27:01 GMT", 156 | "version": "0.4.7", 157 | "tag": "azureauth_v0.4.7", 158 | "comments": { 159 | "patch": [ 160 | { 161 | "author": "dannyvv@microsoft.com", 162 | "package": "azureauth", 163 | "commit": "bb5431e37d0b40ace73c9f06e736219c5412f32c", 164 | "comment": "Initial official release" 165 | } 166 | ] 167 | } 168 | } 169 | ] 170 | } 171 | -------------------------------------------------------------------------------- /packages/node-azureauth/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - azureauth 2 | 3 | This log was last generated on Wed, 30 Oct 2024 20:31:48 GMT and should not be manually modified. 4 | 5 | 6 | 7 | ## 0.12.1 8 | 9 | Wed, 30 Oct 2024 20:31:48 GMT 10 | 11 | ### Patches 12 | 13 | - fix the types for node-azureauth (jcreamer@microsoft.com) 14 | 15 | ## 0.12.0 16 | 17 | Mon, 07 Oct 2024 17:28:12 GMT 18 | 19 | ### Minor changes 20 | 21 | - Expose functionality as a library (dannyvv@microsoft.com) 22 | 23 | ## 0.11.0 24 | 25 | Fri, 13 Sep 2024 19:47:53 GMT 26 | 27 | ### Minor changes 28 | 29 | - fix install script (dannyvv@microsoft.com) 30 | 31 | ## 0.10.0 32 | 33 | Fri, 13 Sep 2024 18:59:07 GMT 34 | 35 | ### Minor changes 36 | 37 | - files section lacked updated .cjs extension (dannyvv@microsoft.com) 38 | 39 | ## 0.9.0 40 | 41 | Fri, 13 Sep 2024 18:42:55 GMT 42 | 43 | ### Minor changes 44 | 45 | - Migrate bin script to cjs (dannyvv@microsoft.com) 46 | 47 | ## 0.8.0 48 | 49 | Fri, 13 Sep 2024 18:15:05 GMT 50 | 51 | ### Minor changes 52 | 53 | - Make bin script work form shell (dannyvv@microsoft.com) 54 | 55 | ## 0.7.0 56 | 57 | Fri, 13 Sep 2024 17:50:14 GMT 58 | 59 | ### Minor changes 60 | 61 | - fix bin section (dannyvv@microsoft.com) 62 | 63 | ## 0.6.0 64 | 65 | Thu, 12 Sep 2024 21:56:08 GMT 66 | 67 | ### Minor changes 68 | 69 | - Fix #42 include bin\cli.js in package (dannyvv@microsoft.com) 70 | 71 | ## 0.5.1 72 | 73 | Wed, 11 Sep 2024 20:04:18 GMT 74 | 75 | ### Patches 76 | 77 | - Fix missing file (dannyvv@microsoft.com) 78 | 79 | ## 0.5.0 80 | 81 | Wed, 11 Sep 2024 16:57:41 GMT 82 | 83 | ### Minor changes 84 | 85 | - Make node-azureauth use bundles to avoid pulling in 70 packages for a build tool (dannyvv@microsoft.com) 86 | 87 | ## 0.4.7 88 | 89 | Mon, 09 Sep 2024 17:27:01 GMT 90 | 91 | ### Patches 92 | 93 | - Initial official release (dannyvv@microsoft.com) 94 | -------------------------------------------------------------------------------- /packages/node-azureauth/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /packages/node-azureauth/README.md: -------------------------------------------------------------------------------- 1 | # node-azureauth 2 | 3 | This package wraps the https://github.com/AzureAD/microsoft-authentication-cli with a node.js exec wrapper. 4 | 5 | That way the `azureauth` CLI can be downloaded automatically, and therefore scoped to the `./node_modules/.bin` allowing for multiple versions of the AzureAuth CLI 6 | to exist on one machine at once. 7 | 8 | ## Usage 9 | 10 | Install the package wiwth 11 | 12 | ```bash 13 | > npm i azureauth 14 | ``` 15 | 16 | Use the `azureauth` CLI by calling it from NPM scripts. 17 | 18 | ```json 19 | "scripts": { 20 | "authcli": "azureauth --version" 21 | } 22 | ``` 23 | 24 | ```bash 25 | > npm run authcli 26 | ``` 27 | 28 | Use it as a node module by importing azureauth. 29 | 30 | ```js 31 | import { adoPat } from "azureauth"; 32 | 33 | const pat = await adoPat({ 34 | displayName: "test", 35 | organization: "test", 36 | promptHint: "test", 37 | scope: ["test"], 38 | }); 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/node-azureauth/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import path from "node:path"; 3 | import process from "node:process"; 4 | import { execa } from "execa"; 5 | 6 | let azureauth = path.join(__dirname, "..", "bin", "azureauth", "azureauth"); 7 | 8 | if (process.platform === "win32") { 9 | azureauth = azureauth + ".exe"; 10 | } 11 | 12 | execa(azureauth, process.argv.slice(2), { stdio: "inherit" }); 13 | -------------------------------------------------------------------------------- /packages/node-azureauth/examples/pat.js: -------------------------------------------------------------------------------- 1 | import { adoPat } from "../lib/index.js"; 2 | 3 | const name = "feed-name"; 4 | 5 | const pat = await adoPat({ 6 | promptHint: `${name} .npmrc PAT`, 7 | organization: "your-organization-name-here", 8 | displayName: `${name}-npmrc-pat`, 9 | scope: ["vso.packaging"], 10 | timeout: "30", 11 | output: "json", 12 | }); 13 | 14 | console.log(pat); -------------------------------------------------------------------------------- /packages/node-azureauth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azureauth", 3 | "version": "0.12.1", 4 | "description": "node-azure auth wrapps the AzureAuth CLI wrapper for performing AAD Authentication", 5 | "bin": { 6 | "azureauth": "./scripts/azureauth.cjs" 7 | }, 8 | "repository": { 9 | "url": "https://github.com/microsoft/ado-npm-auth" 10 | }, 11 | "main": "dist/index.js", 12 | "types": "lib/index.d.ts", 13 | "type": "module", 14 | "scripts": { 15 | "build": "tsc", 16 | "bundle": "npm run bundleInstall && npm run bundleCli && npm run bundleIndex", 17 | "bundleInstall": "esbuild --sourcemap --bundle --minify --platform=node --outfile=dist/install.cjs src/install.ts", 18 | "bundleCli": "esbuild --sourcemap --bundle --minify --platform=node cli.js --outfile=dist/cli.cjs", 19 | "bundleIndex": "esbuild --sourcemap --bundle --minify --platform=node --format=esm src/index.ts --outfile=dist/index.js", 20 | "lint": "prettier --check src/**/*.ts ./cli.js ", 21 | "start": "tsc --watch", 22 | "test": "vitest run src", 23 | "postinstall": "node ./scripts/install.cjs" 24 | }, 25 | "files": [ 26 | "dist/install.cjs", 27 | "dist/cli.cjs", 28 | "dist/index.js", 29 | "scripts/azureauth.cjs", 30 | "scripts/install.cjs" 31 | ], 32 | "keywords": [ 33 | "node", 34 | "azureauth", 35 | "aad", 36 | "azure active directory", 37 | "authentication" 38 | ], 39 | "author": "Jonathan Creamer", 40 | "license": "MIT", 41 | "devDependencies": { 42 | "@types/decompress": "^4.2.7", 43 | "@types/node": "^20.17.28", 44 | "decompress": "^4.2.1", 45 | "esbuild": "^0.23.1", 46 | "execa": "^7.2.0", 47 | "node-downloader-helper": "^2.1.9", 48 | "prettier": "^3.5.3", 49 | "typescript": "^5.8.2", 50 | "vite": "~5.3.6", 51 | "vitest": "^2.1.9" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/node-azureauth/scripts/azureauth.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../dist/cli.cjs"); -------------------------------------------------------------------------------- /packages/node-azureauth/scripts/install.cjs: -------------------------------------------------------------------------------- 1 | const path = require("node:path"); 2 | const fs = require("node:fs"); 3 | 4 | const installScript = path.join(__dirname, "..", "dist", "install.cjs"); 5 | if (fs.existsSync(installScript)) { 6 | require(installScript); 7 | } else { 8 | console.log(`Skipping downloading of azureauth tool. This package is incomplete.`); 9 | } -------------------------------------------------------------------------------- /packages/node-azureauth/src/azure-auth-command.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import process from "node:process"; 3 | 4 | export const azureAuthCommand = () => { 5 | let azureauth = path.join(__dirname, "..", "bin", "azureauth", "azureauth"); 6 | 7 | if (process.platform === "win32") { 8 | azureauth = azureauth + ".exe"; 9 | } 10 | 11 | return azureauth; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/node-azureauth/src/index.ts: -------------------------------------------------------------------------------- 1 | export { run as adoPat } from "./pat.js"; 2 | -------------------------------------------------------------------------------- /packages/node-azureauth/src/install.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | 4 | import { DownloaderHelper } from "node-downloader-helper"; 5 | import decompress from "decompress"; 6 | 7 | const AZURE_AUTH_VERSION = "0.8.4"; 8 | 9 | async function download(url: string, saveDirectory: string): Promise { 10 | const downloader = new DownloaderHelper(url, saveDirectory); 11 | return new Promise((resolve, reject) => { 12 | downloader.on("end", () => resolve()); 13 | downloader.on("error", (err) => reject(err)); 14 | downloader.on("progress.throttled", (downloadEvents) => { 15 | const percentageComplete = 16 | downloadEvents.progress < 100 17 | ? downloadEvents.progress.toPrecision(2) 18 | : 100; 19 | console.info(`Downloaded: ${percentageComplete}%`); 20 | }); 21 | downloader.start(); 22 | }); 23 | } 24 | 25 | const platform = process.platform; 26 | const arch = process.arch; 27 | 28 | const AZUREAUTH_INFO = { 29 | name: "azureauth", 30 | // https://github.com/AzureAD/microsoft-authentication-cli/releases/download/${AZUREAUTH_INFO.version}/azureauth-${AZUREAUTH_INFO.version}-osx-arm64.tar.gz 31 | // https://github.com/AzureAD/microsoft-authentication-cli/releases/download/${AZUREAUTH_INFO.version}/azureauth-${AZUREAUTH_INFO.version}-osx-x64.tar.gz 32 | // https://github.com/AzureAD/microsoft-authentication-cli/releases/download/${AZUREAUTH_INFO.version}/azureauth-${AZUREAUTH_INFO.version}-win10-x64.zip 33 | url: "https://github.com/AzureAD/microsoft-authentication-cli/releases/download/", 34 | version: AZURE_AUTH_VERSION, 35 | }; 36 | 37 | const AZUREAUTH_NAME_MAP: any = { 38 | def: "azureauth", 39 | win32: "azureauth.exe", 40 | linux: "azureauth.exe", 41 | }; 42 | 43 | export const AZUREAUTH_NAME = 44 | platform in AZUREAUTH_NAME_MAP 45 | ? AZUREAUTH_NAME_MAP[platform] 46 | : AZUREAUTH_NAME_MAP.def; 47 | 48 | export const install = async () => { 49 | const OUTPUT_DIR = path.join(__dirname, "..", "bin"); 50 | const fileExist = (pathToCheck: string) => { 51 | try { 52 | return fs.existsSync(pathToCheck); 53 | } catch (err) { 54 | return false; 55 | } 56 | }; 57 | 58 | if (!fs.existsSync(OUTPUT_DIR)) { 59 | fs.mkdirSync(OUTPUT_DIR, { recursive: true }); 60 | console.info(`${OUTPUT_DIR} directory was created`); 61 | } 62 | 63 | if (fileExist(path.join(OUTPUT_DIR, "azureauth", AZUREAUTH_NAME))) { 64 | console.log("azureauth is already installed"); 65 | return; 66 | } 67 | // if platform is missing, download source instead of executable 68 | const DOWNLOAD_MAP: any = { 69 | win32: { 70 | x64: `azureauth-${AZUREAUTH_INFO.version}-win10-x64.zip`, 71 | }, 72 | darwin: { 73 | x64: `azureauth-${AZUREAUTH_INFO.version}-osx-x64.tar.gz`, 74 | arm64: `azureauth-${AZUREAUTH_INFO.version}-osx-arm64.tar.gz`, 75 | }, 76 | // TODO: support linux when the binaries are available 77 | // linux: { 78 | // def: "azureauth.exe", 79 | // x64: "azureauth-${AZUREAUTH_INFO.version}-win10-x64.zip", 80 | // }, 81 | }; 82 | if (platform in DOWNLOAD_MAP) { 83 | // download the executable 84 | let filename = ""; 85 | if (arch in DOWNLOAD_MAP[platform]) { 86 | filename = DOWNLOAD_MAP[platform][arch]; 87 | } else { 88 | throw new Error("Arch is not supported in azureauth"); 89 | } 90 | const url = `${AZUREAUTH_INFO.url}${AZUREAUTH_INFO.version}/${filename}`; 91 | const distPath = path.join(OUTPUT_DIR, "azureauth"); 92 | const archivePath = path.join(OUTPUT_DIR, filename); 93 | 94 | console.log(`Downloading azureauth from ${url}`); 95 | try { 96 | await download(url, OUTPUT_DIR); 97 | } catch (err: any) { 98 | throw new Error(`Download failed: ${err.message}`); 99 | } 100 | console.log(`Downloaded in ${OUTPUT_DIR}`); 101 | 102 | // Make a dir to uncompress the zip or tar into 103 | fs.mkdirSync(distPath, { 104 | recursive: true, 105 | }); 106 | 107 | const binaryPath = path.join(distPath, AZUREAUTH_NAME); 108 | 109 | await decompress(archivePath, distPath); 110 | 111 | if (fileExist(binaryPath)) { 112 | fs.chmodSync(binaryPath, fs.constants.S_IXUSR || 0o100); 113 | // Huan(202111): we need the read permission so that the build system can pack the node_modules/ folder, 114 | // i.e. build with Heroku CI/CD, docker build, etc. 115 | fs.chmodSync(binaryPath, 0o755); 116 | } 117 | 118 | console.log(`Unzipped in ${archivePath}`); 119 | fs.unlinkSync(archivePath); 120 | } 121 | }; 122 | 123 | async function retryLoop() { 124 | const MAX_RETRIES = 3; 125 | for (let i = 0; i < MAX_RETRIES; i++) { 126 | try { 127 | await install(); 128 | break; // success, so exit the loop 129 | } catch (err: any) { 130 | console.log(`Install failed: ${err.message}`); 131 | } 132 | if (i === MAX_RETRIES - 1) { 133 | throw new Error(`Install failed after ${MAX_RETRIES} attempts`); 134 | } 135 | } 136 | } 137 | 138 | retryLoop().catch((err) => { 139 | console.error(err); 140 | process.exit(1); 141 | }); 142 | -------------------------------------------------------------------------------- /packages/node-azureauth/src/pat.test.ts: -------------------------------------------------------------------------------- 1 | import { run, args } from "./pat.js"; 2 | import { expect, test, vi } from "vitest"; 3 | 4 | test("adoPat", async () => { 5 | vi.mock("execa", () => { 6 | return { 7 | execa: () => { 8 | return Promise.resolve({ 9 | stdout: "test123", 10 | }); 11 | }, 12 | }; 13 | }); 14 | 15 | const pat = await run({ 16 | displayName: "test", 17 | organization: "test", 18 | promptHint: "test", 19 | scope: ["test"], 20 | }); 21 | 22 | expect(pat).toBe("test123"); 23 | }); 24 | 25 | test("adoPat args", () => { 26 | const commandArgs = args({ 27 | displayName: "test", 28 | organization: "test", 29 | promptHint: "test", 30 | scope: ["test"], 31 | }); 32 | 33 | expect(commandArgs).toEqual([ 34 | "ado", 35 | "pat", 36 | `--prompt-hint "test"`, 37 | `--organization "test"`, 38 | `--display-name "test"`, 39 | `--scope test`, 40 | `--output json`, 41 | ]); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/node-azureauth/src/pat.ts: -------------------------------------------------------------------------------- 1 | import { execa } from "execa"; 2 | import { azureAuthCommand } from "./azure-auth-command.js"; 3 | 4 | export type AdoPatOptions = { 5 | promptHint: string; 6 | organization: string; 7 | displayName: string; 8 | scope: string[]; 9 | output?: string; 10 | mode?: string; 11 | domain?: string; 12 | timeout?: string; 13 | }; 14 | 15 | export type AdoPatResponse = { 16 | displayName: string; 17 | validTo: string; 18 | scope: string[]; 19 | targetAccounts: string[]; 20 | validFrom: string; 21 | authorizationId: string; 22 | token: string; 23 | }; 24 | 25 | export const args = (options: AdoPatOptions) => { 26 | const args = [ 27 | "ado", 28 | "pat", 29 | `--prompt-hint "${options.promptHint}"`, 30 | `--organization "${options.organization}"`, 31 | `--display-name "${options.displayName}"`, 32 | ...options.scope.map((scope) => `--scope ${scope}`), 33 | ]; 34 | 35 | if (options.output) { 36 | args.push(`--output ${options.output}`); 37 | } else { 38 | args.push(`--output json`); 39 | } 40 | 41 | if (options.mode) { 42 | args.push(`--mode "${options.mode}"`); 43 | } 44 | 45 | if (options.domain) { 46 | args.push(`--domain "${options.domain}"`); 47 | } 48 | 49 | if (options.timeout) { 50 | args.push(`--timeout ${options.timeout}`); 51 | } 52 | 53 | return args; 54 | }; 55 | 56 | /** 57 | * Wrapper for `azureauth ado pat`. Please run `azureauth ado pat --help` for full command options and description 58 | * @param options Options for PAT generation 59 | * @returns ADO PAT details 60 | */ 61 | export const run = async ( 62 | options: AdoPatOptions, 63 | ): Promise => { 64 | const commandArgs = args(options); 65 | 66 | try { 67 | const result = await execa(azureAuthCommand(), commandArgs); 68 | 69 | if (options.output === "json") { 70 | return JSON.parse(result.stdout) as AdoPatResponse; 71 | } 72 | 73 | return result.stdout; 74 | } catch (error: any) { 75 | throw new Error("Failed to get Ado Pat: " + error.message); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /packages/node-azureauth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "composite": true, 5 | "target": "ES2019", 6 | "rootDir": "src", 7 | "outDir": "lib", 8 | "incremental": true, 9 | "tsBuildInfoFile": "lib/.tsbuildinfo", 10 | "lib": ["ES2019", "dom"], 11 | "jsx": "react", 12 | "declaration": true, 13 | "declarationMap": true, 14 | "sourceMap": true, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "bundler", 17 | "strict": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noImplicitReturns": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "importHelpers": true, 24 | "esModuleInterop": true, 25 | "downlevelIteration": true 26 | }, 27 | "include": ["./src"], 28 | "references": [] 29 | } 30 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in subdirs of packages/ and components/ 3 | - 'packages/**' -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /publish.npmrc: -------------------------------------------------------------------------------- 1 | # DO NOT SPECIFY AUTHENTICATION CREDENTIALS IN THIS FILE. 2 | # It should only be used to configure NPM registry sources. 3 | 4 | # Public NPM registry is already an upstream of Office feed. 5 | # Additional registries should be added as upstreams instead of being added here. 6 | 7 | registry=https://pkgs.dev.azure.com/office/_packaging/Office/npm/registry/ 8 | always-auth=true 9 | --------------------------------------------------------------------------------