@preventive/triage 1.0.0-alpha.43 → 1.0.0-alpha.45

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.
Files changed (186) hide show
  1. package/common/bundle-compare-diff.js +2 -167
  2. package/common/bundle-compare-versions.js +172 -0
  3. package/common/bundle-comparison.d.ts +8 -0
  4. package/common/bundle-comparison.js +62 -0
  5. package/common/bundle-sources.js +20 -1
  6. package/common/default-scan-models.ts +1 -1
  7. package/common/dependency-reviews.d.ts +0 -5
  8. package/common/dependency-reviews.js +0 -17
  9. package/common/managed/agent-tasks.ts +292 -0
  10. package/common/managed/github-agents.ts +92 -0
  11. package/common/managed/github-logins.ts +49 -0
  12. package/common/managed/roles.ts +7 -0
  13. package/common/managed/routes.js +91 -10
  14. package/common/managed/team-app.d.ts +5 -2
  15. package/common/managed/team-app.js +2 -2
  16. package/common/server-info.ts +11 -0
  17. package/common/workspace-app.js +19 -3
  18. package/out/brotli-fallback.js +3 -3
  19. package/out/chunk-2XUEZHIP.js +18 -18
  20. package/out/chunk-2ZDZWLFY.js +1 -0
  21. package/out/chunk-3PS4KWD2.js +10 -0
  22. package/out/chunk-5DHL5NG3.js +2 -0
  23. package/out/chunk-6HNXNIQY.js +52 -0
  24. package/out/chunk-7C7DWL7I.js +1 -0
  25. package/out/chunk-AFLZH7WG.js +1 -0
  26. package/out/chunk-BC2UCLFJ.js +1 -0
  27. package/out/chunk-C4Z6ZBXV.js +6 -6
  28. package/out/chunk-GDPCLSOS.js +2 -0
  29. package/out/chunk-GO7FT4OD.js +1 -0
  30. package/out/chunk-HMAJDVXN.js +1 -0
  31. package/out/chunk-I2MDGSQX.js +3 -0
  32. package/out/chunk-IYPY47MC.js +1 -0
  33. package/out/chunk-IZ7POWI3.js +18 -0
  34. package/out/chunk-JCIIBLKR.js +1 -0
  35. package/out/chunk-NPW52BM2.js +17 -0
  36. package/out/chunk-NRPC74AM.js +1 -1
  37. package/out/chunk-NV7ES4EW.js +1 -0
  38. package/out/chunk-OKRLLE5I.js +17 -0
  39. package/out/chunk-OOJM4CTU.js +1 -1
  40. package/out/chunk-PNHBZGDD.js +1 -1
  41. package/out/chunk-PTGDYLNS.js +36 -0
  42. package/out/chunk-PXK7EU4V.js +1 -0
  43. package/out/chunk-Q6MVYGYV.js +3 -0
  44. package/out/chunk-QB4CBOX2.js +1 -0
  45. package/out/chunk-QRD6GIG5.js +229 -0
  46. package/out/chunk-R3NZFXVR.js +1 -0
  47. package/out/chunk-R5O6FAW4.js +1 -1
  48. package/out/chunk-SXV2T5C4.js +80 -0
  49. package/out/chunk-TUII34K2.js +1 -1
  50. package/out/chunk-VFWMQ3LG.js +2 -0
  51. package/out/chunk-VH4XMZIW.js +2 -0
  52. package/out/chunk-WFYLMF6Z.js +10 -0
  53. package/out/chunk-WI7Z5A2T.js +6 -0
  54. package/out/chunk-WQ74ZBZF.js +7 -0
  55. package/out/chunk-XVBEOGOS.js +3 -0
  56. package/out/client-managed-import.js +3 -3
  57. package/out/client-managed.js +31 -35
  58. package/out/client-sync.js +11 -11
  59. package/out/compare.js +35 -0
  60. package/out/dialogs.js +51 -0
  61. package/out/github-issue-authorized.html +1 -1
  62. package/out/github-issue-authorized.js +1 -1
  63. package/out/graph.js +17 -17
  64. package/out/index.html +31 -31
  65. package/out/prism.js +14 -14
  66. package/out/scan.js +2 -0
  67. package/out/terminal.js +294 -293
  68. package/out/view.css +1 -1
  69. package/out/view.js +16 -107
  70. package/package.json +55 -6
  71. package/risk/actions/findings.js +156 -0
  72. package/risk/actions/resolve.js +219 -0
  73. package/risk/actions/scripts/commands.js +182 -0
  74. package/risk/actions/scripts/grade.js +113 -0
  75. package/risk/actions/scripts/packages.js +123 -0
  76. package/risk/actions/scripts/shell.js +165 -0
  77. package/risk/actions/uses.js +76 -0
  78. package/risk/actions/workflow.js +110 -0
  79. package/risk/files/hosts.js +6 -6
  80. package/risk/files/readability.js +23 -17
  81. package/risk/files/runs-code.js +18 -6
  82. package/risk/files/script.js +7 -3
  83. package/risk/files/stamps.js +8 -7
  84. package/risk/index.d.ts +68 -1
  85. package/risk/index.js +6 -2
  86. package/risk/npm/assess.js +13 -8
  87. package/risk/npm/compare.js +34 -10
  88. package/risk/npm/dependencies.js +11 -0
  89. package/risk/npm/files.js +413 -2
  90. package/risk/npm/tarball.js +56 -73
  91. package/server-managed/agent-merges.ts +198 -0
  92. package/server-managed/app-agents.ts +30 -0
  93. package/server-managed/avatar-store.ts +1 -0
  94. package/server-managed/commit-coauthors.ts +83 -0
  95. package/server-managed/commit-lockfiles.ts +62 -0
  96. package/server-managed/comparison.ts +41 -0
  97. package/server-managed/db-methods.ts +29 -10
  98. package/server-managed/db-neon.ts +30 -2
  99. package/server-managed/db-schema.ts +9 -2
  100. package/server-managed/db.ts +7 -0
  101. package/server-managed/dependabot-access.ts +60 -0
  102. package/server-managed/dependabot-alerts.ts +185 -0
  103. package/server-managed/dependency-review-comments.ts +97 -0
  104. package/server-managed/dependency-reviews.ts +65 -15
  105. package/server-managed/github-accounts.ts +33 -0
  106. package/server-managed/github-actions.ts +71 -0
  107. package/server-managed/github-app.ts +23 -9
  108. package/server-managed/github-errors.ts +2 -0
  109. package/server-managed/github-link-oauth.ts +69 -0
  110. package/server-managed/github-links.ts +204 -0
  111. package/server-managed/github-oauth.ts +5 -2
  112. package/server-managed/github-users.ts +30 -0
  113. package/server-managed/http.ts +1219 -81
  114. package/server-managed/index.ts +6 -2
  115. package/server-managed/locked-packages.ts +48 -16
  116. package/server-managed/lockfile-scope.ts +49 -0
  117. package/server-managed/npm-delta-worker.js +23 -0
  118. package/server-managed/npm-delta.ts +173 -0
  119. package/server-managed/npm-insights.ts +20 -11
  120. package/server-managed/npm-kept.ts +51 -4
  121. package/server-managed/npm-loads.ts +20 -13
  122. package/server-managed/npm-packages.ts +24 -64
  123. package/server-managed/pretty-print.ts +14 -9
  124. package/server-managed/public-workspace.ts +26 -8
  125. package/server-managed/pull-comment-reviews.ts +161 -0
  126. package/server-managed/pull-dependencies.ts +280 -0
  127. package/server-managed/pull-dependency-reviews.ts +318 -0
  128. package/server-managed/pull-reviews.ts +205 -0
  129. package/server-managed/release-commits.ts +84 -0
  130. package/server-managed/repository-actions.ts +209 -0
  131. package/server-managed/repository-agents.ts +220 -0
  132. package/server-managed/repository-browser.ts +54 -13
  133. package/server-managed/repository-changes.ts +284 -0
  134. package/server-managed/repository-code.ts +52 -0
  135. package/server-managed/repository-dependency-reviews.ts +135 -0
  136. package/server-managed/repository-history.ts +445 -0
  137. package/server-managed/repository-issues.ts +181 -0
  138. package/server-managed/repository-pulse.ts +432 -0
  139. package/server-managed/repository-risk-read.ts +165 -0
  140. package/server-managed/repository-risk.ts +116 -106
  141. package/server-managed/reviewed-dependencies.ts +149 -0
  142. package/server-managed/session.ts +9 -5
  143. package/server-managed/sql.ts +1 -1
  144. package/server-managed/storage-stores.ts +5 -0
  145. package/server-managed/team-app.ts +54 -14
  146. package/server-managed/workspace-shares.ts +4 -1
  147. package/server.ts +11 -0
  148. package/out/chunk-2KBF5TZ7.js +0 -2
  149. package/out/chunk-35VE6CDH.js +0 -2
  150. package/out/chunk-3BZ7JERB.js +0 -1
  151. package/out/chunk-4F6JSF57.js +0 -3
  152. package/out/chunk-4XCDFGDP.js +0 -10
  153. package/out/chunk-7RU5EDRO.js +0 -226
  154. package/out/chunk-A7M4GV7M.js +0 -1
  155. package/out/chunk-CLPKNNC7.js +0 -51
  156. package/out/chunk-CMQA4DM6.js +0 -1
  157. package/out/chunk-CVVJAXVY.js +0 -2
  158. package/out/chunk-DIJJNTRH.js +0 -10
  159. package/out/chunk-GJ4IUCNS.js +0 -1
  160. package/out/chunk-GNXOKIGP.js +0 -80
  161. package/out/chunk-GZ4MS3TE.js +0 -13
  162. package/out/chunk-IU3CCINR.js +0 -3
  163. package/out/chunk-K3X5EJBQ.js +0 -2
  164. package/out/chunk-KEX2ATEW.js +0 -2
  165. package/out/chunk-KYCNJVB7.js +0 -1
  166. package/out/chunk-LAAB5SZT.js +0 -2
  167. package/out/chunk-NWZ4PVPN.js +0 -1
  168. package/out/chunk-OSIMGFUF.js +0 -1
  169. package/out/chunk-RFZQQXSG.js +0 -17
  170. package/out/chunk-RSHMVYNS.js +0 -1
  171. package/out/chunk-TZ5KB2Y5.js +0 -10
  172. package/out/chunk-UNTRGE26.js +0 -2
  173. package/out/chunk-UXNUAIIC.js +0 -33
  174. package/out/chunk-UYAXVRVL.js +0 -1
  175. package/out/chunk-V3ME6FI5.js +0 -2
  176. package/out/chunk-V5O7M2YT.js +0 -1
  177. package/out/chunk-VIHGO77Y.js +0 -1
  178. package/out/chunk-VQJCXGIY.js +0 -7
  179. package/out/chunk-W4J3MGLB.js +0 -1
  180. package/out/chunk-WGG7KQUH.js +0 -1
  181. package/out/chunk-WZDTZE64.js +0 -1
  182. package/out/chunk-XOCAPDCM.js +0 -1
  183. package/out/chunk-XYFXXB2G.js +0 -10
  184. package/out/chunk-YVZH5SHP.js +0 -1
  185. package/out/chunk-ZBBOBQ43.js +0 -1
  186. package/out/chunk-ZMQQCGF6.js +0 -2
