├── .release-please-manifest.json ├── .npmrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug.yml ├── dependabot.yml ├── matchers │ └── tap.json ├── settings.yml ├── workflows │ ├── codeql-analysis.yml │ ├── audit.yml │ ├── pull-request.yml │ ├── release-integration.yml │ ├── ci.yml │ ├── ci-release.yml │ ├── post-dependabot.yml │ └── release.yml └── actions │ ├── create-check │ └── action.yml │ └── install-latest-npm │ └── action.yml ├── test ├── fixtures │ └── tnock.js ├── index.js └── login.js ├── CODE_OF_CONDUCT.md ├── .commitlintrc.js ├── .eslintrc.js ├── .gitignore ├── LICENSE.md ├── release-please-config.json ├── SECURITY.md ├── package.json ├── tap-snapshots └── test │ └── login.js.test.cjs ├── CONTRIBUTING.md ├── lib └── index.js ├── CHANGELOG.md └── README.md /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "12.0.1" 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ; This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | package-lock=false 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | * @npm/cli-team 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | blank_issues_enabled: true 4 | -------------------------------------------------------------------------------- /test/fixtures/tnock.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const nock = require('nock') 4 | 5 | module.exports = tnock 6 | function tnock (t, host) { 7 | const server = nock(host) 8 | t.teardown(function () { 9 | server.done() 10 | }) 11 | return server 12 | } 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | All interactions in this repo are covered by the [npm Code of 4 | Conduct](https://docs.npmjs.com/policies/conduct) 5 | 6 | The npm cli team may, at its own discretion, moderate, remove, or edit 7 | any interactions such as pull requests, issues, and comments. 8 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | module.exports = { 4 | extends: ['@commitlint/config-conventional'], 5 | rules: { 6 | 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], 7 | 'header-max-length': [2, 'always', 80], 8 | 'subject-case': [0], 9 | 'body-max-line-length': [0], 10 | 'footer-max-line-length': [0], 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | 'use strict' 4 | 5 | const { readdirSync: readdir } = require('fs') 6 | 7 | const localConfigs = readdir(__dirname) 8 | .filter((file) => file.startsWith('.eslintrc.local.')) 9 | .map((file) => `./${file}`) 10 | 11 | module.exports = { 12 | root: true, 13 | ignorePatterns: [ 14 | 'tap-testdir*/', 15 | ], 16 | extends: [ 17 | '@npmcli', 18 | ...localConfigs, 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: npm 7 | directory: / 8 | schedule: 9 | interval: daily 10 | target-branch: "main" 11 | allow: 12 | - dependency-type: direct 13 | versioning-strategy: increase-if-necessary 14 | commit-message: 15 | prefix: deps 16 | prefix-development: chore 17 | labels: 18 | - "Dependencies" 19 | open-pull-requests-limit: 10 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | # ignore everything in the root 4 | /* 5 | 6 | !**/.gitignore 7 | !/.commitlintrc.js 8 | !/.eslint.config.js 9 | !/.eslintrc.js 10 | !/.eslintrc.local.* 11 | !/.git-blame-ignore-revs 12 | !/.github/ 13 | !/.gitignore 14 | !/.npmrc 15 | !/.prettierignore 16 | !/.prettierrc.js 17 | !/.release-please-manifest.json 18 | !/bin/ 19 | !/CHANGELOG* 20 | !/CODE_OF_CONDUCT.md 21 | !/CONTRIBUTING.md 22 | !/docs/ 23 | !/lib/ 24 | !/LICENSE* 25 | !/map.js 26 | !/package.json 27 | !/README* 28 | !/release-please-config.json 29 | !/scripts/ 30 | !/SECURITY.md 31 | !/tap-snapshots/ 32 | !/test/ 33 | !/tsconfig.json 34 | tap-testdir*/ 35 | -------------------------------------------------------------------------------- /.github/matchers/tap.json: -------------------------------------------------------------------------------- 1 | { 2 | "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", 3 | "problemMatcher": [ 4 | { 5 | "owner": "tap", 6 | "pattern": [ 7 | { 8 | "regexp": "^\\s*not ok \\d+ - (.*)", 9 | "message": 1 10 | }, 11 | { 12 | "regexp": "^\\s*---" 13 | }, 14 | { 15 | "regexp": "^\\s*at:" 16 | }, 17 | { 18 | "regexp": "^\\s*line:\\s*(\\d+)", 19 | "line": 1 20 | }, 21 | { 22 | "regexp": "^\\s*column:\\s*(\\d+)", 23 | "column": 1 24 | }, 25 | { 26 | "regexp": "^\\s*file:\\s*(.*)", 27 | "file": 1 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ISC License 4 | 5 | Copyright npm, Inc. 6 | 7 | Permission to use, copy, modify, and/or distribute this 8 | software for any purpose with or without fee is hereby 9 | granted, provided that the above copyright notice and this 10 | permission notice appear in all copies. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS" AND NPM DISCLAIMS ALL 13 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO 15 | EVENT SHALL NPM BE LIABLE FOR ANY SPECIAL, DIRECT, 16 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 17 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 18 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 19 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE 20 | USE OR PERFORMANCE OF THIS SOFTWARE. 21 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "group-pull-request-title-pattern": "chore: release ${version}", 3 | "pull-request-title-pattern": "chore: release${component} ${version}", 4 | "changelog-sections": [ 5 | { 6 | "type": "feat", 7 | "section": "Features", 8 | "hidden": false 9 | }, 10 | { 11 | "type": "fix", 12 | "section": "Bug Fixes", 13 | "hidden": false 14 | }, 15 | { 16 | "type": "docs", 17 | "section": "Documentation", 18 | "hidden": false 19 | }, 20 | { 21 | "type": "deps", 22 | "section": "Dependencies", 23 | "hidden": false 24 | }, 25 | { 26 | "type": "chore", 27 | "section": "Chores", 28 | "hidden": true 29 | } 30 | ], 31 | "packages": { 32 | ".": { 33 | "package-name": "" 34 | } 35 | }, 36 | "prerelease-type": "pre.0" 37 | } 38 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | repository: 4 | allow_merge_commit: false 5 | allow_rebase_merge: true 6 | allow_squash_merge: true 7 | squash_merge_commit_title: PR_TITLE 8 | squash_merge_commit_message: PR_BODY 9 | delete_branch_on_merge: true 10 | enable_automated_security_fixes: true 11 | enable_vulnerability_alerts: true 12 | 13 | branches: 14 | - name: main 15 | protection: 16 | required_status_checks: null 17 | enforce_admins: true 18 | block_creations: true 19 | required_pull_request_reviews: 20 | required_approving_review_count: 1 21 | require_code_owner_reviews: true 22 | require_last_push_approval: true 23 | dismiss_stale_reviews: true 24 | restrictions: 25 | apps: [] 26 | users: [] 27 | teams: [ "cli-team" ] 28 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CodeQL 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | schedule: 13 | # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 14 | - cron: "0 10 * * 1" 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | analyze: 21 | name: Analyze 22 | runs-on: ubuntu-latest 23 | permissions: 24 | actions: read 25 | contents: read 26 | security-events: write 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Setup Git User 31 | run: | 32 | git config --global user.email "npm-cli+bot@github.com" 33 | git config --global user.name "npm CLI robot" 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: javascript 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 4 | 5 | If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 6 | 7 | If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). 8 | 9 | If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Thanks for helping make GitHub safe for everyone. 14 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Audit 4 | 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 9 | - cron: "0 8 * * 1" 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | audit: 16 | name: Audit Dependencies 17 | if: github.repository_owner == 'npm' 18 | runs-on: ubuntu-latest 19 | defaults: 20 | run: 21 | shell: bash 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Setup Git User 26 | run: | 27 | git config --global user.email "npm-cli+bot@github.com" 28 | git config --global user.name "npm CLI robot" 29 | - name: Setup Node 30 | uses: actions/setup-node@v4 31 | id: node 32 | with: 33 | node-version: 22.x 34 | check-latest: contains('22.x', '.x') 35 | - name: Install Latest npm 36 | uses: ./.github/actions/install-latest-npm 37 | with: 38 | node: ${{ steps.node.outputs.node-version }} 39 | - name: Install Dependencies 40 | run: npm i --ignore-scripts --no-audit --no-fund --package-lock 41 | - name: Run Production Audit 42 | run: npm audit --omit=dev 43 | - name: Run Full Audit 44 | run: npm audit --audit-level=none 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-profile", 3 | "version": "12.0.1", 4 | "description": "Library for updating an npmjs.com profile", 5 | "keywords": [], 6 | "author": "GitHub Inc.", 7 | "license": "ISC", 8 | "dependencies": { 9 | "npm-registry-fetch": "^19.0.0", 10 | "proc-log": "^6.0.0" 11 | }, 12 | "main": "./lib/index.js", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/npm/npm-profile.git" 16 | }, 17 | "files": [ 18 | "bin/", 19 | "lib/" 20 | ], 21 | "devDependencies": { 22 | "@npmcli/eslint-config": "^6.0.0", 23 | "@npmcli/template-oss": "4.28.1", 24 | "nock": "^13.5.6", 25 | "tap": "^16.0.1" 26 | }, 27 | "scripts": { 28 | "posttest": "npm run lint", 29 | "test": "tap", 30 | "snap": "tap", 31 | "lint": "npm run eslint", 32 | "postlint": "template-oss-check", 33 | "lintfix": "npm run eslint -- --fix", 34 | "template-oss-apply": "template-oss-apply --force", 35 | "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"" 36 | }, 37 | "tap": { 38 | "check-coverage": true, 39 | "nyc-arg": [ 40 | "--exclude", 41 | "tap-snapshots/**" 42 | ] 43 | }, 44 | "engines": { 45 | "node": "^20.17.0 || >=22.9.0" 46 | }, 47 | "templateOSS": { 48 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", 49 | "version": "4.28.1", 50 | "publish": true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Bug 4 | description: File a bug/issue 5 | title: "[BUG] " 6 | labels: [ Bug, Needs Triage ] 7 | 8 | body: 9 | - type: checkboxes 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please [search here](./issues) to see if an issue already exists for your problem. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Current Behavior 19 | description: A clear & concise description of what you're experiencing. 20 | validations: 21 | required: false 22 | - type: textarea 23 | attributes: 24 | label: Expected Behavior 25 | description: A clear & concise description of what you expected to happen. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Steps To Reproduce 31 | description: Steps to reproduce the behavior. 32 | value: | 33 | 1. In this environment... 34 | 2. With this config... 35 | 3. Run '...' 36 | 4. See error... 37 | validations: 38 | required: false 39 | - type: textarea 40 | attributes: 41 | label: Environment 42 | description: | 43 | examples: 44 | - **npm**: 7.6.3 45 | - **Node**: 13.14.0 46 | - **OS**: Ubuntu 20.04 47 | - **platform**: Macbook Pro 48 | value: | 49 | - npm: 50 | - Node: 51 | - OS: 52 | - platform: 53 | validations: 54 | required: false 55 | -------------------------------------------------------------------------------- /.github/actions/create-check/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Create Check' 4 | inputs: 5 | name: 6 | required: true 7 | token: 8 | required: true 9 | sha: 10 | required: true 11 | check-name: 12 | default: '' 13 | outputs: 14 | check-id: 15 | value: ${{ steps.create-check.outputs.check_id }} 16 | runs: 17 | using: "composite" 18 | steps: 19 | - name: Get Workflow Job 20 | uses: actions/github-script@v7 21 | id: workflow 22 | env: 23 | JOB_NAME: "${{ inputs.name }}" 24 | SHA: "${{ inputs.sha }}" 25 | with: 26 | result-encoding: string 27 | script: | 28 | const { repo: { owner, repo}, runId, serverUrl } = context 29 | const { JOB_NAME, SHA } = process.env 30 | 31 | const job = await github.rest.actions.listJobsForWorkflowRun({ 32 | owner, 33 | repo, 34 | run_id: runId, 35 | per_page: 100 36 | }).then(r => r.data.jobs.find(j => j.name.endsWith(JOB_NAME))) 37 | 38 | return [ 39 | `This check is assosciated with ${serverUrl}/${owner}/${repo}/commit/${SHA}.`, 40 | 'Run logs:', 41 | job?.html_url || `could not be found for a job ending with: "${JOB_NAME}"`, 42 | ].join(' ') 43 | - name: Create Check 44 | uses: LouisBrunner/checks-action@v1.6.0 45 | id: create-check 46 | with: 47 | token: ${{ inputs.token }} 48 | sha: ${{ inputs.sha }} 49 | status: in_progress 50 | name: ${{ inputs.check-name || inputs.name }} 51 | output: | 52 | {"summary":"${{ steps.workflow.outputs.result }}"} 53 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Pull Request 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - edited 11 | - synchronize 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | commitlint: 18 | name: Lint Commits 19 | if: github.repository_owner == 'npm' 20 | runs-on: ubuntu-latest 21 | defaults: 22 | run: 23 | shell: bash 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | - name: Setup Git User 30 | run: | 31 | git config --global user.email "npm-cli+bot@github.com" 32 | git config --global user.name "npm CLI robot" 33 | - name: Setup Node 34 | uses: actions/setup-node@v4 35 | id: node 36 | with: 37 | node-version: 22.x 38 | check-latest: contains('22.x', '.x') 39 | - name: Install Latest npm 40 | uses: ./.github/actions/install-latest-npm 41 | with: 42 | node: ${{ steps.node.outputs.node-version }} 43 | - name: Install Dependencies 44 | run: npm i --ignore-scripts --no-audit --no-fund 45 | - name: Run Commitlint on Commits 46 | id: commit 47 | continue-on-error: true 48 | run: npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }} 49 | - name: Run Commitlint on PR Title 50 | if: steps.commit.outcome == 'failure' 51 | env: 52 | PR_TITLE: ${{ github.event.pull_request.title }} 53 | run: echo "$PR_TITLE" | npx --offline commitlint -V 54 | -------------------------------------------------------------------------------- /.github/actions/install-latest-npm/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Install Latest npm' 4 | description: 'Install the latest version of npm compatible with the Node version' 5 | inputs: 6 | node: 7 | description: 'Current Node version' 8 | required: true 9 | runs: 10 | using: "composite" 11 | steps: 12 | # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows 13 | - name: Update Windows npm 14 | if: | 15 | runner.os == 'Windows' && ( 16 | startsWith(inputs.node, 'v10.') || 17 | startsWith(inputs.node, 'v12.') || 18 | startsWith(inputs.node, 'v14.') 19 | ) 20 | shell: cmd 21 | run: | 22 | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz 23 | tar xf npm-7.5.4.tgz 24 | cd package 25 | node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz 26 | cd .. 27 | rmdir /s /q package 28 | - name: Install Latest npm 29 | shell: bash 30 | env: 31 | NODE_VERSION: ${{ inputs.node }} 32 | working-directory: ${{ runner.temp }} 33 | run: | 34 | MATCH="" 35 | SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") 36 | 37 | echo "node@$NODE_VERSION" 38 | 39 | for SPEC in ${SPECS[@]}; do 40 | ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') 41 | echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" 42 | 43 | if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then 44 | MATCH=$SPEC 45 | echo "Found compatible version: npm@$MATCH" 46 | break 47 | fi 48 | done 49 | 50 | if [ -z $MATCH ]; then 51 | echo "Could not find a compatible version of npm for node@$NODE_VERSION" 52 | exit 1 53 | fi 54 | 55 | npm i --prefer-online --no-fund --no-audit -g npm@$MATCH 56 | - name: npm Version 57 | shell: bash 58 | run: npm -v 59 | -------------------------------------------------------------------------------- /.github/workflows/release-integration.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release Integration 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | releases: 9 | required: true 10 | type: string 11 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 12 | workflow_call: 13 | inputs: 14 | releases: 15 | required: true 16 | type: string 17 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 18 | secrets: 19 | PUBLISH_TOKEN: 20 | required: true 21 | 22 | permissions: 23 | contents: read 24 | id-token: write 25 | 26 | jobs: 27 | publish: 28 | name: Publish 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | shell: bash 33 | permissions: 34 | id-token: write 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | with: 39 | ref: ${{ fromJSON(inputs.releases)[0].tagName }} 40 | - name: Setup Git User 41 | run: | 42 | git config --global user.email "npm-cli+bot@github.com" 43 | git config --global user.name "npm CLI robot" 44 | - name: Setup Node 45 | uses: actions/setup-node@v4 46 | id: node 47 | with: 48 | node-version: 22.x 49 | check-latest: contains('22.x', '.x') 50 | - name: Install Latest npm 51 | uses: ./.github/actions/install-latest-npm 52 | with: 53 | node: ${{ steps.node.outputs.node-version }} 54 | - name: Install Dependencies 55 | run: npm i --ignore-scripts --no-audit --no-fund 56 | - name: Set npm authToken 57 | run: npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN} 58 | - name: Publish 59 | env: 60 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 61 | RELEASES: ${{ inputs.releases }} 62 | run: | 63 | EXIT_CODE=0 64 | 65 | for release in $(echo $RELEASES | jq -r '.[] | @base64'); do 66 | PUBLISH_TAG=$(echo "$release" | base64 --decode | jq -r .publishTag) 67 | npm publish --provenance --tag="$PUBLISH_TAG" 68 | STATUS=$? 69 | if [[ "$STATUS" -eq 1 ]]; then 70 | EXIT_CODE=$STATUS 71 | fi 72 | done 73 | 74 | exit $EXIT_CODE 75 | -------------------------------------------------------------------------------- /tap-snapshots/test/login.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/login.js TAP cleanup > got expected requests 1`] = ` 9 | [ 10 | "POST /weblogin/-/v1/login", 11 | "GET /weblogin/-/v1/login/blerg", 12 | "GET /weblogin/-/v1/login/blerg", 13 | "POST /weblogin/-/v1/login", 14 | "GET /weblogin/-/v1/login/blerg", 15 | "POST /weblogin/-/v1/login", 16 | "GET /weblogin/-/v1/login/blerg", 17 | "POST /weblogin/-/v1/login", 18 | "GET /weblogin/-/v1/login/blerg", 19 | "POST /weblogin/-/v1/login", 20 | "GET /weblogin/-/v1/login/blerg", 21 | "PUT /couchdb/-/user/org.couchdb.user:user", 22 | "PUT /couchdb/-/user/org.couchdb.user:exists", 23 | "GET /couchdb/-/user/org.couchdb.user:exists?write=true", 24 | "PUT /couchdb/-/user/org.couchdb.user:exists/-rev/goodbloodmoon", 25 | "PUT /couchdb/-/user/org.couchdb.user:nemo", 26 | "PUT /couchdb/-/user/org.couchdb.user:user", 27 | "POST /couchdb/-/v1/login", 28 | "PUT /couchdb/-/user/org.couchdb.user:user", 29 | "POST /couchdb/-/v1/login", 30 | "PUT /couchdb/-/user/org.couchdb.user:user", 31 | "POST /501/-/v1/login", 32 | "POST /501/-/v1/login", 33 | "POST /501/-/v1/login", 34 | "POST /501/-/v1/login", 35 | "PUT /501/-/user/org.couchdb.user:user", 36 | "PUT /501/-/user/org.couchdb.user:user", 37 | "POST /invalid-login/-/v1/login", 38 | "POST /invalid-login-url/-/v1/login", 39 | "POST /invalid-done/-/v1/login", 40 | "GET /invalid-done/-/v1/login/blerg", 41 | "POST /notoken/-/v1/login", 42 | "GET /notoken/-/v1/login/blerg", 43 | "POST /notoken/-/v1/login", 44 | "GET /notoken/-/v1/login/blerg", 45 | "POST /retry-after/-/v1/login", 46 | "GET /retry-after/-/v1/login/blerg", 47 | "GET /retry-after/-/v1/login/blerg", 48 | "POST /retry-after/-/v1/login", 49 | "GET /retry-after/-/v1/login/blerg", 50 | "GET /retry-after/-/v1/login/blerg", 51 | "POST /retry-again/-/v1/login", 52 | "GET /retry-again/-/v1/login/blerg", 53 | "GET /retry-again/-/v1/login/blerg", 54 | "POST /retry-again/-/v1/login", 55 | "GET /retry-again/-/v1/login/blerg", 56 | "GET /retry-again/-/v1/login/blerg", 57 | "POST /retry-long-time/-/v1/login", 58 | "GET /retry-long-time/-/v1/login/loooooong", 59 | "POST /retry-long-time/-/v1/login", 60 | "GET /retry-long-time/-/v1/login/loooooong", 61 | "POST /retry-long-time/-/v1/login", 62 | "GET /retry-long-time/-/v1/login/loooooong", 63 | "POST /retry-long-time/-/v1/login", 64 | "GET /retry-long-time/-/v1/login/loooooong" 65 | ] 66 | ` 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | # Contributing 4 | 5 | ## Code of Conduct 6 | 7 | All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). 8 | 9 | ## Reporting Bugs 10 | 11 | Before submitting a new bug report please search for an existing or similar report. 12 | 13 | Use one of our existing issue templates if you believe you've come across a unique problem. 14 | 15 | Duplicate issues, or issues that don't use one of our templates may get closed without a response. 16 | 17 | ## Pull Request Conventions 18 | 19 | ### Commits 20 | 21 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | 23 | When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 24 | 25 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 26 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 27 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 28 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). 29 | 30 | ### Test Coverage 31 | 32 | Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. 33 | 34 | Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. 35 | 36 | ### Linting 37 | 38 | Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. 39 | 40 | Please make sure linting passes before submitting a PR. 41 | 42 | ## What _not_ to contribute? 43 | 44 | ### Dependencies 45 | 46 | It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 47 | 48 | ### Tools/Automation 49 | 50 | Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. 51 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | schedule: 12 | # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 13 | - cron: "0 9 * * 1" 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | lint: 20 | name: Lint 21 | if: github.repository_owner == 'npm' 22 | runs-on: ubuntu-latest 23 | defaults: 24 | run: 25 | shell: bash 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Setup Git User 30 | run: | 31 | git config --global user.email "npm-cli+bot@github.com" 32 | git config --global user.name "npm CLI robot" 33 | - name: Setup Node 34 | uses: actions/setup-node@v4 35 | id: node 36 | with: 37 | node-version: 22.x 38 | check-latest: contains('22.x', '.x') 39 | - name: Install Latest npm 40 | uses: ./.github/actions/install-latest-npm 41 | with: 42 | node: ${{ steps.node.outputs.node-version }} 43 | - name: Install Dependencies 44 | run: npm i --ignore-scripts --no-audit --no-fund 45 | - name: Lint 46 | run: npm run lint --ignore-scripts 47 | - name: Post Lint 48 | run: npm run postlint --ignore-scripts 49 | 50 | test: 51 | name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 52 | if: github.repository_owner == 'npm' 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | platform: 57 | - name: Linux 58 | os: ubuntu-latest 59 | shell: bash 60 | - name: macOS 61 | os: macos-latest 62 | shell: bash 63 | - name: macOS 64 | os: macos-13 65 | shell: bash 66 | - name: Windows 67 | os: windows-latest 68 | shell: cmd 69 | node-version: 70 | - 20.17.0 71 | - 20.x 72 | - 22.9.0 73 | - 22.x 74 | exclude: 75 | - platform: { name: macOS, os: macos-13, shell: bash } 76 | node-version: 20.17.0 77 | - platform: { name: macOS, os: macos-13, shell: bash } 78 | node-version: 20.x 79 | - platform: { name: macOS, os: macos-13, shell: bash } 80 | node-version: 22.9.0 81 | - platform: { name: macOS, os: macos-13, shell: bash } 82 | node-version: 22.x 83 | runs-on: ${{ matrix.platform.os }} 84 | defaults: 85 | run: 86 | shell: ${{ matrix.platform.shell }} 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v4 90 | - name: Setup Git User 91 | run: | 92 | git config --global user.email "npm-cli+bot@github.com" 93 | git config --global user.name "npm CLI robot" 94 | - name: Setup Node 95 | uses: actions/setup-node@v4 96 | id: node 97 | with: 98 | node-version: ${{ matrix.node-version }} 99 | check-latest: contains(matrix.node-version, '.x') 100 | - name: Install Latest npm 101 | uses: ./.github/actions/install-latest-npm 102 | with: 103 | node: ${{ steps.node.outputs.node-version }} 104 | - name: Install Dependencies 105 | run: npm i --ignore-scripts --no-audit --no-fund 106 | - name: Add Problem Matcher 107 | run: echo "::add-matcher::.github/matchers/tap.json" 108 | - name: Test 109 | run: npm test --ignore-scripts 110 | -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI - Release 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | ref: 9 | required: true 10 | type: string 11 | default: main 12 | workflow_call: 13 | inputs: 14 | ref: 15 | required: true 16 | type: string 17 | check-sha: 18 | required: true 19 | type: string 20 | 21 | permissions: 22 | contents: read 23 | checks: write 24 | 25 | jobs: 26 | lint-all: 27 | name: Lint All 28 | if: github.repository_owner == 'npm' 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | shell: bash 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | with: 37 | ref: ${{ inputs.ref }} 38 | - name: Setup Git User 39 | run: | 40 | git config --global user.email "npm-cli+bot@github.com" 41 | git config --global user.name "npm CLI robot" 42 | - name: Create Check 43 | id: create-check 44 | if: ${{ inputs.check-sha }} 45 | uses: ./.github/actions/create-check 46 | with: 47 | name: "Lint All" 48 | token: ${{ secrets.GITHUB_TOKEN }} 49 | sha: ${{ inputs.check-sha }} 50 | - name: Setup Node 51 | uses: actions/setup-node@v4 52 | id: node 53 | with: 54 | node-version: 22.x 55 | check-latest: contains('22.x', '.x') 56 | - name: Install Latest npm 57 | uses: ./.github/actions/install-latest-npm 58 | with: 59 | node: ${{ steps.node.outputs.node-version }} 60 | - name: Install Dependencies 61 | run: npm i --ignore-scripts --no-audit --no-fund 62 | - name: Lint 63 | run: npm run lint --ignore-scripts 64 | - name: Post Lint 65 | run: npm run postlint --ignore-scripts 66 | - name: Conclude Check 67 | uses: LouisBrunner/checks-action@v1.6.0 68 | if: steps.create-check.outputs.check-id && always() 69 | with: 70 | token: ${{ secrets.GITHUB_TOKEN }} 71 | conclusion: ${{ job.status }} 72 | check_id: ${{ steps.create-check.outputs.check-id }} 73 | 74 | test-all: 75 | name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 76 | if: github.repository_owner == 'npm' 77 | strategy: 78 | fail-fast: false 79 | matrix: 80 | platform: 81 | - name: Linux 82 | os: ubuntu-latest 83 | shell: bash 84 | - name: macOS 85 | os: macos-latest 86 | shell: bash 87 | - name: macOS 88 | os: macos-13 89 | shell: bash 90 | - name: Windows 91 | os: windows-latest 92 | shell: cmd 93 | node-version: 94 | - 20.17.0 95 | - 20.x 96 | - 22.9.0 97 | - 22.x 98 | exclude: 99 | - platform: { name: macOS, os: macos-13, shell: bash } 100 | node-version: 20.17.0 101 | - platform: { name: macOS, os: macos-13, shell: bash } 102 | node-version: 20.x 103 | - platform: { name: macOS, os: macos-13, shell: bash } 104 | node-version: 22.9.0 105 | - platform: { name: macOS, os: macos-13, shell: bash } 106 | node-version: 22.x 107 | runs-on: ${{ matrix.platform.os }} 108 | defaults: 109 | run: 110 | shell: ${{ matrix.platform.shell }} 111 | steps: 112 | - name: Checkout 113 | uses: actions/checkout@v4 114 | with: 115 | ref: ${{ inputs.ref }} 116 | - name: Setup Git User 117 | run: | 118 | git config --global user.email "npm-cli+bot@github.com" 119 | git config --global user.name "npm CLI robot" 120 | - name: Create Check 121 | id: create-check 122 | if: ${{ inputs.check-sha }} 123 | uses: ./.github/actions/create-check 124 | with: 125 | name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" 126 | token: ${{ secrets.GITHUB_TOKEN }} 127 | sha: ${{ inputs.check-sha }} 128 | - name: Setup Node 129 | uses: actions/setup-node@v4 130 | id: node 131 | with: 132 | node-version: ${{ matrix.node-version }} 133 | check-latest: contains(matrix.node-version, '.x') 134 | - name: Install Latest npm 135 | uses: ./.github/actions/install-latest-npm 136 | with: 137 | node: ${{ steps.node.outputs.node-version }} 138 | - name: Install Dependencies 139 | run: npm i --ignore-scripts --no-audit --no-fund 140 | - name: Add Problem Matcher 141 | run: echo "::add-matcher::.github/matchers/tap.json" 142 | - name: Test 143 | run: npm test --ignore-scripts 144 | - name: Conclude Check 145 | uses: LouisBrunner/checks-action@v1.6.0 146 | if: steps.create-check.outputs.check-id && always() 147 | with: 148 | token: ${{ secrets.GITHUB_TOKEN }} 149 | conclusion: ${{ job.status }} 150 | check_id: ${{ steps.create-check.outputs.check-id }} 151 | -------------------------------------------------------------------------------- /.github/workflows/post-dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Post Dependabot 4 | 5 | on: pull_request 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | template-oss: 12 | name: template-oss 13 | if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | shell: bash 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.pull_request.head.ref }} 23 | - name: Setup Git User 24 | run: | 25 | git config --global user.email "npm-cli+bot@github.com" 26 | git config --global user.name "npm CLI robot" 27 | - name: Setup Node 28 | uses: actions/setup-node@v4 29 | id: node 30 | with: 31 | node-version: 22.x 32 | check-latest: contains('22.x', '.x') 33 | - name: Install Latest npm 34 | uses: ./.github/actions/install-latest-npm 35 | with: 36 | node: ${{ steps.node.outputs.node-version }} 37 | - name: Install Dependencies 38 | run: npm i --ignore-scripts --no-audit --no-fund 39 | - name: Fetch Dependabot Metadata 40 | id: metadata 41 | uses: dependabot/fetch-metadata@v1 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Dependabot can update multiple directories so we output which directory 46 | # it is acting on so we can run the command for the correct root or workspace 47 | - name: Get Dependabot Directory 48 | if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') 49 | id: flags 50 | run: | 51 | dependabot_dir="${{ steps.metadata.outputs.directory }}" 52 | if [[ "$dependabot_dir" == "/" || "$dependabot_dir" == "/main" ]]; then 53 | echo "workspace=-iwr" >> $GITHUB_OUTPUT 54 | else 55 | # strip leading slash from directory so it works as a 56 | # a path to the workspace flag 57 | echo "workspace=--workspace ${dependabot_dir#/}" >> $GITHUB_OUTPUT 58 | fi 59 | 60 | - name: Apply Changes 61 | if: steps.flags.outputs.workspace 62 | id: apply 63 | run: | 64 | npm run template-oss-apply ${{ steps.flags.outputs.workspace }} 65 | if [[ `git status --porcelain` ]]; then 66 | echo "changes=true" >> $GITHUB_OUTPUT 67 | fi 68 | # This only sets the conventional commit prefix. This workflow can't reliably determine 69 | # what the breaking change is though. If a BREAKING CHANGE message is required then 70 | # this PR check will fail and the commit will be amended with stafftools 71 | if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then 72 | prefix='feat!' 73 | else 74 | prefix='chore' 75 | fi 76 | echo "message=$prefix: postinstall for dependabot template-oss PR" >> $GITHUB_OUTPUT 77 | 78 | # This step will fail if template-oss has made any workflow updates. It is impossible 79 | # for a workflow to update other workflows. In the case it does fail, we continue 80 | # and then try to apply only a portion of the changes in the next step 81 | - name: Push All Changes 82 | if: steps.apply.outputs.changes 83 | id: push 84 | continue-on-error: true 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | run: | 88 | git commit -am "${{ steps.apply.outputs.message }}" 89 | git push 90 | 91 | # If the previous step failed, then reset the commit and remove any workflow changes 92 | # and attempt to commit and push again. This is helpful because we will have a commit 93 | # with the correct prefix that we can then --amend with @npmcli/stafftools later. 94 | - name: Push All Changes Except Workflows 95 | if: steps.apply.outputs.changes && steps.push.outcome == 'failure' 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | run: | 99 | git reset HEAD~ 100 | git checkout HEAD -- .github/workflows/ 101 | git clean -fd .github/workflows/ 102 | git commit -am "${{ steps.apply.outputs.message }}" 103 | git push 104 | 105 | # Check if all the necessary template-oss changes were applied. Since we continued 106 | # on errors in one of the previous steps, this check will fail if our follow up 107 | # only applied a portion of the changes and we need to followup manually. 108 | # 109 | # Note that this used to run `lint` and `postlint` but that will fail this action 110 | # if we've also shipped any linting changes separate from template-oss. We do 111 | # linting in another action, so we want to fail this one only if there are 112 | # template-oss changes that could not be applied. 113 | - name: Check Changes 114 | if: steps.apply.outputs.changes 115 | run: | 116 | npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check 117 | 118 | - name: Fail on Breaking Change 119 | if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') 120 | run: | 121 | echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'" 122 | echo "for more information on how to fix this with a BREAKING CHANGE footer." 123 | exit 1 124 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { URL } = require('node:url') 2 | const timers = require('node:timers/promises') 3 | const fetch = require('npm-registry-fetch') 4 | const { HttpErrorBase } = require('npm-registry-fetch/lib/errors') 5 | const { log } = require('proc-log') 6 | 7 | // try loginWeb, catch the "not supported" message and fall back to couch 8 | const login = async (opener, prompter, opts = {}) => { 9 | try { 10 | return await loginWeb(opener, opts) 11 | } catch (er) { 12 | if (er instanceof WebLoginNotSupported) { 13 | log.verbose('web login', 'not supported, trying couch') 14 | const { username, password } = await prompter(opts.creds) 15 | return loginCouch(username, password, opts) 16 | } 17 | throw er 18 | } 19 | } 20 | 21 | const adduser = async (opener, prompter, opts = {}) => { 22 | try { 23 | return await adduserWeb(opener, opts) 24 | } catch (er) { 25 | if (er instanceof WebLoginNotSupported) { 26 | log.verbose('web adduser', 'not supported, trying couch') 27 | const { username, email, password } = await prompter(opts.creds) 28 | return adduserCouch(username, email, password, opts) 29 | } 30 | throw er 31 | } 32 | } 33 | 34 | const adduserWeb = (opener, opts = {}) => { 35 | log.verbose('web adduser', 'before first POST') 36 | return webAuth(opener, opts, { create: true }) 37 | } 38 | 39 | const loginWeb = (opener, opts = {}) => { 40 | log.verbose('web login', 'before first POST') 41 | return webAuth(opener, opts, {}) 42 | } 43 | 44 | const isValidUrl = u => { 45 | try { 46 | return /^https?:$/.test(new URL(u).protocol) 47 | } catch { 48 | return false 49 | } 50 | } 51 | 52 | const webAuth = async (opener, opts, body) => { 53 | try { 54 | const res = await fetch('/-/v1/login', { 55 | ...opts, 56 | method: 'POST', 57 | body, 58 | }) 59 | 60 | const content = await res.json() 61 | log.verbose('web auth', 'got response', content) 62 | 63 | const { doneUrl, loginUrl } = content 64 | if (!isValidUrl(doneUrl) || !isValidUrl(loginUrl)) { 65 | throw new WebLoginInvalidResponse('POST', res, content) 66 | } 67 | 68 | return await webAuthOpener(opener, loginUrl, doneUrl, opts) 69 | } catch (er) { 70 | if ((er.statusCode >= 400 && er.statusCode <= 499) || er.statusCode === 500) { 71 | throw new WebLoginNotSupported('POST', { 72 | status: er.statusCode, 73 | headers: er.headers, 74 | }, er.body) 75 | } 76 | throw er 77 | } 78 | } 79 | 80 | const webAuthOpener = async (opener, loginUrl, doneUrl, opts) => { 81 | const abortController = new AbortController() 82 | const { signal } = abortController 83 | try { 84 | log.verbose('web auth', 'opening url pair') 85 | const [, authResult] = await Promise.all([ 86 | opener(loginUrl, { signal }).catch((err) => { 87 | if (err.name === 'AbortError') { 88 | abortController.abort() 89 | return 90 | } 91 | throw err 92 | }), 93 | webAuthCheckLogin(doneUrl, { ...opts, cache: false }, { signal }).then((r) => { 94 | log.verbose('web auth', 'done-check finished') 95 | abortController.abort() 96 | return r 97 | }), 98 | ]) 99 | return authResult 100 | } catch (er) { 101 | abortController.abort() 102 | throw er 103 | } 104 | } 105 | 106 | const webAuthCheckLogin = async (doneUrl, opts, { signal } = {}) => { 107 | signal?.throwIfAborted() 108 | 109 | const res = await fetch(doneUrl, opts) 110 | const content = await res.json() 111 | 112 | if (res.status === 200) { 113 | if (!content.token) { 114 | throw new WebLoginInvalidResponse('GET', res, content) 115 | } 116 | return content 117 | } 118 | 119 | if (res.status === 202) { 120 | const retry = +res.headers.get('retry-after') * 1000 121 | if (retry > 0) { 122 | await timers.setTimeout(retry, null, { signal }) 123 | } 124 | return webAuthCheckLogin(doneUrl, opts, { signal }) 125 | } 126 | 127 | throw new WebLoginInvalidResponse('GET', res, content) 128 | } 129 | 130 | const couchEndpoint = (username) => `/-/user/org.couchdb.user:${encodeURIComponent(username)}` 131 | 132 | const putCouch = async (path, username, body, opts) => { 133 | const result = await fetch.json(`${couchEndpoint(username)}${path}`, { 134 | ...opts, 135 | method: 'PUT', 136 | body, 137 | }) 138 | result.username = username 139 | return result 140 | } 141 | 142 | const adduserCouch = async (username, email, password, opts = {}) => { 143 | const body = { 144 | _id: `org.couchdb.user:${username}`, 145 | name: username, 146 | password: password, 147 | email: email, 148 | type: 'user', 149 | roles: [], 150 | date: new Date().toISOString(), 151 | } 152 | 153 | log.verbose('adduser', 'before first PUT', { 154 | ...body, 155 | password: 'XXXXX', 156 | }) 157 | 158 | return putCouch('', username, body, opts) 159 | } 160 | 161 | const loginCouch = async (username, password, opts = {}) => { 162 | const body = { 163 | _id: `org.couchdb.user:${username}`, 164 | name: username, 165 | password: password, 166 | type: 'user', 167 | roles: [], 168 | date: new Date().toISOString(), 169 | } 170 | 171 | log.verbose('login', 'before first PUT', { 172 | ...body, 173 | password: 'XXXXX', 174 | }) 175 | 176 | try { 177 | return await putCouch('', username, body, opts) 178 | } catch (err) { 179 | if (err.code === 'E400') { 180 | err.message = `There is no user with the username "${username}".` 181 | throw err 182 | } 183 | 184 | if (err.code !== 'E409') { 185 | throw err 186 | } 187 | } 188 | 189 | const result = await fetch.json(couchEndpoint(username), { 190 | ...opts, 191 | query: { write: true }, 192 | }) 193 | 194 | for (const k of Object.keys(result)) { 195 | if (!body[k] || k === 'roles') { 196 | body[k] = result[k] 197 | } 198 | } 199 | 200 | return putCouch(`/-rev/${body._rev}`, username, body, { 201 | ...opts, 202 | forceAuth: { 203 | username, 204 | password: Buffer.from(password, 'utf8').toString('base64'), 205 | otp: opts.otp, 206 | }, 207 | }) 208 | } 209 | 210 | const get = (opts = {}) => fetch.json('/-/npm/v1/user', opts) 211 | 212 | const set = (profile, opts = {}) => fetch.json('/-/npm/v1/user', { 213 | ...opts, 214 | method: 'POST', 215 | // profile keys can't be empty strings, but they CAN be null 216 | body: Object.fromEntries(Object.entries(profile).map(([k, v]) => [k, v === '' ? null : v])), 217 | }) 218 | 219 | const paginate = async (href, opts, items = []) => { 220 | const result = await fetch.json(href, opts) 221 | items = items.concat(result.objects) 222 | if (result.urls.next) { 223 | return paginate(result.urls.next, opts, items) 224 | } 225 | return items 226 | } 227 | 228 | const listTokens = (opts = {}) => paginate('/-/npm/v1/tokens', opts) 229 | 230 | const removeToken = async (tokenKey, opts = {}) => { 231 | await fetch(`/-/npm/v1/tokens/token/${tokenKey}`, { 232 | ...opts, 233 | method: 'DELETE', 234 | ignoreBody: true, 235 | }) 236 | return null 237 | } 238 | 239 | const createToken = (password, readonly, cidrs, opts = {}) => fetch.json('/-/npm/v1/tokens', { 240 | ...opts, 241 | method: 'POST', 242 | body: { 243 | password: password, 244 | readonly: readonly, 245 | cidr_whitelist: cidrs, 246 | }, 247 | }) 248 | 249 | class WebLoginInvalidResponse extends HttpErrorBase { 250 | constructor (method, res, body) { 251 | super(method, res, body) 252 | this.message = 'Invalid response from web login endpoint' 253 | } 254 | } 255 | 256 | class WebLoginNotSupported extends HttpErrorBase { 257 | constructor (method, res, body) { 258 | super(method, res, body) 259 | this.message = 'Web login not supported' 260 | this.code = 'ENYI' 261 | } 262 | } 263 | 264 | module.exports = { 265 | adduserCouch, 266 | loginCouch, 267 | adduserWeb, 268 | loginWeb, 269 | login, 270 | adduser, 271 | get, 272 | set, 273 | listTokens, 274 | removeToken, 275 | createToken, 276 | webAuthCheckLogin, 277 | webAuthOpener, 278 | } 279 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const tnock = require('./fixtures/tnock.js') 3 | const profile = require('..') 4 | 5 | const registry = 'https://registry.npmjs.org/' 6 | 7 | test('get', t => { 8 | const srv = tnock(t, registry) 9 | const getUrl = '/-/npm/v1/user' 10 | srv.get(getUrl).reply(function () { 11 | const auth = this.req.headers.authorization 12 | t.notOk(auth, 'no authorization info sent') 13 | return [auth ? 200 : 401, '', {}] 14 | }) 15 | return profile.get().then(() => { 16 | return t.fail('GET w/o auth should fail') 17 | }, err => { 18 | t.equal(err.code, 'E401', 'auth errors are passed through') 19 | }).then(() => { 20 | srv.get(getUrl).reply(function () { 21 | const auth = this.req.headers.authorization 22 | t.same(auth, ['Bearer deadbeef'], 'got auth header') 23 | return [auth ? 200 : 401, { auth: 'bearer' }, {}] 24 | }) 25 | return profile.get({ '//registry.npmjs.org/:_authToken': 'deadbeef' }) 26 | }).then(result => { 27 | return t.match(result, { auth: 'bearer' }) 28 | }).then(() => { 29 | srv.get(getUrl).reply(function () { 30 | const auth = this.req.headers.authorization 31 | t.match(auth[0], /^Basic /, 'got basic auth') 32 | const [username, password] = Buffer.from( 33 | auth[0].match(/^Basic (.*)$/)[1], 'base64' 34 | ).toString('utf8').split(':') 35 | t.equal(username, 'abc', 'got username') 36 | t.equal(password, '123', 'got password') 37 | return [auth ? 200 : 401, { auth: 'basic' }, {}] 38 | }) 39 | return profile.get({ 40 | '//registry.npmjs.org/:username': 'abc', 41 | // Passwords are stored in base64 form and npm-related consumers expect 42 | // them in this format. Changing this for npm would be a bigger change. 43 | '//registry.npmjs.org/:_password': Buffer.from('123', 'utf8').toString('base64'), 44 | }) 45 | }).then(result => { 46 | return t.match(result, { auth: 'basic' }) 47 | }).then(() => { 48 | srv.get(getUrl).reply(function () { 49 | const auth = this.req.headers.authorization 50 | const otp = this.req.headers['npm-otp'] 51 | t.same(auth, ['Bearer deadbeef'], 'got auth header') 52 | t.same(otp, ['1234'], 'got otp token') 53 | return [auth ? 200 : 401, { auth: 'bearer', otp: !!otp }, {}] 54 | }) 55 | return profile.get({ 56 | otp: '1234', 57 | '//registry.npmjs.org/:_authToken': 'deadbeef', 58 | }) 59 | }).then(result => { 60 | return t.match(result, { auth: 'bearer', otp: true }) 61 | }) 62 | // with otp, with token, with basic 63 | // prob should make w/o token 401 64 | }) 65 | 66 | test('set', t => { 67 | const prof = { user: 'zkat', github: 'zkat' } 68 | tnock(t, registry).post('/-/npm/v1/user', { 69 | github: 'zkat', 70 | email: null, 71 | }).reply(200, prof) 72 | return profile.set({ 73 | github: 'zkat', 74 | email: '', 75 | }).then(json => { 76 | return t.same(json, prof, 'got the profile data in return') 77 | }) 78 | }) 79 | 80 | test('listTokens', t => { 81 | const tokens = [ 82 | { key: 'sha512-hahaha', token: 'blah' }, 83 | { key: 'sha512-meh', token: 'bleh' }, 84 | ] 85 | tnock(t, registry).get('/-/npm/v1/tokens').reply(200, { 86 | objects: tokens, 87 | total: 2, 88 | urls: {}, 89 | }) 90 | return profile.listTokens().then(tok => t.same(tok, tokens)) 91 | }) 92 | 93 | test('loginCouch happy path', t => { 94 | tnock(t, registry) 95 | .put('/-/user/org.couchdb.user:blerp') 96 | .reply(201, { 97 | ok: true, 98 | }) 99 | return t.resolveMatch(profile.loginCouch('blerp', 'password'), { 100 | ok: true, 101 | username: 'blerp', 102 | }) 103 | }) 104 | 105 | test('login fallback to couch', t => { 106 | tnock(t, registry) 107 | .put('/-/user/org.couchdb.user:blerp') 108 | .reply(201, { 109 | ok: true, 110 | }) 111 | .post('/-/v1/login') 112 | .reply(404, { error: 'not found' }) 113 | const opener = url => t.fail('called opener', { url }) 114 | const prompter = () => Promise.resolve({ 115 | username: 'blerp', 116 | password: 'prelb', 117 | email: 'blerp@blerp.blerp', 118 | }) 119 | return t.resolveMatch(profile.login(opener, prompter), { 120 | ok: true, 121 | username: 'blerp', 122 | }) 123 | }) 124 | 125 | test('login fallback to couch when web login fails cancels opener promise', t => { 126 | const loginUrl = 'https://www.npmjs.com/login?next=/login/cli/123' 127 | tnock(t, registry) 128 | .put('/-/user/org.couchdb.user:blerp') 129 | .reply(201, { 130 | ok: true, 131 | }) 132 | .post('/-/v1/login') 133 | .reply(200, { 134 | loginUrl, 135 | doneUrl: 'https://registry.npmjs.org:443/-/v1/done?sessionId=123', 136 | }) 137 | .get('/-/v1/done?sessionId=123') 138 | .reply(404, { error: 'Not found' }) 139 | 140 | let cancelled = false 141 | const opener = async (url, { signal }) => { 142 | t.equal(url, loginUrl) 143 | signal.addEventListener('abort', () => { 144 | cancelled = true 145 | }) 146 | } 147 | 148 | const prompter = () => Promise.resolve({ 149 | username: 'blerp', 150 | password: 'prelb', 151 | email: 'blerp@blerp.blerp', 152 | }) 153 | return t.resolveMatch(profile.login(opener, prompter), { 154 | ok: true, 155 | username: 'blerp', 156 | }).then(() => { 157 | return t.equal(cancelled, true) 158 | }) 159 | }) 160 | 161 | test('adduserCouch happy path', t => { 162 | tnock(t, registry) 163 | .put('/-/user/org.couchdb.user:blerp') 164 | .reply(201, { 165 | ok: true, 166 | }) 167 | return t.resolveMatch(profile.adduserCouch('blerp', 'password'), { 168 | ok: true, 169 | username: 'blerp', 170 | }) 171 | }) 172 | 173 | test('adduser fallback to couch', t => { 174 | tnock(t, registry) 175 | .put('/-/user/org.couchdb.user:blerp') 176 | .reply(201, { 177 | ok: true, 178 | }) 179 | .post('/-/v1/login') 180 | .reply(404, { error: 'not found' }) 181 | const opener = url => t.fail('called opener', { url }) 182 | const prompter = () => Promise.resolve({ 183 | username: 'blerp', 184 | password: 'prelb', 185 | email: 'blerp@blerp.blerp', 186 | }) 187 | return t.resolveMatch(profile.adduser(opener, prompter), { 188 | ok: true, 189 | username: 'blerp', 190 | }) 191 | }) 192 | 193 | test('adduserCouch happy path', t => { 194 | tnock(t, registry) 195 | .put('/-/user/org.couchdb.user:blerp') 196 | .reply(201, { 197 | ok: true, 198 | }) 199 | return t.resolveMatch(profile.adduserCouch('blerp', 'password'), { 200 | ok: true, 201 | username: 'blerp', 202 | }) 203 | }) 204 | 205 | test('adduserWeb fail, just testing default opts setting', t => { 206 | tnock(t, registry) 207 | .post('/-/v1/login') 208 | .reply(404, { error: 'not found' }) 209 | const opener = url => t.fail('called opener', { url }) 210 | return t.rejects(profile.adduserWeb(opener), { 211 | message: 'Web login not supported', 212 | }) 213 | }) 214 | 215 | test('loginWeb fail, just testing default opts setting', t => { 216 | tnock(t, registry) 217 | .post('/-/v1/login') 218 | .reply(404, { error: 'not found' }) 219 | const opener = url => t.fail('called opener', { url }) 220 | return t.rejects(profile.loginWeb(opener), { 221 | message: 'Web login not supported', 222 | }) 223 | }) 224 | 225 | test('listTokens multipage', t => { 226 | const tokens1 = [ 227 | { key: 'sha512-hahaha', token: 'blah' }, 228 | { key: 'sha512-meh', token: 'bleh' }, 229 | ] 230 | const tokens2 = [ 231 | { key: 'sha512-ugh', token: 'blih' }, 232 | { key: 'sha512-ohno', token: 'bloh' }, 233 | ] 234 | const tokens3 = [ 235 | { key: 'sha512-stahp', token: 'bluh' }, 236 | ] 237 | const srv = tnock(t, registry) 238 | srv.get('/-/npm/v1/tokens').reply(200, { 239 | objects: tokens1, 240 | total: 2, 241 | urls: { 242 | next: '/idk/some/other/one', 243 | }, 244 | }) 245 | srv.get('/idk/some/other/one').reply(200, { 246 | objects: tokens2, 247 | total: 2, 248 | urls: { 249 | next: '/-/npm/last/one-i-swear', 250 | }, 251 | }) 252 | srv.get('/-/npm/last/one-i-swear').reply(200, { 253 | objects: tokens3, 254 | total: 1, 255 | urls: {}, 256 | }) 257 | return profile.listTokens().then(tok => { 258 | return t.same( 259 | tok, 260 | tokens1.concat(tokens2).concat(tokens3), 261 | 'supports multi-URL token requests and concats them' 262 | ) 263 | }) 264 | }) 265 | 266 | test('removeToken', t => { 267 | tnock(t, registry).delete('/-/npm/v1/tokens/token/deadbeef').reply(200) 268 | return profile.removeToken('deadbeef').then(ret => { 269 | return t.equal(ret, null, 'null return value on success') 270 | }) 271 | }) 272 | 273 | test('createToken', t => { 274 | const base = { 275 | password: 'secretPassw0rd!', 276 | readonly: true, 277 | cidr_whitelist: ['8.8.8.8/32', '127.0.0.1', '192.168.1.1'], 278 | } 279 | const obj = Object.assign({ 280 | token: 'deadbeef', 281 | key: 'sha512-badc0ffee', 282 | created: (new Date()).toString(), 283 | }) 284 | delete obj.password 285 | tnock(t, registry).post('/-/npm/v1/tokens', base).reply(200, obj) 286 | return profile.createToken( 287 | base.password, 288 | base.readonly, 289 | base.cidr_whitelist 290 | ).then(ret => t.same(ret, obj, 'got the right return value')) 291 | }) 292 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | checks: write 14 | 15 | jobs: 16 | release: 17 | outputs: 18 | pr: ${{ steps.release.outputs.pr }} 19 | pr-branch: ${{ steps.release.outputs.pr-branch }} 20 | pr-number: ${{ steps.release.outputs.pr-number }} 21 | pr-sha: ${{ steps.release.outputs.pr-sha }} 22 | releases: ${{ steps.release.outputs.releases }} 23 | comment-id: ${{ steps.create-comment.outputs.comment-id || steps.update-comment.outputs.comment-id }} 24 | check-id: ${{ steps.create-check.outputs.check-id }} 25 | name: Release 26 | if: github.repository_owner == 'npm' 27 | runs-on: ubuntu-latest 28 | defaults: 29 | run: 30 | shell: bash 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Git User 35 | run: | 36 | git config --global user.email "npm-cli+bot@github.com" 37 | git config --global user.name "npm CLI robot" 38 | - name: Setup Node 39 | uses: actions/setup-node@v4 40 | id: node 41 | with: 42 | node-version: 22.x 43 | check-latest: contains('22.x', '.x') 44 | - name: Install Latest npm 45 | uses: ./.github/actions/install-latest-npm 46 | with: 47 | node: ${{ steps.node.outputs.node-version }} 48 | - name: Install Dependencies 49 | run: npm i --ignore-scripts --no-audit --no-fund 50 | - name: Release Please 51 | id: release 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | run: npx --offline template-oss-release-please --branch="${{ github.ref_name }}" --backport="" --defaultTag="latest" 55 | - name: Create Release Manager Comment Text 56 | if: steps.release.outputs.pr-number 57 | uses: actions/github-script@v7 58 | id: comment-text 59 | with: 60 | result-encoding: string 61 | script: | 62 | const { runId, repo: { owner, repo } } = context 63 | const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) 64 | return['## Release Manager', `Release workflow run: ${workflow.html_url}`].join('\n\n') 65 | - name: Find Release Manager Comment 66 | uses: peter-evans/find-comment@v2 67 | if: steps.release.outputs.pr-number 68 | id: found-comment 69 | with: 70 | issue-number: ${{ steps.release.outputs.pr-number }} 71 | comment-author: 'github-actions[bot]' 72 | body-includes: '## Release Manager' 73 | - name: Create Release Manager Comment 74 | id: create-comment 75 | if: steps.release.outputs.pr-number && !steps.found-comment.outputs.comment-id 76 | uses: peter-evans/create-or-update-comment@v3 77 | with: 78 | issue-number: ${{ steps.release.outputs.pr-number }} 79 | body: ${{ steps.comment-text.outputs.result }} 80 | - name: Update Release Manager Comment 81 | id: update-comment 82 | if: steps.release.outputs.pr-number && steps.found-comment.outputs.comment-id 83 | uses: peter-evans/create-or-update-comment@v3 84 | with: 85 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 86 | body: ${{ steps.comment-text.outputs.result }} 87 | edit-mode: 'replace' 88 | - name: Create Check 89 | id: create-check 90 | uses: ./.github/actions/create-check 91 | if: steps.release.outputs.pr-sha 92 | with: 93 | name: "Release" 94 | token: ${{ secrets.GITHUB_TOKEN }} 95 | sha: ${{ steps.release.outputs.pr-sha }} 96 | 97 | update: 98 | needs: release 99 | outputs: 100 | sha: ${{ steps.commit.outputs.sha }} 101 | check-id: ${{ steps.create-check.outputs.check-id }} 102 | name: Update - Release 103 | if: github.repository_owner == 'npm' && needs.release.outputs.pr 104 | runs-on: ubuntu-latest 105 | defaults: 106 | run: 107 | shell: bash 108 | steps: 109 | - name: Checkout 110 | uses: actions/checkout@v4 111 | with: 112 | fetch-depth: 0 113 | ref: ${{ needs.release.outputs.pr-branch }} 114 | - name: Setup Git User 115 | run: | 116 | git config --global user.email "npm-cli+bot@github.com" 117 | git config --global user.name "npm CLI robot" 118 | - name: Setup Node 119 | uses: actions/setup-node@v4 120 | id: node 121 | with: 122 | node-version: 22.x 123 | check-latest: contains('22.x', '.x') 124 | - name: Install Latest npm 125 | uses: ./.github/actions/install-latest-npm 126 | with: 127 | node: ${{ steps.node.outputs.node-version }} 128 | - name: Install Dependencies 129 | run: npm i --ignore-scripts --no-audit --no-fund 130 | - name: Create Release Manager Checklist Text 131 | id: comment-text 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | run: npm exec --offline -- template-oss-release-manager --pr="${{ needs.release.outputs.pr-number }}" --backport="" --defaultTag="latest" --publish 135 | - name: Append Release Manager Comment 136 | uses: peter-evans/create-or-update-comment@v3 137 | with: 138 | comment-id: ${{ needs.release.outputs.comment-id }} 139 | body: ${{ steps.comment-text.outputs.result }} 140 | edit-mode: 'append' 141 | - name: Run Post Pull Request Actions 142 | env: 143 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 144 | run: npm run rp-pull-request --ignore-scripts --if-present -- --pr="${{ needs.release.outputs.pr-number }}" --commentId="${{ needs.release.outputs.comment-id }}" 145 | - name: Commit 146 | id: commit 147 | env: 148 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 149 | run: | 150 | git commit --all --amend --no-edit || true 151 | git push --force-with-lease 152 | echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 153 | - name: Create Check 154 | id: create-check 155 | uses: ./.github/actions/create-check 156 | with: 157 | name: "Update - Release" 158 | check-name: "Release" 159 | token: ${{ secrets.GITHUB_TOKEN }} 160 | sha: ${{ steps.commit.outputs.sha }} 161 | - name: Conclude Check 162 | uses: LouisBrunner/checks-action@v1.6.0 163 | with: 164 | token: ${{ secrets.GITHUB_TOKEN }} 165 | conclusion: ${{ job.status }} 166 | check_id: ${{ needs.release.outputs.check-id }} 167 | 168 | ci: 169 | name: CI - Release 170 | needs: [ release, update ] 171 | if: needs.release.outputs.pr 172 | uses: ./.github/workflows/ci-release.yml 173 | with: 174 | ref: ${{ needs.release.outputs.pr-branch }} 175 | check-sha: ${{ needs.update.outputs.sha }} 176 | 177 | post-ci: 178 | needs: [ release, update, ci ] 179 | name: Post CI - Release 180 | if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() 181 | runs-on: ubuntu-latest 182 | defaults: 183 | run: 184 | shell: bash 185 | steps: 186 | - name: Get CI Conclusion 187 | id: conclusion 188 | run: | 189 | result="" 190 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 191 | result="failure" 192 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 193 | result="cancelled" 194 | else 195 | result="success" 196 | fi 197 | echo "result=$result" >> $GITHUB_OUTPUT 198 | - name: Conclude Check 199 | uses: LouisBrunner/checks-action@v1.6.0 200 | with: 201 | token: ${{ secrets.GITHUB_TOKEN }} 202 | conclusion: ${{ steps.conclusion.outputs.result }} 203 | check_id: ${{ needs.update.outputs.check-id }} 204 | 205 | post-release: 206 | needs: release 207 | outputs: 208 | comment-id: ${{ steps.create-comment.outputs.comment-id }} 209 | name: Post Release - Release 210 | if: github.repository_owner == 'npm' && needs.release.outputs.releases 211 | runs-on: ubuntu-latest 212 | defaults: 213 | run: 214 | shell: bash 215 | steps: 216 | - name: Create Release PR Comment Text 217 | id: comment-text 218 | uses: actions/github-script@v7 219 | env: 220 | RELEASES: ${{ needs.release.outputs.releases }} 221 | with: 222 | result-encoding: string 223 | script: | 224 | const releases = JSON.parse(process.env.RELEASES) 225 | const { runId, repo: { owner, repo } } = context 226 | const issue_number = releases[0].prNumber 227 | const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}` 228 | 229 | return [ 230 | '## Release Workflow\n', 231 | ...releases.map(r => `- \`${r.pkgName}@${r.version}\` ${r.url}`), 232 | `- Workflow run: :arrows_counterclockwise: ${runUrl}`, 233 | ].join('\n') 234 | - name: Create Release PR Comment 235 | id: create-comment 236 | uses: peter-evans/create-or-update-comment@v3 237 | with: 238 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 239 | body: ${{ steps.comment-text.outputs.result }} 240 | 241 | release-integration: 242 | needs: release 243 | name: Release Integration 244 | if: needs.release.outputs.releases 245 | uses: ./.github/workflows/release-integration.yml 246 | permissions: 247 | contents: read 248 | id-token: write 249 | secrets: 250 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 251 | with: 252 | releases: ${{ needs.release.outputs.releases }} 253 | 254 | post-release-integration: 255 | needs: [ release, release-integration, post-release ] 256 | name: Post Release Integration - Release 257 | if: github.repository_owner == 'npm' && needs.release.outputs.releases && always() 258 | runs-on: ubuntu-latest 259 | defaults: 260 | run: 261 | shell: bash 262 | steps: 263 | - name: Get Post Release Conclusion 264 | id: conclusion 265 | run: | 266 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 267 | result="x" 268 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 269 | result="heavy_multiplication_x" 270 | else 271 | result="white_check_mark" 272 | fi 273 | echo "result=$result" >> $GITHUB_OUTPUT 274 | - name: Find Release PR Comment 275 | uses: peter-evans/find-comment@v2 276 | id: found-comment 277 | with: 278 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 279 | comment-author: 'github-actions[bot]' 280 | body-includes: '## Release Workflow' 281 | - name: Create Release PR Comment Text 282 | id: comment-text 283 | if: steps.found-comment.outputs.comment-id 284 | uses: actions/github-script@v7 285 | env: 286 | RESULT: ${{ steps.conclusion.outputs.result }} 287 | BODY: ${{ steps.found-comment.outputs.comment-body }} 288 | with: 289 | result-encoding: string 290 | script: | 291 | const { RESULT, BODY } = process.env 292 | const body = [BODY.replace(/(Workflow run: :)[a-z_]+(:)/, `$1${RESULT}$2`)] 293 | if (RESULT !== 'white_check_mark') { 294 | body.push(':rotating_light::rotating_light::rotating_light:') 295 | body.push([ 296 | '@npm/cli-team: The post-release workflow failed for this release.', 297 | 'Manual steps may need to be taken after examining the workflow output.' 298 | ].join(' ')) 299 | body.push(':rotating_light::rotating_light::rotating_light:') 300 | } 301 | return body.join('\n\n').trim() 302 | - name: Update Release PR Comment 303 | if: steps.comment-text.outputs.result 304 | uses: peter-evans/create-or-update-comment@v3 305 | with: 306 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 307 | body: ${{ steps.comment-text.outputs.result }} 308 | edit-mode: 'replace' 309 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [12.0.1](https://github.com/npm/npm-profile/compare/v12.0.0...v12.0.1) (2025-10-23) 4 | ### Dependencies 5 | * [`f147b6d`](https://github.com/npm/npm-profile/commit/f147b6d195b1cf4b56e5e459b0d6f526517a8679) [#171](https://github.com/npm/npm-profile/pull/171) bump proc-log from 5.0.0 to 6.0.0 (#171) (@dependabot[bot]) 6 | ### Chores 7 | * [`baa7d79`](https://github.com/npm/npm-profile/commit/baa7d7955774a5b738695d73028d3176620b534c) [#170](https://github.com/npm/npm-profile/pull/170) bump @npmcli/template-oss from 4.26.0 to 4.27.1 (#170) (@dependabot[bot], @npm-cli-bot) 8 | 9 | ## [12.0.0](https://github.com/npm/npm-profile/compare/v11.0.1...v12.0.0) (2025-07-24) 10 | ### ⚠️ BREAKING CHANGES 11 | * `npm-profile` now supports node `^20.17.0 || >=22.9.0` 12 | ### Bug Fixes 13 | * [`637f654`](https://github.com/npm/npm-profile/commit/637f6545c641b0be98d1996c4bc863d5a3729f7b) [#164](https://github.com/npm/npm-profile/pull/164) align to npm 11 node engine range (@owlstronaut) 14 | ### Dependencies 15 | * [`7366688`](https://github.com/npm/npm-profile/commit/7366688ede67cbe133b48423289e44a5a9f8ae00) [#164](https://github.com/npm/npm-profile/pull/164) `npm-registry-fetch@19.0.0` 16 | ### Chores 17 | * [`8934c09`](https://github.com/npm/npm-profile/commit/8934c09cb8d4d04b168747fe7905b9c21d9a27b1) [#164](https://github.com/npm/npm-profile/pull/164) `nock@13.5.6` (@owlstronaut) 18 | * [`ea70eaa`](https://github.com/npm/npm-profile/commit/ea70eaa9a5e03c7e0e755cb929bff338b8b0bde8) [#164](https://github.com/npm/npm-profile/pull/164) template-oss apply fix (@owlstronaut) 19 | * [`2aa199f`](https://github.com/npm/npm-profile/commit/2aa199f07c1271ffad51a0d716bc89a054c3e6f8) [#161](https://github.com/npm/npm-profile/pull/161) postinstall workflow updates (#161) (@owlstronaut) 20 | * [`87e427e`](https://github.com/npm/npm-profile/commit/87e427e95d32cfe52e7a7ecf35a170ce60068073) [#160](https://github.com/npm/npm-profile/pull/160) bump nock from 13.5.6 to 14.0.3 (#160) (@dependabot[bot]) 21 | * [`49ce76a`](https://github.com/npm/npm-profile/commit/49ce76a5adaf71914b1e6d326773d2adb2aa5ad8) [#163](https://github.com/npm/npm-profile/pull/163) bump @npmcli/template-oss from 4.24.4 to 4.25.0 (#163) (@dependabot[bot], @npm-cli-bot) 22 | 23 | ## [11.0.1](https://github.com/npm/npm-profile/compare/v11.0.0...v11.0.1) (2024-10-02) 24 | ### Dependencies 25 | * [`7c3c631`](https://github.com/npm/npm-profile/commit/7c3c631c3268f7a305af06bbc7ad48834811d063) [#155](https://github.com/npm/npm-profile/pull/155) bump `npm-registry-fetch@18.0.0` 26 | 27 | ## [11.0.0](https://github.com/npm/npm-profile/compare/v10.0.0...v11.0.0) (2024-09-26) 28 | ### ⚠️ BREAKING CHANGES 29 | * `npm-profile` now supports node `^18.17.0 || >=20.5.0` 30 | ### Bug Fixes 31 | * [`a0ea10b`](https://github.com/npm/npm-profile/commit/a0ea10b41698df6f8077178f008ac16c27db4620) [#152](https://github.com/npm/npm-profile/pull/152) align to npm 10 node engine range (@reggi) 32 | * [`4ea3f70`](https://github.com/npm/npm-profile/commit/4ea3f70330f36b3d019709fa2ac41426e605e895) [#144](https://github.com/npm/npm-profile/pull/144) exit handler error on login (#144) (@milaninfy) 33 | ### Dependencies 34 | * [`66bcc40`](https://github.com/npm/npm-profile/commit/66bcc4087b938e5ea23fb5d238c1e0620591b90d) [#152](https://github.com/npm/npm-profile/pull/152) `proc-log@5.0.0` 35 | ### Chores 36 | * [`8ac1fdb`](https://github.com/npm/npm-profile/commit/8ac1fdbfef7b863d0a9d38fd47970614b745a3eb) [#152](https://github.com/npm/npm-profile/pull/152) run template-oss-apply (@reggi) 37 | * [`1fdff2e`](https://github.com/npm/npm-profile/commit/1fdff2e67aa37a26457c60a20656eecd3408a8f6) [#146](https://github.com/npm/npm-profile/pull/146) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (@dependabot[bot]) 38 | * [`5b3ebbc`](https://github.com/npm/npm-profile/commit/5b3ebbc6580e4389bcc85dbdfb74816810b62f4c) [#134](https://github.com/npm/npm-profile/pull/134) bump @npmcli/template-oss to 4.22.0 (@lukekarrys) 39 | * [`6b4558f`](https://github.com/npm/npm-profile/commit/6b4558f6d91ffc7f21f181941462146acccb218e) [#147](https://github.com/npm/npm-profile/pull/147) postinstall for dependabot template-oss PR (@hashtagchris) 40 | * [`c644e89`](https://github.com/npm/npm-profile/commit/c644e89bf1abfdea2a84359c9a394bb85df12d6e) [#147](https://github.com/npm/npm-profile/pull/147) bump @npmcli/template-oss from 4.23.1 to 4.23.3 (@dependabot[bot]) 41 | 42 | ## [10.0.0](https://github.com/npm/npm-profile/compare/v9.0.2...v10.0.0) (2024-05-02) 43 | 44 | ### ⚠️ BREAKING CHANGES 45 | 46 | * this uses AbortSignal.throwIfAborted() which is not available in all versions of Node 16 47 | * `hostname` is no longer sent as part of the web auth body 48 | * the opener function will now receive an object with an abort signal which can be used to listen for the abort event intead of an event emitter 49 | 50 | ### Features 51 | 52 | * [`f67687d`](https://github.com/npm/npm-profile/commit/f67687d2bdc58ace8ee4e236254525cb2f3c07ef) [#131](https://github.com/npm/npm-profile/pull/131) drop node 16 support (@lukekarrys) 53 | * [`d6f6ebe`](https://github.com/npm/npm-profile/commit/d6f6ebe1b7e2ac75f63d261271ab442a9a96c610) [#131](https://github.com/npm/npm-profile/pull/131) remove hostname from body (@lukekarrys, @wraithgar) 54 | * [`c0bb22f`](https://github.com/npm/npm-profile/commit/c0bb22fa0027859b494b643fe3c670e3a366c822) [#131](https://github.com/npm/npm-profile/pull/131) add webAuthOpener method (@lukekarrys) 55 | * [`df44705`](https://github.com/npm/npm-profile/commit/df44705b4a7b4590531536449fcfd81de01bc36b) [#131](https://github.com/npm/npm-profile/pull/131) use AbortSignal instead of EventEmitter for opener (@lukekarrys) 56 | 57 | ### Bug Fixes 58 | 59 | * [`53db633`](https://github.com/npm/npm-profile/commit/53db633662252216c23599d43cf2daac3dac1c20) [#131](https://github.com/npm/npm-profile/pull/131) pass signal to webAuthCheckLogin timer (@lukekarrys) 60 | 61 | ### Dependencies 62 | 63 | * [`5c4221b`](https://github.com/npm/npm-profile/commit/5c4221b67306792ab5ec27ab4c2ce27f320f81f9) [#133](https://github.com/npm/npm-profile/pull/133) `npm-registry-fetch@17.0.1` (#133) 64 | 65 | ## [9.0.2](https://github.com/npm/npm-profile/compare/v9.0.1...v9.0.2) (2024-04-30) 66 | 67 | ### Bug Fixes 68 | 69 | * [`06687c8`](https://github.com/npm/npm-profile/commit/06687c86200ad1d12114f002d6eb53d646e96eee) [#130](https://github.com/npm/npm-profile/pull/130) linting: no-unused-vars (#130) (@wraithgar) 70 | 71 | ### Dependencies 72 | 73 | * [`29e6172`](https://github.com/npm/npm-profile/commit/29e6172878c19deef8ebd6aacac63ad17a11e37d) [#129](https://github.com/npm/npm-profile/pull/129) `npm-registry-fetch@17.0.0` (#129) 74 | 75 | ### Chores 76 | 77 | * [`1c8afe8`](https://github.com/npm/npm-profile/commit/1c8afe8a2652b91bca147641b4a90d7fb590919b) [#127](https://github.com/npm/npm-profile/pull/127) postinstall for dependabot template-oss PR (@lukekarrys) 78 | * [`3b68ec1`](https://github.com/npm/npm-profile/commit/3b68ec1791d555545ad86d5f95995a963a4110ce) [#127](https://github.com/npm/npm-profile/pull/127) bump @npmcli/template-oss from 4.21.3 to 4.21.4 (@dependabot[bot]) 79 | 80 | ## [9.0.1](https://github.com/npm/npm-profile/compare/v9.0.0...v9.0.1) (2024-04-12) 81 | 82 | ### Dependencies 83 | 84 | * [`44972e2`](https://github.com/npm/npm-profile/commit/44972e20d5eb75097c8a3d7ee438b604fd8cbc98) [#126](https://github.com/npm/npm-profile/pull/126) `proc-log@4.0.0` (#126) 85 | 86 | ### Chores 87 | 88 | * [`11f4605`](https://github.com/npm/npm-profile/commit/11f46058f159a0ec15c0733a61d6620db1a6b96a) [#122](https://github.com/npm/npm-profile/pull/122) postinstall for dependabot template-oss PR (@lukekarrys) 89 | * [`0719640`](https://github.com/npm/npm-profile/commit/0719640d5c1122aa3bd64381672a6a2280a7fe8b) [#122](https://github.com/npm/npm-profile/pull/122) bump @npmcli/template-oss from 4.21.1 to 4.21.3 (@dependabot[bot]) 90 | * [`e944f88`](https://github.com/npm/npm-profile/commit/e944f88129d2179697f4f85d8cda164eed4445e7) [#119](https://github.com/npm/npm-profile/pull/119) postinstall for dependabot template-oss PR (@lukekarrys) 91 | * [`28888c7`](https://github.com/npm/npm-profile/commit/28888c7d277f65bc0b14cdd232848730eada2856) [#119](https://github.com/npm/npm-profile/pull/119) bump @npmcli/template-oss from 4.19.0 to 4.21.1 (@dependabot[bot]) 92 | * [`30097a5`](https://github.com/npm/npm-profile/commit/30097a5eef4239399b964c2efc121e64e75ecaf5) [#101](https://github.com/npm/npm-profile/pull/101) postinstall for dependabot template-oss PR (@lukekarrys) 93 | * [`efe9f20`](https://github.com/npm/npm-profile/commit/efe9f20d9b456ad3f3c96a686b5c71ef7dd97f4f) [#101](https://github.com/npm/npm-profile/pull/101) bump @npmcli/template-oss from 4.18.1 to 4.19.0 (@dependabot[bot]) 94 | * [`cd076f1`](https://github.com/npm/npm-profile/commit/cd076f19c289abc4e14c18c111163dca7ceb7047) [#100](https://github.com/npm/npm-profile/pull/100) postinstall for dependabot template-oss PR (@lukekarrys) 95 | * [`e928f0c`](https://github.com/npm/npm-profile/commit/e928f0c356e7207b5bd99460c361b7534ed913a4) [#100](https://github.com/npm/npm-profile/pull/100) bump @npmcli/template-oss from 4.18.0 to 4.18.1 (@dependabot[bot]) 96 | 97 | ## [9.0.0](https://github.com/npm/npm-profile/compare/v8.0.0...v9.0.0) (2023-08-15) 98 | 99 | ### ⚠️ BREAKING CHANGES 100 | 101 | * support for node <=16.13 has been removed 102 | 103 | ### Bug Fixes 104 | 105 | * [`d2fdd5e`](https://github.com/npm/npm-profile/commit/d2fdd5eeb907fab727797750e8340b213bffd60c) [#97](https://github.com/npm/npm-profile/pull/97) drop node 16.13.x support (@lukekarrys) 106 | 107 | ### Dependencies 108 | 109 | * [`1855caf`](https://github.com/npm/npm-profile/commit/1855cafdc9028168fd44ac805310a782761cd89b) [#96](https://github.com/npm/npm-profile/pull/96) bump npm-registry-fetch from 15.0.0 to 16.0.0 110 | 111 | ## [8.0.0](https://github.com/npm/npm-profile/compare/v7.0.1...v8.0.0) (2023-08-14) 112 | 113 | ### ⚠️ BREAKING CHANGES 114 | 115 | * support for node 14 has been removed 116 | 117 | ### Bug Fixes 118 | 119 | * [`cfd2d07`](https://github.com/npm/npm-profile/commit/cfd2d07ef5eeaac7187c75e31718c2d73af596da) [#93](https://github.com/npm/npm-profile/pull/93) drop node14 support (@lukekarrys) 120 | 121 | ### Dependencies 122 | 123 | * [`96370c2`](https://github.com/npm/npm-profile/commit/96370c2f60a6a50a8e6027ca2b4972716af5efec) [#92](https://github.com/npm/npm-profile/pull/92) bump npm-registry-fetch from 14.0.5 to 15.0.0 124 | 125 | ## [7.0.1](https://github.com/npm/npm-profile/compare/v7.0.0...v7.0.1) (2022-10-17) 126 | 127 | ### Dependencies 128 | 129 | * [`36fa4b1`](https://github.com/npm/npm-profile/commit/36fa4b13df38e2dde7a74bf23e49e013ca8129c6) [#76](https://github.com/npm/npm-profile/pull/76) bump npm-registry-fetch from 13.3.1 to 14.0.0 130 | * [`29616ad`](https://github.com/npm/npm-profile/commit/29616ad8e59847e2350d3376e755a4688d0c81a0) [#77](https://github.com/npm/npm-profile/pull/77) bump proc-log from 2.0.1 to 3.0.0 131 | 132 | ## [7.0.0](https://github.com/npm/npm-profile/compare/v6.2.1...v7.0.0) (2022-09-30) 133 | 134 | ### ⚠️ BREAKING CHANGES 135 | 136 | * `npm-profile` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0` 137 | 138 | ### Features 139 | 140 | * [`e16befb`](https://github.com/npm/npm-profile/commit/e16befb96a182525432f4023952033f759f8c814) [#68](https://github.com/npm/npm-profile/pull/68) postinstall for dependabot template-oss PR (@lukekarrys) 141 | 142 | ## [6.2.1](https://github.com/npm/npm-profile/compare/v6.2.0...v6.2.1) (2022-08-02) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * cancel opener promise if web login fails ([#57](https://github.com/npm/npm-profile/issues/57)) ([cdc4acb](https://github.com/npm/npm-profile/commit/cdc4acb81d28924bdc8f5503f2eb2515884b6478)) 148 | * remove `npm-use-webauthn` header ([#53](https://github.com/npm/npm-profile/issues/53)) ([b701df2](https://github.com/npm/npm-profile/commit/b701df2630c12de0db555138238eb24a026a438b)) 149 | 150 | ## [6.2.0](https://github.com/npm/npm-profile/compare/v6.1.0...v6.2.0) (2022-07-12) 151 | 152 | 153 | ### Features 154 | 155 | * Add export for `webauthCheckLogin` ([#54](https://github.com/npm/npm-profile/issues/54)) ([05a7811](https://github.com/npm/npm-profile/commit/05a78112a4a5c473813cb1f26be346452687899b)) 156 | 157 | ## [6.1.0](https://github.com/npm/npm-profile/compare/v6.0.3...v6.1.0) (2022-06-08) 158 | 159 | 160 | ### Features 161 | 162 | * Allow web-login donecheck to cancel opener promise ([#50](https://github.com/npm/npm-profile/issues/50)) ([aa82de0](https://github.com/npm/npm-profile/commit/aa82de07a3c2e5adf90c79d6401dba7b9705da27)) 163 | * set 'npm-use-webauthn' header depending on option ([#48](https://github.com/npm/npm-profile/issues/48)) ([6bdd233](https://github.com/npm/npm-profile/commit/6bdd2331b988d981be58953b28ec93a2c3412b58)) 164 | 165 | ### [6.0.3](https://github.com/npm/npm-profile/compare/v6.0.2...v6.0.3) (2022-04-20) 166 | 167 | 168 | ### Dependencies 169 | 170 | * update npm-registry-fetch requirement from ^13.0.0 to ^13.0.1 ([#34](https://github.com/npm/npm-profile/issues/34)) ([a444b51](https://github.com/npm/npm-profile/commit/a444b5149653e7cfba2cdbcd8e31bb12d97bc152)) 171 | 172 | ### [6.0.2](https://www.github.com/npm/npm-profile/compare/v6.0.1...v6.0.2) (2022-02-15) 173 | 174 | 175 | ### Dependencies 176 | 177 | * bump npm-registry-fetch from 12.0.2 to 13.0.0 ([#28](https://www.github.com/npm/npm-profile/issues/28)) ([7c01310](https://www.github.com/npm/npm-profile/commit/7c0131079fb3ab955b304f34e28374f0c1321565)) 178 | 179 | ### [6.0.1](https://www.github.com/npm/npm-profile/compare/v6.0.0...v6.0.1) (2022-02-14) 180 | 181 | 182 | ### Bug Fixes 183 | 184 | * Modify logincouch target ([#27](https://www.github.com/npm/npm-profile/issues/27)) ([1889375](https://www.github.com/npm/npm-profile/commit/1889375c240f85fbb2d38b72c2dda7e5a73bf9f0)) 185 | 186 | 187 | ### Dependencies 188 | 189 | * update npm-registry-fetch requirement from ^12.0.0 to ^12.0.2 ([82b65f8](https://www.github.com/npm/npm-profile/commit/82b65f8fef07497c116a64f141e275c173cb8cf1)) 190 | * use proc-log instead of process.emit ([fe2b8f3](https://www.github.com/npm/npm-profile/commit/fe2b8f3988578661d688415feb4c37dd1aa8b82d)) 191 | 192 | ## [6.0.0](https://www.github.com/npm/npm-profile/compare/v5.0.4...v6.0.0) (2022-01-19) 193 | 194 | 195 | ### ⚠ BREAKING CHANGES 196 | 197 | * this drops support for node<=10 and non-LTS versions of node12 and node14 198 | 199 | ### Features 200 | 201 | * move to template-oss ([3f668be](https://www.github.com/npm/npm-profile/commit/3f668be0e12b4752fe87f7410cdb5ae6a97eef70)) 202 | 203 | 204 | ### Documentation 205 | 206 | * Update README.md ([#14](https://www.github.com/npm/npm-profile/issues/14)) ([531e352](https://www.github.com/npm/npm-profile/commit/531e35262bec5bb8f3611f774bc87cbd42437c3f)) 207 | 208 | 209 | ### dependencies 210 | 211 | * npm-registry-fetch@12.0.0 ([852b3f0](https://www.github.com/npm/npm-profile/commit/852b3f07b56c9c0a10efacde841d5c6172f87c5c)) 212 | 213 | ## v5.0.0 (2020-02-27) 214 | 215 | - Drop the CLI from the project, just maintain the library 216 | - Drop support for EOL Node.js versions 217 | - Remove `Promise` option, just use native Promises 218 | - Remove `figgy-pudding` 219 | - Use `npm-registry-fetch` v8 220 | - fix: do not try to open invalid URLs for WebLogin 221 | 222 | ## v4.0.3 (2020-02-27) 223 | 224 | - fix: do not try to open invalid URLs for WebLogin 225 | 226 | ## v4.0.2 (2019-07-16) 227 | 228 | - Update `npm-registry-fetch` to 4.0.0 229 | 230 | ## v4.0.1 (2018-08-29) 231 | 232 | - `opts.password` needs to be base64-encoded when passed in for login 233 | - Bump `npm-registry-fetch` dep because we depend on `opts.forceAuth` 234 | 235 | ## v4.0.0 (2018-08-28) 236 | 237 | ### BREAKING CHANGES: 238 | 239 | - Networking and auth-related options now use the latest [`npm-registry-fetch` config format](https://www.npmjs.com/package/npm-registry-fetch#fetch-opts). 240 | 241 | ## v3.0.2 (2018-06-07) 242 | 243 | - Allow newer make-fetch-happen. 244 | - Report 500s from weblogin end point as unsupported. 245 | - EAUTHUNKNOWN errors were incorrectly reported as EAUTHIP. 246 | 247 | ## v3.0.1 (2018-02-18) 248 | 249 | - Log `npm-notice` headers 250 | 251 | ## v3.0.0 (2018-02-18) 252 | 253 | ### BREAKING CHANGES: 254 | 255 | - profile.login() and profile.adduser() take 2 functions: opener() and 256 | prompter(). opener is used when we get the url couplet from the 257 | registry. prompter is used if web-based login fails. 258 | - Non-200 status codes now always throw. Previously if the `content.error` 259 | property was set, `content` would be returned. Content is available on the 260 | thrown error object in the `body` property. 261 | 262 | ### FEATURES: 263 | 264 | - The previous adduser is available as adduserCouch 265 | - The previous login is available as loginCouch 266 | - New loginWeb and adduserWeb commands added, which take an opener 267 | function to open up the web browser. 268 | - General errors have better error message reporting 269 | 270 | ### FIXES: 271 | 272 | - General errors now correctly include the URL. 273 | - Missing user errors from Couch are now thrown. (As was always intended.) 274 | - Many errors have better stacktrace filtering. 275 | -------------------------------------------------------------------------------- /test/login.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | const profile = require('..') 3 | const http = require('http') 4 | const timers = require('timers/promises') 5 | const PORT = +process.env.PORT || 13445 6 | 7 | const reg = 'http://localhost:' + PORT 8 | 9 | // track requests made so that any change is noticed 10 | const requests = [] 11 | 12 | t.beforeEach(async () => { 13 | if (this.parent === t) { 14 | requests.push(this.name) 15 | } 16 | }) 17 | 18 | let retryTimer = null 19 | let retryAgain = 0 20 | 21 | const server = http.createServer((q, s) => { 22 | // Retries are nondeterministic 23 | // We should get SOME retrying, but can't know the amount 24 | const reqLog = `${q.method} ${q.url}` 25 | if (reqLog !== requests[requests.length - 1] || 26 | reqLog !== requests[requests.length - 2] || 27 | reqLog.indexOf(' /retry') === -1) { 28 | requests.push(reqLog) 29 | } 30 | let body = '' 31 | q.on('data', c => { 32 | body += c 33 | }) 34 | q.on('end', () => { 35 | try { 36 | body = JSON.parse(body) 37 | } catch (er) { 38 | body = undefined 39 | } 40 | switch (q.url) { 41 | case '/weblogin/-/v1/login': 42 | return respond(s, 200, { 43 | loginUrl: 'http://example.com/blerg', 44 | doneUrl: reg + '/weblogin/-/v1/login/blerg', 45 | }) 46 | 47 | case '/weblogin/-/v1/login/blerg': 48 | return respond(s, 200, { token: 'blerg' }) 49 | 50 | case '/couchdb/-/user/org.couchdb.user:user': 51 | if (body && body.name === 'user' && body.password === 'pass') { 52 | body.token = 'blerg' 53 | return respond(s, 201, body) 54 | } else { 55 | return respond(s, 403, { error: 'nope' }) 56 | } 57 | 58 | case '/couchdb/-/user/org.couchdb.user:nemo': 59 | return respond(s, 400, { error: 'invalid request' }) 60 | 61 | case '/couchdb/-/user/org.couchdb.user:exists': 62 | return respond(s, 409, { error: 'conflict' }) 63 | 64 | case '/couchdb/-/user/org.couchdb.user:exists?write=true': 65 | return respond(s, 200, { 66 | name: 'exists', 67 | _rev: 'goodbloodmoon', 68 | roles: ['yabba', 'dabba', 'doo'], 69 | email: 'i@izs.me', 70 | }) 71 | 72 | case '/couchdb/-/user/org.couchdb.user:exists/-rev/goodbloodmoon': 73 | // make sure we got the stuff we wanted. 74 | if (body && body.roles && 75 | body.roles.join(',') === 'yabba,dabba,doo' && 76 | body.email === 'i@izs.me' && 77 | body.password === 'pass' && 78 | body._rev === 'goodbloodmoon') { 79 | return respond(s, 201, { ok: 'created', token: 'blerg' }) 80 | } else { 81 | return respond(s, 403, { 82 | error: 'what are you even doing?', 83 | sentBody: body, 84 | }) 85 | } 86 | 87 | case '/501/-/v1/login': 88 | case '/501/-/v1/login/blerg': 89 | case '/501/-/user/org.couchdb.user:user': 90 | return respond(s, 501, { pwn: 'witaf idk lol' }) 91 | 92 | case '/notoken/-/v1/login': 93 | return respond(s, 200, { 94 | loginUrl: 'http://example.com/blerg', 95 | doneUrl: reg + '/notoken/-/v1/login/blerg', 96 | }) 97 | case '/notoken/-/v1/login/blerg': 98 | return respond(s, 200, { oh: 'no' }) 99 | 100 | case '/retry-after/-/v1/login': 101 | return respond(s, 200, { 102 | loginUrl: 'http://www.example.com/blerg', 103 | doneUrl: reg + '/retry-after/-/v1/login/blerg', 104 | }) 105 | 106 | case '/retry-after/-/v1/login/blerg': 107 | if (!retryTimer) { 108 | retryTimer = Date.now() + 250 109 | } 110 | 111 | if (retryTimer > Date.now()) { 112 | const newRTA = (retryTimer - Date.now()) / 1000 113 | s.setHeader('retry-after', newRTA) 114 | return respond(s, 202, {}) 115 | } else { 116 | retryTimer = null 117 | return respond(s, 200, { token: 'blerg' }) 118 | } 119 | 120 | case '/retry-again/-/v1/login': 121 | return respond(s, 200, { 122 | loginUrl: 'http://www.example.com/blerg', 123 | doneUrl: reg + '/retry-again/-/v1/login/blerg', 124 | }) 125 | 126 | case '/retry-again/-/v1/login/blerg': 127 | retryAgain++ 128 | 129 | if (retryAgain < 5) { 130 | return respond(s, 202, {}) 131 | } else { 132 | retryAgain = 0 133 | return respond(s, 200, { token: 'blerg' }) 134 | } 135 | 136 | case '/retry-long-time/-/v1/login': 137 | return respond(s, 200, { 138 | loginUrl: 'http://www.example.com/loooooong', 139 | doneUrl: reg + '/retry-long-time/-/v1/login/loooooong', 140 | }) 141 | 142 | case '/retry-long-time/-/v1/login/loooooong': { 143 | s.setHeader('retry-after', 60 * 1000) 144 | return respond(s, 202, {}) 145 | } 146 | 147 | case '/invalid-login/-/v1/login': 148 | return respond(s, 200, { salt: 'im helping' }) 149 | 150 | case '/invalid-login-url/-/v1/login': 151 | return respond(s, 200, { 152 | loginUrl: 'ftp://this.is/not-a-webpage/now/is/it?', 153 | doneUrl: reg + '/invalid-done/-/v1/login', 154 | }) 155 | 156 | case '/invalid-done/-/v1/login': 157 | return respond(s, 200, { 158 | loginUrl: 'http://www.example.com/blerg', 159 | doneUrl: reg + '/invalid-done/-/v1/login/blerg', 160 | }) 161 | case '/invalid-done/-/v1/login/blerg': 162 | return respond(s, 299, { salt: 'im helping' }) 163 | 164 | default: 165 | return respond(s, 404, { error: 'not found' }) 166 | } 167 | }) 168 | }) 169 | 170 | const respond = (s, code, body) => { 171 | s.statusCode = code || 200 172 | s.setHeader('connection', 'close') 173 | s.end(JSON.stringify(body || {})) 174 | } 175 | 176 | t.test('start server', t => server.listen(PORT, t.end)) 177 | 178 | t.test('login web', t => { 179 | let calledOpener = false 180 | const opener = async () => new Promise(resolve => { 181 | calledOpener = true 182 | resolve() 183 | }) 184 | 185 | t.resolveMatch(profile.loginWeb(opener, { 186 | registry: reg + '/weblogin/', 187 | }), { token: 'blerg' }) 188 | 189 | return t.test('called opener', t => { 190 | t.equal(calledOpener, true) 191 | t.end() 192 | }) 193 | }) 194 | 195 | t.test('webAuthCheckLogin', async t => { 196 | await t.resolveMatch(profile.webAuthCheckLogin(reg + '/weblogin/-/v1/login/blerg', { 197 | registry: reg + '/weblogin/', 198 | }), { token: 'blerg' }) 199 | }) 200 | 201 | t.test('adduser web', t => { 202 | let calledOpener = false 203 | const opener = () => new Promise(resolve => { 204 | calledOpener = true 205 | resolve() 206 | }) 207 | 208 | t.resolveMatch(profile.adduserWeb(opener, { 209 | registry: reg + '/weblogin/', 210 | opts: {}, 211 | }), { token: 'blerg' }) 212 | 213 | return t.test('called opener', t => { 214 | t.equal(calledOpener, true) 215 | t.end() 216 | }) 217 | }) 218 | 219 | t.test('login web by default', t => { 220 | let calledOpener = false 221 | 222 | const opener = () => new Promise(resolve => { 223 | calledOpener = true 224 | resolve() 225 | }) 226 | 227 | const prompter = () => { 228 | throw new Error('should not do this') 229 | } 230 | 231 | t.resolveMatch(profile.login(opener, prompter, { 232 | registry: reg + '/weblogin/', 233 | }), { token: 'blerg' }) 234 | 235 | return t.test('called opener', t => { 236 | t.equal(calledOpener, true) 237 | t.end() 238 | }) 239 | }) 240 | 241 | t.test('adduser web', t => { 242 | let calledOpener = false 243 | const opener = () => new Promise(resolve => { 244 | calledOpener = true 245 | resolve() 246 | }) 247 | 248 | t.resolveMatch(profile.adduserWeb(opener, { 249 | registry: reg + '/weblogin/', 250 | }), { token: 'blerg' }) 251 | 252 | return t.test('called opener', t => { 253 | t.equal(calledOpener, true) 254 | t.end() 255 | }) 256 | }) 257 | 258 | t.test('adduser web by default', t => { 259 | let calledOpener = false 260 | 261 | const opener = () => new Promise(resolve => { 262 | calledOpener = true 263 | resolve() 264 | }) 265 | 266 | const prompter = () => { 267 | throw new Error('should not do this') 268 | } 269 | 270 | t.resolveMatch(profile.adduser(opener, prompter, { 271 | registry: reg + '/weblogin/', 272 | }), { token: 'blerg' }) 273 | 274 | return t.test('called opener', t => { 275 | t.equal(calledOpener, true) 276 | t.end() 277 | }) 278 | }) 279 | 280 | t.test('login couch', t => { 281 | const registry = reg + '/couchdb/' 282 | const expect = { token: 'blerg' } 283 | t.test('login as new user', t => 284 | t.resolveMatch(profile.loginCouch('user', 'pass', { registry }), expect)) 285 | 286 | t.test('login as existing user', t => 287 | t.resolveMatch(profile.loginCouch('exists', 'pass', { registry }), expect)) 288 | 289 | const expectedErr = { 290 | code: 'E400', 291 | statusCode: 400, 292 | method: 'PUT', 293 | message: 'There is no user with the username "nemo"', 294 | } 295 | t.test('unknown user', t => 296 | profile.loginCouch('nemo', 'pass', { registry }) 297 | .catch(er => t.match(er, expectedErr))) 298 | 299 | t.end() 300 | }) 301 | 302 | t.test('adduser couch', t => { 303 | return t.resolveMatch(profile.adduserCouch('user', 'i@izs.me', 'pass', { 304 | registry: reg + '/couchdb/', 305 | }), { token: 'blerg' }) 306 | }) 307 | 308 | t.test('login fallback to couch', t => { 309 | let calledPrompter = false 310 | 311 | const opener = () => new Promise(() => { 312 | throw new Error('should not call opener') 313 | }) 314 | 315 | const prompter = () => { 316 | return new Promise((resolve) => { 317 | calledPrompter = true 318 | resolve({ 319 | username: 'user', 320 | password: 'pass', 321 | email: 'i@izs.me', 322 | }) 323 | }) 324 | } 325 | 326 | t.resolveMatch(profile.login(opener, prompter, { 327 | registry: reg + '/couchdb/', 328 | }), { token: 'blerg' }) 329 | 330 | return t.test('called prompter', t => { 331 | t.equal(calledPrompter, true) 332 | t.end() 333 | }) 334 | }) 335 | 336 | t.test('adduser fallback to couch', t => { 337 | let calledPrompter = false 338 | 339 | const opener = () => new Promise(() => { 340 | throw new Error('should not call opener') 341 | }) 342 | 343 | const prompter = () => { 344 | return new Promise((resolve) => { 345 | calledPrompter = true 346 | resolve({ 347 | username: 'user', 348 | password: 'pass', 349 | email: 'i@izs.me', 350 | }) 351 | }) 352 | } 353 | 354 | t.resolveMatch(profile.adduser(opener, prompter, { 355 | registry: reg + '/couchdb/', 356 | }), { token: 'blerg' }) 357 | 358 | return t.test('called prompter', t => { 359 | t.equal(calledPrompter, true) 360 | t.end() 361 | }) 362 | }) 363 | 364 | t.test('501s', t => { 365 | const opener = () => { 366 | throw new Error('poop') 367 | } 368 | const prompter = () => new Promise(resolve => resolve({ 369 | username: 'user', 370 | password: 'pass', 371 | email: 'i@izs.me', 372 | })) 373 | 374 | const registry = reg + '/501/' 375 | 376 | t.plan(6) 377 | 378 | const expectErr = { 379 | code: 'E501', 380 | body: { 381 | pwn: 'witaf idk lol', 382 | }, 383 | } 384 | 385 | t.test('login', t => 386 | profile.login(opener, prompter, { registry }) 387 | .catch(er => t.match(er, expectErr))) 388 | 389 | t.test('adduser', t => 390 | profile.adduser(opener, prompter, { registry }) 391 | .catch(er => t.match(er, expectErr))) 392 | 393 | t.test('loginWeb', t => 394 | profile.loginWeb(opener, { registry }) 395 | .catch(er => t.match(er, expectErr))) 396 | 397 | t.test('adduserWeb', t => 398 | profile.adduserWeb(opener, { registry }) 399 | .catch(er => t.match(er, expectErr))) 400 | 401 | t.test('loginCouch', t => 402 | profile.loginCouch('user', 'pass', { registry }) 403 | .catch(er => t.match(er, expectErr))) 404 | 405 | t.test('adduserCouch', t => 406 | profile.adduserCouch('user', 'i@izs.me', 'pass', { registry }) 407 | .catch(er => t.match(er, expectErr))) 408 | }) 409 | 410 | t.test('fail at login step', t => { 411 | const registry = reg + '/invalid-login/' 412 | const opener = () => new Promise(resolve => resolve()) 413 | const prompter = () => { 414 | throw new Error('should not do this') 415 | } 416 | t.plan(1) 417 | const expectedErr = { 418 | statusCode: 200, 419 | code: 'E200', 420 | method: 'POST', 421 | uri: reg + '/invalid-login/-/v1/login', 422 | body: { 423 | salt: 'im helping', 424 | }, 425 | message: 'Invalid response from web login endpoint', 426 | } 427 | profile.login(opener, prompter, { registry }) 428 | .catch(er => t.match(er, expectedErr)) 429 | }) 430 | 431 | t.test('fail at login step by having an invalid url', t => { 432 | const registry = reg + '/invalid-login-url/' 433 | const opener = () => new Promise(resolve => resolve()) 434 | const prompter = () => { 435 | throw new Error('should not do this') 436 | } 437 | t.plan(1) 438 | const expectedErr = { 439 | statusCode: 200, 440 | code: 'E200', 441 | method: 'POST', 442 | uri: reg + '/invalid-login-url/-/v1/login', 443 | body: { 444 | loginUrl: 'ftp://this.is/not-a-webpage/now/is/it?', 445 | doneUrl: reg + '/invalid-done/-/v1/login', 446 | }, 447 | message: 'Invalid response from web login endpoint', 448 | } 449 | profile.login(opener, prompter, { registry }) 450 | .catch(er => t.match(er, expectedErr)) 451 | }) 452 | 453 | t.test('fail at the done step', t => { 454 | const registry = reg + '/invalid-done/' 455 | const opener = () => new Promise(resolve => resolve()) 456 | const prompter = () => { 457 | throw new Error('should not do this') 458 | } 459 | t.plan(1) 460 | const expectedErr = { 461 | statusCode: 299, 462 | code: 'E299', 463 | method: 'GET', 464 | uri: reg + '/invalid-done/-/v1/login/blerg', 465 | body: { 466 | salt: 'im helping', 467 | }, 468 | message: 'Invalid response from web login endpoint', 469 | } 470 | profile.login(opener, prompter, { registry }) 471 | .catch(er => t.match(er, expectedErr)) 472 | }) 473 | 474 | t.test('notoken response from login endpoint (status 200, bad data)', t => { 475 | const registry = reg + '/notoken/' 476 | 477 | const opener = () => new Promise(resolve => resolve()) 478 | const prompter = () => { 479 | throw new Error('should not do this') 480 | } 481 | 482 | const expectedErr = { 483 | code: 'E200', 484 | statusCode: 200, 485 | method: 'GET', 486 | uri: registry + '-/v1/login/blerg', 487 | body: { 488 | oh: 'no', 489 | }, 490 | message: 'Invalid response from web login endpoint', 491 | } 492 | 493 | t.test('loginWeb', t => 494 | profile.loginWeb(opener, { registry }) 495 | .catch(er => t.match(er, expectedErr))) 496 | 497 | t.test('login with fallback', t => 498 | profile.login(opener, prompter, { registry }) 499 | .catch(er => t.match(er, expectedErr))) 500 | 501 | t.end() 502 | }) 503 | 504 | t.test('retry-after 202 response', t => { 505 | const registry = reg + '/retry-after/' 506 | 507 | const opener = () => new Promise(resolve => resolve()) 508 | const prompter = () => { 509 | throw new Error('should not do this') 510 | } 511 | 512 | const expect = { token: 'blerg' } 513 | 514 | t.test('loginWeb', t => 515 | t.resolveMatch(profile.loginWeb(opener, { registry }), expect)) 516 | t.test('login', t => 517 | t.resolveMatch(profile.login(opener, prompter, { registry }), expect)) 518 | t.end() 519 | }) 520 | 521 | t.test('no retry-after 202 response', t => { 522 | const registry = reg + '/retry-again/' 523 | 524 | const opener = () => new Promise(resolve => resolve()) 525 | const prompter = () => { 526 | throw new Error('should not do this') 527 | } 528 | 529 | const expect = { token: 'blerg' } 530 | 531 | t.test('loginWeb', t => 532 | t.resolveMatch(profile.loginWeb(opener, { registry }), expect)) 533 | t.test('login', t => 534 | t.resolveMatch(profile.login(opener, prompter, { registry }), expect)) 535 | t.end() 536 | }) 537 | 538 | t.test('opener error with long 202 response', t => { 539 | const registry = reg + '/retry-long-time/' 540 | 541 | const err = new Error('opener error') 542 | const throwOpener = async () => { 543 | await timers.setTimeout(100) 544 | throw err 545 | } 546 | const abortOpener = async () => { 547 | await timers.setTimeout(100, null, { signal: AbortSignal.timeout(10) }) 548 | } 549 | const prompter = () => { 550 | throw new Error('should not do this') 551 | } 552 | 553 | t.test('loginWeb', async t => { 554 | await t.rejects(profile.loginWeb(throwOpener, { registry }), err) 555 | await t.rejects(profile.loginWeb(abortOpener, { registry }), { name: 'AbortError' }) 556 | }) 557 | t.test('login', async t => { 558 | await t.rejects(profile.login(throwOpener, prompter, { registry }), err) 559 | await t.rejects(profile.login(abortOpener, prompter, { registry }), { name: 'AbortError' }) 560 | }) 561 | t.end() 562 | }) 563 | 564 | t.test('cleanup', t => { 565 | // NOTE: snapshot paths are not platform-independent 566 | process.platform !== 'win32' && 567 | t.matchSnapshot(JSON.stringify(requests, 0, 2), 'got expected requests') 568 | server.close() 569 | t.end() 570 | }) 571 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # npm-profile 2 | 3 | Provides functions for fetching and updating an npmjs.com profile. 4 | 5 | ```js 6 | const profile = require('npm-profile') 7 | const result = await profile.get({token}) 8 | //... 9 | ``` 10 | 11 | The API that this implements is documented here: 12 | 13 | * [authentication](https://github.com/npm/registry/blob/master/docs/user/authentication.md) 14 | * [profile editing](https://github.com/npm/registry/blob/master/docs/user/profile.md) (and two-factor authentication) 15 | 16 | ## Table of Contents 17 | 18 | * [API](#api) 19 | * Login and Account Creation 20 | * [`adduser()`](#adduser) 21 | * [`login()`](#login) 22 | * [`adduserWeb()`](#adduser-web) 23 | * [`loginWeb()`](#login-web) 24 | * [`adduserCouch()`](#adduser-couch) 25 | * [`loginCouch()`](#login-couch) 26 | * Profile Data Management 27 | * [`get()`](#get) 28 | * [`set()`](#set) 29 | * Token Management 30 | * [`listTokens()`](#list-tokens) 31 | * [`removeToken()`](#remove-token) 32 | * [`createToken()`](#create-token) 33 | 34 | ## API 35 | 36 | ### <a name="adduser"></a> `> profile.adduser(opener, prompter, [opts]) → Promise` 37 | 38 | Tries to create a user new web based login, if that fails it falls back to 39 | using the legacy CouchDB APIs. 40 | 41 | * `opener` Function (url) → Promise, returns a promise that resolves after a browser has been opened for the user at `url`. 42 | * `prompter` Function (creds) → Promise, returns a promise that resolves to an object with `username`, `email` and `password` properties. 43 | 44 | #### **Promise Value** 45 | 46 | An object with the following properties: 47 | 48 | * `token` String, to be used to authenticate further API calls 49 | * `username` String, the username the user authenticated as 50 | 51 | #### **Promise Rejection** 52 | 53 | An error object indicating what went wrong. 54 | 55 | The `headers` property will contain the HTTP headers of the response. 56 | 57 | If the action was denied because it came from an IP address that this action 58 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 59 | 60 | Otherwise the code will be `'E'` followed by the HTTP response code, for 61 | example a Forbidden response would be `E403`. 62 | 63 | ### <a name="login"></a> `> profile.login(opener, prompter, [opts]) → Promise` 64 | 65 | Tries to login using new web based login, if that fails it falls back to 66 | using the legacy CouchDB APIs. 67 | 68 | * `opener` Function (url) → Promise, returns a promise that resolves after a browser has been opened for the user at `url`. 69 | * `prompter` Function (creds) → Promise, returns a promise that resolves to an object with `username`, and `password` properties. 70 | 71 | #### **Promise Value** 72 | 73 | An object with the following properties: 74 | 75 | * `token` String, to be used to authenticate further API calls 76 | * `username` String, the username the user authenticated as 77 | 78 | #### **Promise Rejection** 79 | 80 | An error object indicating what went wrong. 81 | 82 | The `headers` property will contain the HTTP headers of the response. 83 | 84 | If the action was denied because an OTP is required then `code` will be set 85 | to `EOTP`. This error code can only come from a legacy CouchDB login and so 86 | this should be retried with loginCouch. 87 | 88 | If the action was denied because it came from an IP address that this action 89 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 90 | 91 | Otherwise the code will be `'E'` followed by the HTTP response code, for 92 | example a Forbidden response would be `E403`. 93 | 94 | ### <a name="adduser-web"></a> `> profile.adduserWeb(opener, [opts]) → Promise` 95 | 96 | Tries to create a user new web based login, if that fails it falls back to 97 | using the legacy CouchDB APIs. 98 | 99 | * `opener` Function (url) → Promise, returns a promise that resolves after a browser has been opened for the user at `url`. 100 | * [`opts`](#opts) Object 101 | 102 | #### **Promise Value** 103 | 104 | An object with the following properties: 105 | 106 | * `token` String, to be used to authenticate further API calls 107 | * `username` String, the username the user authenticated as 108 | 109 | #### **Promise Rejection** 110 | 111 | An error object indicating what went wrong. 112 | 113 | The `headers` property will contain the HTTP headers of the response. 114 | 115 | If the registry does not support web-login then an error will be thrown with 116 | its `code` property set to `ENYI` . You should retry with `adduserCouch`. 117 | If you use `adduser` then this fallback will be done automatically. 118 | 119 | If the action was denied because it came from an IP address that this action 120 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 121 | 122 | Otherwise the code will be `'E'` followed by the HTTP response code, for 123 | example a Forbidden response would be `E403`. 124 | 125 | ### <a name="login-web"></a> `> profile.loginWeb(opener, [opts]) → Promise` 126 | 127 | Tries to login using new web based login, if that fails it falls back to 128 | using the legacy CouchDB APIs. 129 | 130 | * `opener` Function (url) → Promise, returns a promise that resolves after a browser has been opened for the user at `url`. 131 | * [`opts`](#opts) Object (optional) 132 | 133 | #### **Promise Value** 134 | 135 | An object with the following properties: 136 | 137 | * `token` String, to be used to authenticate further API calls 138 | * `username` String, the username the user authenticated as 139 | 140 | #### **Promise Rejection** 141 | 142 | An error object indicating what went wrong. 143 | 144 | The `headers` property will contain the HTTP headers of the response. 145 | 146 | If the registry does not support web-login then an error will be thrown with 147 | its `code` property set to `ENYI` . You should retry with `loginCouch`. 148 | If you use `login` then this fallback will be done automatically. 149 | 150 | If the action was denied because it came from an IP address that this action 151 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 152 | 153 | Otherwise the code will be `'E'` followed by the HTTP response code, for 154 | example a Forbidden response would be `E403`. 155 | 156 | ### <a name="adduser-couch"></a> `> profile.adduserCouch(username, email, password, [opts]) → Promise` 157 | 158 | ```js 159 | const {token} = await profile.adduser(username, email, password, {registry}) 160 | // `token` can be passed in through `opts` for authentication. 161 | ``` 162 | 163 | Creates a new user on the server along with a fresh bearer token for future 164 | authentication as this user. This is what you see as an `authToken` in an 165 | `.npmrc`. 166 | 167 | If the user already exists then the npm registry will return an error, but 168 | this is registry specific and not guaranteed. 169 | 170 | * `username` String 171 | * `email` String 172 | * `password` String 173 | * [`opts`](#opts) Object (optional) 174 | 175 | #### **Promise Value** 176 | 177 | An object with the following properties: 178 | 179 | * `token` String, to be used to authenticate further API calls 180 | * `username` String, the username the user authenticated as 181 | 182 | #### **Promise Rejection** 183 | 184 | An error object indicating what went wrong. 185 | 186 | The `headers` property will contain the HTTP headers of the response. 187 | 188 | If the action was denied because an OTP is required then `code` will be set 189 | to `EOTP`. 190 | 191 | If the action was denied because it came from an IP address that this action 192 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 193 | 194 | Otherwise the code will be `'E'` followed by the HTTP response code, for 195 | example a Forbidden response would be `E403`. 196 | 197 | ### <a name="login-couch"></a> `> profile.loginCouch(username, password, [opts]) → Promise` 198 | 199 | ```js 200 | let token 201 | try { 202 | {token} = await profile.login(username, password, {registry}) 203 | } catch (err) { 204 | if (err.code === 'otp') { 205 | const otp = await getOTPFromSomewhere() 206 | {token} = await profile.login(username, password, {otp}) 207 | } 208 | } 209 | // `token` can now be passed in through `opts` for authentication. 210 | ``` 211 | 212 | Logs you into an existing user. Does not create the user if they do not 213 | already exist. Logging in means generating a new bearer token for use in 214 | future authentication. This is what you use as an `authToken` in an `.npmrc`. 215 | 216 | * `username` String 217 | * `email` String 218 | * `password` String 219 | * [`opts`](#opts) Object (optional) 220 | 221 | #### **Promise Value** 222 | 223 | An object with the following properties: 224 | 225 | * `token` String, to be used to authenticate further API calls 226 | * `username` String, the username the user authenticated as 227 | 228 | #### **Promise Rejection** 229 | 230 | An error object indicating what went wrong. 231 | 232 | If the object has a `code` property set to `EOTP` then that indicates that 233 | this account must use two-factor authentication to login. Try again with a 234 | one-time password. 235 | 236 | If the object has a `code` property set to `EAUTHIP` then that indicates that 237 | this account is only allowed to login from certain networks and this ip is 238 | not on one of those networks. 239 | 240 | If the error was neither of these then the error object will have a 241 | `code` property set to the HTTP response code and a `headers` property with 242 | the HTTP headers in the response. 243 | 244 | ### <a name="get"></a> `> profile.get([opts]) → Promise` 245 | 246 | ```js 247 | const {name, email} = await profile.get({token}) 248 | console.log(`${token} belongs to https://npm.im/~${name}, (mailto:${email})`) 249 | ``` 250 | 251 | Fetch profile information for the authenticated user. 252 | 253 | * [`opts`](#opts) Object 254 | 255 | #### **Promise Value** 256 | 257 | An object that looks like this: 258 | 259 | ```js 260 | // "*" indicates a field that may not always appear 261 | { 262 | tfa: null | 263 | false | 264 | {"mode": "auth-only", pending: Boolean} | 265 | ["recovery", "codes"] | 266 | "otpauth://...", 267 | name: String, 268 | email: String, 269 | email_verified: Boolean, 270 | created: Date, 271 | updated: Date, 272 | cidr_whitelist: null | ["192.168.1.1/32", ...], 273 | fullname: String, // * 274 | homepage: String, // * 275 | freenode: String, // * 276 | twitter: String, // * 277 | github: String // * 278 | } 279 | ``` 280 | 281 | #### **Promise Rejection** 282 | 283 | An error object indicating what went wrong. 284 | 285 | The `headers` property will contain the HTTP headers of the response. 286 | 287 | If the action was denied because an OTP is required then `code` will be set 288 | to `EOTP`. 289 | 290 | If the action was denied because it came from an IP address that this action 291 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 292 | 293 | Otherwise the code will be the HTTP response code. 294 | 295 | ### <a name="set"></a> `> profile.set(profileData, [opts]) → Promise` 296 | 297 | ```js 298 | await profile.set({github: 'great-github-account-name'}, {token}) 299 | ``` 300 | 301 | Update profile information for the authenticated user. 302 | 303 | * `profileData` An object, like that returned from `profile.get`, but see 304 | below for caveats relating to `password`, `tfa` and `cidr_whitelist`. 305 | * [`opts`](#opts) Object (optional) 306 | 307 | #### **SETTING `password`** 308 | 309 | This is used to change your password and is not visible (for obvious 310 | reasons) through the `get()` API. The value should be an object with `old` 311 | and `new` properties, where the former has the user's current password and 312 | the latter has the desired new password. For example 313 | 314 | ```js 315 | await profile.set({ 316 | password: { 317 | old: 'abc123', 318 | new: 'my new (more secure) password' 319 | } 320 | }, {token}) 321 | ``` 322 | 323 | #### **SETTING `cidr_whitelist`** 324 | 325 | The value for this is an Array. Only valid CIDR ranges are allowed in it. 326 | Be very careful as it's possible to lock yourself out of your account with 327 | this. This is not currently exposed in `npm` itself. 328 | 329 | ```js 330 | await profile.set({ 331 | cidr_whitelist: [ '8.8.8.8/32' ] 332 | }, {token}) 333 | // ↑ only one of google's dns servers can now access this account. 334 | ``` 335 | 336 | #### **SETTING `tfa`** 337 | 338 | Enabling two-factor authentication is a multi-step process. 339 | 340 | 1. Call `profile.get` and check the status of `tfa`. If `pending` is true then 341 | you'll need to disable it with `profile.set({tfa: {password, mode: 'disable'}, …)`. 342 | 2. `profile.set({tfa: {password, mode}}, {registry, token})` 343 | * Note that the user's `password` is required here in the `tfa` object, 344 | regardless of how you're authenticating. 345 | * `mode` is either `auth-only` which requires an `otp` when calling `login` 346 | or `createToken`, or `mode` is `auth-and-writes` and an `otp` will be 347 | required on login, publishing or when granting others access to your 348 | modules. 349 | * Be aware that this set call may require otp as part of the auth object. 350 | If otp is needed it will be indicated through a rejection in the usual 351 | way. 352 | 3. If tfa was already enabled then you're just switch modes and a 353 | successful response means that you're done. If the tfa property is empty 354 | and tfa _wasn't_ enabled then it means they were in a pending state. 355 | 3. The response will have a `tfa` property set to an `otpauth` URL, as 356 | [used by Google Authenticator](https://github.com/google/google-authenticator/wiki/Key-Uri-Format). 357 | You will need to show this to the user for them to add to their 358 | authenticator application. This is typically done as a QRCODE, but you 359 | can also show the value of the `secret` key in the `otpauth` query string 360 | and they can type or copy paste that in. 361 | 4. To complete setting up two factor auth you need to make a second call to 362 | `profile.set` with `tfa` set to an array of TWO codes from the user's 363 | authenticator, eg: `profile.set(tfa: [otp1, otp2]}, {registry, token})` 364 | 5. On success you'll get a result object with a `tfa` property that has an 365 | array of one-time-use recovery codes. These are used to authenticate 366 | later if the second factor is lost and generally should be printed and 367 | put somewhere safe. 368 | 369 | Disabling two-factor authentication is more straightforward, set the `tfa` 370 | attribute to an object with a `password` property and a `mode` of `disable`. 371 | 372 | ```js 373 | await profile.set({tfa: {password, mode: 'disable'}}, {token}) 374 | ``` 375 | 376 | #### **Promise Value** 377 | 378 | An object reflecting the changes you made, see description for `profile.get`. 379 | 380 | #### **Promise Rejection** 381 | 382 | An error object indicating what went wrong. 383 | 384 | The `headers` property will contain the HTTP headers of the response. 385 | 386 | If the action was denied because an OTP is required then `code` will be set 387 | to `EOTP`. 388 | 389 | If the action was denied because it came from an IP address that this action 390 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 391 | 392 | Otherwise the code will be the HTTP response code. 393 | 394 | ### <a name="list-tokens"></a> `> profile.listTokens([opts]) → Promise` 395 | 396 | ```js 397 | const tokens = await profile.listTokens({registry, token}) 398 | console.log(`Number of tokens in your accounts: ${tokens.length}`) 399 | ``` 400 | 401 | Fetch a list of all of the authentication tokens the authenticated user has. 402 | 403 | * [`opts`](#opts) Object (optional) 404 | 405 | #### **Promise Value** 406 | 407 | An array of token objects. Each token object has the following properties: 408 | 409 | * key — A sha512 that can be used to remove this token. 410 | * token — The first six characters of the token UUID. This should be used 411 | by the user to identify which token this is. 412 | * created — The date and time the token was created 413 | * readonly — If true, this token can only be used to download private modules. Critically, it CAN NOT be used to publish. 414 | * cidr_whitelist — An array of CIDR ranges that this token is allowed to be used from. 415 | 416 | #### **Promise Rejection** 417 | 418 | An error object indicating what went wrong. 419 | 420 | The `headers` property will contain the HTTP headers of the response. 421 | 422 | If the action was denied because an OTP is required then `code` will be set 423 | to `EOTP`. 424 | 425 | If the action was denied because it came from an IP address that this action 426 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 427 | 428 | Otherwise the code will be the HTTP response code. 429 | 430 | ### <a name="remove-token"><a> `> profile.removeToken(token|key, opts) → Promise` 431 | 432 | ```js 433 | await profile.removeToken(key, {token}) 434 | // token is gone! 435 | ``` 436 | 437 | Remove a specific authentication token. 438 | 439 | * `token|key` String, either a complete authentication token or the key returned by `profile.listTokens`. 440 | * [`opts`](#opts) Object (optional) 441 | 442 | #### **Promise Value** 443 | 444 | No value. 445 | 446 | #### **Promise Rejection** 447 | 448 | An error object indicating what went wrong. 449 | 450 | The `headers` property will contain the HTTP headers of the response. 451 | 452 | If the action was denied because an OTP is required then `code` will be set 453 | to `EOTP`. 454 | 455 | If the action was denied because it came from an IP address that this action 456 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 457 | 458 | Otherwise the code will be the HTTP response code. 459 | 460 | ### <a name="create-token"></a> `> profile.createToken(password, readonly, cidr_whitelist, [opts]) → Promise` 461 | 462 | ```js 463 | const newToken = await profile.createToken( 464 | password, readonly, cidr_whitelist, {token, otp} 465 | ) 466 | // do something with the newToken 467 | ``` 468 | 469 | Create a new authentication token, possibly with restrictions. 470 | 471 | * `password` String 472 | * `readonly` Boolean 473 | * `cidr_whitelist` Array 474 | * [`opts`](#opts) Object Optional 475 | 476 | #### **Promise Value** 477 | 478 | The promise will resolve with an object very much like the one's returned by 479 | `profile.listTokens`. The only difference is that `token` is not truncated. 480 | 481 | ```js 482 | { 483 | token: String, 484 | key: String, // sha512 hash of the token UUID 485 | cidr_whitelist: [String], 486 | created: Date, 487 | readonly: Boolean 488 | } 489 | ``` 490 | 491 | #### **Promise Rejection** 492 | 493 | An error object indicating what went wrong. 494 | 495 | The `headers` property will contain the HTTP headers of the response. 496 | 497 | If the action was denied because an OTP is required then `code` will be set 498 | to `EOTP`. 499 | 500 | If the action was denied because it came from an IP address that this action 501 | on this account isn't allowed from then the `code` will be set to `EAUTHIP`. 502 | 503 | Otherwise the code will be the HTTP response code. 504 | 505 | ### <a name="opts"></a> options objects 506 | 507 | The various API functions accept an optional `opts` object as a final 508 | argument. 509 | 510 | Options are passed to 511 | [`npm-registry-fetch` 512 | options](https://www.npmjs.com/package/npm-registry-fetch#fetch-opts), so 513 | anything provided to this module will affect the behavior of that one as 514 | well. 515 | 516 | Of particular note are `opts.registry`, and the auth-related options: 517 | 518 | * `opts.creds` Object, passed through to prompter, common values are: 519 | * `username` String, default value for username 520 | * `email` String, default value for email 521 | * `opts.username` and `opts.password` - used for Basic auth 522 | * `opts.otp` String, the two-factor-auth one-time-password (Will prompt for 523 | this if needed and not provided.) 524 | * `opts.hostname` String, the hostname of the current machine, to show the 525 | user during the WebAuth flow. (Defaults to `os.hostname()`.) 526 | 527 | ## <a name="logging"></a> Logging 528 | 529 | This modules logs by emitting `log` events on the global `process` object 530 | via [`proc-log`](https://www.npmjs.com/package/proc-log). 531 | These events look like this: 532 | 533 | ```js 534 | procLog[loglevel]('feature', 'message part 1', 'part 2', 'part 3', 'etc') 535 | ``` 536 | 537 | `loglevel` can be one of: `error`, `warn`, `notice`, `http`, `info`, `verbose`, and `silly`. 538 | 539 | `feature` is any brief string that describes the component doing the logging. 540 | 541 | The remaining arguments are evaluated like `console.log` and joined together with spaces. 542 | 543 | A real world example of this is: 544 | 545 | ```js 546 | procLog.http('request', '→', conf.method || 'GET', conf.target) 547 | ``` 548 | 549 | To handle the log events, you would do something like this: 550 | 551 | ```js 552 | process.on('log', (level, feature, ...args) => { 553 | console.log(level, feature, ...args) 554 | }) 555 | ``` 556 | --------------------------------------------------------------------------------