├── .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 |
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 | ### `> 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 | ### `> 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 | ### `> 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 | ### `> 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 | ### `> 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 | ### `> 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 | ### `> 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 | ### `> 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 | ### `> 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 | ### `> 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 | ### `> 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 | ### 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 | ## 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 |
--------------------------------------------------------------------------------