@preventive/triage 1.0.0-alpha.50 → 1.0.0-alpha.52

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 (61) hide show
  1. package/common/managed/finding-detail.d.ts +2 -1
  2. package/common/managed/finding-detail.js +42 -11
  3. package/common/managed/finding-values.d.ts +2 -0
  4. package/common/managed/finding-values.js +44 -0
  5. package/common/managed/lite-name.d.ts +2 -0
  6. package/common/managed/lite-name.js +20 -0
  7. package/common/managed/npm-packages.d.ts +2 -0
  8. package/common/managed/npm-packages.js +18 -1
  9. package/common/managed/routes.js +19 -7
  10. package/common/managed/team-app.d.ts +1 -0
  11. package/common/managed/team-app.js +17 -1
  12. package/out/activity.js +4 -4
  13. package/out/{chunk-75YLHBCM.js → chunk-2K6UN3PC.js} +1 -1
  14. package/out/{chunk-HTBYHSPG.js → chunk-CHRBUGVN.js} +1 -1
  15. package/out/{chunk-4XMP7YVH.js → chunk-FMXWXDR4.js} +1 -1
  16. package/out/chunk-HT2SSA3V.js +1 -0
  17. package/out/{chunk-2WDZPXVL.js → chunk-IMM274IW.js} +1 -1
  18. package/out/{chunk-FKBXNUW4.js → chunk-KOGNQDSJ.js} +1 -1
  19. package/out/chunk-LJ7XRXFU.js +52 -0
  20. package/out/{chunk-PBZPYIG7.js → chunk-MYGJBYYJ.js} +1 -1
  21. package/out/chunk-NCN3M5EP.js +3 -0
  22. package/out/chunk-OGAYMEER.js +232 -0
  23. package/out/client-managed-import.js +1 -1
  24. package/out/client-managed.js +26 -26
  25. package/out/compare.js +14 -14
  26. package/out/dialogs.js +31 -31
  27. package/out/graph.js +1 -1
  28. package/out/index.html +7 -7
  29. package/out/scan.js +2 -2
  30. package/out/view.css +1 -1
  31. package/out/view.js +11 -11
  32. package/package.json +8 -1
  33. package/server-managed/agent-merges.ts +1 -1
  34. package/server-managed/app-agents.ts +4 -2
  35. package/server-managed/commit-lockfiles.ts +7 -4
  36. package/server-managed/comparison.ts +13 -2
  37. package/server-managed/db-neon.ts +13 -3
  38. package/server-managed/db.ts +2 -1
  39. package/server-managed/dependency-review-comments.ts +16 -18
  40. package/server-managed/dependency-reviews.ts +45 -27
  41. package/server-managed/github-app.ts +3 -42
  42. package/server-managed/github-requests.ts +127 -0
  43. package/server-managed/http.ts +433 -129
  44. package/server-managed/listed-activity.ts +39 -0
  45. package/server-managed/locked-packages.ts +28 -5
  46. package/server-managed/npm-kept.ts +18 -4
  47. package/server-managed/npm-patch.ts +325 -0
  48. package/server-managed/pull-dependencies.ts +9 -6
  49. package/server-managed/pull-dependency-reviews.ts +19 -13
  50. package/server-managed/repository-agents.ts +162 -69
  51. package/server-managed/repository-dependency-reviews.ts +6 -5
  52. package/server-managed/repository-pulls.ts +114 -58
  53. package/server-managed/repository-pulse.ts +5 -3
  54. package/server-managed/repository-stamp.ts +41 -0
  55. package/server-managed/reviewed-dependencies.ts +4 -5
  56. package/server-managed/team-app.ts +3 -1
  57. package/server-managed/team-reports.ts +29 -10
  58. package/out/chunk-6LNQTKAV.js +0 -52
  59. package/out/chunk-6S2U2UYG.js +0 -232
  60. package/out/chunk-FC65TPUB.js +0 -1
  61. package/out/chunk-UK7RGJT6.js +0 -3
@@ -1,4 +1,9 @@
1
1
  import { createHash } from 'node:crypto'
2
+ import { GithubApiError, githubUnreachable, throwGithubResponseError } from './github-errors.ts'
3
+ import { timed } from './request-timing.ts'
4
+
5
+ const API_VERSION = '2022-11-28'
6
+ export const USER_AGENT = 'deepview-triage'
2
7
 
