New Upstream Release - node-ssri

Ready changes

Summary

Merged new upstream version: 10.0.2 (was: 10.0.1).

Diff

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..2c54b0d
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,3 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+* @npm/cli-team
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 0000000..d043192
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -0,0 +1,54 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Bug
+description: File a bug/issue
+title: "[BUG] <title>"
+labels: [ Bug, Needs Triage ]
+
+body:
+  - type: checkboxes
+    attributes:
+      label: Is there an existing issue for this?
+      description: Please [search here](./issues) to see if an issue already exists for your problem.
+      options:
+        - label: I have searched the existing issues
+          required: true
+  - type: textarea
+    attributes:
+      label: Current Behavior
+      description: A clear & concise description of what you're experiencing.
+    validations:
+      required: false
+  - type: textarea
+    attributes:
+      label: Expected Behavior
+      description: A clear & concise description of what you expected to happen.
+    validations:
+      required: false
+  - type: textarea
+    attributes:
+      label: Steps To Reproduce
+      description: Steps to reproduce the behavior.
+      value: |
+        1. In this environment...
+        2. With this config...
+        3. Run '...'
+        4. See error...
+    validations:
+      required: false
+  - type: textarea
+    attributes:
+      label: Environment
+      description: |
+        examples:
+          - **npm**: 7.6.3
+          - **Node**: 13.14.0
+          - **OS**: Ubuntu 20.04
+          - **platform**: Macbook Pro
+      value: |
+        - npm:
+        - Node:
+        - OS:
+        - platform:
+    validations:
+      required: false
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..d640909
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,3 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+blank_issues_enabled: true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..8da2a45
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,17 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+version: 2
+
+updates:
+  - package-ecosystem: npm
+    directory: /
+    schedule:
+      interval: daily
+    allow:
+      - dependency-type: direct
+    versioning-strategy: increase-if-necessary
+    commit-message:
+      prefix: deps
+      prefix-development: chore
+    labels:
+      - "Dependencies"
diff --git a/.github/matchers/tap.json b/.github/matchers/tap.json
new file mode 100644
index 0000000..2c81ea9
--- /dev/null
+++ b/.github/matchers/tap.json
@@ -0,0 +1,32 @@
+{
+  "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.",
+  "problemMatcher": [
+    {
+      "owner": "tap",
+      "pattern": [
+        {
+          "regexp": "^\\s*not ok \\d+ - (.*)",
+          "message": 1
+        },
+        {
+          "regexp": "^\\s*---"
+        },
+        {
+          "regexp": "^\\s*at:"
+        },
+        {
+          "regexp": "^\\s*line:\\s*(\\d+)",
+          "line": 1
+        },
+        {
+          "regexp": "^\\s*column:\\s*(\\d+)",
+          "column": 1
+        },
+        {
+          "regexp": "^\\s*file:\\s*(.*)",
+          "file": 1
+        }
+      ]
+    }
+  ]
+}
diff --git a/.github/settings.yml b/.github/settings.yml
new file mode 100644
index 0000000..1019e26
--- /dev/null
+++ b/.github/settings.yml
@@ -0,0 +1,2 @@
+---
+_extends: '.github:npm-cli/settings.yml'
diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml
new file mode 100644
index 0000000..62892f9
--- /dev/null
+++ b/.github/workflows/audit.yml
@@ -0,0 +1,39 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Audit
+
+on:
+  workflow_dispatch:
+  schedule:
+    # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1
+    - cron: "0 8 * * 1"
+
+jobs:
+  audit:
+    name: Audit Dependencies
+    if: github.repository_owner == 'npm'
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18.x
+      - name: Install npm@latest
+        run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+      - name: npm Version
+        run: npm -v
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund --package-lock
+      - name: Run Production Audit
+        run: npm audit --omit=dev
+      - name: Run Full Audit
+        run: npm audit --audit-level=none
diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml
new file mode 100644
index 0000000..6e80aa6
--- /dev/null
+++ b/.github/workflows/ci-release.yml
@@ -0,0 +1,216 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: CI - Release
+
+on:
+  workflow_dispatch:
+    inputs:
+      ref:
+        required: true
+        type: string
+        default: main
+  workflow_call:
+    inputs:
+      ref:
+        required: true
+        type: string
+      check-sha:
+        required: true
+        type: string
+
+jobs:
+  lint-all:
+    name: Lint All
+    if: github.repository_owner == 'npm'
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Get Workflow Job
+        uses: actions/github-script@v6
+        if: inputs.check-sha
+        id: check-output
+        env:
+          JOB_NAME: "Lint All"
+          MATRIX_NAME: ""
+        with:
+          script: |
+            const { owner, repo } = context.repo
+
+            const { data } = await github.rest.actions.listJobsForWorkflowRun({
+              owner,
+              repo,
+              run_id: context.runId,
+              per_page: 100
+            })
+
+            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
+            const job = data.jobs.find(j => j.name.endsWith(jobName))
+            const jobUrl = job?.html_url
+
+            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
+
+            let summary = `This check is assosciated with ${shaUrl}\n\n`
+
+            if (jobUrl) {
+              summary += `For run logs, click here: ${jobUrl}`
+            } else {
+              summary += `Run logs could not be found for a job with name: "${jobName}"`
+            }
+
+            return { summary }
+      - name: Create Check
+        uses: LouisBrunner/checks-action@v1.3.1
+        id: check
+        if: inputs.check-sha
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          status: in_progress
+          name: Lint All
+          sha: ${{ inputs.check-sha }}
+          output: ${{ steps.check-output.outputs.result }}
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          ref: ${{ inputs.ref }}
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18.x
+      - name: Install npm@latest
+        run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+      - name: npm Version
+        run: npm -v
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Lint
+        run: npm run lint --ignore-scripts
+      - name: Post Lint
+        run: npm run postlint --ignore-scripts
+      - name: Conclude Check
+        uses: LouisBrunner/checks-action@v1.3.1
+        if: steps.check.outputs.check_id && always()
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          conclusion: ${{ job.status }}
+          check_id: ${{ steps.check.outputs.check_id }}
+
+  test-all:
+    name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
+    if: github.repository_owner == 'npm'
+    strategy:
+      fail-fast: false
+      matrix:
+        platform:
+          - name: Linux
+            os: ubuntu-latest
+            shell: bash
+          - name: macOS
+            os: macos-latest
+            shell: bash
+          - name: Windows
+            os: windows-latest
+            shell: cmd
+        node-version:
+          - 14.17.0
+          - 14.x
+          - 16.13.0
+          - 16.x
+          - 18.0.0
+          - 18.x
+    runs-on: ${{ matrix.platform.os }}
+    defaults:
+      run:
+        shell: ${{ matrix.platform.shell }}
+    steps:
+      - name: Get Workflow Job
+        uses: actions/github-script@v6
+        if: inputs.check-sha
+        id: check-output
+        env:
+          JOB_NAME: "Test All"
+          MATRIX_NAME: " - ${{ matrix.platform.name }} - ${{ matrix.node-version }}"
+        with:
+          script: |
+            const { owner, repo } = context.repo
+
+            const { data } = await github.rest.actions.listJobsForWorkflowRun({
+              owner,
+              repo,
+              run_id: context.runId,
+              per_page: 100
+            })
+
+            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
+            const job = data.jobs.find(j => j.name.endsWith(jobName))
+            const jobUrl = job?.html_url
+
+            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ inputs.check-sha }}`
+
+            let summary = `This check is assosciated with ${shaUrl}\n\n`
+
+            if (jobUrl) {
+              summary += `For run logs, click here: ${jobUrl}`
+            } else {
+              summary += `Run logs could not be found for a job with name: "${jobName}"`
+            }
+
+            return { summary }
+      - name: Create Check
+        uses: LouisBrunner/checks-action@v1.3.1
+        id: check
+        if: inputs.check-sha
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          status: in_progress
+          name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
+          sha: ${{ inputs.check-sha }}
+          output: ${{ steps.check-output.outputs.result }}
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          ref: ${{ inputs.ref }}
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: Update Windows npm
+        # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
+        if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
+        run: |
+          curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
+          tar xf npm-7.5.4.tgz
+          cd package
+          node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
+          cd ..
+          rmdir /s /q package
+      - name: Install npm@7
+        if: startsWith(matrix.node-version, '10.')
+        run: npm i --prefer-online --no-fund --no-audit -g npm@7
+      - name: Install npm@latest
+        if: ${{ !startsWith(matrix.node-version, '10.') }}
+        run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+      - name: npm Version
+        run: npm -v
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Add Problem Matcher
+        run: echo "::add-matcher::.github/matchers/tap.json"
+      - name: Test
+        run: npm test --ignore-scripts
+      - name: Conclude Check
+        uses: LouisBrunner/checks-action@v1.3.1
+        if: steps.check.outputs.check_id && always()
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          conclusion: ${{ job.status }}
+          check_id: ${{ steps.check.outputs.check_id }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9cc149d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,107 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: CI
+
+on:
+  workflow_dispatch:
+  pull_request:
+  push:
+    branches:
+      - main
+      - latest
+  schedule:
+    # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1
+    - cron: "0 9 * * 1"
+
+jobs:
+  lint:
+    name: Lint
+    if: github.repository_owner == 'npm'
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18.x
+      - name: Install npm@latest
+        run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+      - name: npm Version
+        run: npm -v
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Lint
+        run: npm run lint --ignore-scripts
+      - name: Post Lint
+        run: npm run postlint --ignore-scripts
+
+  test:
+    name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }}
+    if: github.repository_owner == 'npm'
+    strategy:
+      fail-fast: false
+      matrix:
+        platform:
+          - name: Linux
+            os: ubuntu-latest
+            shell: bash
+          - name: macOS
+            os: macos-latest
+            shell: bash
+          - name: Windows
+            os: windows-latest
+            shell: cmd
+        node-version:
+          - 14.17.0
+          - 14.x
+          - 16.13.0
+          - 16.x
+          - 18.0.0
+          - 18.x
+    runs-on: ${{ matrix.platform.os }}
+    defaults:
+      run:
+        shell: ${{ matrix.platform.shell }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: Update Windows npm
+        # node 12 and 14 ship with npm@6, which is known to fail when updating itself in windows
+        if: matrix.platform.os == 'windows-latest' && (startsWith(matrix.node-version, '12.') || startsWith(matrix.node-version, '14.'))
+        run: |
+          curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz
+          tar xf npm-7.5.4.tgz
+          cd package
+          node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz
+          cd ..
+          rmdir /s /q package
+      - name: Install npm@7
+        if: startsWith(matrix.node-version, '10.')
+        run: npm i --prefer-online --no-fund --no-audit -g npm@7
+      - name: Install npm@latest
+        if: ${{ !startsWith(matrix.node-version, '10.') }}
+        run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+      - name: npm Version
+        run: npm -v
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Add Problem Matcher
+        run: echo "::add-matcher::.github/matchers/tap.json"
+      - name: Test
+        run: npm test --ignore-scripts
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..66b9498
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,38 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: CodeQL
+
+on:
+  push:
+    branches:
+      - main
+      - latest
+  pull_request:
+    branches:
+      - main
+      - latest
+  schedule:
+    # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1
+    - cron: "0 10 * * 1"
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Initialize CodeQL
+        uses: github/codeql-action/init@v2
+        with:
+          languages: javascript
+      - name: Perform CodeQL Analysis
+        uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/post-dependabot.yml b/.github/workflows/post-dependabot.yml
new file mode 100644
index 0000000..ce38340
--- /dev/null
+++ b/.github/workflows/post-dependabot.yml
@@ -0,0 +1,121 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Post Dependabot
+
+on: pull_request
+
+permissions:
+  contents: write
+
+jobs:
+  template-oss:
+    name: template-oss
+    if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]'
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          ref: ${{ github.event.pull_request.head.ref }}
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18.x
+      - name: Install npm@latest
+        run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+      - name: npm Version
+        run: npm -v
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Fetch Dependabot Metadata
+        id: metadata
+        uses: dependabot/fetch-metadata@v1
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+
+      # Dependabot can update multiple directories so we output which directory
+      # it is acting on so we can run the command for the correct root or workspace
+      - name: Get Dependabot Directory
+        if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss')
+        id: flags
+        run: |
+          dependabot_dir="${{ steps.metadata.outputs.directory }}"
+          if [[ "$dependabot_dir" == "/" ]]; then
+            echo "workspace=-iwr" >> $GITHUB_OUTPUT
+          else
+            # strip leading slash from directory so it works as a
+            # a path to the workspace flag
+            echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT
+          fi
+
+      - name: Apply Changes
+        if: steps.flags.outputs.workspace
+        id: apply
+        run: |
+          npm run template-oss-apply ${{ steps.flags.outputs.workspace }}
+          if [[ `git status --porcelain` ]]; then
+            echo "changes=true" >> $GITHUB_OUTPUT
+          fi
+          # This only sets the conventional commit prefix. This workflow can't reliably determine
+          # what the breaking change is though. If a BREAKING CHANGE message is required then
+          # this PR check will fail and the commit will be amended with stafftools
+          if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then
+            prefix='feat!'
+          else
+            prefix='chore'
+          fi
+          echo "message=$prefix: postinstall for dependabot template-oss PR" >> $GITHUB_OUTPUT
+
+      # This step will fail if template-oss has made any workflow updates. It is impossible
+      # for a workflow to update other workflows. In the case it does fail, we continue
+      # and then try to apply only a portion of the changes in the next step
+      - name: Push All Changes
+        if: steps.apply.outputs.changes
+        id: push
+        continue-on-error: true
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          git commit -am "${{ steps.apply.outputs.message }}"
+          git push
+
+      # If the previous step failed, then reset the commit and remove any workflow changes
+      # and attempt to commit and push again. This is helpful because we will have a commit
+      # with the correct prefix that we can then --amend with @npmcli/stafftools later.
+      - name: Push All Changes Except Workflows
+        if: steps.apply.outputs.changes && steps.push.outcome == 'failure'
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          git reset HEAD~
+          git checkout HEAD -- .github/workflows/
+          git clean -fd .github/workflows/
+          git commit -am "${{ steps.apply.outputs.message }}"
+          git push
+
+      # Check if all the necessary template-oss changes were applied. Since we continued
+      # on errors in one of the previous steps, this check will fail if our follow up
+      # only applied a portion of the changes and we need to followup manually.
+      #
+      # Note that this used to run `lint` and `postlint` but that will fail this action
+      # if we've also shipped any linting changes separate from template-oss. We do
+      # linting in another action, so we want to fail this one only if there are
+      # template-oss changes that could not be applied.
+      - name: Check Changes
+        if: steps.apply.outputs.changes
+        run: |
+          npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check
+
+      - name: Fail on Breaking Change
+        if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!')
+        run: |
+          echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'"
+          echo "for more information on how to fix this with a BREAKING CHANGE footer."
+          exit 1
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
new file mode 100644
index 0000000..99877da
--- /dev/null
+++ b/.github/workflows/pull-request.yml
@@ -0,0 +1,48 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Pull Request
+
+on:
+  pull_request:
+    types:
+      - opened
+      - reopened
+      - edited
+      - synchronize
+
+jobs:
+  commitlint:
+    name: Lint Commits
+    if: github.repository_owner == 'npm'
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18.x
+      - name: Install npm@latest
+        run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+      - name: npm Version
+        run: npm -v
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Run Commitlint on Commits
+        id: commit
+        continue-on-error: true
+        run: |
+          npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }}
+      - name: Run Commitlint on PR Title
+        if: steps.commit.outcome == 'failure'
+        run: |
+          echo '${{ github.event.pull_request.title }}' | npx --offline commitlint -V
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..0eb163d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,398 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+name: Release
+
+on:
+  workflow_dispatch:
+    inputs:
+      release-pr:
+        description: a release PR number to rerun release jobs on
+        type: string
+  push:
+    branches:
+      - main
+      - latest
+      - release/v*
+
+permissions:
+  contents: write
+  pull-requests: write
+  checks: write
+
+jobs:
+  release:
+    outputs:
+      pr: ${{ steps.release.outputs.pr }}
+      release: ${{ steps.release.outputs.release }}
+      releases: ${{ steps.release.outputs.releases }}
+      branch: ${{ steps.release.outputs.pr-branch }}
+      pr-number: ${{ steps.release.outputs.pr-number }}
+      comment-id: ${{ steps.pr-comment.outputs.result }}
+      check-id: ${{ steps.check.outputs.check_id }}
+    name: Release
+    if: github.repository_owner == 'npm'
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18.x
+      - name: Install npm@latest
+        run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+      - name: npm Version
+        run: npm -v
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Release Please
+        id: release
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          npx --offline template-oss-release-please "${{ github.ref_name }}" "${{ inputs.release-pr }}"
+      - name: Post Pull Request Comment
+        if: steps.release.outputs.pr-number
+        uses: actions/github-script@v6
+        id: pr-comment
+        env:
+          PR_NUMBER: ${{ steps.release.outputs.pr-number }}
+          REF_NAME: ${{ github.ref_name }}
+        with:
+          script: |
+            const { REF_NAME, PR_NUMBER: issue_number } = process.env
+            const { runId, repo: { owner, repo } } = context
+
+            const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId })
+
+            let body = '## Release Manager\n\n'
+
+            const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
+            let commentId = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.startsWith(body))?.id
+
+            body += `Release workflow run: ${workflow.html_url}\n\n#### Force CI to Update This Release\n\n`
+            body += `This PR will be updated and CI will run for every non-\`chore:\` commit that is pushed to \`main\`. `
+            body += `To force CI to update this PR, run this command:\n\n`
+            body += `\`\`\`\ngh workflow run release.yml -r ${REF_NAME} -R ${owner}/${repo} -f release-pr=${issue_number}\n\`\`\``
+
+            if (commentId) {
+              await github.rest.issues.updateComment({ owner, repo, comment_id: commentId, body })
+            } else {
+              const { data: comment } = await github.rest.issues.createComment({ owner, repo, issue_number, body })
+              commentId = comment?.id
+            }
+
+            return commentId
+      - name: Get Workflow Job
+        uses: actions/github-script@v6
+        if: steps.release.outputs.pr-sha
+        id: check-output
+        env:
+          JOB_NAME: "Release"
+          MATRIX_NAME: ""
+        with:
+          script: |
+            const { owner, repo } = context.repo
+
+            const { data } = await github.rest.actions.listJobsForWorkflowRun({
+              owner,
+              repo,
+              run_id: context.runId,
+              per_page: 100
+            })
+
+            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
+            const job = data.jobs.find(j => j.name.endsWith(jobName))
+            const jobUrl = job?.html_url
+
+            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.release.outputs.pr-sha }}`
+
+            let summary = `This check is assosciated with ${shaUrl}\n\n`
+
+            if (jobUrl) {
+              summary += `For run logs, click here: ${jobUrl}`
+            } else {
+              summary += `Run logs could not be found for a job with name: "${jobName}"`
+            }
+
+            return { summary }
+      - name: Create Check
+        uses: LouisBrunner/checks-action@v1.3.1
+        id: check
+        if: steps.release.outputs.pr-sha
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          status: in_progress
+          name: Release
+          sha: ${{ steps.release.outputs.pr-sha }}
+          output: ${{ steps.check-output.outputs.result }}
+
+  update:
+    needs: release
+    outputs:
+      sha: ${{ steps.commit.outputs.sha }}
+      check-id: ${{ steps.check.outputs.check_id }}
+    name: Update - Release
+    if: github.repository_owner == 'npm' && needs.release.outputs.pr
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+          ref: ${{ needs.release.outputs.branch }}
+      - name: Setup Git User
+        run: |
+          git config --global user.email "npm-cli+bot@github.com"
+          git config --global user.name "npm CLI robot"
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18.x
+      - name: Install npm@latest
+        run: npm i --prefer-online --no-fund --no-audit -g npm@latest
+      - name: npm Version
+        run: npm -v
+      - name: Install Dependencies
+        run: npm i --ignore-scripts --no-audit --no-fund
+      - name: Run Post Pull Request Actions
+        env:
+          RELEASE_PR_NUMBER: ${{ needs.release.outputs.pr-number }}
+          RELEASE_COMMENT_ID: ${{ needs.release.outputs.comment-id }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          npm exec --offline -- template-oss-release-manager --lockfile=false --publish=true
+          npm run rp-pull-request --ignore-scripts --if-present
+      - name: Commit
+        id: commit
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          git commit --all --amend --no-edit || true
+          git push --force-with-lease
+          echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
+      - name: Get Workflow Job
+        uses: actions/github-script@v6
+        if: steps.commit.outputs.sha
+        id: check-output
+        env:
+          JOB_NAME: "Update - Release"
+          MATRIX_NAME: ""
+        with:
+          script: |
+            const { owner, repo } = context.repo
+
+            const { data } = await github.rest.actions.listJobsForWorkflowRun({
+              owner,
+              repo,
+              run_id: context.runId,
+              per_page: 100
+            })
+
+            const jobName = process.env.JOB_NAME + process.env.MATRIX_NAME
+            const job = data.jobs.find(j => j.name.endsWith(jobName))
+            const jobUrl = job?.html_url
+
+            const shaUrl = `${context.serverUrl}/${owner}/${repo}/commit/${{ steps.commit.outputs.sha }}`
+
+            let summary = `This check is assosciated with ${shaUrl}\n\n`
+
+            if (jobUrl) {
+              summary += `For run logs, click here: ${jobUrl}`
+            } else {
+              summary += `Run logs could not be found for a job with name: "${jobName}"`
+            }
+
+            return { summary }
+      - name: Create Check
+        uses: LouisBrunner/checks-action@v1.3.1
+        id: check
+        if: steps.commit.outputs.sha
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          status: in_progress
+          name: Release
+          sha: ${{ steps.commit.outputs.sha }}
+          output: ${{ steps.check-output.outputs.result }}
+      - name: Conclude Check
+        uses: LouisBrunner/checks-action@v1.3.1
+        if: needs.release.outputs.check-id && always()
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          conclusion: ${{ job.status }}
+          check_id: ${{ needs.release.outputs.check-id }}
+
+  ci:
+    name: CI - Release
+    needs: [ release, update ]
+    if: needs.release.outputs.pr
+    uses: ./.github/workflows/ci-release.yml
+    with:
+      ref: ${{ needs.release.outputs.branch }}
+      check-sha: ${{ needs.update.outputs.sha }}
+
+  post-ci:
+    needs: [ release, update, ci ]
+    name: Post CI - Release
+    if: github.repository_owner == 'npm' && needs.release.outputs.pr && always()
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Get Needs Result
+        id: needs-result
+        run: |
+          result=""
+          if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
+            result="failure"
+          elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
+            result="cancelled"
+          else
+            result="success"
+          fi
+          echo "result=$result" >> $GITHUB_OUTPUT
+      - name: Conclude Check
+        uses: LouisBrunner/checks-action@v1.3.1
+        if: needs.update.outputs.check-id && always()
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          conclusion: ${{ steps.needs-result.outputs.result }}
+          check_id: ${{ needs.update.outputs.check-id }}
+
+  post-release:
+    needs: release
+    name: Post Release - Release
+    if: github.repository_owner == 'npm' && needs.release.outputs.releases
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Create Release PR Comment
+        uses: actions/github-script@v6
+        env:
+          RELEASES: ${{ needs.release.outputs.releases }}
+        with:
+          script: |
+            const releases = JSON.parse(process.env.RELEASES)
+            const { runId, repo: { owner, repo } } = context
+            const issue_number = releases[0].prNumber
+
+            let body = '## Release Workflow\n\n'
+            for (const { pkgName, version, url } of releases) {
+              body += `- \`${pkgName}@${version}\` ${url}\n`
+            }
+
+            const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
+              .then(cs => cs.map(c => ({ id: c.id, login: c.user.login, body: c.body })))
+            console.log(`Found comments: ${JSON.stringify(comments, null, 2)}`)
+            const releaseComments = comments.filter(c => c.login === 'github-actions[bot]' && c.body.includes('Release is at'))
+
+            for (const comment of releaseComments) {
+              console.log(`Release comment: ${JSON.stringify(comment, null, 2)}`)
+              await github.rest.issues.deleteComment({ owner, repo, comment_id: comment.id })
+            }
+
+            const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`
+            await github.rest.issues.createComment({
+              owner,
+              repo,
+              issue_number,
+              body: `${body}- Workflow run: :arrows_counterclockwise: ${runUrl}`,
+            })
+
+  release-integration:
+    needs: release
+    name: Release Integration
+    if: needs.release.outputs.release
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    permissions:
+      deployments: write
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          ref: ${{ fromJSON(needs.release.outputs.release).tagName }}
+      - name: Setup Node
+        uses: actions/setup-node@v3
+        with:
+          node-version: 18.x
+      - name: Install npm@latest
+        run: |
+          npm i --prefer-online --no-fund --no-audit -g npm@latest
+          npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN}
+      - name: Publish
+        env:
+          PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
+        run: npm publish
+
+  post-release-integration:
+    needs: [ release, release-integration ]
+    name: Post Release Integration - Release
+    if: github.repository_owner == 'npm' && needs.release.outputs.release && always()
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Get Needs Result
+        id: needs-result
+        run: |
+          if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
+            result="x"
+          elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
+            result="heavy_multiplication_x"
+          else
+            result="white_check_mark"
+          fi
+          echo "result=$result" >> $GITHUB_OUTPUT
+      - name: Update Release PR Comment
+        uses: actions/github-script@v6
+        env:
+          PR_NUMBER: ${{ fromJSON(needs.release.outputs.release).prNumber }}
+          RESULT: ${{ steps.needs-result.outputs.result }}
+        with:
+          script: |
+            const { PR_NUMBER: issue_number, RESULT } = process.env
+            const { runId, repo: { owner, repo } } = context
+
+            const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
+            const updateComment = comments.find(c =>
+              c.user.login === 'github-actions[bot]' &&
+              c.body.startsWith('## Release Workflow\n\n') &&
+              c.body.includes(runId)
+            )
+
+            if (updateComment) {
+              console.log('Found comment to update:', JSON.stringify(updateComment, null, 2))
+              let body = updateComment.body.replace(/Workflow run: :[a-z_]+:/, `Workflow run: :${RESULT}:`)
+              const tagCodeowner = RESULT !== 'white_check_mark'
+              if (tagCodeowner) {
+                body += `\n\n:rotating_light:`
+                body += ` @npm/cli-team: The post-release workflow failed for this release.`
+                body += ` Manual steps may need to be taken after examining the workflow output`
+                body += ` from the above workflow run. :rotating_light:`
+              }
+              await github.rest.issues.updateComment({
+                owner,
+                repo,
+                body,
+                comment_id: updateComment.id,
+              })
+            } else {
+              console.log('No matching comments found:', JSON.stringify(comments, null, 2))
+            }
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0ec3c84
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,28 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+# ignore everything in the root
+/*
+
+# keep these
+!**/.gitignore
+!/.commitlintrc.js
+!/.eslintrc.js
+!/.eslintrc.local.*
+!/.github/
+!/.gitignore
+!/.npmrc
+!/.release-please-manifest.json
+!/bin/
+!/CHANGELOG*
+!/CODE_OF_CONDUCT.md
+!/docs/
+!/lib/
+!/LICENSE*
+!/map.js
+!/package.json
+!/README*
+!/release-please-config.json
+!/scripts/
+!/SECURITY.md
+!/tap-snapshots/
+!/test/
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..952c140
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+  ".": "10.0.2"
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a9f998..e55a91a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
 # Changelog
 
+## [10.0.2](https://github.com/npm/ssri/compare/v10.0.1...v10.0.2) (2023-04-03)
+
+### Bug Fixes
+
+* [`8e80eca`](https://github.com/npm/ssri/commit/8e80eca497bd7f6a714498df6ad64f3faef3bc5e) [#74](https://github.com/npm/ssri/pull/74) move from symbols to private methods (#74) (@wraithgar)
+* [`a316b12`](https://github.com/npm/ssri/commit/a316b12e0c177757528c643ac28ce8b2425fcb3f) [#75](https://github.com/npm/ssri/pull/75) faster toString for integrity (#75) (@H4ad)
+* [`6e6877d`](https://github.com/npm/ssri/commit/6e6877d55cf608ea9b80b19a389f7e1775421e24) [#72](https://github.com/npm/ssri/pull/72) remove spread of defaultOpts (#72) (@H4ad)
+
+## [10.0.1](https://github.com/npm/ssri/compare/v10.0.0...v10.0.1) (2022-12-07)
+
+### Dependencies
+
+* [`4f6ba1e`](https://github.com/npm/ssri/commit/4f6ba1e5cc30bf4ba564206d4358ee6951e46f87) [#64](https://github.com/npm/ssri/pull/64) bump minipass from 3.3.6 to 4.0.0
+
+## [10.0.0](https://github.com/npm/ssri/compare/v9.0.1...v10.0.0) (2022-10-10)
+
+### ⚠️ BREAKING CHANGES
+
+* `ssri` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0`
+
+### Features
+
+* [`3de0c45`](https://github.com/npm/ssri/commit/3de0c4502db78dbd3595207511362f65191793a8) [#52](https://github.com/npm/ssri/pull/52) postinstall for dependabot template-oss PR (@lukekarrys)
+
+### Bug Fixes
+
+* [`2e876d1`](https://github.com/npm/ssri/commit/2e876d12a67f2778c382dd29edc5ecc150279975) [#48](https://github.com/npm/ssri/pull/48) properly handle missing algorithm type (#48) (@ahmedwelhakim)
+
 ### [9.0.1](https://github.com/npm/ssri/compare/v9.0.0...v9.0.1) (2022-05-19)
 
 
diff --git a/SECURITY.md b/SECURITY.md
index a93106d..9cd2dea 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,3 +1,13 @@
 <!-- This file is automatically added by @npmcli/template-oss. Do not edit. -->
 
-Please send vulnerability reports through [hackerone](https://hackerone.com/github).
+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).
+
+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. 
+
+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).
+
+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.
+
+**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
+
+Thanks for helping make GitHub safe for everyone.
diff --git a/debian/changelog b/debian/changelog
index 957aa12..3a26e21 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+node-ssri (10.0.2-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 07 Apr 2023 13:32:39 -0000
+
 node-ssri (9.0.1-2) unstable; urgency=medium
 
   [ Yadd ]
diff --git a/lib/index.js b/lib/index.js
index 1443137..e142431 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -3,7 +3,8 @@
 const crypto = require('crypto')
 const MiniPass = require('minipass')
 
-const SPEC_ALGORITHMS = ['sha256', 'sha384', 'sha512']
+const SPEC_ALGORITHMS = ['sha512', 'sha384', 'sha256']
+const DEFAULT_ALGORITHMS = ['sha512']
 
 // TODO: this should really be a hardcoded list of algorithms we support,
 // rather than [a-z0-9].
@@ -12,72 +13,50 @@ const SRI_REGEX = /^([a-z0-9]+)-([^?]+)([?\S*]*)$/
 const STRICT_SRI_REGEX = /^([a-z0-9]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)?$/
 const VCHAR_REGEX = /^[\x21-\x7E]+$/
 
-const defaultOpts = {
-  algorithms: ['sha512'],
-  error: false,
-  options: [],
-  pickAlgorithm: getPrioritizedHash,
-  sep: ' ',
-  single: false,
-  strict: false,
-}
-
-const ssriOpts = (opts = {}) => ({ ...defaultOpts, ...opts })
-
-const getOptString = options => !options || !options.length
-  ? ''
-  : `?${options.join('?')}`
-
-const _onEnd = Symbol('_onEnd')
-const _getOptions = Symbol('_getOptions')
-const _emittedSize = Symbol('_emittedSize')
-const _emittedIntegrity = Symbol('_emittedIntegrity')
-const _emittedVerified = Symbol('_emittedVerified')
+const getOptString = options => options?.length ? `?${options.join('?')}` : ''
 
 class IntegrityStream extends MiniPass {
+  #emittedIntegrity
+  #emittedSize
+  #emittedVerified
+
   constructor (opts) {
     super()
     this.size = 0
     this.opts = opts
 
     // may be overridden later, but set now for class consistency
-    this[_getOptions]()
+    this.#getOptions()
 
     // options used for calculating stream.  can't be changed.
-    const { algorithms = defaultOpts.algorithms } = opts
+    const algorithms = opts?.algorithms || DEFAULT_ALGORITHMS
     this.algorithms = Array.from(
       new Set(algorithms.concat(this.algorithm ? [this.algorithm] : []))
     )
     this.hashes = this.algorithms.map(crypto.createHash)
   }
 
-  [_getOptions] () {
-    const {
-      integrity,
-      size,
-      options,
-    } = { ...defaultOpts, ...this.opts }
-
+  #getOptions () {
     // For verification
-    this.sri = integrity ? parse(integrity, this.opts) : null
-    this.expectedSize = size
+    this.sri = this.opts?.integrity ? parse(this.opts?.integrity, this.opts) : null
+    this.expectedSize = this.opts?.size
     this.goodSri = this.sri ? !!Object.keys(this.sri).length : false
     this.algorithm = this.goodSri ? this.sri.pickAlgorithm(this.opts) : null
     this.digests = this.goodSri ? this.sri[this.algorithm] : null
-    this.optString = getOptString(options)
+    this.optString = getOptString(this.opts?.options)
   }
 
   on (ev, handler) {
-    if (ev === 'size' && this[_emittedSize]) {
-      return handler(this[_emittedSize])
+    if (ev === 'size' && this.#emittedSize) {
+      return handler(this.#emittedSize)
     }
 
-    if (ev === 'integrity' && this[_emittedIntegrity]) {
-      return handler(this[_emittedIntegrity])
+    if (ev === 'integrity' && this.#emittedIntegrity) {
+      return handler(this.#emittedIntegrity)
     }
 
-    if (ev === 'verified' && this[_emittedVerified]) {
-      return handler(this[_emittedVerified])
+    if (ev === 'verified' && this.#emittedVerified) {
+      return handler(this.#emittedVerified)
     }
 
     return super.on(ev, handler)
@@ -85,7 +64,7 @@ class IntegrityStream extends MiniPass {
 
   emit (ev, data) {
     if (ev === 'end') {
-      this[_onEnd]()
+      this.#onEnd()
     }
     return super.emit(ev, data)
   }
@@ -96,9 +75,9 @@ class IntegrityStream extends MiniPass {
     return super.write(data)
   }
 
-  [_onEnd] () {
+  #onEnd () {
     if (!this.goodSri) {
-      this[_getOptions]()
+      this.#getOptions()
     }
     const newSri = parse(this.hashes.map((h, i) => {
       return `${this.algorithms[i]}-${h.digest('base64')}${this.optString}`
@@ -123,12 +102,12 @@ class IntegrityStream extends MiniPass {
       err.sri = this.sri
       this.emit('error', err)
     } else {
-      this[_emittedSize] = this.size
+      this.#emittedSize = this.size
       this.emit('size', this.size)
-      this[_emittedIntegrity] = newSri
+      this.#emittedIntegrity = newSri
       this.emit('integrity', newSri)
       if (match) {
-        this[_emittedVerified] = match
+        this.#emittedVerified = match
         this.emit('verified', match)
       }
     }
@@ -141,8 +120,7 @@ class Hash {
   }
 
   constructor (hash, opts) {
-    opts = ssriOpts(opts)
-    const strict = !!opts.strict
+    const strict = opts?.strict
     this.source = hash.trim()
 
     // set default values so that we make V8 happy to
@@ -161,7 +139,7 @@ class Hash {
     if (!match) {
       return
     }
-    if (strict && !SPEC_ALGORITHMS.some(a => a === match[1])) {
+    if (strict && !SPEC_ALGORITHMS.includes(match[1])) {
       return
     }
     this.algorithm = match[1]
@@ -182,14 +160,13 @@ class Hash {
   }
 
   toString (opts) {
-    opts = ssriOpts(opts)
-    if (opts.strict) {
+    if (opts?.strict) {
       // Strict mode enforces the standard as close to the foot of the
       // letter as it can.
       if (!(
         // The spec has very restricted productions for algorithms.
         // https://www.w3.org/TR/CSP2/#source-list-syntax
-        SPEC_ALGORITHMS.some(x => x === this.algorithm) &&
+        SPEC_ALGORITHMS.includes(this.algorithm) &&
         // Usually, if someone insists on using a "different" base64, we
         // leave it as-is, since there's multiple standards, and the
         // specified is not a URL-safe variant.
@@ -203,11 +180,41 @@ class Hash {
         return ''
       }
     }
-    const options = this.options && this.options.length
-      ? `?${this.options.join('?')}`
-      : ''
-    return `${this.algorithm}-${this.digest}${options}`
+    return `${this.algorithm}-${this.digest}${getOptString(this.options)}`
+  }
+}
+
+function integrityHashToString (toString, sep, opts, hashes) {
+  const toStringIsNotEmpty = toString !== ''
+
+  let shouldAddFirstSep = false
+  let complement = ''
+
+  const lastIndex = hashes.length - 1
+
+  for (let i = 0; i < lastIndex; i++) {
+    const hashString = Hash.prototype.toString.call(hashes[i], opts)
+
+    if (hashString) {
+      shouldAddFirstSep = true
+
+      complement += hashString
+      complement += sep
+    }
   }
+
+  const finalHashString = Hash.prototype.toString.call(hashes[lastIndex], opts)
+
+  if (finalHashString) {
+    shouldAddFirstSep = true
+    complement += finalHashString
+  }
+
+  if (toStringIsNotEmpty && shouldAddFirstSep) {
+    return toString + sep + complement
+  }
+
+  return toString + complement
 }
 
 class Integrity {
@@ -224,21 +231,28 @@ class Integrity {
   }
 
   toString (opts) {
-    opts = ssriOpts(opts)
-    let sep = opts.sep || ' '
-    if (opts.strict) {
+    let sep = opts?.sep || ' '
+    let toString = ''
+
+    if (opts?.strict) {
       // Entries must be separated by whitespace, according to spec.
       sep = sep.replace(/\S+/g, ' ')
+
+      for (const hash of SPEC_ALGORITHMS) {
+        if (this[hash]) {
+          toString = integrityHashToString(toString, sep, opts, this[hash])
+        }
+      }
+    } else {
+      for (const hash of Object.keys(this)) {
+        toString = integrityHashToString(toString, sep, opts, this[hash])
+      }
     }
-    return Object.keys(this).map(k => {
-      return this[k].map(hash => {
-        return Hash.prototype.toString.call(hash, opts)
-      }).filter(x => x.length).join(sep)
-    }).filter(x => x.length).join(sep)
+
+    return toString
   }
 
   concat (integrity, opts) {
-    opts = ssriOpts(opts)
     const other = typeof integrity === 'string'
       ? integrity
       : stringify(integrity, opts)
@@ -252,7 +266,6 @@ class Integrity {
   // add additional hashes to an integrity value, but prevent
   // *changing* an existing integrity hash.
   merge (integrity, opts) {
-    opts = ssriOpts(opts)
     const other = parse(integrity, opts)
     for (const algo in other) {
       if (this[algo]) {
@@ -268,8 +281,10 @@ class Integrity {
   }
 
   match (integrity, opts) {
-    opts = ssriOpts(opts)
     const other = parse(integrity, opts)
+    if (!other) {
+      return false
+    }
     const algo = other.pickAlgorithm(opts)
     return (
       this[algo] &&
@@ -283,8 +298,7 @@ class Integrity {
   }
 
   pickAlgorithm (opts) {
-    opts = ssriOpts(opts)
-    const pickAlgorithm = opts.pickAlgorithm
+    const pickAlgorithm = opts?.pickAlgorithm || getPrioritizedHash
     const keys = Object.keys(this)
     return keys.reduce((acc, algo) => {
       return pickAlgorithm(acc, algo) || acc
@@ -297,7 +311,6 @@ function parse (sri, opts) {
   if (!sri) {
     return null
   }
-  opts = ssriOpts(opts)
   if (typeof sri === 'string') {
     return _parse(sri, opts)
   } else if (sri.algorithm && sri.digest) {
@@ -312,7 +325,7 @@ function parse (sri, opts) {
 function _parse (integrity, opts) {
   // 3.4.3. Parse metadata
   // https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
-  if (opts.single) {
+  if (opts?.single) {
     return new Hash(integrity, opts)
   }
   const hashes = integrity.trim().split(/\s+/).reduce((acc, string) => {
@@ -331,7 +344,6 @@ function _parse (integrity, opts) {
 
 module.exports.stringify = stringify
 function stringify (obj, opts) {
-  opts = ssriOpts(opts)
   if (obj.algorithm && obj.digest) {
     return Hash.prototype.toString.call(obj, opts)
   } else if (typeof obj === 'string') {
@@ -343,8 +355,7 @@ function stringify (obj, opts) {
 
 module.exports.fromHex = fromHex
 function fromHex (hexDigest, algorithm, opts) {
-  opts = ssriOpts(opts)
-  const optString = getOptString(opts.options)
+  const optString = getOptString(opts?.options)
   return parse(
     `${algorithm}-${
       Buffer.from(hexDigest, 'hex').toString('base64')
@@ -354,9 +365,8 @@ function fromHex (hexDigest, algorithm, opts) {
 
 module.exports.fromData = fromData
 function fromData (data, opts) {
-  opts = ssriOpts(opts)
-  const algorithms = opts.algorithms
-  const optString = getOptString(opts.options)
+  const algorithms = opts?.algorithms || DEFAULT_ALGORITHMS
+  const optString = getOptString(opts?.options)
   return algorithms.reduce((acc, algo) => {
     const digest = crypto.createHash(algo).update(data).digest('base64')
     const hash = new Hash(
@@ -379,7 +389,6 @@ function fromData (data, opts) {
 
 module.exports.fromStream = fromStream
 function fromStream (stream, opts) {
-  opts = ssriOpts(opts)
   const istream = integrityStream(opts)
   return new Promise((resolve, reject) => {
     stream.pipe(istream)
@@ -396,10 +405,9 @@ function fromStream (stream, opts) {
 
 module.exports.checkData = checkData
 function checkData (data, sri, opts) {
-  opts = ssriOpts(opts)
   sri = parse(sri, opts)
   if (!sri || !Object.keys(sri).length) {
-    if (opts.error) {
+    if (opts?.error) {
       throw Object.assign(
         new Error('No valid integrity hashes to check against'), {
           code: 'EINTEGRITY',
@@ -413,7 +421,8 @@ function checkData (data, sri, opts) {
   const digest = crypto.createHash(algorithm).update(data).digest('base64')
   const newSri = parse({ algorithm, digest })
   const match = newSri.match(sri, opts)
-  if (match || !opts.error) {
+  opts = opts || {}
+  if (match || !(opts.error)) {
     return match
   } else if (typeof opts.size === 'number' && (data.length !== opts.size)) {
     /* eslint-disable-next-line max-len */
@@ -437,7 +446,7 @@ function checkData (data, sri, opts) {
 
 module.exports.checkStream = checkStream
 function checkStream (stream, sri, opts) {
-  opts = ssriOpts(opts)
+  opts = opts || Object.create(null)
   opts.integrity = sri
   sri = parse(sri, opts)
   if (!sri || !Object.keys(sri).length) {
@@ -462,15 +471,14 @@ function checkStream (stream, sri, opts) {
 }
 
 module.exports.integrityStream = integrityStream
-function integrityStream (opts = {}) {
+function integrityStream (opts = Object.create(null)) {
   return new IntegrityStream(opts)
 }
 
 module.exports.create = createIntegrity
 function createIntegrity (opts) {
-  opts = ssriOpts(opts)
-  const algorithms = opts.algorithms
-  const optString = getOptString(opts.options)
+  const algorithms = opts?.algorithms || DEFAULT_ALGORITHMS
+  const optString = getOptString(opts?.options)
 
   const hashes = algorithms.map(crypto.createHash)
 
diff --git a/package.json b/package.json
index 91c1f91..4d5963e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ssri",
-  "version": "9.0.1",
+  "version": "10.0.2",
   "description": "Standard Subresource Integrity library -- parses, serializes, generates, and verifies integrity metadata according to the SRI spec.",
   "main": "lib/index.js",
   "files": [
@@ -10,7 +10,6 @@
   "scripts": {
     "prerelease": "npm t",
     "postrelease": "npm publish",
-    "prepublishOnly": "git push origin --follow-tags",
     "posttest": "npm run lint",
     "test": "tap",
     "coverage": "tap",
@@ -18,12 +17,14 @@
     "postlint": "template-oss-check",
     "template-oss-apply": "template-oss-apply --force",
     "lintfix": "npm run lint -- --fix",
-    "preversion": "npm test",
-    "postversion": "npm publish",
     "snap": "tap"
   },
   "tap": {
-    "check-coverage": true
+    "check-coverage": true,
+    "nyc-arg": [
+      "--exclude",
+      "tap-snapshots/**"
+    ]
   },
   "repository": {
     "type": "git",
@@ -46,18 +47,19 @@
   "author": "GitHub Inc.",
   "license": "ISC",
   "dependencies": {
-    "minipass": "^3.1.1"
+    "minipass": "^4.0.0"
   },
   "devDependencies": {
-    "@npmcli/eslint-config": "^3.0.1",
-    "@npmcli/template-oss": "3.5.0",
+    "@npmcli/eslint-config": "^4.0.0",
+    "@npmcli/template-oss": "4.13.0",
     "tap": "^16.0.1"
   },
   "engines": {
-    "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+    "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
   },
   "templateOSS": {
     "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
-    "version": "3.5.0"
+    "version": "4.13.0",
+    "publish": "true"
   }
 }
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..73d1e35
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,36 @@
+{
+  "exclude-packages-from-root": true,
+  "group-pull-request-title-pattern": "chore: release ${version}",
+  "pull-request-title-pattern": "chore: release${component} ${version}",
+  "changelog-sections": [
+    {
+      "type": "feat",
+      "section": "Features",
+      "hidden": false
+    },
+    {
+      "type": "fix",
+      "section": "Bug Fixes",
+      "hidden": false
+    },
+    {
+      "type": "docs",
+      "section": "Documentation",
+      "hidden": false
+    },
+    {
+      "type": "deps",
+      "section": "Dependencies",
+      "hidden": false
+    },
+    {
+      "type": "chore",
+      "hidden": true
+    }
+  ],
+  "packages": {
+    ".": {
+      "package-name": ""
+    }
+  }
+}
diff --git a/test/from.js b/test/from.js
index d10ec76..4a1d330 100644
--- a/test/from.js
+++ b/test/from.js
@@ -59,40 +59,32 @@ test('fromData', t => {
   t.end()
 })
 
-test('fromStream', t => {
+test('fromStream', async t => {
   let streamEnded
   const stream = fileStream().on('end', () => {
     streamEnded = true
   })
-  return ssri.fromStream(stream).then(integrity => {
-    t.equal(
-      integrity.toString(),
-      `sha512-${hash(TEST_DATA, 'sha512')}`,
-      'generates sha512 from a stream'
-    )
-    t.ok(streamEnded, 'source stream ended')
-    return ssri.fromStream(fileStream(), {
-      algorithms: ['sha256', 'sha384'],
-    })
-  }).then(integrity => {
-    t.equal(
-      integrity.toString(), [
-        `sha256-${hash(TEST_DATA, 'sha256')}`,
-        `sha384-${hash(TEST_DATA, 'sha384')}`,
-      ].join(' '),
-      'can generate multiple metadata entries with opts.algorithms'
-    )
-    return ssri.fromStream(fileStream(), {
-      algorithms: ['sha256', 'sha384'],
-      options: ['foo', 'bar'],
-    })
-  }).then(integrity => {
-    t.equal(
-      integrity.toString(), [
-        `sha256-${hash(TEST_DATA, 'sha256')}?foo?bar`,
-        `sha384-${hash(TEST_DATA, 'sha384')}?foo?bar`,
-      ].join(' '),
-      'can add opts.options to each entry'
-    )
+  const noAlgs = await ssri.fromStream(stream)
+  t.equal(
+    noAlgs.toString(),
+    `sha512-${hash(TEST_DATA, 'sha512')}`,
+    'generates sha512 from a stream'
+  )
+  t.ok(streamEnded, 'source stream ended')
+  const goodAlgs = await ssri.fromStream(fileStream(), {
+    algorithms: ['sha256', 'sha384'],
+  })
+  t.equal(
+    goodAlgs.toString(),
+    [`sha256-${hash(TEST_DATA, 'sha256')}`, `sha384-${hash(TEST_DATA, 'sha384')}`].join(' '),
+    'can generate multiple metadata entries with opts.algorithms'
+  )
+  const badAlgs = await ssri.fromStream(fileStream(), {
+    algorithms: ['sha256', 'sha384'],
+    options: ['foo', 'bar'],
   })
+  t.equal(badAlgs.toString(), [
+    `sha256-${hash(TEST_DATA, 'sha256')}?foo?bar`,
+    `sha384-${hash(TEST_DATA, 'sha384')}?foo?bar`,
+  ].join(' '), 'can add opts.options to each entry')
 })
diff --git a/test/integrity.js b/test/integrity.js
index ba33846..acd0a1e 100644
--- a/test/integrity.js
+++ b/test/integrity.js
@@ -1,7 +1,5 @@
 'use strict'
 
-const Buffer = require('safe-buffer').Buffer
-
 const test = require('tap').test
 
 const ssri = require('..')
@@ -110,6 +108,7 @@ test('match()', t => {
   }, 'returns the strongest match')
   t.notOk(sri.match('sha512-foo'), 'falsy when match fails')
   t.notOk(sri.match('sha384-foo'), 'falsy when match fails')
+  t.notOk(sri.match(null), 'falsy when integrity is null')
   t.end()
 })
 

More details

Full run details

Historical runs