@preventive/triage 1.0.0-alpha.46 → 1.0.0-alpha.47
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/agent-tasks.ts +5 -3
- package/common/managed/github-agents.ts +28 -10
- package/out/activity.js +4 -0
- package/out/chunk-5AHIQFFG.js +36 -0
- package/out/chunk-6ANSHRFQ.js +6 -0
- package/out/chunk-6WF3KNTL.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/{chunk-7XRR4OFU.js → chunk-FH24VHSJ.js} +1 -1
- package/out/chunk-NOZJULWS.js +52 -0
- package/out/{chunk-OKRLLE5I.js → chunk-PWFUOLZK.js} +1 -1
- package/out/chunk-S6U2J62U.js +232 -0
- package/out/{chunk-G24A42V6.js → chunk-SPEFPML6.js} +1 -1
- package/out/chunk-UENXT2HK.js +3 -0
- package/out/chunk-VL43BTSO.js +1 -0
- package/out/{chunk-3PS4KWD2.js → chunk-WCPJDAYA.js} +1 -1
- package/out/chunk-ZCS4UYHI.js +1 -0
- package/out/chunk-ZGNMDGSB.js +1 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +22 -22
- package/out/client-sync.js +2 -2
- package/out/compare.js +14 -14
- package/out/dialogs.js +40 -40
- package/out/graph.js +11 -11
- package/out/index.html +11 -11
- package/out/scan.js +2 -2
- package/out/terminal.js +6 -6
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +2 -1
- package/risk/index.d.ts +4 -2
- package/risk/npm/assess.js +15 -4
- package/risk/npm/registry.js +6 -1
- package/server-managed/activity.ts +24 -11
- package/server-managed/app-agents.ts +10 -21
- package/server-managed/bundle-build-worker.js +7 -3
- package/server-managed/bundle-build.ts +5 -3
- package/server-managed/bundle-cache.ts +9 -3
- package/server-managed/bundle.ts +3 -4
- package/server-managed/commit-coauthors.ts +5 -9
- package/server-managed/db-methods.ts +286 -149
- package/server-managed/db-neon.ts +11 -4
- package/server-managed/http.ts +75 -33
- package/server-managed/link-reports.ts +5 -1
- package/server-managed/npm-kept.ts +19 -5
- package/server-managed/npm-loads.ts +27 -15
- package/server-managed/npm-packages.ts +18 -11
- package/server-managed/npm-risk.ts +64 -17
- package/server-managed/pretty-print.ts +45 -12
- package/server-managed/report-store.ts +16 -11
- package/server-managed/repository-agents.ts +1 -1
- package/server-managed/repository-browser.ts +4 -2
- package/server-managed/repository-code.ts +28 -11
- package/server-managed/repository-pulse.ts +1 -1
- package/server-managed/solidity-entry-points.ts +5 -3
- package/server-managed/sql-postgres.ts +3 -0
- package/server-managed/sql.ts +2 -1
- package/server-managed/storage-db.ts +11 -3
- package/server-managed/storage-encryption.ts +7 -4
- package/server-managed/storage.ts +4 -2
- package/server-managed/team-feed.ts +3 -1
- package/server-managed/team-reports.ts +6 -1
- package/server-managed/uploads.ts +29 -9
- package/out/chunk-4UFAMJAX.js +0 -1
- package/out/chunk-BOIXMUU5.js +0 -3
- package/out/chunk-CC4OM6TZ.js +0 -3
- package/out/chunk-JKGQ3O7O.js +0 -6
- package/out/chunk-LE5ZWLG4.js +0 -235
- package/out/chunk-MSION3G4.js +0 -36
- package/out/chunk-S6GIXWQW.js +0 -52
- package/out/chunk-V3KWNNMY.js +0 -1
- package/out/chunk-WQ74ZBZF.js +0 -7
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
// instance, or this one restarted, asks npm's downloads API, which answers
|
|
15
15
|
// some 40 lookups a minute, none of them again that week, each removed a
|
|
16
16
|
// day past it (npm-kept.ts pruneKept); a private one's in memory.
|
|
17
|
-
// - `risk/npm/packages`, `risk/npm/
|
|
18
|
-
//
|
|
17
|
+
// - `risk/npm/packages`, `risk/npm/old-versions`, `risk/npm/public` and
|
|
18
|
+
// `risk/npm/advisories`, which risk asks again after an hour: a public
|
|
19
|
+
// package's in memory and in the database too, read from memory first, so
|
|
20
|
+
// that a cold instance, as a serverless one starts, reads no whole document
|
|
21
|
+
// of npm's again that one before it read within the hour; by a hash of
|
|
22
|
+
// their type and key, as a dependency's spec runs to any length, each
|
|
23
|
+
// removed a day on, and one past 256 KiB as JSON (a package's deprecated
|
|
24
|
+
// versions, its messages and all, run long) in memory alone. A private
|
|
25
|
+
// one's in memory.
|
|
19
26
|
// - `npm/tarballs`: none asked, as tarballs are read as the viewer's loads
|
|
20
27
|
// read them, kept where they keep them (npm-loads.ts loadNpmTarball), and
|
|
21
28
|
// an assessment kept needs none.
|
|
@@ -69,11 +76,20 @@ function remember(key: string, value: unknown) {
|
|
|
69
76
|
while (memory.size > MEMORY_ENTRIES || memoryLength > MEMORY_LENGTH) forget(memory.keys().next().value!)
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
//
|
|
79
|
+
const digest = (type: string, key: string) => createHash('sha256').update(`${type}\0${key}`).digest('hex')
|
|
80
|
+
const hashed = (type: string, key: string) => `risk/${digest(type, key)}`
|
|
81
|
+
// What of a public package is kept in the database by its key, and for how
|
|
82
|
+
// long its downloads are: risk's week, and a day more.
|
|
75
83
|
const IN_DATABASE = new Set(['risk/npm/versions', 'risk/npm/downloads'])
|
|
76
84
|
const DOWNLOADS_KEPT_MS = 8 * 24 * 60 * 60_000
|
|
85
|
+
// What of a public package risk asks again after an hour, kept in memory and
|
|
86
|
+
// in the database (HOURLY_PREFIX), each a day, none past HOURLY_LENGTH as
|
|
87
|
+
// JSON there.
|
|
88
|
+
const HOURLY = new Set(['risk/npm/packages', 'risk/npm/old-versions', 'risk/npm/public', 'risk/npm/advisories'])
|
|
89
|
+
const HOURLY_PREFIX = 'risk/hourly/'
|
|
90
|
+
const HOURLY_KEPT_MS = 24 * 60 * 60_000
|
|
91
|
+
const HOURLY_LENGTH = 256 * 1024
|
|
92
|
+
const databaseKey = (type: string, key: string) => HOURLY.has(type) ? `${HOURLY_PREFIX}${digest(type, key)}` : `${type}/${key}`
|
|
77
93
|
|
|
78
94
|
export interface NpmRiskStores {
|
|
79
95
|
db: UpstreamCacheStore
|
|
@@ -82,10 +98,13 @@ export interface NpmRiskStores {
|
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
// A cache that reads and writes many of a type at once too, as risk's
|
|
85
|
-
// readying of downloads asks (npmReadyDownloads)
|
|
86
|
-
//
|
|
87
|
-
//
|
|
101
|
+
// readying of downloads asks (npmReadyDownloads), and reads records of any
|
|
102
|
+
// types at once, as risk reads a package's record and its versions'
|
|
103
|
+
// (registry.js npmPackageFacts): one read and one write of the database each,
|
|
104
|
+
// not one a key, as a request's queries take turns on its connection
|
|
105
|
+
// (db-neon.ts).
|
|
88
106
|
export interface NpmRiskCache extends CacheStore {
|
|
107
|
+
readEach(entries: [type: string, key: string][]): Promise<unknown[]>
|
|
89
108
|
readMany(type: string, keys: string[]): Promise<Map<string, unknown>>
|
|
90
109
|
writeMany(type: string, entries: [key: string, value: unknown][]): Promise<void>
|
|
91
110
|
}
|
|
@@ -94,13 +113,32 @@ export interface NpmRiskCache extends CacheStore {
|
|
|
94
113
|
// that what is kept of it can be shared.
|
|
95
114
|
export function npmRiskCache({ db, npmCache, debug }: NpmRiskStores, isPublic: boolean): NpmRiskCache {
|
|
96
115
|
const warn = (what: string, error: unknown) => { if (debug) console.warn(`managed: risk cache ${what} failed:`, error) }
|
|
116
|
+
const memoryKey = (type: string, key: string) => `${isPublic ? '' : 'private\0'}${type}\0${key}`
|
|
117
|
+
// Whether a record of `type` is read of the database: unless it is one
|
|
118
|
+
// kept in memory too (HOURLY) and there.
|
|
119
|
+
const ofDatabase = (type: string, key: string) => isPublic && (IN_DATABASE.has(type) || HOURLY.has(type) && !memory.has(memoryKey(type, key)))
|
|
120
|
+
// A record as the database keeps it, read: an hourly one kept in memory
|
|
121
|
+
// too, for the next to ask.
|
|
122
|
+
const readOf = (type: string, key: string, row: string | null | undefined) => {
|
|
123
|
+
const value: unknown = row == null ? null : JSON.parse(row)
|
|
124
|
+
if (value !== null && HOURLY.has(type)) remember(memoryKey(type, key), value)
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
97
127
|
const cache: NpmRiskCache = {
|
|
128
|
+
async readEach(entries) {
|
|
129
|
+
const asked = new Set(entries.flatMap(([type, key], i) => ofDatabase(type, key) ? [i] : []))
|
|
130
|
+
let rows = new Map<string, string>()
|
|
131
|
+
if (asked.size > 0) {
|
|
132
|
+
try { rows = await db.getUpstreamCacheEntries([...asked].map(i => databaseKey(...entries[i]!))) } catch (error) { warn('read', error) }
|
|
133
|
+
}
|
|
134
|
+
return Promise.all(entries.map(([type, key], i) => {
|
|
135
|
+
if (!asked.has(i)) return cache.read(type, key)
|
|
136
|
+
try { return readOf(type, key, rows.get(databaseKey(type, key))) } catch (error) { warn('read', error); return null }
|
|
137
|
+
}))
|
|
138
|
+
},
|
|
98
139
|
async readMany(type, keys) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const found = await db.getUpstreamCacheEntries(keys.map(key => `${type}/${key}`))
|
|
102
|
-
return new Map(keys.map(key => { const value = found.get(`${type}/${key}`); return [key, value === undefined ? null : JSON.parse(value)] }))
|
|
103
|
-
} catch (error) { warn('read', error); return new Map() }
|
|
140
|
+
const values = await cache.readEach(keys.map(key => [type, key]))
|
|
141
|
+
return new Map(keys.map((key, i) => [key, values[i]]))
|
|
104
142
|
},
|
|
105
143
|
async writeMany(type, entries) {
|
|
106
144
|
if (!isPublic || !IN_DATABASE.has(type)) { for (const [key, value] of entries) await cache.write(type, key, value); return }
|
|
@@ -112,22 +150,31 @@ export function npmRiskCache({ db, npmCache, debug }: NpmRiskStores, isPublic: b
|
|
|
112
150
|
if (type === 'npm/tarballs') return null
|
|
113
151
|
try {
|
|
114
152
|
if (isPublic && type === 'risk/npm/assessments' && npmCache) return JSON.parse((await buffer((await npmCache.open(hashed(type, key))).stream)).toString('utf8'))
|
|
115
|
-
if (
|
|
153
|
+
if (ofDatabase(type, key)) return readOf(type, key, await db.getUpstreamCacheEntry(databaseKey(type, key)))
|
|
116
154
|
} catch (error) { if (!(error instanceof CacheMissError)) warn('read', error); return null }
|
|
117
|
-
return memory.get(
|
|
155
|
+
return memory.get(memoryKey(type, key))?.value ?? null
|
|
118
156
|
},
|
|
119
157
|
async write(type, key, value) {
|
|
120
158
|
if (type === 'npm/tarballs') return
|
|
159
|
+
if (isPublic && HOURLY.has(type)) remember(memoryKey(type, key), value)
|
|
121
160
|
try {
|
|
122
161
|
if (isPublic && type === 'risk/npm/assessments' && npmCache) { await npmCache.put(hashed(type, key), Buffer.from(JSON.stringify(value))); return }
|
|
123
162
|
if (isPublic && IN_DATABASE.has(type)) {
|
|
124
163
|
const at = Date.now()
|
|
125
|
-
await db.setUpstreamCacheEntry(
|
|
164
|
+
await db.setUpstreamCacheEntry(databaseKey(type, key), JSON.stringify(value), at)
|
|
126
165
|
if (type === 'risk/npm/downloads') await pruneKept(db, `${type}/`, DOWNLOADS_KEPT_MS, at, error => { warn('prune', error) })
|
|
127
166
|
return
|
|
128
167
|
}
|
|
168
|
+
if (isPublic && HOURLY.has(type)) {
|
|
169
|
+
const json = JSON.stringify(value)
|
|
170
|
+
if (json.length > HOURLY_LENGTH) return
|
|
171
|
+
const at = Date.now()
|
|
172
|
+
await db.setUpstreamCacheEntry(databaseKey(type, key), json, at)
|
|
173
|
+
await pruneKept(db, HOURLY_PREFIX, HOURLY_KEPT_MS, at, error => { warn('prune', error) })
|
|
174
|
+
return
|
|
175
|
+
}
|
|
129
176
|
} catch (error) { warn('write', error); return }
|
|
130
|
-
remember(
|
|
177
|
+
remember(memoryKey(type, key), value)
|
|
131
178
|
},
|
|
132
179
|
}
|
|
133
180
|
return cache
|
|
@@ -45,6 +45,10 @@ const MAX_KEPT_DETAILS_BYTES = 256 * 1024 * 1024
|
|
|
45
45
|
// Files hashed at once: one at a time, each waits its own round trip to the
|
|
46
46
|
// thread pool; all at once, every one's job is held together.
|
|
47
47
|
const HASHES_AT_ONCE = 32
|
|
48
|
+
// Kept copies a stream reads ahead of the line it is sending (readAhead):
|
|
49
|
+
// one at a time, each waits its own storage round trip; each read whole, as
|
|
50
|
+
// its line will send it.
|
|
51
|
+
const KEPT_AHEAD = 8
|
|
48
52
|
|
|
49
53
|
export class PrettyError extends Error {
|
|
50
54
|
status: number
|
|
@@ -166,6 +170,29 @@ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<Ope
|
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
// A file's line from its kept copy, `open` opening it, or null where none is
|
|
174
|
+
// kept.
|
|
175
|
+
async function keptLine(request: PrettyRequest, open: () => Promise<OpenedBlob | null>): Promise<PrettyLine | null> {
|
|
176
|
+
const copy = await open()
|
|
177
|
+
return copy && prettyLine(request, () => Promise.resolve(copy))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Each of `items` with what `read` reads of it, in their order, up to
|
|
181
|
+
// KEPT_AHEAD of them read at once ahead of the one iterated, each answered
|
|
182
|
+
// (or failing) in its turn. One iterated no further starts no more; those
|
|
183
|
+
// under way settle unread.
|
|
184
|
+
async function* readAhead<T, R>(items: readonly T[], read: (item: T) => Promise<R>): AsyncGenerator<[T, R]> {
|
|
185
|
+
const reads: Promise<R>[] = []
|
|
186
|
+
for (const [i, item] of items.entries()) {
|
|
187
|
+
while (reads.length < Math.min(items.length, i + KEPT_AHEAD)) {
|
|
188
|
+
const reading = read(items[reads.length]!)
|
|
189
|
+
reading.catch(() => {})
|
|
190
|
+
reads.push(reading)
|
|
191
|
+
}
|
|
192
|
+
yield [item, await reads[i]!]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
// The files that changed between two sides, pretty-printed: each side's
|
|
170
197
|
// formattable files the other has another text at the path of, or, where it
|
|
171
198
|
// has none there, none of their text at a path of its own (a file renamed, or
|
|
@@ -174,8 +201,9 @@ async function prettyLine({ path, hash }: PrettyRequest, copy: () => Promise<Ope
|
|
|
174
201
|
// the viewer diffs but where it pairs files otherwise (among copies of one
|
|
175
202
|
// text, across packages), whose copies it asks for alone. As they are
|
|
176
203
|
// iterated, in path order, the base's first where both have one: those kept,
|
|
177
|
-
// at once; then the rest, formatted one after
|
|
178
|
-
// the others (needsPrettyPrint), as the viewer
|
|
204
|
+
// at once, a few read ahead (readAhead); then the rest, formatted one after
|
|
205
|
+
// another, the minified before the others (needsPrettyPrint), as the viewer
|
|
206
|
+
// prints those unasked.
|
|
179
207
|
export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGenerator<ComparedLine> {
|
|
180
208
|
// Each side's texts at the paths the other hasn't: where a file moved has its own.
|
|
181
209
|
const moved = sides.map((own, i) => new Set([...own.hashes].filter(([path]) => !sides[1 - i]!.hashes.has(path)).map(([, hash]) => hash)))
|
|
@@ -185,9 +213,8 @@ export function compared(sides: readonly [ComparedSide, ComparedSide]): AsyncGen
|
|
|
185
213
|
}).toSorted((a, b) => (a.file.path < b.file.path ? -1 : a.file.path > b.file.path ? 1 : 0))
|
|
186
214
|
return (async function* () {
|
|
187
215
|
const unkept: typeof changed = []
|
|
188
|
-
for (const { own, file } of changed) {
|
|
189
|
-
|
|
190
|
-
if (copy) yield { side: own.side, ...await prettyLine(file, () => Promise.resolve(copy)) }
|
|
216
|
+
for await (const [{ own, file }, line] of readAhead(changed, item => keptLine(item.file, () => item.own.kept(item.file)))) {
|
|
217
|
+
if (line) yield { side: own.side, ...line }
|
|
191
218
|
else unkept.push({ own, file })
|
|
192
219
|
}
|
|
193
220
|
const minified = new Map(unkept.map(({ file }) => [file, needsPrettyPrint(file.path, file.text)]))
|
|
@@ -325,15 +352,18 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
325
352
|
return body
|
|
326
353
|
})
|
|
327
354
|
}
|
|
355
|
+
// The copy kept for a public version's file, or null where none is (and
|
|
356
|
+
// for a private version's, which none is kept for).
|
|
357
|
+
const npmKept = (doc: NpmVersionDocument, request: PrettyRequest) => (doc.private ? Promise.resolve(null) : kept(() => npm.open(prettyFile(request))))
|
|
328
358
|
// A version's file pretty-printed, `read` reading its text (and checking
|
|
329
359
|
// it is the file named): from the copy kept for a public version, else
|
|
330
360
|
// made, and kept for a public one.
|
|
331
361
|
async function npmCopy(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
|
|
362
|
+
return await npmKept(doc, request) ?? npmMade(doc, request, read)
|
|
363
|
+
}
|
|
364
|
+
// The file pretty-printed from the text `read` reads, kept for a public version.
|
|
365
|
+
async function npmMade(doc: NpmVersionDocument, request: PrettyRequest, read: () => Promise<string>): Promise<OpenedBlob> {
|
|
332
366
|
const file = prettyFile(request)
|
|
333
|
-
if (!doc.private) {
|
|
334
|
-
try { return await npm.open(file) }
|
|
335
|
-
catch (err) { if (!(err instanceof CacheMissError)) throw err }
|
|
336
|
-
}
|
|
337
367
|
return opened(await shared(doc.private ? `npm ${doc.dist.integrity} ${file}` : `npm ${file}`, async () => {
|
|
338
368
|
const body = await prettyBody(await read(), request.extension)
|
|
339
369
|
if (!doc.private) await keep(() => npm.put(file, body))
|
|
@@ -369,11 +399,14 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
369
399
|
},
|
|
370
400
|
// Every file of the version that needs pretty-printing (npmPrintable),
|
|
371
401
|
// read before this resolves; then, as they are iterated, each
|
|
372
|
-
// pretty-printed as `npm` has it, one after another
|
|
402
|
+
// pretty-printed as `npm` has it, one after another, those kept a few
|
|
403
|
+
// read ahead (readAhead).
|
|
373
404
|
async npmAll(doc: NpmVersionDocument): Promise<AsyncGenerator<PrettyLine>> {
|
|
374
405
|
const files = await npmPrintable(doc)
|
|
375
406
|
return (async function* () {
|
|
376
|
-
for (const
|
|
407
|
+
for await (const [file, line] of readAhead(files, request => keptLine(request, () => npmKept(doc, request)))) {
|
|
408
|
+
yield line ?? await prettyLine(file, () => npmMade(doc, file, () => Promise.resolve(file.text)))
|
|
409
|
+
}
|
|
377
410
|
})()
|
|
378
411
|
},
|
|
379
412
|
// Two bundles' sides, or two versions', as a comparison reads them, read
|
|
@@ -392,7 +425,7 @@ export function createPrettyCache(bundles: BundleCacheStorage, npm: CacheStorage
|
|
|
392
425
|
npmSides(base: NpmVersionDocument, other: NpmVersionDocument): Promise<[ComparedSide, ComparedSide]> {
|
|
393
426
|
const sideOf = async (doc: NpmVersionDocument, side: Side): Promise<ComparedSide> => ({ side, ...await sideFiles(await npmFiles(doc), (_, bytes) => npmFileHash(bytes)),
|
|
394
427
|
packageDirs: null,
|
|
395
|
-
kept: request => (doc
|
|
428
|
+
kept: request => npmKept(doc, request),
|
|
396
429
|
made: (request, text) => npmCopy(doc, request, () => Promise.resolve(text)) })
|
|
397
430
|
return Promise.all([sideOf(base, 'base'), sideOf(other, 'other')])
|
|
398
431
|
},
|
|
@@ -32,9 +32,7 @@ export function createReportStore(legacy: BlobStore, compressed: BlobStore,
|
|
|
32
32
|
if (compressedOnly.size >= COMPRESSED_ONLY_MAX) compressedOnly.delete(compressedOnly.values().next().value!)
|
|
33
33
|
compressedOnly.add(id)
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
const stored = await compressed.open(id)
|
|
37
|
-
if (!stored) return null
|
|
35
|
+
function decoded(stored: OpenedBlob): OpenedBlob {
|
|
38
36
|
const stream = createBrotliDecompress()
|
|
39
37
|
// pipeline carries storage/authentication errors into the decoded stream
|
|
40
38
|
// and closes the stored source when a reader leaves before EOF.
|
|
@@ -43,22 +41,29 @@ export function createReportStore(legacy: BlobStore, compressed: BlobStore,
|
|
|
43
41
|
}
|
|
44
42
|
async function open(id: string): Promise<OpenedBlob | null> {
|
|
45
43
|
if (compressedOnly.has(id)) {
|
|
46
|
-
const stored = await
|
|
47
|
-
if (stored) return stored
|
|
44
|
+
const stored = await compressed.open(id)
|
|
45
|
+
if (stored) return decoded(stored)
|
|
48
46
|
compressedOnly.delete(id)
|
|
49
47
|
}
|
|
50
48
|
// Keep the original available until conversion succeeds. Concurrent reads
|
|
51
49
|
// can finish using an already-open stream while another reader converts it.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
// Both copies are asked for at once, the original answering where it is
|
|
51
|
+
// still there: one round trip of storage for a report not known yet, as
|
|
52
|
+
// a cold instance knows none.
|
|
53
|
+
const [original, stored] = await Promise.allSettled([legacy.open(id), compressed.open(id)])
|
|
54
|
+
const unused = (copy: PromiseSettledResult<OpenedBlob | null>) => { if (copy.status === 'fulfilled') copy.value?.stream.destroy() }
|
|
55
|
+
if (original.status === 'rejected') { unused(stored); throw original.reason }
|
|
56
|
+
if (original.value) {
|
|
57
|
+
unused(stored)
|
|
58
|
+
const bytes = await readBytes(original.value)
|
|
55
59
|
await replaceLegacy(id, await encodeBrotli(bytes, 9))
|
|
56
60
|
remember(id)
|
|
57
61
|
return { size: bytes.length, stream: Readable.from([bytes]) }
|
|
58
62
|
}
|
|
59
|
-
|
|
60
|
-
if (stored)
|
|
61
|
-
|
|
63
|
+
if (stored.status === 'rejected') throw stored.reason
|
|
64
|
+
if (!stored.value) return null
|
|
65
|
+
remember(id)
|
|
66
|
+
return decoded(stored.value)
|
|
62
67
|
}
|
|
63
68
|
return {
|
|
64
69
|
async put(id, bytes) {
|
|
@@ -175,7 +175,7 @@ async function agentBranches(owner: string, name: string, prefix: string, token:
|
|
|
175
175
|
// What its answers are kept by, with their cursors (http.ts
|
|
176
176
|
// handleRepositoryAgents): bumped as what a page reads, or how it tells an
|
|
177
177
|
// agent, changes, so that no answer an older server kept is sent for a day.
|
|
178
|
-
export const AGENTS_READ_VERSION =
|
|
178
|
+
export const AGENTS_READ_VERSION = 6
|
|
179
179
|
|
|
180
180
|
// A cursor of the default branch's commits: GitHub names one by a commit
|
|
181
181
|
// and how many on from it, `<oid> <offset>`, a space between.
|
|
@@ -272,8 +272,10 @@ async function repositoryReader(repo: SelectedRepo, userToken: string | null, in
|
|
|
272
272
|
const manifestPath = path ? `${path}/package.json` : 'package.json'
|
|
273
273
|
const manifest = data.find(item => item?.path === manifestPath && item.name === 'package.json' && item.type === 'file' && !item.submodule_git_url
|
|
274
274
|
&& typeof item.size === 'number' && item.size >= 0 && item.size <= MAX_PACKAGE_BYTES && typeof item.sha === 'string' && /^[a-f\d]{40}$/iu.test(item.sha))
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
// Both optional suggestions, read together: neither fails, each
|
|
276
|
+
// answering none where its reads do.
|
|
277
|
+
const [packageEntryPoints, solidity] = await Promise.all([manifest ? readPackageEntryPoints(manifest.sha, path, read) : undefined,
|
|
278
|
+
readSolidityEntryPoints(path, data, read)])
|
|
277
279
|
return { entries, limited: data.length >= 1000, ...(packageEntryPoints ? { packageEntryPoints } : {}),
|
|
278
280
|
...(solidity.paths.length > 0 || solidity.limited ? { solidityEntryPoints: solidity.paths, soliditySuggestionsLimited: solidity.limited } : {}),
|
|
279
281
|
}
|
|
@@ -7,24 +7,36 @@
|
|
|
7
7
|
// active ones (npm-loads.ts sharedLoad).
|
|
8
8
|
import { Buffer } from 'node:buffer'
|
|
9
9
|
import { encodeBrotli } from './brotli.ts'
|
|
10
|
-
import {
|
|
11
|
-
import { type NpmLoad, readTarballFiles, sharedLoad } from './npm-loads.ts'
|
|
10
|
+
import { MAX_NPM_JSON_LENGTH, NpmPackageError, npmFileRows } from './npm-packages.ts'
|
|
11
|
+
import { type NpmLoad, loadPlaces, readTarballFiles, sharedLoad } from './npm-loads.ts'
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
export const
|
|
13
|
+
// What a directory may hold, bounded apart from a package's (npm-packages.ts),
|
|
14
|
+
// as the viewer holds every file in memory: its files' count and bytes.
|
|
15
|
+
export const MAX_REPOSITORY_CODE_FILES = 80_000
|
|
16
|
+
export const MAX_REPOSITORY_CODE_BYTES = 256 * 1024 * 1024
|
|
17
|
+
// The tarball they come in, bounded in proportion as a package's is: its tar
|
|
18
|
+
// stream, headers and padding included, and its entries, as many again as
|
|
19
|
+
// files, as GitHub's has one for each directory too.
|
|
20
|
+
const REPOSITORY_TAR = { maxFiles: MAX_REPOSITORY_CODE_FILES, maxBytes: MAX_REPOSITORY_CODE_BYTES, maxEntries: 2 * MAX_REPOSITORY_CODE_FILES, maxTarBytes: 384 * 1024 * 1024 }
|
|
21
|
+
// What the files may take as the viewer's JSON, written as one string: short
|
|
22
|
+
// of the longest V8 holds (2^29 - 24 characters).
|
|
23
|
+
const MAX_REPOSITORY_CODE_JSON_LENGTH = 384 * 1024 * 1024
|
|
16
24
|
|
|
17
25
|
// Whether a directory's files fit, by GitHub's listing of its tree and every
|
|
18
26
|
// tree under it (repository-browser.ts repositoryTreeFiles): asked before
|
|
19
27
|
// its tarball is, which may run to hundreds of MiB. One GitHub cut short
|
|
20
28
|
// (null) has too many.
|
|
21
|
-
export function repositoryCodeFits(files:
|
|
29
|
+
export function repositoryCodeFits(files: RepositoryCodeFiles | null): files is RepositoryCodeFiles {
|
|
22
30
|
return files !== null && files.files <= MAX_REPOSITORY_CODE_FILES && files.bytes <= MAX_REPOSITORY_CODE_BYTES
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
// A directory's files' count and bytes, as GitHub lists its tree.
|
|
34
|
+
export interface RepositoryCodeFiles { files: number; bytes: number }
|
|
35
|
+
|
|
25
36
|
// The directory read: the repository by its id and name, the commit, the
|
|
26
|
-
// directory (`''` for the root)
|
|
27
|
-
|
|
37
|
+
// directory (`''` for the root), its tree's id, and its files as listed,
|
|
38
|
+
// which weigh its load among the active ones.
|
|
39
|
+
export interface RepositoryCodeInput { repoId: number; repo: string; commit: string; path: string; tree: string; listed: RepositoryCodeFiles }
|
|
28
40
|
|
|
29
41
|
// A package's refusals, as a directory's: past the bounds, or an archive
|
|
30
42
|
// @preventive/archive refuses, as it does one holding a symlink that climbs
|
|
@@ -39,14 +51,19 @@ const directoryError = (err: unknown) => {
|
|
|
39
51
|
// repo, commit, path, tree, files }`, its files as npmFileRows gives them,
|
|
40
52
|
// by their paths in the directory, from `tarball`, the tree's.
|
|
41
53
|
export function loadRepositoryCodeBody(input: RepositoryCodeInput, tarball: () => Promise<Uint8Array>): NpmLoad<Buffer> {
|
|
42
|
-
const { repoId, repo, commit, path, tree } = input
|
|
54
|
+
const { repoId, repo, commit, path, tree, listed } = input
|
|
55
|
+
// Its places among the active loads, by its files as listed, hold their
|
|
56
|
+
// JSON too, which the listing cannot tell, as escaping may grow a text
|
|
57
|
+
// sixfold: as much as a package's may take for each.
|
|
58
|
+
const places = loadPlaces(listed)
|
|
59
|
+
const maxLength = Math.min(MAX_REPOSITORY_CODE_JSON_LENGTH, places * MAX_NPM_JSON_LENGTH)
|
|
43
60
|
return sharedLoad(`repository ${repoId} ${repo} ${commit} ${path} ${tree}`, async () => {
|
|
44
61
|
let files
|
|
45
62
|
// A symlink may reach the repository's root, as a monorepo package's
|
|
46
63
|
// LICENSE links the root's, and no further: the directory's depth, less
|
|
47
64
|
// the level the archive's root already stands above its top directory.
|
|
48
|
-
try { files = npmFileRows(await readTarballFiles(await tarball(), { symlinksAbove: path.split('/').length - 1 })) }
|
|
65
|
+
try { files = npmFileRows(await readTarballFiles(await tarball(), { ...REPOSITORY_TAR, symlinksAbove: path.split('/').length - 1 }), maxLength) }
|
|
49
66
|
catch (err) { throw directoryError(err) }
|
|
50
67
|
return encodeBrotli(Buffer.from(JSON.stringify({ repoId, repo, commit, path, tree, files })))
|
|
51
|
-
}, 'repository-busy')
|
|
68
|
+
}, 'repository-busy', { places })
|
|
52
69
|
}
|
|
@@ -20,7 +20,7 @@ import { ACTOR, type GraphAccount, type HistoryCommit, type HistoryPages, accoun
|
|
|
20
20
|
export const PULSE_WEEKS = [1, 4, 13, 52]
|
|
21
21
|
// What a year counted from its commits holds, its version: kept years of
|
|
22
22
|
// another are counted again.
|
|
23
|
-
export const PULSE_YEAR_VERSION =
|
|
23
|
+
export const PULSE_YEAR_VERSION = 9
|
|
24
24
|
const WEEK_MS = 7 * 24 * 60 * 60_000
|
|
25
25
|
const DAY_MS = 24 * 60 * 60_000
|
|
26
26
|
const MAX_CONTRIBUTORS = 100
|
|
@@ -49,10 +49,12 @@ export async function readSolidityEntryPoints(directory: string, listing: unknow
|
|
|
49
49
|
for (const entry of entries) {
|
|
50
50
|
if (entry.type === 'file' && sourcePath(entry.path) && addSoliditySuggestion(paths, entry.path)) limited = true
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// Both roots' listings asked together, then read in order.
|
|
53
|
+
const listings = roots.map(root => Promise.resolve().then(() => (sourcePath(`${root.path}/Contract.sol`) ? read(`/git/trees/${root.sha}?recursive=1`) : null)))
|
|
54
|
+
for (const listed of listings) listed.catch(() => {})
|
|
55
|
+
for (const [i, root] of roots.entries()) {
|
|
54
56
|
try {
|
|
55
|
-
const data = await
|
|
57
|
+
const data = await listings[i] as { tree?: unknown; truncated?: unknown } | null
|
|
56
58
|
if (!Array.isArray(data?.tree)) continue
|
|
57
59
|
limited ||= data.truncated === true
|
|
58
60
|
for (const entry of data.tree) {
|
|
@@ -10,6 +10,9 @@ export function postgresSql(source: string): { sql: string; names: string[] } {
|
|
|
10
10
|
.replaceAll('INSERT OR IGNORE INTO', 'INSERT INTO')
|
|
11
11
|
.replaceAll('CROSS JOIN', 'JOIN')
|
|
12
12
|
.replaceAll(/\binstr\(/gu, 'strpos(')
|
|
13
|
+
// Rows gathered as one JSON array value, NULL (not '[]') where none are.
|
|
14
|
+
.replaceAll(/\bjson_group_array\(/gu, 'json_agg(')
|
|
15
|
+
.replaceAll(/\bjson_object\(/gu, 'json_build_object(')
|
|
13
16
|
// Quoted literals (including ':') are never placeholders or identifiers.
|
|
14
17
|
sql = sql.split(/('(?:''|[^'])*')/u).map((part, i) => i % 2 ? part : part
|
|
15
18
|
.replaceAll(/\?|(?<!:):([a-zA-Z]\w*)/gu, (_match, name: string | undefined) => {
|
package/server-managed/sql.ts
CHANGED
|
@@ -21,8 +21,9 @@ export interface ManagedSqlDriver extends ManagedSql {
|
|
|
21
21
|
// Lease and cache writes coordinate through their conditional UPSERT/UPDATE alone.
|
|
22
22
|
const SINGLE_STATEMENTS = new Set(['getReport', 'getBundle', 'getStorageRow', 'getStorageEncryption', 'getTeam', 'getComment',
|
|
23
23
|
'userCanReadReport', 'userCanReadRepo', 'userCanReadRepoPath', 'userCanReadBundle', 'reportPermissionsFor', 'listComments', 'listManagedIssues',
|
|
24
|
-
'listTriageHistory', 'listUsers', 'listUserOptions', 'listActivityReports', 'getRepoFullName', 'getSessionWithUser', 'getViewSessionWithUser',
|
|
24
|
+
'listTriageHistory', 'listUsers', 'listUserOptions', 'listActivityReports', 'getRepoFullName', 'getUser', 'getSessionWithUser', 'getViewSessionWithUser',
|
|
25
25
|
'listTriageFixes',
|
|
26
|
+
'sessionWithUser', 'getTeamReportAccessSnapshot', 'getReportAccessSnapshot', 'getReportView', 'getUserGithubId', 'getUserTeamFeedSnapshot', 'getAnnotations', 'getUserTokens',
|
|
26
27
|
'listReports', 'listBundles', 'listReadableBundleIds', 'listAllRepos', 'listSelectedRepos', 'listRepoScopesForUser', 'listTriage',
|
|
27
28
|
'getFeedState', 'getWorkspaceShareFeedState', 'claimMaintenanceLease', 'finishMaintenanceLease', 'cacheRepoDefaultBranch', 'releaseBundleBuildLease',
|
|
28
29
|
'getUpstreamCacheEntry', 'getUpstreamCacheEntries', 'getFirstUpstreamCacheEntries', 'setUpstreamCacheEntry', 'setUpstreamCacheEntries', 'ensureSessionUploadKey',
|
|
@@ -47,10 +47,18 @@ export function unwrapDataKey(key: StorageKey, type: StorageRowKind, row: Pick<S
|
|
|
47
47
|
return value
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// What storageState reads of managed_storage_encryption, as `alias` names it,
|
|
51
|
+
// for a statement reading it with more (storageStateOf).
|
|
52
|
+
export const storageStateColumns = (alias: string) => `${alias}.test_value AS testValue, ${alias}.enabled_at AS enabledAt,
|
|
53
|
+
${alias}.cursor AS cursor, ${alias}.complete AS complete, ${alias}.migrated AS migrated, ${alias}.cleanup_cursor AS cleanupCursor,
|
|
54
|
+
${alias}.cleanup_found AS cleanupFound, ${alias}.cleanup_complete AS cleanupComplete`
|
|
50
55
|
export async function storageState(db: ManagedSql, key: StorageKey | null): Promise<StorageEncryptionState | null> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
return storageStateOf(await db.prepare(`SELECT ${storageStateColumns('e')} FROM managed_storage_encryption e WHERE e.id = 1`).get() as StoredState | undefined, key)
|
|
57
|
+
}
|
|
58
|
+
type StoredState = StorageEncryptionState & { testValue: string }
|
|
59
|
+
// The state storageStateColumns read, none where its row is missing, its
|
|
60
|
+
// test value checked against `key`.
|
|
61
|
+
export function storageStateOf(row: StoredState | undefined, key: StorageKey | null): StorageEncryptionState | null {
|
|
54
62
|
if (!row) return null
|
|
55
63
|
if (!key) throw new Error('Managed storage requires its configured encryption key')
|
|
56
64
|
const plain = unwrapStorageValue(key, 'managed_storage_encryption:1', row.testValue)
|
|
@@ -180,8 +180,10 @@ async function convertStoredReport(raw: RawObjectStorage, db: StorageDb, key: St
|
|
|
180
180
|
|
|
181
181
|
// Startup enables encryption before constructing stores; validate the persisted
|
|
182
182
|
// requirement even when this adapter is opened without a key.
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
// `known` is the state the caller read a moment before, where it found it
|
|
184
|
+
// enabled, so a cold start reads it once.
|
|
185
|
+
async function storagePolicy(db: StorageDb, key: StorageKey | null, known: StorageEncryptionState | null) {
|
|
186
|
+
let enabled = known ?? await db.getStorageEncryption()
|
|
185
187
|
let checkedAt = Date.now()
|
|
186
188
|
return async (fresh = false) => {
|
|
187
189
|
// Enabled is irreversible. Keyless instances cache "off" briefly, while
|
|
@@ -196,8 +198,9 @@ async function storagePolicy(db: StorageDb, key: StorageKey | null) {
|
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
export async function createEncryptedObjectStorage(raw: RawObjectStorage, db: StorageDb, key: StorageKey | null
|
|
200
|
-
|
|
201
|
+
export async function createEncryptedObjectStorage(raw: RawObjectStorage, db: StorageDb, key: StorageKey | null,
|
|
202
|
+
known: StorageEncryptionState | null = null): Promise<ObjectStorage> {
|
|
203
|
+
const mode = await storagePolicy(db, key, known)
|
|
201
204
|
async function open(identity: string): ReturnType<ObjectStorage['open']> {
|
|
202
205
|
logicalKey(identity)
|
|
203
206
|
const state = await mode()
|
|
@@ -31,11 +31,13 @@ export async function openManagedStorage(config: ManagedConfig) {
|
|
|
31
31
|
const raw = config.neonUrl ? await openVercelObjectStorage(config.blobToken!) : createDiskObjectStorage(dirname(config.dbPath))
|
|
32
32
|
// Enable before exposing stores or token writers. Concurrent first starts
|
|
33
33
|
// serialize activation in the database and validate the winning key.
|
|
34
|
-
|
|
34
|
+
const enabled = key ? await db.getStorageEncryption() : null
|
|
35
|
+
if (key && !enabled) {
|
|
35
36
|
if (config.vercelPreview) throw new Error('Storage encryption must be enabled outside a Vercel preview deployment')
|
|
36
37
|
await db.enableStorageEncryption()
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
// Enabled already: that state, not read again.
|
|
40
|
+
const objects = await createEncryptedObjectStorage(raw, db, key, enabled)
|
|
39
41
|
const storage = createManagedStores(objects, !config.neonUrl)
|
|
40
42
|
// Move legacy team link uploads into encrypted SQL before serving catalogs.
|
|
41
43
|
// The DB transaction commits first; a failed blob cleanup cannot lose links.
|
|
@@ -74,7 +74,9 @@ export async function serveUserTeamFeed(res: ServerResponse, deps: ManagedHttpDe
|
|
|
74
74
|
}
|
|
75
75
|
const catalog = await deps.db.getUserTeamFeedSnapshot(sessionId, Date.now())
|
|
76
76
|
checkUser(catalog)
|
|
77
|
-
|
|
77
|
+
// The session read again after the catalog, as the catalog is published
|
|
78
|
+
// next: one statement, as the poll's own check is.
|
|
79
|
+
checkUser(await deps.db.getFeedState(sessionId, Date.now()))
|
|
78
80
|
// Publish catalog state before loading any report blobs. A slow or broken
|
|
79
81
|
// focused report must not hide membership/content changes from the client.
|
|
80
82
|
if (!publish('teams', catalog!.revision)) return
|
|
@@ -21,6 +21,11 @@ const VISIBILITY_CACHE_MAX_SNAPSHOTS = 32
|
|
|
21
21
|
const caches = new WeakMap<ManagedDb, Map<string, Visibility>>()
|
|
22
22
|
const REPORT_CACHE_BYTES = 16 * 1024 * 1024
|
|
23
23
|
const reportCaches = new WeakMap<ManagedDb, Map<string, Buffer>>()
|
|
24
|
+
// A workspace's report reads at once. Its reports' sizes are bounded together
|
|
25
|
+
// before any is read, and each is held parsed till the build ends, so more at
|
|
26
|
+
// once costs only the raw bytes in flight: a cold build of a team's reports
|
|
27
|
+
// is so many storage round trips one after another.
|
|
28
|
+
const WORKSPACE_READS_AT_ONCE = 16
|
|
24
29
|
function retainReports(db: ManagedDb, key: string, bytes: Buffer) {
|
|
25
30
|
if (bytes.length > REPORT_CACHE_BYTES) return
|
|
26
31
|
let cache = reportCaches.get(db)
|
|
@@ -93,7 +98,7 @@ async function buildTeamWorkspace(db: ManagedDb, store: BlobStore, snapshot: Tea
|
|
|
93
98
|
let inputBytes = 0, next = 0
|
|
94
99
|
// Bound both remote reads and raw buffers, retaining stable report order for
|
|
95
100
|
// the cross-report classification that follows. Finish workers on failure.
|
|
96
|
-
const results = await Promise.allSettled(Array.from({ length: Math.min(
|
|
101
|
+
const results = await Promise.allSettled(Array.from({ length: Math.min(WORKSPACE_READS_AT_ONCE, snapshot.reports.length) }, async () => {
|
|
97
102
|
while (next < snapshot.reports.length) {
|
|
98
103
|
const index = next++
|
|
99
104
|
const access = snapshot.reports[index]!
|
|
@@ -45,25 +45,45 @@ export async function putUploadPart(store: BlobStore, session: string, kind: Upl
|
|
|
45
45
|
await store.put(partId(session, kind, id, index), bytes)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// Parts read at once while an upload is put together.
|
|
49
|
+
const PARTS_READ_AT_ONCE = 6
|
|
50
|
+
|
|
51
|
+
// The staged upload `req` names, its parts read a few at a time into the one
|
|
52
|
+
// buffer it fills, each as long as its place in it. Its parts are deleted
|
|
53
|
+
// once read, or once it fails: awaited, unless `behind` takes that cleanup to
|
|
54
|
+
// go on past the response (http.ts), which staging maintenance backs up.
|
|
55
|
+
export async function readUpload(store: BlobStore, req: IncomingMessage, session: string, kind: UploadKind, maxBytes: number,
|
|
56
|
+
behind?: (cleanup: Promise<void>) => void): Promise<Buffer> {
|
|
49
57
|
const id = req.headers['x-upload-id']
|
|
50
58
|
const count = Number(req.headers['x-upload-parts']), size = Number(req.headers['x-upload-size'])
|
|
51
59
|
if (typeof id !== 'string' || !validUpload(id, count)) throw new Error('bad-upload')
|
|
52
60
|
try {
|
|
53
61
|
if (!Number.isSafeInteger(size) || size <= 0 || count !== Math.ceil(size / UPLOAD_CHUNK_BYTES)) throw new Error('bad-upload')
|
|
54
62
|
if (size > maxBytes || size > MAX_STAGED_BYTES) throw new Error('too-large')
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
// Every byte of it is written below before it is returned: each part
|
|
64
|
+
// must fill its place exactly, or the upload fails.
|
|
65
|
+
const body = Buffer.allocUnsafe(size)
|
|
66
|
+
let failed = false, next = 0
|
|
67
|
+
const readParts = async () => {
|
|
68
|
+
while (!failed && next < count) {
|
|
69
|
+
const index = next++
|
|
70
|
+
const bytes = await store.get(partId(session, kind, id, index))
|
|
71
|
+
const expected = Math.min(UPLOAD_CHUNK_BYTES, size - index * UPLOAD_CHUNK_BYTES)
|
|
72
|
+
if (!bytes || bytes.length !== expected) { failed = true; throw new Error('bad-upload') }
|
|
73
|
+
bytes.copy(body, index * UPLOAD_CHUNK_BYTES)
|
|
74
|
+
}
|
|
61
75
|
}
|
|
62
|
-
|
|
76
|
+
// Each read settled before its parts are deleted.
|
|
77
|
+
const results = await Promise.allSettled(Array.from({ length: Math.min(PARTS_READ_AT_ONCE, count) }, readParts))
|
|
78
|
+
const rejected = results.find(result => result.status === 'rejected')
|
|
79
|
+
if (rejected) throw rejected.reason
|
|
80
|
+
return body
|
|
63
81
|
} finally {
|
|
64
82
|
// These bytes can no longer be finalized. Failed/abandoned chunk uploads
|
|
65
83
|
// are also covered by automatic staging maintenance.
|
|
66
|
-
|
|
84
|
+
const cleanup = deleteUpload(store, session, kind, id, count).catch(err => console.warn('managed: upload cleanup failed:', err))
|
|
85
|
+
if (behind) behind(cleanup)
|
|
86
|
+
else await cleanup
|
|
67
87
|
}
|
|
68
88
|
}
|
|
69
89
|
|
package/out/chunk-4UFAMJAX.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{z as e}from"./chunk-S6GIXWQW.js";import{m as t,p as n,u as r}from"./chunk-JKGQ3O7O.js";import{a as i}from"./chunk-3PS4KWD2.js";import{b as a}from"./chunk-IYPY47MC.js";import{D as o,J as s,L as c,q as l}from"./chunk-SXV2T5C4.js";function u(e){let{data:t}=c(e);return t&&d(t)>0?t:null}function d(e){return(s(e)??[]).filter(t=>(Array.isArray(t)?t:[t]).some(t=>t&&typeof t==`object`&&l(t,t.source??e.source))).length}function f(e,t){let n=[],r=new Map;for(let{name:t,content:i,fallbackRepo:a}of e){if(typeof i!=`string`)continue;let e=u(i);if(!e)continue;let c=s(e),l=o(e)??o({repo:{github:a}}),f=new Set(l?[l]:[]);if(!l)for(let e of c.flat()){if(/(?:^|\/)(?:node_modules|dependencies)\//u.test(e?.file??``))continue;let t=o(e);t&&f.add(t)}for(let e of f)r.set(e,{id:e,label:e});n.push({id:t,filename:t,repoIds:[...f],analyzer:e.source,appFindings:d(e)})}let i=new Set(n.map(e=>e.id));return{merge:{bundles:[],results:[]},link:{reports:n,repositories:[...r.values()],workspaces:t.map(e=>({id:e.id,label:e.name,reports:e.reports.filter(e=>i.has(e))})).filter(e=>e.reports.length>0)}}}function p(e,t){let n=(e.reports??[]).filter(e=>e.visible&&e.repoId!=null).flatMap(e=>{let t=u(e.content??``);return t?[{id:e.id,filename:e.filename,repoIds:[e.repoId],repo:e.repoFullName,directory:e.repoDirectory,analyzer:e.analyzer,appFindings:d(t)}]:[]}),r=new Map((e.repos??[]).map(e=>[e.repoId,{id:e.repoId,label:e.fullName}]));for(let e of n)r.has(e.repoIds[0])||r.set(e.repoIds[0],{id:e.repoIds[0],label:e.repo??String(e.repoIds[0])});let a=t.results??[],o=new Set(a.map(e=>e.bundleId));return{merge:{bundles:(t.bundles??[]).filter(e=>o.has(e.id)).map(e=>({...e,size:i(e.byteSize)})),results:a},link:{repositories:[...r.values()].filter(e=>n.some(t=>t.repoIds.includes(e.id))),reports:n}}}var m=new Map([[`openai/gpt-6-astra`,`GPT-6 Astra`],[`openai/gpt-6-astra-pro`,`GPT-6 Astra Pro`],[`openai/gpt-5.6-sol`,`GPT-5.6 Sol`],[`openai/gpt-5.6-terra`,`GPT-5.6 Terra`],[`openai/gpt-5.6-luna`,`GPT-5.6 Luna`],[`anthropic/claude-fable-5.1`,`Claude Fable 5.1`],[`anthropic/claude-opus-5`,`Claude Opus 5`],[`anthropic/claude-sonnet-5`,`Claude Sonnet 5`],[`moonshotai/kimi-k3`,`Kimi K3`],[`z-ai/glm-5.3`,`GLM 5.3`],[`qwen/qwen3.8-max`,`Qwen3.8 Max`],[`deepseek/deepseek-v4.1-flash`,`DeepSeek V4.1 Flash`],[`deepseek/deepseek-v4-pro`,`DeepSeek V4 Pro`]]),h=new Map([[`openai`,`OpenAI`],[`anthropic`,`Anthropic`],[`moonshotai`,`Moonshot AI`],[`google`,`Google`],[`nvidia`,`NVIDIA`],[`qwen`,`Qwen`],[`deepseek`,`DeepSeek`],[`x-ai`,`xAI`],[`z-ai`,`Z.ai`],[`mistralai`,`Mistral AI`],[`meta`,`Meta`],[`unbiased`,`Unbiased`]]),g=new Map([[`meta-llama`,`meta`]]),_=new Map([[`gpt`,`GPT`],[`gptoss`,`GPT OSS`],[`oss`,`OSS`],[`o`,`O`],[`ai`,`AI`],[`api`,`API`],[`4o`,`4o`],[`k3`,`K3`],[`k4`,`K4`],[`qwen`,`Qwen`],[`kimi`,`Kimi`],[`mimo`,`MiMo`],[`gemini`,`Gemini`],[`claude`,`Claude`],[`fable`,`Fable`],[`opus`,`Opus`],[`sonnet`,`Sonnet`],[`astra`,`Astra`],[`terra`,`Terra`],[`luna`,`Luna`],[`codex`,`Codex`],[`deepseek`,`DeepSeek`],[`nemotron`,`Nemotron`],[`gemma`,`Gemma`]]);function v(e){let t=e.replace(/:free$/u,``).replaceAll(`_`,`-`).split(`-`).filter(Boolean),n=[];for(let e=0;e<t.length;e++){let r=t[e],i=t[e+1];if(/^\d+$/u.test(r)&&/^\d+$/u.test(i??``)){n.push(`${r}.${i}`),e++;continue}let a=_.get(r.toLowerCase());n.push(a??(/^(?:[a-z]+\d+|\d+[a-z]+)$/iu.test(r)?r.toUpperCase():r[0]?.toUpperCase()+r.slice(1)))}return n.join(` `).replace(/^GPT (\d)/u,`GPT-$1`)}function y(e){return m.get(e)??v(e.includes(`/`)?e.slice(e.indexOf(`/`)+1):e)}function b(e){let t=e.includes(`/`)?e.split(`/`)[0].replace(/^~/u,``):`other`,n=g.get(t)??t;return{key:n,name:h.get(n)??(n===`other`?`Other`:v(n))}}function x(e){return e===`xhigh`?`Extra high`:e?e[0].toUpperCase()+e.slice(1):`Unavailable`}function S(e){return e.efforts.includes(`max`)?`max`:e.efforts.at(-1)??null}async function C(e,t=fetch){let n=await t(`/api/admin/scan/models`,{credentials:`same-origin`,headers:{accept:`application/json`},signal:e});if(!n.ok)throw Error(`HTTP ${n.status}`);return w(await n.json())}function w(e){if(!Array.isArray(e?.models))throw Error(`No model catalogue returned`);let t=e.models.filter(e=>typeof e?.id==`string`&&e.id.length>0).map(e=>({id:e.id,efforts:Array.isArray(e.efforts)?[...new Set(e.efforts.filter(e=>typeof e==`string`&&e.length>0))]:[]}));if(t.length===0)throw Error(`No models available`);return{models:t,defaultModel:t.find(t=>t.id===e.defaultModel)?.id??t[0].id}}function T(e,t=[]){let n=new Map,r=e.flatMap(e=>{let r=t.filter(t=>t.bundles?.includes(e.integrity));return(r.length>0?r.map(e=>({id:e.id,label:e.name})):[{id:`unattached`,label:`Unattached`}]).map(t=>(n.set(t.id,t),{id:e.integrity,integrity:e.integrity,filename:e.name,repoId:t.id,repo:t.label,files:null,reasons:[],size:`—`}))});return{repositories:[...n.values()],bundles:r,reports:[],scans:[]}}function E(o,s){if(s.error)throw Error(s.error);let c=n(s),l=s.kind===`stasis`?s.bundle?.formats:null,u=r(s),d=[...t(s)].filter(([,e])=>e!=null).map(([e,t])=>({path:e,format:l?.get(e)??null,bytes:t,lines:u.get(e)??0,size:i(t),module:a(e,{packageDir:c?.get(e)})})),f=[{id:`all`,label:`All`,filePaths:null},...[...e(s,d.map(e=>e.path))].map(([e,t])=>({id:`reason:${e}`,label:e,filePaths:[...t]}))];return{...o,files:d,reasons:f,size:i(s.size)}}export{T as a,E as b,f as c,p as d,y as e,b as f,x as g,S as h,C as i,w as j};
|