@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50
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/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +3 -0
- package/client/dependency-paths.js +36 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +115 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +357 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-compare-versions.js +172 -0
- package/common/bundle-comparison.d.ts +8 -0
- package/common/bundle-comparison.js +62 -0
- package/common/bundle-metadata.d.ts +15 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +333 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +18 -0
- package/common/dependency-reviews.js +64 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +57 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/finding-detail.d.ts +2 -0
- package/common/managed/finding-detail.js +32 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +109 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +35 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +347 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +5 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/security-links.d.ts +5 -0
- package/common/security-links.js +33 -0
- package/common/server-info.ts +46 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +39 -0
- package/common/workspace-groups.js +176 -0
- package/out/activity.js +4 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2WDZPXVL.js +3 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-4XMP7YVH.js +36 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-6S2U2UYG.js +232 -0
- package/out/chunk-75YLHBCM.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FC65TPUB.js +1 -0
- package/out/chunk-FKBXNUW4.js +1 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HTBYHSPG.js +1 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-PWFUOLZK.js +17 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TQJDYHVE.js +2 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-UK7RGJT6.js +3 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WCPJDAYA.js +10 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WYLQWYLA.js +1 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +33 -0
- package/out/client-sync.js +11 -17
- package/out/compare.js +35 -0
- package/out/dialogs.js +55 -0
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +14 -10
- package/out/scan.js +2 -0
- package/out/stasis.svg +20 -0
- package/out/terminal.js +366 -39
- package/out/view.css +1 -1
- package/out/view.js +19 -144
- package/package.json +362 -59
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +177 -0
- package/risk/files/runs-code.js +615 -0
- package/risk/files/script.js +118 -0
- package/risk/files/stamps.js +47 -0
- package/risk/index.d.ts +254 -0
- package/risk/index.js +11 -0
- package/risk/npm/assess.js +285 -0
- package/risk/npm/combined.js +68 -0
- package/risk/npm/compare.js +257 -0
- package/risk/npm/dependencies.js +172 -0
- package/risk/npm/diff.js +79 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +445 -0
- package/risk/npm/held.js +63 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +60 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +388 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +96 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +487 -0
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +50 -42
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +44 -13
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +19 -27
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +3 -3
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +60 -33
- package/{server → server-e2e}/static.ts +31 -21
- package/{server → server-e2e}/sync-handlers.ts +64 -30
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +258 -0
- package/server-managed/agent-merges.ts +109 -0
- package/server-managed/app-agents.ts +21 -0
- package/server-managed/avatar-store.ts +13 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +115 -0
- package/server-managed/bundle-build.ts +200 -0
- package/server-managed/bundle-cache.ts +211 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +119 -0
- package/server-managed/cache-storage.ts +20 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +47 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/commit-coauthors.ts +106 -0
- package/server-managed/commit-lockfiles.ts +73 -0
- package/server-managed/comparison.ts +41 -0
- package/server-managed/config.ts +164 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1923 -0
- package/server-managed/db-neon.ts +458 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +142 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +100 -0
- package/server-managed/dependency-reviews.ts +346 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +415 -0
- package/server-managed/github-errors.ts +70 -0
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +140 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +238 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5329 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +192 -0
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +162 -0
- package/server-managed/locked-packages.ts +129 -0
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +78 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +194 -0
- package/server-managed/npm-insights.ts +449 -0
- package/server-managed/npm-kept.ts +215 -0
- package/server-managed/npm-loads.ts +264 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +454 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +285 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +75 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +434 -0
- package/server-managed/public-workspace.ts +238 -0
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +348 -0
- package/server-managed/pull-dependency-reviews.ts +328 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +39 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +279 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +85 -0
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +355 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +294 -0
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +215 -0
- package/server-managed/repository-dependency-reviews.ts +165 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-history.ts +432 -0
- package/server-managed/repository-issues.ts +195 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-pulls.ts +156 -0
- package/server-managed/repository-pulse.ts +460 -0
- package/server-managed/repository-risk-assess.ts +105 -0
- package/server-managed/repository-risk-audit.ts +79 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +491 -0
- package/server-managed/repository-squashes.ts +168 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +150 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/scan-downloads.ts +87 -0
- package/server-managed/session.ts +132 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +71 -0
- package/server-managed/sql-postgres.ts +34 -0
- package/server-managed/sql.ts +88 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +183 -0
- package/server-managed/storage-encryption.ts +290 -0
- package/server-managed/storage-maintenance.ts +259 -0
- package/server-managed/storage-payload.ts +121 -0
- package/server-managed/storage-stores.ts +91 -0
- package/server-managed/storage.ts +82 -0
- package/server-managed/team-app.ts +134 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +180 -0
- package/server-managed/team-npm-scopes.ts +106 -0
- package/server-managed/team-reports.ts +307 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +105 -0
- package/server-managed/uploads.ts +106 -0
- package/server-managed/upstream-cache.ts +243 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +169 -0
- package/server.ts +70 -0
- package/server/index.ts +0 -480
- package/server/lifecycle.ts +0 -208
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/auth.ts +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/objstore/sign.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/sse-session.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Values by key, the least recently used going first once their weights
|
|
2
|
+
// (`weight`, 1 each unless given) are past `limit`.
|
|
3
|
+
export interface Lru<V> { get(key: string): V | undefined; set(key: string, value: V): void }
|
|
4
|
+
export function lru<V>(limit: number, weight: (value: V) => number = () => 1): Lru<V> {
|
|
5
|
+
const kept = new Map<string, V>()
|
|
6
|
+
let total = 0
|
|
7
|
+
const forget = (key: string) => {
|
|
8
|
+
const value = kept.get(key)
|
|
9
|
+
if (value === undefined) return
|
|
10
|
+
kept.delete(key)
|
|
11
|
+
total -= weight(value)
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
get(key) {
|
|
15
|
+
const value = kept.get(key)
|
|
16
|
+
if (value !== undefined) { kept.delete(key); kept.set(key, value) }
|
|
17
|
+
return value
|
|
18
|
+
},
|
|
19
|
+
set(key, value) {
|
|
20
|
+
forget(key)
|
|
21
|
+
if (weight(value) > limit) return
|
|
22
|
+
kept.set(key, value)
|
|
23
|
+
total += weight(value)
|
|
24
|
+
for (const first of kept.keys()) {
|
|
25
|
+
if (total <= limit) break
|
|
26
|
+
forget(first)
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ManagedSql } from './sql.ts'
|
|
2
|
+
import type { TeamReportAccessSnapshot } from './db.ts'
|
|
3
|
+
import { parseGithubPrUrl } from '../common/github-pr.ts'
|
|
4
|
+
|
|
5
|
+
// One immutable reference per finding, shared across reports and teams. Keep
|
|
6
|
+
// reservations across crashes/uncertain GitHub responses to prevent duplicates.
|
|
7
|
+
// No triage/import/admin patch accepts these fields.
|
|
8
|
+
export const MANAGED_ISSUE_SCHEMA = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS managed_finding_issue (
|
|
10
|
+
finding_id TEXT PRIMARY KEY,
|
|
11
|
+
repo_id INTEGER NOT NULL,
|
|
12
|
+
repository TEXT NOT NULL,
|
|
13
|
+
request_id TEXT NOT NULL,
|
|
14
|
+
issue_url TEXT,
|
|
15
|
+
auto_fix_url TEXT,
|
|
16
|
+
auto_fix_checked_at INTEGER,
|
|
17
|
+
created_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
|
|
18
|
+
created_at INTEGER NOT NULL
|
|
19
|
+
) STRICT;
|
|
20
|
+
`
|
|
21
|
+
export interface ManagedIssue {
|
|
22
|
+
findingId: string
|
|
23
|
+
repoId: number
|
|
24
|
+
repository: string
|
|
25
|
+
requestId: string
|
|
26
|
+
issueUrl: string | null
|
|
27
|
+
autoFixUrl: string | null
|
|
28
|
+
autoFixCheckedAt: number | null
|
|
29
|
+
createdBy: string | null
|
|
30
|
+
createdAt: number
|
|
31
|
+
}
|
|
32
|
+
export interface ManagedIssueStore {
|
|
33
|
+
getManagedIssue(findingId: string): Promise<ManagedIssue | null>
|
|
34
|
+
listManagedIssues(findingIds: readonly string[]): Promise<ManagedIssue[]>
|
|
35
|
+
claimManagedIssue(issue: Omit<ManagedIssue, 'issueUrl' | 'autoFixUrl' | 'autoFixCheckedAt'>): Promise<boolean>
|
|
36
|
+
finishManagedIssue(findingId: string, requestId: string, url: string): Promise<boolean>
|
|
37
|
+
releaseManagedIssue(findingId: string, requestId: string): Promise<void>
|
|
38
|
+
setManagedIssueAutoFix(findingId: string, issueUrl: string, previous: string | null, next: string | null, checkedAt: number): Promise<boolean>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A repeated finding ID never grants access to another team's repository.
|
|
42
|
+
export function visibleManagedIssues(issues: ManagedIssue[], snapshot: TeamReportAccessSnapshot) {
|
|
43
|
+
if (snapshot.user.id.startsWith('share:')) return []
|
|
44
|
+
const repos = new Set(snapshot.repositories.map(repo => repo.repoId))
|
|
45
|
+
const names = new Set(snapshot.repositories.map(repo => repo.github.toLowerCase()))
|
|
46
|
+
return issues.filter(issue => issue.issueUrl && repos.has(issue.repoId)).map(issue => {
|
|
47
|
+
const ref = parseGithubPrUrl(issue.autoFixUrl)
|
|
48
|
+
return { ...issue, autoFixUrl: ref && names.has(ref.repo.toLowerCase()) ? issue.autoFixUrl : null }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
export function managedIssueMethods(db: ManagedSql): ManagedIssueStore {
|
|
52
|
+
const fields = `finding_id AS findingId, repo_id AS repoId, repository, request_id AS requestId,
|
|
53
|
+
issue_url AS issueUrl, auto_fix_url AS autoFixUrl, auto_fix_checked_at AS autoFixCheckedAt, created_by AS createdBy, created_at AS createdAt`
|
|
54
|
+
const get = db.prepare(`SELECT ${fields} FROM managed_finding_issue WHERE finding_id = ?`)
|
|
55
|
+
const list = db.prepare(`SELECT ${fields} FROM managed_finding_issue WHERE finding_id IN (SELECT value FROM json_each(?)) ORDER BY finding_id`)
|
|
56
|
+
// A lookup's derived Fix replaces the one it read, unless one started later
|
|
57
|
+
// changed it since: the check time, which only changes stamp
|
|
58
|
+
// (github-issue-links.ts issueUpdates), orders them.
|
|
59
|
+
const autoFix = db.prepare(`UPDATE managed_finding_issue SET auto_fix_url = ?, auto_fix_checked_at = ?
|
|
60
|
+
WHERE finding_id = ? AND issue_url = ? AND COALESCE(auto_fix_url, '') = COALESCE(?, '')
|
|
61
|
+
AND (auto_fix_checked_at IS NULL OR auto_fix_checked_at <= ?)`)
|
|
62
|
+
const claim = db.prepare(`INSERT INTO managed_finding_issue (finding_id, repo_id, repository, request_id, created_by, created_at)
|
|
63
|
+
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (finding_id) DO NOTHING`)
|
|
64
|
+
const finish = db.prepare(`UPDATE managed_finding_issue SET issue_url = ? WHERE finding_id = ? AND request_id = ? AND issue_url IS NULL`)
|
|
65
|
+
const release = db.prepare(`DELETE FROM managed_finding_issue WHERE finding_id = ? AND request_id = ? AND issue_url IS NULL`)
|
|
66
|
+
return {
|
|
67
|
+
async getManagedIssue(findingId) { return (await get.get(findingId) as ManagedIssue | undefined) ?? null },
|
|
68
|
+
async listManagedIssues(ids) { return await list.all(JSON.stringify(ids)) as ManagedIssue[] },
|
|
69
|
+
async setManagedIssueAutoFix(findingId, issueUrl, previous, next, checkedAt) {
|
|
70
|
+
return Number((await autoFix.run(next, checkedAt, findingId, issueUrl, previous, checkedAt)).changes) > 0
|
|
71
|
+
},
|
|
72
|
+
async claimManagedIssue(issue) {
|
|
73
|
+
return Number((await claim.run(issue.findingId, issue.repoId, issue.repository, issue.requestId, issue.createdBy, issue.createdAt)).changes) > 0
|
|
74
|
+
},
|
|
75
|
+
async finishManagedIssue(findingId, requestId, url) { return Number((await finish.run(url, findingId, requestId)).changes) > 0 },
|
|
76
|
+
async releaseManagedIssue(findingId, requestId) { await release.run(findingId, requestId) },
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { roleAtLeast } from '../common/managed/roles.ts'
|
|
2
|
+
import { type RepoScope, contentAccess } from './content-access.ts'
|
|
3
|
+
import type { AdminBundle, AdminReport, ManagedDb } from './db-methods.ts'
|
|
4
|
+
import { ManagedMutationError } from './management.ts'
|
|
5
|
+
|
|
6
|
+
type CatalogAccess = { repos: { repoId: number; fullName: string }[]; repoScopes: RepoScope[] | null }
|
|
7
|
+
export interface ManagementCatalogStore {
|
|
8
|
+
getReportCatalog(sessionId: string, now: number): Promise<CatalogAccess & { reports: (AdminReport & { canChangeRepo: boolean })[] }>
|
|
9
|
+
getBundleCatalog(sessionId: string, now: number): Promise<CatalogAccess & { bundles: (AdminBundle & { canChangeRepo: boolean })[] }>
|
|
10
|
+
}
|
|
11
|
+
type CatalogDb = Pick<ManagedDb, 'getReportAccessSnapshot' | 'listRepoScopesForUser' | 'listSelectedRepos'
|
|
12
|
+
| 'listReports' | 'listBundles' | 'listReadableBundleIds'>
|
|
13
|
+
|
|
14
|
+
// The outer store method owns one read snapshot. Use raw methods here so the
|
|
15
|
+
// session, grants, rows and action flags agree without per-row transactions.
|
|
16
|
+
export function managementCatalogMethods(db: CatalogDb): ManagementCatalogStore {
|
|
17
|
+
async function access(sessionId: string, now: number) {
|
|
18
|
+
const snapshot = await db.getReportAccessSnapshot(sessionId, now, [])
|
|
19
|
+
if (!snapshot) throw new ManagedMutationError(401, 'unauthenticated')
|
|
20
|
+
if (!roleAtLeast(snapshot.user.role, 'manage')) throw new ManagedMutationError(403, 'forbidden')
|
|
21
|
+
const scope = await contentAccess(db, snapshot.user)
|
|
22
|
+
const repos = (await db.listSelectedRepos()).filter(scope.bundle).map(({ repoId, fullName }) => ({ repoId, fullName }))
|
|
23
|
+
return { userId: snapshot.user.role === 'admin' ? undefined : snapshot.user.id, scope, repos }
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
async getReportCatalog(sessionId, now) {
|
|
27
|
+
const { userId, scope, repos } = await access(sessionId, now)
|
|
28
|
+
const reports = await db.listReports(userId)
|
|
29
|
+
const linked = reports.flatMap(report => report.bundleId ? [report.bundleId] : [])
|
|
30
|
+
const readable = userId === undefined ? new Set(linked) : new Set(await db.listReadableBundleIds(userId, linked))
|
|
31
|
+
return { repos, repoScopes: scope.scopes, reports: reports.map(report => ({ ...report,
|
|
32
|
+
canChangeRepo: report.repoId === null || scope.report(report),
|
|
33
|
+
bundleId: report.bundleId && readable.has(report.bundleId) ? report.bundleId : null,
|
|
34
|
+
bundleFilename: report.bundleId && readable.has(report.bundleId) ? report.bundleFilename : null,
|
|
35
|
+
})) }
|
|
36
|
+
},
|
|
37
|
+
async getBundleCatalog(sessionId, now) {
|
|
38
|
+
const { userId, scope, repos } = await access(sessionId, now)
|
|
39
|
+
const bundles = await db.listBundles(userId)
|
|
40
|
+
return { repos, repoScopes: scope.scopes, bundles: bundles.map(bundle => ({ ...bundle,
|
|
41
|
+
canChangeRepo: bundle.repoId === null || scope.report(bundle),
|
|
42
|
+
})) }
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { AdminReport, BundleReuse, ManagedBundle, ManagedDb, ManagedRepo, RepoDataItem, RepoReportItem, ReportRecord, StoredUser } from './db-methods.ts'
|
|
2
|
+
import { roleAtLeast } from '../common/managed/roles.ts'
|
|
3
|
+
|
|
4
|
+
export class ManagedMutationError extends Error {
|
|
5
|
+
status: number
|
|
6
|
+
constructor(status: number, message: string) { super(message); this.status = status }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ContentMutation = { type: 'delete' } | { type: 'repo'; repoId: number | null; directory: string } | { type: 'visibility'; visible: boolean }
|
|
10
|
+
export type ReportMutation = ContentMutation
|
|
11
|
+
type RemovalRepo = Pick<ManagedRepo, 'repoId' | 'fullName' | 'addedAt'>
|
|
12
|
+
type RemovalAnnotations = { reports: string; ids: string[] }
|
|
13
|
+
export interface ManagementStore {
|
|
14
|
+
mutateReport(sessionId: string, id: string, change: ReportMutation): Promise<{ report: ReportRecord; user: StoredUser }>
|
|
15
|
+
mutateBundle(sessionId: string, id: string, change: ContentMutation): Promise<{ bundle: ManagedBundle; user: StoredUser }>
|
|
16
|
+
// A repeated upload/build of stored bytes (see reuseBundle). Renames need the
|
|
17
|
+
// access other bundle mutations need; a server build labels the row anyway.
|
|
18
|
+
reuseBundleUpload(sessionId: string, id: string, reuse: Omit<BundleReuse, 'rename'>): Promise<boolean>
|
|
19
|
+
removeRepository(sessionId: string, expected: RemovalRepo, annotations: RemovalAnnotations | null): Promise<{
|
|
20
|
+
reports: RepoReportItem[]; bundles: RepoDataItem[]; deletedReports: number; deletedBundles: number; deletedTriage: number
|
|
21
|
+
}>
|
|
22
|
+
}
|
|
23
|
+
type ManagementDb = Pick<ManagedDb, 'sessionWithUser' | 'getReport' | 'getBundle' | 'listReports' | 'listReportsForRepo'
|
|
24
|
+
| 'listBundlesForRepo' | 'listAllRepos' | 'listSelectedRepos' | 'userCanReadReport' | 'userCanReadBundle' | 'userCanReadRepoPath'
|
|
25
|
+
| 'deleteReport' | 'setReportRepo' | 'setReportVisible' | 'deleteBundle' | 'setBundleRepo' | 'setBundleVisible' | 'reuseBundle'
|
|
26
|
+
| 'deleteReportsForRepo' | 'deleteBundlesForRepo' | 'deleteRepo' | 'deleteTriage' | 'recordActivity'>
|
|
27
|
+
|
|
28
|
+
// Only immutable bytes/format and repository membership affect the overlap
|
|
29
|
+
// scan. Ignore publication, user display names, and unrelated bundle links.
|
|
30
|
+
export function reportReferenceSnapshot(reports: readonly Pick<AdminReport, 'id' | 'sha256' | 'filename' | 'repoId'>[]): string {
|
|
31
|
+
return JSON.stringify(reports.map(r => [r.id, r.sha256, r.filename, r.repoId]).toSorted((a, b) => String(a[0]).localeCompare(String(b[0]))))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// These methods are wrapped by scopeManagedMethods: authorization, comparison,
|
|
35
|
+
// metadata changes (and repository-removal audit) share the writer transaction. Blob reads
|
|
36
|
+
// and deletes stay outside it. Use the raw store here, never nested scopes.
|
|
37
|
+
export function managementMethods(db: ManagementDb): ManagementStore {
|
|
38
|
+
async function authorize(sessionId: string, role: 'admin' | 'manage') {
|
|
39
|
+
const s = await db.sessionWithUser(sessionId, Date.now())
|
|
40
|
+
if (!s) throw new ManagedMutationError(401, 'unauthenticated')
|
|
41
|
+
if (!roleAtLeast(s.user.role, role)) throw new ManagedMutationError(403, 'forbidden')
|
|
42
|
+
return s.user
|
|
43
|
+
}
|
|
44
|
+
async function destination(user: { id: string; role: string }, change: ContentMutation) {
|
|
45
|
+
if (change.type !== 'repo' || change.repoId === null) return
|
|
46
|
+
if (!(await db.listSelectedRepos()).some(repo => repo.repoId === change.repoId)) throw new ManagedMutationError(400, 'bad-repo')
|
|
47
|
+
if (user.role !== 'admin' && !await db.userCanReadRepoPath(user.id, change.repoId, change.directory)) throw new ManagedMutationError(403, 'repo-forbidden')
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
async mutateReport(sessionId: string, id: string, change: ReportMutation) {
|
|
51
|
+
const user = await authorize(sessionId, 'manage')
|
|
52
|
+
const report = await db.getReport(id)
|
|
53
|
+
if (!report || (user.role !== 'admin' && report.uploadedBy !== user.id && !await db.userCanReadReport(user.id, id))) throw new ManagedMutationError(404, 'no-report')
|
|
54
|
+
if (user.role !== 'admin' && report.repoId !== null && !await db.userCanReadReport(user.id, id)) throw new ManagedMutationError(403, 'repo-forbidden')
|
|
55
|
+
if (change.type === 'repo' && report.repoEmbedded) throw new ManagedMutationError(409, 'repo-in-report')
|
|
56
|
+
if (change.type !== 'visibility') await destination(user, change)
|
|
57
|
+
if (change.type === 'delete') await db.deleteReport(id)
|
|
58
|
+
else if (change.type === 'visibility') await db.setReportVisible(id, change.visible)
|
|
59
|
+
else await db.setReportRepo(id, change.repoId, change.directory)
|
|
60
|
+
return { report, user }
|
|
61
|
+
},
|
|
62
|
+
async mutateBundle(sessionId: string, id: string, change: ContentMutation) {
|
|
63
|
+
const user = await authorize(sessionId, 'manage')
|
|
64
|
+
const bundle = await db.getBundle(id)
|
|
65
|
+
if (!bundle || (user.role !== 'admin' && !await db.userCanReadBundle(user.id, id))) throw new ManagedMutationError(404, 'no-bundle')
|
|
66
|
+
if (user.role !== 'admin' && bundle.repoId !== null && !await db.userCanReadRepoPath(user.id, bundle.repoId, bundle.repoDirectory)) throw new ManagedMutationError(403, 'repo-forbidden')
|
|
67
|
+
await destination(user, change)
|
|
68
|
+
if (change.type === 'delete') await db.deleteBundle(id)
|
|
69
|
+
else if (change.type === 'visibility') await db.setBundleVisible(id, change.visible)
|
|
70
|
+
else await db.setBundleRepo(id, change.repoId, change.directory)
|
|
71
|
+
return { bundle, user }
|
|
72
|
+
},
|
|
73
|
+
async reuseBundleUpload(sessionId: string, id: string, reuse: Omit<BundleReuse, 'rename'>) {
|
|
74
|
+
const user = await authorize(sessionId, 'manage')
|
|
75
|
+
const bundle = await db.getBundle(id)
|
|
76
|
+
if (!bundle) return false
|
|
77
|
+
const rename = user.role === 'admin' || (await db.userCanReadBundle(user.id, id)
|
|
78
|
+
&& (bundle.repoId === null || await db.userCanReadRepoPath(user.id, bundle.repoId, bundle.repoDirectory)))
|
|
79
|
+
if (!rename && reuse.provenance !== 'build') return false
|
|
80
|
+
return db.reuseBundle(id, { ...reuse, rename })
|
|
81
|
+
},
|
|
82
|
+
async removeRepository(sessionId: string, expected: RemovalRepo, annotations: RemovalAnnotations | null) {
|
|
83
|
+
const user = await authorize(sessionId, 'admin')
|
|
84
|
+
const repo = (await db.listAllRepos()).find(r => r.repoId === expected.repoId)
|
|
85
|
+
if (!repo) throw new ManagedMutationError(404, 'no-repo')
|
|
86
|
+
if (repo.fullName !== expected.fullName || repo.addedAt !== expected.addedAt) throw new ManagedMutationError(409, 'repository-changed')
|
|
87
|
+
if (annotations && reportReferenceSnapshot(await db.listReports()) !== annotations.reports) throw new ManagedMutationError(409, 'repository-changed')
|
|
88
|
+
const reports = await db.listReportsForRepo(repo.repoId)
|
|
89
|
+
const bundles = await db.listBundlesForRepo(repo.repoId)
|
|
90
|
+
const deletedReports = await db.deleteReportsForRepo(repo.repoId)
|
|
91
|
+
const deletedBundles = await db.deleteBundlesForRepo(repo.repoId)
|
|
92
|
+
const deletedTriage = await db.deleteTriage(annotations?.ids ?? [])
|
|
93
|
+
await db.deleteRepo(repo.repoId)
|
|
94
|
+
await db.recordActivity({ kind: 'delete', actor: user.login, actorId: user.id, repo: repo.fullName,
|
|
95
|
+
action: `removed a repository (${deletedReports} reports, ${deletedBundles} bundles, ${deletedTriage} triage entries)` }, Date.now())
|
|
96
|
+
return { reports, bundles, deletedReports, deletedBundles, deletedTriage }
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The lines a package's files change from one side to another (npm-delta.ts),
|
|
2
|
+
// as @preventive/compare's compareTrees counts them (git's --numstat), off
|
|
3
|
+
// the server's event loop: of its text files, renames found as git finds
|
|
4
|
+
// them, a side not there empty; and, apart, those of its changes it stamps
|
|
5
|
+
// `typeOnly`, declaration files changed in types alone.
|
|
6
|
+
import { parentPort, workerData } from 'node:worker_threads'
|
|
7
|
+
import { compareTrees } from '@preventive/compare'
|
|
8
|
+
|
|
9
|
+
const tree = files => files.map(([name, data]) => ({ name, data }))
|
|
10
|
+
const changes = compareTrees(tree(workerData.base), tree(workerData.other), { paths: 'keys', lines: true, renames: true })
|
|
11
|
+
const answer = { additions: 0, deletions: 0, typeAdditions: 0, typeDeletions: 0, typeFiles: 0 }
|
|
12
|
+
for (const change of changes) {
|
|
13
|
+
if (change.binary) continue
|
|
14
|
+
answer.additions += change.insertions ?? 0
|
|
15
|
+
answer.deletions += change.deletions ?? 0
|
|
16
|
+
if (change.typeOnly !== true) continue
|
|
17
|
+
answer.typeAdditions += change.insertions ?? 0
|
|
18
|
+
answer.typeDeletions += change.deletions ?? 0
|
|
19
|
+
answer.typeFiles++
|
|
20
|
+
}
|
|
21
|
+
// Node worker messages have no browser target origin.
|
|
22
|
+
// oxlint-disable-next-line unicorn/require-post-message-target-origin
|
|
23
|
+
parentPort.postMessage(answer)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// The lines a version of a package changes against another, or against none,
|
|
2
|
+
// its whole package's (GET /api/npm/delta), as a list of versions wanting
|
|
3
|
+
// reviews tells them beside each: added and removed (`additions`,
|
|
4
|
+
// `deletions`), as git's --numstat counts them, of their text files, a
|
|
5
|
+
// renamed one diffed with what it was; and, where some of the files changed
|
|
6
|
+
// are declaration files @preventive/compare's compareTrees proves changed in
|
|
7
|
+
// types alone (`typeOnly`, proveTypeOnly: types and comments alone erased,
|
|
8
|
+
// the runtime they leave as it was, an added one's empty or a single bare
|
|
9
|
+
// `export {}`), those of the rest (`typeless`), what a reviewer may pass by
|
|
10
|
+
// leaving those out; and whether a binary file, which they count none of,
|
|
11
|
+
// was added, removed or changed (`binary`), one moved as it was not. Worked
|
|
12
|
+
// out in a worker, off the event loop, as many at once per process as
|
|
13
|
+
// MAX_ACTIVE, both sides' files read together only once their turn has come,
|
|
14
|
+
// those waiting past MAX_WAITING refused, each waiting a minute at most and
|
|
15
|
+
// counting in a minute at most; one asked again meanwhile sharing the
|
|
16
|
+
// first's; and kept by both sides' integrities, in this instance and beyond
|
|
17
|
+
// it (`kept`), what a version holds never changing. Access is the caller's
|
|
18
|
+
// to check.
|
|
19
|
+
import { Worker } from 'node:worker_threads'
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
21
|
+
import type { KeptStore } from './npm-kept.ts'
|
|
22
|
+
import { type Lru, lru } from './lru.ts'
|
|
23
|
+
import { NpmPackageError, type NpmPackageFile, npmFileText } from './npm-packages.ts'
|
|
24
|
+
|
|
25
|
+
export interface NpmDelta { additions: number; deletions: number; typeless: { additions: number; deletions: number } | null; binary: boolean }
|
|
26
|
+
|
|
27
|
+
const MAX_ACTIVE = 2
|
|
28
|
+
const MAX_WAITING = 64
|
|
29
|
+
const DELTA_TIMEOUT_MS = 60_000
|
|
30
|
+
// The text the two sides may hold together, past which none is counted.
|
|
31
|
+
const MAX_TEXT_LENGTH = 64 * 1024 * 1024
|
|
32
|
+
|
|
33
|
+
const deltas: Lru<NpmDelta> = lru(20_000)
|
|
34
|
+
const working = new Map<string, Promise<NpmDelta>>()
|
|
35
|
+
let active = 0
|
|
36
|
+
const waiting: Array<() => void> = []
|
|
37
|
+
|
|
38
|
+
async function turn(deadline: number): Promise<void> {
|
|
39
|
+
if (active < MAX_ACTIVE) { active++; return }
|
|
40
|
+
if (waiting.length >= MAX_WAITING) throw new NpmPackageError(429, 'delta-busy')
|
|
41
|
+
await new Promise<void>((resolve, reject) => {
|
|
42
|
+
const next = () => { clearTimeout(timer); resolve() }
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
waiting.splice(waiting.indexOf(next), 1)
|
|
45
|
+
reject(new NpmPackageError(504, 'delta-timeout'))
|
|
46
|
+
}, deadline - Date.now())
|
|
47
|
+
waiting.push(next)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
function done() {
|
|
51
|
+
const next = waiting.shift()
|
|
52
|
+
if (next) next()
|
|
53
|
+
else active--
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lines = (told: unknown): told is { additions: number; deletions: number } => typeof told === 'object' && told !== null
|
|
57
|
+
&& Number.isSafeInteger((told as { additions?: unknown }).additions) && Number.isSafeInteger((told as { deletions?: unknown }).deletions)
|
|
58
|
+
// A delta as kept, none where it isn't one, or was kept before it told of
|
|
59
|
+
// binary files.
|
|
60
|
+
const deltaOf = (told: unknown): NpmDelta | null => lines(told) && ((told as NpmDelta).typeless === null || lines((told as NpmDelta).typeless))
|
|
61
|
+
&& typeof (told as NpmDelta).binary === 'boolean' ? { additions: told.additions, deletions: told.deletions, typeless: (told as NpmDelta).typeless,
|
|
62
|
+
binary: (told as NpmDelta).binary } : null
|
|
63
|
+
|
|
64
|
+
type Counted = { additions: number; deletions: number; typeAdditions: number; typeDeletions: number; typeFiles: number }
|
|
65
|
+
const isCounted = (told: unknown): told is Counted => typeof told === 'object' && told !== null
|
|
66
|
+
&& ['additions', 'deletions', 'typeAdditions', 'typeDeletions', 'typeFiles'].every(key => Number.isSafeInteger((told as Record<string, unknown>)[key]))
|
|
67
|
+
|
|
68
|
+
// What the worker counts of `base` against `other`, each its text files' paths and texts.
|
|
69
|
+
function counted(base: [string, string][], other: [string, string][], deadline: number): Promise<Counted> {
|
|
70
|
+
let worker: Worker
|
|
71
|
+
try {
|
|
72
|
+
worker = new Worker(new URL('./npm-delta-worker.js', import.meta.url), {
|
|
73
|
+
workerData: { base, other }, execArgv: ['--disable-warning=ExperimentalWarning'], resourceLimits: { maxOldGenerationSizeMb: 1536 },
|
|
74
|
+
})
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn('managed: a package delta could not start:', err)
|
|
77
|
+
throw new NpmPackageError(503, 'unavailable')
|
|
78
|
+
}
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
let answer: Counted | null = null, failure = new NpmPackageError(422, 'delta-failed')
|
|
81
|
+
const timer = setTimeout(() => { failure = new NpmPackageError(504, 'delta-timeout'); void worker.terminate() }, deadline - Date.now())
|
|
82
|
+
worker.once('message', (told: unknown) => {
|
|
83
|
+
if (isCounted(told)) answer = told
|
|
84
|
+
void worker.terminate()
|
|
85
|
+
})
|
|
86
|
+
worker.once('error', err => { console.warn('managed: a package delta failed:', err) })
|
|
87
|
+
worker.once('exit', () => {
|
|
88
|
+
clearTimeout(timer)
|
|
89
|
+
if (answer) resolve(answer)
|
|
90
|
+
else reject(failure)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A side's text files, by path, how long their texts are, and its other,
|
|
96
|
+
// binary, files.
|
|
97
|
+
function texts(files: NpmPackageFile[] | null) {
|
|
98
|
+
let length = 0
|
|
99
|
+
const binaries: NpmPackageFile[] = []
|
|
100
|
+
const read = (files ?? []).flatMap(file => {
|
|
101
|
+
const text = npmFileText(file.bytes)
|
|
102
|
+
if (text === null) { binaries.push(file); return [] }
|
|
103
|
+
length += text.length
|
|
104
|
+
return [[file.path, text] as [string, string]]
|
|
105
|
+
})
|
|
106
|
+
return { read, length, binaries }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const same = (a: Uint8Array, b: Uint8Array) => Buffer.from(a.buffer, a.byteOffset, a.byteLength).equals(b)
|
|
110
|
+
const digest = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('base64')
|
|
111
|
+
|
|
112
|
+
// Whether `news`, a side's binary files, differ from `olds`, the other's:
|
|
113
|
+
// one added, removed or changed, one moved where it is the same as one that
|
|
114
|
+
// was elsewhere not, matched by their digests, each file looked at once.
|
|
115
|
+
function binaryChanged(olds: NpmPackageFile[], news: NpmPackageFile[]): boolean {
|
|
116
|
+
if (olds.length !== news.length) return true
|
|
117
|
+
const left = new Map(olds.map(({ path, bytes }) => [path, bytes]))
|
|
118
|
+
const moved: Uint8Array[] = []
|
|
119
|
+
for (const { path, bytes } of news) {
|
|
120
|
+
const was = left.get(path)
|
|
121
|
+
if (was && same(was, bytes)) left.delete(path)
|
|
122
|
+
else moved.push(bytes)
|
|
123
|
+
}
|
|
124
|
+
// Those not where they were, by their digests, as many of each as there are.
|
|
125
|
+
const unmoved = new Map<string, number>()
|
|
126
|
+
for (const bytes of left.values()) {
|
|
127
|
+
const key = digest(bytes)
|
|
128
|
+
unmoved.set(key, (unmoved.get(key) ?? 0) + 1)
|
|
129
|
+
}
|
|
130
|
+
return moved.some(bytes => {
|
|
131
|
+
const key = digest(bytes)
|
|
132
|
+
const count = unmoved.get(key) ?? 0
|
|
133
|
+
if (count === 0) return true
|
|
134
|
+
unmoved.set(key, count - 1)
|
|
135
|
+
return false
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type Side = { integrity: string; files: () => Promise<NpmPackageFile[]> }
|
|
140
|
+
|
|
141
|
+
// The delta of `other` (its integrity, and how to read its files) against
|
|
142
|
+
// `base`, or none, as held, else shared with one asked before it, else
|
|
143
|
+
// looked for where it is kept, else worked out and kept: shared from before
|
|
144
|
+
// it is looked for, lest those asked together each look, find none, and work
|
|
145
|
+
// it out.
|
|
146
|
+
export function npmDelta(base: Side | null, other: Side, kept: KeptStore<NpmDelta> | null = null): Promise<NpmDelta> {
|
|
147
|
+
const key = `${base?.integrity ?? '(empty)'}\0${other.integrity}`
|
|
148
|
+
const known = deltas.get(key)
|
|
149
|
+
if (known) return Promise.resolve(known)
|
|
150
|
+
const shared = working.get(key)
|
|
151
|
+
if (shared) return shared
|
|
152
|
+
const work = worked(key, base, other, kept).finally(() => working.delete(key))
|
|
153
|
+
working.set(key, work)
|
|
154
|
+
return work
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The deltas of those of `sides` (each the integrities of the version it is
|
|
158
|
+
// against, or none, and of the version) held or kept, by their keys
|
|
159
|
+
// (npmDelta's), in one read of `kept` for those not held: none worked out.
|
|
160
|
+
export async function keptNpmDeltas(sides: [string | null, string][], kept: KeptStore<NpmDelta>): Promise<Map<string, NpmDelta>> {
|
|
161
|
+
const keys = [...new Set(sides.map(([base, other]) => `${base ?? '(empty)'}\0${other}`))]
|
|
162
|
+
const found = new Map(keys.flatMap(key => { const known = deltas.get(key); return known ? [[key, known] as const] : [] }))
|
|
163
|
+
const unheld = keys.filter(key => !found.has(key))
|
|
164
|
+
for (const [key, row] of unheld.length > 0 ? await kept.readMany(unheld) : []) {
|
|
165
|
+
const known = deltaOf(row.value)
|
|
166
|
+
if (!known) continue
|
|
167
|
+
deltas.set(key, known)
|
|
168
|
+
found.set(key, known)
|
|
169
|
+
}
|
|
170
|
+
return found
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// As kept, else its turn awaited, then both sides' files read, counted and kept.
|
|
174
|
+
async function worked(key: string, base: Side | null, other: Side, kept: KeptStore<NpmDelta> | null): Promise<NpmDelta> {
|
|
175
|
+
const known = deltaOf((await kept?.read(key))?.value)
|
|
176
|
+
if (known) { deltas.set(key, known); return known }
|
|
177
|
+
await turn(Date.now() + DELTA_TIMEOUT_MS)
|
|
178
|
+
let binary, told
|
|
179
|
+
try {
|
|
180
|
+
// Its turn held till both sides are read, or have failed.
|
|
181
|
+
const [baseRead, otherRead] = await Promise.allSettled([base ? base.files() : Promise.resolve(null), other.files()])
|
|
182
|
+
if (baseRead.status === 'rejected') throw baseRead.reason
|
|
183
|
+
if (otherRead.status === 'rejected') throw otherRead.reason
|
|
184
|
+
const [olds, news] = [texts(baseRead.value), texts(otherRead.value)]
|
|
185
|
+
if (olds.length + news.length > MAX_TEXT_LENGTH) throw new NpmPackageError(413, 'package-too-large')
|
|
186
|
+
binary = binaryChanged(olds.binaries, news.binaries)
|
|
187
|
+
told = await counted(olds.read, news.read, Date.now() + DELTA_TIMEOUT_MS)
|
|
188
|
+
} finally { done() }
|
|
189
|
+
const delta = { additions: told.additions, deletions: told.deletions,
|
|
190
|
+
typeless: told.typeFiles > 0 ? { additions: told.additions - told.typeAdditions, deletions: told.deletions - told.typeDeletions } : null, binary }
|
|
191
|
+
deltas.set(key, delta)
|
|
192
|
+
await kept?.write(key, delta, Date.now())
|
|
193
|
+
return delta
|
|
194
|
+
}
|