├── .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 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | # Contributing 4 | 5 | ## Code of Conduct 6 | 7 | All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). 8 | 9 | ## Reporting Bugs 10 | 11 | Before submitting a new bug report please search for an existing or similar report. 12 | 13 | Use one of our existing issue templates if you believe you've come across a unique problem. 14 | 15 | Duplicate issues, or issues that don't use one of our templates may get closed without a response. 16 | 17 | ## Pull Request Conventions 18 | 19 | ### Commits 20 | 21 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | 23 | When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 24 | 25 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 26 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 27 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 28 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). 29 | 30 | ### Test Coverage 31 | 32 | Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. 33 | 34 | Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. 35 | 36 | ### Linting 37 | 38 | Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. 39 | 40 | Please make sure linting passes before submitting a PR. 41 | 42 | ## What _not_ to contribute? 43 | 44 | ### Dependencies 45 | 46 | It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 47 | 48 | ### Tools/Automation 49 | 50 | Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. 51 | -------------------------------------------------------------------------------- /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 //<host>[:<port>], 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 `@<scope>: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 | #### <a name="fetch"></a> `> fetch(url, [opts]) -> Promise<Response>` 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 | #### <a name="fetch-json"></a> `> fetch.json(url, [opts]) -> Promise<ResponseJSON>` 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 | #### <a name="fetch-json-stream"></a> `> 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 | #### <a name="fetch-opts"></a> `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 | ##### <a name="opts-agent"></a> `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 | ##### <a name="opts-body"></a> `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 | ##### <a name="opts-ca"></a> `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 | ##### <a name="opts-cache"></a> `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 | ##### <a name="opts-cert"></a> `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 | ##### <a name="opts-fetchRetries"></a> `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 | ##### <a name="opts-fetchRetryFactor"></a> `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 | ##### <a name="opts-fetchRetryMintimeout"></a> `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 | ##### <a name="opts-fetchRetryMaxtimeout"></a> `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 | ##### <a name="opts-forceAuth"></a> `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 | ##### <a name="opts-gzip"></a> `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 | ##### <a name="opts-headers"></a> `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 | ##### <a name="opts-ignoreBody"></a> `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 | ##### <a name="opts-integrity"></a> `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 | ##### <a name="opts-key"></a> `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 | ##### <a name="opts-localAddress"></a> `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 | ##### <a name="opts-mapJSON"></a> `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 | ##### <a name="opts-maxSockets"></a> `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 | ##### <a name="opts-method"></a> `opts.method` 358 | 359 | * Type: String 360 | * Default: 'GET' 361 | 362 | HTTP method to use for the outgoing request. Case-insensitive. 363 | 364 | ##### <a name="opts-noProxy"></a> `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 | ##### <a name="opts-npmSession"></a> `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 | ##### <a name="opts-npmCommand"></a> `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 | ##### <a name="opts-offline"></a> `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 | ##### <a name="opts-otp"></a> `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 | ##### <a name="opts-otpPrompt"></a> `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 | ##### <a name="opts-password"></a> `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 | ##### <a name="opts-preferOffline"></a> `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 | ##### <a name="opts-preferOnline"></a> `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 | ##### <a name="opts-scope"></a> `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 | ##### <a name="opts-proxy"></a> `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 | ##### <a name="opts-query"></a> `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 | ##### <a name="opts-registry"></a> `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.<scope>:registry`](#opts-scope-registry) which can all affect the actual 516 | registry URL used by the outgoing request. 517 | 518 | ##### <a name="opts-retry"></a> `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 | ##### <a name="opts-scope"></a> `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.<scope>:registry`](#opts-scope-registry) and 535 | [`opts.spec`](#opts-spec) for interactions with this option. 536 | 537 | ##### <a name="opts-scope-registry"></a> `opts.<scope>: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 | ##### <a name="opts-spec"></a> `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 | ##### <a name="opts-strictSSL"></a> `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 | ##### <a name="opts-timeout"></a> `opts.timeout` 571 | 572 | * Type: Milliseconds 573 | * Default: 300000 (5 minutes) 574 | 575 | Time before a hanging request times out. 576 | 577 | ##### <a name="opts-authtoken"></a> `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 | ##### <a name="opts-userAgent"></a> `opts.userAgent` 593 | 594 | * Type: String 595 | * Default: `'npm-registry-fetch@<version>/node@<node-version>+<arch> (<platform>)'` 596 | 597 | User agent string to send in the `User-Agent` header. 598 | 599 | ##### <a name="opts-username"></a> `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 | <a name="5.0.1"></a> 485 | ## [5.0.1](https://github.com/npm/registry-fetch/compare/v5.0.0...v5.0.1) (2019-11-11) 486 | 487 | 488 | 489 | <a name="5.0.0"></a> 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 | <a name="4.0.2"></a> 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 | <a name="4.0.0"></a> 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 | <a name="3.9.1"></a> 538 | ## [3.9.1](https://github.com/npm/registry-fetch/compare/v3.9.0...v3.9.1) (2019-07-02) 539 | 540 | 541 | 542 | <a name="3.9.0"></a> 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 | <a name="3.8.0"></a> 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 | <a name="3.7.0"></a> 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 | <a name="3.6.0"></a> 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 | <a name="3.5.0"></a> 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 | <a name="3.4.0"></a> 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 | <a name="3.3.0"></a> 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 | <a name="3.2.1"></a> 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 | <a name="3.2.0"></a> 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 | <a name="3.1.1"></a> 648 | ## [3.1.1](https://github.com/npm/registry-fetch/compare/v3.1.0...v3.1.1) (2018-04-09) 649 | 650 | 651 | 652 | <a name="3.1.0"></a> 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 | <a name="3.0.0"></a> 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 | <a name="2.1.0"></a> 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 | <a name="2.0.0"></a> 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 | <a name="1.1.1"></a> 704 | ## [1.1.1](https://github.com/npm/registry-fetch/compare/v1.1.0...v1.1.1) (2018-04-06) 705 | 706 | 707 | 708 | <a name="1.1.0"></a> 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 | <a name="1.0.1"></a> 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 | <a name="1.0.0"></a> 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 | --------------------------------------------------------------------------------