@preventive/triage 1.0.0-alpha.38 → 1.0.0-alpha.39

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.
@@ -16,7 +16,8 @@ const MINIFIED_SPACES = .01
16
16
  // What a regular expression set aside holds.
17
17
  const REGEX = '\uE000'
18
18
  const JS = {
19
- // A hashbang, the file's first line (`#!/usr/bin/env node`), a comment.
19
+ // A hashbang, the file's first line (`#!/usr/bin/env node`), a comment,
20
+ // after a byte order mark too, which node's ES module loader drops first.
20
21
  // A string's or template's escape may be a line continuation (`\` before a
21
22
  // line break, CRLF's whole).
22
23
  // Linear on any text: a regular expression's context looked back on only
@@ -25,7 +26,7 @@ const JS = {
25
26
  // regular expression its line's end, a template or a comment the text's,
26
27
  // rather than tried again from each later quote or slash.
27
28
  // A regular expression first where a value starts (after `=>`, `...`, an operator or a keyword, not a property: `x.default/2`, `this.#in/2`), so a quote in it (`/["']/`) starts no string.
28
- stringOrComment: /((?=\/)(?<=(?:^|\.\.\.|[(,=:[!&|?{};>+\-*/%^<~]|(?<![\p{ID_Continue}$.#])(?:await|case|default|delete|do|else|extends|in|instanceof|new|of|return|throw|typeof|void|yield))[ \t]*)\/(?![/*])(?:[^/\\[\n]|\\.|\[(?:[^\]\\\n]|\\.)*(?:\]|(?=\\?$)))+(?:\/[a-z]*|(?=\\?$))|"(?:[^"\\\n]|\\(?:\r\n|[\s\S]))*(?:"|$)|'(?:[^'\\\n]|\\(?:\r\n|[\s\S]))*(?:'|$)|`(?:[^`\\]|\\(?:\r\n|[\s\S]))*(?:`|(?![\s\S])))|(?<![ \t])[ \t]*(?:\/\*[\s\S]*?(?:\*\/|(?![\s\S]))|\/\/.*|(?<![\s\S])#!.*)[ \t]*/gmu,
29
+ stringOrComment: /((?=\/)(?<=(?:^|\.\.\.|[(,=:[!&|?{};>+\-*/%^<~]|(?<![\p{ID_Continue}$.#])(?:await|case|default|delete|do|else|extends|in|instanceof|new|of|return|throw|typeof|void|yield))[ \t]*)\/(?![/*])(?:[^/\\[\n]|\\.|\[(?:[^\]\\\n]|\\.)*(?:\]|(?=\\?$)))+(?:\/[a-z]*|(?=\\?$))|"(?:[^"\\\n]|\\(?:\r\n|[\s\S]))*(?:"|$)|'(?:[^'\\\n]|\\(?:\r\n|[\s\S]))*(?:'|$)|`(?:[^`\\]|\\(?:\r\n|[\s\S]))*(?:`|(?![\s\S])))|(?<![ \t])[ \t]*(?:\/\*[\s\S]*?(?:\*\/|(?![\s\S]))|\/\/.*|(?<![\s\S])\uFEFF?#!.*)[ \t]*/gmu,
29
30
  // Beside punctuation (`a, b`, `x = 1`), not between two words, as
30
31
  // JavaScript tells a word's characters (`return a`, `var π`, `var \u03c0`),
31
32
  droppable: /(?<![\p{ID_Continue}$\\\u200C\u200D])[ \t]+|[ \t]+(?![\p{ID_Continue}$\\\u200C\u200D])/gu,
@@ -76,9 +77,10 @@ export function minifiedAverage(whole, { stringOrComment, droppable, needed }) {
76
77
 
77
78
  // JavaScript or TypeScript (a declaration file too, which node runs as any
78
79
  // other), or a script run by node by its hashbang (`bin/cli`, with no
79
- // extension).
80
+ // extension), a byte order mark before it or not, as node runs one after it
81
+ // as an ES module (in a `"type": "module"` package).
80
82
  const SCRIPT = /\.[cm]?[jt]sx?$/iu
81
- const NODE_HASHBANG = /^#!.*\bnode\b/u
83
+ const NODE_HASHBANG = /^\uFEFF?#!.*\bnode\b/u
82
84
  // Whether a file (its path and text) is a script, as SCRIPT and NODE_HASHBANG tell.
83
85
  export const npmIsScript = (path, text) => typeof text === 'string' && (SCRIPT.test(path) || NODE_HASHBANG.test(text))
84
86
  // How many places a file is told doing a thing, the first ones, at most
@@ -17,7 +17,7 @@ import { npmAddedDependency } from './new-dependency.js'
17
17
  // Which reading of a version an assessment kept (`risk/npm/assessments`) is:
18
18
  // one more whenever a heuristic changes what it finds, so one kept by an
19
19
  // older reading is read anew.
20
- const RISK_VERSION = 11
20
+ const RISK_VERSION = 12
21
21
 
22
22
  // How long an assessment kept may be as JSON, and what risk reads of either
23
23
  // version's files: a package past it, whose files name thousands of hosts or
@@ -11,7 +11,7 @@ import { bundleFileSizes, bundleFilesAsMap } from '../../common/bundle-sources.j
11
11
  import { npmVersionFileRisk, npmVersionHosts } from './files.js'
12
12
  import { npmHostFinding } from './held.js'
13
13
  import { HOST_TAGS, npmHostSeverity, npmHostsTagged } from '../files/hosts.js'
14
- import { npmBecameUnreadable, npmLargeCode, npmUnreadable } from '../files/readability.js'
14
+ import { npmBecameUnreadable, npmBinaryCode, npmLargeCode, npmUnreadable } from '../files/readability.js'
15
15
  import { SEVERITIES } from '../files/stamps.js'
16
16
  import { npmFoundAdded, npmRunsCodeFlags, npmRunsCodeWithout } from '../files/runs-code.js'
17
17
  import { npmDependencySpecs } from './dependencies.js'
@@ -27,9 +27,11 @@ import { npmInstallScriptsChange, npmProvenanceLost, npmPublisherChange } from '
27
27
  // (npmHostSeverity: `exfiltration` and `remote` red, `ip-lookup` orange); a
28
28
  // code file of 256 KiB or more it adds (npmLargeCode, `large-code`); a file
29
29
  // it changes that had no line over 200 characters and now has one over 2,000
30
- // (npmBecameUnreadable, `now-unreadable`); and a file it adds or changes with
30
+ // (npmBecameUnreadable, `now-unreadable`); a file it adds or changes with
31
31
  // an obfuscator's fingerprint where the one compared with had none
32
- // (`obfuscated`). Then in orange what any of its files runs beyond itself that
32
+ // (`obfuscated`); and a code file it adds or changes that isn't text, where
33
+ // the one compared with's was text, or no code, or none (npmBinaryCode,
34
+ // `binary-code`). Then in orange what any of its files runs beyond itself that
33
35
  // `runs-code` doesn't name, kept from the version compared with
34
36
  // (`kept-runs-code`). A renamed file by its old path; one renamed alone,
35
37
  // unchanged, holds what it held. A file become unreadable calls for less
@@ -61,6 +63,8 @@ function npmFileFindings(base, other, files, major) {
61
63
  ...rebuiltWithMap(base, other, row.path, row.renamedFrom ?? row.path) ? { rebuilt: true } : {} }))) },
62
64
  { id: 'obfuscated', severity: 'critical', files: touched.filter(row => after(row).readability.obfuscation && !before(row)?.readability.obfuscation)
63
65
  .map(row => ({ path: row.path, obfuscation: after(row).readability.obfuscation })) },
66
+ { id: 'binary-code', severity: 'critical', files: touched.filter(row => npmBinaryCode(row.path, after(row).readability) && !npmBinaryCode(row.renamedFrom ?? row.path, before(row)?.readability))
67
+ .map(row => ({ path: row.path, bytes: sizes.get(row.path) })) },
64
68
  { id: 'kept-runs-code', severity: 'high', files: kept },
65
69
  ].filter(finding => finding.files.length > 0)
66
70
  }
@@ -159,7 +163,8 @@ function npmNewHosts(base, other, files) {
159
163
  // files hold that the one compared with's didn't, so where none of them runs
160
164
  // code beyond itself, names a service under one of the hosts' tags or a host
161
165
  // past ALLOWED_HOSTS, bears an obfuscator's fingerprint, has a line no one
162
- // writes (npmUnreadable) or is large code, nothing is found of them whatever
166
+ // writes (npmUnreadable), is large code or is code that isn't text
167
+ // (npmBinaryCode), nothing is found of them whatever
163
168
  // the one compared with holds. Nor do the one compared with's install scripts
164
169
  // differ by a binding.gyp among its files, but where its manifest names
165
170
  // neither `preinstall` nor `install`, and the open version has some.
@@ -170,6 +175,7 @@ export function npmBaseFilesTell(other, manifest) {
170
175
  return [...bundleFilesAsMap(other).keys()].some(path => {
171
176
  const { runsCode, exfiltration, readability, hosts } = npmVersionFileRisk(other, path)
172
177
  return Boolean(runsCode || exfiltration || readability.obfuscation) || npmUnreadable(path, readability) || npmLargeCode(path, sizes.get(path))
178
+ || npmBinaryCode(path, readability)
173
179
  || [...hosts?.keys() ?? []].some(host => !ALLOWED_HOSTS.has(host))
174
180
  })
175
181
  }
@@ -23,11 +23,12 @@ export function npmInstallScripts(manifest, paths = []) {
23
23
  return installScripts(manifest?.installScripts, Boolean(paths && (paths instanceof Set ? paths.has('binding.gyp') : paths.includes('binding.gyp'))))
24
24
  }
25
25
 
26
- // A package.json's install scripts, from its text; none where it isn't one,
26
+ // A package.json's install scripts, from its text, a byte order mark it
27
+ // starts with dropped, as npm drops one reading it; none where it isn't one,
27
28
  // or names none.
28
29
  export function packageJsonInstallScripts(text, bindingGyp = false) {
29
30
  let json = null
30
- try { json = typeof text === 'string' ? JSON.parse(text) : null } catch {}
31
+ try { json = typeof text === 'string' ? JSON.parse(text.replace(/^\uFEFF/u, '')) : null } catch {}
31
32
  const scripts = json && typeof json === 'object' && json.scripts && typeof json.scripts === 'object' ? json.scripts : {}
32
33
  return installScripts(scripts, bindingGyp)
33
34
  }
@@ -107,7 +107,12 @@ export async function fetchBundleAdvisories(packages: Package[], signal: AbortSi
107
107
  const result = await Promise.race([Promise.all(asked.map(({ part, osv }) => part.length === 0 ? Promise.resolve([]) : advisories(part, { ...options, osv }))), deadline])
108
108
  return { status: 200, body: result.flat().toSorted((a, b) => order(a.ecosystem, b.ecosystem) || order(a.name, b.name)) }
109
109
  } catch (error) {
110
- if (debug) console.warn('managed: advisories failed:', error)
110
+ // Answered as upstream's failure alone: logged, its message, or with
111
+ // `debug` the whole error, but for a request its client gave up on. One
112
+ // past its deadline (aborted with a TimeoutError, as AbortSignal.timeout
113
+ // and the bundle route's timer abort) is the server giving up: logged.
114
+ const cancelled = signal.aborted && (signal.reason as { name?: unknown } | undefined)?.name !== 'TimeoutError'
115
+ if (!cancelled) console.warn('managed: advisories failed:', debug || !(error instanceof Error) ? error : error.message)
111
116
  return { status: 502, body: { error: 'upstream-unavailable' } }
112
117
  } finally {
113
118
  if (onAbort) signal.removeEventListener('abort', onAbort)
@@ -6,7 +6,7 @@ import { Buffer } from 'node:buffer'
6
6
  import { createHash, createSign } from 'node:crypto'
7
7
  import { createClient } from '@preventive/upstream/github.js'
8
8
  import type { ManagedConfig } from './config.ts'
9
- import { GithubApiError, throwGithubResponseError, upstreamGithub } from './github-errors.ts'
9
+ import { GithubApiError, githubUnreachable, throwGithubResponseError, upstreamGithub } from './github-errors.ts'
10
10
 
11
11
  export { GithubApiError } from './github-errors.ts'
12
12
 
@@ -71,6 +71,7 @@ export function mergeRepos(...lists: ConnectedRepo[][]): ConnectedRepo[] {
71
71
  // fold into a GithubApiError (401 passes through for the caller to handle).
72
72
  export async function githubJson(url: string, token: string | null, fetchImpl: typeof fetch, method: 'GET' | 'POST' = 'GET'): Promise<unknown> {
73
73
  let res: Response
74
+ const started = Date.now()
74
75
  try {
75
76
  res = await fetchImpl(url, {
76
77
  method,
@@ -81,10 +82,15 @@ export async function githubJson(url: string, token: string | null, fetchImpl: t
81
82
  'user-agent': USER_AGENT, 'x-github-api-version': API_VERSION,
82
83
  },
83
84
  })
84
- } catch { throw new GithubApiError(502, 'github-unreachable') }
85
+ } catch (error) { throw githubUnreachable(error, started) }
85
86
  if (res.status === 401) discardInstallationToken(token, fetchImpl)
86
87
  if (!res.ok) await throwGithubResponseError(res)
87
- try { return await res.json() } catch { throw new GithubApiError(502, 'github-malformed') }
88
+ // The answer's body is read within the same timeout: one not read whole is
89
+ // GitHub not reached, as one not answered is; only one that isn't JSON is
90
+ // malformed.
91
+ try { return await res.json() } catch (error) {
92
+ throw error instanceof SyntaxError ? new GithubApiError(502, 'github-malformed') : githubUnreachable(error, started)
93
+ }
88
94
  }
89
95
 
90
96
  // Validate one repo object into a ConnectedRepo (or null to skip), tagging it
@@ -5,14 +5,32 @@ import { HttpError } from '@preventive/upstream/github.js'
5
5
  export class GithubApiError extends Error {
6
6
  status: number
7
7
  retryAfter: number | null
8
- constructor(status: number, message: string, retryAfter: number | null = null) {
8
+ // What failed, where GitHub wasn't reached (githubUnreachable).
9
+ detail: string | null
10
+ constructor(status: number, message: string, retryAfter: number | null = null, detail: string | null = null) {
9
11
  super(message)
10
12
  this.name = 'GithubApiError'
11
13
  this.status = status
12
14
  this.retryAfter = retryAfter
15
+ this.detail = detail
13
16
  }
14
17
  }
15
18
 
19
+ // GitHub not reached, or its answer not read whole, `started` ms since the
20
+ // epoch: as `github-unreachable`, with what failed as the error and its causes
21
+ // tell it (a timeout, a connection closed, an answer too large) and after how
22
+ // long, for an admin's page to show (repository-risk.ts). Only the errors'
23
+ // names, codes and messages, never GitHub's text nor a request's headers.
24
+ export function githubUnreachable(error: unknown, started: number): GithubApiError {
25
+ const told: string[] = []
26
+ for (let at = error, depth = 0; at instanceof Error && depth < 4; at = at.cause, depth++) {
27
+ const code = 'code' in at && typeof at.code === 'string' && at.code !== at.name ? ` ${at.code}` : ''
28
+ told.push(`${at.name}${code}${at.message ? `: ${at.message.slice(0, 200)}` : ''}`)
29
+ }
30
+ const failed = told.length > 0 ? told.join('; ') : String(error).slice(0, 200)
31
+ return new GithubApiError(502, 'github-unreachable', null, `${failed}, after ${((Date.now() - started) / 1000).toFixed(1)}s`)
32
+ }
33
+
16
34
  export async function throwGithubResponseError(res: Response): Promise<never> {
17
35
  if (res.status === 403 || res.status === 429) {
18
36
  const body = await res.json().catch(() => null) as { message?: unknown } | null
@@ -30,6 +48,7 @@ export async function throwGithubResponseError(res: Response): Promise<never> {
30
48
  }
31
49
 
32
50
  export async function upstreamGithub<T>(request: () => Promise<T>): Promise<T> {
51
+ const started = Date.now()
33
52
  try { return await request() } catch (error) {
34
53
  if (error instanceof HttpError) {
35
54
  // The upstream error includes GitHub's response text. Retain managed
@@ -44,6 +63,6 @@ export async function upstreamGithub<T>(request: () => Promise<T>): Promise<T> {
44
63
  if (error instanceof Error && (error.name === 'AssertionError' || error.cause instanceof SyntaxError)) {
45
64
  throw new GithubApiError(502, 'github-malformed')
46
65
  }
47
- throw new GithubApiError(502, 'github-unreachable')
66
+ throw githubUnreachable(error, started)
48
67
  }
49
68
  }
@@ -221,6 +221,13 @@ const NPM_RISK_PATH = '/api/npm/risk'
221
221
  // having no such package or version, or a tarball unpacking past risk's
222
222
  // bounds; npm failing otherwise a 502.
223
223
  const NPM_RISK_STATUS: Partial<Record<string, number>> = { 'package-not-found': 404, 'version-not-found': 404, 'package-too-large': 413 }
224
+ // A risk failure answered with its code alone, as risk's status for it (NPM_RISK_STATUS, else 502), its cause
225
+ // logged where it is the server's or npm's, not the reader's (a 5xx), for the logs to say why.
226
+ function sendRiskError(res: ServerResponse, err: NpmRiskError) {
227
+ const status = NPM_RISK_STATUS[err.code] ?? 502
228
+ if (status >= 500) console.error(`managed: npm risk failed (${err.code}):`, err.message)
229
+ sendJson(res, status, { error: err.code })
230
+ }
224
231
  const NPM_PATHS = new Set([NPM_PACKAGE_PATH, NPM_VERSIONS_PATH, NPM_DOWNLOAD_PATH, NPM_STATS_PATH, NPM_ADVISORIES_PATH, NPM_TAGS_PATH, NPM_SOCKET_PATH, NPM_PROVENANCE_PATH, NPM_PRETTY_PATH,
225
232
  NPM_RENAME_DIFF_PATH, NPM_TYPE_ONLY_PATH, NPM_RISK_PATH])
226
233
  // How long an npm response may go unread before its connection is dropped.
@@ -1548,7 +1555,8 @@ async function handleBundleAdvisories(res: ServerResponse, deps: ManagedHttpDeps
1548
1555
  const controller = new AbortController()
1549
1556
  const onClose = () => { if (!res.writableEnded) controller.abort() }
1550
1557
  res.on('close', onClose)
1551
- const timer = setTimeout(() => controller.abort(), ADVISORIES_TIMEOUT_MS)
1558
+ // Its deadline aborts as AbortSignal.timeout does, for a failure it leaves to be told from the reader's leaving.
1559
+ const timer = setTimeout(() => controller.abort(new DOMException('advisories took too long', 'TimeoutError')), ADVISORIES_TIMEOUT_MS)
1552
1560
  try {
1553
1561
  if (res.destroyed) return
1554
1562
  const needsGithub = inventory.packages.some(pkg => repoAdvisories || pkg.ecosystem === 'github' || pkg.ecosystem === 'soldeer')
@@ -2804,7 +2812,7 @@ async function handleNpmReviews(req: IncomingMessage, res: ServerResponse, deps:
2804
2812
  } catch (err) {
2805
2813
  if (controller.signal.aborted) return
2806
2814
  if (err instanceof NpmPackageError) { sendJson(res, err.status, { error: err.message }); return }
2807
- if (err instanceof NpmRiskError) { sendJson(res, NPM_RISK_STATUS[err.code] ?? 502, { error: err.code }); return }
2815
+ if (err instanceof NpmRiskError) { sendRiskError(res, err); return }
2808
2816
  throw err
2809
2817
  } finally { res.off('close', onClose) }
2810
2818
  }
@@ -3010,7 +3018,7 @@ async function handleNpm(req: IncomingMessage, res: ServerResponse, deps: Manage
3010
3018
  writeResponse(res, Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength))
3011
3019
  } catch (err) {
3012
3020
  if (err instanceof NpmPackageError || err instanceof PrettyError) { if (!res.headersSent && !res.destroyed) sendJson(res, err.status, { error: err.message }); return }
3013
- if (err instanceof NpmRiskError) { if (!res.headersSent && !res.destroyed) sendJson(res, NPM_RISK_STATUS[err.code] ?? 502, { error: err.code }); return }
3021
+ if (err instanceof NpmRiskError) { if (!res.headersSent && !res.destroyed) sendRiskError(res, err); return }
3014
3022
  if (controller.signal.aborted) return
3015
3023
  throw err
3016
3024
  } finally {
@@ -111,8 +111,11 @@ export type RepositoryRiskLine =
111
111
  | { type: 'done' }
112
112
 
113
113
  // What an error is known by: a GitHub or npm failure's code, or `failed`;
114
- // risk's with what it asked of npm and how that failed, where it says.
115
- function failureOf(error: unknown): RiskFailure {
114
+ // GitHub not reached with what failed (githubUnreachable), while `doing` what
115
+ // of a repository, and risk's with what it asked of npm and how that failed,
116
+ // where they say.
117
+ function failureOf(error: unknown, doing?: string): RiskFailure {
118
+ if (error instanceof GithubApiError && error.detail) return { error: error.message, detail: doing ? `${doing}: ${error.detail}` : error.detail }
116
119
  if (error instanceof GithubApiError || error instanceof NpmPackageError) return { error: error.message }
117
120
  if (!(error instanceof NpmRiskError)) return { error: 'failed' }
118
121
  return error.message === error.code ? { error: error.code } : { error: error.code, detail: error.message }
@@ -299,16 +302,18 @@ function assessLater(scan: Scan, packages: LockedPackage[]) {
299
302
  // for its commit or else from GitHub, those of its `paths` alone; null, with
300
303
  // its line telling why, where either failed, or where the scan stopped.
301
304
  async function listLockfiles(scan: Scan, repo: RepositoryRiskRepo) {
305
+ let doing = 'opening it'
302
306
  try {
303
307
  const opened = await scan.source.open(repo)
304
308
  if (scan.signal.aborted) return null
305
309
  const key = `${repo.repoId}@${opened.commit}`
306
310
  let listed = scan.caches.listings.get(key)
311
+ doing = 'listing its lockfiles'
307
312
  if (!listed) scan.caches.listings.set(key, listed = await opened.lockfiles())
308
313
  const lockfiles = listed.filter(({ path, format }) => NPM_LOCKFILE_FORMATS.has(format) && lockfileOf(path, repo.paths))
309
314
  return { opened, lockfiles }
310
315
  } catch (error) {
311
- if (!scan.signal.aborted) scan.lines.add({ type: 'repo', id: repo.repoId, ...failureOf(error) })
316
+ if (!scan.signal.aborted) scan.lines.add({ type: 'repo', id: repo.repoId, ...failureOf(error, doing) })
312
317
  return null
313
318
  }
314
319
  }
@@ -353,7 +358,7 @@ async function readRepository(scan: Scan, repo: RepositoryRiskRepo) {
353
358
  lockfiles.push(await readLockfile(scan, repo.repoId, listed.opened, lockfile))
354
359
  }
355
360
  try { await listed.opened.recheck() } catch (error) {
356
- if (!scan.signal.aborted) scan.lines.add({ type: 'repo', id: repo.repoId, ...failureOf(error) })
361
+ if (!scan.signal.aborted) scan.lines.add({ type: 'repo', id: repo.repoId, ...failureOf(error, 'checking access to it again') })
357
362
  return
358
363
  }
359
364
  if (scan.signal.aborted) return
@@ -1,7 +0,0 @@
1
- import{a}from"./chunk-TUII34K2.js";var v="deepview-objstore.v1.put",y="deepview-objstore.v1.delete",h="deepview-objstore.v1.fetch",I="deepview-objstore.v1.fetch-rest",w="deepview-objstore.v1.put-rest",j="deepview-objstore.v1.delete-rest";function l(e){return e==null?"":String(e)}function p(e){return e??""}function x(e,n){return a([v,e.workspaceTag,e.resourceTag,l(e.prevVersion),p(e.prevIncarnation),e.contentHash,String(e.expectedLength),n].join(`
2
- `))}function O(e,n){return a([y,e.workspaceTag,e.resourceTag,l(e.prevVersion),p(e.prevIncarnation),n].join(`
3
- `))}function P(e,n,r){return a([h,e,n,r].join(`
4
- `))}async function i(e,n){return new Uint8Array(await crypto.subtle.sign({name:"Ed25519"},e,n)).toBase64({alphabet:"base64url",omitPadding:!0})}function B(e,n,r){return i(e,x(n,r))}function D(e,n,r){return i(e,O(n,r))}function R(e,n,r,o){return i(e,P(n,r,o))}function A(e,n,r){return a([I,e,n,String(r)].join(`
5
- `))}function _(e,n,r,o){return i(e,A(n,r,o))}function N(e,n){return a([w,e.workspaceTag,e.resourceTag,l(e.prevVersion),p(e.prevIncarnation),e.contentHash,String(e.expectedLength),String(n)].join(`
6
- `))}function U(e,n,r){return i(e,N(n,r))}function T(e,n){return a([j,e.workspaceTag,e.resourceTag,l(e.prevVersion),p(e.prevIncarnation),String(n)].join(`
7
- `))}function L(e,n,r){return i(e,T(n,r))}async function H(e){let n=await crypto.subtle.digest("SHA-256",e);return new Uint8Array(n).toBase64({alphabet:"base64url",omitPadding:!0})}var d="/api/config";var J="http://127.0.0.1:3123/";function m(e){if(typeof e!="string"||!e.trim())return null;try{let n=new URL(e);return!["http:","https:"].includes(n.protocol)||n.username||n.password||n.search||n.hash?null:(n.pathname=n.pathname.replace(/\/+$/u,"")+"/",n.href)}catch{return null}}var b="deepview.sync.serverInfo",g="deepview.sync.standaloneProbe";function $(){try{return localStorage.getItem(g)==="1"}catch{return!1}}function X(){try{localStorage.setItem(g,"1")}catch{}}function f(e){return e==="managed+e2e"||e==="e2e+managed"}function q(e,n){return(e==="managed"||f(e))&&/^\/(?:team|manage)(?:\/|$)/u.test(n)}function Q(e,n=null){return n&&(f(e)||e===n)?n:e==="managed"||e==="managed+e2e"?"managed":"e2e"}function W(e,n){return e&&f(e.mode)&&n.mode==="e2e"?{...e,githubNewIssueLabels:n.githubNewIssueLabels??"",...n.deepviewScanServer?{deepviewScanServer:n.deepviewScanServer}:{}}:n}function S(e){if(e==null||typeof e!="object")return null;let n=e.mode;if(n!=="e2e"&&n!=="managed"&&n!=="managed+e2e"&&n!=="e2e+managed")return null;let r=null,o=e.managed;if(o!=null&&typeof o=="object"){let u=o.loginPath,c=o.cookieName;typeof u=="string"&&typeof c=="string"&&(r={loginPath:u,cookieName:c},o.allowShare===!0&&(r.allowShare=!0))}let t=n==="managed"?null:m(e.deepviewScanServer),s=e.githubNewIssueLabels;return{mode:n,managed:r,...t?{deepviewScanServer:t}:{},...typeof s=="string"&&s.length<=4096?{githubNewIssueLabels:s}:{}}}async function Z(){try{let e=await fetch(d,{credentials:"same-origin",cache:"no-store",headers:{accept:"application/json"}});return e.status===404?"standalone":e.ok?S(await e.json()):null}catch{return null}}async function ee(e,n=3e3){let r;try{return await Promise.race([e,new Promise(o=>{r=setTimeout(()=>o(null),n)})])}catch{return null}finally{clearTimeout(r)}}function ne(){try{let e=localStorage.getItem(b);if(e==null)return null;let n=S(JSON.parse(e));return n?{mode:n.mode,managed:n.managed}:null}catch{return null}}function re(e){try{localStorage.setItem(b,JSON.stringify({mode:e.mode,managed:e.managed})),localStorage.removeItem(g)}catch{}}var E="deepview.objstore-presence.";function M(e){return E+e}function C(e){try{let n=localStorage.getItem(M(e));if(!n)return null;let r=JSON.parse(n);return!r||typeof r!="object"?null:{names:r.names&&typeof r.names=="object"?r.names:{},bundles:r.bundles&&typeof r.bundles=="object"?r.bundles:{},bundleNames:r.bundleNames&&typeof r.bundleNames=="object"?r.bundleNames:{},baselines:k(r.baselines,r.localVersions)}}catch{return null}}function k(e,n){let r={};if(n&&typeof n=="object")for(let[o,t]of Object.entries(n))Number.isSafeInteger(t)&&t>=0&&(r[o]={version:t,incarnation:null,synced:!0,hash:null});if(e&&typeof e=="object")for(let[o,t]of Object.entries(e))!t||typeof t!="object"||!Number.isSafeInteger(t.version)||t.version<0||t.incarnation!=null&&typeof t.incarnation!="string"||(r[o]={version:t.version,incarnation:t.incarnation??null,synced:t.synced===!0,hash:typeof t.hash=="string"?t.hash:null});return r}function oe(e,n,r,o){let t=C(e),s=Object.entries((n==="bundle"?t?.bundles:t?.names)??{}).find(([,c])=>c===r)?.[0],u=t?.baselines?.[s];return{synced:s!==void 0&&(n==="bundle"||u?.synced===!0&&u.hash===o),cached:!0}}export{B as a,D as b,R as c,_ as d,U as e,L as f,H as g,J as h,m as i,$ as j,X as k,f as l,q as m,Q as n,W as o,S as p,Z as q,ee as r,ne as s,re as t,M as u,C as v,oe as w};
File without changes