dsh-vision-router 3.0.3 → 3.0.4
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/README.md +13 -10
- package/README.zh.md +11 -8
- package/cordis.patch.yml +14 -10
- package/docs/architecture/dsh-0.2.1-alpha.2-compat.md +41 -0
- package/docs/development/dsh-alpha3-compatibility.md +36 -0
- package/docs/development/dsh-alpha4-adaptation.md +12 -0
- package/docs/development/dsh-alpha4-validation-evidence.md +29 -0
- package/docs/development/dsh-alpha5-adaptation.md +31 -0
- package/docs/development/web-live-reload.md +157 -0
- package/docs/releases/v3.0.4.md +18 -0
- package/docs/remote-settings.md +4 -0
- package/docs/v2-capability-routing.md +25 -9
- package/lib/adapter-update-coalescer.js +41 -8
- package/lib/client-presentation-boundary-main.js +26 -3
- package/lib/client.js +20 -19
- package/lib/core-primitives.js +4 -0
- package/lib/maintenance-routes.js +7 -1
- package/lib/vision-backend-runtime-policy.js +3 -3
- package/lib/vision-background-benchmark.js +33 -5
- package/lib/vision-capability-benchmark-service.js +61 -23
- package/lib/vision-exact-check-client.js +24 -6
- package/lib/vision-image-input-verdict.js +5 -3
- package/lib/vision-toggle-root-hardening.js +20 -3
- package/lib/web-capability-boundary.js +164 -31
- package/package.json +6 -4
- package/presets/web-connection-ready/index.mjs +48 -0
- package/presets/web-connection-ready/package.json +6 -0
|
@@ -80,6 +80,9 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
|
|
|
80
80
|
VISION_CHECK_BACKEND_STALE:[text('当前模型暂时无法直接调用,请确认供应商/模型仍可用后重试','This model is not currently callable; verify the provider/model and retry')],
|
|
81
81
|
VISION_CHECK_TIMEOUT:[text('测试超时,已自动结束','Test timed out and was stopped')],
|
|
82
82
|
VISION_CHECK_UNSUPPORTED_IMAGE:[text('实测仅文本 · 图片请求被模型拒绝','Measured text-only · the model rejected image input')],
|
|
83
|
+
VISION_IMAGE_DELIVERY_UNAVAILABLE:[text('Host未声明图片输入,请在DSH模型设置中启用Image再测','Host declares text-only input; enable Image in DSH Models settings and retest')],
|
|
84
|
+
VISION_CHECK_STALE_CONFIG:[text('测试期间模型配置已变化,请重新测试','Model input settings changed during the test; retest required')],
|
|
85
|
+
VISION_LOCAL_ONLY_POLICY:[text('本地识图模式下不允许测试非本地后端','Local-only vision policy blocks testing nonlocal backends')],
|
|
83
86
|
VISION_CHECK_INFRASTRUCTURE:[text('识图测试组件暂不可用','Vision check infrastructure is unavailable')],
|
|
84
87
|
CAPABILITY_BENCHMARK_INFRASTRUCTURE:[text('识图测试组件暂不可用','Vision check infrastructure is unavailable')],
|
|
85
88
|
CAPABILITY_BENCHMARK_VISUAL_PROOF_FAILED:[text('模型没有正确读出测试图','The model did not correctly inspect the test image')]
|
|
@@ -91,9 +94,16 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
|
|
|
91
94
|
}
|
|
92
95
|
function reset(control){
|
|
93
96
|
if(!control)return;
|
|
97
|
+
delete control.dataset.lastResult;
|
|
94
98
|
setStatus(control,text('快速自检 · 1次请求 · 只验证当前模型能否识图 · 不写入Auto能力分数','Quick check · 1 request · verifies image input only · does not write Auto capability scores'));
|
|
95
99
|
setButton(control,text('测试识图','Test vision'),false);
|
|
96
100
|
}
|
|
101
|
+
function makeRemoteNotice(row){
|
|
102
|
+
var note=document.createElement('div');note.setAttribute(CONTROL_ATTR,'remote');
|
|
103
|
+
note.style.cssText='flex:1 0 100%;font-size:12px;opacity:.7;margin-top:4px';
|
|
104
|
+
note.textContent=text('识图自检仅可在DSH本机界面执行;远程页面不发送测试请求。','Vision testing is local-only. Open the DSH UI on the Host to run the image check.');
|
|
105
|
+
row.appendChild(note);return note;
|
|
106
|
+
}
|
|
97
107
|
function makeControl(row){
|
|
98
108
|
if(!row||!row.style)return undefined;
|
|
99
109
|
row.style.flexWrap='wrap';
|
|
@@ -131,15 +141,15 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
|
|
|
131
141
|
if(!currentRun(control,token,key))return;
|
|
132
142
|
if(!response.ok||!body||body.ok!==true){
|
|
133
143
|
var raw=String(body&&(body.error||body.code)||('HTTP '+response.status));
|
|
134
|
-
setStatus(control,'✗ '+selection.provider+'/'+selection.model+' · '+failureMessage(body,response.status),raw);notifyChanged();return;
|
|
144
|
+
setStatus(control,'✗ '+selection.provider+'/'+selection.model+' · '+failureMessage(body,response.status),raw);control.dataset.lastResult=String(Date.now());notifyChanged();return;
|
|
135
145
|
}
|
|
136
146
|
var via=transport(body.transport),latency=seconds(body.latencyMs),output=short(body.output,72);
|
|
137
147
|
var suffix=(via?' · '+via:'')+(latency?' · '+latency:'')+(output?' · '+output:'');
|
|
138
|
-
setStatus(control,'✓ '+selection.provider+'/'+selection.model+text(' · 图片识别正常',' · image verified')+suffix);notifyChanged();
|
|
148
|
+
setStatus(control,'✓ '+selection.provider+'/'+selection.model+text(' · 图片识别正常',' · image verified')+suffix+' · '+new Date().toLocaleTimeString());control.dataset.lastResult=String(Date.now());notifyChanged();
|
|
139
149
|
}catch(error){
|
|
140
150
|
if(!currentRun(control,token,key))return;
|
|
141
151
|
var message=error&&error.name==='AbortError'?text('测试超时,已自动结束','Test timed out and was stopped'):text('测试失败','Vision check failed');
|
|
142
|
-
setStatus(control,'✗ '+selection.provider+'/'+selection.model+' · '+message,String(error&&error.message||error));
|
|
152
|
+
setStatus(control,'✗ '+selection.provider+'/'+selection.model+' · '+message,String(error&&error.message||error));control.dataset.lastResult=String(Date.now());
|
|
143
153
|
}finally{
|
|
144
154
|
if(timer!==undefined)clearTimeout(timer);
|
|
145
155
|
if(ticker!==undefined)clearInterval(ticker);
|
|
@@ -148,12 +158,13 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
|
|
|
148
158
|
}
|
|
149
159
|
}
|
|
150
160
|
function scan(){
|
|
151
|
-
|
|
161
|
+
var local=localPage();
|
|
152
162
|
var rows=[];try{rows=Array.prototype.slice.call(document.querySelectorAll(ROW_SELECTOR));}catch(_){rows=[];}
|
|
153
163
|
rows.forEach(function(row){
|
|
154
164
|
var selection=rowSelection(row),control=row.querySelector('['+CONTROL_ATTR+']');
|
|
155
165
|
if(!complete(selection)){if(control){abortActive(control);if(control.remove)control.remove();}return;}
|
|
156
|
-
if(!control)control=makeControl(row);
|
|
166
|
+
if(!control)control=local?makeControl(row):makeRemoteNotice(row);
|
|
167
|
+
if(!local)return;
|
|
157
168
|
var key=selectionKey(selection);
|
|
158
169
|
if(control&&control.dataset.selection!==key){abortActive(control);control.dataset.selection=key;reset(control);}
|
|
159
170
|
});
|
|
@@ -167,8 +178,15 @@ export const VISION_EXACT_CHECK_CLIENT = String.raw`(function(){
|
|
|
167
178
|
}catch(_){}
|
|
168
179
|
return false;
|
|
169
180
|
}
|
|
170
|
-
function
|
|
181
|
+
function invalidatePreviousResults(){
|
|
171
182
|
if(!localPage())return;
|
|
183
|
+
var controls=document.querySelectorAll('['+CONTROL_ATTR+'][data-last-result]');
|
|
184
|
+
controls.forEach(function(control){abortActive(control);reset(control);
|
|
185
|
+
setStatus(control,text('配置可能已变化,请重新测试','Configuration may have changed; retest required'));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function start(){
|
|
189
|
+
window.addEventListener('focus',invalidatePreviousResults);
|
|
172
190
|
document.addEventListener('change',function(event){var row=event&&event.target&&event.target.closest?event.target.closest(ROW_SELECTOR):undefined;if(row)schedule();},true);
|
|
173
191
|
if(typeof MutationObserver==='function'&&document.documentElement){
|
|
174
192
|
var observer=new MutationObserver(function(records){
|
|
@@ -5,7 +5,7 @@ import process from 'node:process'
|
|
|
5
5
|
import { resolveDshHome } from './doctor.js'
|
|
6
6
|
import { CAPABILITY_BENCHMARK_SUITE_REVISION } from './vision-capability-benchmark.js'
|
|
7
7
|
|
|
8
|
-
const CACHE_VERSION =
|
|
8
|
+
const CACHE_VERSION = 2
|
|
9
9
|
const MAX_ENTRIES = 128
|
|
10
10
|
|
|
11
11
|
function cleanText(value, max = 256) {
|
|
@@ -14,7 +14,7 @@ function cleanText(value, max = 256) {
|
|
|
14
14
|
|
|
15
15
|
function cleanVerdict(value) {
|
|
16
16
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
17
|
-
if (value.state !== 'unsupported') return undefined
|
|
17
|
+
if (value.state !== 'unsupported' || value.hostMode !== 'native-image') return undefined
|
|
18
18
|
if (Number(value.suiteRevision) !== CAPABILITY_BENCHMARK_SUITE_REVISION) return undefined
|
|
19
19
|
const fingerprint = cleanText(value.fingerprint, 64)
|
|
20
20
|
if (!/^ep2_[0-9a-f]{32}$/.test(fingerprint)) return undefined
|
|
@@ -29,6 +29,7 @@ function cleanVerdict(value) {
|
|
|
29
29
|
provider,
|
|
30
30
|
model,
|
|
31
31
|
state: 'unsupported',
|
|
32
|
+
hostMode: 'native-image',
|
|
32
33
|
reason: 'provider-rejected-image',
|
|
33
34
|
measuredAt,
|
|
34
35
|
suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
@@ -103,7 +104,7 @@ export function createImageInputVerdictStore(options = {}) {
|
|
|
103
104
|
await ready
|
|
104
105
|
return records.get(String(fingerprint ?? ''))
|
|
105
106
|
},
|
|
106
|
-
async markUnsupported({ fingerprint, key, provider, model, measuredAt = Date.now() } = {}) {
|
|
107
|
+
async markUnsupported({ fingerprint, key, provider, model, hostMode, measuredAt = Date.now() } = {}) {
|
|
107
108
|
await ready
|
|
108
109
|
const clean = cleanVerdict({
|
|
109
110
|
fingerprint,
|
|
@@ -111,6 +112,7 @@ export function createImageInputVerdictStore(options = {}) {
|
|
|
111
112
|
provider,
|
|
112
113
|
model,
|
|
113
114
|
state: 'unsupported',
|
|
115
|
+
hostMode,
|
|
114
116
|
measuredAt,
|
|
115
117
|
suiteRevision: CAPABILITY_BENCHMARK_SUITE_REVISION,
|
|
116
118
|
})
|
|
@@ -345,7 +345,15 @@ const CLIENT_PRELUDE = String.raw`(function(){
|
|
|
345
345
|
})();`
|
|
346
346
|
|
|
347
347
|
function replaceAll(source, before, after) {
|
|
348
|
-
|
|
348
|
+
let result = source.includes(before) ? source.split(before).join(after) : source
|
|
349
|
+
// Windows git checkouts can put CRLF inside injected client-source template
|
|
350
|
+
// literals. Match only the same owned seam with CRLF; do not normalize or
|
|
351
|
+
// rewrite the surrounding HTML or any unrelated plugin's source.
|
|
352
|
+
if (before.includes('\n')) {
|
|
353
|
+
const windowsBefore = before.replaceAll('\n', '\r\n')
|
|
354
|
+
if (result.includes(windowsBefore)) result = result.split(windowsBefore).join(after)
|
|
355
|
+
}
|
|
356
|
+
return result
|
|
349
357
|
}
|
|
350
358
|
|
|
351
359
|
export function hardenVisionClientSource(source) {
|
|
@@ -403,7 +411,7 @@ export function hardenVisionClientSource(source) {
|
|
|
403
411
|
next = replaceAll(
|
|
404
412
|
next,
|
|
405
413
|
` var message = latest && typeof latest.error === 'string' && latest.error !== ''\n ? latest.error\n : t('failedUnknown');`,
|
|
406
|
-
` var
|
|
414
|
+
` var recoveryStore = typeof recovery === 'undefined' ? null : recovery;\n if (!recoveryStore && typeof window !== 'undefined') {\n var hardening = window.__dshVisionRouterRootHardening;\n if (hardening && typeof hardening.recoveryFor === 'function') recoveryStore = hardening.recoveryFor(directory);\n }\n var recovered = recoveryStore && typeof recoveryStore.getSnapshot === 'function' ? recoveryStore.getSnapshot() : null;\n var message = latest && typeof latest.error === 'string' && latest.error !== ''\n ? latest.error\n : recovered && recovered.message ? recovered.message : t('failedUnknown');`,
|
|
407
415
|
)
|
|
408
416
|
|
|
409
417
|
next = replaceAll(
|
|
@@ -431,10 +439,19 @@ export function hardenVisionClientSource(source) {
|
|
|
431
439
|
)
|
|
432
440
|
|
|
433
441
|
next = replaceAll(next, `function wrapStore(store, settings) {`, `function wrapStore(store, settings, recovery) {`)
|
|
442
|
+
// A synthetic recovery snapshot must retain referential identity until
|
|
443
|
+
// its Host source snapshot or recovery error actually changes. Otherwise
|
|
444
|
+
// React's useSyncExternalStore sees a new snapshot on every read and can
|
|
445
|
+
// enter a maximum-update-depth loop after a failed select + catalog refresh.
|
|
446
|
+
next = replaceAll(
|
|
447
|
+
next,
|
|
448
|
+
` var lastProjected;\n function getSnapshot() {`,
|
|
449
|
+
` var lastProjected;\n var lastRecoverySource;\n var lastRecoveryError;\n var lastRecoveryRaw;\n function getSnapshot() {`,
|
|
450
|
+
)
|
|
434
451
|
next = replaceAll(
|
|
435
452
|
next,
|
|
436
453
|
` var raw = typeof store.getSnapshot === 'function' ? store.getSnapshot() : undefined;\n var config = currentVisionConfig(settings);`,
|
|
437
|
-
` var raw = typeof store.getSnapshot === 'function' ? store.getSnapshot() : undefined;\n var recovered = recovery && typeof recovery.getSnapshot === 'function' ? recovery.getSnapshot() : null;\n if (recovered && raw && (raw.status === 'selecting' || raw.status === 'loading')) {\n raw = Object.assign({}, raw, { status: 'error', error: recovered.message || String(recovered) });\n }\n var config = currentVisionConfig(settings);`,
|
|
454
|
+
` var raw = typeof store.getSnapshot === 'function' ? store.getSnapshot() : undefined;\n var recovered = recovery && typeof recovery.getSnapshot === 'function' ? recovery.getSnapshot() : null;\n if (recovered && raw && (raw.status === 'selecting' || raw.status === 'loading')) {\n if (raw !== lastRecoverySource || recovered !== lastRecoveryError) {\n lastRecoverySource = raw;\n lastRecoveryError = recovered;\n lastRecoveryRaw = Object.assign({}, raw, { status: 'error', error: recovered.message || String(recovered) });\n }\n raw = lastRecoveryRaw;\n }\n var config = currentVisionConfig(settings);`,
|
|
438
455
|
)
|
|
439
456
|
next = replaceAll(
|
|
440
457
|
next,
|
|
@@ -126,17 +126,42 @@ function isLocalHostName(value) {
|
|
|
126
126
|
return host === 'localhost' || host.endsWith('.localhost') || isLoopbackAddress(host)
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Forwarding metadata can only *deny* a local-machine capability. Never
|
|
130
|
+
// interpret X-Forwarded-For or similar caller-controlled headers as proof
|
|
131
|
+
// that a request really originated at the local browser.
|
|
132
|
+
const PROXY_PROVENANCE_HEADERS = new Set([
|
|
133
|
+
'forwarded',
|
|
134
|
+
'x-forwarded-for',
|
|
135
|
+
'x-forwarded-host',
|
|
136
|
+
'x-forwarded-proto',
|
|
137
|
+
'x-real-ip',
|
|
138
|
+
'x-client-ip',
|
|
139
|
+
'true-client-ip',
|
|
140
|
+
'cf-connecting-ip',
|
|
141
|
+
'via',
|
|
142
|
+
])
|
|
143
|
+
|
|
144
|
+
function hasProxyProvenance(req) {
|
|
145
|
+
const headers = req?.headers
|
|
146
|
+
return !!headers && typeof headers === 'object' &&
|
|
147
|
+
Object.keys(headers).some((name) => PROXY_PROVENANCE_HEADERS.has(name.toLowerCase()))
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
/**
|
|
130
151
|
* Browser-facing local capability check. A real network request needs BOTH a
|
|
131
|
-
* loopback TCP peer and a loopback/localhost Host header
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
152
|
+
* loopback TCP peer and a loopback/localhost Host header, with no forwarding
|
|
153
|
+
* metadata. This protects supported proxy deployments preserving external Host
|
|
154
|
+
* and fails closed when a proxy rewrites Host but keeps standard hop markers.
|
|
155
|
+
* A proxy that erases every originating fact is indistinguishable from a
|
|
156
|
+
* genuine loopback browser request; these headers are not an attestation.
|
|
157
|
+
* Internal direct handler calls have no socket and remain supported.
|
|
135
158
|
*/
|
|
136
159
|
export function isLocalUiRequest(req) {
|
|
137
160
|
const transport = req?.socket ?? req?.connection
|
|
138
161
|
if (!transport) return true
|
|
139
|
-
return isLoopbackAddress(transport.remoteAddress) &&
|
|
162
|
+
return isLoopbackAddress(transport.remoteAddress) &&
|
|
163
|
+
isLocalHostName(requestHostName(req)) &&
|
|
164
|
+
!hasProxyProvenance(req)
|
|
140
165
|
}
|
|
141
166
|
|
|
142
167
|
export function isLocalMutationRoute(path, method) {
|
|
@@ -204,40 +229,148 @@ function redactRemoteBody(path, body) {
|
|
|
204
229
|
return body
|
|
205
230
|
}
|
|
206
231
|
|
|
207
|
-
|
|
232
|
+
// Remote metadata is small. Never buffer an unbounded response from a future
|
|
233
|
+
// handler revision: local-only diagnostics should not turn into remote memory DoS.
|
|
234
|
+
const MAX_REMOTE_REDACTION_BYTES = 1024 * 1024
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A remote response requiring redaction must be completely validated *before*
|
|
238
|
+
* emitting headers or bytes. Unlike intercepting end() alone, this also covers
|
|
239
|
+
* write()+end(), malformed JSON, oversize bodies and async stream completion.
|
|
240
|
+
* No handler may transmit its raw token, path or other local fields while the
|
|
241
|
+
* remote body remains unvalidated.
|
|
242
|
+
*/
|
|
208
243
|
function runWithRemoteReadRedaction(path, handler, req, res) {
|
|
209
244
|
const originalEnd = res?.end
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
245
|
+
const originalWrite = res?.write
|
|
246
|
+
const originalWriteHead = res?.writeHead
|
|
247
|
+
const originalFlushHeaders = res?.flushHeaders
|
|
248
|
+
if (typeof originalEnd !== 'function' || typeof originalWriteHead !== 'function') {
|
|
249
|
+
throw new TypeError('vision-router: remote redaction requires a buffered HTTP response')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const chunks = []
|
|
253
|
+
let bytes = 0
|
|
254
|
+
let done = false
|
|
255
|
+
let head
|
|
256
|
+
const restore = () => {
|
|
257
|
+
if (res.end === redactedEnd) res.end = originalEnd
|
|
258
|
+
if (res.write === redactedWrite) {
|
|
259
|
+
if (originalWrite === undefined) delete res.write
|
|
260
|
+
else res.write = originalWrite
|
|
261
|
+
}
|
|
262
|
+
if (res.writeHead === redactedWriteHead) res.writeHead = originalWriteHead
|
|
263
|
+
if (res.flushHeaders === redactedFlushHeaders) {
|
|
264
|
+
if (originalFlushHeaders === undefined) delete res.flushHeaders
|
|
265
|
+
else res.flushHeaders = originalFlushHeaders
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const stripStagedHeaders = () => {
|
|
269
|
+
// Route handlers own the pre-redaction response. Even header values may
|
|
270
|
+
// contain future local-only tokens, paths or Set-Cookie material.
|
|
271
|
+
// Never forward any unreviewed staged header to a remote client.
|
|
215
272
|
try {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
} catch {
|
|
222
|
-
// If a future handler stops returning JSON, do not corrupt its response.
|
|
273
|
+
for (const key of res.getHeaderNames?.() ?? []) res.removeHeader?.(key)
|
|
274
|
+
} catch { /* a test double may not expose the header table */ }
|
|
275
|
+
for (const key of ['content-length', 'content-encoding', 'transfer-encoding']) {
|
|
276
|
+
try { res.removeHeader?.(key) } catch {}
|
|
223
277
|
}
|
|
224
|
-
return originalEnd.call(this, next, ...args)
|
|
225
278
|
}
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
279
|
+
const safeHeaders = () => ({
|
|
280
|
+
'content-type': 'application/json; charset=utf-8',
|
|
281
|
+
'cache-control': 'no-store',
|
|
282
|
+
})
|
|
283
|
+
const complete = (body, rejected = false, callback) => {
|
|
284
|
+
if (done) return res
|
|
285
|
+
done = true
|
|
286
|
+
// On rejection, deliberately retain the buffered wrappers after sending
|
|
287
|
+
// the safe error. A drifting async handler may continue calling write()
|
|
288
|
+
// or end(); restoring the raw methods here would expose those later bytes.
|
|
289
|
+
// The response is already ended and these wrappers swallow late writes.
|
|
290
|
+
if (!rejected) restore()
|
|
291
|
+
stripStagedHeaders()
|
|
292
|
+
if (rejected) {
|
|
293
|
+
// An invalid or oversized response is an internal gateway failure. Do
|
|
294
|
+
// not repeat the untrusted data or send previously staged headers.
|
|
295
|
+
originalWriteHead.call(res, 502, safeHeaders())
|
|
296
|
+
return originalEnd.call(res, JSON.stringify({
|
|
297
|
+
ok: false,
|
|
298
|
+
error: 'remote response could not be safely redacted',
|
|
299
|
+
}), callback)
|
|
300
|
+
}
|
|
301
|
+
if (head) {
|
|
302
|
+
originalWriteHead.call(res, head.status, safeHeaders())
|
|
303
|
+
} else {
|
|
304
|
+
originalWriteHead.call(res, Number.isInteger(res.statusCode) ? res.statusCode : 200, safeHeaders())
|
|
305
|
+
}
|
|
306
|
+
return originalEnd.call(res, body, callback)
|
|
230
307
|
}
|
|
231
|
-
|
|
308
|
+
const append = (chunk, encoding) => {
|
|
309
|
+
if (chunk === undefined || chunk === null) return true
|
|
310
|
+
let value
|
|
311
|
+
try {
|
|
312
|
+
if (Buffer.isBuffer(chunk)) value = chunk
|
|
313
|
+
else if (typeof chunk === 'string') value = Buffer.from(chunk, typeof encoding === 'string' ? encoding : 'utf8')
|
|
314
|
+
else if (chunk instanceof Uint8Array) value = Buffer.from(chunk)
|
|
315
|
+
else return false
|
|
316
|
+
} catch { return false }
|
|
317
|
+
if (value.length > MAX_REMOTE_REDACTION_BYTES - bytes) return false
|
|
318
|
+
bytes += value.length
|
|
319
|
+
chunks.push(value)
|
|
320
|
+
return true
|
|
321
|
+
}
|
|
322
|
+
const failed = () => complete(undefined, true)
|
|
323
|
+
function redactedWriteHead(status, maybeMessage, maybeHeaders) {
|
|
324
|
+
if (done) return this
|
|
325
|
+
const message = typeof maybeMessage === 'string' ? maybeMessage : undefined
|
|
326
|
+
const headers = message === undefined ? maybeMessage : maybeHeaders
|
|
327
|
+
if (head || !Number.isInteger(status) || status < 100 || status > 599 ||
|
|
328
|
+
(headers !== undefined && (!headers || typeof headers !== 'object' || Array.isArray(headers)))) {
|
|
329
|
+
failed()
|
|
330
|
+
return this
|
|
331
|
+
}
|
|
332
|
+
head = { status, message, headers }
|
|
333
|
+
return this
|
|
334
|
+
}
|
|
335
|
+
function redactedFlushHeaders() {
|
|
336
|
+
// Deferring the header is essential: once sent, an invalid JSON body
|
|
337
|
+
// cannot be replaced by a safe 502.
|
|
338
|
+
}
|
|
339
|
+
function redactedWrite(chunk, encoding, callback) {
|
|
340
|
+
const completeWrite = typeof encoding === 'function' ? encoding : callback
|
|
341
|
+
if (!done && !append(chunk, encoding)) failed()
|
|
342
|
+
if (typeof completeWrite === 'function') queueMicrotask(completeWrite)
|
|
343
|
+
return !done
|
|
344
|
+
}
|
|
345
|
+
function redactedEnd(chunk, encoding, callback) {
|
|
346
|
+
if (done) return res
|
|
347
|
+
const completeEnd = typeof encoding === 'function' ? encoding : callback
|
|
348
|
+
if (!append(chunk, encoding)) return failed()
|
|
349
|
+
try {
|
|
350
|
+
const body = JSON.parse(Buffer.concat(chunks, bytes).toString('utf8'))
|
|
351
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return failed()
|
|
352
|
+
const redacted = JSON.stringify(redactRemoteBody(path, body))
|
|
353
|
+
if (Buffer.byteLength(redacted, 'utf8') > MAX_REMOTE_REDACTION_BYTES) return failed()
|
|
354
|
+
return complete(redacted, false, completeEnd)
|
|
355
|
+
} catch {
|
|
356
|
+
return failed()
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
res.writeHead = redactedWriteHead
|
|
361
|
+
res.write = redactedWrite
|
|
362
|
+
res.end = redactedEnd
|
|
363
|
+
res.flushHeaders = redactedFlushHeaders
|
|
232
364
|
try {
|
|
233
|
-
result = handler(req, res)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
365
|
+
const result = handler(req, res)
|
|
366
|
+
if (result && typeof result.then === 'function') {
|
|
367
|
+
return Promise.resolve(result).catch(() => { if (!done) failed() })
|
|
368
|
+
}
|
|
369
|
+
return result
|
|
370
|
+
} catch {
|
|
371
|
+
if (!done) failed()
|
|
372
|
+
return undefined
|
|
237
373
|
}
|
|
238
|
-
if (result && typeof result.then === 'function') return result.finally(restore)
|
|
239
|
-
restore()
|
|
240
|
-
return result
|
|
241
374
|
}
|
|
242
375
|
|
|
243
376
|
function guardedRoute(route, getConnection) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain + pixel-level tools. Remote vision providers, including the default free fallback, receive image content unless strict local-only use is configured.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"presets"
|
|
36
36
|
],
|
|
37
37
|
"engines": {
|
|
38
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
38
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
39
|
+
"dsh": ">=0.1.5-rc.1 <0.2.0-0 || >=0.2.0-rc.2 <0.3.0-0"
|
|
39
40
|
},
|
|
40
41
|
"packageManager": "pnpm@11.20.0",
|
|
41
42
|
"keywords": [
|
|
@@ -89,13 +90,13 @@
|
|
|
89
90
|
"@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2"
|
|
90
91
|
},
|
|
91
92
|
"scripts": {
|
|
92
|
-
"test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/cache-atomic-write-concurrency.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/degraded-local-evidence.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/release-registry-identity.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/activation-shim-contract.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/official-deepseek-catalog.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/provider-transport-threading.test.js tests/architecture-2.3-gate.test.js tests/host-settings-seam.test.js tests/drift-canary-settings-seam.test.js tests/esm-cycle-audit.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/local-only-vision.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/vision-diagnostics-routes.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/free-cloud-first.test.js tests/dsh-host-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/issue-512-tool-restriction.test.js tests/issue-606-session-source-ownership.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/provider-probe-proxy-scope.test.js tests/lifecycle-invariants.test.js tests/vision-tool-settings-generation.test.js tests/release-package-content-identity.test.js tests/release-state-machine.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/security-resource-boundaries.test.js tests/security-property-fuzz.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/long-ocr-empty-retry.test.js tests/tesseract-adaptive-ocr.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-event-feed.test.js tests/session-vision-policy.test.js tests/session-vision-policy-domain.test.js tests/session-vision-delegation.test.js tests/session-vision-delegation-boundary.test.js tests/subagent-vision-inheritance-characterization.test.js tests/subagent-vision-runtime-integration.test.js tests/subagent-vision-cold-resume.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && pnpm test:regression-closure && node --test tests/vision-backend-runtime-policy.test.js tests/vision-router-adversarial-review-fixes.test.js tests/web-panel-delegation.test.js",
|
|
93
|
+
"test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/cache-atomic-write-concurrency.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/degraded-local-evidence.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/dev-staged-build.test.js tests/dev-web-reload.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/release-registry-identity.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/activation-shim-contract.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/official-deepseek-catalog.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/provider-transport-threading.test.js tests/architecture-2.3-gate.test.js tests/host-settings-seam.test.js tests/drift-canary-settings-seam.test.js tests/esm-cycle-audit.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/local-only-vision.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/vision-diagnostics-routes.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/free-cloud-first.test.js tests/dsh-host-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/issue-512-tool-restriction.test.js tests/issue-606-session-source-ownership.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/provider-probe-proxy-scope.test.js tests/lifecycle-invariants.test.js tests/vision-tool-settings-generation.test.js tests/release-package-content-identity.test.js tests/release-state-machine.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/security-resource-boundaries.test.js tests/security-property-fuzz.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/long-ocr-empty-retry.test.js tests/tesseract-adaptive-ocr.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-event-feed.test.js tests/session-vision-policy.test.js tests/session-vision-policy-domain.test.js tests/session-vision-delegation.test.js tests/session-vision-delegation-boundary.test.js tests/subagent-vision-inheritance-characterization.test.js tests/subagent-vision-runtime-integration.test.js tests/subagent-vision-cold-resume.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && pnpm test:regression-closure && node --test tests/vision-backend-runtime-policy.test.js tests/vision-router-adversarial-review-fixes.test.js tests/web-panel-delegation.test.js",
|
|
93
94
|
"test:core": "node --test tests/core.test.js tests/degraded-local-evidence.test.js tests/runtime-e2e.test.js tests/structured-bootstrap.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-execution-policy.test.js tests/vision-backend-runtime-policy.test.js",
|
|
94
95
|
"test:routing": "node --test tests/issue-374-delegated-call-config.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/vision-routing-product.test.js tests/vision-capability-router.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow-health.test.js tests/vision-runtime-performance.test.js tests/vision-background-benchmark.test.js tests/vision-image-input-verdict.test.js",
|
|
95
96
|
"test:session": "node --test tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-policy.test.js tests/session-vision-delegation.test.js tests/session-vision-delegation-boundary.test.js tests/subagent-vision-inheritance-characterization.test.js tests/subagent-vision-runtime-integration.test.js tests/subagent-vision-cold-resume.test.js tests/session-vision-policy-domain.test.js tests/replay-delegation.test.js tests/legacy-session-repair.test.js tests/session-repair-v2.test.js tests/issue-276-session-image-ownership.test.js",
|
|
96
97
|
"test:resources": "node --test tests/artifact-path-security.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js",
|
|
97
98
|
"test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/dsh-host-compat.test.js tests/http-compat.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/native-image-coexistence.test.js tests/tesseract-node24-boot.test.js tests/dsh-host-capabilities.test.js",
|
|
98
|
-
"test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-acceptance-regression.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/local-remote-settings-permission.test.js tests/v2-settings-ia-integration.test.js tests/logging-ui.test.js",
|
|
99
|
+
"test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-acceptance-regression.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/local-remote-settings-permission.test.js tests/v2-settings-ia-integration.test.js tests/logging-ui.test.js tests/dev-staged-build.test.js tests/dev-web-reload.test.js",
|
|
99
100
|
"test:contract": "node --test tests/manifest-dependencies.test.js tests/bundle-defaults.test.js tests/zero-regression-gate.test.js tests/issue-289-native-nonintervention.test.js tests/issue-374-delegated-call-config.test.js tests/runtime-boundary-fixes.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/adapter-prepare-call-compat.test.js tests/dsh-host-compat.test.js tests/attachment-admission-policy.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js",
|
|
100
101
|
"test:grounding": "node --test tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/vision-capability-grounding-proof-contract.test.js",
|
|
101
102
|
"test:stress": "node scripts/image-resource-stress.mjs && node --test tests/large-image-resource-integration.test.js tests/resource-retention-lifecycle.test.js",
|
|
@@ -109,6 +110,7 @@
|
|
|
109
110
|
"client:sync": "node scripts/sync-client-embedded-modules.mjs && pnpm build",
|
|
110
111
|
"client:check": "node scripts/sync-client-embedded-modules.mjs --check && pnpm build:check",
|
|
111
112
|
"build": "node scripts/build-source-artifacts.mjs",
|
|
113
|
+
"dev:web": "node scripts/dev-web-reload.mjs",
|
|
112
114
|
"build:check": "node scripts/build-source-artifacts.mjs --check",
|
|
113
115
|
"pretest": "pnpm build",
|
|
114
116
|
"build:reproducible": "node scripts/verify-build-reproducibility.mjs",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private, fail-closed bridge for the two supported DSH Web trust generations.
|
|
3
|
+
*
|
|
4
|
+
* It waits for the Host-owned authority source instead of supplying a fake
|
|
5
|
+
* webRuntime service or guessing additional trusted hosts. Its only consumer
|
|
6
|
+
* is the DVR connection row; Host Connection retains all authentication and
|
|
7
|
+
* route ownership.
|
|
8
|
+
*/
|
|
9
|
+
export const name = 'vision-router-web-connection-ready'
|
|
10
|
+
export const inject = ['webStartup', 'webServer']
|
|
11
|
+
export const SERVICE = 'visionRouterWebConnectionReady'
|
|
12
|
+
|
|
13
|
+
/** Return a detached, immutable copy of the Host-approved authority list. */
|
|
14
|
+
export function validatedTrustedHosts(value) {
|
|
15
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
16
|
+
throw new Error('Vision Router Web compatibility: Host trust list is invalid')
|
|
17
|
+
}
|
|
18
|
+
return Object.freeze([...value])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function classifyHostWebGeneration(webServer) {
|
|
22
|
+
const protocol = webServer?.protocol
|
|
23
|
+
if (protocol === 'http:' || protocol === 'https:') return 'startup'
|
|
24
|
+
if (protocol === undefined) return 'legacy-runtime'
|
|
25
|
+
throw new Error('Vision Router Web compatibility: unknown Host Web protocol')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function apply(ctx) {
|
|
29
|
+
const generation = classifyHostWebGeneration(ctx.webServer)
|
|
30
|
+
if (generation === 'startup') {
|
|
31
|
+
// 0.2.1-alpha.2+: Web Startup owns invocation trust; Web Runtime no
|
|
32
|
+
// longer provides a webRuntime service. Do not resurrect that service.
|
|
33
|
+
ctx.provide(SERVICE, Object.freeze({
|
|
34
|
+
trustedHosts: validatedTrustedHosts(ctx.webStartup.trustedHosts),
|
|
35
|
+
}))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Older supported Hosts materialize additional bind-derived LAN addresses
|
|
40
|
+
// in their native webRuntime service. Wait for the real service rather than
|
|
41
|
+
// falling back to the earlier webStartup list (which would silently lose
|
|
42
|
+
// LAN authority and race asynchronous Web runtime activation).
|
|
43
|
+
ctx.inject(['webRuntime'], (legacyCtx) => {
|
|
44
|
+
legacyCtx.provide(SERVICE, Object.freeze({
|
|
45
|
+
trustedHosts: validatedTrustedHosts(legacyCtx.webRuntime.trustedHosts),
|
|
46
|
+
}))
|
|
47
|
+
})
|
|
48
|
+
}
|