3
8
  // Only outstanding reads: a completed answer (or failure) is never reused.
4
9
  // Keep transports and credentials separate, including anonymous reads. When
@@ -37,3 +42,125 @@ export async function shareGithubRead<T>(fetchImpl: typeof fetch, request: strin
37
42
  return (entry.shared ? structuredClone(value) : value) as T
38
43
  } finally { pending.delete(key) }
39
44
  }
45
+
46
+ // GitHub's answers to GETs that came with an ETag, by the request (its URL
47
+ // and credential, hashed as outstanding reads are; an installation's token,
48
+ // which the App mints anew every few minutes on every instance, as the
49
+ // installations' one credential, GitHub telling apart which may read what):
50
+ // the next asked again If-None-Match, GitHub checking the credential as
51
+ // ever, and a 304, which it doesn't count against the rate limit, answered
52
+ // by the body kept. Each transport's own, as many as MAX_TAGGED_READS and
53
+ // MAX_TAGGED_BYTES hold, the least recently used first to go; none larger
54
+ // than MAX_TAGGED_READ_BYTES. Those one this process doesn't hold, read of
55
+ // the store kept beyond it (keepTaggedReadsIn), as a serverless instance
56
+ // starting anew holds none, as many as fit it (MAX_STORED_READ_BYTES).
57
+ const MAX_TAGGED_READS = 1000
58
+ const MAX_TAGGED_BYTES = 32 * 1024 * 1024
59
+ const MAX_TAGGED_READ_BYTES = 1024 * 1024
60
+ const MAX_STORED_READ_BYTES = 256 * 1024
61
+ export type TaggedRead = { etag: string; text: string }
62
+ const taggedReads = new WeakMap<typeof fetch, { reads: Map<string, TaggedRead>; bytes: number }>()
63
+ const taggedOf = (fetchImpl: typeof fetch) => {
64
+ let tagged = taggedReads.get(fetchImpl)
65
+ if (!tagged) taggedReads.set(fetchImpl, tagged = { reads: new Map(), bytes: 0 })
66
+ return tagged
67
+ }
68
+ const requestKey = (request: string) => createHash('sha256').update(request).digest('hex')
69
+ // A GET's request as its answer is kept by: its URL and credential.
70
+ const taggedRequest = (url: string, token: string | null) => JSON.stringify([url, token?.startsWith('ghs_') ? { installation: true } : token])
71
+
72
+ // Where GitHub's tagged answers are kept beyond this process (http.ts
73
+ // registers the database's), by the request's key; its failures are none
74
+ // kept, or none written.
75
+ export interface TaggedStore { read(key: string): Promise<TaggedRead | null>; write(key: string, read: TaggedRead): Promise<void> }
76
+ let taggedStore: TaggedStore | null = null
77
+ export function keepTaggedReadsIn(store: TaggedStore | null): void { taggedStore = store }
78
+
79
+ // The answer kept to `request`, its ETag and body as GitHub wrote it: as
80
+ // this process holds it, else, where a store is kept beyond it, as that
81
+ // keeps it.
82
+ export function taggedRead(fetchImpl: typeof fetch, request: string): TaggedRead | undefined | Promise<TaggedRead | undefined> {
83
+ const { reads } = taggedOf(fetchImpl)
84
+ const key = requestKey(request)
85
+ const read = reads.get(key)
86
+ if (read) { reads.delete(key); reads.set(key, read); return read }
87
+ return taggedStore ? storedTaggedRead(fetchImpl, key, taggedStore) : undefined
88
+ }
89
+ async function storedTaggedRead(fetchImpl: typeof fetch, key: string, store: TaggedStore) {
90
+ const stored = await store.read(key).catch(() => null)
91
+ if (!stored || typeof stored.etag !== 'string' || typeof stored.text !== 'string') return undefined
92
+ holdTaggedRead(fetchImpl, key, stored)
93
+ return stored
94
+ }
95
+
96
+ // `text` kept as GitHub's answer to `request`, by its ETag.
97
+ export async function keepTaggedRead(fetchImpl: typeof fetch, request: string, etag: string, text: string): Promise<void> {
98
+ const key = requestKey(request)
99
+ holdTaggedRead(fetchImpl, key, { etag, text })
100
+ if (text.length <= MAX_STORED_READ_BYTES) await taggedStore?.write(key, { etag, text }).catch(() => {})
101
+ }
102
+ function holdTaggedRead(fetchImpl: typeof fetch, key: string, read: TaggedRead) {
103
+ const tagged = taggedOf(fetchImpl)
104
+ const before = tagged.reads.get(key)
105
+ if (before) { tagged.reads.delete(key); tagged.bytes -= before.text.length }
106
+ if (read.text.length > MAX_TAGGED_READ_BYTES) return
107
+ tagged.reads.set(key, read)
108
+ tagged.bytes += read.text.length
109
+ for (const [oldest, held] of tagged.reads) {
110
+ if (tagged.reads.size <= MAX_TAGGED_READS && tagged.bytes <= MAX_TAGGED_BYTES) break
111
+ tagged.reads.delete(oldest)
112
+ tagged.bytes -= held.text.length
113
+ }
114
+ }
115
+
116
+ // One GitHub API call returning parsed JSON, optionally Bearer-authed by a
117
+ // user token, App JWT, or installation token, `body` sent as JSON (a GraphQL
118
+ // query's, given longer to answer). Network / non-2xx / malformed fold into a
119
+ // GithubApiError (401 passes through for the caller to handle). Identical GETs
120
+ // share an outstanding read through its body; one GitHub answered with an
121
+ // ETag is asked again If-None-Match, its body kept answering a 304
122
+ // (taggedRead). POSTs only opt in for read-only GraphQL
123
+ // queries (repository-history.ts), never for mutations or token mints.
124
+ export function githubJson(url: string, token: string | null, fetchImpl: typeof fetch, method: 'GET' | 'POST' = 'GET', body?: unknown,
125
+ readOnly = method === 'GET'): Promise<unknown> {
126
+ if (readOnly) return shareGithubRead(fetchImpl, JSON.stringify([url, token, method, body]), () => githubJson(url, token, fetchImpl, method, body, false))
127
+ const request = method === 'GET' && body === undefined ? taggedRequest(url, token) : null
128
+ // Each call told in its request's Server-Timing (request-timing.ts).
129
+ return timed('gh', async () => {
130
+ const held = request === null ? undefined : taggedRead(fetchImpl, request)
131
+ const tagged = held instanceof Promise ? await held : held
132
+ let res: Response
133
+ const started = Date.now()
134
+ try {
135
+ res = await fetchImpl(url, {
136
+ method,
137
+ signal: AbortSignal.timeout(body === undefined ? 10_000 : 20_000),
138
+ redirect: 'error',
139
+ headers: {
140
+ ...(token == null ? {} : { authorization: `Bearer ${token}` }), 'accept': 'application/vnd.github+json',
141
+ ...(body === undefined ? {} : { 'content-type': 'application/json' }), 'user-agent': USER_AGENT, 'x-github-api-version': API_VERSION,
142
+ ...(tagged ? { 'if-none-match': tagged.etag } : {}),
143
+ },
144
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
145
+ })
146
+ } catch (error) { throw githubUnreachable(error, started) }
147
+ if (res.status === 401 && token) tokenRejected(token, fetchImpl)
148
+ if (res.status === 304 && tagged) return JSON.parse(tagged.text) as unknown
149
+ if (!res.ok) await throwGithubResponseError(res)
150
+ // The answer's body is read within the same timeout: one not read whole is
151
+ // GitHub not reached, as one not answered is; only one that isn't JSON is
152
+ // malformed.
153
+ // Kept only whole: never a 202's, GitHub still working out its answer.
154
+ const etag = request === null || res.status !== 200 ? null : res.headers?.get('etag') ?? null
155
+ let text: string | null = null, value: unknown
156
+ try {
157
+ if (etag === null) return await res.json()
158
+ text = await res.text()
159
+ value = JSON.parse(text)
160
+ } catch (error) {
161
+ throw error instanceof SyntaxError ? new GithubApiError(502, 'github-malformed') : githubUnreachable(error, started)
162
+ }
163
+ await keepTaggedRead(fetchImpl, request!, etag, text)
164
+ return value
165
+ })
166
+ }