├── .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
├── util
│ └── tnock.js
├── json-stream.js
├── cache.js
├── check-response.js
├── stream.js
├── errors.js
├── index.js
└── auth.js
├── CODE_OF_CONDUCT.md
├── .commitlintrc.js
├── lib
├── default-opts.js
├── errors.js
├── check-response.js
├── auth.js
├── json-stream.js
└── index.js
├── .eslintrc.js
├── .gitignore
├── LICENSE.md
├── release-please-config.json
├── SECURITY.md
├── package.json
├── CONTRIBUTING.md
├── README.md
└── CHANGELOG.md
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "19.1.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/util/tnock.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const nock = require('nock')
4 | const clearMemoized = require('cacache').clearMemoized
5 |
6 | module.exports = tnock
7 | function tnock (t, host) {
8 | clearMemoized()
9 | const server = nock(host)
10 | t.teardown(function () {
11 | server.done()
12 | })
13 | return server
14 | }
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/default-opts.js:
--------------------------------------------------------------------------------
1 | const pkg = require('../package.json')
2 | module.exports = {
3 | maxSockets: 12,
4 | method: 'GET',
5 | registry: 'https://registry.npmjs.org/',
6 | timeout: 5 * 60 * 1000, // 5 minutes
7 | strictSSL: true,
8 | noProxy: process.env.NOPROXY,
9 | userAgent: `${pkg.name
10 | }@${
11 | pkg.version
12 | }/node@${
13 | process.version
14 | }+${
15 | process.arch
16 | } (${
17 | process.platform
18 | })`,
19 | }
20 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/test/json-stream.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 | const { JSONStreamError, parse } = require('../lib/json-stream.js')
3 |
4 | t.test('JSONStream', (t) => {
5 | t.test('JSONStreamError constructor', (t) => {
6 | const error = new JSONStreamError(new Error('error'))
7 | t.equal(error.message, 'error')
8 | t.equal(error.name, 'JSONStreamError')
9 | t.end()
10 | })
11 |
12 | t.test('JSONStream.write', (t) => {
13 | t.test('JSONStream write error from numerical (not string not buffer)', async (t) => {
14 | const stream = parse('*', {})
15 | try {
16 | stream.write(5)
17 | } catch (error) {
18 | t.equal(error.message, 'Can only parse JSON from string or buffer input')
19 | t.equal(error.name, 'TypeError')
20 | }
21 | t.end()
22 | })
23 |
24 | t.end()
25 | })
26 |
27 | t.test('JSONStream.end', (t) => {
28 | t.test(
29 | 'JSONStream end invalid chunk throws JSONStreamError from parser',
30 | (t) => {
31 | const stream = parse('*', {})
32 | try {
33 | stream.end('not a valid chunk')
34 | } catch (error) {
35 | t.equal(error.name, 'JSONStreamError')
36 | t.equal(error.message, 'Unexpected "o" at position 1 in state STOP')
37 | }
38 | t.end()
39 | }
40 | )
41 |
42 | t.end()
43 | })
44 |
45 | t.end()
46 | })
47 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "npm-registry-fetch",
3 | "version": "19.1.1",
4 | "description": "Fetch-based http client for use with npm registry APIs",
5 | "main": "lib",
6 | "files": [
7 | "bin/",
8 | "lib/"
9 | ],
10 | "scripts": {
11 | "eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"",
12 | "lint": "npm run eslint",
13 | "lintfix": "npm run eslint -- --fix",
14 | "test": "tap",
15 | "posttest": "npm run lint",
16 | "npmclilint": "npmcli-lint",
17 | "postsnap": "npm run lintfix --",
18 | "postlint": "template-oss-check",
19 | "snap": "tap",
20 | "template-oss-apply": "template-oss-apply --force"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/npm/npm-registry-fetch.git"
25 | },
26 | "keywords": [
27 | "npm",
28 | "registry",
29 | "fetch"
30 | ],
31 | "author": "GitHub Inc.",
32 | "license": "ISC",
33 | "dependencies": {
34 | "@npmcli/redact": "^4.0.0",
35 | "jsonparse": "^1.3.1",
36 | "make-fetch-happen": "^15.0.0",
37 | "minipass": "^7.0.2",
38 | "minipass-fetch": "^5.0.0",
39 | "minizlib": "^3.0.1",
40 | "npm-package-arg": "^13.0.0",
41 | "proc-log": "^6.0.0"
42 | },
43 | "devDependencies": {
44 | "@npmcli/eslint-config": "^6.0.0",
45 | "@npmcli/template-oss": "4.28.0",
46 | "cacache": "^20.0.0",
47 | "nock": "^13.2.4",
48 | "require-inject": "^1.4.4",
49 | "ssri": "^13.0.0",
50 | "tap": "^16.0.1"
51 | },
52 | "tap": {
53 | "check-coverage": true,
54 | "test-ignore": "test[\\\\/](util|cache)[\\\\/]",
55 | "nyc-arg": [
56 | "--exclude",
57 | "tap-snapshots/**"
58 | ]
59 | },
60 | "engines": {
61 | "node": "^20.17.0 || >=22.9.0"
62 | },
63 | "templateOSS": {
64 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
65 | "version": "4.28.0",
66 | "publish": "true"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/lib/errors.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { URL } = require('node:url')
4 |
5 | function packageName (href) {
6 | try {
7 | let basePath = new URL(href).pathname.slice(1)
8 | if (!basePath.match(/^-/)) {
9 | basePath = basePath.split('/')
10 | var index = basePath.indexOf('_rewrite')
11 | if (index === -1) {
12 | index = basePath.length - 1
13 | } else {
14 | index++
15 | }
16 | return decodeURIComponent(basePath[index])
17 | }
18 | } catch {
19 | // this is ok
20 | }
21 | }
22 |
23 | class HttpErrorBase extends Error {
24 | constructor (method, res, body, spec) {
25 | super()
26 | this.name = this.constructor.name
27 | this.headers = typeof res.headers?.raw === 'function' ? res.headers.raw() : res.headers
28 | this.statusCode = res.status
29 | this.code = `E${res.status}`
30 | this.method = method
31 | this.uri = res.url
32 | this.body = body
33 | this.pkgid = spec ? spec.toString() : packageName(res.url)
34 | Error.captureStackTrace(this, this.constructor)
35 | }
36 | }
37 |
38 | class HttpErrorGeneral extends HttpErrorBase {
39 | constructor (method, res, body, spec) {
40 | super(method, res, body, spec)
41 | this.message = `${res.status} ${res.statusText} - ${
42 | this.method.toUpperCase()
43 | } ${
44 | this.spec || this.uri
45 | }${
46 | (body && body.error) ? ' - ' + body.error : ''
47 | }`
48 | }
49 | }
50 |
51 | class HttpErrorAuthOTP extends HttpErrorBase {
52 | constructor (method, res, body, spec) {
53 | super(method, res, body, spec)
54 | this.message = 'OTP required for authentication'
55 | this.code = 'EOTP'
56 | }
57 | }
58 |
59 | class HttpErrorAuthIPAddress extends HttpErrorBase {
60 | constructor (method, res, body, spec) {
61 | super(method, res, body, spec)
62 | this.message = 'Login is not allowed from your IP address'
63 | this.code = 'EAUTHIP'
64 | }
65 | }
66 |
67 | class HttpErrorAuthUnknown extends HttpErrorBase {
68 | constructor (method, res, body, spec) {
69 | super(method, res, body, spec)
70 | this.message = 'Unable to authenticate, need: ' + res.headers.get('www-authenticate')
71 | }
72 | }
73 |
74 | module.exports = {
75 | HttpErrorBase,
76 | HttpErrorGeneral,
77 | HttpErrorAuthOTP,
78 | HttpErrorAuthIPAddress,
79 | HttpErrorAuthUnknown,
80 | }
81 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/cache.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { promisify } = require('util')
4 | const statAsync = promisify(require('fs').stat)
5 | const path = require('path')
6 | const t = require('tap')
7 | const tnock = require('./util/tnock.js')
8 |
9 | const fetch = require('..')
10 |
11 | const testDir = t.testdir({})
12 |
13 | const REGISTRY = 'https://mock.reg'
14 | const OPTS = {
15 | memoize: false,
16 | timeout: 0,
17 | retry: {
18 | retries: 1,
19 | factor: 1,
20 | minTimeout: 1,
21 | maxTimeout: 10,
22 | },
23 | cache: path.join(testDir, '_cacache'),
24 | registry: REGISTRY,
25 | }
26 |
27 | t.test('can cache GET requests', t => {
28 | tnock(t, REGISTRY)
29 | .get('/normal')
30 | .times(1)
31 | .reply(200, { obj: 'value' })
32 | return fetch.json('/normal', OPTS)
33 | .then(val => t.same(val, { obj: 'value' }, 'got expected response'))
34 | .then(() => statAsync(OPTS.cache))
35 | .then(stat => t.ok(stat.isDirectory(), 'cache directory created'))
36 | .then(() => fetch.json('/normal', OPTS))
37 | .then(val => t.same(val, { obj: 'value' }, 'response was cached'))
38 | })
39 |
40 | t.test('preferOffline', t => {
41 | tnock(t, REGISTRY)
42 | .get('/preferOffline')
43 | .times(1)
44 | .reply(200, { obj: 'value' })
45 | return fetch.json('/preferOffline', { ...OPTS, preferOffline: true })
46 | .then(val => t.same(val, { obj: 'value' }, 'got expected response'))
47 | .then(() => statAsync(OPTS.cache))
48 | .then(stat => t.ok(stat.isDirectory(), 'cache directory created'))
49 | .then(() => fetch.json('/preferOffline', { ...OPTS, preferOffline: true }))
50 | .then(val => t.same(val, { obj: 'value' }, 'response was cached'))
51 | })
52 |
53 | t.test('offline', t => {
54 | tnock(t, REGISTRY)
55 | .get('/offline')
56 | .times(1)
57 | .reply(200, { obj: 'value' })
58 | return fetch.json('/offline', OPTS)
59 | .then(val => t.same(val, { obj: 'value' }, 'got expected response'))
60 | .then(() => statAsync(OPTS.cache))
61 | .then(stat => t.ok(stat.isDirectory(), 'cache directory created'))
62 | .then(() => fetch.json('/offline', { ...OPTS, offline: true }))
63 | .then(val => t.same(val, { obj: 'value' }, 'response was cached'))
64 | })
65 |
66 | t.test('offline fails if not cached', t =>
67 | t.rejects(() => fetch('/offline-fails', { ...OPTS, offline: true })))
68 |
69 | t.test('preferOnline', t => {
70 | tnock(t, REGISTRY)
71 | .get('/preferOnline')
72 | .times(2)
73 | .reply(200, { obj: 'value' })
74 | return fetch.json('/preferOnline', OPTS)
75 | .then(val => t.same(val, { obj: 'value' }, 'got expected response'))
76 | .then(() => statAsync(OPTS.cache))
77 | .then(stat => t.ok(stat.isDirectory(), 'cache directory created'))
78 | .then(() => fetch.json('/preferOnline', { ...OPTS, preferOnline: true }))
79 | .then(val => t.same(val, { obj: 'value' }, 'response was refetched'))
80 | })
81 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/lib/check-response.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const errors = require('./errors.js')
4 | const { Response } = require('minipass-fetch')
5 | const defaultOpts = require('./default-opts.js')
6 | const { log } = require('proc-log')
7 | const { redact: cleanUrl } = require('@npmcli/redact')
8 |
9 | /* eslint-disable-next-line max-len */
10 | const moreInfoUrl = 'https://github.com/npm/cli/wiki/No-auth-for-URI,-but-auth-present-for-scoped-registry'
11 | const checkResponse =
12 | async ({ method, uri, res, startTime, auth, opts }) => {
13 | opts = { ...defaultOpts, ...opts }
14 | if (res.headers.has('npm-notice') && !res.headers.has('x-local-cache')) {
15 | log.notice('', res.headers.get('npm-notice'))
16 | }
17 |
18 | if (res.status >= 400) {
19 | logRequest(method, res, startTime)
20 | if (auth && auth.scopeAuthKey && !auth.token && !auth.auth) {
21 | // we didn't have auth for THIS request, but we do have auth for
22 | // requests to the registry indicated by the spec's scope value.
23 | // Warn the user.
24 | log.warn('registry', `No auth for URI, but auth present for scoped registry.
25 |
26 | URI: ${uri}
27 | Scoped Registry Key: ${auth.scopeAuthKey}
28 |
29 | More info here: ${moreInfoUrl}`)
30 | }
31 | return checkErrors(method, res, startTime, opts)
32 | } else {
33 | res.body.on('end', () => logRequest(method, res, startTime, opts))
34 | if (opts.ignoreBody) {
35 | res.body.resume()
36 | return new Response(null, res)
37 | }
38 | return res
39 | }
40 | }
41 | module.exports = checkResponse
42 |
43 | function logRequest (method, res, startTime) {
44 | const elapsedTime = Date.now() - startTime
45 | const attempt = res.headers.get('x-fetch-attempts')
46 | const attemptStr = attempt && attempt > 1 ? ` attempt #${attempt}` : ''
47 | const cacheStatus = res.headers.get('x-local-cache-status')
48 | const cacheStr = cacheStatus ? ` (cache ${cacheStatus})` : ''
49 | const urlStr = cleanUrl(res.url)
50 |
51 | // If make-fetch-happen reports a cache hit, then there was no fetch
52 | if (cacheStatus === 'hit') {
53 | log.http(
54 | 'cache',
55 | `${urlStr} ${elapsedTime}ms${attemptStr}${cacheStr}`
56 | )
57 | } else {
58 | log.http(
59 | 'fetch',
60 | `${method.toUpperCase()} ${res.status} ${urlStr} ${elapsedTime}ms${attemptStr}${cacheStr}`
61 | )
62 | }
63 | }
64 |
65 | function checkErrors (method, res, startTime, opts) {
66 | return res.buffer()
67 | .catch(() => null)
68 | .then(body => {
69 | let parsed = body
70 | try {
71 | parsed = JSON.parse(body.toString('utf8'))
72 | } catch {
73 | // ignore errors
74 | }
75 | if (res.status === 401 && res.headers.get('www-authenticate')) {
76 | const auth = res.headers.get('www-authenticate')
77 | .split(/,\s*/)
78 | .map(s => s.toLowerCase())
79 | if (auth.indexOf('ipaddress') !== -1) {
80 | throw new errors.HttpErrorAuthIPAddress(
81 | method, res, parsed, opts.spec
82 | )
83 | } else if (auth.indexOf('otp') !== -1) {
84 | throw new errors.HttpErrorAuthOTP(
85 | method, res, parsed, opts.spec
86 | )
87 | } else {
88 | throw new errors.HttpErrorAuthUnknown(
89 | method, res, parsed, opts.spec
90 | )
91 | }
92 | } else if (
93 | res.status === 401 &&
94 | body != null &&
95 | /one-time pass/.test(body.toString('utf8'))
96 | ) {
97 | // Heuristic for malformed OTP responses that don't include the
98 | // www-authenticate header.
99 | throw new errors.HttpErrorAuthOTP(
100 | method, res, parsed, opts.spec
101 | )
102 | } else {
103 | throw new errors.HttpErrorGeneral(
104 | method, res, parsed, opts.spec
105 | )
106 | }
107 | })
108 | }
109 |
--------------------------------------------------------------------------------
/.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/auth.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const fs = require('fs')
3 | const npa = require('npm-package-arg')
4 | const { URL } = require('url')
5 |
6 | // Find the longest registry key that is used for some kind of auth
7 | // in the options. Returns the registry key and the auth config.
8 | const regFromURI = (uri, opts) => {
9 | const parsed = new URL(uri)
10 | // try to find a config key indicating we have auth for this registry
11 | // can be one of :_authToken, :_auth, :_password and :username, or
12 | // :certfile and :keyfile
13 | // We walk up the "path" until we're left with just //[:],
14 | // stopping when we reach '//'.
15 | let regKey = `//${parsed.host}${parsed.pathname}`
16 | while (regKey.length > '//'.length) {
17 | const authKey = hasAuth(regKey, opts)
18 | // got some auth for this URI
19 | if (authKey) {
20 | return { regKey, authKey }
21 | }
22 |
23 | // can be either //host/some/path/:_auth or //host/some/path:_auth
24 | // walk up by removing EITHER what's after the slash OR the slash itself
25 | regKey = regKey.replace(/([^/]+|\/)$/, '')
26 | }
27 | return { regKey: false, authKey: null }
28 | }
29 |
30 | // Not only do we want to know if there is auth, but if we are calling `npm
31 | // logout` we want to know what config value specifically provided it. This is
32 | // so we can look up where the config came from to delete it (i.e. user vs
33 | // project)
34 | const hasAuth = (regKey, opts) => {
35 | if (opts[`${regKey}:_authToken`]) {
36 | return '_authToken'
37 | }
38 | if (opts[`${regKey}:_auth`]) {
39 | return '_auth'
40 | }
41 | if (opts[`${regKey}:username`] && opts[`${regKey}:_password`]) {
42 | // 'password' can be inferred to also be present
43 | return 'username'
44 | }
45 | if (opts[`${regKey}:certfile`] && opts[`${regKey}:keyfile`]) {
46 | // 'keyfile' can be inferred to also be present
47 | return 'certfile'
48 | }
49 | return false
50 | }
51 |
52 | const sameHost = (a, b) => {
53 | const parsedA = new URL(a)
54 | const parsedB = new URL(b)
55 | return parsedA.host === parsedB.host
56 | }
57 |
58 | const getRegistry = opts => {
59 | const { spec } = opts
60 | const { scope: specScope, subSpec } = spec ? npa(spec) : {}
61 | const subSpecScope = subSpec && subSpec.scope
62 | const scope = subSpec ? subSpecScope : specScope
63 | const scopeReg = scope && opts[`${scope}:registry`]
64 | return scopeReg || opts.registry
65 | }
66 |
67 | const maybeReadFile = file => {
68 | try {
69 | return fs.readFileSync(file, 'utf8')
70 | } catch (er) {
71 | if (er.code !== 'ENOENT') {
72 | throw er
73 | }
74 | return null
75 | }
76 | }
77 |
78 | const getAuth = (uri, opts = {}) => {
79 | const { forceAuth } = opts
80 | if (!uri) {
81 | throw new Error('URI is required')
82 | }
83 | const { regKey, authKey } = regFromURI(uri, forceAuth || opts)
84 |
85 | // we are only allowed to use what's in forceAuth if specified
86 | if (forceAuth && !regKey) {
87 | return new Auth({
88 | // if we force auth we don't want to refer back to anything in config
89 | regKey: false,
90 | authKey: null,
91 | scopeAuthKey: null,
92 | token: forceAuth._authToken || forceAuth.token,
93 | username: forceAuth.username,
94 | password: forceAuth._password || forceAuth.password,
95 | auth: forceAuth._auth || forceAuth.auth,
96 | certfile: forceAuth.certfile,
97 | keyfile: forceAuth.keyfile,
98 | })
99 | }
100 |
101 | // no auth for this URI, but might have it for the registry
102 | if (!regKey) {
103 | const registry = getRegistry(opts)
104 | if (registry && uri !== registry && sameHost(uri, registry)) {
105 | return getAuth(registry, opts)
106 | } else if (registry !== opts.registry) {
107 | // If making a tarball request to a different base URI than the
108 | // registry where we logged in, but the same auth SHOULD be sent
109 | // to that artifact host, then we track where it was coming in from,
110 | // and warn the user if we get a 4xx error on it.
111 | const { regKey: scopeAuthKey, authKey: _authKey } = regFromURI(registry, opts)
112 | return new Auth({ scopeAuthKey, regKey: scopeAuthKey, authKey: _authKey })
113 | }
114 | }
115 |
116 | const {
117 | [`${regKey}:_authToken`]: token,
118 | [`${regKey}:username`]: username,
119 | [`${regKey}:_password`]: password,
120 | [`${regKey}:_auth`]: auth,
121 | [`${regKey}:certfile`]: certfile,
122 | [`${regKey}:keyfile`]: keyfile,
123 | } = opts
124 |
125 | return new Auth({
126 | scopeAuthKey: null,
127 | regKey,
128 | authKey,
129 | token,
130 | auth,
131 | username,
132 | password,
133 | certfile,
134 | keyfile,
135 | })
136 | }
137 |
138 | class Auth {
139 | constructor ({
140 | token,
141 | auth,
142 | username,
143 | password,
144 | scopeAuthKey,
145 | certfile,
146 | keyfile,
147 | regKey,
148 | authKey,
149 | }) {
150 | // same as regKey but only present for scoped auth. Should have been named scopeRegKey
151 | this.scopeAuthKey = scopeAuthKey
152 | // `${regKey}:${authKey}` will get you back to the auth config that gave us auth
153 | this.regKey = regKey
154 | this.authKey = authKey
155 | this.token = null
156 | this.auth = null
157 | this.isBasicAuth = false
158 | this.cert = null
159 | this.key = null
160 | if (token) {
161 | this.token = token
162 | } else if (auth) {
163 | this.auth = auth
164 | } else if (username && password) {
165 | const p = Buffer.from(password, 'base64').toString('utf8')
166 | this.auth = Buffer.from(`${username}:${p}`, 'utf8').toString('base64')
167 | this.isBasicAuth = true
168 | }
169 | // mTLS may be used in conjunction with another auth method above
170 | if (certfile && keyfile) {
171 | const cert = maybeReadFile(certfile, 'utf-8')
172 | const key = maybeReadFile(keyfile, 'utf-8')
173 | if (cert && key) {
174 | this.cert = cert
175 | this.key = key
176 | }
177 | }
178 | }
179 | }
180 |
181 | module.exports = getAuth
182 |
--------------------------------------------------------------------------------
/lib/json-stream.js:
--------------------------------------------------------------------------------
1 | const Parser = require('jsonparse')
2 | const { Minipass } = require('minipass')
3 |
4 | class JSONStreamError extends Error {
5 | constructor (err, caller) {
6 | super(err.message)
7 | Error.captureStackTrace(this, caller || this.constructor)
8 | }
9 |
10 | get name () {
11 | return 'JSONStreamError'
12 | }
13 | }
14 |
15 | const check = (x, y) =>
16 | typeof x === 'string' ? String(y) === x
17 | : x && typeof x.test === 'function' ? x.test(y)
18 | : typeof x === 'boolean' || typeof x === 'object' ? x
19 | : typeof x === 'function' ? x(y)
20 | : false
21 |
22 | class JSONStream extends Minipass {
23 | #count = 0
24 | #ending = false
25 | #footer = null
26 | #header = null
27 | #map = null
28 | #onTokenOriginal
29 | #parser
30 | #path = null
31 | #root = null
32 |
33 | constructor (opts) {
34 | super({
35 | ...opts,
36 | objectMode: true,
37 | })
38 |
39 | const parser = this.#parser = new Parser()
40 | parser.onValue = value => this.#onValue(value)
41 | this.#onTokenOriginal = parser.onToken
42 | parser.onToken = (token, value) => this.#onToken(token, value)
43 | parser.onError = er => this.#onError(er)
44 |
45 | this.#path = typeof opts.path === 'string'
46 | ? opts.path.split('.').map(e =>
47 | e === '$*' ? { emitKey: true }
48 | : e === '*' ? true
49 | : e === '' ? { recurse: true }
50 | : e)
51 | : Array.isArray(opts.path) && opts.path.length ? opts.path
52 | : null
53 |
54 | if (typeof opts.map === 'function') {
55 | this.#map = opts.map
56 | }
57 | }
58 |
59 | #setHeaderFooter (key, value) {
60 | // header has not been emitted yet
61 | if (this.#header !== false) {
62 | this.#header = this.#header || {}
63 | this.#header[key] = value
64 | }
65 |
66 | // footer has not been emitted yet but header has
67 | if (this.#footer !== false && this.#header === false) {
68 | this.#footer = this.#footer || {}
69 | this.#footer[key] = value
70 | }
71 | }
72 |
73 | #onError (er) {
74 | // error will always happen during a write() call.
75 | const caller = this.#ending ? this.end : this.write
76 | this.#ending = false
77 | return this.emit('error', new JSONStreamError(er, caller))
78 | }
79 |
80 | #onToken (token, value) {
81 | const parser = this.#parser
82 | this.#onTokenOriginal.call(this.#parser, token, value)
83 | if (parser.stack.length === 0) {
84 | if (this.#root) {
85 | const root = this.#root
86 | if (!this.#path) {
87 | super.write(root)
88 | }
89 | this.#root = null
90 | this.#count = 0
91 | }
92 | }
93 | }
94 |
95 | #onValue (value) {
96 | const parser = this.#parser
97 | // the LAST onValue encountered is the root object.
98 | // just overwrite it each time.
99 | this.#root = value
100 |
101 | if (!this.#path) {
102 | return
103 | }
104 |
105 | let i = 0 // iterates on path
106 | let j = 0 // iterates on stack
107 | let emitKey = false
108 | while (i < this.#path.length) {
109 | const key = this.#path[i]
110 | j++
111 |
112 | if (key && !key.recurse) {
113 | const c = (j === parser.stack.length) ? parser : parser.stack[j]
114 | if (!c) {
115 | return
116 | }
117 | if (!check(key, c.key)) {
118 | this.#setHeaderFooter(c.key, value)
119 | return
120 | }
121 | emitKey = !!key.emitKey
122 | i++
123 | } else {
124 | i++
125 | if (i >= this.#path.length) {
126 | return
127 | }
128 | const nextKey = this.#path[i]
129 | if (!nextKey) {
130 | return
131 | }
132 | while (true) {
133 | const c = (j === parser.stack.length) ? parser : parser.stack[j]
134 | if (!c) {
135 | return
136 | }
137 | if (check(nextKey, c.key)) {
138 | i++
139 | if (!Object.isFrozen(parser.stack[j])) {
140 | parser.stack[j].value = null
141 | }
142 | break
143 | } else {
144 | this.#setHeaderFooter(c.key, value)
145 | }
146 | j++
147 | }
148 | }
149 | }
150 |
151 | // emit header
152 | if (this.#header) {
153 | const header = this.#header
154 | this.#header = false
155 | this.emit('header', header)
156 | }
157 | if (j !== parser.stack.length) {
158 | return
159 | }
160 |
161 | this.#count++
162 | const actualPath = parser.stack.slice(1)
163 | .map(e => e.key).concat([parser.key])
164 | if (value !== null && value !== undefined) {
165 | const data = this.#map ? this.#map(value, actualPath) : value
166 | if (data !== null && data !== undefined) {
167 | const emit = emitKey ? { value: data } : data
168 | if (emitKey) {
169 | emit.key = parser.key
170 | }
171 | super.write(emit)
172 | }
173 | }
174 |
175 | if (parser.value) {
176 | delete parser.value[parser.key]
177 | }
178 |
179 | for (const k of parser.stack) {
180 | k.value = null
181 | }
182 | }
183 |
184 | write (chunk, encoding) {
185 | if (typeof chunk === 'string') {
186 | chunk = Buffer.from(chunk, encoding)
187 | } else if (!Buffer.isBuffer(chunk)) {
188 | return this.emit('error', new TypeError(
189 | 'Can only parse JSON from string or buffer input'))
190 | }
191 | this.#parser.write(chunk)
192 | return this.flowing
193 | }
194 |
195 | end (chunk, encoding) {
196 | this.#ending = true
197 | if (chunk) {
198 | this.write(chunk, encoding)
199 | }
200 |
201 | const h = this.#header
202 | this.#header = null
203 | const f = this.#footer
204 | this.#footer = null
205 | if (h) {
206 | this.emit('header', h)
207 | }
208 | if (f) {
209 | this.emit('footer', f)
210 | }
211 | return super.end()
212 | }
213 |
214 | static get JSONStreamError () {
215 | return JSONStreamError
216 | }
217 |
218 | static parse (path, map) {
219 | return new JSONStream({ path, map })
220 | }
221 | }
222 |
223 | module.exports = JSONStream
224 |
--------------------------------------------------------------------------------
/test/check-response.js:
--------------------------------------------------------------------------------
1 | const { Readable } = require('stream')
2 | const t = require('tap')
3 |
4 | const checkResponse = require('../lib/check-response.js')
5 | const errors = require('../lib/errors.js')
6 | const registry = 'registry'
7 | const startTime = Date.now()
8 |
9 | class Body extends Readable {
10 | _read () {
11 | return ''
12 | }
13 | }
14 | class Headers {
15 | has () {}
16 | get () {}
17 | raw () {}
18 | }
19 | const mockFetchRes = {
20 | body: new Body(),
21 | buffer: () => Promise.resolve(),
22 | headers: new Headers(),
23 | status: 200,
24 | }
25 |
26 | t.test('any response error should be silent', t => {
27 | const res = Object.assign({}, mockFetchRes, {
28 | buffer: () => Promise.reject(new Error('ERR')),
29 | status: 400,
30 | url: 'https://example.com/',
31 | })
32 |
33 | t.rejects(checkResponse({
34 | method: 'get',
35 | res,
36 | registry,
37 | startTime,
38 | opts: { ignoreBody: true },
39 | }), errors.HttpErrorGeneral)
40 | t.end()
41 | })
42 |
43 | t.test('all checks are ok, nothing to report', t => {
44 | const res = Object.assign({}, mockFetchRes, {
45 | buffer: () => Promise.resolve(Buffer.from('ok')),
46 | status: 400,
47 | url: 'https://example.com/',
48 | })
49 | t.rejects(checkResponse({
50 | method: 'get',
51 | res,
52 | registry,
53 | startTime,
54 | }), errors.HttpErrorGeneral)
55 | t.end()
56 | })
57 |
58 | t.test('log x-fetch-attempts header value', async t => {
59 | const headers = new Headers()
60 | headers.get = header => header === 'x-fetch-attempts' ? 3 : undefined
61 | const res = Object.assign({}, mockFetchRes, {
62 | headers,
63 | status: 400,
64 | })
65 | t.plan(2)
66 | let msg
67 | process.on('log', (level, ...args) => {
68 | if (level === 'http') {
69 | [, msg] = args
70 | }
71 | })
72 | await t.rejects(checkResponse({
73 | method: 'get',
74 | res,
75 | registry,
76 | startTime,
77 | }))
78 | t.ok(msg.endsWith('attempt #3'), 'should log correct number of attempts')
79 | })
80 |
81 | t.test('log the url fetched', t => {
82 | const headers = new Headers()
83 | const EE = require('events')
84 | headers.get = () => undefined
85 | const res = Object.assign({}, mockFetchRes, {
86 | headers,
87 | status: 200,
88 | url: 'http://example.com/foo/bar/baz',
89 | body: new EE(),
90 | })
91 | t.plan(2)
92 | let header, msg
93 | process.on('log', (level, ...args) => {
94 | if (level === 'http') {
95 | [header, msg] = args
96 | }
97 | })
98 | checkResponse({
99 | method: 'get',
100 | res,
101 | registry,
102 | startTime,
103 |
104 | })
105 | res.body.emit('end')
106 | t.equal(header, 'fetch')
107 | t.match(msg, /^GET 200 http:\/\/example.com\/foo\/bar\/baz [0-9]+m?s/)
108 | })
109 |
110 | t.test('redact password from log', t => {
111 | const headers = new Headers()
112 | const EE = require('events')
113 | headers.get = () => undefined
114 | const res = Object.assign({}, mockFetchRes, {
115 | headers,
116 | status: 200,
117 | url: 'http://username:password@example.com/foo/bar/baz',
118 | body: new EE(),
119 | })
120 | t.plan(2)
121 | let header, msg
122 | process.on('log', (level, ...args) => {
123 | if (level === 'http') {
124 | [header, msg] = args
125 | }
126 | })
127 | checkResponse({
128 | method: 'get',
129 | res,
130 | registry,
131 | startTime,
132 | })
133 | res.body.emit('end')
134 | t.equal(header, 'fetch')
135 | t.match(msg, /^GET 200 http:\/\/username:\*\*\*@example.com\/foo\/bar\/baz [0-9]+m?s/)
136 | })
137 |
138 | t.test('redact well known token from log', t => {
139 | const headers = new Headers()
140 | const EE = require('events')
141 | headers.get = () => undefined
142 | const res = Object.assign({}, mockFetchRes, {
143 | headers,
144 | status: 200,
145 | url: `http://example.com/foo/bar/baz/npm_${'a'.repeat(36)}`,
146 | body: new EE(),
147 | })
148 | t.plan(2)
149 | let header, msg
150 | process.on('log', (level, ...args) => {
151 | if (level === 'http') {
152 | [header, msg] = args
153 | }
154 | })
155 | checkResponse({
156 | method: 'get',
157 | res,
158 | registry,
159 | startTime,
160 | })
161 | res.body.emit('end')
162 | t.equal(header, 'fetch')
163 | t.match(msg, /^GET 200 http:\/\/example.com\/foo\/bar\/baz\/npm_\*\*\* [0-9]+m?s/)
164 | })
165 |
166 | /* eslint-disable-next-line max-len */
167 | const moreInfoUrl = 'https://github.com/npm/cli/wiki/No-auth-for-URI,-but-auth-present-for-scoped-registry'
168 |
169 | t.test('report auth for registry, but not for this request', async t => {
170 | const res = Object.assign({}, mockFetchRes, {
171 | buffer: () => Promise.resolve(Buffer.from('ok')),
172 | status: 400,
173 | url: 'https://example.com/',
174 | })
175 | t.plan(3)
176 | let header, msg
177 | process.on('log', (level, ...args) => {
178 | if (level === 'warn') {
179 | [header, msg] = args
180 | }
181 | })
182 | await t.rejects(checkResponse({
183 | method: 'get',
184 | res,
185 | uri: 'https://example.com/',
186 | registry,
187 | startTime,
188 | auth: {
189 | scopeAuthKey: '//some-scope-registry.com/',
190 | auth: null,
191 | token: null,
192 | },
193 | }), errors.HttpErrorGeneral)
194 | t.equal(header, 'registry')
195 | t.equal(msg, `No auth for URI, but auth present for scoped registry.
196 |
197 | URI: https://example.com/
198 | Scoped Registry Key: //some-scope-registry.com/
199 |
200 | More info here: ${moreInfoUrl}`)
201 | })
202 |
203 | t.test('logs the value of x-local-cache-status when set', t => {
204 | const headers = new Headers()
205 | const EE = require('events')
206 | headers.get = header => header === 'x-local-cache-status' ? 'hit' : undefined
207 | const res = Object.assign({}, mockFetchRes, {
208 | headers,
209 | status: 200,
210 | url: 'http://username:password@example.com/foo/bar/baz',
211 | body: new EE(),
212 | })
213 | t.plan(2)
214 | let header, msg
215 | process.on('log', (level, ...args) => {
216 | if (level === 'http') {
217 | [header, msg] = args
218 | }
219 | })
220 | checkResponse({
221 | method: 'get',
222 | res,
223 | registry,
224 | startTime,
225 | })
226 | res.body.emit('end')
227 | t.equal(header, 'cache')
228 | t.match(
229 | msg,
230 | /^http:\/\/username:\*\*\*@example.com\/foo\/bar\/baz [0-9]+m?s \(cache hit\)$/
231 | )
232 | })
233 |
--------------------------------------------------------------------------------
/test/stream.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 |
3 | const tnock = require('./util/tnock.js')
4 | const defaultOpts = require('../lib/default-opts.js')
5 | defaultOpts.registry = 'https://mock.reg/'
6 |
7 | const fetch = require('..')
8 |
9 | const OPTS = {
10 | timeout: 0,
11 | retry: {
12 | retries: 1,
13 | factor: 1,
14 | minTimeout: 1,
15 | maxTimeout: 10,
16 | },
17 | }
18 |
19 | t.test('json.stream', (t) => {
20 | t.test('fetch.json.stream()', async (t) => {
21 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
22 | a: 1,
23 | b: 2,
24 | c: 3,
25 | something: null,
26 | })
27 | const data = await fetch.json.stream('/hello', '$*', OPTS).collect()
28 | t.same(
29 | data,
30 | [
31 | { key: 'a', value: 1 },
32 | { key: 'b', value: 2 },
33 | { key: 'c', value: 3 },
34 | ],
35 | 'got a streamed JSON body'
36 | )
37 | })
38 |
39 | t.test('fetch.json.stream opts.mapJSON', async (t) => {
40 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
41 | a: 1,
42 | b: 2,
43 | c: 3,
44 | })
45 | const data = await fetch.json
46 | .stream('/hello', '*', {
47 | ...OPTS,
48 | mapJSON (value, [key]) {
49 | return [key, value]
50 | },
51 | })
52 | .collect()
53 | t.same(
54 | data,
55 | [
56 | ['a', 1],
57 | ['b', 2],
58 | ['c', 3],
59 | ],
60 | 'data mapped'
61 | )
62 | })
63 |
64 | t.test('fetch.json.stream opts.mapJSON that returns null', async (t) => {
65 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
66 | a: 1,
67 | b: 2,
68 | c: 3,
69 | })
70 | const data = await fetch.json
71 | .stream('/hello', '*', {
72 | ...OPTS,
73 | // eslint-disable-next-line no-unused-vars
74 | mapJSON (_value, [_key]) {
75 | return null
76 | },
77 | })
78 | .collect()
79 | t.same(data, [])
80 | })
81 |
82 | t.test('fetch.json.stream gets fetch error on stream', async (t) => {
83 | await t.rejects(
84 | fetch.json
85 | .stream('/hello', '*', {
86 | ...OPTS,
87 | body: Promise.reject(new Error('no body for you')),
88 | method: 'POST',
89 | gzip: true, // make sure we don't gzip the promise, lol!
90 | })
91 | .collect(),
92 | {
93 | message: 'no body for you',
94 | }
95 | )
96 | })
97 |
98 | t.test('fetch.json.stream() sets header and footer', async (t) => {
99 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
100 | a: 1,
101 | b: 2,
102 | c: 3,
103 | })
104 | const data = await fetch.json
105 | .stream('/hello', 'something-random', OPTS)
106 | .collect()
107 | t.same(data, [], 'no data')
108 | })
109 |
110 | t.test('fetch.json.stream() with recursive JSON', async (t) => {
111 | tnock(t, defaultOpts.registry)
112 | .get('/hello')
113 | .reply(200, {
114 | dogs: [
115 | {
116 | name: 'george',
117 | owner: {
118 | name: 'bob',
119 | },
120 | },
121 | {
122 | name: 'fred',
123 | owner: {
124 | name: 'alice',
125 | },
126 | },
127 | {
128 | name: 'jill',
129 | owner: {
130 | name: 'fred',
131 | },
132 | },
133 | ],
134 | })
135 |
136 | const data = await fetch.json
137 | .stream('/hello', 'dogs..name', OPTS)
138 | .collect()
139 | t.same(data, ['george', 'bob', 'fred', 'alice', 'jill', 'fred'])
140 | })
141 |
142 | t.test('fetch.json.stream() with undefined path', async (t) => {
143 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
144 | a: 1,
145 | })
146 | const data = await fetch.json.stream('/hello', undefined, OPTS).collect()
147 | t.same(data, [{ a: 1 }])
148 | })
149 |
150 | t.test('fetch.json.stream() with empty path', async (t) => {
151 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
152 | a: 1,
153 | })
154 | const data = await fetch.json.stream('/hello', '', OPTS).collect()
155 | t.same(data, [])
156 | })
157 |
158 | t.test('fetch.json.stream() with path with function', async (t) => {
159 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
160 | a: {
161 | b: {
162 | c: 1,
163 | },
164 | d: 2,
165 | },
166 | })
167 | const data = await fetch.json
168 | .stream('/hello', [
169 | (a) => a,
170 | {
171 | test: (a) => a,
172 | },
173 | ])
174 | .collect()
175 | t.same(data, [{ c: 1 }, 2])
176 | })
177 |
178 | t.test('fetch.json.stream() with path array with number in path', async (t) => {
179 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
180 | a: 1,
181 | })
182 | const data = await fetch.json.stream('/hello', [1], OPTS).collect()
183 | t.same(data, [])
184 | })
185 |
186 | t.test(
187 | 'fetch.json.stream() with path array with recursive and undefined value',
188 | async (t) => {
189 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
190 | a: {
191 | b: {
192 | c: 1,
193 | },
194 | d: 2,
195 | },
196 | })
197 | const data = await fetch.json
198 | .stream('/hello', ['a', '', undefined], OPTS)
199 | .collect()
200 | t.same(data, [])
201 | }
202 | )
203 |
204 | t.test('fetch.json.stream() emitKey in path', async (t) => {
205 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
206 | a: {
207 | b: 1,
208 | },
209 | })
210 | const data = await fetch.json.stream('/hello', ['a', { emitKey: true }], OPTS).collect()
211 | t.same(data, [{ key: 'b', value: 1 }])
212 | })
213 |
214 | t.test('fetch.json.stream with recursive path followed by valid key', async (t) => {
215 | tnock(t, defaultOpts.registry).get('/hello').reply(200, {
216 | a: {
217 | b: 1,
218 | },
219 | })
220 | const data = await fetch.json.stream('/hello', ['', 'a'], OPTS).collect()
221 | t.same(data, [{ b: 1 }])
222 | })
223 |
224 | t.test('fetch.json.stream encounters malformed json', async (t) => {
225 | tnock(t, defaultOpts.registry).get('/hello').reply(200, '{')
226 | const data = await fetch.json.stream('/hello', '*', OPTS).collect()
227 |
228 | t.same(data, [])
229 | })
230 |
231 | t.test('fetch.json.stream encounters not json string data', async (t) => {
232 | tnock(t, defaultOpts.registry).get('/hello').reply(200, 'not json')
233 |
234 | // catch rejected promise
235 | t.rejects(fetch.json.stream('/hello', '*', OPTS).collect(), {
236 | message: 'Unexpected "o" at position 1 in state STOP',
237 | })
238 | })
239 |
240 | t.test('fetch.json.stream encounters not json numerical data', async (t) => {
241 | tnock(t, defaultOpts.registry).get('/hello').reply(200, 555)
242 |
243 | const data = await fetch.json.stream('/hello', '*', OPTS).collect()
244 | t.same(data, [])
245 | })
246 |
247 | t.end()
248 | })
249 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { HttpErrorAuthOTP } = require('./errors.js')
4 | const checkResponse = require('./check-response.js')
5 | const getAuth = require('./auth.js')
6 | const fetch = require('make-fetch-happen')
7 | const JSONStream = require('./json-stream')
8 | const npa = require('npm-package-arg')
9 | const qs = require('querystring')
10 | const url = require('url')
11 | const zlib = require('minizlib')
12 | const { Minipass } = require('minipass')
13 |
14 | const defaultOpts = require('./default-opts.js')
15 |
16 | // WhatWG URL throws if it's not fully resolved
17 | const urlIsValid = u => {
18 | try {
19 | return !!new url.URL(u)
20 | } catch (_) {
21 | return false
22 | }
23 | }
24 |
25 | module.exports = regFetch
26 | function regFetch (uri, /* istanbul ignore next */ opts_ = {}) {
27 | const opts = {
28 | ...defaultOpts,
29 | ...opts_,
30 | }
31 |
32 | // if we did not get a fully qualified URI, then we look at the registry
33 | // config or relevant scope to resolve it.
34 | const uriValid = urlIsValid(uri)
35 | let registry = opts.registry || defaultOpts.registry
36 | if (!uriValid) {
37 | registry = opts.registry = (
38 | (opts.spec && pickRegistry(opts.spec, opts)) ||
39 | opts.registry ||
40 | registry
41 | )
42 | uri = `${
43 | registry.trim().replace(/\/?$/g, '')
44 | }/${
45 | uri.trim().replace(/^\//, '')
46 | }`
47 | // asserts that this is now valid
48 | new url.URL(uri)
49 | }
50 |
51 | const method = opts.method || 'GET'
52 |
53 | // through that takes into account the scope, the prefix of `uri`, etc
54 | const startTime = Date.now()
55 | const auth = getAuth(uri, opts)
56 | const headers = getHeaders(uri, auth, opts)
57 | let body = opts.body
58 | const bodyIsStream = Minipass.isStream(body)
59 | const bodyIsPromise = body &&
60 | typeof body === 'object' &&
61 | typeof body.then === 'function'
62 |
63 | if (
64 | body && !bodyIsStream && !bodyIsPromise && typeof body !== 'string' && !Buffer.isBuffer(body)
65 | ) {
66 | headers['content-type'] = headers['content-type'] || 'application/json'
67 | body = JSON.stringify(body)
68 | } else if (body && !headers['content-type']) {
69 | headers['content-type'] = 'application/octet-stream'
70 | }
71 |
72 | if (opts.gzip) {
73 | headers['content-encoding'] = 'gzip'
74 | if (bodyIsStream) {
75 | const gz = new zlib.Gzip()
76 | body.on('error', /* istanbul ignore next: unlikely and hard to test */
77 | err => gz.emit('error', err))
78 | body = body.pipe(gz)
79 | } else if (!bodyIsPromise) {
80 | body = new zlib.Gzip().end(body).concat()
81 | }
82 | }
83 |
84 | const parsed = new url.URL(uri)
85 |
86 | if (opts.query) {
87 | const q = typeof opts.query === 'string' ? qs.parse(opts.query)
88 | : opts.query
89 |
90 | Object.keys(q).forEach(key => {
91 | if (q[key] !== undefined) {
92 | parsed.searchParams.set(key, q[key])
93 | }
94 | })
95 | uri = url.format(parsed)
96 | }
97 |
98 | if (parsed.searchParams.get('write') === 'true' && method === 'GET') {
99 | // do not cache, because this GET is fetching a rev that will be
100 | // used for a subsequent PUT or DELETE, so we need to conditionally
101 | // update cache.
102 | opts.offline = false
103 | opts.preferOffline = false
104 | opts.preferOnline = true
105 | }
106 |
107 | const doFetch = async fetchBody => {
108 | const p = fetch(uri, {
109 | agent: opts.agent,
110 | algorithms: opts.algorithms,
111 | body: fetchBody,
112 | cache: getCacheMode(opts),
113 | cachePath: opts.cache,
114 | ca: opts.ca,
115 | cert: auth.cert || opts.cert,
116 | headers,
117 | integrity: opts.integrity,
118 | key: auth.key || opts.key,
119 | localAddress: opts.localAddress,
120 | maxSockets: opts.maxSockets,
121 | memoize: opts.memoize,
122 | method: method,
123 | noProxy: opts.noProxy,
124 | proxy: opts.httpsProxy || opts.proxy,
125 | retry: opts.retry ? opts.retry : {
126 | retries: opts.fetchRetries,
127 | factor: opts.fetchRetryFactor,
128 | minTimeout: opts.fetchRetryMintimeout,
129 | maxTimeout: opts.fetchRetryMaxtimeout,
130 | },
131 | strictSSL: opts.strictSSL,
132 | timeout: opts.timeout || 30 * 1000,
133 | signal: opts.signal,
134 | }).then(res => checkResponse({
135 | method,
136 | uri,
137 | res,
138 | registry,
139 | startTime,
140 | auth,
141 | opts,
142 | }))
143 |
144 | if (typeof opts.otpPrompt === 'function') {
145 | return p.catch(async er => {
146 | if (er instanceof HttpErrorAuthOTP) {
147 | let otp
148 | // if otp fails to complete, we fail with that failure
149 | try {
150 | otp = await opts.otpPrompt()
151 | } catch (_) {
152 | // ignore this error
153 | }
154 | // if no otp provided, or otpPrompt errored, throw the original HTTP error
155 | if (!otp) {
156 | throw er
157 | }
158 | return regFetch(uri, { ...opts, otp })
159 | }
160 | throw er
161 | })
162 | } else {
163 | return p
164 | }
165 | }
166 |
167 | return Promise.resolve(body).then(doFetch)
168 | }
169 |
170 | module.exports.getAuth = getAuth
171 |
172 | module.exports.json = fetchJSON
173 | function fetchJSON (uri, opts) {
174 | return regFetch(uri, opts).then(res => res.json())
175 | }
176 |
177 | module.exports.json.stream = fetchJSONStream
178 | function fetchJSONStream (uri, jsonPath,
179 | /* istanbul ignore next */ opts_ = {}) {
180 | const opts = { ...defaultOpts, ...opts_ }
181 | const parser = JSONStream.parse(jsonPath, opts.mapJSON)
182 | regFetch(uri, opts).then(res =>
183 | res.body.on('error',
184 | /* istanbul ignore next: unlikely and difficult to test */
185 | er => parser.emit('error', er)).pipe(parser)
186 | ).catch(er => parser.emit('error', er))
187 | return parser
188 | }
189 |
190 | module.exports.pickRegistry = pickRegistry
191 | function pickRegistry (spec, opts = {}) {
192 | spec = npa(spec)
193 | let registry = spec.scope &&
194 | opts[spec.scope.replace(/^@?/, '@') + ':registry']
195 |
196 | if (!registry && opts.scope) {
197 | registry = opts[opts.scope.replace(/^@?/, '@') + ':registry']
198 | }
199 |
200 | if (!registry) {
201 | registry = opts.registry || defaultOpts.registry
202 | }
203 |
204 | return registry
205 | }
206 |
207 | function getCacheMode (opts) {
208 | return opts.offline ? 'only-if-cached'
209 | : opts.preferOffline ? 'force-cache'
210 | : opts.preferOnline ? 'no-cache'
211 | : 'default'
212 | }
213 |
214 | function getHeaders (uri, auth, opts) {
215 | const headers = Object.assign({
216 | 'user-agent': opts.userAgent,
217 | }, opts.headers || {})
218 |
219 | if (opts.authType) {
220 | headers['npm-auth-type'] = opts.authType
221 | }
222 |
223 | if (opts.scope) {
224 | headers['npm-scope'] = opts.scope
225 | }
226 |
227 | if (opts.npmSession) {
228 | headers['npm-session'] = opts.npmSession
229 | }
230 |
231 | if (opts.npmCommand) {
232 | headers['npm-command'] = opts.npmCommand
233 | }
234 |
235 | // If a tarball is hosted on a different place than the manifest, only send
236 | // credentials on `alwaysAuth`
237 | if (auth.token) {
238 | headers.authorization = `Bearer ${auth.token}`
239 | } else if (auth.auth) {
240 | headers.authorization = `Basic ${auth.auth}`
241 | }
242 |
243 | if (opts.otp) {
244 | headers['npm-otp'] = opts.otp
245 | }
246 |
247 | return headers
248 | }
249 |
--------------------------------------------------------------------------------
/test/errors.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const npa = require('npm-package-arg')
4 | const t = require('tap')
5 | const tnock = require('./util/tnock.js')
6 | const errors = require('../lib/errors.js')
7 |
8 | const fetch = require('..')
9 |
10 | const OPTS = {
11 | timeout: 0,
12 | retry: {
13 | retries: 1,
14 | factor: 1,
15 | minTimeout: 1,
16 | maxTimeout: 10,
17 | },
18 | registry: 'https://mock.reg/',
19 | }
20 |
21 | t.test('generic request errors', t => {
22 | tnock(t, OPTS.registry)
23 | .get('/ohno/oops')
24 | .reply(400, 'failwhale!')
25 | // verify that the otpPrompt won't save from non-OTP errors
26 | const otpPrompt = () => {
27 | throw new Error('nope')
28 | }
29 | return fetch('/ohno/oops', { ...OPTS, otpPrompt })
30 | .then(
31 | () => {
32 | throw new Error('should not have succeeded!')
33 | },
34 | err => {
35 | t.equal(
36 | err.message,
37 | `400 Bad Request - GET ${OPTS.registry}ohno/oops`,
38 | 'neatly printed message'
39 | )
40 | t.equal(err.code, 'E400', 'HTTP code used for err.code')
41 | t.equal(err.statusCode, 400, 'numerical HTTP code available')
42 | t.equal(err.method, 'GET', 'method in error object')
43 | t.equal(err.body.toString('utf8'), 'failwhale!', 'req body reported')
44 | t.equal(err.pkgid, 'oops', 'base path used for pkgid')
45 | }
46 | )
47 | })
48 |
49 | t.test('pkgid tie fighter', t => {
50 | tnock(t, OPTS.registry)
51 | .get('/-/ohno/_rewrite/ohyeah/maybe')
52 | .reply(400, 'failwhale!')
53 | return fetch('/-/ohno/_rewrite/ohyeah/maybe', OPTS)
54 | .then(
55 | () => {
56 | throw new Error('should not have succeeded!')
57 | },
58 | err => t.equal(err.pkgid, undefined, 'no pkgid on tie fighters')
59 | )
60 | })
61 |
62 | t.test('pkgid _rewrite', t => {
63 | tnock(t, OPTS.registry)
64 | .get('/ohno/_rewrite/ohyeah/maybe')
65 | .reply(400, 'failwhale!')
66 | return fetch('/ohno/_rewrite/ohyeah/maybe', OPTS)
67 | .then(
68 | () => {
69 | throw new Error('should not have succeeded!')
70 | },
71 | err => t.equal(err.pkgid, 'ohyeah', '_rewrite filtered for pkgid')
72 | )
73 | })
74 |
75 | t.test('pkgid with `opts.spec`', t => {
76 | tnock(t, OPTS.registry)
77 | .get('/ohno/_rewrite/ohyeah')
78 | .reply(400, 'failwhale!')
79 | return fetch('/ohno/_rewrite/ohyeah', {
80 | ...OPTS,
81 | spec: npa('foo@1.2.3'),
82 | })
83 | .then(
84 | () => {
85 | throw new Error('should not have succeeded!')
86 | },
87 | err => t.equal(err.pkgid, 'foo@1.2.3', 'opts.spec used for pkgid')
88 | )
89 | })
90 |
91 | t.test('JSON error reporing', t => {
92 | tnock(t, OPTS.registry)
93 | .get('/ohno')
94 | .reply(400, { error: 'badarg' })
95 | return fetch('/ohno', OPTS)
96 | .then(
97 | () => {
98 | throw new Error('should not have succeeded!')
99 | },
100 | err => {
101 | t.equal(
102 | err.message,
103 | `400 Bad Request - GET ${OPTS.registry}ohno - badarg`,
104 | 'neatly printed message'
105 | )
106 | t.equal(err.code, 'E400', 'HTTP code used for err.code')
107 | t.equal(err.statusCode, 400, 'numerical HTTP code available')
108 | t.equal(err.method, 'GET', 'method in error object')
109 | t.same(err.body, {
110 | error: 'badarg',
111 | }, 'parsed JSON error response available')
112 | }
113 | )
114 | })
115 |
116 | t.test('OTP error', t => {
117 | tnock(t, OPTS.registry)
118 | .get('/otplease')
119 | .reply(401, { error: 'needs an otp, please' }, {
120 | 'www-authenticate': 'otp',
121 | })
122 | return fetch('/otplease', OPTS)
123 | .then(
124 | () => {
125 | throw new Error('Should not have succeeded!')
126 | },
127 | err => {
128 | t.equal(err.code, 'EOTP', 'got special OTP error code')
129 | }
130 | )
131 | })
132 |
133 | t.test('OTP error with prompt', async t => {
134 | let OTP = null
135 | tnock(t, OPTS.registry)
136 | .get('/otplease').times(2)
137 | .matchHeader('npm-otp', otp => {
138 | if (otp) {
139 | OTP = otp[0]
140 | t.strictSame(otp, ['12345'], 'got expected otp')
141 | }
142 | return true
143 | })
144 | .reply(() => {
145 | if (OTP === '12345') {
146 | return [200, { ok: 'this is fine' }, {}]
147 | } else {
148 | return [401, { error: 'otp, please' }, { 'www-authenticate': 'otp' }]
149 | }
150 | })
151 |
152 | const otpPrompt = async () => '12345'
153 | const res = await fetch('/otplease', { ...OPTS, otpPrompt })
154 | t.strictSame(res.status, 200, 'got 200 response')
155 | const body = await res.json()
156 | t.strictSame(body, { ok: 'this is fine' }, 'got expected body')
157 | })
158 |
159 | t.test('OTP error with prompt, expired OTP in settings', async t => {
160 | let OTP = null
161 | tnock(t, OPTS.registry)
162 | .get('/otplease').times(2)
163 | .matchHeader('npm-otp', otp => {
164 | if (otp) {
165 | if (!OTP) {
166 | t.strictSame(otp, ['98765'], 'got invalid otp first')
167 | } else {
168 | t.strictSame(otp, ['12345'], 'got expected otp')
169 | }
170 | OTP = otp[0]
171 | }
172 | return true
173 | })
174 | .reply(() => {
175 | if (OTP === '12345') {
176 | return [200, { ok: 'this is fine' }, {}]
177 | } else {
178 | return [401, { error: 'otp, please' }, { 'www-authenticate': 'otp' }]
179 | }
180 | })
181 |
182 | const otpPrompt = async () => '12345'
183 | const res = await fetch('/otplease', { ...OPTS, otpPrompt, otp: '98765' })
184 | t.strictSame(res.status, 200, 'got 200 response')
185 | const body = await res.json()
186 | t.strictSame(body, { ok: 'this is fine' }, 'got expected body')
187 | })
188 |
189 | t.test('OTP error with prompt that fails', t => {
190 | tnock(t, OPTS.registry)
191 | .get('/otplease')
192 | .reply(() => {
193 | return [401, { error: 'otp, please' }, { 'www-authenticate': 'otp' }]
194 | })
195 |
196 | const otpPrompt = async () => {
197 | throw new Error('whoopsie')
198 | }
199 | return t.rejects(fetch('/otplease', { ...OPTS, otpPrompt }), errors.HttpErrorAuthOTP)
200 | })
201 |
202 | t.test('OTP error with prompt that returns nothing', t => {
203 | tnock(t, OPTS.registry)
204 | .get('/otplease')
205 | .reply(() => {
206 | return [401, { error: 'otp, please' }, { 'www-authenticate': 'otp' }]
207 | })
208 |
209 | const otpPrompt = async () => {}
210 | return t.rejects(fetch('/otplease', { ...OPTS, otpPrompt }), errors.HttpErrorAuthOTP)
211 | })
212 |
213 | t.test('OTP error when missing www-authenticate', t => {
214 | tnock(t, OPTS.registry)
215 | .get('/otplease')
216 | .reply(401, { error: 'needs a one-time password' })
217 | return fetch('/otplease', OPTS)
218 | .then(
219 | () => {
220 | throw new Error('Should not have succeeded!')
221 | },
222 | err => {
223 | t.equal(
224 | err.code,
225 | 'EOTP',
226 | 'got special OTP error code even with missing www-authenticate header'
227 | )
228 | }
229 | )
230 | })
231 |
232 | t.test('Bad IP address error', t => {
233 | tnock(t, OPTS.registry)
234 | .get('/badaddr')
235 | .reply(401, { error: 'you are using the wrong IP address, friend' }, {
236 | 'www-authenticate': 'ipaddress',
237 | })
238 | return fetch('/badaddr', OPTS)
239 | .then(
240 | () => {
241 | throw new Error('Should not have succeeded!')
242 | },
243 | err => {
244 | t.equal(err.code, 'EAUTHIP', 'got special OTP error code')
245 | }
246 | )
247 | })
248 |
249 | t.test('Unexpected www-authenticate error', t => {
250 | tnock(t, OPTS.registry)
251 | .get('/unown')
252 | .reply(401, {
253 | error: `
254 | Pat-a-cake, pat-a-cake, baker's man.
255 | Bake me a cake as fast as you can
256 | Pat it, and prick it, and mark it with a "B"
257 | And put it in the oven for baby and me!
258 | `,
259 | }, {
260 | 'www-authenticate': 'pattie-cake-protocol',
261 | })
262 | return fetch('/unown', OPTS)
263 | .then(
264 | () => {
265 | throw new Error('Should not have succeeded!')
266 | },
267 | err => {
268 | t.match(err.body.error, /Pat-a-cake/ig, 'error body explains it')
269 | t.equal(err.code, 'E401', 'Unknown auth errors are generic 401s')
270 | }
271 | )
272 | })
273 |
274 | t.test('error can take headers object', (t) => {
275 | t.strictSame(new errors.HttpErrorBase('GET', { headers: { a: 1 } }).headers, { a: 1 })
276 | t.strictSame(new errors.HttpErrorBase('GET', { }).headers, undefined)
277 | t.end()
278 | })
279 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | const t = require('tap')
2 |
3 | const { Minipass } = require('minipass')
4 | const ssri = require('ssri')
5 | const zlib = require('zlib')
6 | const defaultOpts = require('../lib/default-opts.js')
7 | const tnock = require('./util/tnock.js')
8 |
9 | t.equal(defaultOpts.registry, 'https://registry.npmjs.org/',
10 | 'default registry is the npm public registry')
11 |
12 | // ok, now change it for the tests
13 | defaultOpts.registry = 'https://mock.reg/'
14 |
15 | const fetch = require('..')
16 |
17 | const OPTS = {
18 | timeout: 0,
19 | retry: {
20 | retries: 1,
21 | factor: 1,
22 | minTimeout: 1,
23 | maxTimeout: 10,
24 | },
25 | }
26 |
27 | t.test('hello world', t => {
28 | tnock(t, defaultOpts.registry)
29 | .get('/hello')
30 | .reply(200, { hello: 'world' })
31 | return fetch('/hello', {
32 | method: false, // will fall back to GET if falsey,
33 | ...OPTS,
34 | })
35 | .then(res => {
36 | t.equal(res.status, 200, 'got successful response')
37 | return res.json()
38 | })
39 | .then(json => t.same(json, { hello: 'world' }, 'got correct body'))
40 | })
41 |
42 | t.test('JSON body param', t => {
43 | tnock(t, defaultOpts.registry)
44 | .matchHeader('content-type', ctype => {
45 | t.equal(ctype[0], 'application/json', 'content-type automatically set')
46 | return ctype[0] === 'application/json'
47 | })
48 | .post('/hello')
49 | .reply(200, (uri, reqBody) => {
50 | t.same(reqBody, {
51 | hello: 'world',
52 | }, 'got the JSON version of the body')
53 | return reqBody
54 | })
55 | const opts = {
56 | ...OPTS,
57 | method: 'POST',
58 | body: { hello: 'world' },
59 | }
60 | return fetch('/hello', opts)
61 | .then(res => {
62 | t.equal(res.status, 200)
63 | return res.json()
64 | })
65 | .then(json => t.same(json, { hello: 'world' }))
66 | })
67 |
68 | t.test('buffer body param', t => {
69 | tnock(t, defaultOpts.registry)
70 | .matchHeader('content-type', ctype => {
71 | t.equal(ctype[0], 'application/octet-stream', 'content-type automatically set')
72 | return ctype[0] === 'application/octet-stream'
73 | })
74 | .post('/hello')
75 | .reply(200, (uri, reqBody) => {
76 | t.same(
77 | Buffer.from(reqBody, 'utf8'),
78 | Buffer.from('hello', 'utf8'),
79 | 'got the JSON version of the body'
80 | )
81 | return reqBody
82 | })
83 | const opts = {
84 | ...OPTS,
85 | method: 'POST',
86 | body: Buffer.from('hello', 'utf8'),
87 | }
88 | return fetch('/hello', opts)
89 | .then(res => {
90 | t.equal(res.status, 200)
91 | return res.buffer()
92 | })
93 | .then(buf =>
94 | t.same(buf, Buffer.from('hello', 'utf8'), 'got response')
95 | )
96 | })
97 |
98 | t.test('stream body param', t => {
99 | tnock(t, defaultOpts.registry)
100 | .matchHeader('content-type', ctype => {
101 | t.equal(ctype[0], 'application/octet-stream', 'content-type automatically set')
102 | return ctype[0] === 'application/octet-stream'
103 | })
104 | .post('/hello')
105 | .reply(200, (uri, reqBody) => {
106 | t.same(JSON.parse(reqBody), {
107 | hello: 'world',
108 | }, 'got the stringified version of the body')
109 | return reqBody
110 | })
111 | const stream = new Minipass()
112 | setImmediate(() => stream.end(JSON.stringify({ hello: 'world' })))
113 | const opts = {
114 | ...OPTS,
115 | method: 'POST',
116 | body: stream,
117 | }
118 | return fetch('/hello', opts)
119 | .then(res => {
120 | t.equal(res.status, 200)
121 | return res.json()
122 | })
123 | .then(json => t.same(json, { hello: 'world' }))
124 | })
125 |
126 | t.test('JSON body param', async t => {
127 | tnock(t, defaultOpts.registry)
128 | .matchHeader('content-type', ctype => {
129 | t.equal(ctype[0], 'application/json', 'content-type automatically set')
130 | return ctype[0] === 'application/json'
131 | })
132 | .matchHeader('content-encoding', enc => {
133 | t.equal(enc[0], 'gzip', 'content-encoding automatically set')
134 | return enc[0] === 'gzip'
135 | })
136 | .post('/hello')
137 | // NOTE: can't really test the body itself here because nock freaks out.
138 | .reply(200)
139 | const opts = {
140 | ...OPTS,
141 | method: 'POST',
142 | body: { hello: 'world' },
143 | gzip: true,
144 | }
145 | const res = await fetch('/hello', opts)
146 | t.equal(res.status, 200, 'request succeeded')
147 | })
148 |
149 | t.test('gzip + buffer body param', t => {
150 | tnock(t, defaultOpts.registry)
151 | .matchHeader('content-type', ctype => {
152 | t.equal(ctype[0], 'application/octet-stream', 'content-type automatically set')
153 | return ctype[0] === 'application/octet-stream'
154 | })
155 | .matchHeader('content-encoding', enc => {
156 | t.equal(enc[0], 'gzip', 'content-encoding automatically set')
157 | return enc[0] === 'gzip'
158 | })
159 | .post('/hello')
160 | .reply(200, (uri, reqBody) => {
161 | reqBody = zlib.gunzipSync(Buffer.from(reqBody, 'hex'))
162 | t.same(
163 | Buffer.from(reqBody, 'utf8').toString('utf8'),
164 | 'hello',
165 | 'got the JSON version of the body'
166 | )
167 | return reqBody
168 | })
169 | const opts = {
170 | ...OPTS,
171 | method: 'POST',
172 | body: Buffer.from('hello', 'utf8'),
173 | gzip: true,
174 | }
175 | return fetch('/hello', opts)
176 | .then(res => {
177 | t.equal(res.status, 200)
178 | return res.buffer()
179 | })
180 | .then(buf =>
181 | t.same(buf, Buffer.from('hello', 'utf8'), 'got response')
182 | )
183 | })
184 |
185 | t.test('gzip + stream body param', t => {
186 | tnock(t, defaultOpts.registry)
187 | .matchHeader('content-type', ctype => {
188 | t.equal(ctype[0], 'application/octet-stream', 'content-type automatically set')
189 | return ctype[0] === 'application/octet-stream'
190 | })
191 | .matchHeader('content-encoding', enc => {
192 | t.equal(enc[0], 'gzip', 'content-encoding automatically set')
193 | return enc[0] === 'gzip'
194 | })
195 | .post('/hello')
196 | .reply(200, (uri, reqBody) => {
197 | reqBody = zlib.gunzipSync(Buffer.from(reqBody, 'hex'))
198 | t.same(JSON.parse(reqBody.toString('utf8')), {
199 | hello: 'world',
200 | }, 'got the stringified version of the body')
201 | return reqBody
202 | })
203 | const stream = new Minipass()
204 | setImmediate(() => stream.end(JSON.stringify({ hello: 'world' })))
205 | const opts = {
206 | ...OPTS,
207 | method: 'POST',
208 | body: stream,
209 | gzip: true,
210 | query: {
211 | everything: undefined,
212 | is: undefined,
213 | },
214 | }
215 | return fetch('/hello', opts)
216 | .then(res => {
217 | t.equal(res.status, 200)
218 | return res.json()
219 | })
220 | .then(json => t.same(json, { hello: 'world' }))
221 | })
222 |
223 | t.test('query strings', t => {
224 | tnock(t, defaultOpts.registry)
225 | .get('/hello?hi=there&who=wor%20ld')
226 | .reply(200, { hello: 'world' })
227 | return fetch.json('/hello?hi=there', {
228 | ...OPTS,
229 | query: 'who=wor ld',
230 | }).then(json => t.equal(json.hello, 'world', 'query-string merged'))
231 | })
232 |
233 | t.test('query strings - undefined values', t => {
234 | tnock(t, defaultOpts.registry)
235 | .get('/hello?who=wor%20ld')
236 | .reply(200, { ok: true })
237 | return fetch.json('/hello', {
238 | ...OPTS,
239 | query: { hi: undefined, who: 'wor ld' },
240 | }).then(json => t.ok(json.ok, 'undefined keys not included in query string'))
241 | })
242 |
243 | t.test('json()', t => {
244 | tnock(t, defaultOpts.registry)
245 | .get('/hello')
246 | .reply(200, { hello: 'world' })
247 | return fetch.json('/hello', OPTS)
248 | .then(json => t.same(json, { hello: 'world' }, 'got json body'))
249 | })
250 |
251 | t.test('query string with ?write=true', t => {
252 | const cache = t.testdir()
253 | const opts = { ...OPTS, preferOffline: true, cache }
254 | const qsString = { ...opts, query: { write: 'true' } }
255 | const qsBool = { ...opts, query: { write: true } }
256 | tnock(t, defaultOpts.registry)
257 | .get('/writeTrueTest?write=true')
258 | .times(6)
259 | .reply(200, { write: 'go for it' })
260 |
261 | return fetch.json('/writeTrueTest?write=true', opts)
262 | .then(res => t.strictSame(res, { write: 'go for it' }))
263 | .then(() => fetch.json('/writeTrueTest?write=true', opts))
264 | .then(res => t.strictSame(res, { write: 'go for it' }))
265 | .then(() => fetch.json('/writeTrueTest', qsString))
266 | .then(res => t.strictSame(res, { write: 'go for it' }))
267 | .then(() => fetch.json('/writeTrueTest', qsString))
268 | .then(res => t.strictSame(res, { write: 'go for it' }))
269 | .then(() => fetch.json('/writeTrueTest', qsBool))
270 | .then(res => t.strictSame(res, { write: 'go for it' }))
271 | .then(() => fetch.json('/writeTrueTest', qsBool))
272 | .then(res => t.strictSame(res, { write: 'go for it' }))
273 | })
274 |
275 | t.test('opts.ignoreBody', async t => {
276 | tnock(t, defaultOpts.registry)
277 | .get('/hello')
278 | .reply(200, { hello: 'world' })
279 | const res = await fetch('/hello', { ...OPTS, ignoreBody: true })
280 | t.equal(res.body, null, 'body omitted')
281 | })
282 |
283 | t.test('method configurable', async t => {
284 | tnock(t, defaultOpts.registry)
285 | .delete('/hello')
286 | .reply(200)
287 | const opts = {
288 | ...OPTS,
289 | method: 'DELETE',
290 | }
291 | const res = await fetch('/hello', opts)
292 | t.equal(res.status, 200, 'successfully used DELETE method')
293 | })
294 |
295 | t.test('npm-notice header logging', async t => {
296 | tnock(t, defaultOpts.registry)
297 | .get('/hello')
298 | .reply(200, { hello: 'world' }, {
299 | 'npm-notice': 'npm <3 u',
300 | })
301 |
302 | let header, msg
303 | process.on('log', (level, ...args) => {
304 | if (level === 'notice') {
305 | [header, msg] = args
306 | }
307 | })
308 |
309 | t.plan(3)
310 | const res = await fetch('/hello', { ...OPTS })
311 | t.equal(res.status, 200, 'got successful response')
312 | t.equal(header, '', 'empty log header thing')
313 | t.equal(msg, 'npm <3 u', 'logged out npm-notice at NOTICE level')
314 | })
315 |
316 | t.test('optionally verifies request body integrity', t => {
317 | t.plan(3)
318 | tnock(t, defaultOpts.registry)
319 | .get('/hello')
320 | .times(2)
321 | .reply(200, 'hello')
322 | const integrity = ssri.fromData('hello')
323 | return fetch('/hello', { ...OPTS, integrity })
324 | .then(res => res.buffer())
325 | .then(buf => t.equal(
326 | buf.toString('utf8'), 'hello', 'successfully got the right data')
327 | )
328 | .then(() => {
329 | return fetch('/hello', { ...OPTS, integrity: 'sha1-nope' })
330 | .then(res => {
331 | t.ok(res.body, 'no error until body starts getting read')
332 | return res
333 | })
334 | .then(res => res.buffer())
335 | .then(
336 | () => {
337 | throw new Error('should not have succeeded')
338 | },
339 | err => t.equal(err.code, 'EINTEGRITY', 'got EINTEGRITY error')
340 | )
341 | })
342 | })
343 |
344 | t.test('pickRegistry() utility', t => {
345 | const pick = fetch.pickRegistry
346 | t.equal(pick('foo@1.2.3'), defaultOpts.registry, 'has good default')
347 | t.equal(
348 | pick('foo@1.2.3', {
349 | registry: 'https://my.registry/here/',
350 | scope: '@otherscope',
351 | '@myscope:registry': 'https://my.scoped.registry/here/',
352 | }),
353 | 'https://my.registry/here/',
354 | 'unscoped package uses `registry` setting'
355 | )
356 | t.equal(
357 | pick('@user/foo@1.2.3', {
358 | registry: 'https://my.registry/here/',
359 | scope: '@myscope',
360 | '@myscope:registry': 'https://my.scoped.registry/here/',
361 | }),
362 | 'https://my.scoped.registry/here/',
363 | 'scoped package uses `@:registry` setting'
364 | )
365 | t.equal(
366 | pick('@user/foo@1.2.3', {
367 | registry: 'https://my.registry/here/',
368 | scope: 'myscope',
369 | '@myscope:registry': 'https://my.scoped.registry/here/',
370 | }),
371 | 'https://my.scoped.registry/here/',
372 | 'scope @ is option@l'
373 | )
374 | t.end()
375 | })
376 |
377 | t.test('pickRegistry through opts.spec', t => {
378 | tnock(t, defaultOpts.registry)
379 | .get('/pkg')
380 | .reply(200, { source: defaultOpts.registry })
381 | const scopedReg = 'https://scoped.mock.reg/'
382 | tnock(t, scopedReg)
383 | .get('/pkg')
384 | .times(2)
385 | .reply(200, { source: scopedReg })
386 | return fetch.json('/pkg', {
387 | ...OPTS,
388 | spec: 'pkg@1.2.3',
389 | '@myscope:registry': scopedReg,
390 | }).then(json => t.equal(
391 | json.source,
392 | defaultOpts.registry,
393 | 'request made to main registry'
394 | )).then(() => fetch.json('/pkg', {
395 | ...OPTS,
396 | spec: 'pkg@1.2.3',
397 | '@myscope:registry': scopedReg,
398 | scope: '@myscope',
399 | })).then(json => t.equal(
400 | json.source,
401 | scopedReg,
402 | 'request made to scope registry using opts.scope'
403 | )).then(() => fetch.json('/pkg', Object.assign({
404 | spec: '@myscope/pkg@1.2.3',
405 | '@myscope:registry': scopedReg,
406 | }))).then(json => t.equal(
407 | json.source,
408 | scopedReg,
409 | 'request made to scope registry using spec scope'
410 | ))
411 | })
412 |
413 | t.test('miscellaneous headers', async t => {
414 | tnock(t, defaultOpts.registry)
415 | .matchHeader('npm-session', session =>
416 | t.strictSame(session, ['foobarbaz'], 'session set from options'))
417 | .matchHeader('npm-scope', scope =>
418 | t.strictSame(scope, ['@foo'], 'scope set from options'))
419 | .matchHeader('user-agent', ua =>
420 | t.strictSame(ua, ['agent of use'], 'UA set from options'))
421 | .matchHeader('npm-command', cmd =>
422 | t.strictSame(cmd, ['hello-world'], 'command set from options'))
423 | .matchHeader('npm-auth-type', authType =>
424 | t.strictSame(authType, ['auth'], 'auth-type set from options'))
425 | .get('/hello')
426 | .reply(200, { hello: 'world' })
427 |
428 | const res = await fetch('/hello', {
429 | ...OPTS,
430 | registry: null, // always falls back on falsey registry value
431 | npmSession: 'foobarbaz',
432 | scope: '@foo',
433 | userAgent: 'agent of use',
434 | npmCommand: 'hello-world',
435 | authType: 'auth',
436 | })
437 | t.equal(res.status, 200, 'got successful response')
438 | })
439 |
440 | t.test('miscellaneous headers not being set if not present in options', async t => {
441 | tnock(t, defaultOpts.registry)
442 | .matchHeader('npm-auth-type', authType =>
443 | t.strictSame(authType, undefined, 'auth-type not set from options'))
444 | .get('/hello')
445 | .reply(200, { hello: 'world' })
446 |
447 | const res = await fetch('/hello', {
448 | ...OPTS,
449 | authType: undefined,
450 | })
451 | t.equal(res.status, 200, 'got successful response')
452 | })
453 |
454 | t.test('opts.signal', async t => {
455 | const controller = new AbortController()
456 | const { signal } = controller
457 |
458 | controller.abort()
459 |
460 | try {
461 | await fetch('/hello', {
462 | ...OPTS,
463 | signal,
464 | })
465 | } catch (err) {
466 | t.equal(err.name, 'AbortError')
467 | return true
468 | }
469 | t.fail('should have thrown AbortError')
470 | })
471 |
--------------------------------------------------------------------------------
/test/auth.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const t = require('tap')
4 | const tnock = require('./util/tnock.js')
5 |
6 | const fetch = require('..')
7 | const getAuth = require('../lib/auth.js')
8 |
9 | const OPTS = {
10 | timeout: 0,
11 | retry: {
12 | retries: 1,
13 | factor: 1,
14 | minTimeout: 1,
15 | maxTimeout: 10,
16 | },
17 | registry: 'https://mock.reg/',
18 | }
19 |
20 | t.test('basic auth', t => {
21 | const config = {
22 | registry: 'https://my.custom.registry/here/',
23 | username: 'globaluser',
24 | password: Buffer.from('globalpass', 'utf8').toString('base64'),
25 | email: 'global@ma.il',
26 | '//my.custom.registry/here/:username': 'user',
27 | '//my.custom.registry/here/:_password': Buffer.from('pass', 'utf8').toString('base64'),
28 | '//my.custom.registry/here/:email': 'e@ma.il',
29 | }
30 | const gotAuth = getAuth(config.registry, config)
31 | t.same(gotAuth, {
32 | scopeAuthKey: null,
33 | regKey: '//my.custom.registry/here/',
34 | authKey: 'username',
35 | token: null,
36 | isBasicAuth: true,
37 | auth: Buffer.from('user:pass').toString('base64'),
38 | cert: null,
39 | key: null,
40 | }, 'basic auth details generated')
41 |
42 | const opts = Object.assign({}, OPTS, config)
43 | const encoded = Buffer.from('user:pass', 'utf8').toString('base64')
44 | tnock(t, opts.registry)
45 | .matchHeader('authorization', auth => {
46 | t.equal(auth[0], `Basic ${encoded}`, 'got encoded basic auth')
47 | return auth[0] === `Basic ${encoded}`
48 | })
49 | .get('/hello')
50 | .reply(200, '"success"')
51 | return fetch.json('/hello', opts)
52 | .then(res => t.equal(res, 'success', 'basic auth succeeded'))
53 | })
54 |
55 | t.test('token auth', t => {
56 | const config = {
57 | registry: 'https://my.custom.registry/here/',
58 | token: 'deadbeef',
59 | '//my.custom.registry/here/:_authToken': 'c0ffee',
60 | '//my.custom.registry/here/:token': 'nope',
61 | '//my.custom.registry/:_authToken': 'c0ffee',
62 | '//my.custom.registry/:token': 'nope',
63 | }
64 | t.same(getAuth(`${config.registry}/foo/-/foo.tgz`, config), {
65 | scopeAuthKey: null,
66 | regKey: '//my.custom.registry/here/',
67 | authKey: '_authToken',
68 | isBasicAuth: false,
69 | token: 'c0ffee',
70 | auth: null,
71 | cert: null,
72 | key: null,
73 | }, 'correct auth token picked out')
74 |
75 | const opts = Object.assign({}, OPTS, config)
76 | tnock(t, opts.registry)
77 | .matchHeader('authorization', auth => {
78 | t.equal(auth[0], 'Bearer c0ffee', 'got correct bearer token')
79 | return auth[0] === 'Bearer c0ffee'
80 | })
81 | .get('/hello')
82 | .reply(200, '"success"')
83 | return fetch.json('/hello', opts)
84 | .then(res => t.equal(res, 'success', 'token auth succeeded'))
85 | })
86 |
87 | t.test('forceAuth', t => {
88 | const dir = t.testdir({
89 | 'my.cert': 'my cert',
90 | 'my.key': 'my key',
91 | 'other.cert': 'other cert',
92 | 'other.key': 'other key',
93 | })
94 |
95 | const config = {
96 | registry: 'https://my.custom.registry/here/',
97 | token: 'deadbeef',
98 | 'always-auth': false,
99 | '//my.custom.registry/here/:_authToken': 'c0ffee',
100 | '//my.custom.registry/here/:token': 'nope',
101 | '//my.custom.registry/here/:certfile': `${dir}/my.cert`,
102 | '//my.custom.registry/here/:keyfile': `${dir}/my.key`,
103 | forceAuth: {
104 | username: 'user',
105 | password: Buffer.from('pass', 'utf8').toString('base64'),
106 | email: 'e@ma.il',
107 | 'always-auth': true,
108 | certfile: `${dir}/other.cert`,
109 | keyfile: `${dir}/other.key`,
110 | },
111 | }
112 | t.same(getAuth(config.registry, config), {
113 | scopeAuthKey: null,
114 | regKey: false,
115 | authKey: null,
116 | token: null,
117 | isBasicAuth: true,
118 | auth: Buffer.from('user:pass').toString('base64'),
119 | cert: 'other cert',
120 | key: 'other key',
121 | }, 'only forceAuth details included')
122 |
123 | const opts = Object.assign({}, OPTS, config)
124 | const encoded = Buffer.from('user:pass', 'utf8').toString('base64')
125 | tnock(t, opts.registry)
126 | .matchHeader('authorization', auth => {
127 | t.equal(auth[0], `Basic ${encoded}`, 'got encoded basic auth')
128 | return auth[0] === `Basic ${encoded}`
129 | })
130 | .get('/hello')
131 | .reply(200, '"success"')
132 | return fetch.json('/hello', opts)
133 | .then(res => t.equal(res, 'success', 'used forced auth details'))
134 | })
135 |
136 | t.test('forceAuth token', t => {
137 | const config = {
138 | registry: 'https://my.custom.registry/here/',
139 | token: 'deadbeef',
140 | 'always-auth': false,
141 | '//my.custom.registry/here/:_authToken': 'c0ffee',
142 | '//my.custom.registry/here/:token': 'nope',
143 | forceAuth: {
144 | token: 'cafebad',
145 | },
146 | }
147 | t.same(getAuth(config.registry, config), {
148 | scopeAuthKey: null,
149 | regKey: false,
150 | authKey: null,
151 | isBasicAuth: false,
152 | token: 'cafebad',
153 | auth: null,
154 | cert: null,
155 | key: null,
156 | }, 'correct forceAuth token picked out')
157 |
158 | const opts = Object.assign({}, OPTS, config)
159 | tnock(t, opts.registry)
160 | .matchHeader('authorization', auth => {
161 | t.equal(auth[0], 'Bearer cafebad', 'got correct bearer token')
162 | return auth[0] === 'Bearer cafebad'
163 | })
164 | .get('/hello')
165 | .reply(200, '"success"')
166 | return fetch.json('/hello', opts)
167 | .then(res => t.equal(res, 'success', 'token forceAuth succeeded'))
168 | })
169 |
170 | t.test('_auth auth', t => {
171 | const config = {
172 | registry: 'https://my.custom.registry/here/',
173 | _auth: 'deadbeef',
174 | '//my.custom.registry/:_auth': 'decafbad',
175 | '//my.custom.registry/here/:_auth': 'c0ffee',
176 | }
177 | t.same(getAuth(`${config.registry}/asdf/foo/bar/baz`, config), {
178 | scopeAuthKey: null,
179 | regKey: '//my.custom.registry/here/',
180 | authKey: '_auth',
181 | token: null,
182 | isBasicAuth: false,
183 | auth: 'c0ffee',
184 | cert: null,
185 | key: null,
186 | }, 'correct _auth picked out')
187 |
188 | const opts = Object.assign({}, OPTS, config)
189 | tnock(t, opts.registry)
190 | .matchHeader('authorization', 'Basic c0ffee')
191 | .get('/hello')
192 | .reply(200, '"success"')
193 | return fetch.json('/hello', opts)
194 | .then(res => t.equal(res, 'success', '_auth auth succeeded'))
195 | })
196 |
197 | t.test('_auth username:pass auth', t => {
198 | const username = 'foo'
199 | const password = 'bar'
200 | const auth = Buffer.from(`${username}:${password}`, 'utf8').toString('base64')
201 | const config = {
202 | registry: 'https://my.custom.registry/here/',
203 | _auth: 'foobarbaz',
204 | '//my.custom.registry/here/:_auth': auth,
205 | }
206 | t.same(getAuth(config.registry, config), {
207 | scopeAuthKey: null,
208 | regKey: '//my.custom.registry/here/',
209 | authKey: '_auth',
210 | token: null,
211 | isBasicAuth: false,
212 | auth: auth,
213 | cert: null,
214 | key: null,
215 | }, 'correct _auth picked out')
216 |
217 | const opts = Object.assign({}, OPTS, config)
218 | tnock(t, opts.registry)
219 | .matchHeader('authorization', `Basic ${auth}`)
220 | .get('/hello')
221 | .reply(200, '"success"')
222 | return fetch.json('/hello', opts)
223 | .then(res => t.equal(res, 'success', '_auth auth succeeded'))
224 | })
225 |
226 | t.test('ignore user/pass when _auth is set', t => {
227 | const username = 'foo'
228 | const password = Buffer.from('bar', 'utf8').toString('base64')
229 | const auth = Buffer.from('not:foobar', 'utf8').toString('base64')
230 | const config = {
231 | '//registry/:_auth': auth,
232 | '//registry/:username': username,
233 | '//registry/:password': password,
234 | 'always-auth': 'false',
235 | }
236 |
237 | const expect = {
238 | scopeAuthKey: null,
239 | auth,
240 | isBasicAuth: false,
241 | token: null,
242 | }
243 |
244 | t.match(getAuth('http://registry/pkg/-/pkg-1.2.3.tgz', config), expect)
245 |
246 | t.end()
247 | })
248 |
249 | t.test('globally-configured auth', t => {
250 | const basicConfig = {
251 | registry: 'https://different.registry/',
252 | '//different.registry/:username': 'globaluser',
253 | '//different.registry/:_password': Buffer.from('globalpass', 'utf8').toString('base64'),
254 | '//different.registry/:email': 'global@ma.il',
255 | '//my.custom.registry/here/:username': 'user',
256 | '//my.custom.registry/here/:_password': Buffer.from('pass', 'utf8').toString('base64'),
257 | '//my.custom.registry/here/:email': 'e@ma.il',
258 | }
259 | t.same(getAuth(basicConfig.registry, basicConfig), {
260 | scopeAuthKey: null,
261 | regKey: '//different.registry/',
262 | authKey: 'username',
263 | token: null,
264 | isBasicAuth: true,
265 | auth: Buffer.from('globaluser:globalpass').toString('base64'),
266 | cert: null,
267 | key: null,
268 | }, 'basic auth details generated from global settings')
269 |
270 | const tokenConfig = {
271 | registry: 'https://different.registry/',
272 | '//different.registry/:_authToken': 'deadbeef',
273 | '//my.custom.registry/here/:_authToken': 'c0ffee',
274 | '//my.custom.registry/here/:token': 'nope',
275 | }
276 | t.same(getAuth(tokenConfig.registry, tokenConfig), {
277 | scopeAuthKey: null,
278 | regKey: '//different.registry/',
279 | authKey: '_authToken',
280 | token: 'deadbeef',
281 | isBasicAuth: false,
282 | auth: null,
283 | cert: null,
284 | key: null,
285 | }, 'correct global auth token picked out')
286 |
287 | const _authConfig = {
288 | registry: 'https://different.registry/',
289 | '//different.registry:_auth': 'deadbeef',
290 | '//different.registry/bar:_auth': 'incorrect',
291 | '//my.custom.registry/here/:_auth': 'c0ffee',
292 | }
293 | t.same(getAuth(`${_authConfig.registry}/foo`, _authConfig), {
294 | scopeAuthKey: null,
295 | regKey: '//different.registry',
296 | authKey: '_auth',
297 | token: null,
298 | isBasicAuth: false,
299 | auth: 'deadbeef',
300 | cert: null,
301 | key: null,
302 | }, 'correct _auth picked out')
303 |
304 | t.end()
305 | })
306 |
307 | t.test('otp token passed through', t => {
308 | const config = {
309 | registry: 'https://my.custom.registry/here/',
310 | token: 'deadbeef',
311 | otp: '694201',
312 | '//my.custom.registry/here/:_authToken': 'c0ffee',
313 | '//my.custom.registry/here/:token': 'nope',
314 | }
315 | t.same(getAuth(config.registry, config), {
316 | scopeAuthKey: null,
317 | regKey: '//my.custom.registry/here/',
318 | authKey: '_authToken',
319 | token: 'c0ffee',
320 | isBasicAuth: false,
321 | auth: null,
322 | cert: null,
323 | key: null,
324 | }, 'correct auth token picked out')
325 |
326 | const opts = Object.assign({}, OPTS, config)
327 | tnock(t, opts.registry)
328 | .matchHeader('authorization', 'Bearer c0ffee')
329 | .matchHeader('npm-otp', otp => {
330 | t.equal(otp[0], config.otp, 'got the right otp token')
331 | return otp[0] === config.otp
332 | })
333 | .get('/hello')
334 | .reply(200, '"success"')
335 | return fetch.json('/hello', opts)
336 | .then(res => t.equal(res, 'success', 'otp auth succeeded'))
337 | })
338 |
339 | t.test('different hosts for uri vs registry', t => {
340 | const config = {
341 | 'always-auth': false,
342 | registry: 'https://my.custom.registry/here/',
343 | token: 'deadbeef',
344 | '//my.custom.registry/here/:_authToken': 'c0ffee',
345 | '//my.custom.registry/here/:token': 'nope',
346 | }
347 |
348 | const opts = Object.assign({}, OPTS, config)
349 | tnock(t, 'https://some.other.host/')
350 | .matchHeader('authorization', auth => {
351 | t.notOk(auth, 'no authorization header was sent')
352 | return !auth
353 | })
354 | .get('/hello')
355 | .reply(200, '"success"')
356 | return fetch.json('https://some.other.host/hello', opts)
357 | .then(res => t.equal(res, 'success', 'token auth succeeded'))
358 | })
359 |
360 | t.test('http vs https auth sending', t => {
361 | const config = {
362 | 'always-auth': false,
363 | registry: 'https://my.custom.registry/here/',
364 | token: 'deadbeef',
365 | '//my.custom.registry/here/:_authToken': 'c0ffee',
366 | '//my.custom.registry/here/:token': 'nope',
367 | }
368 |
369 | const opts = Object.assign({}, OPTS, config)
370 | tnock(t, 'http://my.custom.registry/here/')
371 | .matchHeader('authorization', 'Bearer c0ffee')
372 | .get('/hello')
373 | .reply(200, '"success"')
374 | return fetch.json('http://my.custom.registry/here/hello', opts)
375 | .then(res => t.equal(res, 'success', 'token auth succeeded'))
376 | })
377 |
378 | t.test('always-auth', t => {
379 | const config = {
380 | registry: 'https://my.custom.registry/here/',
381 | 'always-auth': 'true',
382 | '//some.other.host/:_authToken': 'deadbeef',
383 | '//my.custom.registry/here/:_authToken': 'c0ffee',
384 | '//my.custom.registry/here/:token': 'nope',
385 | }
386 | t.same(getAuth(config.registry, config), {
387 | scopeAuthKey: null,
388 | regKey: '//my.custom.registry/here/',
389 | authKey: '_authToken',
390 | token: 'c0ffee',
391 | isBasicAuth: false,
392 | auth: null,
393 | cert: null,
394 | key: null,
395 | }, 'correct auth token picked out')
396 |
397 | const opts = Object.assign({}, OPTS, config)
398 | tnock(t, 'https://some.other.host/')
399 | .matchHeader('authorization', 'Bearer deadbeef')
400 | .get('/hello')
401 | .reply(200, '"success"')
402 | return fetch.json('https://some.other.host/hello', opts)
403 | .then(res => t.equal(res, 'success', 'token auth succeeded'))
404 | })
405 |
406 | t.test('scope-based auth', t => {
407 | const dir = t.testdir({
408 | 'my.cert': 'my cert',
409 | 'my.key': 'my key',
410 | })
411 |
412 | const config = {
413 | registry: 'https://my.custom.registry/here/',
414 | scope: '@myscope',
415 | '@myscope:registry': 'https://my.custom.registry/here/',
416 | token: 'deadbeef',
417 | '//my.custom.registry/here/:_authToken': 'c0ffee',
418 | '//my.custom.registry/here/:token': 'nope',
419 | '//my.custom.registry/here/:certfile': `${dir}/my.cert`,
420 | '//my.custom.registry/here/:keyfile': `${dir}/my.key`,
421 | }
422 | t.same(getAuth(config['@myscope:registry'], config), {
423 | scopeAuthKey: null,
424 | regKey: '//my.custom.registry/here/',
425 | authKey: '_authToken',
426 | auth: null,
427 | isBasicAuth: false,
428 | token: 'c0ffee',
429 | cert: 'my cert',
430 | key: 'my key',
431 | }, 'correct auth token picked out')
432 | t.same(getAuth(config['@myscope:registry'], config), {
433 | scopeAuthKey: null,
434 | regKey: '//my.custom.registry/here/',
435 | authKey: '_authToken',
436 | auth: null,
437 | isBasicAuth: false,
438 | token: 'c0ffee',
439 | cert: 'my cert',
440 | key: 'my key',
441 | }, 'correct auth token picked out without scope config having an @')
442 |
443 | const opts = Object.assign({}, OPTS, config)
444 | tnock(t, opts['@myscope:registry'])
445 | .matchHeader('authorization', auth => {
446 | t.equal(auth[0], 'Bearer c0ffee', 'got correct bearer token for scope')
447 | return auth[0] === 'Bearer c0ffee'
448 | })
449 | .get('/hello')
450 | .times(2)
451 | .reply(200, '"success"')
452 | return fetch.json('/hello', opts)
453 | .then(res => t.equal(res, 'success', 'token auth succeeded'))
454 | .then(() => fetch.json('/hello', Object.assign({}, opts, {
455 | scope: 'myscope',
456 | })))
457 | .then(res => t.equal(res, 'success', 'token auth succeeded without @ in scope'))
458 | })
459 |
460 | t.test('auth needs a uri', t => {
461 | t.throws(() => getAuth(null), { message: 'URI is required' })
462 | t.end()
463 | })
464 |
465 | t.test('certfile and keyfile errors', t => {
466 | const dir = t.testdir({
467 | 'my.cert': 'my cert',
468 | })
469 |
470 | t.same(getAuth('https://my.custom.registry/here/', {
471 | '//my.custom.registry/here/:certfile': `${dir}/my.cert`,
472 | '//my.custom.registry/here/:keyfile': `${dir}/nosuch.key`,
473 | }), {
474 | scopeAuthKey: null,
475 | regKey: '//my.custom.registry/here/',
476 | authKey: 'certfile',
477 | auth: null,
478 | isBasicAuth: false,
479 | token: null,
480 | cert: null,
481 | key: null,
482 | }, 'cert and key ignored if one doesn\'t exist')
483 |
484 | t.throws(() => {
485 | getAuth('https://my.custom.registry/here/', {
486 | '//my.custom.registry/here/:certfile': `${dir}/my.cert`,
487 | '//my.custom.registry/here/:keyfile': dir,
488 | })
489 | }, /EISDIR/, 'other read errors are propagated')
490 | t.end()
491 | })
492 |
493 | t.test('do not be thrown by other weird configs', t => {
494 | const opts = {
495 | scope: '@asdf',
496 | '@asdf:_authToken': 'does this work?',
497 | '//registry.npmjs.org:_authToken': 'do not share this',
498 | _authToken: 'definitely do not share this, either',
499 | '//localhost:15443:_authToken': 'wrong',
500 | '//localhost:15443/foo:_authToken': 'correct bearer token',
501 | '//localhost:_authToken': 'not this one',
502 | '//other-registry:_authToken': 'this should not be used',
503 | '@asdf:registry': 'https://other-registry/',
504 | spec: '@asdf/foo',
505 | }
506 | const uri = 'http://localhost:15443/foo/@asdf/bar/-/bar-1.2.3.tgz'
507 | const auth = getAuth(uri, opts)
508 | t.same(auth, {
509 | scopeAuthKey: null,
510 | regKey: '//localhost:15443/foo',
511 | authKey: '_authToken',
512 | token: 'correct bearer token',
513 | isBasicAuth: false,
514 | auth: null,
515 | cert: null,
516 | key: null,
517 | })
518 | t.end()
519 | })
520 |
521 | t.test('scopeAuthKey tests', t => {
522 | const opts = {
523 | '@other-scope:registry': 'https://other-scope-registry.com/',
524 | '//other-scope-registry.com/:_authToken': 'cafebad',
525 | '@scope:registry': 'https://scope-host.com/',
526 | '//scope-host.com/:_authToken': 'c0ffee',
527 | }
528 | const uri = 'https://tarball-host.com/foo/foo.tgz'
529 |
530 | t.same(getAuth(uri, { ...opts, spec: '@scope/foo@latest' }), {
531 | scopeAuthKey: '//scope-host.com/',
532 | regKey: '//scope-host.com/',
533 | authKey: '_authToken',
534 | auth: null,
535 | isBasicAuth: false,
536 | token: null,
537 | cert: null,
538 | key: null,
539 | }, 'regular scoped spec')
540 |
541 | t.same(getAuth(uri, { ...opts, spec: 'foo@npm:@scope/foo@latest' }), {
542 | scopeAuthKey: '//scope-host.com/',
543 | regKey: '//scope-host.com/',
544 | authKey: '_authToken',
545 | auth: null,
546 | isBasicAuth: false,
547 | token: null,
548 | cert: null,
549 | key: null,
550 | }, 'scoped pkg aliased to unscoped name')
551 |
552 | t.same(getAuth(uri, { ...opts, spec: '@other-scope/foo@npm:@scope/foo@latest' }), {
553 | scopeAuthKey: '//scope-host.com/',
554 | regKey: '//scope-host.com/',
555 | authKey: '_authToken',
556 | auth: null,
557 | isBasicAuth: false,
558 | token: null,
559 | cert: null,
560 | key: null,
561 | }, 'scoped name aliased to other scope with auth')
562 |
563 | t.same(getAuth(uri, { ...opts, spec: '@scope/foo@npm:foo@latest' }), {
564 | scopeAuthKey: null,
565 | regKey: false,
566 | authKey: null,
567 | auth: null,
568 | isBasicAuth: false,
569 | token: null,
570 | cert: null,
571 | key: null,
572 | }, 'unscoped aliased to scoped name')
573 |
574 | t.end()
575 | })
576 |
577 | t.test('registry host matches, path does not, send auth', t => {
578 | const opts = {
579 | '@other-scope:registry': 'https://other-scope-registry.com/other/scope/',
580 | '//other-scope-registry.com/other/scope/:_authToken': 'cafebad',
581 | '@scope:registry': 'https://scope-host.com/scope/host/',
582 | '//scope-host.com/scope/host/:_authToken': 'c0ffee',
583 | registry: 'https://registry.example.com/some/path/',
584 | }
585 | const uri = 'https://scope-host.com/blahblah/bloobloo/foo.tgz'
586 | t.same(getAuth(uri, { ...opts, spec: '@scope/foo' }), {
587 | scopeAuthKey: null,
588 | regKey: '//scope-host.com/scope/host/',
589 | authKey: '_authToken',
590 | token: 'c0ffee',
591 | auth: null,
592 | isBasicAuth: false,
593 | cert: null,
594 | key: null,
595 | })
596 | t.same(getAuth(uri, { ...opts, spec: '@other-scope/foo' }), {
597 | scopeAuthKey: '//other-scope-registry.com/other/scope/',
598 | regKey: '//other-scope-registry.com/other/scope/',
599 | authKey: '_authToken',
600 | token: null,
601 | auth: null,
602 | isBasicAuth: false,
603 | cert: null,
604 | key: null,
605 | })
606 | t.same(getAuth(uri, { ...opts, registry: 'https://scope-host.com/scope/host/' }), {
607 | scopeAuthKey: null,
608 | regKey: '//scope-host.com/scope/host/',
609 | authKey: '_authToken',
610 | token: 'c0ffee',
611 | auth: null,
612 | isBasicAuth: false,
613 | cert: null,
614 | key: null,
615 | })
616 | t.end()
617 | })
618 |
619 | t.test('getAuth is exported', async t => {
620 | t.equal(fetch.getAuth, getAuth)
621 | })
622 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # npm-registry-fetch
2 |
3 | [`npm-registry-fetch`](https://github.com/npm/npm-registry-fetch) is a Node.js
4 | library that implements a `fetch`-like API for accessing npm registry APIs
5 | consistently. It's able to consume npm-style configuration values and has all
6 | the necessary logic for picking registries, handling scopes, and dealing with
7 | authentication details built-in.
8 |
9 | This package is meant to replace the older
10 | [`npm-registry-client`](https://npm.im/npm-registry-client).
11 |
12 | ## Example
13 |
14 | ```javascript
15 | const npmFetch = require('npm-registry-fetch')
16 |
17 | console.log(
18 | await npmFetch.json('/-/ping')
19 | )
20 | ```
21 |
22 | ## Table of Contents
23 |
24 | * [Installing](#install)
25 | * [Example](#example)
26 | * [Contributing](#contributing)
27 | * [API](#api)
28 | * [`fetch`](#fetch)
29 | * [`fetch.json`](#fetch-json)
30 | * [`fetch` options](#fetch-opts)
31 |
32 | ### Install
33 |
34 | `$ npm install npm-registry-fetch`
35 |
36 | ### Contributing
37 |
38 | The npm team enthusiastically welcomes contributions and project participation!
39 | There's a bunch of things you can do if you want to contribute! The [Contributor
40 | Guide](CONTRIBUTING.md) has all the information you need for everything from
41 | reporting bugs to contributing entire new features. Please don't hesitate to
42 | jump in if you'd like to, or even ask us questions if something isn't clear.
43 |
44 | All participants and maintainers in this project are expected to follow [Code of
45 | Conduct](CODE_OF_CONDUCT.md), and just generally be excellent to each other.
46 |
47 | Please refer to the [Changelog](CHANGELOG.md) for project history details, too.
48 |
49 | Happy hacking!
50 |
51 | ### API
52 |
53 | #### Caching and `write=true` query strings
54 |
55 | Before performing any PUT or DELETE operation, npm clients first make a
56 | GET request to the registry resource being updated, which includes
57 | the query string `?write=true`.
58 |
59 | The semantics of this are, effectively, "I intend to write to this thing,
60 | and need to know the latest current value, so that my write can land
61 | cleanly".
62 |
63 | The public npm registry handles these `?write=true` requests by ensuring
64 | that the cache is re-validated before sending a response. In order to
65 | maintain the same behavior on the client, and not get tripped up by an
66 | overeager local cache when we intend to write data to the registry, any
67 | request that comes through `npm-registry-fetch` that contains `write=true`
68 | in the query string will forcibly set the `prefer-online` option to `true`,
69 | and set both `prefer-offline` and `offline` to false, so that any local
70 | cached value will be revalidated.
71 |
72 | #### `> fetch(url, [opts]) -> Promise`
73 |
74 | Performs a request to a given URL.
75 |
76 | The URL can be either a full URL, or a path to one. The appropriate registry
77 | will be automatically picked if only a URL path is given.
78 |
79 | For available options, please see the section on [`fetch` options](#fetch-opts).
80 |
81 | ##### Example
82 |
83 | ```javascript
84 | const res = await fetch('/-/ping')
85 | console.log(res.headers)
86 | res.on('data', d => console.log(d.toString('utf8')))
87 | ```
88 |
89 | #### `> fetch.json(url, [opts]) -> Promise`
90 |
91 | Performs a request to a given registry URL, parses the body of the response as
92 | JSON, and returns it as its final value. This is a utility shorthand for
93 | `fetch(url).then(res => res.json())`.
94 |
95 | For available options, please see the section on [`fetch` options](#fetch-opts).
96 |
97 | ##### Example
98 |
99 | ```javascript
100 | const res = await fetch.json('/-/ping')
101 | console.log(res) // Body parsed as JSON
102 | ```
103 |
104 | #### `> fetch.json.stream(url, jsonPath, [opts]) -> Stream`
105 |
106 | Performs a request to a given registry URL and parses the body of the response
107 | as JSON, with each entry being emitted through the stream.
108 |
109 | The `jsonPath` argument is a [`JSONStream.parse()`
110 | path](https://github.com/dominictarr/JSONStream#jsonstreamparsepath), and the
111 | returned stream (unlike default `JSONStream`s), has a valid
112 | `Symbol.asyncIterator` implementation.
113 |
114 | For available options, please see the section on [`fetch` options](#fetch-opts).
115 |
116 | ##### Example
117 |
118 | ```javascript
119 | console.log('https://npm.im/~zkat has access to the following packages:')
120 | for await (let {key, value} of fetch.json.stream('/-/user/zkat/package', '$*')) {
121 | console.log(`https://npm.im/${key} (perms: ${value})`)
122 | }
123 | ```
124 |
125 | #### `fetch` Options
126 |
127 | Fetch options are optional, and can be passed in as either a Map-like object
128 | (one with a `.get()` method), a plain javascript object, or a
129 | [`figgy-pudding`](https://npm.im/figgy-pudding) instance.
130 |
131 | ##### `opts.agent`
132 |
133 | * Type: http.Agent
134 | * Default: an appropriate agent based on URL protocol and proxy settings
135 |
136 | An [`Agent`](https://nodejs.org/api/http.html#http_class_http_agent) instance to
137 | be shared across requests. This allows multiple concurrent `fetch` requests to
138 | happen on the same socket.
139 |
140 | You do _not_ need to provide this option unless you want something particularly
141 | specialized, since proxy configurations and http/https agents are already
142 | automatically managed internally when this option is not passed through.
143 |
144 | ##### `opts.body`
145 |
146 | * Type: Buffer | Stream | Object
147 | * Default: null
148 |
149 | Request body to send through the outgoing request. Buffers and Streams will be
150 | passed through as-is, with a default `content-type` of
151 | `application/octet-stream`. Plain JavaScript objects will be `JSON.stringify`ed
152 | and the `content-type` will default to `application/json`.
153 |
154 | Use [`opts.headers`](#opts-headers) to set the content-type to something else.
155 |
156 | ##### `opts.ca`
157 |
158 | * Type: String, Array, or null
159 | * Default: null
160 |
161 | The Certificate Authority signing certificate that is trusted for SSL
162 | connections to the registry. Values should be in PEM format (Windows calls it
163 | "Base-64 encoded X.509 (.CER)") with newlines replaced by the string `'\n'`. For
164 | example:
165 |
166 | ```
167 | {
168 | ca: '-----BEGIN CERTIFICATE-----\nXXXX\nXXXX\n-----END CERTIFICATE-----'
169 | }
170 | ```
171 |
172 | Set to `null` to only allow "known" registrars, or to a specific CA cert
173 | to trust only that specific signing authority.
174 |
175 | Multiple CAs can be trusted by specifying an array of certificates instead of a
176 | single string.
177 |
178 | See also [`opts.strictSSL`](#opts-strictSSL), [`opts.ca`](#opts-ca) and
179 | [`opts.key`](#opts-key)
180 |
181 | ##### `opts.cache`
182 |
183 | * Type: path
184 | * Default: null
185 |
186 | The location of the http cache directory. If provided, certain cachable requests
187 | will be cached according to [IETF RFC 7234](https://tools.ietf.org/html/rfc7234)
188 | rules. This will speed up future requests, as well as make the cached data
189 | available offline if necessary/requested.
190 |
191 | See also [`offline`](#opts-offline), [`preferOffline`](#opts-preferOffline),
192 | and [`preferOnline`](#opts-preferOnline).
193 |
194 | ##### `opts.cert`
195 |
196 | * Type: String
197 | * Default: null
198 |
199 | A client certificate to pass when accessing the registry. Values should be in
200 | PEM format (Windows calls it "Base-64 encoded X.509 (.CER)") with newlines
201 | replaced by the string `'\n'`. For example:
202 |
203 | ```
204 | {
205 | cert: '-----BEGIN CERTIFICATE-----\nXXXX\nXXXX\n-----END CERTIFICATE-----'
206 | }
207 | ```
208 |
209 | It is _not_ the path to a certificate file (and there is no "certfile" option).
210 |
211 | See also: [`opts.ca`](#opts-ca) and [`opts.key`](#opts-key)
212 |
213 | ##### `opts.fetchRetries`
214 |
215 | * Type: Number
216 | * Default: 2
217 |
218 | The "retries" config for [`retry`](https://npm.im/retry) to use when fetching
219 | packages from the registry.
220 |
221 | See also [`opts.retry`](#opts-retry) to provide all retry options as a single
222 | object.
223 |
224 | ##### `opts.fetchRetryFactor`
225 |
226 | * Type: Number
227 | * Default: 10
228 |
229 | The "factor" config for [`retry`](https://npm.im/retry) to use when fetching
230 | packages.
231 |
232 | See also [`opts.retry`](#opts-retry) to provide all retry options as a single
233 | object.
234 |
235 | ##### `opts.fetchRetryMintimeout`
236 |
237 | * Type: Number
238 | * Default: 10000 (10 seconds)
239 |
240 | The "minTimeout" config for [`retry`](https://npm.im/retry) to use when fetching
241 | packages.
242 |
243 | See also [`opts.retry`](#opts-retry) to provide all retry options as a single
244 | object.
245 |
246 | ##### `opts.fetchRetryMaxtimeout`
247 |
248 | * Type: Number
249 | * Default: 60000 (1 minute)
250 |
251 | The "maxTimeout" config for [`retry`](https://npm.im/retry) to use when fetching
252 | packages.
253 |
254 | See also [`opts.retry`](#opts-retry) to provide all retry options as a single
255 | object.
256 |
257 | ##### `opts.forceAuth`
258 |
259 | * Type: Object
260 | * Default: null
261 |
262 | If present, other auth-related values in `opts` will be completely ignored,
263 | including `alwaysAuth`, `email`, and `otp`, when calculating auth for a request,
264 | and the auth details in `opts.forceAuth` will be used instead.
265 |
266 | ##### `opts.gzip`
267 |
268 | * Type: Boolean
269 | * Default: false
270 |
271 | If true, `npm-registry-fetch` will set the `Content-Encoding` header to `gzip`
272 | and use `zlib.gzip()` or `zlib.createGzip()` to gzip-encode
273 | [`opts.body`](#opts-body).
274 |
275 | ##### `opts.headers`
276 |
277 | * Type: Object
278 | * Default: null
279 |
280 | Additional headers for the outgoing request. This option can also be used to
281 | override headers automatically generated by `npm-registry-fetch`, such as
282 | `Content-Type`.
283 |
284 | ##### `opts.ignoreBody`
285 |
286 | * Type: Boolean
287 | * Default: false
288 |
289 | If true, the **response body** will be thrown away and `res.body` set to `null`.
290 | This will prevent dangling response sockets for requests where you don't usually
291 | care what the response body is.
292 |
293 | ##### `opts.integrity`
294 |
295 | * Type: String | [SRI object](https://npm.im/ssri)
296 | * Default: null
297 |
298 | If provided, the response body's will be verified against this integrity string,
299 | using [`ssri`](https://npm.im/ssri). If verification succeeds, the response will
300 | complete as normal. If verification fails, the response body will error with an
301 | `EINTEGRITY` error.
302 |
303 | Body integrity is only verified if the body is actually consumed to completion --
304 | that is, if you use `res.json()`/`res.buffer()`, or if you consume the default
305 | `res` stream data to its end.
306 |
307 | Cached data will have its integrity automatically verified using the
308 | previously-generated integrity hash for the saved request information, so
309 | `EINTEGRITY` errors can happen if [`opts.cache`](#opts-cache) is used, even if
310 | `opts.integrity` is not passed in.
311 |
312 | ##### `opts.key`
313 |
314 | * Type: String
315 | * Default: null
316 |
317 | A client key to pass when accessing the registry. Values should be in PEM
318 | format with newlines replaced by the string `'\n'`. For example:
319 |
320 | ```
321 | {
322 | key: '-----BEGIN PRIVATE KEY-----\nXXXX\nXXXX\n-----END PRIVATE KEY-----'
323 | }
324 | ```
325 |
326 | It is _not_ the path to a key file (and there is no "keyfile" option).
327 |
328 | See also: [`opts.ca`](#opts-ca) and [`opts.cert`](#opts-cert)
329 |
330 | ##### `opts.localAddress`
331 |
332 | * Type: IP Address String
333 | * Default: null
334 |
335 | The IP address of the local interface to use when making connections
336 | to the registry.
337 |
338 | See also [`opts.proxy`](#opts-proxy)
339 |
340 | ##### `opts.mapJSON`
341 |
342 | * Type: Function
343 | * Default: undefined
344 |
345 | When using `fetch.json.stream()` (NOT `fetch.json()`), this will be passed down
346 | to [`JSONStream`](https://npm.im/JSONStream) as the second argument to
347 | `JSONStream.parse`, and can be used to transform stream data before output.
348 |
349 | ##### `opts.maxSockets`
350 |
351 | * Type: Integer
352 | * Default: 12
353 |
354 | Maximum number of sockets to keep open during requests. Has no effect if
355 | [`opts.agent`](#opts-agent) is used.
356 |
357 | ##### `opts.method`
358 |
359 | * Type: String
360 | * Default: 'GET'
361 |
362 | HTTP method to use for the outgoing request. Case-insensitive.
363 |
364 | ##### `opts.noProxy`
365 |
366 | * Type: String | String[]
367 | * Default: process.env.NOPROXY
368 |
369 | If present, should be a comma-separated string or an array of domain extensions
370 | that a proxy should _not_ be used for.
371 |
372 | ##### `opts.npmSession`
373 |
374 | * Type: String
375 | * Default: null
376 |
377 | If provided, will be sent in the `npm-session` header. This header is used by
378 | the npm registry to identify individual user sessions (usually individual
379 | invocations of the CLI).
380 |
381 | ##### `opts.npmCommand`
382 |
383 | * Type: String
384 | * Default: null
385 |
386 | If provided, it will be sent in the `npm-command` header. This header is
387 | used by the npm registry to identify the npm command that caused this
388 | request to be made.
389 |
390 | ##### `opts.offline`
391 |
392 | * Type: Boolean
393 | * Default: false
394 |
395 | Force offline mode: no network requests will be done during install. To allow
396 | `npm-registry-fetch` to fill in missing cache data, see
397 | [`opts.preferOffline`](#opts-preferOffline).
398 |
399 | This option is only really useful if you're also using
400 | [`opts.cache`](#opts-cache).
401 |
402 | This option is set to `true` when the request includes `write=true` in the
403 | query string.
404 |
405 | ##### `opts.otp`
406 |
407 | * Type: Number | String
408 | * Default: null
409 |
410 | This is a one-time password from a two-factor authenticator. It is required for
411 | certain registry interactions when two-factor auth is enabled for a user
412 | account.
413 |
414 | ##### `opts.otpPrompt`
415 |
416 | * Type: Function
417 | * Default: null
418 |
419 | This is a method which will be called to provide an OTP if the server
420 | responds with a 401 response indicating that a one-time-password is
421 | required.
422 |
423 | It may return a promise, which must resolve to the OTP value to be used.
424 | If the method fails to provide an OTP value, then the fetch will fail with
425 | the auth error that indicated an OTP was needed.
426 |
427 | ##### `opts.password`
428 |
429 | * Alias: `_password`
430 | * Type: String
431 | * Default: null
432 |
433 | Password used for basic authentication. For the more modern authentication
434 | method, please use the (more secure) [`opts.token`](#opts-token)
435 |
436 | Can optionally be scoped to a registry by using a "nerf dart" for that registry.
437 | That is:
438 |
439 | ```
440 | {
441 | '//registry.npmjs.org/:password': 't0k3nH34r'
442 | }
443 | ```
444 |
445 | See also [`opts.username`](#opts-username)
446 |
447 | ##### `opts.preferOffline`
448 |
449 | * Type: Boolean
450 | * Default: false
451 |
452 | If true, staleness checks for cached data will be bypassed, but missing data
453 | will be requested from the server. To force full offline mode, use
454 | [`opts.offline`](#opts-offline).
455 |
456 | This option is generally only useful if you're also using
457 | [`opts.cache`](#opts-cache).
458 |
459 | This option is set to `false` when the request includes `write=true` in the
460 | query string.
461 |
462 | ##### `opts.preferOnline`
463 |
464 | * Type: Boolean
465 | * Default: false
466 |
467 | If true, staleness checks for cached data will be forced, making the CLI look
468 | for updates immediately even for fresh package data.
469 |
470 | This option is generally only useful if you're also using
471 | [`opts.cache`](#opts-cache).
472 |
473 | This option is set to `true` when the request includes `write=true` in the
474 | query string.
475 |
476 | ##### `opts.scope`
477 |
478 | * Type: String
479 | * Default: null
480 |
481 | If provided, will be sent in the `npm-scope` header. This header is used by the
482 | npm registry to identify the toplevel package scope that a particular project
483 | installation is using.
484 |
485 | ##### `opts.proxy`
486 |
487 | * Type: url
488 | * Default: null
489 |
490 | A proxy to use for outgoing http requests. If not passed in, the `HTTP(S)_PROXY`
491 | environment variable will be used.
492 |
493 | ##### `opts.query`
494 |
495 | * Type: String | Object
496 | * Default: null
497 |
498 | If provided, the request URI will have a query string appended to it using this
499 | query. If `opts.query` is an object, it will be converted to a query string
500 | using
501 | [`querystring.stringify()`](https://nodejs.org/api/querystring.html#querystring_querystring_stringify_obj_sep_eq_options).
502 |
503 | If the request URI already has a query string, it will be merged with
504 | `opts.query`, preferring `opts.query` values.
505 |
506 | ##### `opts.registry`
507 |
508 | * Type: URL
509 | * Default: `'https://registry.npmjs.org'`
510 |
511 | Registry configuration for a request. If a request URL only includes the URL
512 | path, this registry setting will be prepended.
513 |
514 | See also [`opts.scope`](#opts-scope), [`opts.spec`](#opts-spec), and
515 | [`opts.:registry`](#opts-scope-registry) which can all affect the actual
516 | registry URL used by the outgoing request.
517 |
518 | ##### `opts.retry`
519 |
520 | * Type: Object
521 | * Default: null
522 |
523 | Single-object configuration for request retry settings. If passed in, will
524 | override individually-passed `fetch-retry-*` settings.
525 |
526 | ##### `opts.scope`
527 |
528 | * Type: String
529 | * Default: null
530 |
531 | Associate an operation with a scope for a scoped registry. This option can force
532 | lookup of scope-specific registries and authentication.
533 |
534 | See also [`opts.:registry`](#opts-scope-registry) and
535 | [`opts.spec`](#opts-spec) for interactions with this option.
536 |
537 | ##### `opts.:registry`
538 |
539 | * Type: String
540 | * Default: null
541 |
542 | This option type can be used to configure the registry used for requests
543 | involving a particular scope. For example, `opts['@myscope:registry'] =
544 | 'https://scope-specific.registry/'` will make it so requests go out to this
545 | registry instead of [`opts.registry`](#opts-registry) when
546 | [`opts.scope`](#opts-scope) is used, or when [`opts.spec`](#opts-spec) is a
547 | scoped package spec.
548 |
549 | The `@` before the scope name is optional, but recommended.
550 |
551 | ##### `opts.spec`
552 |
553 | * Type: String | [`npm-registry-arg`](https://npm.im/npm-registry-arg) object.
554 | * Default: null
555 |
556 | If provided, can be used to automatically configure [`opts.scope`](#opts-scope)
557 | based on a specific package name. Non-registry package specs will throw an
558 | error.
559 |
560 | ##### `opts.strictSSL`
561 |
562 | * Type: Boolean
563 | * Default: true
564 |
565 | Whether or not to do SSL key validation when making requests to the
566 | registry via https.
567 |
568 | See also [`opts.ca`](#opts-ca).
569 |
570 | ##### `opts.timeout`
571 |
572 | * Type: Milliseconds
573 | * Default: 300000 (5 minutes)
574 |
575 | Time before a hanging request times out.
576 |
577 | ##### `opts._authToken`
578 |
579 | * Type: String
580 | * Default: null
581 |
582 | Authentication token string.
583 |
584 | Can be scoped to a registry by using a "nerf dart" for that registry. That is:
585 |
586 | ```
587 | {
588 | '//registry.npmjs.org/:_authToken': 't0k3nH34r'
589 | }
590 | ```
591 |
592 | ##### `opts.userAgent`
593 |
594 | * Type: String
595 | * Default: `'npm-registry-fetch@/node@+ ()'`
596 |
597 | User agent string to send in the `User-Agent` header.
598 |
599 | ##### `opts.username`
600 |
601 | * Type: String
602 | * Default: null
603 |
604 | Username used for basic authentication. For the more modern authentication
605 | method, please use the (more secure) [`opts.authtoken`](#opts-authtoken)
606 |
607 | Can optionally be scoped to a registry by using a "nerf dart" for that registry.
608 | That is:
609 |
610 | ```
611 | {
612 | '//registry.npmjs.org/:username': 't0k3nH34r'
613 | }
614 | ```
615 |
616 | See also [`opts.password`](#opts-password)
617 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [19.1.1](https://github.com/npm/npm-registry-fetch/compare/v19.1.0...v19.1.1) (2025-11-13)
4 | ### Dependencies
5 | * [`360ec4e`](https://github.com/npm/npm-registry-fetch/commit/360ec4ebb295c292b367ca6d12d22fdb370fed98) [#282](https://github.com/npm/npm-registry-fetch/pull/282) `@npmcli/redact@4.0.0`
6 | * [`c703622`](https://github.com/npm/npm-registry-fetch/commit/c7036227a3dda6ff620d6a8bd423bcf0348040a8) [#282](https://github.com/npm/npm-registry-fetch/pull/282) `minipass-fetch@5.0.0`
7 | * [`61662d9`](https://github.com/npm/npm-registry-fetch/commit/61662d969569aeda6ccaa368da8317ec23a01b54) [#282](https://github.com/npm/npm-registry-fetch/pull/282) `ssri@13.0.0`
8 | * [`4ab8c96`](https://github.com/npm/npm-registry-fetch/commit/4ab8c96033cbb519677d1db01bed1e9cb86ea878) [#282](https://github.com/npm/npm-registry-fetch/pull/282) `proc-log@6.0.0`
9 | ### Chores
10 | * [`10ecc6a`](https://github.com/npm/npm-registry-fetch/commit/10ecc6a6b5f1bdcf443f95546bbb58c181ea3881) [#282](https://github.com/npm/npm-registry-fetch/pull/282) `@npmcli/eslint-config@6.0.0` (@wraithgar)
11 | * [`139321c`](https://github.com/npm/npm-registry-fetch/commit/139321ccf72c7f72fd4bb4ddeaf07b16ea193e15) [#282](https://github.com/npm/npm-registry-fetch/pull/282) `@npmcli/template-oss@4.28.0` (@wraithgar)
12 |
13 | ## [19.1.0](https://github.com/npm/npm-registry-fetch/compare/v19.0.0...v19.1.0) (2025-10-28)
14 | ### Features
15 | * [`d8074d6`](https://github.com/npm/npm-registry-fetch/commit/d8074d6a0344085180e0acc37d1ebeb789c7de63) [#280](https://github.com/npm/npm-registry-fetch/pull/280) add signal opt (#280) (@clemgbld)
16 |
17 | ## [19.0.0](https://github.com/npm/npm-registry-fetch/compare/v18.0.2...v19.0.0) (2025-07-24)
18 | ### ⚠️ BREAKING CHANGES
19 | * `npm-registry-fetch` now supports node `^20.17.0 || >=22.9.0`
20 | ### Bug Fixes
21 | * [`364c6c0`](https://github.com/npm/npm-registry-fetch/commit/364c6c0278b81d106239ba1d1a0f51b96d4aa53d) [#277](https://github.com/npm/npm-registry-fetch/pull/277) align to npm 11 node engine range (@owlstronaut)
22 | ### Dependencies
23 | * [`a94aa45`](https://github.com/npm/npm-registry-fetch/commit/a94aa458bdac7ef117c731580fe5775397c43ed0) [#277](https://github.com/npm/npm-registry-fetch/pull/277) `npm-package-arg@13.0.0`
24 | * [`743188b`](https://github.com/npm/npm-registry-fetch/commit/743188bf301d854d1cebdc8079b22f106374b038) [#277](https://github.com/npm/npm-registry-fetch/pull/277) `make-fetch-happen@15.0.0`
25 | ### Chores
26 | * [`563fda6`](https://github.com/npm/npm-registry-fetch/commit/563fda6a01433bbcf60fce30f30f5961fd85c39f) [#277](https://github.com/npm/npm-registry-fetch/pull/277) `cacache@20.0.0` (@owlstronaut)
27 | * [`d5519d6`](https://github.com/npm/npm-registry-fetch/commit/d5519d64377a159a855f963922927fba8b5ad838) [#277](https://github.com/npm/npm-registry-fetch/pull/277) template-oss apply fix (@owlstronaut)
28 | * [`894f3a7`](https://github.com/npm/npm-registry-fetch/commit/894f3a7f22f73d9246c3591d171f1c3cec40e3ee) [#277](https://github.com/npm/npm-registry-fetch/pull/277) `@npmcli/template-oss@4.25.0` (@owlstronaut)
29 |
30 | ## [18.0.2](https://github.com/npm/npm-registry-fetch/compare/v18.0.1...v18.0.2) (2024-10-16)
31 | ### Bug Fixes
32 | * [`8044781`](https://github.com/npm/npm-registry-fetch/commit/80447811a5d532e917488917eea6e5b10267d843) [#273](https://github.com/npm/npm-registry-fetch/pull/273) log cache hits distinct from fetch (#273) (@mbtools)
33 | ### Chores
34 | * [`99b99d2`](https://github.com/npm/npm-registry-fetch/commit/99b99d2971724518152c1121d6a672ffff111885) [#269](https://github.com/npm/npm-registry-fetch/pull/269) bump cacache from 18.0.4 to 19.0.1 (#269) (@dependabot[bot])
35 | * [`bd3f7d1`](https://github.com/npm/npm-registry-fetch/commit/bd3f7d1be56a884086a84a5a96194313c0ed0065) [#272](https://github.com/npm/npm-registry-fetch/pull/272) bump @npmcli/template-oss from 4.23.3 to 4.23.4 (#272) (@dependabot[bot], @npm-cli-bot)
36 |
37 | ## [18.0.1](https://github.com/npm/npm-registry-fetch/compare/v18.0.0...v18.0.1) (2024-10-02)
38 | ### Dependencies
39 | * [`ad9139a`](https://github.com/npm/npm-registry-fetch/commit/ad9139a8638f85ab159ba6733fae6fe763083204) [#270](https://github.com/npm/npm-registry-fetch/pull/270) bump `make-fetch-happen@14.0.0`
40 |
41 | ## [18.0.0](https://github.com/npm/npm-registry-fetch/compare/v17.1.0...v18.0.0) (2024-09-26)
42 | ### ⚠️ BREAKING CHANGES
43 | * `npm-registry-fetch` now supports node `^18.17.0 || >=20.5.0`
44 | ### Bug Fixes
45 | * [`93d71a6`](https://github.com/npm/npm-registry-fetch/commit/93d71a6cb45e5622f0dad0ecb039b7d160045194) [#264](https://github.com/npm/npm-registry-fetch/pull/264) align to npm 10 node engine range (@reggi)
46 | ### Dependencies
47 | * [`78aa620`](https://github.com/npm/npm-registry-fetch/commit/78aa620415a40978f78a92a8c2f2a07593c8afcc) [#266](https://github.com/npm/npm-registry-fetch/pull/266) bump minizlib from 2.1.2 to 3.0.1 (#266)
48 | * [`842f324`](https://github.com/npm/npm-registry-fetch/commit/842f324b7f0045b4ade9c9ee2033bcd38d9a5f71) [#264](https://github.com/npm/npm-registry-fetch/pull/264) `proc-log@5.0.0`
49 | * [`b394f34`](https://github.com/npm/npm-registry-fetch/commit/b394f34ba2e1ba062614ef7c8bdefac6355a659a) [#264](https://github.com/npm/npm-registry-fetch/pull/264) `npm-package-arg@12.0.0`
50 | * [`c2b986a`](https://github.com/npm/npm-registry-fetch/commit/c2b986a20ffa4a0c36ac0c5e5096a41cf91a9387) [#264](https://github.com/npm/npm-registry-fetch/pull/264) `minipass-fetch@4.0.0`
51 | * [`351e1f4`](https://github.com/npm/npm-registry-fetch/commit/351e1f42371de69bac3e560595f62b14d28a8e28) [#264](https://github.com/npm/npm-registry-fetch/pull/264) `@npmcli/redact@3.0.0`
52 | ### Chores
53 | * [`bd5f617`](https://github.com/npm/npm-registry-fetch/commit/bd5f61742fe304c0e366131108337e68711fe4a2) [#262](https://github.com/npm/npm-registry-fetch/pull/262) bump ssri from 10.0.6 to 12.0.0 (#262) (@dependabot[bot])
54 | * [`60a396a`](https://github.com/npm/npm-registry-fetch/commit/60a396a095e08d72bd56190d3a31e3e0d63f575e) [#264](https://github.com/npm/npm-registry-fetch/pull/264) run template-oss-apply (@reggi)
55 | * [`0a9f05b`](https://github.com/npm/npm-registry-fetch/commit/0a9f05be03a8410edffbbf3dc121a930c21535ac) [#256](https://github.com/npm/npm-registry-fetch/pull/256) bump @npmcli/eslint-config from 4.0.5 to 5.0.0 (@dependabot[bot])
56 | * [`4317115`](https://github.com/npm/npm-registry-fetch/commit/4317115fe040d701121c65bc57a053caafcafb8b) [#257](https://github.com/npm/npm-registry-fetch/pull/257) postinstall for dependabot template-oss PR (@hashtagchris)
57 | * [`81080b9`](https://github.com/npm/npm-registry-fetch/commit/81080b9f53cc4dea3aeab2b38396527756011902) [#257](https://github.com/npm/npm-registry-fetch/pull/257) bump @npmcli/template-oss from 4.23.1 to 4.23.3 (@dependabot[bot])
58 |
59 | ## [17.1.0](https://github.com/npm/npm-registry-fetch/compare/v17.0.1...v17.1.0) (2024-06-12)
60 |
61 | ### Features
62 |
63 | * [`29712af`](https://github.com/npm/npm-registry-fetch/commit/29712af1ff756d652313710c0c54a82961a0d038) [#246](https://github.com/npm/npm-registry-fetch/pull/246) merging functionality from minipass-json-stream (@wraithgar)
64 |
65 | ### Dependencies
66 |
67 | * [`9a3e7e8`](https://github.com/npm/npm-registry-fetch/commit/9a3e7e8f1644028685e97d27412592a215799afd) [#246](https://github.com/npm/npm-registry-fetch/pull/246) remove minipass-json-stream
68 | * [`089d0f9`](https://github.com/npm/npm-registry-fetch/commit/089d0f9749fa42a4ecb56e6d5cfdbc7b51dbf817) [#246](https://github.com/npm/npm-registry-fetch/pull/246) add `jsonparse@1.3.1`
69 |
70 | ### Chores
71 |
72 | * [`920a3d8`](https://github.com/npm/npm-registry-fetch/commit/920a3d843fc16938b4430a1619f3d5cf41f8bac9) [#241](https://github.com/npm/npm-registry-fetch/pull/241) bump @npmcli/template-oss to 4.22.0 (@lukekarrys)
73 | * [`17a1013`](https://github.com/npm/npm-registry-fetch/commit/17a10133f95f4ee163c281cd5c1e1ffc2b9a109c) [#241](https://github.com/npm/npm-registry-fetch/pull/241) postinstall for dependabot template-oss PR (@lukekarrys)
74 |
75 | ## [17.0.1](https://github.com/npm/npm-registry-fetch/compare/v17.0.0...v17.0.1) (2024-05-02)
76 |
77 | ### Bug Fixes
78 |
79 | * [`45cef0a`](https://github.com/npm/npm-registry-fetch/commit/45cef0a7c4c1dfa7fc3c78ef3807029065bcf45b) [#239](https://github.com/npm/npm-registry-fetch/pull/239) allow HttpErrorBase to take headers object (@lukekarrys, @wraithgar)
80 | * [`45cef0a`](https://github.com/npm/npm-registry-fetch/commit/45cef0a7c4c1dfa7fc3c78ef3807029065bcf45b) [#239](https://github.com/npm/npm-registry-fetch/pull/239) make ErrorBase always capture stack trace (#239) (@lukekarrys, @wraithgar)
81 |
82 | ## [17.0.0](https://github.com/npm/npm-registry-fetch/compare/v16.2.1...v17.0.0) (2024-04-30)
83 |
84 | ### ⚠️ BREAKING CHANGES
85 |
86 | * remove undcoumented cleanUrl export (#234)
87 |
88 | ### Features
89 |
90 | * [`105f786`](https://github.com/npm/npm-registry-fetch/commit/105f7865bf0da8bdb2e29dffa92c0fc2e93debc5) [#234](https://github.com/npm/npm-registry-fetch/pull/234) remove undcoumented cleanUrl export (#234) (@lukekarrys)
91 |
92 | ### Dependencies
93 |
94 | * [`7cc481b`](https://github.com/npm/npm-registry-fetch/commit/7cc481b8763ac60dc3fea7e14870a36f74e3b4d2) [#238](https://github.com/npm/npm-registry-fetch/pull/238) `@npmcli/redact@2.0.0`
95 |
96 | ### Chores
97 |
98 | * [`bdc9039`](https://github.com/npm/npm-registry-fetch/commit/bdc9039538a07cb6d7f64873fa5ee1ba638e4c8f) [#238](https://github.com/npm/npm-registry-fetch/pull/238) fix linting in test files (@wraithgar)
99 | * [`ceaf77e`](https://github.com/npm/npm-registry-fetch/commit/ceaf77ece4eb71a4ab1f9542bf4e75aec0dd4b05) [#236](https://github.com/npm/npm-registry-fetch/pull/236) postinstall for dependabot template-oss PR (@lukekarrys)
100 | * [`377d981`](https://github.com/npm/npm-registry-fetch/commit/377d981cb6165adec1a9b96627c1431604400a68) [#236](https://github.com/npm/npm-registry-fetch/pull/236) bump @npmcli/template-oss from 4.21.3 to 4.21.4 (@dependabot[bot])
101 |
102 | ## [16.2.1](https://github.com/npm/npm-registry-fetch/compare/v16.2.0...v16.2.1) (2024-04-12)
103 |
104 | ### Dependencies
105 |
106 | * [`7a18f69`](https://github.com/npm/npm-registry-fetch/commit/7a18f69e303a680b991fa225fbe3800c9222fd8c) [#232](https://github.com/npm/npm-registry-fetch/pull/232) `proc-log@4.0.0` (#232)
107 |
108 | ## [16.2.0](https://github.com/npm/npm-registry-fetch/compare/v16.1.0...v16.2.0) (2024-04-03)
109 |
110 | ### Features
111 |
112 | * [`76b02e8`](https://github.com/npm/npm-registry-fetch/commit/76b02e8356a49c81d4fff32a1470778ccabf8477) [#231](https://github.com/npm/npm-registry-fetch/pull/231) use @npmcli/redact for url cleaning (#231) (@lukekarrys)
113 |
114 | ### Chores
115 |
116 | * [`e58e8bc`](https://github.com/npm/npm-registry-fetch/commit/e58e8bc6c5b25d53a764f58190fc3a5c764a2e78) [#225](https://github.com/npm/npm-registry-fetch/pull/225) postinstall for dependabot template-oss PR (@lukekarrys)
117 | * [`ac8ae30`](https://github.com/npm/npm-registry-fetch/commit/ac8ae309a436fa075bd8526d8e39ed017b41067f) [#225](https://github.com/npm/npm-registry-fetch/pull/225) bump @npmcli/template-oss from 4.21.1 to 4.21.3 (@dependabot[bot])
118 | * [`8fc28d0`](https://github.com/npm/npm-registry-fetch/commit/8fc28d0722fcf05feda4847bcb3ba8825d3cb51a) [#222](https://github.com/npm/npm-registry-fetch/pull/222) postinstall for dependabot template-oss PR (@lukekarrys)
119 | * [`b7c4309`](https://github.com/npm/npm-registry-fetch/commit/b7c43093733ff53deb27f99f9c6fc736a8b33bbd) [#222](https://github.com/npm/npm-registry-fetch/pull/222) bump @npmcli/template-oss from 4.19.0 to 4.21.1 (@dependabot[bot])
120 |
121 | ## [16.1.0](https://github.com/npm/npm-registry-fetch/compare/v16.0.0...v16.1.0) (2023-10-10)
122 |
123 | ### Features
124 |
125 | * [`84823a5`](https://github.com/npm/npm-registry-fetch/commit/84823a5c85b010f62d9a9f3b890fdee0d1d6f80a) [#212](https://github.com/npm/npm-registry-fetch/pull/212) include regKey and authKey in auth object (@wraithgar)
126 | * [`92ec0da`](https://github.com/npm/npm-registry-fetch/commit/92ec0dacb20af1b32ae5f07cf59440ade21b25e7) [#212](https://github.com/npm/npm-registry-fetch/pull/212) add getAuth to main exports (@wraithgar)
127 |
128 | ## [16.0.0](https://github.com/npm/npm-registry-fetch/compare/v15.0.0...v16.0.0) (2023-08-15)
129 |
130 | ### ⚠️ BREAKING CHANGES
131 |
132 | * support for node <=16.13 has been removed
133 |
134 | ### Bug Fixes
135 |
136 | * [`4c0be5e`](https://github.com/npm/npm-registry-fetch/commit/4c0be5ea200fd48994fb0a39f19d451fde8a9b30) [#199](https://github.com/npm/npm-registry-fetch/pull/199) drop node 16.13.x support (@lukekarrys)
137 |
138 | ### Dependencies
139 |
140 | * [`c859195`](https://github.com/npm/npm-registry-fetch/commit/c8591951477b66149bb0663b3e26d054a3bafcef) [#197](https://github.com/npm/npm-registry-fetch/pull/197) bump npm-package-arg from 10.1.0 to 11.0.0
141 | * [`c1d490d`](https://github.com/npm/npm-registry-fetch/commit/c1d490dd63a2268aa17adc14420278ceb1468e0e) [#198](https://github.com/npm/npm-registry-fetch/pull/198) bump make-fetch-happen from 12.0.0 to 13.0.0
142 |
143 | ## [15.0.0](https://github.com/npm/npm-registry-fetch/compare/v14.0.5...v15.0.0) (2023-07-28)
144 |
145 | ### ⚠️ BREAKING CHANGES
146 |
147 | * the underlying fetch module now uses `@npmcli/agent`. Backwards compatibility should be fully implemented but due to the scope of this change it was made a breaking change out of an abundance of caution.
148 | * support for node 14 has been removed
149 |
150 | ### Bug Fixes
151 |
152 | * [`b875c26`](https://github.com/npm/npm-registry-fetch/commit/b875c269f35da1a878c3dc353d622a07c3257c7c) [#193](https://github.com/npm/npm-registry-fetch/pull/193) drop node14 support (#193) (@wraithgar)
153 |
154 | ### Dependencies
155 |
156 | * [`a97564f`](https://github.com/npm/npm-registry-fetch/commit/a97564fac4fc1f8ff76b325906583c8d4d207eb3) [#195](https://github.com/npm/npm-registry-fetch/pull/195) bump make-fetch-happen from 11.1.1 to 12.0.0 (#195)
157 | * [`e154d49`](https://github.com/npm/npm-registry-fetch/commit/e154d4918aa16495d01bdd7232221d2ae87d3c3d) [#191](https://github.com/npm/npm-registry-fetch/pull/191) bump minipass from 5.0.0 to 7.0.2
158 |
159 | ## [14.0.5](https://github.com/npm/npm-registry-fetch/compare/v14.0.4...v14.0.5) (2023-04-27)
160 |
161 | ### Dependencies
162 |
163 | * [`a2d5880`](https://github.com/npm/npm-registry-fetch/commit/a2d5880ba09bfdf1ec67aed0bca1a68e5db9786c) [#177](https://github.com/npm/npm-registry-fetch/pull/177) bump minipass from 4.2.7 to 5.0.0 (#177)
164 |
165 | ## [14.0.4](https://github.com/npm/npm-registry-fetch/compare/v14.0.3...v14.0.4) (2023-04-13)
166 |
167 | ### Bug Fixes
168 |
169 | * [`15dd221`](https://github.com/npm/npm-registry-fetch/commit/15dd2216a52393fbce2246c071045c6597c922ea) [#178](https://github.com/npm/npm-registry-fetch/pull/178) clean password by using url object itself (#178) (@DEMON1A)
170 |
171 | ### Documentation
172 |
173 | * [`14d1159`](https://github.com/npm/npm-registry-fetch/commit/14d11597d499882aba36403300b128bee19a8b53) [#173](https://github.com/npm/npm-registry-fetch/pull/173) update API documentation of noProxy option (#173) (@lingyuncai)
174 |
175 | ## [14.0.3](https://github.com/npm/npm-registry-fetch/compare/v14.0.2...v14.0.3) (2022-12-07)
176 |
177 | ### Dependencies
178 |
179 | * [`c669335`](https://github.com/npm/npm-registry-fetch/commit/c669335c163439d7b860967154249713f51253d8) [#158](https://github.com/npm/npm-registry-fetch/pull/158) bump minipass from 3.3.6 to 4.0.0
180 |
181 | ## [14.0.2](https://github.com/npm/npm-registry-fetch/compare/v14.0.1...v14.0.2) (2022-10-18)
182 |
183 | ### Dependencies
184 |
185 | * [`36b7685`](https://github.com/npm/npm-registry-fetch/commit/36b768515b37af9e1dac22c5ef211f64e279461e) [#154](https://github.com/npm/npm-registry-fetch/pull/154) bump npm-package-arg from 9.1.2 to 10.0.0
186 |
187 | ## [14.0.1](https://github.com/npm/npm-registry-fetch/compare/v14.0.0...v14.0.1) (2022-10-17)
188 |
189 | ### Dependencies
190 |
191 | * [`ade1c8b`](https://github.com/npm/npm-registry-fetch/commit/ade1c8b03ee694e6dc4025805cb3d59eca768c43) [#150](https://github.com/npm/npm-registry-fetch/pull/150) bump minipass-fetch from 2.1.2 to 3.0.0
192 | * [`84bb850`](https://github.com/npm/npm-registry-fetch/commit/84bb850f63af899d471f7761cdb2cb17c53e3784) [#149](https://github.com/npm/npm-registry-fetch/pull/149) bump proc-log from 2.0.1 to 3.0.0
193 |
194 | ## [14.0.0](https://github.com/npm/npm-registry-fetch/compare/v13.3.1...v14.0.0) (2022-10-13)
195 |
196 | ### ⚠️ BREAKING CHANGES
197 |
198 | * this module no longer attempts to change file ownership automatically
199 | * `npm-registry-fetch` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0`
200 |
201 | ### Features
202 |
203 | * [`104a51f`](https://github.com/npm/npm-registry-fetch/commit/104a51f869dd97e3decca389f742c920f29687d0) [#138](https://github.com/npm/npm-registry-fetch/pull/138) postinstall for dependabot template-oss PR (@lukekarrys)
204 |
205 | ### Dependencies
206 |
207 | * [`b5aeed0`](https://github.com/npm/npm-registry-fetch/commit/b5aeed0cb4e639b6460fb1419f0bb86eb45ddcb3) [#146](https://github.com/npm/npm-registry-fetch/pull/146) bump make-fetch-happen from 10.2.1 to 11.0.0 (#146)
208 |
209 | ## [13.3.1](https://github.com/npm/npm-registry-fetch/compare/v13.3.0...v13.3.1) (2022-08-15)
210 |
211 |
212 | ### Bug Fixes
213 |
214 | * linting ([c9a8727](https://github.com/npm/npm-registry-fetch/commit/c9a8727f026367a86c50d103035f1c02d89431fd))
215 |
216 | ## [13.3.0](https://github.com/npm/npm-registry-fetch/compare/v13.2.0...v13.3.0) (2022-07-18)
217 |
218 |
219 | ### Features
220 |
221 | * respect registry-scoped certfile and keyfile options ([#125](https://github.com/npm/npm-registry-fetch/issues/125)) ([42d605c](https://github.com/npm/npm-registry-fetch/commit/42d605cb00986c6775e6ec1732b114e065266ae9))
222 |
223 | ## [13.2.0](https://github.com/npm/npm-registry-fetch/compare/v13.1.1...v13.2.0) (2022-06-29)
224 |
225 |
226 | ### Features
227 |
228 | * set 'npm-auth-type' header depending on config option ([#123](https://github.com/npm/npm-registry-fetch/issues/123)) ([ff4ed65](https://github.com/npm/npm-registry-fetch/commit/ff4ed65b04127dea40c1ebce741ac91088deaf1a))
229 |
230 | ### [13.1.1](https://github.com/npm/npm-registry-fetch/compare/v13.1.0...v13.1.1) (2022-04-13)
231 |
232 |
233 | ### Bug Fixes
234 |
235 | * replace deprecated String.prototype.substr() ([#115](https://github.com/npm/npm-registry-fetch/issues/115)) ([804411f](https://github.com/npm/npm-registry-fetch/commit/804411f345be9737e5edadffb0e686bc7263ce1d))
236 |
237 | ## [13.1.0](https://www.github.com/npm/npm-registry-fetch/compare/v13.0.1...v13.1.0) (2022-03-22)
238 |
239 |
240 | ### Features
241 |
242 | * clean token from logged urls ([#107](https://www.github.com/npm/npm-registry-fetch/issues/107)) ([9894911](https://www.github.com/npm/npm-registry-fetch/commit/989491178115ad3bb898b7c681ab6cad4293e8f2))
243 |
244 |
245 | ### Dependencies
246 |
247 | * update make-fetch-happen requirement from ^10.0.3 to ^10.0.4 ([#96](https://www.github.com/npm/npm-registry-fetch/issues/96)) ([38d9782](https://www.github.com/npm/npm-registry-fetch/commit/38d978297c1f1183da1b1c55f3ea9e670a9934d3))
248 | * update make-fetch-happen requirement from ^10.0.4 to ^10.0.6 ([#101](https://www.github.com/npm/npm-registry-fetch/issues/101)) ([1d2f3ed](https://www.github.com/npm/npm-registry-fetch/commit/1d2f3edec1a0399ba046df8662cc1097da2be434))
249 | * update minipass-fetch requirement from ^2.0.1 to ^2.0.2 ([#95](https://www.github.com/npm/npm-registry-fetch/issues/95)) ([d8c3180](https://www.github.com/npm/npm-registry-fetch/commit/d8c3180d0b8a584a9024b4b6f9a34cb00a9e8523))
250 | * update minipass-fetch requirement from ^2.0.2 to ^2.0.3 ([#99](https://www.github.com/npm/npm-registry-fetch/issues/99)) ([3e08986](https://www.github.com/npm/npm-registry-fetch/commit/3e08986b4283f88719a2c92f058177a3123f81e4))
251 | * update npm-package-arg requirement from ^9.0.0 to ^9.0.1 ([#102](https://www.github.com/npm/npm-registry-fetch/issues/102)) ([a6192b4](https://www.github.com/npm/npm-registry-fetch/commit/a6192b48b2dc95f488cf479e448ff97fc1bc5d4a))
252 |
253 | ### [13.0.1](https://www.github.com/npm/npm-registry-fetch/compare/v13.0.0...v13.0.1) (2022-03-02)
254 |
255 |
256 | ### Dependencies
257 |
258 | * bump minipass-fetch from 1.4.1 to 2.0.1 ([#92](https://www.github.com/npm/npm-registry-fetch/issues/92)) ([33d0ecd](https://www.github.com/npm/npm-registry-fetch/commit/33d0ecd411e4af03db2a2adf8db2f21ea72e2c42))
259 | * update make-fetch-happen requirement from ^10.0.2 to ^10.0.3 ([ee38552](https://www.github.com/npm/npm-registry-fetch/commit/ee38552b304eafd706b8a67093f1904c57ca1af3))
260 |
261 | ## [13.0.0](https://www.github.com/npm/npm-registry-fetch/compare/v12.0.2...v13.0.0) (2022-02-14)
262 |
263 |
264 | ### ⚠ BREAKING CHANGES
265 |
266 | * this drops support for passing in a `log` property. All logs are now emitted on the process object via `proc-log`
267 |
268 | ### Features
269 |
270 | * use proc-log and drop npmlog support ([#85](https://www.github.com/npm/npm-registry-fetch/issues/85)) ([db90766](https://www.github.com/npm/npm-registry-fetch/commit/db907663bcde5871dc99840a7dab5358a8fc410e))
271 |
272 |
273 | ### Dependencies
274 |
275 | * bump npm-package-arg from 8.1.5 to 9.0.0 ([0b41730](https://www.github.com/npm/npm-registry-fetch/commit/0b41730bc55e82180d369b8a4a7a198c16f26d34))
276 | * update make-fetch-happen requirement from ^10.0.1 to ^10.0.2 ([6644733](https://www.github.com/npm/npm-registry-fetch/commit/6644733199ab7569d2452174b9a467193b7171a8))
277 |
278 | ### [12.0.2](https://www.github.com/npm/npm-registry-fetch/compare/v12.0.1...v12.0.2) (2022-02-09)
279 |
280 |
281 | ### Bug Fixes
282 |
283 | * consistent use of url.URL ([847e5f9](https://www.github.com/npm/npm-registry-fetch/commit/847e5f93cc3c468ed1fdecfd00aba5ef1fa37b16))
284 |
285 |
286 | ### Dependencies
287 |
288 | * update make-fetch-happen requirement from ^10.0.0 to ^10.0.1 ([d1a2a7f](https://www.github.com/npm/npm-registry-fetch/commit/d1a2a7f72af3650879e5bfc7c094830eaa59181c))
289 | * update minipass requirement from ^3.1.3 to ^3.1.6 ([caa4309](https://www.github.com/npm/npm-registry-fetch/commit/caa43093d382be9a54a9e22c5f1ab8fa834f0612))
290 | * update minipass-fetch requirement from ^1.3.0 to ^1.4.1 ([52f2ca9](https://www.github.com/npm/npm-registry-fetch/commit/52f2ca98b79969f9ba19df38bf1aee79c04518e6))
291 | * update minizlib requirement from ^2.0.0 to ^2.1.2 ([9258e8a](https://www.github.com/npm/npm-registry-fetch/commit/9258e8a06fd9458c22584cf267b2d4d18751659a))
292 | * update npm-package-arg requirement from ^8.0.0 to ^8.1.5 ([131ab16](https://www.github.com/npm/npm-registry-fetch/commit/131ab16408867e987e144d1267dee086c5fa8507))
293 |
294 |
295 | ### Documentation
296 |
297 | * rename token to _authToken ([d615b4e](https://www.github.com/npm/npm-registry-fetch/commit/d615b4e6cf284c4a8f39b24700beacbf07dfec5d))
298 |
299 | ### [12.0.1](https://www.github.com/npm/npm-registry-fetch/compare/v12.0.0...v12.0.1) (2022-01-25)
300 |
301 |
302 | ### dependencies
303 |
304 | * @npmcli/template-oss@2.5.1 ([cc4cc11](https://www.github.com/npm/npm-registry-fetch/commit/cc4cc11050a3b0e35f6bc59f5dd49a957f7a0569))
305 | * make-fetch-happen@10.0.0 ([6926dd1](https://www.github.com/npm/npm-registry-fetch/commit/6926dd1270b6c1292cf9adb25429db77755ff5d6))
306 |
307 | ### [12.0.0](https://github.com/npm/registry-fetch/compare/v11.0.0...v12.0.0) (2021-11-23)
308 |
309 | ### ⚠ BREAKING CHANGES
310 |
311 | * feat(opts): use scope instead of projectScope
312 | * drop support for node10 and non-LTS versions of node12 and node14
313 |
314 | ### [11.0.0](https://github.com/npm/registry-fetch/compare/v10.1.2...v11.0.0)
315 |
316 | ### ⚠ BREAKING CHANGES
317 |
318 | * remove handling of deprecated warning headers (#53)
319 |
320 | ### Features
321 |
322 | * better cache status (#54)
323 |
324 | ### Bug Fixes
325 |
326 | * docs: fix header typo for npm-command (#51)
327 | * fix(docs): update registry param (#52)
328 |
329 | ### [10.1.2](https://github.com/npm/registry-fetch/compare/v10.1.1...v10.1.2)
330 |
331 | ### Bug Fixes
332 |
333 | * fix: get auth token correct when login with sso
334 |
335 | ### [10.1.1](https://github.com/npm/registry-fetch/compare/v10.1.0...v10.1.1)
336 |
337 | ### Bug Fixes
338 |
339 | * Send auth when hostname matches registry, and reg has auth
340 |
341 | ### [10.1.1](https://github.com/npm/registry-fetch/compare/v10.0.0...v10.1.0)
342 |
343 | ### Features
344 |
345 | * feat(auth): set basicAuth (#45)
346 |
347 | ### [10.0.0](https://github.com/npm/registry-fetch/compare/v9.0.0...v10.0.0)
348 |
349 | ### ⚠ BREAKING CHANGES
350 |
351 | * feat(auth) load/send based on URI, not registry
352 |
353 | ### Features:
354 |
355 | * feat(otp): Adds opts.otpPrompt to provide an OTP on demand
356 |
357 | ### Bug Fixes
358 |
359 | * fix(config): remove isFromCI and npm-is-ci
360 |
361 | ### [9.0.0](https://github.com/npm/registry-fetch/compare/v8.1.4...v9.0.0)
362 |
363 | ### ⚠ BREAKING CHANGES
364 |
365 | * Remove publishConfig option
366 |
367 | ### [8.1.5](https://github.com/npm/registry-fetch/compare/v8.1.4...v8.1.5) (2020-10-12)
368 |
369 |
370 | ### Bug Fixes
371 |
372 | * respect publishConfig.registry when specified ([32e36ef](https://github.com/npm/registry-fetch/commit/32e36efe86302ed319973cd5b1e6ccc3f62e557e)), closes [#35](https://github.com/npm/registry-fetch/issues/35)
373 |
374 | ### [8.1.4](https://github.com/npm/registry-fetch/compare/v8.1.3...v8.1.4) (2020-08-17)
375 |
376 |
377 | ### Bug Fixes
378 |
379 | * redact passwords from http logs ([3c294eb](https://github.com/npm/registry-fetch/commit/3c294ebbd7821725db4ff1bc5fe368c49613efcc))
380 |
381 | ### [8.1.3](https://github.com/npm/registry-fetch/compare/v8.1.2...v8.1.3) (2020-07-21)
382 |
383 | ### [8.1.2](https://github.com/npm/registry-fetch/compare/v8.1.1...v8.1.2) (2020-07-11)
384 |
385 | ### [8.1.1](https://github.com/npm/registry-fetch/compare/v8.1.0...v8.1.1) (2020-06-30)
386 |
387 | ## [8.1.0](https://github.com/npm/registry-fetch/compare/v8.0.3...v8.1.0) (2020-05-20)
388 |
389 |
390 | ### Features
391 |
392 | * add npm-command HTTP header ([1bb4eb2](https://github.com/npm/registry-fetch/commit/1bb4eb2c66ee8a0dc62558bdcff1b548e2bb9820))
393 |
394 | ### [8.0.3](https://github.com/npm/registry-fetch/compare/v8.0.2...v8.0.3) (2020-05-13)
395 |
396 |
397 | ### Bug Fixes
398 |
399 | * update minipass and make-fetch-happen to latest ([3b6c5d0](https://github.com/npm/registry-fetch/commit/3b6c5d0d8ccd4c4a97862a65acef956f19aec127)), closes [#23](https://github.com/npm/registry-fetch/issues/23)
400 |
401 | ### [8.0.2](https://github.com/npm/registry-fetch/compare/v8.0.1...v8.0.2) (2020-05-04)
402 |
403 |
404 | ### Bug Fixes
405 |
406 | * update make-fetch-happen to 8.0.6 ([226df2c](https://github.com/npm/registry-fetch/commit/226df2c32e3f9ed8ceefcfdbd11efb178181b442))
407 |
408 | ## [8.0.0](https://github.com/npm/registry-fetch/compare/v7.0.1...v8.0.0) (2020-02-24)
409 |
410 |
411 | ### ⚠ BREAKING CHANGES
412 |
413 | * Removes the 'opts.refer' option and the HTTP Referer
414 | header (unless explicitly added to the 'headers' option, of course).
415 |
416 | PR-URL: https://github.com/npm/npm-registry-fetch/pull/25
417 | Credit: @isaacs
418 |
419 | ### Bug Fixes
420 |
421 | * remove referer header and opts.refer ([eb8f7af](https://github.com/npm/registry-fetch/commit/eb8f7af3c102834856604c1be664b00ca0fe8ef2)), closes [#25](https://github.com/npm/registry-fetch/issues/25)
422 |
423 | ### [7.0.1](https://github.com/npm/registry-fetch/compare/v7.0.0...v7.0.1) (2020-02-24)
424 |
425 | ## [7.0.0](https://github.com/npm/registry-fetch/compare/v6.0.2...v7.0.0) (2020-02-18)
426 |
427 |
428 | ### ⚠ BREAKING CHANGES
429 |
430 | * figgy pudding is now nowhere to be found.
431 | * this removes figgy-pudding, and drops several option
432 | aliases.
433 |
434 | Defaults and behavior are all the same, and this module is now using the
435 | canonical camelCase option names that npm v7 will provide to all its
436 | deps.
437 |
438 | Related to: https://github.com/npm/rfcs/pull/102
439 |
440 | PR-URL: https://github.com/npm/npm-registry-fetch/pull/22
441 | Credit: @isaacs
442 |
443 | ### Bug Fixes
444 |
445 | * Remove figgy-pudding, use canonical option names ([ede3c08](https://github.com/npm/registry-fetch/commit/ede3c087007fd1808e02b1af70562220d03b18a9)), closes [#22](https://github.com/npm/registry-fetch/issues/22)
446 |
447 |
448 | * update cacache, ssri, make-fetch-happen ([57fcc88](https://github.com/npm/registry-fetch/commit/57fcc889bee03edcc0a2025d96a171039108c231))
449 |
450 | ### [6.0.2](https://github.com/npm/registry-fetch/compare/v6.0.1...v6.0.2) (2020-02-14)
451 |
452 |
453 | ### Bug Fixes
454 |
455 | * always bypass cache when ?write=true ([83f89f3](https://github.com/npm/registry-fetch/commit/83f89f35abd2ed0507c869e37f90ed746375772c))
456 |
457 | ### [6.0.1](https://github.com/npm/registry-fetch/compare/v6.0.0...v6.0.1) (2020-02-14)
458 |
459 |
460 | ### Bug Fixes
461 |
462 | * use 30s default for timeout as per README ([50e8afc](https://github.com/npm/registry-fetch/commit/50e8afc6ff850542feb588f9f9c64ebae59e72a0)), closes [#20](https://github.com/npm/registry-fetch/issues/20)
463 |
464 | ## [6.0.0](https://github.com/npm/registry-fetch/compare/v5.0.1...v6.0.0) (2019-12-17)
465 |
466 |
467 | ### ⚠ BREAKING CHANGES
468 |
469 | * This drops support for node < 10.
470 |
471 | There are some lint failures due to standard pushing for using WhatWG URL
472 | objects instead of url.parse/url.resolve. However, the code in this lib
473 | does some fancy things with the query/search portions of the parsed url
474 | object, so it'll take a bit of care to make it work properly.
475 |
476 | ### Bug Fixes
477 |
478 | * detect CI so our tests don't fail in CI ([5813da6](https://github.com/npm/registry-fetch/commit/5813da634cef73b12e40373972d7937e6934fce0))
479 | * Use WhatWG URLs instead of url.parse ([8ccfa8a](https://github.com/npm/registry-fetch/commit/8ccfa8a72c38cfedb0f525b7f453644fd4444f99))
480 |
481 |
482 | * normalize settings, drop old nodes, update deps ([510b125](https://github.com/npm/registry-fetch/commit/510b1255cc7ed4bb397a34e0007757dae33e2275))
483 |
484 |
485 | ## [5.0.1](https://github.com/npm/registry-fetch/compare/v5.0.0...v5.0.1) (2019-11-11)
486 |
487 |
488 |
489 |
490 | # [5.0.0](https://github.com/npm/registry-fetch/compare/v4.0.2...v5.0.0) (2019-10-04)
491 |
492 |
493 | ### Bug Fixes
494 |
495 | * prefer const in getAuth function ([90ac7b1](https://github.com/npm/registry-fetch/commit/90ac7b1))
496 | * use minizlib instead of core zlib ([e64702e](https://github.com/npm/registry-fetch/commit/e64702e))
497 |
498 |
499 | ### Features
500 |
501 | * refactor to use Minipass streams ([bb37f20](https://github.com/npm/registry-fetch/commit/bb37f20))
502 |
503 |
504 | ### BREAKING CHANGES
505 |
506 | * this replaces all core streams (except for some
507 | PassThrough streams in a few tests) with Minipass streams, and updates
508 | all deps to the latest and greatest Minipass versions of things.
509 |
510 |
511 |
512 |
513 | ## [4.0.2](https://github.com/npm/registry-fetch/compare/v4.0.0...v4.0.2) (2019-10-04)
514 |
515 |
516 | ### Bug Fixes
517 |
518 | * Add null check on body on 401 errors ([e3a0186](https://github.com/npm/registry-fetch/commit/e3a0186)), closes [#9](https://github.com/npm/registry-fetch/issues/9)
519 | * **deps:** Add explicit dependency on safe-buffer ([8eae5f0](https://github.com/npm/registry-fetch/commit/8eae5f0)), closes [npm/libnpmaccess#2](https://github.com/npm/libnpmaccess/issues/2) [#3](https://github.com/npm/registry-fetch/issues/3)
520 |
521 |
522 |
523 |
524 | # [4.0.0](https://github.com/npm/registry-fetch/compare/v3.9.1...v4.0.0) (2019-07-15)
525 |
526 |
527 | * cacache@12.0.0, infer uid from cache folder ([0c4f060](https://github.com/npm/registry-fetch/commit/0c4f060))
528 |
529 |
530 | ### BREAKING CHANGES
531 |
532 | * uid and gid are inferred from cache folder, rather than
533 | being passed in as options.
534 |
535 |
536 |
537 |
538 | ## [3.9.1](https://github.com/npm/registry-fetch/compare/v3.9.0...v3.9.1) (2019-07-02)
539 |
540 |
541 |
542 |
543 | # [3.9.0](https://github.com/npm/registry-fetch/compare/v3.8.0...v3.9.0) (2019-01-24)
544 |
545 |
546 | ### Features
547 |
548 | * **auth:** support username:password encoded legacy _auth ([a91f90c](https://github.com/npm/registry-fetch/commit/a91f90c))
549 |
550 |
551 |
552 |
553 | # [3.8.0](https://github.com/npm/registry-fetch/compare/v3.7.0...v3.8.0) (2018-08-23)
554 |
555 |
556 | ### Features
557 |
558 | * **mapJson:** add support for passing in json stream mapper ([0600986](https://github.com/npm/registry-fetch/commit/0600986))
559 |
560 |
561 |
562 |
563 | # [3.7.0](https://github.com/npm/registry-fetch/compare/v3.6.0...v3.7.0) (2018-08-23)
564 |
565 |
566 | ### Features
567 |
568 | * **json.stream:** add utility function for streamed JSON parsing ([051d969](https://github.com/npm/registry-fetch/commit/051d969))
569 |
570 |
571 |
572 |
573 | # [3.6.0](https://github.com/npm/registry-fetch/compare/v3.5.0...v3.6.0) (2018-08-22)
574 |
575 |
576 | ### Bug Fixes
577 |
578 | * **docs:** document opts.forceAuth ([40bcd65](https://github.com/npm/registry-fetch/commit/40bcd65))
579 |
580 |
581 | ### Features
582 |
583 | * **opts.ignoreBody:** add a boolean to throw away response bodies ([6923702](https://github.com/npm/registry-fetch/commit/6923702))
584 |
585 |
586 |
587 |
588 | # [3.5.0](https://github.com/npm/registry-fetch/compare/v3.4.0...v3.5.0) (2018-08-22)
589 |
590 |
591 | ### Features
592 |
593 | * **pkgid:** heuristic pkgid calculation for errors ([2e789a5](https://github.com/npm/registry-fetch/commit/2e789a5))
594 |
595 |
596 |
597 |
598 | # [3.4.0](https://github.com/npm/registry-fetch/compare/v3.3.0...v3.4.0) (2018-08-22)
599 |
600 |
601 | ### Bug Fixes
602 |
603 | * **deps:** use new figgy-pudding with aliases fix ([0308f54](https://github.com/npm/registry-fetch/commit/0308f54))
604 |
605 |
606 | ### Features
607 |
608 | * **auth:** add forceAuth option to force a specific auth mechanism ([4524d17](https://github.com/npm/registry-fetch/commit/4524d17))
609 |
610 |
611 |
612 |
613 | # [3.3.0](https://github.com/npm/registry-fetch/compare/v3.2.1...v3.3.0) (2018-08-21)
614 |
615 |
616 | ### Bug Fixes
617 |
618 | * **query:** stop including undefined keys ([4718b1b](https://github.com/npm/registry-fetch/commit/4718b1b))
619 |
620 |
621 | ### Features
622 |
623 | * **otp:** use heuristic detection for malformed EOTP responses ([f035194](https://github.com/npm/registry-fetch/commit/f035194))
624 |
625 |
626 |
627 |
628 | ## [3.2.1](https://github.com/npm/registry-fetch/compare/v3.2.0...v3.2.1) (2018-08-16)
629 |
630 |
631 | ### Bug Fixes
632 |
633 | * **opts:** pass through non-null opts.retry ([beba040](https://github.com/npm/registry-fetch/commit/beba040))
634 |
635 |
636 |
637 |
638 | # [3.2.0](https://github.com/npm/registry-fetch/compare/v3.1.1...v3.2.0) (2018-07-27)
639 |
640 |
641 | ### Features
642 |
643 | * **gzip:** add opts.gzip convenience opt ([340abe0](https://github.com/npm/registry-fetch/commit/340abe0))
644 |
645 |
646 |
647 |
648 | ## [3.1.1](https://github.com/npm/registry-fetch/compare/v3.1.0...v3.1.1) (2018-04-09)
649 |
650 |
651 |
652 |
653 | # [3.1.0](https://github.com/npm/registry-fetch/compare/v3.0.0...v3.1.0) (2018-04-09)
654 |
655 |
656 | ### Features
657 |
658 | * **config:** support no-proxy and https-proxy options ([9aa906b](https://github.com/npm/registry-fetch/commit/9aa906b))
659 |
660 |
661 |
662 |
663 | # [3.0.0](https://github.com/npm/registry-fetch/compare/v2.1.0...v3.0.0) (2018-04-09)
664 |
665 |
666 | ### Bug Fixes
667 |
668 | * **api:** pacote integration-related fixes ([a29de4f](https://github.com/npm/registry-fetch/commit/a29de4f))
669 | * **config:** stop caring about opts.config ([5856a6f](https://github.com/npm/registry-fetch/commit/5856a6f))
670 |
671 |
672 | ### BREAKING CHANGES
673 |
674 | * **config:** opts.config is no longer supported. Pass the options down in opts itself.
675 |
676 |
677 |
678 |
679 | # [2.1.0](https://github.com/npm/registry-fetch/compare/v2.0.0...v2.1.0) (2018-04-08)
680 |
681 |
682 | ### Features
683 |
684 | * **token:** accept opts.token for opts._authToken ([108c9f0](https://github.com/npm/registry-fetch/commit/108c9f0))
685 |
686 |
687 |
688 |
689 | # [2.0.0](https://github.com/npm/registry-fetch/compare/v1.1.1...v2.0.0) (2018-04-08)
690 |
691 |
692 | ### meta
693 |
694 | * drop support for node@4 ([758536e](https://github.com/npm/registry-fetch/commit/758536e))
695 |
696 |
697 | ### BREAKING CHANGES
698 |
699 | * node@4 is no longer supported
700 |
701 |
702 |
703 |
704 | ## [1.1.1](https://github.com/npm/registry-fetch/compare/v1.1.0...v1.1.1) (2018-04-06)
705 |
706 |
707 |
708 |
709 | # [1.1.0](https://github.com/npm/registry-fetch/compare/v1.0.1...v1.1.0) (2018-03-16)
710 |
711 |
712 | ### Features
713 |
714 | * **specs:** can use opts.spec to trigger pickManifest ([85c4ac9](https://github.com/npm/registry-fetch/commit/85c4ac9))
715 |
716 |
717 |
718 |
719 | ## [1.0.1](https://github.com/npm/registry-fetch/compare/v1.0.0...v1.0.1) (2018-03-16)
720 |
721 |
722 | ### Bug Fixes
723 |
724 | * **query:** oops console.log ([870e4f5](https://github.com/npm/registry-fetch/commit/870e4f5))
725 |
726 |
727 |
728 |
729 | # 1.0.0 (2018-03-16)
730 |
731 |
732 | ### Bug Fixes
733 |
734 | * **auth:** get auth working with all the little details ([84b94ba](https://github.com/npm/registry-fetch/commit/84b94ba))
735 | * **deps:** add bluebird as an actual dep ([1286e31](https://github.com/npm/registry-fetch/commit/1286e31))
736 | * **errors:** Unknown auth errors use default code ([#1](https://github.com/npm/registry-fetch/issues/1)) ([3d91b93](https://github.com/npm/registry-fetch/commit/3d91b93))
737 | * **standard:** remove args from invocation ([9620a0a](https://github.com/npm/registry-fetch/commit/9620a0a))
738 |
739 |
740 | ### Features
741 |
742 | * **api:** baseline kinda-working API impl ([bf91f9f](https://github.com/npm/registry-fetch/commit/bf91f9f))
743 | * **body:** automatic handling of different opts.body values ([f3b97db](https://github.com/npm/registry-fetch/commit/f3b97db))
744 | * **config:** nicer input config input handling ([b9ce21d](https://github.com/npm/registry-fetch/commit/b9ce21d))
745 | * **opts:** use figgy-pudding for opts handling ([0abd527](https://github.com/npm/registry-fetch/commit/0abd527))
746 | * **query:** add query utility support ([65ea8b1](https://github.com/npm/registry-fetch/commit/65ea8b1))
747 |
--------------------------------------------------------------------------------