@@ -0,0 +1,149 @@
1
+ // The reviewed dependencies a host keeps beyond the app, as it registers
2
+ // them before the server is assembled (server.ts registerReviewedDependencies):
3
+ // an async method telling whether a dependency, as a lockfile locks it, is
4
+ // reviewed there (`lookup`), and, optionally, one reading the reviews of
5
+ // dependencies an open pull request's comments make (`parseComments`,
6
+ // pull-comment-reviews.ts), and one formatting one's own dependency reviews
7
+ // as a comment, for a copy button to give (`formatDependencyReviews`,
8
+ // dependency-review-comments.ts). Registered, they replace the commit a repository
9
+ // is reviewed since (pull-dependency-reviews.ts): a version passes by the
10
+ // reviews its teams publish, and those its pull request's comments make, as
11
+ // many as the repository requires, or by `lookup`'s saying so, and is
12
+ // otherwise unreviewed. A patched version (pnpm's patchedDependencies)
13
+ // passes by `lookup`, or reviews its comments make of its patch, alone, the
14
+ // app reviewing no patch yet. Those comments' reviews count for the pull
15
+ // request alone, as it is open: the host's to keep, as it is merged, among
16
+ // those `lookup` tells of.
17
+ //
18
+ // Each version is asked of `lookup` once a request, as many at once as
19
+ // ASK_AT_ONCE, its answer kept in this instance KEPT_MS; one it fails to
20
+ // answer is unreviewed, and counted so (`unasked`).
21
+ import { type Lru, lru } from './lru.ts'
22
+
23
+ export type ReviewedLockfile = 'npm' | 'pnpm' | 'yarn1'
24
+ // A dependency as a lockfile locks it: npm's always with its integrity (one
25
+ // with none is never reviewed, nor asked of), its shasum where the lockfile
26
+ // has one (Yarn 1's, after a tarball's `#`), and the hash of its patch where
27
+ // it is patched (pnpm's).
28
+ export interface ReviewedDependency {
29
+ ecosystem: 'npm'
30
+ lockfile: ReviewedLockfile
31
+ name: string
32
+ version: string
33
+ integrity?: string
34
+ shasum?: string
35
+ patchHash?: string
36
+ }
37
+ export type ReviewVerdict = 'accept' | 'accept-with-nits' | 'request-changes' | 'reject'
38
+ // A comment of an open pull request's, as `parseComments` is given it: one
39
+ // on the pull request (`comment`), a review's own (`review`, whatever its
40
+ // state, a comment's alone too), or one on a line of its diff
41
+ // (`review-comment`); by its author's GitHub login; when it was written, and
42
+ // last edited (`editedAt`) where it was.
43
+ export interface ReviewComment {
44
+ kind: 'comment' | 'review' | 'review-comment'
45
+ id: string
46
+ author: string | null
47
+ body: string
48
+ state?: string
49
+ createdAt: string
50
+ editedAt?: string
51
+ url: string
52
+ }
53
+ // What `parseComments` reads: the pull request, its comments, and the
54
+ // dependencies: every one the lockfiles its review holds to add that can be
55
+ // reviewed and `lookup` doesn't pass, and the others still wanting reviews.
56
+ export interface ParseCommentsInput { repository: string; number: number; comments: ReviewComment[]; dependencies: ReviewedDependency[] }
57
+ // A review its comments make, by `user` (a GitHub login, one of their
58
+ // authors'), of each of `dependencies`, with its verdict, as of `time` (when
59
+ // it was made, an ISO date or milliseconds; that user's latest comment's
60
+ // where none is told).
61
+ export interface CommentReview {
62
+ user: string
63
+ time?: string | number
64
+ dependencies: { ecosystem?: 'npm'; name: string; version: string; integrity: string; patchHash?: string; status: ReviewVerdict }[]
65
+ }
66
+ // A version a dependency review is of, or compared it with: its package's
67
+ // name, its version and integrity, and its tarball's shasum and its patch's
68
+ // hash where they are known (a lockfile's, pnpm's patched dependencies').
69
+ export interface ReviewedVersion { name: string; version: string; integrity: string; shasum?: string; patchHash?: string }
70
+ // One of one's own published dependency reviews, as `formatDependencyReviews`
71
+ // is given it: the version it is of (`new`) and the one it compared it with
72
+ // (`old`, none where it read its package whole); its verdict (`status`);
73
+ // whether it read every file (`coverage`, `full`, else `partial`); its final
74
+ // comment; each file's verdict and comment (`files`, of those with either);
75
+ // by whom (`reviewer`, their GitHub login), and when it was published.
76
+ export interface DependencyReviewEntry {
77
+ ecosystem: 'npm'
78
+ old: ReviewedVersion | null
79
+ new: ReviewedVersion
80
+ status: ReviewVerdict
81
+ coverage: 'full' | 'partial'
82
+ comment: string
83
+ files: { path: string; status: ReviewVerdict | null; comment: string }[]
84
+ reviewer: string
85
+ publishedAt: number
86
+ }
87
+ // What `formatDependencyReviews` formats: one's own reviews, never another's,
88
+ // and the pull request whose versions they are of, where its copy asks.
89
+ export interface FormatDependencyReviewsInput { reviews: DependencyReviewEntry[]; pullRequest?: { repository: string; number: number } }
90
+ export interface ReviewedDependencies {
91
+ lookup(dependency: ReviewedDependency): Promise<boolean>
92
+ parseComments?(input: ParseCommentsInput): Promise<CommentReview[]>
93
+ // The comment to copy, none where it is empty, null or undefined.
94
+ formatDependencyReviews?(input: FormatDependencyReviewsInput): Promise<string | null | undefined>
95
+ }
96
+
97
+ const ASK_AT_ONCE = 16
98
+ const KEPT_MS = 60_000
99
+
100
+ let registered: ReviewedDependencies | null = null
101
+ let assembled = false
102
+ // Each lookup's answers, apart.
103
+ const answers = new WeakMap<ReviewedDependencies['lookup'], Lru<{ at: number; reviewed: boolean }>>()
104
+
105
+ // Registers the host's reviewed dependencies, once, before the server is
106
+ // assembled (init()): a server already assembled keeps what it was given.
107
+ export function registerReviewedDependencies(reviewed: ReviewedDependencies): void {
108
+ const optional = (method: unknown) => method === undefined || typeof method === 'function'
109
+ if (typeof reviewed?.lookup !== 'function' || !optional(reviewed.parseComments) || !optional(reviewed.formatDependencyReviews)) {
110
+ throw new TypeError('registerReviewedDependencies: expected { lookup, parseComments?, formatDependencyReviews? }, async methods')
111
+ }
112
+ if (assembled) throw new Error('registerReviewedDependencies: call it before init(), as the server takes them as it is assembled')
113
+ if (registered) throw new Error('registerReviewedDependencies: reviewed dependencies are registered already')
114
+ registered = { lookup: reviewed.lookup.bind(reviewed), ...reviewed.parseComments ? { parseComments: reviewed.parseComments.bind(reviewed) } : {},
115
+ ...reviewed.formatDependencyReviews ? { formatDependencyReviews: reviewed.formatDependencyReviews.bind(reviewed) } : {} }
116
+ }
117
+
118
+ // The reviewed dependencies registered, as a server takes them once it is
119
+ // assembled; none registers after.
120
+ export function assembledReviewedDependencies(): ReviewedDependencies | null {
121
+ assembled = true
122
+ return registered
123
+ }
124
+
125
+ // Which of `dependencies`, by their keys, `lookup` passes, and how many it
126
+ // failed to answer.
127
+ export async function askReviewed(lookup: ReviewedDependencies['lookup'], dependencies: Map<string, ReviewedDependency>): Promise<{ passed: Set<string>; unasked: number }> {
128
+ const passed = new Set<string>()
129
+ let unasked = 0
130
+ const pending = [...dependencies]
131
+ const kept = answers.get(lookup) ?? answers.set(lookup, lru(100_000)).get(lookup)!
132
+ await Promise.all(Array.from({ length: Math.min(ASK_AT_ONCE, pending.length) }, async () => {
133
+ for (let next = pending.pop(); next; next = pending.pop()) {
134
+ const [key, dependency] = next
135
+ const known = kept.get(key)
136
+ let answer = known && Date.now() - known.at < KEPT_MS ? known.reviewed : null
137
+ if (answer === null) {
138
+ try { answer = await lookup({ ...dependency }) === true } catch (error) {
139
+ unasked++
140
+ console.warn('reviewed dependencies: failed to answer', `${dependency.name}@${dependency.version}`, error instanceof Error ? error.message : error)
141
+ continue
142
+ }
143
+ kept.set(key, { at: Date.now(), reviewed: answer })
144
+ }
145
+ if (answer) passed.add(key)
146
+ }
147
+ }))
148
+ return { passed, unasked }
149
+ }
@@ -13,13 +13,15 @@ import { hashToken, randomToken } from './crypto.ts'
13
13
  // `cookieName` when cookies are Secure.
