@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,106 @@
|
|
|
1
|
+
// Who made a commit beside its author, as the Commits tab shows them
|
|
2
|
+
// (repository-history.ts): its co-authors, as its `Co-authored-by:` trailers
|
|
3
|
+
// name them, and the agents its message marks.
|
|
4
|
+
import { agentSignals, trailerBlock } from '../common/managed/agent-tasks.ts'
|
|
5
|
+
import { GITHUB_AGENTS, type GithubAgentId, emailAgent, githubAgent } from '../common/managed/github-agents.ts'
|
|
6
|
+
import { APP_AGENTS } from './app-agents.ts'
|
|
7
|
+
import { type NamedAccount, commitAccount } from './github-accounts.ts'
|
|
8
|
+
|
|
9
|
+
export const MAX_COAUTHORS = 3
|
|
10
|
+
// The rules who a commit names beside its author is read by (coauthorFacts),
|
|
11
|
+
// and how they are kept: what was kept read by others (history pages,
|
|
12
|
+
// squashed pull requests) read again by these, once raised. The agents'
|
|
13
|
+
// accounts, addresses and names are none of them (coauthorsOf).
|
|
14
|
+
export const COAUTHORS_READ = 5
|
|
15
|
+
const MAX_NAME = 256
|
|
16
|
+
const textOf = (value: unknown, max: number) => typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null
|
|
17
|
+
|
|
18
|
+
// One who made a commit beside its author: by name and account, as its
|
|
19
|
+
// author is, and the agent it is where known (common/managed/github-agents.ts),
|
|
20
|
+
// by its account, its commits' address or name, or the marks it leaves.
|
|
21
|
+
export interface Coauthor { name: string | null; login: string | null; id: number | null; agent?: string }
|
|
22
|
+
|
|
23
|
+
// `Co-authored-by: <name> <address>` trailers, GitHub matching the key
|
|
24
|
+
// regardless of case; Cursor's agents' links, as their work carries them;
|
|
25
|
+
// the app agents known by the fingerprints they leave.
|
|
26
|
+
const COAUTHOR_TRAILER = /^co-authored-by:[ \t]*([^<\n]*?)[ \t]*<([^<>\s]+)>[ \t]*$/gimu
|
|
27
|
+
const CURSOR_AGENT = /https:\/\/cursor\.com\/(?:agents\?id=|background-agent\?bcId=)bc-/u
|
|
28
|
+
const FINGERPRINTED = APP_AGENTS.filter(agent => agent.fingerprint)
|
|
29
|
+
|
|
30
|
+
// One named as a commit's author or co-author: by name, the address its
|
|
31
|
+
// commit gives (where it is read), and account.
|
|
32
|
+
export type Maker = { name: unknown; email?: unknown; account: NamedAccount }
|
|
33
|
+
|
|
34
|
+
// Those a commit's message names in its `Co-authored-by:` trailers
|
|
35
|
+
// (agent-tasks.ts trailerBlock), each by name and address.
|
|
36
|
+
export const coauthorTrailers = (message: unknown) => typeof message === 'string'
|
|
37
|
+
? [...trailerBlock(message.replaceAll('\r\n', '\n')).matchAll(COAUTHOR_TRAILER)].map(([, name, email]) => ({ name: name!, email: email! })) : []
|
|
38
|
+
|
|
39
|
+
// The agent one who made a commit is: by their account, else the address
|
|
40
|
+
// their commit names them by; else, GitHub naming no account for them, by
|
|
41
|
+
// their name.
|
|
42
|
+
const makerAgent = ({ name, email, account }: Maker): GithubAgentId | null => githubAgent(account.login, account.id) ?? emailAgent(email)
|
|
43
|
+
?? (account.login ? null : githubAgent(null, null, typeof name === 'string' ? name : null))
|
|
44
|
+
|
|
45
|
+
// Who a commit names beside its author, as its history keeps them
|
|
46
|
+
// (repository-history.ts, `coauthorFacts`), its message never kept: its
|
|
47
|
+
// co-authors (`named`), those GitHub's GraphQL API lists past the author
|
|
48
|
+
// (`listed`, the addresses its `Co-authored-by:` trailers give, by the
|
|
49
|
+
// accounts GitHub knows them by), else its trailers (agent-tasks.ts
|
|
50
|
+
// trailerBlock), an account's no-reply address naming it, as many as
|
|
51
|
+
// MAX_NAMED, each by name, address and account; and the agents its message
|
|
52
|
+
// marks (`markedBy`), Claude's and Codex's as the Agents tab reads them
|
|
53
|
+
// (agent-tasks.ts agentSignals: their sessions' and tasks' links, Claude's
|
|
54
|
+
// co-author and "Generated with" lines), the app agents' by their
|
|
55
|
+
// fingerprints, Cursor's by its agents' links. Which of them are agents,
|
|
56
|
+
// and so the same as another, is told as they are answered (coauthorsOf), so
|
|
57
|
+
// that the agents' accounts, addresses and names change none kept.
|
|
58
|
+
export interface Named { name: string | null; email: string | null; login: string | null; id: number | null }
|
|
59
|
+
export const MAX_NAMED = 10
|
|
60
|
+
export function coauthorFacts(message: unknown, listed: Maker[] | null = null): { named: Named[]; markedBy: string[] } {
|
|
61
|
+
const text = typeof message === 'string' ? message.replaceAll('\r\n', '\n') : ''
|
|
62
|
+
const makers = listed ?? coauthorTrailers(text).map(({ name, email }) => ({ name, email, account: commitAccount({ email }) }))
|
|
63
|
+
const signals = agentSignals([text], FINGERPRINTED)
|
|
64
|
+
return {
|
|
65
|
+
named: makers.slice(0, MAX_NAMED).map(({ name, email, account }) => ({ name: textOf(name, MAX_NAME), email: textOf(email, MAX_NAME), login: account.login, id: account.id })),
|
|
66
|
+
markedBy: [...signals.claude || signals.sessions.length > 0 ? ['claude'] : [], ...signals.tasks.length > 0 || signals.codex ? ['codex'] : [],
|
|
67
|
+
...CURSOR_AGENT.test(text) ? ['cursor'] : [], ...signals.agents],
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A commit as its history keeps it: its author by name, address and account,
|
|
72
|
+
// and who it names beside them (coauthorFacts), absent from pages kept before.
|
|
73
|
+
export interface CommitFacts { authorName: string | null; authorEmail?: string | null; authorLogin: string | null; authorId?: number | null; named?: Named[]
|
|
74
|
+
markedBy?: string[] }
|
|
75
|
+
|
|
76
|
+
// Who made a commit, past its author, as its history keeps them
|
|
77
|
+
// (`commit`, coauthorFacts): its co-authors, then the agents its message
|
|
78
|
+
// marks, each the agent it is where known, by the agents the app knows now,
|
|
79
|
+
// and once, the author none of them, as many as MAX_COAUTHORS: two are one by
|
|
80
|
+
// the first of their agent, account, address and name both name, two people
|
|
81
|
+
// of one name with accounts or addresses of their own two.
|
|
82
|
+
export function coauthorsOf({ authorName, authorEmail = null, authorLogin, authorId = null, named = [], markedBy = [] }: CommitFacts): Coauthor[] {
|
|
83
|
+
type Identity = { agent: string | null; login: string | null; email: string | null; name: string | null }
|
|
84
|
+
const identity = (agent: string | null, login: string | null, name: string | null, email: unknown = null): Identity => ({ agent, login: login?.toLowerCase() ?? null,
|
|
85
|
+
email: typeof email === 'string' && email.trim() ? email.trim().toLowerCase() : null, name: name?.toLowerCase() ?? null })
|
|
86
|
+
const same = (a: Identity, b: Identity) => a.agent && b.agent ? a.agent === b.agent : a.login && b.login ? a.login === b.login
|
|
87
|
+
: a.email && b.email ? a.email === b.email : a.name !== null && a.name === b.name
|
|
88
|
+
const author: Maker = { name: authorName, email: authorEmail, account: { login: authorLogin, id: authorId } }
|
|
89
|
+
const seen = [identity(makerAgent(author), authorLogin, textOf(authorName, MAX_NAME), authorEmail)]
|
|
90
|
+
const coauthors: Coauthor[] = []
|
|
91
|
+
const add = (coauthor: Coauthor, email: unknown = null) => {
|
|
92
|
+
const who = identity(coauthor.agent ?? null, coauthor.login, coauthor.name, email)
|
|
93
|
+
if (seen.some(other => same(other, who)) || coauthors.length >= MAX_COAUTHORS || (!coauthor.login && !coauthor.name && !coauthor.agent)) return
|
|
94
|
+
seen.push(who)
|
|
95
|
+
coauthors.push(coauthor)
|
|
96
|
+
}
|
|
97
|
+
for (const { name, email, ...account } of named) {
|
|
98
|
+
const agent = makerAgent({ name, email, account })
|
|
99
|
+
// An agent known by its address where GitHub names someone else's
|
|
100
|
+
// account for it (Codex's), as itself, not that account.
|
|
101
|
+
const { login, id } = agent && account.login && githubAgent(account.login, account.id) !== agent ? { login: null, id: null } : account
|
|
102
|
+
add({ name, login, id, ...agent ? { agent } : {} }, email)
|
|
103
|
+
}
|
|
104
|
+
for (const agent of markedBy) if (Object.hasOwn(GITHUB_AGENTS, agent)) add({ name: GITHUB_AGENTS[agent as GithubAgentId].name, login: null, id: null, agent })
|
|
105
|
+
return coauthors
|
|
106
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The versions a repository's lockfiles of npm's ecosystem lock at a commit,
|
|
2
|
+
// as their review reads them (pull-dependency-reviews.ts): every lockfile
|
|
3
|
+
// there (findRepoLockfiles) a review holds to (`held`, lockfile-scope.ts),
|
|
4
|
+
// read as a pull request's are (pull-dependencies.ts readLockfile), but kept
|
|
5
|
+
// by its blob, in this instance and beyond it (`kept`), as many as
|
|
6
|
+
// MAX_LOCKFILES of those, each's versions (`lockfiles`), and those its reader
|
|
7
|
+
// refuses, of Bun's, or past MAX_LOCKFILES (`unread`), as many read at once
|
|
8
|
+
// as READ_AT_ONCE; and those of some of them (lockedIn), each version once by
|
|
9
|
+
// what its reviews are held to (lockedWith), dev only where it is wherever it
|
|
10
|
+
// is locked. GitHub having none of a lockfile it lists there fails the
|
|
11
|
+
// whole, as it would for any file.
|
|
12
|
+
import { createClient } from '@preventive/upstream/github.js'
|
|
13
|
+
import { upstreamGithub } from './github-errors.ts'
|
|
14
|
+
import { GithubApiError } from './github-app.ts'
|
|
15
|
+
import type { LockedPackage } from './locked-packages.ts'
|
|
16
|
+
import { type Lru, lru } from './lru.ts'
|
|
17
|
+
import { type GithubAsked, type ListedLockfile, type LockfileStore, eachOf, lockedWith, lockfileFormat, readLockfile } from './pull-dependencies.ts'
|
|
18
|
+
|
|
19
|
+
export const MAX_LOCKFILES = 100
|
|
20
|
+
const READ_AT_ONCE = 4
|
|
21
|
+
|
|
22
|
+
export interface CommitLockfiles { lockfiles: { path: string; packages: LockedPackage[] }[]; unread: string[] }
|
|
23
|
+
|
|
24
|
+
// The lockfiles at commit `sha` its review reads, by their paths, each with
|
|
25
|
+
// its format (pull-dependencies.ts lockfileFormat) and its blob's id, kept by
|
|
26
|
+
// the commit, in this instance and beyond it (`kept`); `asked` told where
|
|
27
|
+
// GitHub was.
|
|
28
|
+
const listings: Lru<ListedLockfile[]> = lru(20_000, listed => listed.length + 1)
|
|
29
|
+
export async function lockfilesAt(fullName: string, token: string, sha: string, kept: LockfileStore | null, asked?: GithubAsked) {
|
|
30
|
+
const key = `${fullName}\0${sha}`
|
|
31
|
+
const known = listings.get(key)
|
|
32
|
+
if (known) return known
|
|
33
|
+
const stored = await kept?.listings.read(sha)
|
|
34
|
+
if (stored) { listings.set(key, stored); return stored }
|
|
35
|
+
if (asked) asked.github = true
|
|
36
|
+
const { lockfiles } = await upstreamGithub(() => createClient({ token, userAgent: 'deepview-triage' }).findRepoLockfiles({ repo: fullName, sha }))
|
|
37
|
+
const listed = lockfiles.flatMap(({ path, sha: blob }) => { const format = lockfileFormat(path); return format ? [{ path, format, sha: blob }] : [] })
|
|
38
|
+
listings.set(key, listed)
|
|
39
|
+
await kept?.listings.write(sha, listed)
|
|
40
|
+
return listed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function lockedAt(fullName: string, token: string, sha: string, kept: LockfileStore | null, held: (path: string) => boolean = () => true,
|
|
44
|
+
asked?: GithubAsked): Promise<CommitLockfiles> {
|
|
45
|
+
const listed = (await lockfilesAt(fullName, token, sha, kept, asked)).filter(({ path }) => held(path))
|
|
46
|
+
const unread = listed.slice(MAX_LOCKFILES).map(({ path }) => path)
|
|
47
|
+
const lockfiles: CommitLockfiles['lockfiles'] = []
|
|
48
|
+
const reading = listed.slice(0, MAX_LOCKFILES)
|
|
49
|
+
const reads = await eachOf(reading, READ_AT_ONCE, lockfile => readLockfile(fullName, token, sha, lockfile, kept, asked))
|
|
50
|
+
for (const [at, lockfile] of reading.entries()) {
|
|
51
|
+
const read = reads[at]!
|
|
52
|
+
if ('error' in read && read.error === 'github-not-found') throw new GithubApiError(404, 'github-not-found')
|
|
53
|
+
if ('error' in read) unread.push(lockfile.path)
|
|
54
|
+
else lockfiles.push({ path: lockfile.path, packages: read.packages })
|
|
55
|
+
}
|
|
56
|
+
return { lockfiles, unread: unread.toSorted() }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The versions those of `read`'s lockfiles `held` holds to lock, each once,
|
|
60
|
+
// and their paths.
|
|
61
|
+
export function lockedIn(read: CommitLockfiles, held: (path: string) => boolean = () => true) {
|
|
62
|
+
const packages = new Map<string, LockedPackage>()
|
|
63
|
+
const paths: string[] = []
|
|
64
|
+
for (const lockfile of read.lockfiles.filter(({ path }) => held(path))) {
|
|
65
|
+
paths.push(lockfile.path)
|
|
66
|
+
for (const pkg of lockfile.packages) {
|
|
67
|
+
const kept = packages.get(lockedWith(pkg))
|
|
68
|
+
if (!kept) packages.set(lockedWith(pkg), { ...pkg })
|
|
69
|
+
else if (kept.dev && !pkg.dev) delete kept.dev
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { packages: [...packages.values()], paths }
|
|
73
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Compare's comparison of two managed bundles or two versions of an npm
|
|
2
|
+
// package (GET /api/bundles/:id/comparison?other=, GET /api/npm/comparison):
|
|
3
|
+
// their files paired, at one path or renamed, with the sizes, resolutions,
|
|
4
|
+
// dependency versions and line counts its Overview and Code read beside them,
|
|
5
|
+
// as common/bundle-comparison.js prepareComparison works them out. The viewer
|
|
6
|
+
// of a managed bundle or a package has every comparison worked out here and
|
|
7
|
+
// loads none of the pairing itself. Answered as Brotli-encoded JSON, its
|
|
8
|
+
// `files.basePaths` (a Map) as `[path, as laid out]` pairs where the two
|
|
9
|
+
// sides' roots differ and none else, as the viewer reads none then. Access is
|
|
10
|
+
// the caller's to check.
|
|
11
|
+
import { Buffer } from 'node:buffer'
|
|
12
|
+
import { Readable } from 'node:stream'
|
|
13
|
+
import { prepareComparison } from '../common/bundle-comparison.js'
|
|
14
|
+
import type { BundleDetails } from '../common/bundle-metadata.js'
|
|
15
|
+
import type { OpenedBlob } from './blob-store.ts'
|
|
16
|
+
import { encodeBrotli } from './brotli.ts'
|
|
17
|
+
import { type NpmPackageFile, npmFileRows } from './npm-packages.ts'
|
|
18
|
+
|
|
19
|
+
// A side with no files, as Review compares a package's first version with:
|
|
20
|
+
// one for each comparison, as working one out keeps its line counts on it.
|
|
21
|
+
export const emptySide = (): BundleDetails => ({ integrity: '(empty)', kind: 'sourcemap', size: 0, json: { version: 3, sources: [], sourcesContent: [] } })
|
|
22
|
+
|
|
23
|
+
// A version's files as the viewer reads them for a comparison (ui/view/npm-package.js
|
|
24
|
+
// npmPackageDetails): its texts, and those that are not text by their size and digest.
|
|
25
|
+
export function npmComparedSide(integrity: string, files: NpmPackageFile[]): BundleDetails & { npmBinaries: Map<string, { size: number; digest: string }> } {
|
|
26
|
+
const rows = npmFileRows(files)
|
|
27
|
+
return {
|
|
28
|
+
integrity, kind: 'sourcemap', size: 0,
|
|
29
|
+
json: { version: 3, sources: rows.map(([path]) => path), sourcesContent: rows.map(([, , text]) => text) },
|
|
30
|
+
npmBinaries: new Map(rows.flatMap(([path, size, text, digest]) => (text === null ? [[path, { size, digest: digest! }] as const] : []))),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The comparison of `base` with `other`, within a reason `scope` where given, as sent.
|
|
35
|
+
export async function comparisonBody(base: BundleDetails, other: BundleDetails, scope = ''): Promise<OpenedBlob> {
|
|
36
|
+
const comparison = prepareComparison(base, other, scope)
|
|
37
|
+
const { files } = comparison
|
|
38
|
+
const json = JSON.stringify({ ...comparison, files: { ...files, basePaths: files.root ? [...files.basePaths] : [] } })
|
|
39
|
+
const body = await encodeBrotli(Buffer.from(json))
|
|
40
|
+
return { size: body.byteLength, stream: Readable.from([body]) }
|
|
41
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Managed-server boot config. GitHub user authorization establishes identity;
|
|
2
|
+
// optional App installation credentials enable repository access. A single
|
|
3
|
+
// GitHub App can provide both flows, with repository grants at installation.
|
|
4
|
+
import { env } from 'node:process'
|
|
5
|
+
import { databaseUrls } from '../server-common/database-config.ts'
|
|
6
|
+
import { MAX_UPLOAD_BYTES } from './uploads.ts'
|
|
7
|
+
import { parseStorageKey } from '../server-common/storage-crypto.ts'
|
|
8
|
+
import { defaultCacheDir } from '@preventive/upstream/npm.js'
|
|
9
|
+
import { RESERVED_COOKIES } from './session.ts'
|
|
10
|
+
|
|
11
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost'])
|
|
12
|
+
|
|
13
|
+
export interface ManagedConfig {
|
|
14
|
+
port: number
|
|
15
|
+
host: string
|
|
16
|
+
dbPath: string
|
|
17
|
+
neonUrl?: string | null
|
|
18
|
+
blobToken?: string | null
|
|
19
|
+
storageEncryptionKey?: string | null
|
|
20
|
+
storageEncryptionMigrate?: boolean
|
|
21
|
+
storageEncryptionMigrateMaxMs?: number
|
|
22
|
+
vercelPreview?: boolean
|
|
23
|
+
serverless?: boolean
|
|
24
|
+
// Upstream's disk cache (setCacheDir) for audits and bundle builds: its default
|
|
25
|
+
// location, or null for none, as on Vercel, where the database keeps listings.
|
|
26
|
+
upstreamCacheDir?: string | null
|
|
27
|
+
debug: boolean
|
|
28
|
+
allowShare: boolean
|
|
29
|
+
trustProxyEnv: string | undefined
|
|
30
|
+
// GitHub App user-authorization (identity) credentials — the App's client id
|
|
31
|
+
// + secret, used by the login flow.
|
|
32
|
+
githubClientId: string
|
|
33
|
+
githubClientSecret: string
|
|
34
|
+
// Preapproved GitHub identity promoted on login when it is the only user
|
|
35
|
+
// and its role is No access, including a login after registration.
|
|
36
|
+
initialAdminGithubId: number | null
|
|
37
|
+
// Absolute callback registered with GitHub, e.g.
|
|
38
|
+
// 'https://triage.example.com/api/oauth/github/callback'.
|
|
39
|
+
oauthCallbackUrl: string
|
|
40
|
+
// Whether cookies carry `Secure` (callback is https). Also gates the
|
|
41
|
+
// `__Host-` prefix, so loopback http dev can run with a plain cookie name.
|
|
42
|
+
cookieSecure: boolean
|
|
43
|
+
sessionCookieName: string
|
|
44
|
+
sessionTtlMs: number
|
|
45
|
+
// How long GitHub's answer on a user's access to a repository (its
|
|
46
|
+
// visibility, their permission) is kept for that user and repository
|
|
47
|
+
// (repository-browser.ts), asked again past it; 0 asks on every request
|
|
48
|
+
// (MANAGED_REPOSITORY_ACCESS_CACHE_MS, five minutes by default).
|
|
49
|
+
repositoryAccessCacheMs: number
|
|
50
|
+
// Optional installation credentials for the same GitHub App as login.
|
|
51
|
+
// Repository permissions are approved when connecting repositories.
|
|
52
|
+
githubAppId: string | null
|
|
53
|
+
githubAppPrivateKey: string | null
|
|
54
|
+
githubAppSlug: string | null
|
|
55
|
+
githubNewIssueLabels?: string
|
|
56
|
+
// Max accepted size (bytes) for an uploaded report on the "Manage reports"
|
|
57
|
+
// page, before storage compression (default 25 MiB).
|
|
58
|
+
maxReportBytes: number
|
|
59
|
+
// Max accepted size (bytes) for an uploaded bundle (sourcemap / stasis
|
|
60
|
+
// archive). Bundles run larger than reports, so a higher cap (default 200 MiB).
|
|
61
|
+
maxBundleBytes: number
|
|
62
|
+
// How many triage-trail events to keep per finding (managed_finding_triage_event):
|
|
63
|
+
// 0, the default, keeps everything — the trail is the record. An operator
|
|
64
|
+
// who would rather bound the store sets a positive count; older events of a
|
|
65
|
+
// finding are then trimmed as new ones land.
|
|
66
|
+
triageHistoryLimit: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fail(msg: string): never {
|
|
70
|
+
throw new Error(msg)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requireStr(name: string): string {
|
|
74
|
+
const v = env[name]
|
|
75
|
+
if (v == null || v === '') fail(`Missing required env ${name} (managed mode).`)
|
|
76
|
+
return v
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function intEnv(name: string, def: number, min: number, max: number): number {
|
|
80
|
+
const raw = env[name]
|
|
81
|
+
const n = raw == null ? def : Number(raw)
|
|
82
|
+
if (!Number.isSafeInteger(n) || n < min || n > max) {
|
|
83
|
+
fail(`Invalid ${name}: ${raw} — must be an integer in [${min}, ${max}].`)
|
|
84
|
+
}
|
|
85
|
+
return n
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function optionalGithubId(name: string): number | null {
|
|
89
|
+
const raw = env[name]
|
|
90
|
+
if (raw == null || raw === '') return null
|
|
91
|
+
const id = Number(raw)
|
|
92
|
+
if (!/^[1-9]\d*$/u.test(raw) || !Number.isSafeInteger(id)) {
|
|
93
|
+
fail(`Invalid ${name}: must be a single positive numeric GitHub user ID.`)
|
|
94
|
+
}
|
|
95
|
+
return id
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function urlOrFail(name: string, raw: string): URL {
|
|
99
|
+
try { return new URL(raw) } catch { fail(`${name} is not a valid URL: ${raw}`) }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function loadManagedConfig({ combined = false } = {}): ManagedConfig {
|
|
103
|
+
const serverless = env['VERCEL'] === '1'
|
|
104
|
+
const neonUrl = databaseUrls({ combined }).managed
|
|
105
|
+
const blobToken = env['BLOB_READ_WRITE_TOKEN'] || null
|
|
106
|
+
const storageEncryptionKey = env['MANAGED_STORAGE_ENCRYPTION_KEY'] || null
|
|
107
|
+
parseStorageKey(storageEncryptionKey)
|
|
108
|
+
const storageEncryptionMigrate = env['MANAGED_STORAGE_ENCRYPTION_MIGRATE'] === '1'
|
|
109
|
+
if (storageEncryptionMigrate && !storageEncryptionKey) fail('MANAGED_STORAGE_ENCRYPTION_MIGRATE requires MANAGED_STORAGE_ENCRYPTION_KEY.')
|
|
110
|
+
if (serverless && !neonUrl) fail('Vercel managed mode requires DATABASE_URL or MANAGED_DATABASE_URL.')
|
|
111
|
+
if (neonUrl && !blobToken) fail('Managed Neon mode requires BLOB_READ_WRITE_TOKEN.')
|
|
112
|
+
const host = env['HOST'] ?? '127.0.0.1'
|
|
113
|
+
const oauthCallbackUrl = requireStr('OAUTH_CALLBACK_URL')
|
|
114
|
+
const callback = urlOrFail('OAUTH_CALLBACK_URL', oauthCallbackUrl)
|
|
115
|
+
const cookieSecure = callback.protocol === 'https:'
|
|
116
|
+
// A `__Host-` cookie mandates `Secure`, so any non-loopback bind must be
|
|
117
|
+
// HTTPS — fail fast (mirrors server-e2e's boot check). Loopback dev over
|
|
118
|
+
// http is allowed with a non-prefixed cookie name.
|
|
119
|
+
if ((serverless || !LOOPBACK_HOSTS.has(host)) && !cookieSecure) {
|
|
120
|
+
fail(`Vercel or non-loopback HOST=${host} requires an https OAUTH_CALLBACK_URL — the session cookie needs Secure.`)
|
|
121
|
+
}
|
|
122
|
+
const sessionCookieName = env['SESSION_COOKIE_NAME'] ?? '__Host-dvsid'
|
|
123
|
+
if (sessionCookieName.startsWith('__Host-') && !cookieSecure) {
|
|
124
|
+
fail(`SESSION_COOKIE_NAME=${sessionCookieName} uses the __Host- prefix but the callback is not https. Use a non-prefixed name for loopback http dev.`)
|
|
125
|
+
}
|
|
126
|
+
if (RESERVED_COOKIES.includes(sessionCookieName.replace(/^__Host-/u, ''))) {
|
|
127
|
+
fail(`SESSION_COOKIE_NAME=${sessionCookieName} is reserved for another managed cookie. Choose a different name.`)
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
neonUrl, blobToken, serverless, storageEncryptionKey, storageEncryptionMigrate,
|
|
131
|
+
storageEncryptionMigrateMaxMs: intEnv('MANAGED_STORAGE_ENCRYPTION_MIGRATE_MAX_MS', 150_000, 1, serverless ? 240_000 : 3_600_000),
|
|
132
|
+
vercelPreview: env['VERCEL_ENV'] === 'preview',
|
|
133
|
+
upstreamCacheDir: serverless ? null : defaultCacheDir ?? null,
|
|
134
|
+
port: intEnv('PORT', 8765, 0, 65535),
|
|
135
|
+
host,
|
|
136
|
+
// DB_PATH belongs to e2e in a combined process. Keep the two stores apart.
|
|
137
|
+
dbPath: env['MANAGED_DB_PATH'] ?? (combined ? undefined : env['DB_PATH']) ?? 'server-managed/data/managed.db',
|
|
138
|
+
debug: env['DEBUG'] === '1' || env['DEBUG'] === 'true',
|
|
139
|
+
allowShare: env['DEEPVIEW_ALLOW_SHARE'] === '1',
|
|
140
|
+
trustProxyEnv: env['TRUST_PROXY'] ?? (serverless ? '1' : undefined),
|
|
141
|
+
githubClientId: requireStr('GITHUB_CLIENT_ID'),
|
|
142
|
+
githubClientSecret: requireStr('GITHUB_CLIENT_SECRET'),
|
|
143
|
+
initialAdminGithubId: optionalGithubId('MANAGED_INITIAL_ADMIN_GITHUB_ID'),
|
|
144
|
+
oauthCallbackUrl,
|
|
145
|
+
cookieSecure,
|
|
146
|
+
sessionCookieName,
|
|
147
|
+
sessionTtlMs: intEnv('SESSION_TTL_MS', 1_209_600_000, 60_000, 7_776_000_000),
|
|
148
|
+
repositoryAccessCacheMs: intEnv('MANAGED_REPOSITORY_ACCESS_CACHE_MS', 300_000, 0, 3_600_000),
|
|
149
|
+
githubAppId: env['GITHUB_APP_ID'] ?? null,
|
|
150
|
+
githubAppPrivateKey: normalizePem(env['GITHUB_APP_PRIVATE_KEY']),
|
|
151
|
+
githubAppSlug: env['GITHUB_APP_SLUG'] ?? null,
|
|
152
|
+
githubNewIssueLabels: env['GITHUB_NEW_ISSUE_LABELS'] ?? '',
|
|
153
|
+
maxReportBytes: intEnv('MAX_REPORT_BYTES', 26_214_400, 1, 104_857_600),
|
|
154
|
+
maxBundleBytes: intEnv('MAX_BUNDLE_BYTES', 209_715_200, 1, MAX_UPLOAD_BYTES),
|
|
155
|
+
triageHistoryLimit: intEnv('TRIAGE_HISTORY_LIMIT', 0, 0, 1_000_000_000),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// PEM private keys are awkward in env vars; accept a literal multi-line value or
|
|
160
|
+
// one with escaped newlines (`\n`). Empty/absent → null (private repos off).
|
|
161
|
+
function normalizePem(raw: string | undefined): string | null {
|
|
162
|
+
if (raw == null || raw === '') return null
|
|
163
|
+
return raw.includes('\\n') ? raw.replaceAll('\\n', '\n') : raw
|
|
164
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ManagedDb, StoredUser } from './db.ts'
|
|
2
|
+
|
|
3
|
+
export type RepoScope = { repoId: number; path: string | null }
|
|
4
|
+
type Content = { repoId: number | null; repoDirectory?: string }
|
|
5
|
+
|
|
6
|
+
// Read grants for each request: neither the manager role nor a cached page
|
|
7
|
+
// confers repository access. Bundles have repository scope; reports also have
|
|
8
|
+
// a directory scope. Unassigned content is only available to administrators.
|
|
9
|
+
export async function contentAccess(db: Pick<ManagedDb, 'listRepoScopesForUser'>, user: StoredUser) {
|
|
10
|
+
const scopes = user.role === 'admin' ? null : await db.listRepoScopesForUser(user.id)
|
|
11
|
+
const bundle = (item: Content) => scopes == null || scopes.some(scope => scope.repoId === item.repoId)
|
|
12
|
+
const report = (item: Content) => scopes == null || scopes.some(scope => scope.repoId === item.repoId
|
|
13
|
+
&& (!scope.path || item.repoDirectory === scope.path || item.repoDirectory?.startsWith(scope.path + '/')))
|
|
14
|
+
return { scopes, bundle, report }
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Token helpers for the managed auth server. The raw session token lives ONLY
|
|
2
|
+
// in the cookie; the DB row id is its SHA-256 (see db.ts / session.ts), so a
|
|
3
|
+
// DB read can't reconstruct a live cookie. No HMAC pepper is needed — the
|
|
4
|
+
// tokens are 256-bit random, not guessable or reversible from their hash.
|
|
5
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
// 256-bit URL-safe random token: the session cookie value and the OAuth CSRF
|
|
8
|
+
// `state` nonce.
|
|
9
|
+
export function randomToken(): string {
|
|
10
|
+
return randomBytes(32).toString('base64url')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// At-rest id for a session cookie token (stored as the managed_session PK).
|
|
14
|
+
export function hashToken(token: string): string {
|
|
15
|
+
return createHash('sha256').update(token, 'utf8').digest('base64url')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Constant-time equality for CSRF / OAuth-state checks. A length mismatch
|
|
19
|
+
// short-circuits (the length of these fixed-width tokens isn't a secret).
|
|
20
|
+
export function safeEqual(a: string, b: string): boolean {
|
|
21
|
+
const ab = Buffer.from(a, 'utf8')
|
|
22
|
+
const bb = Buffer.from(b, 'utf8')
|
|
23
|
+
if (ab.length !== bb.length) return false
|
|
24
|
+
return timingSafeEqual(ab, bb)
|
|
25
|
+
}
|