@preventive/upstream 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cocoapods.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ // The name is asserted to be a root's as Podfile.lock writes it, printable
2
+ // ASCII but for spaces, parentheses, slashes and backslashes, with no `.`
3
+ // first; the version one of Pod::Version's; and the checksum 40 lowercase
4
+ // hex digits, before any request or cache path. The cache is setCacheDir's
5
+ // (npm.js).
6
+
7
+ export { HttpError } from './npm.js'
8
+
9
+ // A version's podspec from trunk, the JSON CocoaPods reads, whole, in
10
+ // memory, as the CDN serves it under Specs/<a>/<b>/<c>/<name>/<version>/,
11
+ // the first three hex digits of the md5 of the name: the files of
12
+ // CocoaPods/Specs, so a pod from https://github.com/CocoaPods/Specs.git
13
+ // too. Trunk says nothing of what a podspec hashes to, so `checksum` is
14
+ // required: its sha1, as Podfile.lock records it in SPEC CHECKSUMS. The
15
+ // bytes are checked against it, whether they were downloaded (before they
16
+ // are cached) or read from a cache. The CDN's redirect to jsDelivr is
17
+ // followed, as the bytes are checked whoever answers.
18
+ //
19
+ // A podspec is not fixed once trunk has it: `pod trunk deprecate` adds a
20
+ // field to the latest version's. So one is cached by its checksum as well
21
+ // as its version, and a lockfile written before such a change has a
22
+ // checksum no download matches any longer, which throws.
23
+ //
24
+ // CocoaPods' own spec repos are read first, and never written: that path
25
+ // in each directory under $CP_REPOS_DIR, else $CP_HOME_DIR/repos, else
26
+ // `~/.cocoapods/repos`. A file there that does not match is passed over.
27
+ // One in setCacheDir's cache that does not match throws. A cached podspec
28
+ // that matches is answered with no request at all.
29
+ //
30
+ // What the pod is built from is not trunk's, and is not fetched here: a
31
+ // commit of a GitHub repo, as a podspec's `source` or CHECKOUT OPTIONS
32
+ // may name, is github.js's getRepoTarball's.
33
+ export function getPodspec(name: string, version: string, checksum: string): Promise<Uint8Array>
package/cocoapods.js ADDED
@@ -0,0 +1,2 @@
1
+ export { getPodspec } from './src/cocoapods.js'
2
+ export { HttpError } from './src/http.js'
package/github.d.ts CHANGED
@@ -89,6 +89,17 @@ export interface Client {
89
89
  // `node_modules`, of packages installed. A symlink is no file. Refused
90
90
  // where a path found is one getRepoFile does not take, such as one with a
91
91
  // control character in it.
92
+ // How `head` stands to `base` in the repo (each a full commit sha, or a
93
+ // branch or tag name), as GitHub's compare tells it: `ahead` where `base`
94
+ // is in `head`'s history, `behind` the other way round, `identical`, or
95
+ // `diverged`; with the commits each has that the other hasn't, and their
96
+ // merge base. Refused unless the status agrees with those counts, the
97
+ // merge base is `base` where `head` is ahead of it or the same, and the
98
+ // base GitHub compared is `base` where that is a commit. GitHub answers
99
+ // for a commit of any fork of the repo too, through the repo's own name:
100
+ // whether one is the repo's own is whether a ref of the repo (its default
101
+ // branch, say) is ahead of it or the same.
102
+ compareCommits(options: { repo: RepoName; base: string; head: string }): Promise<{ status: 'ahead' | 'behind' | 'identical' | 'diverged'; aheadBy: number; behindBy: number; mergeBase: string }>
92
103
  findRepoLockfiles(options: { repo: RepoName; sha: string; directory?: string }): Promise<{ lockfiles: RepoLockfile[]; projects: RepoProjectFile[] }>
93
104
  // An advisory by its GHSA id. With `repo`, that repository's published
94
105
  // copy: the maintainer's latest text, there before GitHub reviews it,
@@ -169,6 +180,10 @@ export interface Client {
169
180
  // found as getRepoTreeId finds it: GitHub's listing, hashed back to the
170
181
  // tree's id, so none is left out or changed.
171
182
  listRepoDir(options: { repo: RepoName; sha: string; directory?: string }): Promise<RepoDirEntry[]>
183
+ // Every page of the repo's branches, as GitHub lists them, each with the
184
+ // commit at its head; more than `maxPages` (100 by default) pages of 100
185
+ // is an error, as is a branch with no commit.
186
+ listRepoBranches(options: { repo: RepoName; maxPages?: number }): Promise<{ branch: string; oid: string }[]>
172
187
  // Every page of the repo's tags, as GitHub lists them, each with the
173
188
  // commit GitHub names for it, an annotated tag's included, which
174
189
  // getRepoTag checks is one; more than `maxPages` (100 by default) pages
package/npm.d.ts CHANGED
@@ -37,11 +37,12 @@ export const defaultCacheDir: string | undefined
37
37
  // with no request. Point it only at storage that
38
38
  // nothing less trusted than the caller can write, never a cache shared
39
39
  // with or restored from lower-trust jobs. Tarballs, crates (cargo.js), zips
40
- // (soldeer.js) and tree tarballs (github.js) don't rely on this, since
41
- // they are checked against their integrity on every call; and the version
42
- // documents kept here (getMeta) never supply an integrity, only confirm
43
- // one a caller gives (verifyDist, and getMeta with a `dist`). A call's `cache` option, where it has
44
- // one, keeps what it caches elsewhere, or writes none of it here.
40
+ // (soldeer.js), podspecs (cocoapods.js) and tree tarballs (github.js) don't
41
+ // rely on this, since they are checked against their integrity on every
42
+ // call; and the version documents kept here (getMeta) never supply an
43
+ // integrity, only confirm one a caller gives (verifyDist, and getMeta with
44
+ // a `dist`). A call's `cache` option, where it has one, keeps what it
45
+ // caches elsewhere, or writes none of it here.
45
46
  export function setCacheDir(dir?: string | false): void
46
47
 
47
48
  // A store of the caller's, a database's say, for what would otherwise be
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@preventive/upstream",
3
- "version": "1.3.0",
4
- "description": "Minimal npm, crates.io, Soldeer, GitHub and git checkout clients",
3
+ "version": "1.4.0",
4
+ "description": "Minimal npm, crates.io, Soldeer, CocoaPods trunk, GitHub and git checkout clients",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  "./advisories.js": {
@@ -12,6 +12,10 @@
12
12
  "types": "./cargo.d.ts",
13
13
  "default": "./cargo.js"
14
14
  },
15
+ "./cocoapods.js": {
16
+ "types": "./cocoapods.d.ts",
17
+ "default": "./cocoapods.js"
18
+ },
15
19
  "./github.js": {
16
20
  "types": "./github.d.ts",
17
21
  "default": "./github.js"
@@ -52,6 +56,8 @@
52
56
  "advisories.js",
53
57
  "cargo.d.ts",
54
58
  "cargo.js",
59
+ "cocoapods.d.ts",
60
+ "cocoapods.js",
55
61
  "github.d.ts",
56
62
  "github.js",
57
63
  "git.d.ts",
@@ -79,6 +85,7 @@
79
85
  "src/cache.js",
80
86
  "src/cargo/crate.js",
81
87
  "src/cargo/repos.js",
88
+ "src/cocoapods.js",
82
89
  "src/download.js",
83
90
  "src/git.js",
84
91
  "src/github/client.js",
@@ -38,7 +38,7 @@ export function detailsOf(text, id) {
38
38
  return { details: text }
39
39
  }
40
40
 
41
- const SEVERITIES = new Set(['critical', 'high', 'moderate', 'low', 'info'])
41
+ export const SEVERITIES = ['critical', 'high', 'moderate', 'low', 'info']
42
42
  const isCvssVector = matches(/^CVSS:[34]\.\d(?:\/[A-Z]{1,4}:[A-Z]{1,2})+$/u)
43
43
  const isCwe = matches(/^CWE-\d{1,6}$/u)
44
44
 
@@ -48,7 +48,7 @@ const isCwe = matches(/^CWE-\d{1,6}$/u)
48
48
  export function metrics({ severity, score, vector, cwe }) {
49
49
  const word = typeof severity === 'string' ? severity.toLowerCase().replace(/^medium$/u, 'moderate') : undefined
50
50
  return {
51
- ...(SEVERITIES.has(word) && { severity: word }),
51
+ ...(SEVERITIES.includes(word) && { severity: word }),
52
52
  ...(typeof score === 'number' && score > 0 && score <= 10 && { cvss: score }),
53
53
  ...(isCvssVector(vector) && { cvssVector: vector }),
54
54
  cwe: isStrings(cwe) ? cwe.filter(isCwe) : [],
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
2
2
 
3
3
  import { assertArgs, assertBoolean, assertRepo, assertion, isIterable, optional, sameName } from '../args.js'
4
4
  import { assertCache } from '../cache.js'
5
- import { order } from './common.js'
5
+ import { SEVERITIES, order } from './common.js'
6
6
  import { GITHUB, SOLDEER, assertClient } from './github.js'
7
7
  import { NPM } from './npm.js'
8
8
  import { CARGO, COMPOSER } from './osv.js'
@@ -37,7 +37,6 @@ function collect(packages) {
37
37
  // Which of an advisory's rows stands for it: npm's registry's, then OSV's,
38
38
  // then its repository's.
39
39
  const SOURCES = ['registry', 'osv', 'repository']
40
- const SEVERITIES = ['critical', 'high', 'moderate', 'low', 'info']
41
40
  const worse = (a, b) => (b === undefined || (a !== undefined && SEVERITIES.indexOf(a) < SEVERITIES.indexOf(b)) ? a : b)
42
41
 
43
42
  // An advisory's rows as one: the first's by source, what it lacks from the
package/src/args.js CHANGED
@@ -99,6 +99,12 @@ export const assertSoldeerName = assertion('a Soldeer package name', matches(/^(
99
99
  // Soldeer takes any version but an empty one. Its registry has semver
100
100
  // (`5.7.0-rc.0`, `1.0.2-solc-0.8-simulate`), bare numbers and commit hashes.
101
101
  export const assertSoldeerVersion = assertion('letters, digits, `.`, `_`, `+` and `-`', matches(/^(?=.{1,128}$)[\dA-Za-z][\w.+-]*$/u))
102
+ // A root's name as Podfile.lock has it, and @preventive/lockfile reads it:
103
+ // printable ASCII but for spaces, parentheses, slashes and backslashes, no
104
+ // `.` first, which the spec linter refuses, and 1024 characters at most.
105
+ const assertPodName = assertion('a pod\'s root name', matches(/^(?=.{1,1024}$)(?!\.)[!-'*-.0-[\]-~]+$/u))
106
+ // Pod::Version's pattern, as Podfile.lock writes a version.
107
+ const assertPodVersion = assertion('a CocoaPods version', matches(/^(?=.{1,256}$)\d+(?:\.[\dA-Za-z-]+)*(?:\+[\dA-Za-z.-]+)?$/u))
102
108
  // A package's name and version, as a registry's getters take them.
103
109
  const assertNameVersion = (assertName, assertVersion) => (method, name, version) => {
104
110
  assertName(method, 'name', name)
@@ -107,5 +113,8 @@ const assertNameVersion = (assertName, assertVersion) => (method, name, version)
107
113
  export const assertCrate = assertNameVersion(assertCrateName, assertCrateVersion)
108
114
  export const assertPackage = assertNameVersion(assertPackageName, assertPackageVersion)
109
115
  export const assertSoldeerPackage = assertNameVersion(assertSoldeerName, assertSoldeerVersion)
116
+ export const assertPod = assertNameVersion(assertPodName, assertPodVersion)
110
117
  // As Cargo.lock and soldeer.lock write a checksum.
111
118
  export const assertSha256 = assertion('a sha256 in lowercase hex', matches(/^[\da-f]{64}$/u))
119
+ // As Podfile.lock writes a podspec's checksum.
120
+ export const assertSha1 = assertion('a sha1 in lowercase hex', isSha1)
package/src/cache.js CHANGED
@@ -10,7 +10,7 @@ import { assertCacheName, assertDirectoryPath, assertion, isRepo } from './args.
10
10
  import { MAX_BYTES, decode } from './http.js'
11
11
  import { pool } from './pool.js'
12
12
 
13
- const DIRS = new Set(['npm/repos', 'npm/tarballs', 'npm/versions', 'cargo/repos', 'cargo/crates', 'composer/repos', 'soldeer/repos', 'soldeer/zips', 'github/trees', 'github/advisories'])
13
+ const DIRS = new Set(['npm/repos', 'npm/tarballs', 'npm/versions', 'cargo/repos', 'cargo/crates', 'composer/repos', 'soldeer/repos', 'soldeer/zips', 'cocoapods/podspecs', 'github/trees', 'github/advisories'])
14
14
  const RECORD_TTL_MS = 30 * 24 * 60 * 60 * 1000 // A link only moves on a transfer or rename, and GitHub redirects those.
15
15
  const RECORDS_AT_ONCE = 8
16
16
  // JSON filed gzipped, as `<name>.json.gz`: a listing carries each
@@ -0,0 +1,51 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readdir } from 'node:fs/promises'
3
+ import { homedir } from 'node:os'
4
+ import { join, resolve } from 'node:path'
5
+
6
+ import { assertPod, assertSha1 } from './args.js'
7
+ import { verifiedDownload } from './download.js'
8
+ import { COCOAPODS_CDN, buildUrl, encodeSegment } from './http.js'
9
+
10
+ // No expiry, but kept by checksum as well: trunk refuses a version it
11
+ // already has, yet rewrites a podspec it has, as `pod trunk deprecate`
12
+ // adds a field to the latest version's.
13
+ const DIR = 'cocoapods/podspecs'
14
+
15
+ // Where a spec repo keeps a version's podspec: under a directory each for
16
+ // the first three hex digits of the md5 of the name, as
17
+ // Pod::Source::Metadata shards trunk, and CocoaPods/Specs with it.
18
+ function specPath(name, version) {
19
+ const shard = createHash('md5').update(name).digest('hex').slice(0, 3)
20
+ return ['Specs', ...shard, name, version, `${name}.podspec.json`]
21
+ }
22
+
23
+ // CocoaPods' spec repos, a directory each under $CP_REPOS_DIR, else
24
+ // $CP_HOME_DIR/repos, else ~/.cocoapods/repos: `trunk` with the podspecs
25
+ // it read off the CDN as they were served, `master` a clone of
26
+ // CocoaPods/Specs, others a team's own. os.homedir() throws where there is
27
+ // no home directory to be found.
28
+ async function localPaths(name, version) {
29
+ const { CP_REPOS_DIR, CP_HOME_DIR } = process.env
30
+ let root
31
+ try {
32
+ root = resolve(CP_REPOS_DIR || join(CP_HOME_DIR || join(homedir(), '.cocoapods'), 'repos'))
33
+ } catch {
34
+ return []
35
+ }
36
+ return (await readdir(root).catch(() => [])).map((repo) => join(root, repo, ...specPath(name, version)))
37
+ }
38
+
39
+ // Trunk has no hash to check a podspec against, so the caller brings one,
40
+ // as Podfile.lock records it. The CDN redirects to jsDelivr, which is
41
+ // followed: whoever answers, the bytes are held to the checksum.
42
+ export async function getPodspec(name, version, checksum) {
43
+ assertPod('getPodspec', name, version)
44
+ assertSha1('getPodspec', 'checksum', checksum)
45
+ const what = `${name}@${version}`
46
+ const locate = () => buildUrl(COCOAPODS_CDN, specPath(name, version).map(encodeSegment))
47
+ return await verifiedDownload({
48
+ method: 'getPodspec', dir: DIR, what, entry: `${what}-${checksum}`, ext: 'podspec.json', algorithm: 'sha1', expected: checksum,
49
+ local: await localPaths(name, version), locate, options: { redirect: 'follow' },
50
+ })
51
+ }
package/src/download.js CHANGED
@@ -6,11 +6,13 @@ import { request } from './http.js'
6
6
  import { gitTreeOfTarball } from './tree.js'
7
7
 
8
8
  // A hash as each registry writes it: npm's integrity, one sha512 in base64
9
- // after `sha512-`; crates.io's and Soldeer's checksum, a sha256 in hex; a
10
- // git tree id for a tarball of that tree.
9
+ // after `sha512-`; crates.io's and Soldeer's checksum, a sha256 in hex;
10
+ // Podfile.lock's checksum of a podspec, a sha1 in hex; a git tree id for a
11
+ // tarball of that tree.
11
12
  const DIGESTS = {
12
13
  sha512: (bytes) => `sha512-${createHash('sha512').update(bytes).digest('base64')}`,
13
14
  sha256: (bytes) => createHash('sha256').update(bytes).digest('hex'),
15
+ sha1: (bytes) => createHash('sha1').update(bytes).digest('hex'),
14
16
  tree: gitTreeOfTarball,
15
17
  }
16
18
 
@@ -20,12 +22,12 @@ const DIGESTS = {
20
22
  // whether set or not (read, never written; a mismatch is passed over),
21
23
  // else the cache set, or a `cache` store in its place (readBytes; a
22
24
  // mismatch throws), else what `locate` answers, checked and then kept as
23
- // writeBytes keeps it. A tree's `list` and `symlinksAbove` are
24
- // gitTreeOfTarball's.
25
- export async function verifiedDownload({ method, dir, what, ext, algorithm, expected, local = [], ours = false, cache, locate, options = {}, list, symlinksAbove }) {
25
+ // writeBytes keeps it, as `entry`, `what` unless given. A tree's `list`
26
+ // and `symlinksAbove` are gitTreeOfTarball's.
27
+ export async function verifiedDownload({ method, dir, what, entry = what, ext, algorithm, expected, local = [], ours = false, cache, locate, options = {}, list, symlinksAbove }) {
26
28
  assert.ok(Object.hasOwn(DIGESTS, algorithm) && typeof expected === 'string' && expected !== '', `${method}: nothing to check ${what} against`)
27
29
  const digest = (bytes) => DIGESTS[algorithm](bytes, { expected, list, symlinksAbove })
28
- const key = `${what}.${ext}`
30
+ const key = `${entry}.${ext}`
29
31
  for (const path of ours ? [...local, ...ourCachePaths(dir, key)] : local) {
30
32
  const bytes = await readRegularFile(path)
31
33
  if (bytes && await digest(bytes) === expected) return bytes
@@ -35,10 +37,10 @@ export async function verifiedDownload({ method, dir, what, ext, algorithm, expe
35
37
  assert.ok(actual === expected, `${method}: integrity mismatch for ${what} from ${from}: expected ${expected}, got ${actual}`)
36
38
  return bytes
37
39
  }
38
- const cached = await readBytes(dir, what, key, cache)
40
+ const cached = await readBytes(dir, entry, key, cache)
39
41
  if (cached) return await check(cached, cache ? 'the store' : 'the cache')
40
42
  const url = await locate()
41
43
  const bytes = await check(await request(url, { ...options, as: 'bytes' }), url)
42
- await writeBytes(dir, what, key, bytes, cache)
44
+ await writeBytes(dir, entry, key, bytes, cache)
43
45
  return bytes
44
46
  }
package/src/git.js CHANGED
@@ -52,7 +52,6 @@ export async function findGitCheckout(dir) {
52
52
  const dirs = start && await isDirectory(start) ? await findGitDirs(start) : null
53
53
  if (dirs === null) return {}
54
54
  const directory = relative(dirs.root, start).split(sep).join('/')
55
- const github = await originGitHub(dirs.commonDir)
56
- const commit = await headCommit(dirs.gitDir, dirs.commonDir)
55
+ const [github, commit] = await Promise.all([originGitHub(dirs.commonDir), headCommit(dirs.gitDir, dirs.commonDir)])
57
56
  return { ...(github && { github }), directory, ...(github && { url: `https://github.com/${github}` }), ...(commit && { commit }) }
58
57
  }
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { Buffer } from 'node:buffer'
3
3
 
4
- import { assertArgs, assertBoolean, assertGhsa, assertLevels, assertLogin, assertNumber, assertPath, assertRef, assertRepo, assertSha, assertTagName, assertTreeId, isRepo, isSha, isSha1, isTagName, optional, sameName, show } from '../args.js'
4
+ import { assertArgs, assertBoolean, assertGhsa, assertLevels, assertLogin, assertNumber, assertPath, assertRef, assertRepo, assertSha, assertTagName, assertTreeId, assertion, isRefName, isRepo, isSha, isSha1, isTagName, optional, sameName, show } from '../args.js'
5
5
  import { verifiedDownload } from '../download.js'
6
6
  import { HttpError, decode, encodeSegment, requestWithHeaders } from '../http.js'
7
7
  import { FORMATS, lockfileOf, projectFormats } from '../lockfiles.js'
@@ -111,6 +111,49 @@ async function listRepoTags(headers, options) {
111
111
  })
112
112
  }
113
113
 
114
+ // GitHub lists each branch with the commit at its head.
115
+ async function listRepoBranches(headers, options) {
116
+ assertArgs('listRepoBranches', options, { repo: assertRepo, maxPages: optional(assertNumber) })
117
+ const { repo, maxPages } = options
118
+ const branches = await paginate('listRepoBranches', headers, (paging) => repoApi(repo, ['branches'], paging), maxPages)
119
+ return branches.map((entry) => {
120
+ assert.ok(isRefName(entry?.name) && isSha(entry.commit?.sha), `listRepoBranches: GitHub listed ${show(entry?.name)} in ${repo}, which is no branch name with a commit`)
121
+ return { branch: entry.name, oid: entry.commit.sha }
122
+ })
123
+ }
124
+
125
+ const assertRevision = assertion('a full commit sha, or a branch or tag name', (value) => isSha(value) || isRefName(value))
126
+ // What GitHub's compare says of the counts it gives with it.
127
+ const COMPARED = {
128
+ ahead: (ahead, behind) => ahead > 0 && behind === 0,
129
+ behind: (ahead, behind) => ahead === 0 && behind > 0,
130
+ identical: (ahead, behind) => ahead === 0 && behind === 0,
131
+ diverged: (ahead, behind) => ahead > 0 && behind > 0,
132
+ }
133
+ const isCount = (value) => Number.isSafeInteger(value) && value >= 0
134
+
135
+ // How `head` stands to `base` in the repo, as GitHub's compare tells it:
136
+ // `ahead` where `base` is in `head`'s history, `behind` the other way round,
137
+ // `identical`, or `diverged`; with the commits each has that the other
138
+ // hasn't, and their merge base. Refused unless the status agrees with those
139
+ // counts, the merge base is `base` where `head` is ahead of it or the same,
140
+ // and the base GitHub compared is `base` where that is a commit. GitHub
141
+ // answers for a commit of any fork of the repo too: whether one is the
142
+ // repo's own is whether a ref of the repo has it in its history.
143
+ async function compareCommits(headers, options) {
144
+ assertArgs('compareCommits', options, { repo: assertRepo, base: assertRevision, head: assertRevision })
145
+ const { repo, base, head } = options
146
+ const body = await call(headers, repoApi(repo, ['compare', `${encodeSegment(base)}...${encodeSegment(head)}`], { per_page: 1 }))
147
+ const { status, ahead_by: aheadBy, behind_by: behindBy } = body ?? {}
148
+ const baseCommit = body?.base_commit?.sha
149
+ const mergeBase = body?.merge_base_commit?.sha
150
+ assert.ok(Object.hasOwn(COMPARED, status) && isCount(aheadBy) && isCount(behindBy) && COMPARED[status](aheadBy, behindBy),
151
+ `compareCommits: GitHub compared ${repo} ${base}...${head} as ${show(status)}, ${show(aheadBy)} ahead and ${show(behindBy)} behind`)
152
+ assert.ok(isSha(baseCommit) && isSha(mergeBase) && (!isSha(base) || baseCommit === base) && (!['ahead', 'identical'].includes(status) || mergeBase === baseCommit),
153
+ `compareCommits: GitHub compared ${repo} ${base}...${head} from ${show(baseCommit)}, merge base ${show(mergeBase)}`)
154
+ return { status, aheadBy, behindBy, mergeBase }
155
+ }
156
+
114
157
  // GitHub's object for the path first: a directory, symlink or submodule is
115
158
  // refused rather than read as a file's text. Up to 1 MB the object holds
116
159
  // the content; past it, `encoding: none`, and the blob it names is read,
@@ -305,5 +348,5 @@ async function listRepoAdvisories(headers, options) {
305
348
  return (await Array.fromAsync(cursorPages('listRepoAdvisories', headers, pageUrl))).flat()
306
349
  }
307
350
 
308
- export const readMethods = { getCurrentUser, listUserRepos, getRepo, getRepoHead, getRepoTag, listRepoTags, getRepoFile, getRepoTarball, getRepoTreeTarball, getRepoTreeId, listRepoDir, findRepoLockfiles, getPullRequest, getCollaboratorPermission, getAdvisory, listRepoAdvisories }
351
+ export const readMethods = { getCurrentUser, listUserRepos, getRepo, getRepoHead, getRepoTag, listRepoTags, listRepoBranches, compareCommits, getRepoFile, getRepoTarball, getRepoTreeTarball, getRepoTreeId, listRepoDir, findRepoLockfiles, getPullRequest, getCollaboratorPermission, getAdvisory, listRepoAdvisories }
309
352
  export const createClient = (options) => bindMethods(clientHeaders('createClient', options, true), readMethods)
package/src/http.js CHANGED
@@ -15,9 +15,10 @@ export const CRATES_INDEX = 'https://index.crates.io'
15
15
  export const CRATES_STATIC = 'https://static.crates.io'
16
16
  export const SOLDEER_API = 'https://api.soldeer.xyz'
17
17
  export const SOLDEER_REVISIONS = 'https://soldeer-revisions.s3.amazonaws.com'
18
+ export const COCOAPODS_CDN = 'https://cdn.cocoapods.org'
18
19
  export const MAX_BYTES = 512 * 1024 * 1024
19
20
 
20
- const ORIGINS = new Set([NPM_REGISTRY, GITHUB_API, OSV_API, CRATES_API, PACKAGIST_REPO, CRATES_INDEX, CRATES_STATIC, SOLDEER_API, SOLDEER_REVISIONS])
21
+ const ORIGINS = new Set([NPM_REGISTRY, GITHUB_API, OSV_API, CRATES_API, PACKAGIST_REPO, CRATES_INDEX, CRATES_STATIC, SOLDEER_API, SOLDEER_REVISIONS, COCOAPODS_CDN])
21
22
  const isSegment = matches(/^(?!\.\.?$)(?:[\w.~@-]|%[\dA-F]{2})+$/u)
22
23
  const isQueryKey = matches(/^[a-z_]+(?:\[\])?$/u)
23
24
  const isQueryValue = (value) => (typeof value === 'string' && value !== '') || (Number.isSafeInteger(value) && value >= 0)
@@ -13,6 +13,7 @@ import { readRegularFile } from '../cache.js'
13
13
  const KEY = 'make-fetch-happen:request-cache:'
14
14
  const digest = (algorithm, data, encoding) => createHash(algorithm).update(data).digest(encoding)
15
15
  const shard = (hex) => [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4)]
16
+ export const contentPath = (cache, sha512) => join(cache, 'content-v2', 'sha512', ...shard(Buffer.from(sha512, 'base64').toString('hex')))
16
17
  const decoder = new TextDecoder('utf-8')
17
18
 
18
19
  async function lastEntry(root, key) {
@@ -39,6 +40,6 @@ export async function readNpmCached(root, url) {
39
40
  const integrity = typeof entry?.integrity === 'string' ? entry.integrity : ''
40
41
  const sha512 = /(?:^|\s)sha512-([\d+/A-Za-z]+={0,2})(?=[\s?]|$)/u.exec(integrity)?.[1]
41
42
  if (sha512 === undefined) return null
42
- const body = await readRegularFile(join(root, 'content-v2', 'sha512', ...shard(Buffer.from(sha512, 'base64').toString('hex'))))
43
+ const body = await readRegularFile(contentPath(root, sha512))
43
44
  return body && digest('sha512', body, 'base64') === sha512 ? { body, metadata: entry.metadata } : null
44
45
  }
@@ -1,4 +1,3 @@
1
- import { Buffer } from 'node:buffer'
2
1
  import { homedir } from 'node:os'
3
2
  import { join, resolve } from 'node:path'
4
3
 
@@ -6,7 +5,7 @@ import { assertArgs, assertPackage, assertPackageName, isPlainObject, isSha, opt
6
5
  import { assertCache } from '../cache.js'
7
6
  import { verifiedDownload } from '../download.js'
8
7
  import { NPM_REGISTRY, decode } from '../http.js'
9
- import { readNpmCached } from './cacache.js'
8
+ import { contentPath, readNpmCached } from './cacache.js'
10
9
  import { getDocument, withNpmToken } from './registry.js'
11
10
  import { checkedDist, getVersion, matchingDist } from './versions.js'
12
11
 
@@ -25,8 +24,7 @@ function npmCacheDirs() {
25
24
  // npm 5+ (cacache) files a tarball by its sha512, npm 4 and before as
26
25
  // <name>/<version>/package.tgz; ~/.audit as <org>:<name>-<version>.tgz.
27
26
  function localPaths(name, version, integrity) {
28
- const hex = Buffer.from(integrity.slice('sha512-'.length), 'base64').toString('hex')
29
- const npm = npmCacheDirs().flatMap((root) => [join(root, '_cacache/content-v2/sha512', hex.slice(0, 2), hex.slice(2, 4), hex.slice(4)), join(root, name, version, 'package.tgz')])
27
+ const npm = npmCacheDirs().flatMap((root) => [contentPath(join(root, '_cacache'), integrity.slice('sha512-'.length)), join(root, name, version, 'package.tgz')])
30
28
  return [...npm, join(homedir(), '.audit/cache/tgz', `${name.replace(/^@/u, '').replace('/', ':')}-${version}.tgz`)]
31
29
  }
32
30
 
@@ -117,7 +115,7 @@ export async function getPublishTimes(name) {
117
115
  const time = record(json.time)
118
116
  const times = new Map()
119
117
  for (const version of Object.keys(record(json.versions))) {
120
- const at = readTime(Object.hasOwn(time, version) ? time[version] : undefined)
118
+ const at = readTime(own(time, version))
121
119
  if (at !== undefined) times.set(version, at)
122
120
  }
123
121
  return times