14
14
  export const STATE_COOKIE = 'dvstate'
15
15
 
16
- // Base names for GitHub issue authorization state (github-issue-oauth.ts) and
17
- // an administrator's read-only view as another user (view-as.ts).
16
+ // Base names for GitHub issue authorization state (github-issue-oauth.ts),
17
+ // linking another GitHub account (github-link-oauth.ts) and an
18
+ // administrator's read-only view as another user (view-as.ts).
18
19
  export const ISSUE_STATE_COOKIE = 'dvissuestate'
20
+ export const LINK_STATE_COOKIE = 'dvlinkstate'
19
21
  export const VIEW_COOKIE = 'dvview'
20
22
  // The session cookie cannot share a name with these, or one would overwrite
21
23
  // or be read as the other.
22
- export const RESERVED_COOKIES: readonly string[] = [STATE_COOKIE, ISSUE_STATE_COOKIE, VIEW_COOKIE]
24
+ export const RESERVED_COOKIES: readonly string[] = [STATE_COOKIE, ISSUE_STATE_COOKIE, LINK_STATE_COOKIE, VIEW_COOKIE]
23
25
 
24
26
  // `__Host-`-prefix the base name when cookies are Secure (the prefix mandates
25
27
  // Secure + Path=/ + no Domain, which `buildCookie` already emits).
