@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.41
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 +2 -0
- package/client/dependency-paths.js +24 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +112 -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 +522 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-metadata.d.ts +14 -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 +310 -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 +23 -0
- package/common/dependency-reviews.js +81 -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 +56 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/comments.ts +34 -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 +108 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +254 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +2 -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/server-info.ts +35 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +23 -0
- package/common/workspace-groups.js +176 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2AFQKRAM.js +51 -0
- package/out/chunk-2KBF5TZ7.js +2 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-35VE6CDH.js +2 -0
- package/out/chunk-3BZ7JERB.js +1 -0
- package/out/chunk-4F6JSF57.js +3 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-CMQA4DM6.js +1 -0
- package/out/chunk-CVVJAXVY.js +2 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-K3X5EJBQ.js +2 -0
- package/out/chunk-KEX2ATEW.js +2 -0
- package/out/chunk-KVEUXIEU.js +1 -0
- package/out/chunk-KYCNJVB7.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-QZ3A376W.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-RSHMVYNS.js +1 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TY4RZD3Z.js +226 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-U6OPEXIA.js +1 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UXNUAIIC.js +33 -0
- package/out/chunk-UYAXVRVL.js +1 -0
- package/out/chunk-UZAZM2SX.js +2 -0
- package/out/chunk-V3ME6FI5.js +2 -0
- package/out/chunk-V5O7M2YT.js +1 -0
- package/out/chunk-VIHGO77Y.js +1 -0
- package/out/chunk-VQJCXGIY.js +7 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-WZDTZE64.js +1 -0
- package/out/chunk-XL35ZV6R.js +1 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZBBOBQ43.js +1 -0
- package/out/chunk-ZMQQCGF6.js +2 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +37 -0
- package/out/client-sync.js +10 -13
- 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 +6 -2
- package/out/stasis.svg +20 -0
- package/out/terminal.js +365 -39
- package/out/view.css +1 -1
- package/out/view.js +83 -117
- package/package.json +298 -59
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +171 -0
- package/risk/files/runs-code.js +595 -0
- package/risk/files/script.js +114 -0
- package/risk/files/stamps.js +46 -0
- package/risk/index.d.ts +156 -0
- package/risk/index.js +7 -0
- package/risk/npm/assess.js +223 -0
- package/risk/npm/combined.js +65 -0
- package/risk/npm/compare.js +228 -0
- package/risk/npm/dependencies.js +153 -0
- package/risk/npm/diff.js +43 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +34 -0
- package/risk/npm/held.js +60 -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 +30 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +282 -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 +113 -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 +486 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +56 -35
- 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 +116 -18
- 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 +35 -18
- 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 +27 -26
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- 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 +102 -68
- package/{server → server-e2e}/sse-session.ts +28 -8
- package/{server → server-e2e}/static.ts +31 -21
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +245 -0
- package/server-managed/avatar-store.ts +12 -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 +111 -0
- package/server-managed/bundle-build.ts +198 -0
- package/server-managed/bundle-cache.ts +199 -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 +120 -0
- package/server-managed/cache-storage.ts +19 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +158 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1725 -0
- package/server-managed/db-neon.ts +367 -0
- package/server-managed/db-schema.ts +227 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +132 -0
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +391 -0
- package/server-managed/github-errors.ts +68 -0
- package/server-managed/github-issue-links.ts +127 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +235 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/http-response.ts +50 -0
- package/server-managed/http.ts +3472 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +187 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +158 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +75 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-insights.ts +440 -0
- package/server-managed/npm-kept.ts +65 -0
- package/server-managed/npm-loads.ts +167 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +378 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +177 -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 +74 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +220 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -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 +47 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +153 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-risk.ts +420 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/session.ts +92 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +69 -0
- package/server-managed/sql-postgres.ts +31 -0
- package/server-managed/sql.ts +76 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +175 -0
- package/server-managed/storage-encryption.ts +279 -0
- package/server-managed/storage-maintenance.ts +256 -0
- package/server-managed/storage-payload.ts +103 -0
- package/server-managed/storage-stores.ts +85 -0
- package/server-managed/storage.ts +78 -0
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +173 -0
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/team-reports.ts +228 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +86 -0
- package/server-managed/upstream-cache.ts +215 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +166 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -477
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -320
- /package/{LICENSE → LICENSE-MIT} +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}/peer.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { reportRepoGithub } from '@preventive/report'
|
|
3
|
+
import { type RepositoryAliasInput, matchRepositoryAlias, repositoryAliasNeedsFilePrefix } from '../common/managed/repository-alias.ts'
|
|
4
|
+
import type { ManagedDb } from './db.ts'
|
|
5
|
+
import { ManagedMutationError } from './management.ts'
|
|
6
|
+
import { normalizeTeamPath } from './repo-path.ts'
|
|
7
|
+
import type { ManagedSql } from './sql.ts'
|
|
8
|
+
|
|
9
|
+
export const REPOSITORY_ALIAS_SCHEMA = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS managed_repository_alias (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
old_repo TEXT NOT NULL,
|
|
13
|
+
old_path TEXT NOT NULL DEFAULT '',
|
|
14
|
+
repo_id INTEGER NOT NULL REFERENCES managed_selected_repo(repo_id) ON DELETE CASCADE,
|
|
15
|
+
new_path TEXT NOT NULL DEFAULT '',
|
|
16
|
+
UNIQUE(old_repo, old_path)
|
|
17
|
+
) STRICT;
|
|
18
|
+
`
|
|
19
|
+
export interface RepositoryAlias extends RepositoryAliasInput { id: string }
|
|
20
|
+
export interface RepositoryAliasStore {
|
|
21
|
+
listRepositoryAliases(): Promise<RepositoryAlias[]>
|
|
22
|
+
saveRepositoryAlias(sessionId: string, id: string | null, input: unknown): Promise<RepositoryAlias>
|
|
23
|
+
deleteRepositoryAlias(sessionId: string, id: string): Promise<void>
|
|
24
|
+
getRepositoryImportLocation(github: string, directory: string, filePrefix?: string): Promise<{ repoId: number | null; directory: string }>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Keep expensive inventory reads outside database transactions and skip them
|
|
28
|
+
// unless a currently connected alias can use the bundle's existing paths.
|
|
29
|
+
export async function resolveRepositoryImportLocation(db: ManagedDb, github: string, directory: string, loadFilePrefix: () => Promise<string>) {
|
|
30
|
+
const aliases = await db.listRepositoryAliases()
|
|
31
|
+
let filePrefix = ''
|
|
32
|
+
if (repositoryAliasNeedsFilePrefix(github, directory, aliases)) {
|
|
33
|
+
const active = new Set((await db.listSelectedRepos()).map(repo => repo.repoId))
|
|
34
|
+
if (repositoryAliasNeedsFilePrefix(github, directory, aliases.filter(alias => active.has(alias.repoId)))) filePrefix = await loadFilePrefix()
|
|
35
|
+
}
|
|
36
|
+
return db.getRepositoryImportLocation(github, directory, filePrefix)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseAlias(input: unknown): RepositoryAliasInput {
|
|
40
|
+
if (!input || typeof input !== 'object') throw new ManagedMutationError(400, 'bad-alias')
|
|
41
|
+
const { oldRepo, oldPath, repoId, newPath } = input as RepositoryAliasInput
|
|
42
|
+
const github = typeof oldRepo === 'string' ? reportRepoGithub({ repo: { github: oldRepo.trim() } }) : null
|
|
43
|
+
if (!github) throw new ManagedMutationError(400, 'bad-old-repo')
|
|
44
|
+
if (typeof oldPath !== 'string' || typeof newPath !== 'string') throw new ManagedMutationError(400, 'bad-directory')
|
|
45
|
+
const old = normalizeTeamPath(oldPath), target = normalizeTeamPath(newPath)
|
|
46
|
+
if (!old.ok || !target.ok) throw new ManagedMutationError(400, 'bad-directory')
|
|
47
|
+
if (!Number.isSafeInteger(repoId) || repoId <= 0) throw new ManagedMutationError(400, 'bad-repo')
|
|
48
|
+
return { oldRepo: github.toLowerCase(), oldPath: old.path ?? '', repoId, newPath: target.path ?? '' }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function repositoryAliasMethods(db: ManagedSql): RepositoryAliasStore {
|
|
52
|
+
async function authorize(sessionId: string) {
|
|
53
|
+
const user = await db.prepare(`SELECT u.role FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
54
|
+
WHERE s.id = ? AND s.expires_at > ?`).get(sessionId, Date.now()) as { role: string } | undefined
|
|
55
|
+
if (!user) throw new ManagedMutationError(401, 'unauthenticated')
|
|
56
|
+
if (user.role !== 'admin') throw new ManagedMutationError(403, 'forbidden')
|
|
57
|
+
}
|
|
58
|
+
async function listRepositoryAliases() {
|
|
59
|
+
return await db.prepare(`SELECT id, old_repo AS oldRepo, old_path AS oldPath, repo_id AS repoId, new_path AS newPath
|
|
60
|
+
FROM managed_repository_alias ORDER BY old_repo, old_path, id`).all() as RepositoryAlias[]
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
listRepositoryAliases,
|
|
64
|
+
async saveRepositoryAlias(sessionId, id, input) {
|
|
65
|
+
await authorize(sessionId)
|
|
66
|
+
const alias = parseAlias(input)
|
|
67
|
+
if (!await db.prepare('SELECT repo_id FROM managed_selected_repo WHERE repo_id = ? AND active = 1').get(alias.repoId)) throw new ManagedMutationError(400, 'bad-repo')
|
|
68
|
+
if (id && !await db.prepare('SELECT id FROM managed_repository_alias WHERE id = ?').get(id)) throw new ManagedMutationError(404, 'no-alias')
|
|
69
|
+
const duplicate = await db.prepare('SELECT id FROM managed_repository_alias WHERE old_repo = ? AND old_path = ?').get(alias.oldRepo, alias.oldPath) as { id: string } | undefined
|
|
70
|
+
if (duplicate && duplicate.id !== id) throw new ManagedMutationError(409, 'alias-exists')
|
|
71
|
+
const key = id ?? randomUUID()
|
|
72
|
+
await db.prepare(`INSERT INTO managed_repository_alias (id, old_repo, old_path, repo_id, new_path) VALUES (?, ?, ?, ?, ?)
|
|
73
|
+
ON CONFLICT(id) DO UPDATE SET old_repo = excluded.old_repo, old_path = excluded.old_path, repo_id = excluded.repo_id, new_path = excluded.new_path`)
|
|
74
|
+
.run(key, alias.oldRepo, alias.oldPath, alias.repoId, alias.newPath)
|
|
75
|
+
return { id: key, ...alias }
|
|
76
|
+
},
|
|
77
|
+
async deleteRepositoryAlias(sessionId, id) {
|
|
78
|
+
await authorize(sessionId)
|
|
79
|
+
if (!(await db.prepare('DELETE FROM managed_repository_alias WHERE id = ?').run(id)).changes) throw new ManagedMutationError(404, 'no-alias')
|
|
80
|
+
},
|
|
81
|
+
async getRepositoryImportLocation(github, directory, filePrefix = '') {
|
|
82
|
+
const alias = matchRepositoryAlias(github, directory, await listRepositoryAliases(), filePrefix)
|
|
83
|
+
const normalized = normalizeTeamPath(alias?.directory ?? directory)
|
|
84
|
+
if (!normalized.ok) throw new ManagedMutationError(400, 'bad-directory')
|
|
85
|
+
const repo = await (alias
|
|
86
|
+
? db.prepare('SELECT repo_id AS repoId FROM managed_selected_repo WHERE active = 1 AND repo_id = ?').get(alias.repoId)
|
|
87
|
+
: db.prepare('SELECT repo_id AS repoId FROM managed_selected_repo WHERE active = 1 AND LOWER(full_name) = ?').get(github.toLowerCase())) as { repoId: number } | undefined
|
|
88
|
+
return { repoId: repo?.repoId ?? null, directory: normalized.path ?? '' }
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ManagedConfig } from './config.ts'
|
|
2
|
+
import type { SelectedRepo } from './db.ts'
|
|
3
|
+
import { GithubApiError, githubJson, githubRepoReadPermission, githubUserIdentity, repoAccessToken } from './github-app.ts'
|
|
4
|
+
import { MAX_PACKAGE_BYTES, readPackageEntryPoints } from './package-entry-points.ts'
|
|
5
|
+
import { readSolidityEntryPoints } from './solidity-entry-points.ts'
|
|
6
|
+
import { parseGithubCommit } from './bundle-commits.ts'
|
|
7
|
+
|
|
8
|
+
const REFS_PAGE = 100
|
|
9
|
+
|
|
10
|
+
export interface RepositoryEntry { name: string; path: string; type: 'dir' | 'file' | 'symlink' | 'submodule' }
|
|
11
|
+
|
|
12
|
+
// Restricted managers can walk the ancestors of their grants, without seeing
|
|
13
|
+
// sibling names or files outside those grants. null means the entire repo.
|
|
14
|
+
export function scopedDirectory(path: string, scopes: (string | null)[]): RepositoryEntry[] | null {
|
|
15
|
+
if (scopes.some(scope => !scope || path === scope || path.startsWith(scope + '/'))) return null
|
|
16
|
+
const prefix = path ? path + '/' : ''
|
|
17
|
+
const names = new Set(scopes.filter((scope): scope is string => !!scope && scope.startsWith(prefix)).map(scope => scope.slice(prefix.length).split('/')[0]!))
|
|
18
|
+
return [...names].toSorted().map(name => ({ name, path: prefix + name, type: 'dir' }))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RepositoryBrowserUser { githubUserId: number | null; token: string | null }
|
|
22
|
+
type GithubIdentity = Awaited<ReturnType<typeof githubUserIdentity>>
|
|
23
|
+
|
|
24
|
+
function credentialFailure(err: unknown): err is GithubApiError {
|
|
25
|
+
return err instanceof GithubApiError && ([401, 404].includes(err.status) || err.message === 'github-status-403')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Create once per HTTP request. Coalesce concurrent credential lookups without
|
|
29
|
+
// caching repository visibility or permissions, or sharing credentials across users.
|
|
30
|
+
export function createRepositoryBrowser(config: ManagedConfig, user: RepositoryBrowserUser, fetchImpl: typeof fetch = globalThis.fetch) {
|
|
31
|
+
const tokens = new Map<number, Promise<string | null>>()
|
|
32
|
+
let identity: Promise<GithubIdentity> | undefined
|
|
33
|
+
const getIdentity = (fresh = false): Promise<GithubIdentity> => {
|
|
34
|
+
if (!user.token || user.githubUserId == null) return Promise.reject(new GithubApiError(404, 'no-repository'))
|
|
35
|
+
if (!identity || fresh) {
|
|
36
|
+
identity = githubUserIdentity(user.token, fetchImpl).then(value => {
|
|
37
|
+
if (value.id !== user.githubUserId) throw new GithubApiError(404, 'no-repository')
|
|
38
|
+
return value
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
return identity
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
reader(repo: SelectedRepo) {
|
|
45
|
+
const installationToken = () => {
|
|
46
|
+
if (repo.installationId == null) return Promise.resolve(null)
|
|
47
|
+
const cached = tokens.get(repo.installationId)
|
|
48
|
+
const token = cached ?? repoAccessToken(config, repo.installationId, fetchImpl).catch((err: unknown) => {
|
|
49
|
+
if (!credentialFailure(err)) throw err
|
|
50
|
+
// Stale installations cannot prevent verified public access.
|
|
51
|
+
return null
|
|
52
|
+
})
|
|
53
|
+
if (!cached) tokens.set(repo.installationId, token)
|
|
54
|
+
return token
|
|
55
|
+
}
|
|
56
|
+
return repositoryReader(repo, user.token, installationToken, getIdentity, fetchImpl)
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function repositoryReader(repo: SelectedRepo, userToken: string | null, installationToken: () => Promise<string | null>, getIdentity: (fresh?: boolean) => Promise<GithubIdentity>, fetchImpl: typeof fetch) {
|
|
62
|
+
const base = `https://api.github.com/repos/${repo.fullName.split('/').map(encodeURIComponent).join('/')}`
|
|
63
|
+
// Public reads can use the signed-in user's quota without repository grants.
|
|
64
|
+
// Nonpublic reads still require the repository App and the effective-user gate.
|
|
65
|
+
let token = userToken ?? await installationToken()
|
|
66
|
+
const read = (suffix: string) => githubJson(base + suffix, token, fetchImpl)
|
|
67
|
+
const metadata = async () => {
|
|
68
|
+
try { return await read('') }
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (!token || !credentialFailure(err)) throw err
|
|
71
|
+
const fallback = await installationToken()
|
|
72
|
+
if (fallback && fallback !== token) {
|
|
73
|
+
token = fallback
|
|
74
|
+
try { return await read('') } catch (fallbackError) { if (!credentialFailure(fallbackError)) throw fallbackError }
|
|
75
|
+
}
|
|
76
|
+
// Anonymous fallback is only for unavailable credentials, never rate
|
|
77
|
+
// limits, network errors, or GitHub outages.
|
|
78
|
+
token = null
|
|
79
|
+
return read('')
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const checkAccess = async (freshIdentity = false) => {
|
|
83
|
+
// Stored visibility can be stale (or internal with private=false). Require
|
|
84
|
+
// current, explicit public visibility or the caller's effective permission.
|
|
85
|
+
const data = await metadata() as { id?: unknown; full_name?: unknown; private?: unknown; visibility?: unknown; default_branch?: unknown }
|
|
86
|
+
if (data?.id !== repo.repoId || typeof data.full_name !== 'string' || data.full_name.toLowerCase() !== repo.fullName.toLowerCase()) {
|
|
87
|
+
throw new GithubApiError(404, 'no-repository')
|
|
88
|
+
}
|
|
89
|
+
if (data.private === false && data.visibility === 'public') return data
|
|
90
|
+
if (!token) throw new GithubApiError(404, 'no-repository')
|
|
91
|
+
const appToken = await installationToken()
|
|
92
|
+
if (!appToken) throw new GithubApiError(404, 'no-repository')
|
|
93
|
+
if (!(await githubRepoReadPermission(repo.fullName, appToken, await getIdentity(freshIdentity), fetchImpl))) {
|
|
94
|
+
throw new GithubApiError(404, 'no-repository')
|
|
95
|
+
}
|
|
96
|
+
token = appToken
|
|
97
|
+
return data
|
|
98
|
+
}
|
|
99
|
+
const current = await checkAccess()
|
|
100
|
+
const defaultBranch = typeof current.default_branch === 'string' ? current.default_branch : ''
|
|
101
|
+
return {
|
|
102
|
+
recheckAccess: () => checkAccess(true),
|
|
103
|
+
// Server-only credential, selected by the same live visibility/permission
|
|
104
|
+
// gate as directory browsing. Never included in an HTTP response.
|
|
105
|
+
readToken: () => token,
|
|
106
|
+
async refs() {
|
|
107
|
+
// Suggestions are bounded; the input also accepts any branch or tag name.
|
|
108
|
+
const list = async (kind: string) => {
|
|
109
|
+
const data = await read(`/${kind}?per_page=${REFS_PAGE}`)
|
|
110
|
+
if (!Array.isArray(data)) throw new GithubApiError(502, 'github-malformed')
|
|
111
|
+
return data
|
|
112
|
+
}
|
|
113
|
+
const [branches, tags] = await Promise.all([list('branches'), list('tags')])
|
|
114
|
+
const names = (items: { name?: unknown }[]) => items.flatMap(item => typeof item?.name === 'string' ? [item.name] : [])
|
|
115
|
+
// Each tag's commit, for the tag cache. A full page may leave tags
|
|
116
|
+
// out; only a shorter one lists every tag.
|
|
117
|
+
const tagCommits = tags.flatMap(item => typeof item?.name === 'string' && item.name && typeof item.commit?.sha === 'string'
|
|
118
|
+
&& /^[a-f\d]{40}$/u.test(item.commit.sha) ? [{ name: item.name, sha: item.commit.sha }] : [])
|
|
119
|
+
return { defaultBranch, branches: names(branches), tags: names(tags), tagCommits, tagsComplete: tags.length < REFS_PAGE }
|
|
120
|
+
},
|
|
121
|
+
async commit(ref: string) {
|
|
122
|
+
if (/^[a-f\d]{40}$/iu.test(ref)) return ref
|
|
123
|
+
const data = await read(`/commits/${encodeURIComponent(ref || (defaultBranch ? `heads/${defaultBranch}` : 'HEAD'))}`) as { sha?: unknown }
|
|
124
|
+
if (typeof data?.sha !== 'string' || !/^[a-f\d]{40}$/iu.test(data.sha)) throw new GithubApiError(502, 'github-malformed')
|
|
125
|
+
return data.sha
|
|
126
|
+
},
|
|
127
|
+
// The first commit a listing from `sha` returns is that commit, without
|
|
128
|
+
// the files GET /commits/:sha adds.
|
|
129
|
+
async commitDetails(sha: string) {
|
|
130
|
+
return /^[a-f\d]{40}$/u.test(sha) ? parseGithubCommit(sha, await read(`/commits?sha=${sha}&per_page=1`)) : null
|
|
131
|
+
},
|
|
132
|
+
async directory(path: string, commit: string) {
|
|
133
|
+
const data = await read(`/contents/${path.split('/').map(encodeURIComponent).join('/')}?ref=${encodeURIComponent(commit)}`)
|
|
134
|
+
if (!Array.isArray(data)) throw new GithubApiError(400, 'not-a-directory')
|
|
135
|
+
const entries: RepositoryEntry[] = []
|
|
136
|
+
for (const item of data) {
|
|
137
|
+
if (typeof item?.name !== 'string' || item.name.includes('/') || item.name === '.' || item.name === '..') continue
|
|
138
|
+
const expectedPath = path ? `${path}/${item.name}` : item.name
|
|
139
|
+
if (item.path !== expectedPath) continue
|
|
140
|
+
const type = item.submodule_git_url ? 'submodule' : item.type
|
|
141
|
+
if (['dir', 'file', 'symlink', 'submodule'].includes(type)) entries.push({ name: item.name, path: expectedPath, type })
|
|
142
|
+
}
|
|
143
|
+
const manifestPath = path ? `${path}/package.json` : 'package.json'
|
|
144
|
+
const manifest = data.find(item => item?.path === manifestPath && item.name === 'package.json' && item.type === 'file' && !item.submodule_git_url
|
|
145
|
+
&& typeof item.size === 'number' && item.size >= 0 && item.size <= MAX_PACKAGE_BYTES && typeof item.sha === 'string' && /^[a-f\d]{40}$/iu.test(item.sha))
|
|
146
|
+
const packageEntryPoints = manifest ? await readPackageEntryPoints(manifest.sha, path, read) : undefined
|
|
147
|
+
const solidity = await readSolidityEntryPoints(path, data, read)
|
|
148
|
+
return { entries, limited: data.length >= 1000, ...(packageEntryPoints ? { packageEntryPoints } : {}),
|
|
149
|
+
...(solidity.paths.length > 0 || solidity.limited ? { solidityEntryPoints: solidity.paths, soliditySuggestionsLimited: solidity.limited } : {}),
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import type { ManagedConfig } from './config.ts'
|
|
3
|
+
import { GithubApiError, filterInstalledRepos, listInstalledRepos, listUserRepos } from './github-app.ts'
|
|
4
|
+
import type { ConnectedRepo, RepoListing } from './github-app.ts'
|
|
5
|
+
|
|
6
|
+
const CACHE_MS = 60_000
|
|
7
|
+
const MAX_ENTRIES = 100
|
|
8
|
+
interface Entry {
|
|
9
|
+
expires: number
|
|
10
|
+
pending: boolean
|
|
11
|
+
value: Promise<ConnectedRepo[]>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// One directory per HTTP handler. Searches/pages reuse discovery, but selected
|
|
15
|
+
// flags still come from the database on every request. User results are scoped
|
|
16
|
+
// to both the managed identity and its current GitHub credential. This cache is
|
|
17
|
+
// only for discovery; connecting a repo still verifies fresh installation access.
|
|
18
|
+
export class RepositoryDiscovery {
|
|
19
|
+
private readonly cache = new Map<string, Entry>()
|
|
20
|
+
private readonly config: ManagedConfig
|
|
21
|
+
private readonly fetchImpl: typeof fetch
|
|
22
|
+
private readonly now: () => number
|
|
23
|
+
|
|
24
|
+
constructor(config: ManagedConfig, fetchImpl: typeof fetch = (...args) => globalThis.fetch(...args), now: () => number = Date.now) {
|
|
25
|
+
this.config = config
|
|
26
|
+
this.fetchImpl = fetchImpl
|
|
27
|
+
this.now = now
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private read(key: string, load: () => Promise<ConnectedRepo[]>): Promise<ConnectedRepo[]> {
|
|
31
|
+
const cached = this.cache.get(key)
|
|
32
|
+
if (cached && (cached.pending || cached.expires > this.now())) return cached.value
|
|
33
|
+
this.cache.delete(key)
|
|
34
|
+
while (this.cache.size >= MAX_ENTRIES) this.cache.delete(this.cache.keys().next().value!)
|
|
35
|
+
const entry: Entry = { expires: 0, pending: true, value: Promise.resolve([]) }
|
|
36
|
+
entry.value = load().then(repos => {
|
|
37
|
+
entry.expires = this.now() + CACHE_MS
|
|
38
|
+
entry.pending = false
|
|
39
|
+
return repos
|
|
40
|
+
}, (err: unknown) => {
|
|
41
|
+
if (this.cache.get(key) === entry) this.cache.delete(key)
|
|
42
|
+
throw err
|
|
43
|
+
})
|
|
44
|
+
this.cache.set(key, entry)
|
|
45
|
+
return entry.value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async list(scope: 'installed' | 'public', userId: string, token: string | null, showAll = false, refresh = false): Promise<RepoListing> {
|
|
49
|
+
// Invalidate derived user results along with the installation catalogue.
|
|
50
|
+
// An older in-flight read cannot repopulate this map after refresh.
|
|
51
|
+
if (refresh) this.cache.clear()
|
|
52
|
+
const installed = () => this.read('installed', () => listInstalledRepos(this.config, this.fetchImpl))
|
|
53
|
+
if (scope === 'installed' && showAll) return { repositories: await installed(), tokenMissing: false }
|
|
54
|
+
const publicInstalled = async (): Promise<ConnectedRepo[]> => (await installed()).filter(repo => repo.visibility === 'public')
|
|
55
|
+
if (token == null) return { repositories: scope === 'installed' ? await publicInstalled() : [], tokenMissing: true }
|
|
56
|
+
const credential = createHash('sha256').update(token).digest('hex')
|
|
57
|
+
const key = JSON.stringify([scope, userId, credential])
|
|
58
|
+
try {
|
|
59
|
+
if (scope === 'installed') {
|
|
60
|
+
const repositories = await this.read(key, async () => filterInstalledRepos(this.config, await installed(), token, this.fetchImpl))
|
|
61
|
+
return { repositories, tokenMissing: false }
|
|
62
|
+
}
|
|
63
|
+
// Installations only deduplicate the public picker. Their failure must
|
|
64
|
+
// not block otherwise readable public repos or look like a stale login.
|
|
65
|
+
// Cache the two sources separately so failed installation discovery is
|
|
66
|
+
// retried without caching a degraded combined list for another minute.
|
|
67
|
+
const [userRepos, installedRepos] = await Promise.all([
|
|
68
|
+
this.read(key, () => listUserRepos(token)),
|
|
69
|
+
installed().catch((err: unknown) => {
|
|
70
|
+
console.warn('managed: installed repo deduplication failed:', err)
|
|
71
|
+
return []
|
|
72
|
+
}),
|
|
73
|
+
])
|
|
74
|
+
const installedIds = new Set(installedRepos.map(repo => repo.id))
|
|
75
|
+
const repositories = userRepos.filter(repo => repo.visibility === 'public' && !installedIds.has(repo.id))
|
|
76
|
+
return { repositories, tokenMissing: false }
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (!(err instanceof GithubApiError) || err.status !== 401) throw err
|
|
79
|
+
return { repositories: scope === 'installed' ? await publicInstalled() : [], tokenMissing: true }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StoredUser } from './db.ts'
|
|
2
|
+
|
|
3
|
+
const WHITEHAT = [
|
|
4
|
+
291301, // ChALkeR
|
|
5
|
+
259122746, // exo-nikita
|
|
6
|
+
247161625, // ex0sec
|
|
7
|
+
]
|
|
8
|
+
const whitehatIds = new Set(WHITEHAT)
|
|
9
|
+
|
|
10
|
+
// (1) Server permission to add repositories. Keep this independent of GitHub
|
|
11
|
+
// identity and ecosystem exceptions if other install modes broaden it later.
|
|
12
|
+
export function canAddRepositories(user: StoredUser): boolean {
|
|
13
|
+
return user.role === 'admin'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// (3b) Only the ecosystem safeguard for public repositories is bypassed by
|
|
17
|
+
// WHITEHAT. Callers must separately establish (0) server readability, (1) add
|
|
18
|
+
// permission, and (2) server admin or GitHub access to this repository.
|
|
19
|
+
export function passesPublicRepositorySafeguard(githubUserId: number | null, involved: boolean): boolean {
|
|
20
|
+
return involved || (githubUserId != null && whitehatIds.has(githubUserId))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Arbitrary public repositories have no involvement evidence, so (2) requires
|
|
24
|
+
// admin even if (1) expands in the future. This capability is also the UI gate.
|
|
25
|
+
export function canAddAnyPublicRepository(user: StoredUser, githubUserId: number | null): boolean {
|
|
26
|
+
return canAddRepositories(user) && user.role === 'admin' && passesPublicRepositorySafeguard(githubUserId, false)
|
|
27
|
+
}
|