@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,243 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { constants } from 'node:fs'
|
|
3
|
+
import { mkdir, open, rename, rm, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
import { gunzip, gzip } from 'node:zlib'
|
|
7
|
+
import type { CacheStore, Package } from '@preventive/upstream/advisories.js'
|
|
8
|
+
import type { ManagedSql } from './sql.ts'
|
|
9
|
+
|
|
10
|
+
// Records @preventive/upstream would otherwise keep in its disk cache, shared
|
|
11
|
+
// by every instance: each repository's published advisory listing, written by
|
|
12
|
+
// dependency audits that ask repositories, keyed `github/advisories/owner/name`.
|
|
13
|
+
// Upstream stamps, validates and expires each value (90 minutes for a listing);
|
|
14
|
+
// a refresh replaces the row, and there is one row per repository, so rows are
|
|
15
|
+
// not evicted. Only public repositories publish advisories: rows are shared
|
|
16
|
+
// across viewers.
|
|
17
|
+
export const UPSTREAM_CACHE_SCHEMA = `
|
|
18
|
+
CREATE TABLE IF NOT EXISTS managed_upstream_cache (
|
|
19
|
+
cache_key TEXT PRIMARY KEY,
|
|
20
|
+
value TEXT NOT NULL,
|
|
21
|
+
cached_at INTEGER NOT NULL
|
|
22
|
+
) STRICT;
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
export interface UpstreamCacheStore {
|
|
26
|
+
getUpstreamCacheEntry(key: string): Promise<string | null>
|
|
27
|
+
// Key → value for those of `keys` that are kept.
|
|
28
|
+
getUpstreamCacheEntries(keys: string[]): Promise<Map<string, string>>
|
|
29
|
+
// Those of `first` that are kept; where none is, those of `then`, in the
|
|
30
|
+
// same statement (`first` whether they are `first`'s).
|
|
31
|
+
getFirstUpstreamCacheEntries(first: string[], then: string[]): Promise<{ first: boolean; entries: Map<string, string> }>
|
|
32
|
+
setUpstreamCacheEntry(key: string, value: string, cachedAt: number): Promise<void>
|
|
33
|
+
// Each of `entries` (key, value), as setUpstreamCacheEntry keeps one, in one
|
|
34
|
+
// statement: the last of a key given twice.
|
|
35
|
+
setUpstreamCacheEntries(entries: [key: string, value: string][], cachedAt: number): Promise<void>
|
|
36
|
+
// Removes those under `prefix` (no LIKE wildcard in it) kept before `before`;
|
|
37
|
+
// how many.
|
|
38
|
+
deleteUpstreamCacheEntries(prefix: string, before: number): Promise<number>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function upstreamCacheMethods(db: ManagedSql): UpstreamCacheStore {
|
|
42
|
+
const get = db.prepare('SELECT value FROM managed_upstream_cache WHERE cache_key = ?')
|
|
43
|
+
const getMany = db.prepare('SELECT cache_key, value FROM managed_upstream_cache WHERE cache_key IN (SELECT value FROM json_each(?))')
|
|
44
|
+
const getFirst = db.prepare(`WITH found AS (SELECT cache_key, value FROM managed_upstream_cache WHERE cache_key IN (SELECT value FROM json_each(?)))
|
|
45
|
+
SELECT cache_key, value, 1 AS in_first FROM found
|
|
46
|
+
UNION ALL SELECT cache_key, value, 0 AS in_first FROM managed_upstream_cache
|
|
47
|
+
WHERE cache_key IN (SELECT value FROM json_each(?)) AND NOT EXISTS (SELECT 1 FROM found)`)
|
|
48
|
+
// A slower concurrent audit cannot replace a newer listing with an older one.
|
|
49
|
+
const set = db.prepare(`INSERT INTO managed_upstream_cache (cache_key, value, cached_at) VALUES (?, ?, ?)
|
|
50
|
+
ON CONFLICT (cache_key) DO UPDATE SET value = excluded.value, cached_at = excluded.cached_at
|
|
51
|
+
WHERE managed_upstream_cache.cached_at <= excluded.cached_at`)
|
|
52
|
+
const remove = db.prepare('DELETE FROM managed_upstream_cache WHERE cache_key LIKE ? AND cached_at < ?')
|
|
53
|
+
// setUpstreamCacheEntries's, by how many it keeps at once.
|
|
54
|
+
const setMany = new Map<number, ReturnType<ManagedSql['prepare']>>()
|
|
55
|
+
return {
|
|
56
|
+
async getUpstreamCacheEntry(key) {
|
|
57
|
+
const row = await get.get(key) as { value: string } | undefined
|
|
58
|
+
return row?.value ?? null
|
|
59
|
+
},
|
|
60
|
+
async getUpstreamCacheEntries(keys) {
|
|
61
|
+
if (keys.length === 0) return new Map()
|
|
62
|
+
const rows = await getMany.all(JSON.stringify(keys)) as { cache_key: string; value: string }[]
|
|
63
|
+
return new Map(rows.map(row => [row.cache_key, row.value]))
|
|
64
|
+
},
|
|
65
|
+
async getFirstUpstreamCacheEntries(first, then) {
|
|
66
|
+
if (first.length === 0 && then.length === 0) return { first: false, entries: new Map() }
|
|
67
|
+
const rows = await getFirst.all(JSON.stringify(first), JSON.stringify(then)) as { cache_key: string; value: string; in_first: number }[]
|
|
68
|
+
return { first: rows.some(row => Number(row.in_first) === 1), entries: new Map(rows.map(row => [row.cache_key, row.value])) }
|
|
69
|
+
},
|
|
70
|
+
async setUpstreamCacheEntry(key, value, cachedAt) {
|
|
71
|
+
await set.run(key, value, cachedAt)
|
|
72
|
+
},
|
|
73
|
+
async setUpstreamCacheEntries(entries, cachedAt) {
|
|
74
|
+
const rows = [...new Map(entries)]
|
|
75
|
+
if (rows.length === 0) return
|
|
76
|
+
let many = setMany.get(rows.length)
|
|
77
|
+
if (!many) {
|
|
78
|
+
setMany.set(rows.length, many = db.prepare(`INSERT INTO managed_upstream_cache (cache_key, value, cached_at) VALUES ${rows.map(() => '(?, ?, ?)').join(', ')}
|
|
79
|
+
ON CONFLICT (cache_key) DO UPDATE SET value = excluded.value, cached_at = excluded.cached_at
|
|
80
|
+
WHERE managed_upstream_cache.cached_at <= excluded.cached_at`))
|
|
81
|
+
}
|
|
82
|
+
await many.run(...rows.flatMap(([key, value]) => [key, value, cachedAt]))
|
|
83
|
+
},
|
|
84
|
+
async deleteUpstreamCacheEntries(prefix, before) {
|
|
85
|
+
return Number((await remove.run(`${prefix}%`, before)).changes)
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// An audit's `cache`: where the server has an upstream disk cache (off Vercel,
|
|
91
|
+
// set at startup), that keeps everything; else this store keeps listings.
|
|
92
|
+
export function auditCache(cacheDir: string | null | undefined, db: UpstreamCacheStore, signal: AbortSignal, debug = false, repos: Iterable<string> = []): CacheStore | undefined {
|
|
93
|
+
return cacheDir ? undefined : upstreamCache(db, signal, debug, repos)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The repositories whose listings an audit of `packages` asks for, where they
|
|
97
|
+
// are already known: each `github` package, each `soldeer` package's given
|
|
98
|
+
// repository, and with `repoAdvisories`, every other package's. Upstream looks
|
|
99
|
+
// up the rest itself.
|
|
100
|
+
export function auditedRepos(packages: Iterable<Package>, repoAdvisories: boolean): string[] {
|
|
101
|
+
const repos = [...packages].map(pkg => (pkg.ecosystem === 'github' ? pkg.name
|
|
102
|
+
: pkg.ecosystem === 'soldeer' || repoAdvisories ? pkg.github : undefined))
|
|
103
|
+
return [...new Set(repos.filter(repo => repo !== undefined))]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Only listings are kept: upstream's other records (npm version documents,
|
|
107
|
+
// registries' repository lookups) read as misses and are not written.
|
|
108
|
+
const CACHED_TYPE = 'github/advisories'
|
|
109
|
+
|
|
110
|
+
// The cache is an optimization: a database failure is a miss, never a failed
|
|
111
|
+
// audit. Upstream keeps listing after an abandoned audit; once `signal` aborts,
|
|
112
|
+
// the request's database work is over and the store is left alone.
|
|
113
|
+
// Upstream reads one listing at a time per repository, and a request's queries
|
|
114
|
+
// share one connection (Neon), so a recheck of hundreds of packages would wait
|
|
115
|
+
// on as many round trips: the first read asks for every listing of `repos`
|
|
116
|
+
// at once. Any other key, or all of them if that read fails, is read alone.
|
|
117
|
+
export function upstreamCache(db: UpstreamCacheStore, signal: AbortSignal, debug = false, repos: Iterable<string> = []): CacheStore {
|
|
118
|
+
// Upstream asks for a listing by its repository's lowercased name.
|
|
119
|
+
const batched = new Set([...repos].map(repo => `${CACHED_TYPE}/${repo.toLowerCase()}`))
|
|
120
|
+
let batch: Promise<Map<string, string> | null> | undefined
|
|
121
|
+
const readBatch = () => batch ??= db.getUpstreamCacheEntries([...batched]).catch((error: unknown) => {
|
|
122
|
+
if (debug) console.warn('managed: upstream cache read failed:', error)
|
|
123
|
+
return null
|
|
124
|
+
})
|
|
125
|
+
return {
|
|
126
|
+
async read(type, key) {
|
|
127
|
+
if (type !== CACHED_TYPE || signal.aborted) return null
|
|
128
|
+
const cacheKey = `${type}/${key}`
|
|
129
|
+
try {
|
|
130
|
+
const found = batched.has(cacheKey) ? await readBatch() : null
|
|
131
|
+
const value = found ? found.get(cacheKey) ?? null : await db.getUpstreamCacheEntry(cacheKey)
|
|
132
|
+
return value === null ? null : JSON.parse(value) as unknown
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (debug) console.warn('managed: upstream cache read failed:', error)
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
async write(type, key, value) {
|
|
139
|
+
if (type !== CACHED_TYPE || signal.aborted) return
|
|
140
|
+
try { await db.setUpstreamCacheEntry(`${type}/${key}`, JSON.stringify(value), Date.now()) } catch (error) {
|
|
141
|
+
if (debug) console.warn('managed: upstream cache write failed:', error)
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The file upstream keeps `file` in, under `dir` of its disk cache
|
|
148
|
+
// (setCacheDir), as its cachePath names it: `@babel+core@7.29.7.tgz`, a
|
|
149
|
+
// capital written `!` and the letter, and a Windows device name's first
|
|
150
|
+
// letter escaped.
|
|
151
|
+
export function upstreamCachePath(root: string, dir: string, file: string): string {
|
|
152
|
+
const name = encodeURIComponent(file.replaceAll(/[!A-Z]/gu, char => `!${char.toLowerCase()}`))
|
|
153
|
+
.replaceAll('%40', '@').replaceAll('%2F', '+')
|
|
154
|
+
.replace(/^(?=(?:con|prn|aux|nul|com\d|lpt\d)(?:\.|$))./u, char => `%${char.codePointAt(0)!.toString(16).toUpperCase()}`)
|
|
155
|
+
return join(root, dir, name)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A kept file, where it is a regular file of at most `max` bytes, checked on
|
|
159
|
+
// the file as opened (a FIFO or device would block or never end), and read
|
|
160
|
+
// to the size it had then.
|
|
161
|
+
export async function readKeptFile(path: string, max: number): Promise<Uint8Array | null> {
|
|
162
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK).catch(() => null)
|
|
163
|
+
if (!handle) return null
|
|
164
|
+
try {
|
|
165
|
+
const stats = await handle.stat()
|
|
166
|
+
if (!stats.isFile() || stats.size > max) return null
|
|
167
|
+
const bytes = Buffer.allocUnsafe(stats.size)
|
|
168
|
+
for (let at = 0; at < bytes.length;) {
|
|
169
|
+
const { bytesRead } = await handle.read(bytes, at, bytes.length - at, at)
|
|
170
|
+
if (bytesRead === 0) return null
|
|
171
|
+
at += bytesRead
|
|
172
|
+
}
|
|
173
|
+
return bytes
|
|
174
|
+
} catch { return null }
|
|
175
|
+
finally { await handle.close() }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// `bytes` kept at `path`, written whole beside it first and then moved in, so a
|
|
179
|
+
// reader finds the old file or the new one, never part of one; a failure
|
|
180
|
+
// keeps nothing.
|
|
181
|
+
let tmpSeq = 0
|
|
182
|
+
export async function keepFile(path: string, bytes: Uint8Array) {
|
|
183
|
+
const tmp = `${path}.${process.pid}.${++tmpSeq}.tmp`
|
|
184
|
+
try {
|
|
185
|
+
await mkdir(dirname(path), { recursive: true })
|
|
186
|
+
await writeFile(tmp, bytes)
|
|
187
|
+
await rename(tmp, path)
|
|
188
|
+
} catch { await rm(tmp, { force: true }).catch(() => {}) }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const gunzipAsync = promisify(gunzip)
|
|
192
|
+
const gzipAsync = promisify(gzip)
|
|
193
|
+
|
|
194
|
+
// An upstream's JSON object kept with its ETag, gzipped, as keepJson keeps it:
|
|
195
|
+
// `{ etag, json }`, or null where none is kept, or what is can't be read
|
|
196
|
+
// within `limit` bytes.
|
|
197
|
+
export async function readKeptJson(path: string, limit: number): Promise<{ etag: string; json: Record<string, unknown> } | null> {
|
|
198
|
+
const bytes = await readKeptFile(path, limit)
|
|
199
|
+
if (!bytes) return null
|
|
200
|
+
const object = (value: unknown): value is Record<string, unknown> => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
201
|
+
try {
|
|
202
|
+
const kept: unknown = JSON.parse((await gunzipAsync(bytes, { maxOutputLength: limit + 1024 })).toString('utf8'))
|
|
203
|
+
return object(kept) && typeof kept['etag'] === 'string' && object(kept['json']) ? { etag: kept['etag'], json: kept['json'] } : null
|
|
204
|
+
} catch { return null }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// `body`, an upstream's JSON object as it wrote it, kept with its `etag`.
|
|
208
|
+
export async function keepJson(path: string, etag: string, body: Buffer) {
|
|
209
|
+
await keepFile(path, await gzipAsync(Buffer.concat([Buffer.from(`{"etag":${JSON.stringify(etag)},"json":`), body, Buffer.from('}')])))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// A kept listing is read to no more than this, gzipped or not: one runs to
|
|
213
|
+
// some hundreds of KiB.
|
|
214
|
+
const LISTING_BYTES = 64 * 1024 * 1024
|
|
215
|
+
|
|
216
|
+
// A repository's listing as last kept where audits keep them (auditCache),
|
|
217
|
+
// however old, for when GitHub can't be reached to refresh it: `at`, when it
|
|
218
|
+
// was kept, and `cache`, a store answering it to upstream as kept just now,
|
|
219
|
+
// so that upstream takes it rather than ask GitHub, and keeping nothing.
|
|
220
|
+
// Upstream checks it as it checks its own. Null where none is kept, or it
|
|
221
|
+
// can't be read.
|
|
222
|
+
export async function lastListing(cacheDir: string | null | undefined, db: UpstreamCacheStore, repo: string, debug = false): Promise<{ at: number; cache: CacheStore } | null> {
|
|
223
|
+
const key = repo.toLowerCase()
|
|
224
|
+
let kept: { at?: unknown } | null
|
|
225
|
+
try {
|
|
226
|
+
if (cacheDir) {
|
|
227
|
+
const bytes = await readKeptFile(upstreamCachePath(cacheDir, CACHED_TYPE, `${key}.json.gz`), LISTING_BYTES)
|
|
228
|
+
kept = bytes && JSON.parse((await gunzipAsync(bytes, { maxOutputLength: LISTING_BYTES })).toString('utf8'))
|
|
229
|
+
} else {
|
|
230
|
+
const value = await db.getUpstreamCacheEntry(`${CACHED_TYPE}/${key}`)
|
|
231
|
+
kept = value === null ? null : JSON.parse(value)
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (debug) console.warn('managed: kept listing read failed:', error)
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
const at = kept?.at
|
|
238
|
+
if (typeof at !== 'number') return null
|
|
239
|
+
return { at, cache: {
|
|
240
|
+
read: (type, name) => Promise.resolve(type === CACHED_TYPE && name === key ? { ...kept, at: Date.now() } : null),
|
|
241
|
+
write: () => Promise.resolve(),
|
|
242
|
+
} }
|
|
243
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// An administrator's read-only view as another user, for checking what that
|
|
2
|
+
// account can access. The view is a session row for the viewed user (so every
|
|
3
|
+
// access check applies that user's role, teams and permissions unchanged) tied
|
|
4
|
+
// to the admin's own session, carried in its own cookie beside the session
|
|
5
|
+
// cookie. Ending the admin session, or the admin role, ends the view.
|
|
6
|
+
//
|
|
7
|
+
// The router refuses writes for any request that carries the view cookie, and
|
|
8
|
+
// such requests never act with the viewed user's GitHub credentials: GitHub
|
|
9
|
+
// data that needs them is left out instead.
|
|
10
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
11
|
+
import type { ManagedConfig } from './config.ts'
|
|
12
|
+
import type { ManagedDb, ManagedSession } from './db.ts'
|
|
13
|
+
import { hashToken, randomToken } from './crypto.ts'
|
|
14
|
+
import { VIEW_COOKIE, buildCookie, clearCookie, cookieName, parseCookies } from './session.ts'
|
|
15
|
+
|
|
16
|
+
export const VIEW_AS_PATH = '/api/auth/view-as'
|
|
17
|
+
export const VIEW_ONLY_ERROR = 'view-only'
|
|
18
|
+
|
|
19
|
+
function viewCookieName(config: ManagedConfig): string {
|
|
20
|
+
return cookieName(config, VIEW_COOKIE)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The raw view token, or null when the request carries no view.
|
|
24
|
+
export function viewToken(config: ManagedConfig, cookieHeader: string | undefined): string | null {
|
|
25
|
+
const token = parseCookies(cookieHeader).get(viewCookieName(config))
|
|
26
|
+
return token == null || token === '' ? null : token
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function clearViewCookie(config: ManagedConfig): string {
|
|
30
|
+
return clearCookie(viewCookieName(config), config.cookieSecure)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Open a view as `userId` from the admin's `session`, replacing any earlier
|
|
34
|
+
// view of that session. Null when the database refuses it (see createViewSession).
|
|
35
|
+
export async function startViewSession(config: ManagedConfig, db: ManagedDb, session: ManagedSession, userId: string, now: number): Promise<string | null> {
|
|
36
|
+
const token = randomToken()
|
|
37
|
+
const ok = await db.createViewSession({ id: hashToken(token), viewerSessionId: session.id, userId, csrfToken: randomToken() }, now)
|
|
38
|
+
if (!ok) return null
|
|
39
|
+
return buildCookie(viewCookieName(config), token, {
|
|
40
|
+
maxAgeS: Math.max(0, Math.floor((session.expiresAt - now) / 1000)), secure: config.cookieSecure, sameSite: 'Lax',
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function endViewSession(config: ManagedConfig, db: ManagedDb, cookieHeader: string | undefined): Promise<void> {
|
|
45
|
+
const token = viewToken(config, cookieHeader)
|
|
46
|
+
if (token != null) await db.deleteSession(hashToken(token))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const viewing = new AsyncLocalStorage<boolean>()
|
|
50
|
+
|
|
51
|
+
export function runViewing<T>(active: boolean, work: () => T): T {
|
|
52
|
+
return active ? viewing.run(true, work) : work()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Whether the current request is an admin's view as another user.
|
|
56
|
+
export function isViewing(): boolean {
|
|
57
|
+
return viewing.getStore() === true
|
|
58
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { linkRevision } from './link-reports.ts'
|
|
2
|
+
import type { ManagedSql } from './sql.ts'
|
|
3
|
+
import type { BundleBuildConditions, ManagedBundle, TeamReportAccessSnapshot, UserTeam } from './db.ts'
|
|
4
|
+
import { type TeamUserPermissions, parseTeamUserPermissions } from '../common/managed/permissions.ts'
|
|
5
|
+
|
|
6
|
+
export const WORKSPACE_SHARE_SCHEMA = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS managed_workspace_share (
|
|
8
|
+
token_hash TEXT PRIMARY KEY,
|
|
9
|
+
team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
|
|
10
|
+
created_by TEXT NOT NULL REFERENCES managed_user(id) ON DELETE CASCADE,
|
|
11
|
+
created_at INTEGER NOT NULL,
|
|
12
|
+
dependencies INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
security INTEGER NOT NULL DEFAULT 0
|
|
14
|
+
) STRICT;
|
|
15
|
+
CREATE INDEX IF NOT EXISTS managed_workspace_share_team_idx ON managed_workspace_share(team_id);
|
|
16
|
+
`
|
|
17
|
+
|
|
18
|
+
export interface WorkspaceShareSnapshot extends TeamReportAccessSnapshot {
|
|
19
|
+
team: UserTeam
|
|
20
|
+
permissions: TeamUserPermissions
|
|
21
|
+
repositories: { repoId: number; github: string; path: string }[]
|
|
22
|
+
bundles: ManagedBundle[]
|
|
23
|
+
}
|
|
24
|
+
export interface WorkspaceShareInfo {
|
|
25
|
+
id: string
|
|
26
|
+
createdAt: number
|
|
27
|
+
createdBy: string
|
|
28
|
+
permissions: TeamUserPermissions
|
|
29
|
+
}
|
|
30
|
+
export interface ManagedWorkspaceShare extends WorkspaceShareInfo {
|
|
31
|
+
teamId: string
|
|
32
|
+
teamName: string
|
|
33
|
+
teamSlug: string
|
|
34
|
+
}
|
|
35
|
+
export type WorkspaceShareFeedState = { grant: string; catalog: number; annotations: number }
|
|
36
|
+
export interface WorkspaceShareStore {
|
|
37
|
+
createWorkspaceShare(sessionId: string, now: number, teamId: string, tokenHash: string, permissions?: TeamUserPermissions): Promise<boolean>
|
|
38
|
+
listWorkspaceShares(sessionId: string, now: number, teamId: string): Promise<WorkspaceShareInfo[] | null>
|
|
39
|
+
listManagedWorkspaceShares(sessionId: string, now: number): Promise<ManagedWorkspaceShare[] | null>
|
|
40
|
+
updateWorkspaceShare(sessionId: string, now: number, teamId: string, id: string, permissions: TeamUserPermissions): Promise<boolean>
|
|
41
|
+
revokeWorkspaceShares(sessionId: string, now: number, teamId: string, id?: string): Promise<boolean>
|
|
42
|
+
getWorkspaceShare(tokenHash: string): Promise<WorkspaceShareSnapshot | null>
|
|
43
|
+
getWorkspaceShareFeedState(tokenHash: string): Promise<WorkspaceShareFeedState | null>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A share is a capability, never a user session. Its issuer must still manage
|
|
47
|
+
// this team on every read. No union of the issuer's other memberships applies.
|
|
48
|
+
function shareQueries(db: ManagedSql) {
|
|
49
|
+
const manager = db.prepare(`SELECT 1 FROM managed_session s
|
|
50
|
+
JOIN managed_user u ON u.id = s.user_id
|
|
51
|
+
JOIN managed_team t ON t.id = ? AND t.hidden = 0
|
|
52
|
+
WHERE s.id = ? AND s.expires_at > ? AND (u.role = 'admin' OR (u.role = 'manage'
|
|
53
|
+
AND EXISTS (SELECT 1 FROM managed_team_user tu WHERE tu.user_id = u.id AND tu.team_id = t.id)))`)
|
|
54
|
+
const insert = db.prepare(`INSERT INTO managed_workspace_share (token_hash, team_id, created_by, created_at, dependencies, security)
|
|
55
|
+
SELECT ?, ?, user_id, ?, ?, ? FROM managed_session WHERE id = ?`)
|
|
56
|
+
const remove = db.prepare('DELETE FROM managed_workspace_share WHERE team_id = ? AND (CAST(? AS TEXT) IS NULL OR token_hash = ?)')
|
|
57
|
+
const update = db.prepare('UPDATE managed_workspace_share SET dependencies = ?, security = ? WHERE team_id = ? AND token_hash = ?')
|
|
58
|
+
// The management ID is a hash, never a bearer token. Existing URLs remain
|
|
59
|
+
// unchanged when their permissions are edited; plaintext tokens aren't kept.
|
|
60
|
+
const list = db.prepare(`SELECT s.token_hash AS id, s.created_at AS createdAt, u.login AS createdBy, s.dependencies, s.security
|
|
61
|
+
FROM managed_workspace_share s JOIN managed_user u ON u.id = s.created_by WHERE s.team_id = ? ORDER BY s.created_at DESC, s.token_hash`)
|
|
62
|
+
const session = db.prepare(`SELECT u.id, u.role FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
63
|
+
WHERE s.id = ? AND s.expires_at > ? AND u.role IN ('manage', 'admin')`)
|
|
64
|
+
const all = db.prepare(`SELECT s.token_hash AS id, s.created_at AS createdAt, u.login AS createdBy, s.dependencies, s.security,
|
|
65
|
+
t.id AS teamId, t.slug AS teamSlug, t.name AS teamName FROM managed_workspace_share s
|
|
66
|
+
JOIN managed_user u ON u.id = s.created_by JOIN managed_team t ON t.id = s.team_id AND t.hidden = 0
|
|
67
|
+
WHERE ? = 'admin' OR EXISTS (SELECT 1 FROM managed_team_user tu WHERE tu.user_id = ? AND tu.team_id = s.team_id)
|
|
68
|
+
ORDER BY t.name, t.id, s.created_at DESC, s.token_hash`)
|
|
69
|
+
const shareGrant = `FROM managed_workspace_share s
|
|
70
|
+
JOIN managed_user u ON u.id = s.created_by
|
|
71
|
+
JOIN managed_team t ON t.id = s.team_id AND t.hidden = 0
|
|
72
|
+
WHERE s.token_hash = ? AND (u.role = 'admin' OR (u.role = 'manage'
|
|
73
|
+
AND EXISTS (SELECT 1 FROM managed_team_user tu WHERE tu.user_id = u.id AND tu.team_id = s.team_id)))`
|
|
74
|
+
const share = db.prepare(`SELECT t.id, t.slug, t.name, s.dependencies, s.security ${shareGrant}`)
|
|
75
|
+
// Validate the capability and its issuer every poll; global counters are
|
|
76
|
+
// internal hints that let unchanged feeds skip rebuilding the full catalog.
|
|
77
|
+
const feed = db.prepare(`SELECT t.id, s.dependencies, s.security,
|
|
78
|
+
(SELECT catalog FROM managed_change_revision WHERE id = 1) AS catalog,
|
|
79
|
+
(SELECT annotations FROM managed_change_revision WHERE id = 1) AS annotations ${shareGrant}`)
|
|
80
|
+
const repositories = db.prepare(`SELECT tr.repo_id AS repoId, sr.full_name AS github, tr.path
|
|
81
|
+
FROM managed_team_repo tr JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
82
|
+
WHERE tr.team_id = ? ORDER BY tr.repo_id, tr.path`)
|
|
83
|
+
const reports = db.prepare(`SELECT DISTINCT r.id, r.slug, r.filename, r.analyzer, r.byte_size AS byteSize,
|
|
84
|
+
r.sha256, r.repo_directory AS directory, sr.full_name AS github
|
|
85
|
+
FROM managed_team_repo tr JOIN managed_report r ON r.repo_id = tr.repo_id
|
|
86
|
+
JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
87
|
+
WHERE tr.team_id = ? AND r.visible = 1 AND (tr.path = '' OR r.repo_directory = tr.path
|
|
88
|
+
OR substr(r.repo_directory, 1, length(tr.path) + 1) = tr.path || '/') ORDER BY r.id`)
|
|
89
|
+
// A raw bundle is visible only when its declared root is inside the team scope.
|
|
90
|
+
// Root bundles remain hidden from directory-only grants.
|
|
91
|
+
const bundles = db.prepare(`SELECT DISTINCT b.id, b.slug, b.integrity, b.filename, b.kind, b.byte_size AS byteSize,
|
|
92
|
+
b.uploaded_by AS uploadedBy, b.repo_id AS repoId, b.repo_directory AS repoDirectory, b.uploaded_at AS uploadedAt, b.provenance, b.build_conditions AS buildConditions, sr.full_name AS repoFullName
|
|
93
|
+
FROM managed_team_repo tr JOIN managed_bundle b ON b.repo_id = tr.repo_id
|
|
94
|
+
JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
95
|
+
WHERE tr.team_id = ? AND b.visible = 1 AND (tr.path = '' OR b.repo_directory = tr.path
|
|
96
|
+
OR substr(b.repo_directory, 1, length(tr.path) + 1) = tr.path || '/') ORDER BY b.id`)
|
|
97
|
+
return { manager, insert, remove, update, list, session, all, share, feed, repositories, reports, bundles }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function shareInfo<T extends { dependencies: number; security: number }>(row: T) {
|
|
101
|
+
const { dependencies, security, ...rest } = row
|
|
102
|
+
return { ...rest, permissions: { dependencies: dependencies === 1, security: security === 1 } }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function workspaceShareMethods(db: ManagedSql): WorkspaceShareStore {
|
|
106
|
+
const q = shareQueries(db)
|
|
107
|
+
return {
|
|
108
|
+
async createWorkspaceShare(sessionId, now, teamId, tokenHash, permissions) {
|
|
109
|
+
if (!await q.manager.get(teamId, sessionId, now)) return false
|
|
110
|
+
const p = parseTeamUserPermissions(permissions)
|
|
111
|
+
await q.insert.run(tokenHash, teamId, now, +p.dependencies, +p.security, sessionId)
|
|
112
|
+
return true
|
|
113
|
+
},
|
|
114
|
+
async listWorkspaceShares(sessionId, now, teamId) {
|
|
115
|
+
if (!await q.manager.get(teamId, sessionId, now)) return null
|
|
116
|
+
const rows = await q.list.all(teamId) as { id: string; createdAt: number; createdBy: string; dependencies: number; security: number }[]
|
|
117
|
+
return rows.map(shareInfo)
|
|
118
|
+
},
|
|
119
|
+
async listManagedWorkspaceShares(sessionId, now) {
|
|
120
|
+
const viewer = await q.session.get(sessionId, now) as { id: string; role: string } | undefined
|
|
121
|
+
if (!viewer) return null
|
|
122
|
+
const rows = await q.all.all(viewer.role, viewer.id) as (Omit<ManagedWorkspaceShare, 'permissions'> & { dependencies: number; security: number })[]
|
|
123
|
+
return rows.map(shareInfo)
|
|
124
|
+
},
|
|
125
|
+
async updateWorkspaceShare(sessionId, now, teamId, id, permissions) {
|
|
126
|
+
if (!await q.manager.get(teamId, sessionId, now)) return false
|
|
127
|
+
const p = parseTeamUserPermissions(permissions)
|
|
128
|
+
return (await q.update.run(+p.dependencies, +p.security, teamId, id)).changes > 0
|
|
129
|
+
},
|
|
130
|
+
async revokeWorkspaceShares(sessionId, now, teamId, id) {
|
|
131
|
+
if (!await q.manager.get(teamId, sessionId, now)) return false
|
|
132
|
+
const result = await q.remove.run(teamId, id ?? null, id ?? null)
|
|
133
|
+
return id === undefined || result.changes > 0
|
|
134
|
+
},
|
|
135
|
+
async getWorkspaceShareFeedState(tokenHash) {
|
|
136
|
+
const row = await q.feed.get(tokenHash) as { id: string; dependencies: number; security: number; catalog: number; annotations: number } | undefined
|
|
137
|
+
return row ? { grant: JSON.stringify([row.id, row.dependencies, row.security]), catalog: row.catalog, annotations: row.annotations } : null
|
|
138
|
+
},
|
|
139
|
+
async getWorkspaceShare(tokenHash) {
|
|
140
|
+
const grant = await q.share.get(tokenHash) as { id: string; slug: string; name: string; dependencies: number; security: number } | undefined
|
|
141
|
+
if (!grant) return null
|
|
142
|
+
const { dependencies, security, ...team } = grant
|
|
143
|
+
const permissions = { dependencies: dependencies === 1, security: security === 1 }
|
|
144
|
+
const rows = await q.reports.all(team.id) as { id: string; slug: string; filename: string; analyzer: string | null; byteSize: number; sha256: string; directory: string; github: string }[]
|
|
145
|
+
const links = await linkRevision(db)
|
|
146
|
+
const bundleRows = (await q.bundles.all(team.id) as (Omit<ManagedBundle, 'buildConditions'> & { buildConditions: string | null; repoFullName: string })[])
|
|
147
|
+
.map(row => ({ ...row, buildConditions: row.buildConditions == null ? null : JSON.parse(row.buildConditions) as BundleBuildConditions }))
|
|
148
|
+
const repositories = await q.repositories.all(team.id) as WorkspaceShareSnapshot['repositories']
|
|
149
|
+
return {
|
|
150
|
+
user: { id: `share:${tokenHash}`, login: 'public', name: 'Public workspace', avatarUrl: null, role: 'view' },
|
|
151
|
+
teamId: team.id,
|
|
152
|
+
linkRevision: links,
|
|
153
|
+
permissions,
|
|
154
|
+
repositories,
|
|
155
|
+
reports: rows.map(row => ({ id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
156
|
+
repo: { github: row.github, directory: row.directory }, permissions })),
|
|
157
|
+
team: { ...team, permissions, cacheKey: links,
|
|
158
|
+
repos: repositories.map(repo => ({ repoId: repo.repoId, fullName: repo.github, path: repo.path || null }))
|
|
159
|
+
.toSorted((a, b) => a.fullName.localeCompare(b.fullName) || (a.path ?? '').localeCompare(b.path ?? '')),
|
|
160
|
+
reports: rows.map(row => ({ id: row.id, slug: row.slug, filename: row.filename, visible: true, analyzer: row.analyzer, repoFullName: row.github, repoDirectory: row.directory,
|
|
161
|
+
cacheKey: JSON.stringify([row.sha256, row.github, row.directory, row.filename, permissions, links]) })),
|
|
162
|
+
bundles: bundleRows.map(row => ({ id: row.id, slug: row.slug, integrity: row.integrity,
|
|
163
|
+
filename: row.filename, visible: true, kind: row.kind, byteSize: row.byteSize, repoId: row.repoId!, repoDirectory: row.repoDirectory, repoFullName: row.repoFullName, provenance: row.provenance })),
|
|
164
|
+
},
|
|
165
|
+
bundles: bundleRows.map(row => ({ ...row, visible: true })),
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
package/server.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util'
|
|
2
|
+
import type { Server } from 'node:http'
|
|
3
|
+
import { startServer } from './server-common/standalone.ts'
|
|
4
|
+
|
|
5
|
+
// The reviewed dependencies a host keeps beyond the app, registered before
|
|
6
|
+
// init() (server-managed/reviewed-dependencies.ts): an async method telling
|
|
7
|
+
// whether a dependency, as a lockfile locks it, is reviewed there
|
|
8
|
+
// (`lookup`), and optionally one reading the reviews an open pull request's
|
|
9
|
+
// comments make (`parseComments`), and one formatting one's own dependency
|
|
10
|
+
// reviews as a comment to copy (`formatDependencyReviews`). They replace a
|
|
11
|
+
// repository's commit its dependencies are reviewed since.
|
|
12
|
+
export { registerReviewedDependencies } from './server-managed/reviewed-dependencies.ts'
|
|
13
|
+
export type { CommentReview, DependencyReviewEntry, FormatDependencyReviewsInput, ParseCommentsInput, ReviewComment, ReviewedDependencies, ReviewedDependency,
|
|
14
|
+
ReviewedLockfile, ReviewedVersion, ReviewVerdict } from './server-managed/reviewed-dependencies.ts'
|
|
15
|
+
|
|
16
|
+
const modes = ['e2e', 'managed', 'managed-e2e', 'e2e-managed'] as const
|
|
17
|
+
export type ServerMode = typeof modes[number]
|
|
18
|
+
|
|
19
|
+
function parseMode(mode: string): ServerMode {
|
|
20
|
+
for (const candidate of modes) if (mode === candidate) return candidate
|
|
21
|
+
throw new Error(`Unknown mode: ${mode}. Expected ${modes.join(', ')}.`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type InitOptions = {
|
|
25
|
+
mode?: ServerMode
|
|
26
|
+
// False serves only the API: `out/` is never read, nor its UI served, for
|
|
27
|
+
// hosts that serve the UI themselves.
|
|
28
|
+
ui?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Assemble the selected server without binding a port. Hosts can reuse its
|
|
32
|
+
// request/upgrade listeners or call listen() themselves. `init(mode)` is
|
|
33
|
+
// `init({ mode })`.
|
|
34
|
+
export async function init(options: ServerMode | InitOptions = {}): Promise<Server> {
|
|
35
|
+
const { mode = 'e2e', ui = true }: InitOptions = typeof options === 'string' ? { mode: options } : options
|
|
36
|
+
const selected = parseMode(mode)
|
|
37
|
+
if (selected === 'e2e') {
|
|
38
|
+
const { createE2eApp } = await import('./server-e2e/app.ts')
|
|
39
|
+
return (await createE2eApp(undefined, { ui })).httpServer
|
|
40
|
+
}
|
|
41
|
+
if (selected === 'managed') {
|
|
42
|
+
const managed = await import('./server-managed/index.ts')
|
|
43
|
+
return managed.init(undefined, { ui })
|
|
44
|
+
}
|
|
45
|
+
const combined = await import('./server-managed/combined.ts')
|
|
46
|
+
return combined.init(selected === 'managed-e2e' ? 'managed+e2e' : 'e2e+managed', { ui })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function start(): Promise<void> {
|
|
50
|
+
const { values } = parseArgs({ options: {
|
|
51
|
+
mode: { type: 'string', default: 'e2e' },
|
|
52
|
+
help: { type: 'boolean', short: 'h' },
|
|
53
|
+
} })
|
|
54
|
+
const mode = parseMode(values.mode)
|
|
55
|
+
if (values.help) {
|
|
56
|
+
console.log(`Usage: node cli.js --mode <${modes.join('|')}>
|
|
57
|
+
Defaults to e2e. Combined modes share HOST/PORT; the first mode is the client default.
|
|
58
|
+
Managed modes require GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET and OAUTH_CALLBACK_URL.
|
|
59
|
+
Combined storage: DATABASE_URL for a shared Neon DB, or E2E_DATABASE_URL and
|
|
60
|
+
MANAGED_DATABASE_URL for separate Neon DBs. Do not mix shared and per-mode URLs.
|
|
61
|
+
Without URLs: SQLite at DB_PATH for e2e and MANAGED_DB_PATH for managed.
|
|
62
|
+
Combined mode does not support mixing Neon and SQLite.`)
|
|
63
|
+
} else {
|
|
64
|
+
const config = mode === 'managed'
|
|
65
|
+
? (await import('./server-managed/config.ts')).loadManagedConfig()
|
|
66
|
+
: (await import('./server-e2e/config.ts')).loadConfig()
|
|
67
|
+
const server = await init(mode)
|
|
68
|
+
startServer(server, config)
|
|
69
|
+
}
|
|
70
|
+
}
|