@@ -55,13 +57,15 @@ export function parseCookies(header: string | undefined): Map<string, string> {
55
57
 
56
58
  // Mint a session for a freshly-authenticated user. Upserts the identity, then
57
59
  // stores the session keyed by the cookie token's hash. Returns the cookie to
58
- // set and the CSRF token (also persisted on the row).
60
+ // set and the CSRF token (also persisted on the row), or null for an account
61
+ // a user linked to theirs, which can neither sign in nor register.
59
62
  export async function createSession(
60
63
  config: ManagedConfig, db: ManagedDb, user: ManagedUser, now: number,
61
- ): Promise<{ setCookie: string; csrfToken: string; userId: string }> {
64
+ ): Promise<{ setCookie: string; csrfToken: string; userId: string } | null> {
62
65
  const token = randomToken()
63
66
  const csrfToken = randomToken()
64
67
  const userId = await db.upsertUser(user, now, config.initialAdminGithubId)
68
+ if (userId == null) return null
65
69
  await db.createSession({ id: hashToken(token), userId, csrfToken, expiresAt: now + config.sessionTtlMs }, now)
66
70
  const setCookie = buildCookie(config.sessionCookieName, token, {
67
71
  maxAgeS: Math.floor(config.sessionTtlMs / 1000), secure: config.cookieSecure, sameSite: 'Lax',
@@ -23,7 +23,7 @@ const SINGLE_STATEMENTS = new Set(['getReport', 'getBundle', 'getStorageRow', 'g
23
23
  'listReports', 'listBundles', 'listReadableBundleIds', 'listAllRepos', 'listSelectedRepos', 'listRepoScopesForUser', 'listTriage',
24
24
  'getFeedState', 'getWorkspaceShareFeedState', 'claimMaintenanceLease', 'finishMaintenanceLease', 'cacheRepoDefaultBranch', 'releaseBundleBuildLease',
25
25
  'getUpstreamCacheEntry', 'getUpstreamCacheEntries', 'setUpstreamCacheEntry', 'setUpstreamCacheEntries', 'ensureSessionUploadKey',
26
- 'listGithubCommits', 'listGithubCommitTags', 'setGithubCommits'])
26
+ 'listGithubCommits', 'listGithubCommitTags', 'setGithubCommits', 'getAvatarKey', 'listUsersByAccount'])
27
27
  // Session presence and upload keys change only their own rows.
28
28
  const UNLOCKED_WRITES = new Set(['sessionWithUser', 'viewSessionWithUser', 'ensureSessionUploadKey'])
29
29
 
@@ -61,6 +61,11 @@ function avatarStore(objects: ObjectStorage, sidecar: boolean): AvatarStore {
61
61
  if (split < 0 || split > 100) throw new Error('Invalid cached avatar')
62
62
  return { contentType: bytes.subarray(0, split).toString(), bytes: bytes.subarray(split + 1) }
63
63
  },
64
+ async delete(value) {
65
+ const path = `avatars/${id(value)}`
66
+ await objects.delete(path)
67
+ if (sidecar) await objects.delete(`${path}.type`)
68
+ },
64
69
  }
65
70
  }
66
71
 
@@ -4,6 +4,18 @@
4
4
  // loads. Results are keyed by the catalog's own content keys: an unchanged
5
5
  // catalog reuses them, and every change that can move a result also changes
6
6
  // the catalog revision that clients refresh on.
7
+ //
8
+ // The same read names the reports a restricted reader would find nothing in:
9
+ // under -deps or -security the catalog leaves those out of the team's list.
10
+ // Readers with both permissions keep every report, empty or not.
11
+ //
12
+ // A team that is not App also says how many of its reports open in App mode
13
+ // on their own, and its findings for the badge it collapses to. None is as
14
+ // uniform as an App team, and collapses like one. A team mixing App and non-App
15
+ // reports stays expanded, and so does one whose reports each open as App but
16
+ // whose combined workspace does not (contradicting verdicts, say): it has App
17
+ // findings, and only expanded does it show that they fail to combine.
18
+ import { reportEntries } from '@preventive/report'
7
19
  import { type TeamAppMetadata, managedTeamAppMetadata } from '../common/managed/team-app.js'
8
20
  import type { BlobStore } from './blob-store.ts'
9
21
  import type { ManagedDb, TeamReportAccessSnapshot, UserTeam } from './db.ts'
@@ -11,12 +23,14 @@ import type { WorkspaceShareSnapshot } from './workspace-shares.ts'
11
23
  import { TeamReportsError, loadTeamReportsResponse } from './team-reports.ts'
12
24
 
13
25
  export type { TeamAppMetadata }
26
+ export interface TeamWorkspaceSummary { app: TeamAppMetadata; empty: readonly string[] }
27
+ const NO_APP: TeamWorkspaceSummary = { app: { appMode: false }, empty: [] }
14
28
 
15
29
  const CACHE_MAX_ENTRIES = 4096
16
30
  // Each workspace build already reads four reports at a time.
17
31
  const CLASSIFY_CONCURRENCY = 2
18
- const caches = new WeakMap<ManagedDb, Map<string, TeamAppMetadata>>()
19
- const pendingJobs = new WeakMap<ManagedDb, Map<string, Promise<TeamAppMetadata | null>>>()
32
+ const caches = new WeakMap<ManagedDb, Map<string, TeamWorkspaceSummary>>()
33
+ const pendingJobs = new WeakMap<ManagedDb, Map<string, Promise<TeamWorkspaceSummary | null>>>()
20
34
 
21
35
  // Published reports with their content keys, and the team's own key (access,
