@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.
- package/common/managed/finding-detail.d.ts +2 -1
- package/common/managed/finding-detail.js +42 -11
- package/common/managed/finding-values.d.ts +2 -0
- package/common/managed/finding-values.js +44 -0
- package/common/managed/lite-name.d.ts +2 -0
- package/common/managed/lite-name.js +20 -0
- package/common/managed/npm-packages.d.ts +2 -0
- package/common/managed/npm-packages.js +18 -1
- package/common/managed/routes.js +19 -7
- package/common/managed/team-app.d.ts +1 -0
- package/common/managed/team-app.js +17 -1
- package/out/activity.js +4 -4
- package/out/{chunk-75YLHBCM.js → chunk-2K6UN3PC.js} +1 -1
- package/out/{chunk-HTBYHSPG.js → chunk-CHRBUGVN.js} +1 -1
- package/out/{chunk-4XMP7YVH.js → chunk-FMXWXDR4.js} +1 -1
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/{chunk-2WDZPXVL.js → chunk-IMM274IW.js} +1 -1
- package/out/{chunk-FKBXNUW4.js → chunk-KOGNQDSJ.js} +1 -1
- package/out/chunk-LJ7XRXFU.js +52 -0
- package/out/{chunk-PBZPYIG7.js → chunk-MYGJBYYJ.js} +1 -1
- package/out/chunk-NCN3M5EP.js +3 -0
- package/out/chunk-OGAYMEER.js +232 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +26 -26
- package/out/compare.js +14 -14
- package/out/dialogs.js +31 -31
- package/out/graph.js +1 -1
- package/out/index.html +7 -7
- package/out/scan.js +2 -2
- package/out/view.css +1 -1
- package/out/view.js +11 -11
- package/package.json +8 -1
- package/server-managed/agent-merges.ts +1 -1
- package/server-managed/app-agents.ts +4 -2
- package/server-managed/commit-lockfiles.ts +7 -4
- package/server-managed/comparison.ts +13 -2
- package/server-managed/db-neon.ts +13 -3
- package/server-managed/db.ts +2 -1
- package/server-managed/dependency-review-comments.ts +16 -18
- package/server-managed/dependency-reviews.ts +45 -27
- package/server-managed/github-app.ts +3 -42
- package/server-managed/github-requests.ts +127 -0
- package/server-managed/http.ts +433 -129
- package/server-managed/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +28 -5
- package/server-managed/npm-kept.ts +18 -4
- package/server-managed/npm-patch.ts +325 -0
- package/server-managed/pull-dependencies.ts +9 -6
- package/server-managed/pull-dependency-reviews.ts +19 -13
- package/server-managed/repository-agents.ts +162 -69
- package/server-managed/repository-dependency-reviews.ts +6 -5
- package/server-managed/repository-pulls.ts +114 -58
- package/server-managed/repository-pulse.ts +5 -3
- package/server-managed/repository-stamp.ts +41 -0
- package/server-managed/reviewed-dependencies.ts +4 -5
- package/server-managed/team-app.ts +3 -1
- package/server-managed/team-reports.ts +29 -10
- package/out/chunk-6LNQTKAV.js +0 -52
- package/out/chunk-6S2U2UYG.js +0 -232
- package/out/chunk-FC65TPUB.js +0 -1
- 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
|
+
}
|