@preventive/triage 1.0.0-alpha.17 → 1.0.0-alpha.19
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/api/reap.ts +13 -75
- package/common/server-info.ts +6 -4
- package/out/client-managed-import.js +81 -0
- package/out/client-managed.js +68 -41
- package/out/client-sync.js +14 -14
- package/out/graph.js +10 -10
- package/out/index.html +1 -0
- package/out/view.css +1 -1
- package/out/view.js +179 -142
- package/package.json +16 -6
- package/report/index.js +2 -1
- package/report/src/finding.js +12 -0
- package/report/src/security.js +63 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/server-common/reap.ts +48 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-log.ts +34 -0
- package/server-common/vercel-blob.ts +110 -0
- package/server-e2e/app.ts +485 -0
- package/server-e2e/cli.js +7 -2
- package/server-e2e/config.ts +36 -40
- package/server-e2e/db-neon.ts +1 -1
- package/server-e2e/db.ts +1 -1
- package/server-e2e/http.ts +12 -2
- package/server-e2e/index.ts +12 -481
- package/server-e2e/lifecycle.ts +59 -172
- package/server-e2e/neon-driver.ts +2 -2
- package/server-e2e/npm-proxy.ts +10 -143
- package/server-e2e/objstore/blob-vercel.ts +4 -108
- package/server-e2e/objstore/init.ts +15 -23
- package/server-e2e/objstore/store-neon.ts +5 -9
- package/server-e2e/static.ts +11 -17
- package/out/provider-icons/claude.svg +0 -1
- package/out/provider-icons/deepseek.svg +0 -1
- package/out/provider-icons/google.svg +0 -1
- package/out/provider-icons/grok.svg +0 -1
- package/out/provider-icons/moonshot.svg +0 -1
- package/out/provider-icons/nvidia.svg +0 -1
- package/out/provider-icons/openai.svg +0 -1
- package/out/provider-icons/openrouter.svg +0 -1
- package/out/provider-icons/qwen.svg +0 -1
- package/out/provider-icons/zai.svg +0 -1
package/server-e2e/config.ts
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// vars, the optional config.json) into one immutable `Config`, failing
|
|
3
3
|
// loud on malformed values so a typo surfaces at startup rather than
|
|
4
4
|
// deep in `node:net` / at the first token verification. Pure parsing —
|
|
5
|
-
// no backends opened
|
|
6
|
-
//
|
|
7
|
-
// result and does the wiring (backend selection, password HMAC, …).
|
|
5
|
+
// no backends opened or process termination. Invalid configuration throws so
|
|
6
|
+
// embedding hosts can recover; app.ts handles backend selection and wiring.
|
|
8
7
|
|
|
9
8
|
import { Buffer } from 'node:buffer'
|
|
10
9
|
import { readFileSync } from 'node:fs'
|
|
11
|
-
import {
|
|
10
|
+
import { env } from 'node:process'
|
|
12
11
|
import { fileURLToPath } from 'node:url'
|
|
13
12
|
import { dirname, join } from 'node:path'
|
|
14
13
|
import { configuredScanServer } from '../server-common/scan-config.ts'
|
|
14
|
+
import { databaseUrls } from '../server-common/database-config.ts'
|
|
15
15
|
|
|
16
16
|
export type Config = {
|
|
17
17
|
port: number
|
|
@@ -30,7 +30,7 @@ export type Config = {
|
|
|
30
30
|
deepviewScanServer: string | null
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// Parse + range-validate an integer env var,
|
|
33
|
+
// Parse + range-validate an integer env var, throwing a clear
|
|
34
34
|
// up-front message on a malformed value — a NaN from `Number("abc")`
|
|
35
35
|
// otherwise surfaces as a confusing crash deep inside `node:net`
|
|
36
36
|
// (`WebSocketServer({ port: NaN })`) or a 0-ms `setInterval` loop. An
|
|
@@ -41,13 +41,12 @@ function intEnv(name: string, def: number, min: number, max: number, hint = ''):
|
|
|
41
41
|
const raw = env[name]
|
|
42
42
|
const n = raw == null ? def : Number(raw)
|
|
43
43
|
if (!Number.isSafeInteger(n) || n < min || n > max) {
|
|
44
|
-
|
|
45
|
-
process.exit(1)
|
|
44
|
+
throw new Error(`Invalid ${name}: ${raw} — must be an integer in [${min}, ${max}].${hint ? ` ${hint}` : ''}`)
|
|
46
45
|
}
|
|
47
46
|
return n
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
const HELP = `Usage: node server-e2e/index.ts
|
|
49
|
+
export const HELP = `Usage: node server-e2e/index.ts
|
|
51
50
|
Environment:
|
|
52
51
|
PORT listen port (default 8765)
|
|
53
52
|
HOST bind host (default 127.0.0.1)
|
|
@@ -55,9 +54,15 @@ Environment:
|
|
|
55
54
|
in /api/config and allowed by the UI CSP.
|
|
56
55
|
Unset by default (no external scan service).
|
|
57
56
|
DB_PATH sqlite file (default: server-e2e/data/data.db);
|
|
58
|
-
ignored when
|
|
59
|
-
DATABASE_URL Neon Postgres
|
|
60
|
-
|
|
57
|
+
ignored when an e2e database URL is set
|
|
58
|
+
DATABASE_URL shared Neon Postgres URL for enabled modes.
|
|
59
|
+
Cannot be combined with E2E_DATABASE_URL
|
|
60
|
+
or MANAGED_DATABASE_URL.
|
|
61
|
+
E2E_DATABASE_URL e2e-only Neon URL, used without DATABASE_URL.
|
|
62
|
+
Combined mode also requires
|
|
63
|
+
MANAGED_DATABASE_URL; mixing Neon and
|
|
64
|
+
SQLite backends is rejected.
|
|
65
|
+
Either e2e URL selects Neon instead of
|
|
61
66
|
SQLite. Requires the optional peer dep
|
|
62
67
|
@neondatabase/serverless. The Neon
|
|
63
68
|
pairing additionally requires
|
|
@@ -66,12 +71,12 @@ Environment:
|
|
|
66
71
|
local-FS bytes cannot back a multi-
|
|
67
72
|
replica DB plane.
|
|
68
73
|
BLOB_READ_WRITE_TOKEN Vercel Blob R/W token (private store).
|
|
69
|
-
Required when
|
|
74
|
+
Required when an e2e database URL is set;
|
|
70
75
|
ignored otherwise. Requires the optional
|
|
71
76
|
peer dep @vercel/blob.
|
|
72
77
|
OBJSTORE_TOKEN_SECRET Base64 (32 bytes) HMAC secret for REST
|
|
73
|
-
bearer tokens. REQUIRED
|
|
74
|
-
|
|
78
|
+
bearer tokens. REQUIRED in Neon mode
|
|
79
|
+
(multi-replica deployments: a
|
|
75
80
|
token minted on one replica's WS plane
|
|
76
81
|
must validate on another replica's REST
|
|
77
82
|
plane). Optional under SQLite (a fresh
|
|
@@ -81,16 +86,15 @@ Environment:
|
|
|
81
86
|
OBJSTORE_DIR object store root (default: ./objstore
|
|
82
87
|
next to DB_PATH). Used by the local-FS
|
|
83
88
|
byte plane only; ignored when
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
an e2e database URL selects Neon
|
|
90
|
+
(bytes live in Vercel Blob).
|
|
86
91
|
OBJSTORE_REAP_INTERVAL_MS orphan reaper period (default 600000)
|
|
87
|
-
OBJSTORE_REAP_DISABLED set '1' / 'true' to disable
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Default OFF (reaper runs).
|
|
92
|
+
OBJSTORE_REAP_DISABLED set '1' / 'true' to disable automatic orphan
|
|
93
|
+
sweeps (boot and periodic). Explicit /api/reap
|
|
94
|
+
remains available. Schedule cleanup externally
|
|
95
|
+
to avoid accumulating orphaned bytes.
|
|
96
|
+
CRON_SECRET bearer secret for GET /api/reap (fails closed
|
|
97
|
+
when unset). Runs cleanup for enabled modes.
|
|
94
98
|
TRUST_PROXY set '1' / 'true' to honour X-Forwarded-
|
|
95
99
|
Host / X-Forwarded-Proto when computing
|
|
96
100
|
the same-origin gate's expected origin.
|
|
@@ -126,11 +130,11 @@ function readServerConfigFile(path: string): ServerConfigFile {
|
|
|
126
130
|
let raw: string
|
|
127
131
|
try { raw = readFileSync(path, 'utf8') } catch (err) {
|
|
128
132
|
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return {}
|
|
129
|
-
|
|
133
|
+
throw new Error(`Failed to read ${path}: ${(err as Error)?.message ?? err}`, { cause: err })
|
|
130
134
|
}
|
|
131
135
|
try { return JSON.parse(raw) as ServerConfigFile }
|
|
132
136
|
catch (err) {
|
|
133
|
-
|
|
137
|
+
throw new Error(`Failed to parse ${path} as JSON: ${(err as Error)?.message ?? err}`, { cause: err })
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -145,20 +149,17 @@ function decodeTokenSecret(raw: string): Uint8Array<ArrayBuffer> {
|
|
|
145
149
|
// typo-detector below would fail with a misleading message.
|
|
146
150
|
const trimmed = raw.trim()
|
|
147
151
|
if (trimmed.length === 0) {
|
|
148
|
-
|
|
149
|
-
process.exit(1)
|
|
152
|
+
throw new Error('OBJSTORE_TOKEN_SECRET is empty after trimming whitespace')
|
|
150
153
|
}
|
|
151
154
|
const decoded = Buffer.from(trimmed, 'base64')
|
|
152
155
|
const reencoded = decoded.toString('base64')
|
|
153
156
|
const norm = (s: string): string => s.replace(/=+$/u, '')
|
|
154
157
|
if (norm(reencoded) !== norm(trimmed)) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
process.exit(1)
|
|
158
|
+
throw new Error('OBJSTORE_TOKEN_SECRET contains non-base64 characters (likely a typo, e.g. base64url chars in a base64 secret).\n' +
|
|
159
|
+
'Regenerate with: node -e \'console.log(require("crypto").randomBytes(32).toString("base64"))\'')
|
|
158
160
|
}
|
|
159
161
|
if (decoded.byteLength !== 32) {
|
|
160
|
-
|
|
161
|
-
process.exit(1)
|
|
162
|
+
throw new Error(`OBJSTORE_TOKEN_SECRET must decode to 32 bytes (got ${decoded.byteLength})`)
|
|
162
163
|
}
|
|
163
164
|
// Copy into a fresh ArrayBuffer so the type matches
|
|
164
165
|
// `Uint8Array<ArrayBuffer>` (Buffer may be SharedArrayBuffer-backed).
|
|
@@ -176,7 +177,7 @@ export function loadConfig(): Config {
|
|
|
176
177
|
const objstoreDir = env['OBJSTORE_DIR'] ?? join(dirname(dbPath), 'objstore')
|
|
177
178
|
// No practical upper bound beyond the safe-integer range.
|
|
178
179
|
const reapIntervalMs = intEnv('OBJSTORE_REAP_INTERVAL_MS', 10 * 60 * 1000, 1, Number.MAX_SAFE_INTEGER)
|
|
179
|
-
//
|
|
180
|
+
// Off-switch for automatic orphan reaping (both the boot sweep AND the
|
|
180
181
|
// periodic timer). '1' / 'true' (case-insensitive) → disabled; anything
|
|
181
182
|
// else, including unset, leaves it ON. Same boolean shape as TRUST_PROXY.
|
|
182
183
|
const reapDisabledEnv = env['OBJSTORE_REAP_DISABLED']
|
|
@@ -187,7 +188,7 @@ export function loadConfig(): Config {
|
|
|
187
188
|
const serverConfig = readServerConfigFile(configPath)
|
|
188
189
|
const rawPassword = serverConfig.password
|
|
189
190
|
if (rawPassword != null && typeof rawPassword !== 'string') {
|
|
190
|
-
|
|
191
|
+
throw new Error(`Invalid ${configPath}: "password" must be a string or null`)
|
|
191
192
|
}
|
|
192
193
|
const password = rawPassword ?? null
|
|
193
194
|
// Upper bound 65_536 — bounds memory under hostile load; a deployer
|
|
@@ -196,12 +197,7 @@ export function loadConfig(): Config {
|
|
|
196
197
|
// error-precedence order.
|
|
197
198
|
const maxInflightPerSocket = intEnv('MAX_INFLIGHT_PER_SOCKET', 64, 1, 65_536)
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
console.log(HELP)
|
|
201
|
-
process.exit(0)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const neonUrl = env['DATABASE_URL'] ?? null
|
|
200
|
+
const neonUrl = databaseUrls().e2e
|
|
205
201
|
const blobToken = env['BLOB_READ_WRITE_TOKEN'] ?? null
|
|
206
202
|
const tokenSecretB64 = env['OBJSTORE_TOKEN_SECRET'] ?? null
|
|
207
203
|
const tokenSecret = tokenSecretB64 ? decodeTokenSecret(tokenSecretB64) : null
|
package/server-e2e/db-neon.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// dialect on the wire.
|
|
5
5
|
//
|
|
6
6
|
// `@neondatabase/serverless` is an OPTIONAL peer dep — selected by
|
|
7
|
-
// the
|
|
7
|
+
// the configured database URL branch in `server-e2e/index.ts`. The peer dep
|
|
8
8
|
// itself is loaded lazily via the dynamic `import()` inside
|
|
9
9
|
// `openNeonDb` below, so a SQLite-only deployment never installs it
|
|
10
10
|
// (`autoInstallPeers: false` in `pnpm-workspace.yaml`) and never
|
package/server-e2e/db.ts
CHANGED
|
@@ -125,7 +125,7 @@ export type CommitResult =
|
|
|
125
125
|
// (`./db-neon.ts`) constructs a Handle with `db` unset. Callers that
|
|
126
126
|
// reach into `db` directly (e.g. `openObjstore`, test-only fixture
|
|
127
127
|
// SQL) are SQLite-coupled by construction; passing them a Neon-backed
|
|
128
|
-
// Handle is the operator's mistake to catch at the
|
|
128
|
+
// Handle is the operator's mistake to catch at the database URL
|
|
129
129
|
// switch in `server-e2e/index.ts`.
|
|
130
130
|
//
|
|
131
131
|
// `tryCommit` is the backend-specific atomic-commit primitive
|
package/server-e2e/http.ts
CHANGED
|
@@ -230,7 +230,19 @@ export function createHttpServer(deps: HttpServerDeps): Server {
|
|
|
230
230
|
res.end(NOT_FOUND_BODY)
|
|
231
231
|
})
|
|
232
232
|
|
|
233
|
+
installUpgradeHandler(httpServer, wss, isOriginAllowed)
|
|
234
|
+
|
|
235
|
+
return httpServer
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function installUpgradeHandler(httpServer: Server, wss: WebSocketServer, isOriginAllowed: HttpServerDeps['isOriginAllowed']): void {
|
|
233
239
|
httpServer.on('upgrade', (req, socket, head) => {
|
|
240
|
+
// Combined deployments must not let a managed share capability enter the
|
|
241
|
+
// unscoped sync transport, which bypasses the normal HTTP router.
|
|
242
|
+
if (req.headers['x-deepview-share'] !== undefined) {
|
|
243
|
+
socket.end('HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n')
|
|
244
|
+
return
|
|
245
|
+
}
|
|
234
246
|
// RFC 6455: the WS upgrade IS an HTTP request; reject with a normal
|
|
235
247
|
// HTTP response so a misconfigured client sees the JSON body instead
|
|
236
248
|
// of ECONNRESET. `socket.end(body)` flushes before sending FIN.
|
|
@@ -261,6 +273,4 @@ export function createHttpServer(deps: HttpServerDeps): Server {
|
|
|
261
273
|
}
|
|
262
274
|
wss.handleUpgrade(req, socket, head, (ws) => { wss.emit('connection', ws, req) })
|
|
263
275
|
})
|
|
264
|
-
|
|
265
|
-
return httpServer
|
|
266
276
|
}
|