22
36
  // scopes and links). Equal keys describe the same filtered workspace, whoever
@@ -29,28 +43,40 @@ function published(team: UserTeam) {
29
43
  return team.reports.filter(report => report.visible !== false)
30
44
  }
31
45
 
32
- function remember(db: ManagedDb, key: string, app: TeamAppMetadata): TeamAppMetadata {
46
+ function remember(db: ManagedDb, key: string, summary: TeamWorkspaceSummary): TeamWorkspaceSummary {
33
47
  let cache = caches.get(db)
34
48
  if (!cache) { cache = new Map(); caches.set(db, cache) }
35
49
  cache.delete(key)
36
- cache.set(key, app)
50
+ cache.set(key, summary)
37
51
  for (const oldest of cache.keys()) {
38
52
  if (cache.size <= CACHE_MAX_ENTRIES) break
39
53
  cache.delete(oldest)
40
54
  }
41
- return app
55
+ return summary
56
+ }
57
+
58
+ function isLinks(data: unknown): boolean {
59
+ const record = data as { source?: unknown; links?: unknown } | null
60
+ return record?.source === 'links' && Array.isArray(record.links)
61
+ }
62
+
63
+ // A links document counts by its links, every other report by its findings.
64
+ function hasContent(data: unknown): boolean {
65
+ if (isLinks(data)) return (data as { links: unknown[] }).links.length > 0
66
+ return (reportEntries(data) ?? []).some(entry => !Array.isArray(entry) || entry.length > 0)
42
67
  }
43
68
 
44
69
  // Null when the team cannot be classified right now: its catalog went stale,
45
70
  // the access snapshot is gone, or storage failed. The sidebar then keeps the
46
- // team expanded, and a later catalog asks again. Oversized and unreadable
47
- // workspaces never have an App view, so they are remembered as such.
71
+ // team expanded and lists every report, and a later catalog asks again.
72
+ // Oversized and unreadable workspaces never have an App view, so they are
73
+ // remembered as such, hiding nothing.
48
74
  async function classify(db: ManagedDb, store: BlobStore, key: string, team: UserTeam,
49
- snapshot: () => Promise<TeamReportAccessSnapshot | null>): Promise<TeamAppMetadata | null> {
75
+ snapshot: () => Promise<TeamReportAccessSnapshot | null>): Promise<TeamWorkspaceSummary | null> {
50
76
  const cached = caches.get(db)?.get(key)
51
77
  if (cached) return remember(db, key, cached)
52
78
  const ids = new Set(published(team).map(report => report.id))
53
- if (ids.size === 0) return remember(db, key, { appMode: false })
79
+ if (ids.size === 0) return remember(db, key, NO_APP)
54
80
  let jobs = pendingJobs.get(db)
55
81
  if (!jobs) { jobs = new Map(); pendingJobs.set(db, jobs) }
56
82
  const existing = jobs.get(key)
@@ -60,11 +86,18 @@ async function classify(db: ManagedDb, store: BlobStore, key: string, team: User
60
86
  // A catalog read before a concurrent publication names another report set.
61
87
  if (!access?.teamId || access.reports.length !== ids.size || access.reports.some(report => !ids.has(report.id))) return null
62
88
  const body = JSON.parse((await loadTeamReportsResponse(db, store, access)).toString('utf8')) as {
63
- reports: { filename: string; data: unknown }[]; links?: string[][]
89
+ reports: { id: string; filename: string; data: unknown }[]; links?: string[][]
64
90
  }
65
- return remember(db, key, managedTeamAppMetadata(Object.assign(body.reports, { links: body.links ?? [] })))
91
+ const restricted = access.reports.some(report => !report.permissions.dependencies || !report.permissions.security)
92
+ const empty = restricted ? body.reports.filter(report => !hasContent(report.data)).map(report => report.id) : []
93
+ const links = body.links ?? []
94
+ const app = managedTeamAppMetadata(Object.assign(body.reports, { links }), { countFindings: true })
95
+ if (app.appMode) return remember(db, key, { app, empty })
96
+ const appReports = body.reports.filter(report => !isLinks(report.data)
97
+ && managedTeamAppMetadata(Object.assign([report], { links })).appMode).length
98
+ return remember(db, key, { app: { ...app, appReports }, empty })
66
99
  })().catch((error: unknown) => {
67
- if (error instanceof TeamReportsError && [413, 422].includes(error.status)) return remember(db, key, { appMode: false })
100
+ if (error instanceof TeamReportsError && [413, 422].includes(error.status)) return remember(db, key, NO_APP)
68
101
  if (!(error instanceof TeamReportsError)) console.warn('managed: team App classification failed:', error)
69
102
  return null
70
103
  })
@@ -72,10 +105,17 @@ async function classify(db: ManagedDb, store: BlobStore, key: string, team: User
72
105
  try { return await job } finally { jobs.delete(key) }
73
106
  }
74
107
 
108
+ // The catalog's view of a classified team: its App classification, and its
109
+ // reports without the ones its restricted reader would find empty.
110
+ export function withWorkspaceSummary<T extends { reports: readonly { id: string }[] }>(team: T, summary: TeamWorkspaceSummary | null | undefined) {
111
+ const empty = new Set(summary?.empty)
112
+ return { ...team, app: summary?.app ?? null, reports: team.reports.filter(report => !empty.has(report.id)) }
113
+ }
114
+
75
115
  // Classify a user's catalog, keyed by teamAppKey. `known` carries results from
76
116
  // an earlier read of the same request, so only changed teams are classified.
77
117
  export async function teamAppStates(db: ManagedDb, store: BlobStore, sessionId: string, teams: readonly UserTeam[],
78
- known: ReadonlyMap<string, TeamAppMetadata | null> = new Map()): Promise<Map<string, TeamAppMetadata | null>> {
118
+ known: ReadonlyMap<string, TeamWorkspaceSummary | null> = new Map()): Promise<Map<string, TeamWorkspaceSummary | null>> {
79
119
  const states = new Map(known)
80
120
  const missing = [...new Map(teams.map(team => [teamAppKey(team), team])).entries()].filter(([key]) => !states.has(key))
81
121
  let next = 0
@@ -89,6 +129,6 @@ export async function teamAppStates(db: ManagedDb, store: BlobStore, sessionId:
89
129
  }
90
130
 
91
131
  // A public link's workspace is its own access snapshot.
92
- export function sharedTeamApp(db: ManagedDb, store: BlobStore, snapshot: WorkspaceShareSnapshot): Promise<TeamAppMetadata | null> {
132
+ export function sharedTeamApp(db: ManagedDb, store: BlobStore, snapshot: WorkspaceShareSnapshot): Promise<TeamWorkspaceSummary | null> {
93
133
  return classify(db, store, JSON.stringify(['share', teamAppKey(snapshot.team)]), snapshot.team, () => Promise.resolve(snapshot))
94
134
  }
@@ -145,15 +145,18 @@ export function workspaceShareMethods(db: ManagedSql): WorkspaceShareStore {
145
145
  const links = await linkRevision(db)
146
146
  const bundleRows = (await q.bundles.all(team.id) as (Omit<ManagedBundle, 'buildConditions'> & { buildConditions: string | null; repoFullName: string })[])
147
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']
148
149
  return {
149
150
  user: { id: `share:${tokenHash}`, login: 'public', name: 'Public workspace', avatarUrl: null, role: 'view' },
150
151
  teamId: team.id,
151
152
  linkRevision: links,
152
153
  permissions,
153
- repositories: await q.repositories.all(team.id) as WorkspaceShareSnapshot['repositories'],
154
+ repositories,
154
155
  reports: rows.map(row => ({ id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
155
156
  repo: { github: row.github, directory: row.directory }, permissions })),
156
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 ?? '')),
157
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,
158
161
  cacheKey: JSON.stringify([row.sha256, row.github, row.directory, row.filename, permissions, links]) })),
