@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +2 -0
- package/client/dependency-paths.js +24 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +112 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +522 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-metadata.d.ts +14 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +310 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +23 -0
- package/common/dependency-reviews.js +81 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +56 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +254 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/server-info.ts +35 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +23 -0
- package/common/workspace-groups.js +176 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2AFQKRAM.js +51 -0
- package/out/chunk-2KBF5TZ7.js +2 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-35VE6CDH.js +2 -0
- package/out/chunk-3BZ7JERB.js +1 -0
- package/out/chunk-4F6JSF57.js +3 -0
- package/out/chunk-4XCDFGDP.js +10 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-CMQA4DM6.js +1 -0
- package/out/chunk-CVVJAXVY.js +2 -0
- package/out/chunk-DIJJNTRH.js +10 -0
- package/out/chunk-GJ4IUCNS.js +1 -0
- package/out/chunk-GNXOKIGP.js +80 -0
- package/out/chunk-GZ4MS3TE.js +13 -0
- package/out/chunk-IU3CCINR.js +3 -0
- package/out/chunk-K3X5EJBQ.js +2 -0
- package/out/chunk-KEX2ATEW.js +2 -0
- package/out/chunk-KVEUXIEU.js +1 -0
- package/out/chunk-KYCNJVB7.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-QZ3A376W.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-RFZQQXSG.js +17 -0
- package/out/chunk-RSHMVYNS.js +1 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-TY4RZD3Z.js +226 -0
- package/out/chunk-TZ5KB2Y5.js +10 -0
- package/out/chunk-U6OPEXIA.js +1 -0
- package/out/chunk-UNTRGE26.js +2 -0
- package/out/chunk-UXNUAIIC.js +33 -0
- package/out/chunk-UYAXVRVL.js +1 -0
- package/out/chunk-UZAZM2SX.js +2 -0
- package/out/chunk-V3ME6FI5.js +2 -0
- package/out/chunk-V5O7M2YT.js +1 -0
- package/out/chunk-VIHGO77Y.js +1 -0
- package/out/chunk-VQJCXGIY.js +7 -0
- package/out/chunk-W4J3MGLB.js +1 -0
- package/out/chunk-WGG7KQUH.js +1 -0
- package/out/chunk-WZDTZE64.js +1 -0
- package/out/chunk-XL35ZV6R.js +1 -0
- package/out/chunk-XYFXXB2G.js +10 -0
- package/out/chunk-YVZH5SHP.js +1 -0
- package/out/chunk-ZBBOBQ43.js +1 -0
- package/out/chunk-ZMQQCGF6.js +2 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +37 -0
- package/out/client-sync.js +10 -13
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +6 -2
- package/out/stasis.svg +20 -0
- package/out/terminal.js +365 -39
- package/out/view.css +1 -1
- package/out/view.js +83 -117
- package/package.json +298 -59
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +171 -0
- package/risk/files/runs-code.js +595 -0
- package/risk/files/script.js +114 -0
- package/risk/files/stamps.js +46 -0
- package/risk/index.d.ts +156 -0
- package/risk/index.js +7 -0
- package/risk/npm/assess.js +223 -0
- package/risk/npm/combined.js +65 -0
- package/risk/npm/compare.js +228 -0
- package/risk/npm/dependencies.js +153 -0
- package/risk/npm/diff.js +43 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +34 -0
- package/risk/npm/held.js +60 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +30 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +282 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +113 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +486 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +56 -35
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +116 -18
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +35 -18
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +27 -26
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +102 -68
- package/{server → server-e2e}/sse-session.ts +28 -8
- package/{server → server-e2e}/static.ts +31 -21
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +245 -0
- package/server-managed/avatar-store.ts +12 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +111 -0
- package/server-managed/bundle-build.ts +198 -0
- package/server-managed/bundle-cache.ts +199 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +120 -0
- package/server-managed/cache-storage.ts +19 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +158 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1725 -0
- package/server-managed/db-neon.ts +367 -0
- package/server-managed/db-schema.ts +227 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +132 -0
- package/server-managed/dependency-reviews.ts +248 -0
- package/server-managed/github-app.ts +391 -0
- package/server-managed/github-errors.ts +68 -0
- package/server-managed/github-issue-links.ts +127 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +235 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/http-response.ts +50 -0
- package/server-managed/http.ts +3472 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +187 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +158 -0
- package/server-managed/locked-packages.ts +85 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +75 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-insights.ts +440 -0
- package/server-managed/npm-kept.ts +65 -0
- package/server-managed/npm-loads.ts +167 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +378 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +177 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +74 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +352 -0
- package/server-managed/public-workspace.ts +220 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +270 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +47 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +153 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-risk.ts +420 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/session.ts +92 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +69 -0
- package/server-managed/sql-postgres.ts +31 -0
- package/server-managed/sql.ts +76 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +175 -0
- package/server-managed/storage-encryption.ts +279 -0
- package/server-managed/storage-maintenance.ts +256 -0
- package/server-managed/storage-payload.ts +103 -0
- package/server-managed/storage-stores.ts +85 -0
- package/server-managed/storage.ts +78 -0
- package/server-managed/team-app.ts +94 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +173 -0
- package/server-managed/team-npm-scopes.ts +77 -0
- package/server-managed/team-reports.ts +228 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +103 -0
- package/server-managed/uploads.ts +86 -0
- package/server-managed/upstream-cache.ts +215 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +166 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -477
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -320
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The change blocks between two texts' lines, found by @preventive/diff: what
|
|
2
|
+
// the viewer's Compare diffs (ui/view/bundle-compare-code-model.js); and the
|
|
3
|
+
// server's check, apart from @preventive/compare's own, that its diff with
|
|
4
|
+
// renamed names hidden takes the old side to the new (server-managed/rename-diff.ts).
|
|
5
|
+
//
|
|
6
|
+
// Lines are numbered the way the diff numbers them: from zero, a final
|
|
7
|
+
// newline ending the last line rather than starting an empty one.
|
|
8
|
+
import { diff, parseDiff } from '@preventive/diff'
|
|
9
|
+
|
|
10
|
+
export function textLines(text) {
|
|
11
|
+
if (text === '') return []
|
|
12
|
+
const lines = text.split('\n')
|
|
13
|
+
if (lines.at(-1) === '') lines.pop()
|
|
14
|
+
return lines
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Whether a text's last line ends in a newline. One added or dropped there is
|
|
18
|
+
// a change of that line, as diff has it (the viewer marks it on that line's
|
|
19
|
+
// row), but for diff -b, which ignores it as it does other trailing blanks.
|
|
20
|
+
const endsLine = text => text === '' || text.endsWith('\n')
|
|
21
|
+
|
|
22
|
+
// A line as diff -b reads it (@preventive/diff's): a run of the C locale's
|
|
23
|
+
// blanks as one space, and those that end it as none.
|
|
24
|
+
const spacing = line => line.replace(/[ \t\n\v\f\r]+$/u, '').replaceAll(/[ \t\n\v\f\r]+/gu, ' ')
|
|
25
|
+
|
|
26
|
+
// Each `a[a0..a1)` replaced by `b[b0..b1)`. `ignoreWhitespace` is diff -b:
|
|
27
|
+
// lines differing only in how much whitespace they have pair up as unchanged,
|
|
28
|
+
// whitespace added or removed between other characters still a change.
|
|
29
|
+
export function changeBlocks(before, after, ignoreWhitespace = false) {
|
|
30
|
+
const text = diff(before, after, { format: 'unified', context: 0, whitespace: ignoreWhitespace ? 'change' : 'none' })
|
|
31
|
+
return text ? (parseDiff(text)[0]?.blocks ?? []).map(({ a0, a1, b0, b1 }) => ({ a0, a1, b0, b1 })) : []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Whether `blocks` take `before`'s lines to `after`'s: each in order and apart
|
|
35
|
+
// from the last, within both texts, and every line between them alike on both
|
|
36
|
+
// sides (alike but for how much whitespace they have where `ignoreWhitespace`,
|
|
37
|
+
// as diff -b has them), the last line in a block where a newline was added or
|
|
38
|
+
// dropped after it, so that a diff drawn from them hides no change. A check on
|
|
39
|
+
// what changeBlocks found, or on one found apart from the texts drawn.
|
|
40
|
+
export function blocksHold(before, after, blocks, ignoreWhitespace = false) {
|
|
41
|
+
if (!Array.isArray(blocks)) return false
|
|
42
|
+
const a = textLines(before), b = textLines(after)
|
|
43
|
+
const alike = ignoreWhitespace ? (x, y) => spacing(x) === spacing(y) : (x, y) => x === y
|
|
44
|
+
let i = 0, j = 0
|
|
45
|
+
for (const block of [...blocks, { a0: a.length, a1: a.length, b0: b.length, b1: b.length, last: true }]) {
|
|
46
|
+
const { a0, a1, b0, b1 } = block ?? {}
|
|
47
|
+
if (![a0, a1, b0, b1].every(Number.isInteger) || a0 < i || b0 < j || a1 < a0 || b1 < b0 || a1 > a.length || b1 > b.length) return false
|
|
48
|
+
// A block changes a line, and as many lines are alike before it on each side.
|
|
49
|
+
if ((!block.last && a1 === a0 && b1 === b0) || a0 - i !== b0 - j) return false
|
|
50
|
+
if (block.last && a0 > i && !ignoreWhitespace && endsLine(before) !== endsLine(after)) return false
|
|
51
|
+
for (; i < a0; i++, j++) if (!alike(a[i], b[j])) return false
|
|
52
|
+
i = a1
|
|
53
|
+
j = b1
|
|
54
|
+
}
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Explicit links combine already-resolved workspace App rows. This is a view
|
|
2
|
+
// grouping, not report deduplication: each report's revalidation answer has
|
|
3
|
+
// already been checked in its own row, and links never create report conflicts.
|
|
4
|
+
//
|
|
5
|
+
// `visibleTabs` decides which tabs may BRIDGE two rows; `retainedTabs` (the
|
|
6
|
+
// same projection unless the caller says otherwise) decides which tabs the
|
|
7
|
+
// combined row keeps. They part when a display choice shows a row more of
|
|
8
|
+
// itself without meaning to re-draw the workspace's row boundaries.
|
|
9
|
+
export function mergeLinkedWorkspaceGroups(groups, duplicatesOf, visibleTabs, retainedTabs = visibleTabs) {
|
|
10
|
+
const parent = groups.map((_, i) => i)
|
|
11
|
+
const find = (i) => {
|
|
12
|
+
while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i] }
|
|
13
|
+
return i
|
|
14
|
+
}
|
|
15
|
+
const tabs = groups.map(visibleTabs)
|
|
16
|
+
const byId = new Map()
|
|
17
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
18
|
+
for (const f of tabs[i]) {
|
|
19
|
+
if (!f.id) continue
|
|
20
|
+
if (!byId.has(f.id)) byId.set(f.id, [])
|
|
21
|
+
byId.get(f.id).push(i)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const [id, rows] of byId) {
|
|
25
|
+
for (const other of duplicatesOf(id)) {
|
|
26
|
+
if (other === id) continue
|
|
27
|
+
for (const a of rows) {
|
|
28
|
+
for (const b of byId.get(other) ?? []) {
|
|
29
|
+
const left = find(a), right = find(b)
|
|
30
|
+
if (left !== right) parent[right] = left
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const components = new Map()
|
|
36
|
+
for (let i = 0; i < groups.length; i++) {
|
|
37
|
+
const root = find(i)
|
|
38
|
+
if (!components.has(root)) components.set(root, [])
|
|
39
|
+
components.get(root).push(i)
|
|
40
|
+
}
|
|
41
|
+
if (components.size === groups.length) return groups
|
|
42
|
+
const key = (f) => f.id ?? String(f._id)
|
|
43
|
+
const merged = [...components.values()].map((indices) => {
|
|
44
|
+
if (indices.length === 1) return groups[indices[0]]
|
|
45
|
+
// Retain the tabs visible in each original row. An imported App finding
|
|
46
|
+
// or an unjudged source row must not disappear when linked to a pass row.
|
|
47
|
+
const visible = new Map()
|
|
48
|
+
for (const i of indices) for (const f of retainedTabs(groups[i])) if (!visible.has(key(f))) visible.set(key(f), f)
|
|
49
|
+
const members = new Map()
|
|
50
|
+
for (const i of indices) {
|
|
51
|
+
for (const f of groups[i]) {
|
|
52
|
+
if (!members.has(key(f))) members.set(key(f), visible.get(key(f)) ?? f)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const group = [...members.values()]
|
|
56
|
+
const first = groups[indices[0]]
|
|
57
|
+
group.workspaceKey = first.workspaceKey ?? key(first[0])
|
|
58
|
+
group.linkedTabs = [...visible.values()]
|
|
59
|
+
return group
|
|
60
|
+
})
|
|
61
|
+
Object.defineProperty(merged, 'ruledOutIds', { value: groups.ruledOutIds, configurable: true })
|
|
62
|
+
return merged
|
|
63
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Managed comments are independent records, shared by reports containing the
|
|
2
|
+
// same finding. Imported and legacy comments may lack attribution or dates.
|
|
3
|
+
export interface ManagedComment {
|
|
4
|
+
id: string
|
|
5
|
+
findingId: string
|
|
6
|
+
body: string
|
|
7
|
+
authorId: string | null
|
|
8
|
+
authorLogin: string | null
|
|
9
|
+
createdAt: number | null
|
|
10
|
+
updatedAt: number | null
|
|
11
|
+
version: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const MAX_COMMENT_TEXT = 10_000
|
|
15
|
+
|
|
16
|
+
// Callers also enforce write access to the report containing the finding.
|
|
17
|
+
// Admins can remove comments with no linked user, but cannot claim authorship.
|
|
18
|
+
export function canDeleteComment(comment: Pick<ManagedComment, 'authorId'>, actor: { id: string; role: string } | null | undefined): boolean {
|
|
19
|
+
return actor != null && (comment.authorId === actor.id || (comment.authorId == null && actor.role === 'admin'))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function compareManagedComments(a: ManagedComment, b: ManagedComment): number {
|
|
23
|
+
if (a.createdAt == null && b.createdAt != null) return -1
|
|
24
|
+
if (a.createdAt != null && b.createdAt == null) return 1
|
|
25
|
+
const byDate = (a.createdAt ?? 0) - (b.createdAt ?? 0)
|
|
26
|
+
if (byDate !== 0) return byDate
|
|
27
|
+
if (a.id === b.id) return 0
|
|
28
|
+
return a.id < b.id ? -1 : 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseCommentBody(value: unknown): string | null {
|
|
32
|
+
if (typeof value !== 'string' || value.length > MAX_COMMENT_TEXT) return null
|
|
33
|
+
return value.trim() || null
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const MAX_TEAM_NPM_SCOPES: number
|
|
2
|
+
export function isNpmPackageName(value: unknown): value is string
|
|
3
|
+
export function isNpmPackageSpec(value: unknown): value is string
|
|
4
|
+
export function npmPackageScope(name: string): string | null
|
|
5
|
+
export function normalizeNpmScope(value: unknown): string | null
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// npm package names, versions and scopes as the managed npm viewer takes
|
|
2
|
+
// them, shared by its routes, the server and the Teams page.
|
|
3
|
+
|
|
4
|
+
// Upstream's rule (@preventive/upstream src/args.js): a scope, then a name
|
|
5
|
+
// starting with none of `.`, `_` and `-`, 214 characters in all.
|
|
6
|
+
const PACKAGE_NAME = /^(?=.{1,214}$)(?:@[\w.-]+\/[\w-]|[\dA-Za-z])[\w.-]*$/u
|
|
7
|
+
// An exact version or a dist-tag, as the registry resolves `/<name>/<spec>`.
|
|
8
|
+
const PACKAGE_SPEC = /^(?=.{1,256}$)[\dA-Za-z][\w.+-]*$/u
|
|
9
|
+
// npm's rule for a scope: lowercase, and not starting with `.` or `_`.
|
|
10
|
+
const SCOPE = /^@[\da-z~-][\w.~-]*$/u
|
|
11
|
+
export const MAX_TEAM_NPM_SCOPES = 100
|
|
12
|
+
|
|
13
|
+
export function isNpmPackageName(value) {
|
|
14
|
+
return typeof value === 'string' && PACKAGE_NAME.test(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isNpmPackageSpec(value) {
|
|
18
|
+
return typeof value === 'string' && PACKAGE_SPEC.test(value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// `@scope` of a scoped name, lowercased as scopes are compared; null for an
|
|
22
|
+
// unscoped one.
|
|
23
|
+
export function npmPackageScope(name) {
|
|
24
|
+
return name.startsWith('@') && name.includes('/') ? name.slice(0, name.indexOf('/')).toLowerCase() : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A scope as typed: `scope` or `@scope`, either case, surrounding space
|
|
28
|
+
// ignored. Null when it is not one npm could publish under.
|
|
29
|
+
export function normalizeNpmScope(value) {
|
|
30
|
+
if (typeof value !== 'string') return null
|
|
31
|
+
const trimmed = value.trim().toLowerCase()
|
|
32
|
+
const scope = trimmed.startsWith('@') ? trimmed : `@${trimmed}`
|
|
33
|
+
return scope.length <= 214 && SCOPE.test(scope) ? scope : null
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Per-membership visibility permissions for a team's users — shared by the
|
|
2
|
+
// managed server (which stores + validates them) and the admin client (the
|
|
3
|
+
// team-member checkboxes). Each is an independent opt-in, BOTH DEFAULT OFF:
|
|
4
|
+
// dependencies — scan findings in dependencies' own code; App impact stays visible
|
|
5
|
+
// security — security findings and published bundle advisories
|
|
6
|
+
// The managed server filters report responses and authorizes advisory lookups
|
|
7
|
+
// using these independent permissions.
|
|
8
|
+
export type VisibilityPermission = 'dependencies' | 'security'
|
|
9
|
+
|
|
10
|
+
export const VISIBILITY_PERMISSIONS: readonly VisibilityPermission[] = ['dependencies', 'security']
|
|
11
|
+
|
|
12
|
+
// Human labels for the admin checkboxes (keyed by permission).
|
|
13
|
+
export const VISIBILITY_PERMISSION_LABELS: Record<VisibilityPermission, string> = {
|
|
14
|
+
dependencies: 'Dependencies',
|
|
15
|
+
security: 'Security',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A membership's resolved permission set; every key present (false = off).
|
|
19
|
+
export type TeamUserPermissions = Record<VisibilityPermission, boolean>
|
|
20
|
+
|
|
21
|
+
// All-off — the default for a fresh membership.
|
|
22
|
+
const DEFAULT_TEAM_USER_PERMISSIONS: TeamUserPermissions = { dependencies: false, security: false }
|
|
23
|
+
|
|
24
|
+
// Coerce arbitrary input (a request body) into a full permission set: a key is
|
|
25
|
+
// granted only when explicitly `true`; anything else (absent, non-bool) is off.
|
|
26
|
+
export function parseTeamUserPermissions(x: unknown): TeamUserPermissions {
|
|
27
|
+
const o = (x == null || typeof x !== 'object') ? {} : x as Record<string, unknown>
|
|
28
|
+
const out = { ...DEFAULT_TEAM_USER_PERMISSIONS }
|
|
29
|
+
for (const p of VISIBILITY_PERMISSIONS) out[p] = o[p] === true
|
|
30
|
+
return out
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { parseLinkedFindings } from '../../client/linked-findings.js'
|
|
2
|
+
import { dependencyDirectory, isDependencyFile } from '../../client/dependency-paths.js'
|
|
3
|
+
import { detectFormat, loadFindings, parseCodexCsvToScans, readReport, reportEntries, reportRepoGithub } from '@preventive/report'
|
|
4
|
+
import { commonFileDirectory } from './repository-alias.ts'
|
|
5
|
+
|
|
6
|
+
// A managed blob retains one server identity even when a CSV contains several
|
|
7
|
+
// scans. Keep all their findings (and upstream ids) under that identity. CSV
|
|
8
|
+
// repository columns describe findings, not an embedded report-level location.
|
|
9
|
+
export function readManagedReport(content: string, filename: string): ReturnType<typeof readReport> {
|
|
10
|
+
const links = content.trimStart().startsWith('[') ? parseLinkedFindings(content) : null
|
|
11
|
+
if (links) return { data: { source: 'links', findings: [], links: links.groups }, format: 'links', reason: null }
|
|
12
|
+
const parsed = readReport(content)
|
|
13
|
+
// Permission-filtered responses can be JSON under the original CSV filename.
|
|
14
|
+
if (parsed.data != null || detectFormat(content, filename) !== 'codex') return parsed
|
|
15
|
+
try {
|
|
16
|
+
const scans = parseCodexCsvToScans(content)
|
|
17
|
+
return {
|
|
18
|
+
data: { type: 'security', source: 'codex-security', findings: scans.flatMap((scan) => scan.data.findings) },
|
|
19
|
+
format: 'codex', reason: null,
|
|
20
|
+
}
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return { data: null, format: null, reason: err instanceof Error ? err.message : 'Invalid Codex CSV' }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadManagedFindings(content: string, filename: string): ReturnType<typeof loadFindings> {
|
|
27
|
+
const parsed = readManagedReport(content, filename)
|
|
28
|
+
return parsed.data == null ? Promise.resolve(null) : loadFindings(JSON.stringify(parsed.data))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Source access follows individual visible members, not their shared finding
|
|
32
|
+
// IDs: another member with the same ID can cite files the viewer cannot see.
|
|
33
|
+
export function managedFindingSourcePaths(findings: unknown[]): Set<string> {
|
|
34
|
+
const paths = new Set<string>()
|
|
35
|
+
for (const finding of findings) {
|
|
36
|
+
if (!finding || typeof finding !== 'object') continue
|
|
37
|
+
const f = finding as { file?: unknown; evidence?: { file?: unknown }[] }
|
|
38
|
+
if (typeof f.file === 'string' && f.file) paths.add(f.file)
|
|
39
|
+
for (const evidence of Array.isArray(f.evidence) ? f.evidence : []) {
|
|
40
|
+
if (typeof evidence?.file === 'string' && evidence.file) paths.add(evidence.file)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return paths
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The report's own findings, in either entry shape, outside the dependency
|
|
47
|
+
// directory the local report view would choose: node_modules, else vendor,
|
|
48
|
+
// else dependencies.
|
|
49
|
+
function ownFindings(data: unknown) {
|
|
50
|
+
const objects = (reportEntries(data) ?? []).flat().filter((finding): finding is { file?: unknown } => finding != null && typeof finding === 'object')
|
|
51
|
+
const directory = dependencyDirectory([{ groups: [objects], tree: (data as { tree?: unknown } | null)?.tree }])
|
|
52
|
+
return { directory, own: objects.filter(finding => !isDependencyFile(finding.file, directory)) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Findings name the repository of their own file. Like the local report view,
|
|
56
|
+
// a report without its own repository belongs to the one repository named by
|
|
57
|
+
// its own findings (Claude Security and Codex exports name one on every finding).
|
|
58
|
+
export function findingsRepository(data: unknown): string | null {
|
|
59
|
+
const repos = new Map<string, string>()
|
|
60
|
+
for (const finding of ownFindings(data).own) {
|
|
61
|
+
const github = reportRepoGithub(finding)
|
|
62
|
+
if (github) repos.set(github.toLowerCase(), github)
|
|
63
|
+
}
|
|
64
|
+
return repos.size === 1 ? [...repos.values()][0]! : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The directory shared by the files the report's own findings cite, for alias
|
|
68
|
+
// matching. Dependency paths, including cited evidence, cannot hide it.
|
|
69
|
+
export function ownFileDirectory(data: unknown): string {
|
|
70
|
+
const { directory, own } = ownFindings(data)
|
|
71
|
+
return commonFileDirectory([...managedFindingSourcePaths(own)].filter(path => !isDependencyFile(path, directory)))
|
|
72
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Apply report permissions before serving findings, triage, or source files.
|
|
2
|
+
// Security uses the same complete-row classification as the UI. Dependency
|
|
3
|
+
// access is per finding: App, own source, and the own-source organizations stay.
|
|
4
|
+
import { inheritReportMeta, isAppFinding, reportRepoGithub, stampSecurityGroups } from '@preventive/report'
|
|
5
|
+
import { readManagedReport } from './report-content.ts'
|
|
6
|
+
|
|
7
|
+
export interface ViewerPermissions {
|
|
8
|
+
dependencies: boolean
|
|
9
|
+
security: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Finding = Record<string, unknown>
|
|
13
|
+
function object(value: unknown): Finding | null {
|
|
14
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Finding : null
|
|
15
|
+
}
|
|
16
|
+
function pathOf(finding: Finding): string {
|
|
17
|
+
return typeof finding['file'] === 'string' ? finding['file'].replaceAll('\\', '/') : ''
|
|
18
|
+
}
|
|
19
|
+
function isDependency(finding: Finding): boolean {
|
|
20
|
+
return /(?:^|\/)(?:node_modules|vendor|dependencies)(?:\/|$)/u.test(pathOf(finding))
|
|
21
|
+
}
|
|
22
|
+
function githubOrg(value: unknown): string | null {
|
|
23
|
+
return reportRepoGithub(value)?.split('/')[0]?.toLowerCase() ?? null
|
|
24
|
+
}
|
|
25
|
+
function npmScope(finding: Finding, fromPath = false): string | null {
|
|
26
|
+
let name = object(object(finding['package'])?.['npm'])?.['name']
|
|
27
|
+
if (typeof name !== 'string' && fromPath) {
|
|
28
|
+
const parts = pathOf(finding).split('/')
|
|
29
|
+
for (let i = parts.length - 3; i >= 0; i--) {
|
|
30
|
+
if (['node_modules', 'vendor', 'dependencies'].includes(parts[i]!)) {
|
|
31
|
+
name = `${parts[i + 1]}/${parts[i + 2]}`
|
|
32
|
+
break
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return typeof name === 'string' ? /^(@[^/\s]+)\/[^/\s]+$/u.exec(name)?.[1] ?? null : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Parsed projections keep run inheritance and derived flags out of cached
|
|
40
|
+
// reports. A known positive security stamp may include linked findings outside
|
|
41
|
+
// this report; a negative stamp never overrides the intrinsic classifier.
|
|
42
|
+
export function projectFinding(value: unknown, report: Finding): Finding | null {
|
|
43
|
+
const original = object(value)
|
|
44
|
+
if (!original) return null
|
|
45
|
+
const finding = { ...original }
|
|
46
|
+
inheritReportMeta(finding, report)
|
|
47
|
+
finding['_source'] = finding['source'] ?? report['source'] ?? null
|
|
48
|
+
if (!('isApp' in finding)) finding['isApp'] = isAppFinding(finding, finding['_source'])
|
|
49
|
+
if (finding['isSecurity'] === true) finding['security'] = true
|
|
50
|
+
return finding
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Native JSON and supported text imports obey the same permission rules.
|
|
54
|
+
// A filtered text import is returned as JSON under its original filename, which
|
|
55
|
+
// readManagedReport already accepts. Unchanged inputs keep their original bytes.
|
|
56
|
+
export function filterReportContent(content: string, perms: ViewerPermissions, filename = '', repo?: unknown): string {
|
|
57
|
+
if (perms.dependencies && perms.security) return content
|
|
58
|
+
let data: unknown
|
|
59
|
+
try { data = JSON.parse(content) } catch {
|
|
60
|
+
const parsed = readManagedReport(content, filename)
|
|
61
|
+
if (parsed.data == null) {
|
|
62
|
+
if (/\.csv$/iu.test(filename)) throw new Error('Cannot filter unreadable CSV report')
|
|
63
|
+
return content
|
|
64
|
+
}
|
|
65
|
+
data = parsed.data
|
|
66
|
+
}
|
|
67
|
+
const filtered = filterReportData(data, perms, repo)
|
|
68
|
+
return filtered === data ? content : JSON.stringify(filtered)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// repo, when supplied, is the server's authoritative assignment (including an
|
|
72
|
+
// explicit unassigned value). Finding-specific repos still describe their source.
|
|
73
|
+
export function filterReportData(data: unknown, perms: ViewerPermissions, repo?: unknown, securityIds?: Set<string>): unknown {
|
|
74
|
+
if (perms.dependencies && perms.security) return data
|
|
75
|
+
const report = object(data)
|
|
76
|
+
if (!report) return data
|
|
77
|
+
const key = Array.isArray(report['findings']) ? 'findings' : Array.isArray(report['groups']) ? 'groups' : null
|
|
78
|
+
if (!key) return data
|
|
79
|
+
const entries = (report[key] as unknown[]).map(entry => ({
|
|
80
|
+
entry, members: (Array.isArray(entry) ? entry : [entry]).map(original => ({ original, finding: projectFinding(original, report) })),
|
|
81
|
+
}))
|
|
82
|
+
const groups = entries.map(({ members }) => members.flatMap(({ finding }) => finding ? [finding] : []))
|
|
83
|
+
if (!perms.security) stampSecurityGroups(groups)
|
|
84
|
+
const githubOrgs = new Set<string>(), npmScopes = new Set<string>()
|
|
85
|
+
const addOrgs = (finding: Finding) => {
|
|
86
|
+
const github = githubOrg(finding), npm = npmScope(finding)
|
|
87
|
+
if (github) githubOrgs.add(github)
|
|
88
|
+
if (npm) npmScopes.add(npm)
|
|
89
|
+
}
|
|
90
|
+
addOrgs({ ...report, repo: repo === undefined ? report['repo'] : repo })
|
|
91
|
+
for (const finding of groups.flat()) {
|
|
92
|
+
if (finding['isApp'] !== true && !isDependency(finding)) addOrgs(finding)
|
|
93
|
+
}
|
|
94
|
+
const kept: unknown[] = []
|
|
95
|
+
let changed = false
|
|
96
|
+
for (const { entry, members } of entries) {
|
|
97
|
+
// Classify the complete row before hiding dependency components.
|
|
98
|
+
if (!perms.security && members.some(({ finding }) => finding?.['isSecurity'] === true || securityIds?.has(String(finding?.['id'])))) { changed = true; continue }
|
|
99
|
+
const visible = members.filter(({ finding }) => !finding || perms.dependencies || finding['isApp'] === true || !isDependency(finding)
|
|
100
|
+
|| githubOrgs.has(githubOrg(finding) ?? '') || npmScopes.has(npmScope(finding, true) ?? ''))
|
|
101
|
+
if (visible.length === members.length) kept.push(entry)
|
|
102
|
+
else {
|
|
103
|
+
changed = true
|
|
104
|
+
if (visible.length > 0) kept.push(Array.isArray(entry) ? visible.map(({ original }) => original) : visible[0]!.original)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return changed ? { ...report, [key]: kept } : data
|
|
108
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface RepositoryAliasInput {
|
|
2
|
+
oldRepo: string
|
|
3
|
+
oldPath: string
|
|
4
|
+
repoId: number
|
|
5
|
+
newPath: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function containsPath(parent: string, path: string) {
|
|
9
|
+
return !parent || path === parent || path.startsWith(`${parent}/`)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function aliasLocation(alias: RepositoryAliasInput, directory: string) {
|
|
13
|
+
if (containsPath(alias.oldPath, directory)) {
|
|
14
|
+
const suffix = directory.slice(alias.oldPath.length).replace(/^\//u, '')
|
|
15
|
+
return { directory: [alias.newPath, suffix].filter(Boolean).join('/'), filePrefix: '' }
|
|
16
|
+
}
|
|
17
|
+
if (!containsPath(directory, alias.oldPath)) return null
|
|
18
|
+
const filePrefix = alias.oldPath.slice(directory.length).replace(/^\//u, '')
|
|
19
|
+
// This suffix is already in every file path. The destination must preserve
|
|
20
|
+
// it, so mapping needs only a new directory, never a file-path rewrite.
|
|
21
|
+
if (alias.newPath === filePrefix) return { directory: '', filePrefix }
|
|
22
|
+
if (!alias.newPath.endsWith(`/${filePrefix}`)) return null
|
|
23
|
+
return { directory: alias.newPath.slice(0, -filePrefix.length - 1), filePrefix }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Match directory boundaries, keeping any subdirectory below the alias. A
|
|
27
|
+
// shared file prefix can prove a more specific alias applies without changing
|
|
28
|
+
// those files. The most specific applicable source wins, once, never chained.
|
|
29
|
+
export function matchRepositoryAlias(github: string, directory: string, aliases: readonly RepositoryAliasInput[], filePrefix = '') {
|
|
30
|
+
let match: { repoId: number; directory: string } | null = null, specificity = -1
|
|
31
|
+
for (const alias of aliases) {
|
|
32
|
+
if (alias.oldRepo.toLowerCase() !== github.toLowerCase()) continue
|
|
33
|
+
const location = aliasLocation(alias, directory)
|
|
34
|
+
if (!location || !containsPath(location.filePrefix, filePrefix) || alias.oldPath.length <= specificity) continue
|
|
35
|
+
match = { repoId: alias.repoId, directory: location.directory }
|
|
36
|
+
specificity = alias.oldPath.length
|
|
37
|
+
}
|
|
38
|
+
return match
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Run before decoding a bundle's inventory: unrelated repos/directories and
|
|
42
|
+
// mappings without the required common suffix cannot benefit from its paths.
|
|
43
|
+
export function repositoryAliasNeedsFilePrefix(github: string, directory: string, aliases: readonly RepositoryAliasInput[]) {
|
|
44
|
+
return aliases.some(alias => alias.oldRepo.toLowerCase() === github.toLowerCase()
|
|
45
|
+
&& Boolean(aliasLocation(alias, directory)?.filePrefix))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Whole directory segments only; no source bodies, hashing, or path rewriting.
|
|
49
|
+
// A single file still proves its parent prefix. Stop as soon as none is shared.
|
|
50
|
+
export function commonFileDirectory(paths: Iterable<string>): string {
|
|
51
|
+
let prefix: string[] | undefined
|
|
52
|
+
for (const path of paths) {
|
|
53
|
+
const parts = path.split('/')
|
|
54
|
+
if (parts.some(part => !part || part === '.' || part === '..') || path.includes('\\')) return ''
|
|
55
|
+
parts.pop()
|
|
56
|
+
if (prefix === undefined) prefix = parts
|
|
57
|
+
else {
|
|
58
|
+
let length = 0
|
|
59
|
+
while (length < prefix.length && length < parts.length && prefix[length] === parts[length]) length++
|
|
60
|
+
prefix.length = length
|
|
61
|
+
}
|
|
62
|
+
if (prefix.length === 0) return ''
|
|
63
|
+
}
|
|
64
|
+
return prefix?.join('/') ?? ''
|
|
65
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Managed-mode user roles, shared by the managed server (which stores +
|
|
2
|
+
// validates them) and the admin client (the role picker). A linear privilege
|
|
3
|
+
// ladder, highest first:
|
|
4
|
+
// admin — full access incl. managing other users' roles
|
|
5
|
+
// manage — owned/team content management
|
|
6
|
+
// triage — can triage findings
|
|
7
|
+
// view — read-only
|
|
8
|
+
// none — no access
|
|
9
|
+
// Admins manage workspace access. Managers handle owned uploads and content
|
|
10
|
+
// in their teams; repository changes also require repository/path access. View
|
|
11
|
+
// is the floor for reading content, triage for annotating accessible reports.
|
|
12
|
+
// None cannot read or mutate managed data, regardless of ownership or teams.
|
|
13
|
+
export type Role = 'admin' | 'manage' | 'triage' | 'view' | 'none'
|
|
14
|
+
|
|
15
|
+
export const ROLES: readonly Role[] = ['admin', 'manage', 'triage', 'view', 'none']
|
|
16
|
+
|
|
17
|
+
export function isRole(x: unknown): x is Role {
|
|
18
|
+
return typeof x === 'string' && (ROLES as readonly string[]).includes(x)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// True iff `role` sits at or above `min` in the privilege ladder. ROLES is
|
|
22
|
+
// ordered highest-first, so a lower index is more privileged; an unknown role
|
|
23
|
+
// (index -1) is never "at least" anything. e.g. roleAtLeast(role, 'view') is
|
|
24
|
+
// "has at least read access" (everything except 'none').
|
|
25
|
+
export function roleAtLeast(role: Role, min: Role): boolean {
|
|
26
|
+
const r = ROLES.indexOf(role)
|
|
27
|
+
return r !== -1 && r <= ROLES.indexOf(min)
|
|
28
|
+
}
|