fix: ignore always auth

This commit is contained in:
Zoltan Kochan
2022-10-22 02:17:37 +03:00
parent 0fe9272158
commit 804de211ea
54 changed files with 452 additions and 173 deletions

View File

@@ -0,0 +1,9 @@
---
"@pnpm/audit": major
"@pnpm/default-resolver": major
"@pnpm/fetching-types": major
"@pnpm/npm-resolver": major
"@pnpm/tarball-fetcher": major
---
GetCredentials function replaced with GetAuthHeader.

View File

@@ -0,0 +1,5 @@
---
"pnpm": patch
---
Ignore the always-auth setting.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/network.auth-header": major
---
Initial release.

View File

@@ -1,6 +1,6 @@
import { PnpmError } from '@pnpm/error'
import { AgentOptions, fetchWithAgent, RetryTimeoutOptions } from '@pnpm/fetch'
import { GetCredentials } from '@pnpm/fetching-types'
import { GetAuthHeader } from '@pnpm/fetching-types'
import { Lockfile } from '@pnpm/lockfile-types'
import { DependenciesField } from '@pnpm/types'
import { lockfileToAuditTree } from './lockfileToAuditTree'
@@ -10,7 +10,7 @@ export * from './types'
export async function audit (
lockfile: Lockfile,
getCredentials: GetCredentials,
getAuthHeader: GetAuthHeader,
opts: {
agentOptions?: AgentOptions
include?: { [dependenciesField in DependenciesField]: boolean }
@@ -22,14 +22,14 @@ export async function audit (
const auditTree = lockfileToAuditTree(lockfile, { include: opts.include })
const registry = opts.registry.endsWith('/') ? opts.registry : `${opts.registry}/`
const auditUrl = `${registry}-/npm/v1/security/audits`
const credentials = getCredentials(registry)
const authHeaderValue = getAuthHeader(registry)
const res = await fetchWithAgent(auditUrl, {
agentOptions: opts.agentOptions ?? {},
body: JSON.stringify(auditTree),
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(credentials),
...getAuthHeaders(authHeaderValue),
},
method: 'post',
retry: opts.retry,
@@ -46,15 +46,10 @@ export async function audit (
return res.json() as Promise<AuditReport>
}
function getAuthHeaders (
credentials: {
authHeaderValue: string | undefined
alwaysAuth: boolean | undefined
}
) {
function getAuthHeaders (authHeaderValue: string | undefined) {
const headers: { authorization?: string } = {}
if (credentials.alwaysAuth && credentials.authHeaderValue) {
headers['authorization'] = credentials.authHeaderValue
if (authHeaderValue) {
headers['authorization'] = authHeaderValue
}
return headers
}

View File

@@ -73,7 +73,7 @@ describe('audit', () => {
test('an error is thrown if the audit endpoint responds with a non-OK code', async () => {
const registry = 'http://registry.registry/'
const getCredentials = () => ({ authHeaderValue: undefined, alwaysAuth: undefined })
const getAuthHeader = () => undefined
nock(registry, {
badheaders: ['authorization'],
})
@@ -86,7 +86,7 @@ describe('audit', () => {
importers: {},
lockfileVersion: 5,
},
getCredentials,
getAuthHeader,
{
registry,
retry: {
@@ -101,27 +101,4 @@ describe('audit', () => {
expect(err.code).toEqual('ERR_PNPM_AUDIT_BAD_RESPONSE')
expect(err.message).toEqual('The audit endpoint (at http://registry.registry/-/npm/v1/security/audits) responded with 500: {"message":"Something bad happened"}')
})
test('authorization header is sent if alwaysAuth is true', async () => {
const registry = 'http://registry.registry/'
const getCredentials = () => ({ authHeaderValue: 'Bearer 123', alwaysAuth: true })
nock(registry, {
reqheaders: { authorization: 'Bearer 123' },
})
.post('/-/npm/v1/security/audits')
.reply(200, {})
await audit({
importers: {},
lockfileVersion: 5,
},
getCredentials,
{
registry,
retry: {
retries: 0,
},
})
})
})

View File

@@ -36,10 +36,9 @@
"@pnpm/fetch": "workspace:*",
"@pnpm/fetching-types": "workspace:*",
"@pnpm/git-fetcher": "workspace:*",
"@pnpm/network.auth-header": "workspace:*",
"@pnpm/resolver-base": "workspace:*",
"@pnpm/tarball-fetcher": "workspace:*",
"credentials-by-uri": "^2.1.0",
"mem": "^8.1.1"
"@pnpm/tarball-fetcher": "workspace:*"
},
"devDependencies": {
"@pnpm/client": "workspace:*",

View File

@@ -4,13 +4,12 @@ import {
ResolverFactoryOptions,
} from '@pnpm/default-resolver'
import { AgentOptions, createFetchFromRegistry } from '@pnpm/fetch'
import { FetchFromRegistry, GetCredentials, RetryTimeoutOptions } from '@pnpm/fetching-types'
import { FetchFromRegistry, GetAuthHeader, RetryTimeoutOptions } from '@pnpm/fetching-types'
import type { CustomFetchers, GitFetcher, DirectoryFetcher } from '@pnpm/fetcher-base'
import { createDirectoryFetcher } from '@pnpm/directory-fetcher'
import { createGitFetcher } from '@pnpm/git-fetcher'
import { createTarballFetcher, TarballFetchers } from '@pnpm/tarball-fetcher'
import getCredentialsByURI from 'credentials-by-uri'
import mem from 'mem'
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
export { ResolveFunction }
@@ -31,17 +30,17 @@ export interface Client {
export function createClient (opts: ClientOptions): Client {
const fetchFromRegistry = createFetchFromRegistry(opts)
const getCredentials = mem((registry: string) => getCredentialsByURI(opts.authConfig, registry, opts.userConfig))
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.authConfig, userSettings: opts.userConfig })
return {
fetchers: createFetchers(fetchFromRegistry, getCredentials, opts, opts.customFetchers),
resolve: _createResolver(fetchFromRegistry, getCredentials, opts),
fetchers: createFetchers(fetchFromRegistry, getAuthHeader, opts, opts.customFetchers),
resolve: _createResolver(fetchFromRegistry, getAuthHeader, opts),
}
}
export function createResolver (opts: ClientOptions) {
const fetchFromRegistry = createFetchFromRegistry(opts)
const getCredentials = mem((registry: string) => getCredentialsByURI(opts.authConfig, registry))
return _createResolver(fetchFromRegistry, getCredentials, opts)
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.authConfig, userSettings: opts.userConfig })
return _createResolver(fetchFromRegistry, getAuthHeader, opts)
}
type Fetchers = {
@@ -51,12 +50,12 @@ type Fetchers = {
function createFetchers (
fetchFromRegistry: FetchFromRegistry,
getCredentials: GetCredentials,
getAuthHeader: GetAuthHeader,
opts: Pick<ClientOptions, 'retry' | 'gitShallowHosts'>,
customFetchers?: CustomFetchers
): Fetchers {
const defaultFetchers = {
...createTarballFetcher(fetchFromRegistry, getCredentials, opts),
...createTarballFetcher(fetchFromRegistry, getAuthHeader, opts),
...createGitFetcher(opts),
...createDirectoryFetcher(),
}

View File

@@ -27,6 +27,9 @@
{
"path": "../git-fetcher"
},
{
"path": "../network.auth-header"
},
{
"path": "../resolver-base"
},

View File

@@ -103,8 +103,6 @@ export interface Config {
tag?: string
updateNotifier?: boolean
alwaysAuth?: boolean
// pnpm specific configs
cacheDir: string
configDir: string

View File

@@ -82,7 +82,6 @@ test('a package that need authentication, legacy way', async () => {
const authConfig = {
_auth: 'Zm9vOmJhcg==', // base64 encoded foo:bar
'always-auth': true,
registry: `http://localhost:${REGISTRY_MOCK_PORT}`,
}
await addDependenciesToPackage({}, ['@pnpm.e2e/needs-auth'], await testDefaults({}, {
@@ -145,7 +144,6 @@ test('a scoped package that need legacy authentication specific to scope', async
const authConfig = {
[`//localhost:${REGISTRY_MOCK_PORT}/:_auth`]: 'Zm9vOmJhcg==', // base64 encoded foo:bar
[`//localhost:${REGISTRY_MOCK_PORT}/:always-auth`]: true,
'@private:registry': `http://localhost:${REGISTRY_MOCK_PORT}/`,
registry: 'https://registry.npmjs.org/',
}
@@ -186,7 +184,6 @@ skipOnNode17('a package that need authentication reuses authorization tokens for
const authConfig = {
[`//127.0.0.1:${REGISTRY_MOCK_PORT}/:_authToken`]: data.token,
[`//127.0.0.1:${REGISTRY_MOCK_PORT}/:always-auth`]: true,
registry: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
}
await addDependenciesToPackage({}, ['@pnpm.e2e/needs-auth'], await testDefaults({
@@ -214,7 +211,6 @@ skipOnNode17('a package that need authentication reuses authorization tokens for
const authConfig = {
[`//127.0.0.1:${REGISTRY_MOCK_PORT}/:_authToken`]: data.token,
[`//127.0.0.1:${REGISTRY_MOCK_PORT}/:always-auth`]: true,
registry: `http://127.0.0.1:${REGISTRY_MOCK_PORT}`,
}
let opts = await testDefaults({

View File

@@ -365,8 +365,7 @@ function reportAuthError (
key.endsWith('_auth') ||
key.endsWith('_authToken') ||
key.endsWith('username') ||
key.endsWith('_password') ||
key.endsWith('always-auth')
key.endsWith('_password')
) {
foundSettings.push(`${key}=${hideSecureInfo(key, value)}`)
}

View File

@@ -482,7 +482,6 @@ test('prints authorization error with auth settings', (done) => {
_auth: '0123456789',
_authToken: '0123456789',
_password: '0123456789',
'always-auth': false,
username: 'nagy.gabor',
}
const output$ = toOutput$({
@@ -512,7 +511,6 @@ ${ERROR_PAD}@foo:registry=https://foo.bar
${ERROR_PAD}_auth=0123[hidden]
${ERROR_PAD}_authToken=0123[hidden]
${ERROR_PAD}_password=[hidden]
${ERROR_PAD}always-auth=false
${ERROR_PAD}username=nagy.gabor`)
},
})

View File

@@ -1,5 +1,5 @@
import { PnpmError } from '@pnpm/error'
import { FetchFromRegistry, GetCredentials } from '@pnpm/fetching-types'
import { FetchFromRegistry, GetAuthHeader } from '@pnpm/fetching-types'
import { createGitResolver } from '@pnpm/git-resolver'
import { resolveFromLocal } from '@pnpm/local-resolver'
import {
@@ -21,10 +21,10 @@ export {
export function createResolver (
fetchFromRegistry: FetchFromRegistry,
getCredentials: GetCredentials,
getAuthHeader: GetAuthHeader,
pnpmOpts: ResolverFactoryOptions
): ResolveFunction {
const resolveFromNpm = createNpmResolver(fetchFromRegistry, getCredentials, pnpmOpts)
const resolveFromNpm = createNpmResolver(fetchFromRegistry, getAuthHeader, pnpmOpts)
const resolveFromGit = createGitResolver(pnpmOpts)
return async (wantedDependency, opts) => {
const resolution = await resolveFromNpm(wantedDependency, opts as ResolveFromNpmOptions) ??

View File

@@ -3,8 +3,8 @@ import { createResolver } from '@pnpm/default-resolver'
import { createFetchFromRegistry } from '@pnpm/fetch'
test('createResolver()', () => {
const getCredentials = () => ({ authHeaderValue: '', alwaysAuth: false })
const resolve = createResolver(createFetchFromRegistry({}), getCredentials, {
const getAuthHeader = () => undefined
const resolve = createResolver(createFetchFromRegistry({}), getAuthHeader, {
cacheDir: '.cache',
})
expect(typeof resolve).toEqual('function')

View File

@@ -13,7 +13,4 @@ export type FetchFromRegistry = (
}
) => Promise<Response>
export type GetCredentials = (registry: string) => {
authHeaderValue: string | undefined
alwaysAuth: boolean | undefined
}
export type GetAuthHeader = (uri: string) => string | undefined

View File

@@ -0,0 +1,15 @@
# @pnpm/network.auth-header
> Gets the authorization header for the given URI
[![npm version](https://img.shields.io/npm/v/@pnpm/network.auth-header.svg)](https://www.npmjs.com/package/@pnpm/network.auth-header)
## Installation
```sh
pnpm add @pnpm/network.auth-header
```
## License
MIT

View File

@@ -0,0 +1,3 @@
const config = require('../../jest.config.js');
module.exports = Object.assign({}, config, {});

View File

@@ -0,0 +1,44 @@
{
"name": "@pnpm/network.auth-header",
"version": "0.0.0",
"description": "Gets the authorization header for the given URI",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"!*.map"
],
"engines": {
"node": ">=14.6"
},
"scripts": {
"lint": "eslint src/**/*.ts test/**/*.ts",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"repository": "https://github.com/pnpm/pnpm/blob/main/packages/network.auth-header",
"keywords": [
"pnpm7",
"pnpm",
"auth"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/packages/network.auth-header#readme",
"devDependencies": {
"@pnpm/network.auth-header": "workspace:*",
"safe-buffer": "5.2.1"
},
"dependencies": {
"@pnpm/error": "workspace:*",
"nerf-dart": "1.0.0"
},
"funding": "https://opencollective.com/pnpm",
"exports": {
".": "./lib/index.js"
}
}

View File

@@ -0,0 +1,71 @@
import { PnpmError } from '@pnpm/error'
import { spawnSync } from 'child_process'
import fs from 'fs'
import path from 'path'
import nerfDart from 'nerf-dart'
export function getAuthHeadersFromConfig (
{ allSettings, userSettings }: {
allSettings: Record<string, string>
userSettings: Record<string, string>
}
) {
const authHeaderValueByURI = {}
for (const [key, value] of Object.entries(allSettings)) {
const [uri, authType] = splitKey(key)
switch (authType) {
case '_authToken': {
authHeaderValueByURI[uri] = `Bearer ${value}`
continue
}
case '_auth': {
authHeaderValueByURI[uri] = `Basic ${value}`
continue
}
case 'username': {
if (`${uri}:_password` in allSettings) {
const password = Buffer.from(allSettings[`${uri}:_password`], 'base64').toString('utf8')
authHeaderValueByURI[uri] = `Basic ${Buffer.from(`${value}:${password}`).toString('base64')}`
}
}
}
}
for (const [key, value] of Object.entries(userSettings)) {
const [uri, authType] = splitKey(key)
if (authType === 'tokenHelper') {
authHeaderValueByURI[uri] = loadToken(value, key)
}
}
const registry = allSettings['registry'] ? nerfDart(allSettings['registry']) : '//registry.npmjs.org/'
if (userSettings['tokenHelper']) {
authHeaderValueByURI[registry] = loadToken(userSettings['tokenHelper'], 'tokenHelper')
} else if (allSettings['_authToken']) {
authHeaderValueByURI[registry] = `Bearer ${allSettings['_authToken']}`
} else if (allSettings['_auth']) {
authHeaderValueByURI[registry] = `Basic ${allSettings['_auth']}`
} else if (allSettings['_password'] && allSettings['username']) {
authHeaderValueByURI[registry] = `Basic ${Buffer.from(`${allSettings['username']}:${allSettings['_password']}`).toString('base64')}`
}
return authHeaderValueByURI
}
function splitKey (key: string) {
const index = key.lastIndexOf(':')
if (index === -1) {
return [key, '']
}
return [key.slice(0, index), key.slice(index + 1)]
}
function loadToken (helperPath: string, settingName: string) {
if (!path.isAbsolute(helperPath) || !fs.existsSync(helperPath)) {
throw new PnpmError('BAD_TOKEN_HELPER_PATH', `${settingName} must be an absolute path, without arguments`)
}
const spawnResult = spawnSync(helperPath, { shell: true })
if (spawnResult.status !== 0) {
throw new PnpmError('TOKEN_HELPER_ERROR_STATUS', `Error running "${helperPath}" as a token helper, configured as ${settingName}. Exit code ${spawnResult.status?.toString() ?? ''}`)
}
return spawnResult.stdout.toString('utf8').trimEnd()
}

View File

@@ -0,0 +1,33 @@
import nerfDart from 'nerf-dart'
import { getAuthHeadersFromConfig } from './getAuthHeadersFromConfig'
export function createGetAuthHeaderByURI (
opts: {
allSettings: Record<string, string>
userSettings?: Record<string, string>
}
) {
const authHeaders = getAuthHeadersFromConfig({
allSettings: opts.allSettings,
userSettings: opts.userSettings ?? {},
})
if (Object.keys(authHeaders).length === 0) return () => undefined
return getAuthHeaderByURI.bind(null, authHeaders, getMaxParts(Object.keys(authHeaders)))
}
function getMaxParts (uris: string[]) {
return uris.reduce((max, uri) => {
const parts = uri.split('/').length
return parts > max ? parts : max
}, 0)
}
function getAuthHeaderByURI (authHeaders: Record<string, string>, maxParts: number, uri: string) {
const nerfed = nerfDart(uri)
const parts = nerfed.split('/')
for (let i = Math.min(parts.length, maxParts) - 1; i >= 3; i--) {
const key = `${parts.slice(0, i).join('/')}/` // eslint-disable-line
if (authHeaders[key]) return authHeaders[key]
}
return undefined
}

View File

@@ -0,0 +1,19 @@
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
test('getAuthHeaderByURI()', () => {
const getAuthHeaderByURI = createGetAuthHeaderByURI({
allSettings: {
'//reg.com/:_authToken': 'abc123',
'//reg.co/tarballs/:_authToken': 'xxx',
},
userSettings: {},
})
expect(getAuthHeaderByURI('https://reg.com/')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://reg.com/foo/-/foo-1.0.0.tgz')).toBe('Bearer abc123')
expect(getAuthHeaderByURI('https://reg.io/foo/-/foo-1.0.0.tgz')).toBe(undefined)
expect(getAuthHeaderByURI('https://reg.co/tarballs/foo/-/foo-1.0.0.tgz')).toBe('Bearer xxx')
})
test('returns undefined when the auth header is not found', () => {
expect(createGetAuthHeaderByURI({ allSettings: {}, userSettings: {} })('http://reg.com')).toBe(undefined)
})

View File

@@ -0,0 +1,117 @@
import path from 'path'
import os from 'os'
import { getAuthHeadersFromConfig } from '../src/getAuthHeadersFromConfig'
import { Buffer } from 'safe-buffer'
const osTokenHelper = {
linux: path.join(__dirname, 'utils/test-exec.js'),
win32: path.join(__dirname, 'utils/test-exec.bat'),
}
const osErrorTokenHelper = {
linux: path.join(__dirname, 'utils/test-exec-error.js'),
win32: path.join(__dirname, 'utils/test-exec-error.bat'),
}
// Only exception is win32, all others behave like linux
const osFamily = os.platform() === 'win32' ? 'win32' : 'linux'
describe('getAuthHeadersFromConfig()', () => {
it('should get settings', () => {
const allSettings = {
'//registry.npmjs.org/:_authToken': 'abc123',
'//registry.foobar.eu/:_password': encodeBase64('foobar'),
'//registry.foobar.eu/:username': 'foobar',
'//registry.hu/:_auth': 'foobar',
'//localhost:3000/:_auth': 'foobar',
}
const userSettings = {}
expect(getAuthHeadersFromConfig({ allSettings, userSettings })).toStrictEqual({
'//registry.npmjs.org/': 'Bearer abc123',
'//registry.foobar.eu/': 'Basic Zm9vYmFyOmZvb2Jhcg==',
'//registry.hu/': 'Basic foobar',
'//localhost:3000/': 'Basic foobar',
})
})
describe('should get settings for the default registry', () => {
it('_auth', () => {
const allSettings = {
registry: 'https://reg.com/',
_auth: 'foobar',
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings: {} })).toStrictEqual({
'//reg.com/': 'Basic foobar',
})
})
it('username/_password', () => {
const allSettings = {
registry: 'https://reg.com/',
username: 'foo',
_password: 'bar',
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings: {} })).toStrictEqual({
'//reg.com/': `Basic ${encodeBase64('foo:bar')}`,
})
})
it('tokenHelper', () => {
const allSettings = {
registry: 'https://reg.com/',
}
const userSettings = {
tokenHelper: osTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings })).toStrictEqual({
'//reg.com/': 'Bearer token-from-spawn',
})
})
it('only read token helper from user config', () => {
const allSettings = {
registry: 'https://reg.com/',
tokenHelper: osTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings: {} })).toStrictEqual({})
})
})
it('should get tokenHelper', () => {
const userSettings = {
'//registry.foobar.eu/:tokenHelper': osTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings: {}, userSettings })).toStrictEqual({
'//registry.foobar.eu/': 'Bearer token-from-spawn',
})
})
it('should throw an error if the token helper is not an absolute path', () => {
expect(() => getAuthHeadersFromConfig({
allSettings: {},
userSettings: {
'//reg.com:tokenHelper': './utils/text-exec.js',
},
})).toThrowError('must be an absolute path, without arguments')
})
it('should throw an error if the token helper is not an absolute path with args', () => {
expect(() => getAuthHeadersFromConfig({
allSettings: {},
userSettings: {
'//reg.com:tokenHelper': `${osTokenHelper[osFamily]} arg1`,
},
})).toThrowError('must be an absolute path, without arguments')
})
it('should throw an error if the token helper fails', () => {
expect(() => getAuthHeadersFromConfig({
allSettings: {},
userSettings: {
'//reg.com:tokenHelper': osErrorTokenHelper[osFamily],
},
})).toThrowError('Exit code')
})
it('only read token helper from user config', () => {
const allSettings = {
'//reg.com:tokenHelper': osTokenHelper[osFamily],
}
expect(getAuthHeadersFromConfig({ allSettings, userSettings: {} })).toStrictEqual({})
})
})
function encodeBase64 (s: string) {
return Buffer.from(s, 'utf8').toString('base64')
}

View File

@@ -0,0 +1,3 @@
@echo off
echo Bearer token-from-spawn-errored
exit /b 1

View File

@@ -0,0 +1 @@
process.exit(1)

View File

@@ -0,0 +1 @@
@echo Bearer token-from-spawn

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
console.log('Bearer token-from-spawn')

View File

@@ -0,0 +1,16 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../typings/**/*.d.ts"
],
"references": [
{
"path": "../error"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../typings/**/*.d.ts"
]
}

View File

@@ -32,8 +32,8 @@ export async function fetchNode (fetch: FetchFromRegistry, version: string, targ
await downloadAndUnpackZip(fetch, tarball, targetDir, pkgName)
return
}
const getCredentials = () => ({ authHeaderValue: undefined, alwaysAuth: undefined })
const fetchers = createTarballFetcher(fetch, getCredentials, {
const getAuthHeader = () => undefined
const fetchers = createTarballFetcher(fetch, getAuthHeader, {
retry: opts.retry,
timeout: opts.fetchTimeout,
})

View File

@@ -2,7 +2,7 @@ import path from 'path'
import { PnpmError } from '@pnpm/error'
import {
FetchFromRegistry,
GetCredentials,
GetAuthHeader,
RetryTimeoutOptions,
} from '@pnpm/fetching-types'
import { resolveWorkspaceRange } from '@pnpm/resolve-workspace-range'
@@ -72,7 +72,7 @@ export interface ResolverFactoryOptions {
export function createNpmResolver (
fetchFromRegistry: FetchFromRegistry,
getCredentials: GetCredentials,
getAuthHeader: GetAuthHeader,
opts: ResolverFactoryOptions
) {
if (typeof opts.cacheDir !== 'string') {
@@ -86,13 +86,12 @@ export function createNpmResolver (
cacheKey: (...args) => JSON.stringify(args),
maxAge: 1000 * 20, // 20 seconds
})
const getAuthHeaderValueByURI = (registry: string) => getCredentials(registry).authHeaderValue
const metaCache = new LRU({
max: 10000,
ttl: 120 * 1000, // 2 minutes
})
return resolveNpm.bind(null, {
getAuthHeaderValueByURI,
getAuthHeaderValueByURI: getAuthHeader,
pickPackage: pickPackage.bind(null, {
fetch,
filterMetadata: opts.filterMetadata,

View File

@@ -29,8 +29,8 @@ const registry = 'https://registry.npmjs.org/'
const delay = async (time: number) => new Promise<void>((resolve) => setTimeout(() => resolve(), time))
const fetch = createFetchFromRegistry({})
const getCredentials = () => ({ authHeaderValue: undefined, alwaysAuth: undefined })
const createResolveFromNpm = createNpmResolver.bind(null, fetch, getCredentials)
const getAuthHeader = () => undefined
const createResolveFromNpm = createNpmResolver.bind(null, fetch, getAuthHeader)
async function retryLoadJsonFile<T> (filePath: string) {
let retry = 0

View File

@@ -15,8 +15,8 @@ const badDatesMeta = loadJsonFile.sync<any>(f.find('bad-dates.json'))
/* eslint-enable @typescript-eslint/no-explicit-any */
const fetch = createFetchFromRegistry({})
const getCredentials = () => ({ authHeaderValue: undefined, alwaysAuth: undefined })
const createResolveFromNpm = createNpmResolver.bind(null, fetch, getCredentials)
const getAuthHeader = () => undefined
const createResolveFromNpm = createNpmResolver.bind(null, fetch, getAuthHeader)
test('fall back to a newer version if there is no version published by the given date', async () => {
nock(registry)

View File

@@ -47,11 +47,11 @@
"@pnpm/constants": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/lockfile-file": "workspace:*",
"@pnpm/network.auth-header": "workspace:*",
"@pnpm/read-project-manifest": "workspace:*",
"@pnpm/types": "workspace:*",
"@zkochan/table": "^1.0.0",
"chalk": "^4.1.2",
"credentials-by-uri": "^2.1.0",
"mem": "^8.1.1",
"ramda": "npm:@pnpm/ramda@0.28.1",
"render-help": "^1.0.2"

View File

@@ -1,4 +1,5 @@
import { audit, AuditReport, AuditVulnerabilityCounts } from '@pnpm/audit'
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
import { docsUrl, TABLE_OPTIONS } from '@pnpm/cli-utils'
import { Config, types as allTypes, UniversalOptions } from '@pnpm/config'
import { WANTED_LOCKFILE } from '@pnpm/constants'
@@ -9,7 +10,6 @@ import { table } from '@zkochan/table'
import chalk from 'chalk'
import pick from 'ramda/src/pick'
import renderHelp from 'render-help'
import getCredentialsByURI from 'credentials-by-uri'
import { fix } from './fix'
// eslint-disable
@@ -124,7 +124,6 @@ export async function handler (
| 'production'
| 'dev'
| 'optional'
| 'alwaysAuth'
| 'userConfig'
| 'rawConfig'
>
@@ -139,9 +138,9 @@ export async function handler (
optionalDependencies: opts.optional !== false,
}
let auditReport!: AuditReport
const getCredentials = (registry: string) => getCredentialsByURI(opts.rawConfig, registry, opts.userConfig)
const getAuthHeader = createGetAuthHeaderByURI({ allSettings: opts.rawConfig, userSettings: opts.userConfig })
try {
auditReport = await audit(lockfile, getCredentials, {
auditReport = await audit(lockfile, getAuthHeader, {
agentOptions: {
ca: opts.ca,
cert: opts.cert,

View File

@@ -134,7 +134,7 @@ test('audit does not exit with code 1 if the registry responds with a non-200 re
expect(stripAnsi(output)).toBe(`The audit endpoint (at ${registries.default}-/npm/v1/security/audits) responded with 500: {"message":"Something bad happened"}`)
})
test('audit sends authToken if alwaysAuth is true', async () => {
test('audit sends authToken', async () => {
nock(registries.default, {
reqheaders: { authorization: 'Bearer 123' },
})
@@ -146,7 +146,6 @@ test('audit sends authToken if alwaysAuth is true', async () => {
userConfig: {},
rawConfig: {
registry: registries.default,
'always-auth': true,
[`${registries.default.replace(/^https?:/, '')}:_authToken`]: '123',
},
registries,

View File

@@ -30,6 +30,9 @@
{
"path": "../lockfile-file"
},
{
"path": "../network.auth-header"
},
{
"path": "../read-project-manifest"
},

View File

@@ -3,7 +3,6 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
export const DEFAULT_OPTS = {
alwaysAuth: false,
argv: {
original: [],
},

View File

@@ -14,7 +14,6 @@ const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
const TMP = tempy.directory()
const DEFAULT_OPTS = {
alwaysAuth: false,
ca: undefined,
cacheDir: path.join(TMP, 'cache'),
cert: undefined,

View File

@@ -12,7 +12,6 @@ const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
const TMP = tempy.directory()
const DEFAULT_OPTS = {
alwaysAuth: false,
ca: undefined,
cacheDir: path.join(TMP, 'cache'),
cert: undefined,

View File

@@ -3,7 +3,6 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
export const DEFAULT_OPTS = {
alwaysAuth: false,
argv: {
original: [],
},

View File

@@ -3,7 +3,6 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
export const DEFAULT_OPTS = {
alwaysAuth: false,
argv: {
original: [],
},

View File

@@ -123,7 +123,6 @@ export type OutdatedCommandOptions = {
table?: boolean
} & Pick<Config,
| 'allProjects'
| 'alwaysAuth'
| 'ca'
| 'cacheDir'
| 'cert'

View File

@@ -20,7 +20,6 @@ const withPnpmUpdateIgnore = path.join(fixtures, 'with-pnpm-update-ignore')
const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}`
const OUTDATED_OPTIONS = {
alwaysAuth: false,
cacheDir: 'cache',
fetchRetries: 1,
fetchRetryFactor: 1,

View File

@@ -3,7 +3,6 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
export const DEFAULT_OPTS = {
alwaysAuth: false,
argv: {
original: [],
},

View File

@@ -3,7 +3,6 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
export const DEFAULT_OPTS = {
alwaysAuth: false,
argv: {
original: [],
},

View File

@@ -3,7 +3,6 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
export const DEFAULT_OPTS = {
alwaysAuth: false,
argv: {
original: [],
},

View File

@@ -3,7 +3,6 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
export const REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
export const DEFAULT_OPTS = {
alwaysAuth: false,
argv: {
original: [],
},

View File

@@ -6,7 +6,6 @@ export const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}`
const tmp = tempDir()
export const DEFAULT_OPTS = {
alwaysAuth: false,
argv: {
original: [],
},

View File

@@ -17,7 +17,6 @@ async function main() {
store,
})
const fetchers = createFetcher({
alwaysAuth: true,
registry,
strictSsl: true,
rawConfig,

View File

@@ -6,7 +6,7 @@ import {
import type { Cafs } from '@pnpm/cafs-types'
import {
FetchFromRegistry,
GetCredentials,
GetAuthHeader,
RetryTimeoutOptions,
} from '@pnpm/fetching-types'
import {
@@ -29,7 +29,7 @@ export interface TarballFetchers {
export function createTarballFetcher (
fetchFromRegistry: FetchFromRegistry,
getCredentials: GetCredentials,
getAuthHeader: GetAuthHeader,
opts: {
timeout?: number
retry?: RetryTimeoutOptions
@@ -43,7 +43,7 @@ export function createTarballFetcher (
const remoteTarballFetcher = fetchFromTarball.bind(null, {
download,
getCredentialsByURI: getCredentials,
getAuthHeaderByURI: getAuthHeader,
offline: opts.offline,
}) as FetchFunction
@@ -57,10 +57,7 @@ export function createTarballFetcher (
async function fetchFromTarball (
ctx: {
download: DownloadFunction
getCredentialsByURI: (registry: string) => {
authHeaderValue: string | undefined
alwaysAuth: boolean | undefined
}
getAuthHeaderByURI: (registry: string) => string | undefined
offline?: boolean
},
cafs: Cafs,
@@ -75,9 +72,8 @@ async function fetchFromTarball (
throw new PnpmError('NO_OFFLINE_TARBALL',
`A package is missing from the store but cannot download it in offline mode. The missing package may be downloaded from ${resolution.tarball}.`)
}
const auth = resolution.registry ? ctx.getCredentialsByURI(resolution.registry) : undefined
return ctx.download(resolution.tarball, {
auth,
getAuthHeaderByURI: ctx.getAuthHeaderByURI,
cafs,
integrity: resolution.integrity,
manifest: opts.manifest,

View File

@@ -1,5 +1,4 @@
import { IncomingMessage } from 'http'
import urlLib from 'url'
import { requestRetryLogger } from '@pnpm/core-loggers'
import { FetchError, PnpmError } from '@pnpm/error'
import { FetchResult } from '@pnpm/fetcher-base'
@@ -43,10 +42,7 @@ export interface HttpResponse {
}
export type DownloadFunction = (url: string, opts: {
auth?: {
authHeaderValue: string | undefined
alwaysAuth: boolean | undefined
}
getAuthHeaderByURI: (registry: string) => string | undefined
cafs: Cafs
manifest?: DeferredManifestPromise
registry?: string
@@ -83,10 +79,7 @@ export function createDownloader (
}
return async function download (url: string, opts: {
auth?: {
authHeaderValue: string | undefined
alwaysAuth: boolean | undefined
}
getAuthHeaderByURI: (registry: string) => string | undefined
cafs: Cafs
manifest?: DeferredManifestPromise
registry?: string
@@ -94,13 +87,7 @@ export function createDownloader (
onProgress?: (downloaded: number) => void
integrity?: string
}): Promise<FetchResult> {
// If a tarball is hosted on a different place than the manifest, only send
// credentials on `alwaysAuth`
const shouldAuth = (opts.auth != null) && (
opts.auth.alwaysAuth === true ||
!opts.registry ||
new urlLib.URL(url).host === new urlLib.URL(opts.registry).host
)
const authHeaderValue = opts.getAuthHeaderByURI(url)
const op = retry.operation(retryOpts)
@@ -136,7 +123,6 @@ export function createDownloader (
async function fetch (currentAttempt: number): Promise<FetchResult> {
try {
const authHeaderValue = shouldAuth ? opts.auth?.authHeaderValue : undefined
const res = await fetchFromRegistry(url, {
authHeaderValue,
// The fetch library can retry requests on bad HTTP responses.

View File

@@ -23,8 +23,8 @@ const tarballSize = 1279
const tarballIntegrity = 'sha1-HssnaJydJVE+rbyZFKc/VAi+enY='
const registry = 'http://example.com/'
const fetchFromRegistry = createFetchFromRegistry({})
const getCredentials = () => ({ authHeaderValue: undefined, alwaysAuth: undefined })
const fetch = createTarballFetcher(fetchFromRegistry, getCredentials, {
const getAuthHeader = () => undefined
const fetch = createTarballFetcher(fetchFromRegistry, getAuthHeader, {
retry: {
maxTimeout: 100,
minTimeout: 0,
@@ -204,7 +204,7 @@ test("don't fail when fetching a local tarball in offline mode", async () => {
tarball: `file:${tarballAbsoluteLocation}`,
}
const fetch = createTarballFetcher(fetchFromRegistry, getCredentials, {
const fetch = createTarballFetcher(fetchFromRegistry, getAuthHeader, {
offline: true,
retry: {
maxTimeout: 100,
@@ -228,7 +228,7 @@ test('fail when trying to fetch a non-local tarball in offline mode', async () =
tarball: `${registry}foo.tgz`,
}
const fetch = createTarballFetcher(fetchFromRegistry, getCredentials, {
const fetch = createTarballFetcher(fetchFromRegistry, getAuthHeader, {
offline: true,
retry: {
maxTimeout: 100,
@@ -319,11 +319,8 @@ test('accessing private packages', async () => {
process.chdir(tempy.directory())
const getCredentials = () => ({
alwaysAuth: undefined,
authHeaderValue: 'Bearer ofjergrg349gj3f2',
})
const fetch = createTarballFetcher(fetchFromRegistry, getCredentials, {
const getAuthHeader = () => 'Bearer ofjergrg349gj3f2'
const fetch = createTarballFetcher(fetchFromRegistry, getAuthHeader, {
retry: {
maxTimeout: 100,
minTimeout: 0,

72
pnpm-lock.yaml generated
View File

@@ -214,6 +214,22 @@ importers:
specifier: 13.2.9
version: 13.2.9
packages/network.auth-header:
dependencies:
'@pnpm/error':
specifier: workspace:*
version: link:../error
nerf-dart:
specifier: 1.0.0
version: 1.0.0
devDependencies:
'@pnpm/network.auth-header':
specifier: workspace:*
version: 'link:'
safe-buffer:
specifier: 5.2.1
version: 5.2.1
packages/build-modules:
dependencies:
'@pnpm/calc-dep-state':
@@ -416,6 +432,9 @@ importers:
packages/client:
dependencies:
'@pnpm/network.auth-header':
specifier: workspace:*
version: link:../network.auth-header
'@pnpm/default-resolver':
specifier: workspace:*
version: link:../default-resolver
@@ -437,12 +456,6 @@ importers:
'@pnpm/tarball-fetcher':
specifier: workspace:*
version: link:../tarball-fetcher
credentials-by-uri:
specifier: ^2.1.0
version: 2.1.0
mem:
specifier: ^8.1.1
version: 8.1.1
devDependencies:
'@pnpm/client':
specifier: workspace:*
@@ -2986,6 +2999,9 @@ importers:
'@pnpm/audit':
specifier: workspace:*
version: link:../audit
'@pnpm/network.auth-header':
specifier: workspace:*
version: link:../network.auth-header
'@pnpm/cli-utils':
specifier: workspace:*
version: link:../cli-utils
@@ -3013,9 +3029,6 @@ importers:
chalk:
specifier: ^4.1.2
version: 4.1.2
credentials-by-uri:
specifier: ^2.1.0
version: 2.1.0
mem:
specifier: ^8.1.1
version: 8.1.1
@@ -6392,14 +6405,14 @@ packages:
'@jest/test-result': 29.1.2
'@jest/transform': 29.1.2_@babel+types@7.19.3
'@jest/types': 29.1.2
'@types/node': 18.8.4
'@types/node': 18.8.5
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.5.0
exit: 0.1.2
graceful-fs: 4.2.10
jest-changed-files: 29.0.0
jest-config: 29.1.2_2bpsusnajtsxcfljrp4g4m4x6u
jest-config: 29.1.2_umh5qgof6v332qbluj233ooouq
jest-haste-map: 29.1.2
jest-message-util: 29.1.2
jest-regex-util: 29.0.0
@@ -6602,7 +6615,7 @@ packages:
'@jest/schemas': 29.0.0
'@types/istanbul-lib-coverage': 2.0.4
'@types/istanbul-reports': 3.0.1
'@types/node': 18.8.4
'@types/node': 18.8.5
'@types/yargs': 17.0.13
chalk: 4.1.2
dev: true
@@ -6848,11 +6861,6 @@ packages:
- typanion
dev: true
/@pnpm/constants/5.0.0:
resolution: {integrity: sha512-VhUGKR5jvAtoBHgHAB3Kfc9g42ocVUws9iOafGAQ+xjR8uLokUCReXDpLXRRtrqw8N8yyh3gLNpCJs/AYadA1g==}
engines: {node: '>=12.17'}
dev: false
/@pnpm/constants/6.1.0:
resolution: {integrity: sha512-L6AiU3OXv9kjKGTJN9j8n1TeJGDcLX9atQlZvAkthlvbXjvKc5SKNWESc/eXhr5nEfuMWhQhiKHDJCpYejmeCQ==}
engines: {node: '>=14.19'}
@@ -6983,13 +6991,6 @@ packages:
ramda: /@pnpm/ramda/0.28.1
dev: true
/@pnpm/error/2.1.0:
resolution: {integrity: sha512-IxtPG61WgxsDNbtWW+128RiRo6WHhm7Dm2vm77xxC/zEi/uQ35Uf59olhVT3m2/ETDw/iz7Lv1RpvPYTsglX9g==}
engines: {node: '>=12.17'}
dependencies:
'@pnpm/constants': 5.0.0
dev: false
/@pnpm/error/4.0.0:
resolution: {integrity: sha512-NI4DFCMF6xb1SA0bZiiV5KrMCaJM2QmPJFC6p78FXujn7FpiRSWhT9r032wpuQumsl7DEmN4s3wl/P8TA+bL8w==}
engines: {node: '>=14.6'}
@@ -8023,7 +8024,7 @@ packages:
resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==}
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 14.18.32
'@types/node': 18.8.5
dev: true
/@types/graceful-fs/4.1.5:
@@ -9899,14 +9900,6 @@ packages:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
/credentials-by-uri/2.1.0:
resolution: {integrity: sha512-Ia57VrZYcs4YTPUB4Pirjf3MYsSdc7mAYGC99lUKii0KsohmZgpPvVOeqW1NWBCRg3HVrLOtSreLqkmFIUi8WQ==}
engines: {node: '>=10'}
dependencies:
'@pnpm/error': 2.1.0
nerf-dart: 1.0.0
dev: false
/cross-env/7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@@ -12436,7 +12429,7 @@ packages:
- ts-node
dev: true
/jest-config/29.1.2_2bpsusnajtsxcfljrp4g4m4x6u:
/jest-config/29.1.2_62226uqvmdh4n5ozoaflu2xbzq:
resolution: {integrity: sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
@@ -12451,7 +12444,7 @@ packages:
'@babel/core': 7.19.3
'@jest/test-sequencer': 29.1.2
'@jest/types': 29.1.2
'@types/node': 18.8.4
'@types/node': 14.18.29
babel-jest: 29.1.2_rbxuy635u6cfvllmi4ba4h7ksu
chalk: 4.1.2
ci-info: 3.5.0
@@ -12477,7 +12470,7 @@ packages:
- supports-color
dev: true
/jest-config/29.1.2_62226uqvmdh4n5ozoaflu2xbzq:
/jest-config/29.1.2_umh5qgof6v332qbluj233ooouq:
resolution: {integrity: sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
@@ -12492,7 +12485,7 @@ packages:
'@babel/core': 7.19.3
'@jest/test-sequencer': 29.1.2
'@jest/types': 29.1.2
'@types/node': 14.18.29
'@types/node': 18.8.5
babel-jest: 29.1.2_rbxuy635u6cfvllmi4ba4h7ksu
chalk: 4.1.2
ci-info: 3.5.0
@@ -12764,7 +12757,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.1.2
'@types/node': 18.8.4
'@types/node': 18.8.5
chalk: 4.1.2
ci-info: 3.5.0
graceful-fs: 4.2.10
@@ -17212,7 +17205,6 @@ time:
/cmd-extension/1.0.2: '2021-09-28T21:08:51.481Z'
/comver-to-semver/1.0.0: '2021-04-04T23:59:39.895Z'
/concat-stream/2.0.0: '2018-12-21T14:22:15.876Z'
/credentials-by-uri/2.1.0: '2021-12-25T22:13:49.239Z'
/cross-env/7.0.3: '2020-12-01T20:25:26.541Z'
/cross-spawn/7.0.3: '2020-05-25T15:35:07.209Z'
/cross-var-no-babel/1.2.0: '2017-11-12T10:58:04.011Z'
@@ -17266,6 +17258,7 @@ time:
/mdast-util-to-string/2.0.0: '2020-11-11T09:15:26.835Z'
/mem/8.1.1: '2021-04-20T15:49:07.407Z'
/micromatch/4.0.5: '2022-03-24T19:31:47.722Z'
/nerf-dart/1.0.0: '2015-08-20T12:22:17.009Z'
/nock/13.2.9: '2022-07-19T18:34:55.582Z'
/node-fetch/3.0.0-beta.9: '2020-09-05T12:52:27.791Z'
/node-gyp/9.2.0: '2022-10-04T10:40:24.552Z'
@@ -17314,6 +17307,7 @@ time:
/root-link-target/3.1.0: '2021-02-11T22:54:37.968Z'
/run-groups/3.0.1: '2020-06-06T16:33:09.423Z'
/rxjs/7.5.7: '2022-09-25T18:42:55.719Z'
/safe-buffer/5.2.1: '2020-05-10T16:37:30.776Z'
/safe-execa/0.1.2: '2022-07-18T01:09:17.517Z'
/safe-promise-defer/1.0.1: '2022-06-18T13:48:40.297Z'
/sanitize-filename/1.6.3: '2019-08-26T02:10:56.988Z'

5
typings/local.d.ts vendored
View File

@@ -137,6 +137,11 @@ declare module 'decompress-maybe' {
export = anything
}
declare module 'nerf-dart' {
const anything: any
export = anything
}
declare module 'patch-package/dist/applyPatches' {
export function applyPatch (opts: any): boolean
}