159
162
  bundles: bundleRows.map(row => ({ id: row.id, slug: row.slug, integrity: row.integrity,
package/server.ts CHANGED
@@ -2,6 +2,17 @@ import { parseArgs } from 'node:util'
2
2
  import type { Server } from 'node:http'
3
3
  import { startServer } from './server-common/standalone.ts'
4
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
+
5
16
  const modes = ['e2e', 'managed', 'managed-e2e', 'e2e-managed'] as const
6
17
  export type ServerMode = typeof modes[number]
7
18
 
@@ -1,2 +0,0 @@
1
- var n=`.managed-comment{container:managed-comment / inline-size;white-space:normal}.managed-comment-row{display:grid;grid-template-columns:minmax(0,max-content) minmax(0,1fr) auto;align-items:start;gap:.35rem .65rem;line-height:1.5}.managed-comment-author{grid-column:1;grid-row:1;display:flex;align-items:center;gap:.45rem;min-width:0;max-width:14rem;& strong{overflow:hidden;text-overflow:ellipsis;font-weight:600}}.managed-comment-avatar{position:relative;display:inline-flex;align-items:center;justify-content:center;flex:none;width:1.3rem;height:1.3rem;overflow:hidden;border-radius:50%;background:rgb(from var(--muted) r g b / .15);color:var(--muted);font-size:.65rem;font-weight:600;& img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}& img[hidden]{display:none}}.managed-comment-body{grid-column:2;grid-row:1;min-width:0;white-space:pre-wrap;overflow-wrap:anywhere;& a{color:var(--accent);text-decoration:none}& a:hover{text-decoration:underline}& a.comment-self-ref{text-decoration:underline dotted;text-underline-offset:2px}& a.comment-self-ref:hover{text-decoration:underline solid}}.managed-comment-time{color:var(--muted);font-size:.7rem;font-weight:400;white-space:nowrap;text-align:right}.managed-comment-meta{grid-column:3;grid-row:1;justify-self:end;display:flex;align-items:center;gap:.5rem;min-height:1.3rem}.managed-comment-actions{display:flex;align-items:center;gap:.15rem}.managed-comment-row:not(:has(.managed-comment-author)){grid-template-columns:minmax(0,1fr) auto;.managed-comment-body{grid-column:1}.managed-comment-meta{grid-column:2}}@container managed-comment (max-width: 28rem){.managed-comment-row:has(.managed-comment-meta){grid-template-columns:minmax(0,max-content) minmax(0,1fr);.managed-comment-meta{grid-column:1 / -1;grid-row:1}.managed-comment-author,.managed-comment-body{grid-row:2}}.managed-comment-row:not(:has(.managed-comment-author)){grid-template-columns:minmax(0,1fr)}.managed-comment-author{max-width:9rem}}
2
- `;export{n as a};
@@ -1,2 +0,0 @@
1
- import{a as n}from"./chunk-DIJJNTRH.js";import{c as t,g as i}from"./chunk-2XUEZHIP.js";import{c as d,l as s,m}from"./chunk-V5O7M2YT.js";var c=[["manage","Overview"],["manage-bundles","Bundles"],["manage-scans","Scans"],["manage-reports","Reports"],["manage-deduplication","Deduplication","admin"],["manage-repos","Repositories","admin"],["admin-users","Users","admin"],["manage-teams","Teams","admin"],["manage-import","Import","admin"],["manage-links","Links"],["manage-history","History"]];function h(e,r,l=!1){return t`<header class="manage-header"><nav class="manage-nav" aria-label="Management pages">${c.filter(([a,,o])=>(o==null||o===r)&&(a!=="manage-links"||l)).map(([a,o])=>t`<button type="button" aria-current="${e===a?"page":i}" @click="${()=>{document.dispatchEvent(new CustomEvent("managed-admin-navigate",{detail:{view:a},bubbles:!0,composed:!0}))}}">${o}</button>`)}</nav></header>`}function v(e){if(e==="bundle")return n(d);if(e==="report"||e==="preview")return n(m);if(e==="scan")return n(s);let r={users:"M10.75 4.75a2.75 2.75 0 1 1-5.5 0 2.75 2.75 0 0 1 5.5 0ZM3 14v-1.5A3.5 3.5 0 0 1 6.5 9h3a3.5 3.5 0 0 1 3.5 3.5V14",team:"M8.5 5a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0ZM1.5 14v-1.5A3.5 3.5 0 0 1 5 9h2a3.5 3.5 0 0 1 3.5 3.5V14M11 2.75a2.5 2.5 0 0 1 0 4.5m1 2a3 3 0 0 1 2.5 3V14",repo:"M3 12.5V3a1.5 1.5 0 0 1 1.5-1.5H13v13H4.5a1.5 1.5 0 0 1 0-3H13M7 1.5v6l2-1.5 2 1.5v-6",history:"M2 5a6.25 6.25 0 1 1-.25 5M1.5 1.5V5H5M8 4.5V8l3 1.5",arrow:"M3 8h10M9 4l4 4-4 4",show:"M1 8s2.5-4.5 7-4.5S15 8 15 8s-2.5 4.5-7 4.5S1 8 1 8Zm9 0a2 2 0 1 0-4 0 2 2 0 0 0 4 0",hide:"m2 2 12 12M6 3.75A8 8 0 0 1 8 3.5c4.5 0 7 4.5 7 4.5a12 12 0 0 1-2 2.5M3.5 5A13 13 0 0 0 1 8s2.5 4.5 7 4.5a8 8 0 0 0 3-.6",download:"M8 2v8M5 7l3 3 3-3M2 11v3h12v-3",upload:"M8 11V2m-3 3 3-3 3 3M2 10v4h12v-4"};return t`<svg class="${e==="repo"?"repo-icon":i}" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${r[e]??r.repo}"></path></svg>`}var w=`:host{display:flex;flex-direction:column;position:relative;padding:0 clamp(1rem,3vw,3rem) 3rem;color:var(--text);container-type:inline-size;font-family:var(--sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);font-size:.875rem;line-height:1.5;--panel:color-mix(in srgb, var(--bg) 50%, var(--surface));--panel-hover:color-mix(in srgb, var(--text) 3%, var(--panel))}*,*:before,*:after{box-sizing:border-box}button,summary,select,label,legend,h1,h2,h3,.section-heading,.ui-hint,.page-intro>.intro,.count,.result-count,.repository-count,.breadcrumb-separator,.list-head,.report-list-head,.history-head,.bundle-group-head,.drop-copy,.local-import-copy,.location-field>span,.empty:not(.error,[role=alert]),.msg:not(.error,[role=alert]),.local-import-message:not(.error,[role=status]),.data-empty,.revision-empty{cursor:default;user-select:none;-webkit-user-select:none}input:not([type=checkbox],[type=radio]),textarea,[contenteditable],pre,code,.mono{cursor:text;user-select:text;-webkit-user-select:text}.wrap{width:100%;max-width:76rem;margin:0 auto;flex:1;min-height:0;overflow-y:auto;overscroll-behavior:none;scrollbar-gutter:stable}.manage-header{position:sticky;top:0;z-index:2}.wrap:has(>.manage-list){display:flex;flex-direction:column;overflow:hidden;scrollbar-gutter:auto}.wrap:has(>.manage-list)>:not(.manage-list){flex:none}.manage-list{position:relative;flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable}.manage-nav button{flex:0 0 auto;position:relative;min-height:3.7rem;padding:.9rem 0;border:0;border-radius:0;color:var(--muted);background:none;font-size:.8rem;font-weight:500;white-space:nowrap}.manage-nav button:hover,.manage-nav button[aria-current=page]{color:var(--text)}.manage-nav button[aria-current=page]:after{content:"";position:absolute;bottom:.6rem;inset-inline:0;height:2px;background:var(--accent);border-radius:var(--ui-radius, 2px)}.manage-nav button:focus-visible{outline-offset:-3px}.head{display:flex;align-items:center;flex-wrap:wrap;gap:.7rem;margin:0 0 .5rem}h1,.head h1{margin:0;font-size:1.9rem;font-weight:600;letter-spacing:-.045em;line-height:1.2}.count{display:inline-flex;align-items:center;justify-content:center;min-width:1.6rem;padding:.12rem .5rem;border:1px solid var(--border);border-radius:6px;color:var(--muted);background:var(--surface);font-size:.73rem;font-weight:500;font-variant-numeric:tabular-nums}button,input,select{font:inherit}button,select,input[type=checkbox]{cursor:default}button,a,input,select{-webkit-app-region:no-drag}button,a{transition:background .15s,color .15s,border-color .15s}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:3px}button:disabled,select:disabled{opacity:.45}input:not([type=checkbox]),select{min-width:0;min-height:2.25rem;padding:.45rem .65rem;border:1px solid var(--border);border-radius:var(--ui-radius, 7px);background:var(--bg);color:var(--text);font-size:.8rem}input::placeholder{color:var(--muted)}input[type=checkbox]{accent-color:var(--accent)}.btn,.drop-browse{display:inline-flex;align-items:center;justify-content:center;gap:.45rem;min-height:2.25rem;height:auto;padding:.45rem .8rem;border:1px solid var(--border);border-radius:var(--ui-radius, 7px);background:var(--surface);color:var(--text);font-size:.8rem;font-weight:500;text-decoration:none;white-space:nowrap}.btn:hover:not(:disabled),.drop-browse:hover:not(:disabled){background:var(--surface-active);border-color:var(--muted)}.btn.primary,.drop-browse{color:var(--bg);background:var(--accent);border-color:var(--accent)}.btn.primary:hover:not(:disabled),.drop-browse:hover:not(:disabled){background:var(--accent);filter:brightness(1.08)}.btn svg,.icon-btn svg{width:1rem;height:1rem}.icon-btn{width:2rem;height:2rem}.avatar{color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,var(--bg));border:1px solid color-mix(in srgb,var(--accent) 16%,var(--border))}.breadcrumb{display:flex;align-items:center;flex-basis:100%;gap:.4rem;margin-bottom:.8rem}.breadcrumb-manage{padding:.1rem 0;border:0;background:transparent;color:var(--muted);font-size:.78rem}.breadcrumb-manage:hover{color:var(--text)}.breadcrumb-separator{color:var(--muted)}.msg{margin:1rem 0;padding:1.1rem 1.25rem;border:1px solid var(--border);border-radius:var(--ui-radius, 9px);color:var(--muted);font-size:.85rem}.msg.error{color:var(--critical);border-color:color-mix(in srgb,var(--critical) 35%,var(--border))}.empty{padding:3.5rem 1.5rem;border:1px dashed var(--border);border-radius:var(--ui-radius, 10px);color:var(--muted);text-align:center}.empty strong{display:block;margin-bottom:.4rem;color:var(--text);font-size:.95rem;font-weight:500}.empty p{margin:0;font-size:.85rem}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}@container (max-width: 35rem){.drop-copy>span{display:none}.manage-nav{gap:1.2rem;margin-bottom:1.75rem}.manage-nav button{min-height:3.25rem}h1,.head h1{font-size:1.65rem}.intro{margin-bottom:1.5rem}}@media(prefers-reduced-motion:reduce){button,a{transition:none}}.collection-toolbar input{flex:1;max-width:26rem;min-width:10rem}.collection-toolbar select{min-width:10rem}.result-count{text-align:right;white-space:nowrap;margin-left:auto;color:var(--muted);font-size:.75rem;font-variant-numeric:tabular-nums}@container (max-width: 35rem){.collection-toolbar input{flex-basis:100%;max-width:none}}.manage-nav{display:flex;gap:1.35rem;align-items:stretch;overflow-x:auto;scrollbar-width:thin;border-bottom:1px solid var(--border);margin-bottom:1.5rem;padding:0 .1rem}.wrap>:not(.manage-header),.manage-nav{-webkit-app-region:no-drag;app-region:no-drag}@media(display-mode:window-controls-overlay){.manage-header{display:flow-root}.manage-header:before{content:"";float:right;width:var(--wco-right-toolbar, 0px);height:var(--wco-titlebar-height)}.manage-nav{width:max-content;max-width:100%;border-bottom:0}.manage-nav button{min-height:var(--wco-titlebar-height);padding-block:.35rem}.manage-nav button[aria-current=page]:after{bottom:0}}.intro{margin:0 0 1.25rem;max-width:46rem;color:var(--muted);font-size:.875rem;line-height:1.65}.collection-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:.75rem;margin-bottom:.85rem}.page-intro{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.5rem 1rem;margin-bottom:1.25rem}.page-intro .intro{margin:0}.page-intro>.btn{margin-block:calc(-.45rem - 1px)}a{cursor:default}.loading-rows{border:1px solid var(--border);border-radius:var(--ui-radius, 10px);overflow:hidden}.loading-row{display:grid;align-content:center;gap:.6rem;min-height:4.25rem;padding:.85rem 1rem}.loading-row+.loading-row{border-top:1px solid var(--border)}.loading-row span{width:40%;height:.65rem;border-radius:3px;background:color-mix(in srgb,var(--muted) 15%,transparent)}.loading-row span+span{width:65%;height:.5rem}.drop-card{display:flex;align-items:center;gap:.75rem;margin-bottom:1rem;padding:.7rem .85rem;border:1px dashed color-mix(in srgb,var(--muted) 35%,var(--border));border-radius:var(--ui-radius, 8px);background:var(--panel)}.drop-icon{display:grid;place-items:center;flex-shrink:0;width:1.8rem;height:1.8rem;color:var(--muted)}.drop-icon svg{width:1.1rem;height:1.1rem}.drop-copy{min-width:0;display:flex;align-items:baseline;flex-wrap:wrap;gap:.2rem .6rem}.drop-copy strong{font-size:.8rem;font-weight:500}.drop-copy>span{color:var(--muted);font-size:.75rem}.drop-browse{min-width:7rem;margin-left:auto;flex-shrink:0}.content-hidden .filename,.content-hidden .report-name,.content-hidden .report-uploader{color:var(--muted)}.status{display:inline-flex;align-items:center;gap:.3rem;width:fit-content;padding:.1rem .4rem;border:1px solid var(--border);border-radius:5px;color:var(--muted);font-size:.65rem}.status:before{content:"";width:.25rem;height:.25rem;border-radius:50%;background:currentcolor}.status.visible{color:var(--green);border-color:rgb(from var(--green) r g b / .2);background:rgb(from var(--green) r g b / .06)}
2
- `;export{h as a,v as b,w as c};
@@ -1 +0,0 @@
1
- function n(t){return typeof t=="string"&&/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(t)?t:null}export{n as a};
@@ -1,3 +0,0 @@
1
- import{b as w,c as f,j as m,l as k}from"./chunk-WZDTZE64.js";import{a as h}from"./chunk-TUII34K2.js";var x=3,y=4,H=/^[\w-]{4}$/u,_=/^[\w-]{8}$/u,u="AAAA",R={report:`deepview/finding-link/report/v1
2
- `,workspace:`deepview/finding-link/workspace/v1
3
- `},p=new Map;function $(t,e){return`${t}\0${e}`}function I(t){return typeof t=="string"&&H.test(t)}async function T(t,e){let n=R[t];if(!n||typeof e!="string"||!e)return null;let i=$(t,e),o=p.get(i);if(o!==void 0)return o;let r;try{let s=await crypto.subtle.digest("SHA-256",h(n+e));r=new Uint8Array(s).slice(0,x).toBase64({alphabet:"base64url",omitPadding:!0})}catch{return null}return p.set(i,r),r}function E(t,e){return typeof e!="string"||!e?null:p.get($(t,e))??null}function N(t,e){let n=[];for(let[i,o]of[["report",t],["workspace",e]]){if(o==null||o===""){n.push(u);continue}if(!I(o))throw new TypeError(`encodeFindingRef: ${i} must be a link hint token, not a name`);n.push(o)}return n.every(i=>i===u)?null:n[0]===u?`w${n[1]}`:n.join("")}function b(t){let e=[t.slice(0,y),t.slice(y)].map(n=>n===u?null:n);return{report:e[0],workspace:e[1]}}function a({id:t,report:e,workspace:n}={}){if(!f(t))throw new TypeError("encodeFindingRef: a persistent finding id is required");let i=[`finding=${encodeURIComponent(t)}`],o=N(e,n);return o&&i.push(`v=${o}`),i.join("&")}function O(t,e){let n=a(t);return typeof location>"u"?`${e??""}#${n}`:`${location.origin}${e??location.pathname}#${n}`}function A(t){let e=typeof t=="string"?t:typeof location>"u"?"":location.hash;if(!e)return null;let n=e.replace(/^#/u,"");if(!n)return null;let i={id:null,report:null,workspace:null};for(let o of n.split("&")){let r=o.indexOf("=");if(r<0)continue;let s=o.slice(0,r),c=o.slice(r+1);if(!c)continue;if(s==="v"){if(c.length===5&&c[0]==="w"&&I(c.slice(1))){let g=c.slice(1);Object.assign(i,{report:null,workspace:g===u?null:g});continue}if(!_.test(c))continue;Object.assign(i,b(c));continue}if(s!=="finding"||c.length>w*3)continue;let d;try{d=decodeURIComponent(c)}catch{continue}f(d)&&(i.id=d)}return i.id?i:null}function U(t,{managed:e=!1}={}){if(typeof location>"u")return null;let n;try{n=new URL(t)}catch{return null}if(n.href!==t||n.protocol!==location.protocol||n.host!==location.host||n.username||n.password)return null;let i=k(n);if(i?.finding)return{id:i.finding.id,fragment:a(i.finding),path:m(i)};if(e&&i?.view!=="home"&&!/\/(?:index|view)\.html$/u.test(n.pathname))return null;let o=A(n.hash);if(!o)return null;let r=a(o);return{id:o.id,fragment:r,path:`/#${r}`}}function l(t,e){if(typeof t!="string")return!1;let n=t.replaceAll("\\","/");return n.startsWith(`${e}/`)||n.includes(`/${e}/`)}function C(t){let e=!1;for(let n of t){for(let i of n.groups??[])for(let o of i){if(l(o.file,"node_modules"))return"node_modules";l(o.file,"vendor")&&(e=!0)}for(let i of Object.keys(n.tree??{})){if(l(i,"node_modules"))return"node_modules";l(i,"vendor")&&(e=!0)}}return e?"vendor":"dependencies"}export{l as a,C as b,T as c,E as d,a as e,O as f,A as g,U as h};
@@ -1,10 +0,0 @@
1
- import{d as o}from"./chunk-XYFXXB2G.js";import{b as s,c as i}from"./chunk-C4Z6ZBXV.js";import{g as r}from"./chunk-2XUEZHIP.js";var u=s(class extends i{constructor(){super(...arguments),this.key=r}render(t,e){return this.key=t,e}update(t,[e,m]){return e!==this.key&&(o(t),this.key=e),m}});export{u as a};
2
- /*! Bundled license information:
3
-
4
- lit-html/directives/keyed.js:
5
- (**
6
- * @license
7
- * Copyright 2021 Google LLC
8
- * SPDX-License-Identifier: BSD-3-Clause
9
- *)
10
- */