dsh-vision-router 2.2.0 → 2.2.2

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.
@@ -31,6 +31,14 @@ export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
31
31
  return undefined;
32
32
  }
33
33
 
34
+ function isLoopbackLocation(locationLike) {
35
+ var hostname = locationLike && typeof locationLike.hostname === 'string'
36
+ ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
37
+ : '';
38
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
39
+ return /^127(?:\.\d{1,3}){3}$/.test(hostname);
40
+ }
41
+
34
42
  async function writePermission(operation, value, revision) {
35
43
  if (typeof fetch !== 'function') throw new Error('Vision Router local settings transport is unavailable');
36
44
  var payload = { operation: operation };
@@ -269,6 +277,11 @@ export const SETTINGS_RC8_CLIENT_PRELUDE = String.raw`(function(){
269
277
  if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
270
278
  var wrapped = new Proxy(connection, {
271
279
  get: function(target, property) {
280
+ if (property === 'isLoopback') {
281
+ var locationLike;
282
+ try { locationLike = window && window.location; } catch (_) { locationLike = undefined; }
283
+ if (isLoopbackLocation(locationLike)) return true;
284
+ }
272
285
  if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target));
273
286
  var value = Reflect.get(target, property, target);
274
287
  return typeof value === 'function' ? value.bind(target) : value;
@@ -3,7 +3,17 @@ import { createSessionTurnResolver } from './session-turn-resolver.js'
3
3
 
4
4
  function legacyTurnNumberOf(session) {
5
5
  try {
6
- const events = session?.events
6
+ // dsh 0.1.2-alpha.4 removed the bare `session.events` array in favor of
7
+ // `session.snapshotEvents()`; mirror legacySessionEvents
8
+ // (session-vision-index.js), which prefers the snapshot when both surfaces
9
+ // exist — transitional Hosts keep the bare array but leave it stale, so a
10
+ // bare-array-first read would resolve a stale turn on them.
11
+ let events
12
+ if (typeof session?.snapshotEvents === 'function') {
13
+ events = session.snapshotEvents()
14
+ } else {
15
+ events = session?.events
16
+ }
7
17
  if (!Array.isArray(events)) return 0
8
18
  const last = events.findLast((event) => event && event.type === 'turn/start')
9
19
  return last && Number.isInteger(last.data?.turn) ? last.data.turn : 0
@@ -0,0 +1,299 @@
1
+ import {
2
+ DSH_017_SETTINGS_COMPAT_MARK,
3
+ LOCAL_SETTINGS_PATH,
4
+ } from '../dsh-settings-017-compat.js'
5
+
6
+ const BODY_LIMIT_BYTES = 256 * 1024
7
+ const ROOT_TRANSPORT_REGISTRY_KEY = Symbol.for('dsh-vision-router.settings-017-root-local-transport')
8
+ const SLOW_MUTATION_MS = 5_000
9
+ const TRACE_ENV = 'DVR_SETTINGS_017_TRACE'
10
+
11
+ function objectLike(value) {
12
+ return value !== null && typeof value === 'object'
13
+ }
14
+
15
+ function trace(stage, detail) {
16
+ if (process.env[TRACE_ENV] !== '1') return
17
+ try {
18
+ console.warn(`vision-router: DSH 0.1.7 settings trace ${stage}${detail === undefined ? '' : ` ${JSON.stringify(detail)}`}`)
19
+ } catch {}
20
+ }
21
+
22
+ function transportRegistry() {
23
+ let registry = globalThis[ROOT_TRANSPORT_REGISTRY_KEY]
24
+ if (!(registry instanceof WeakMap)) {
25
+ registry = new WeakMap()
26
+ Object.defineProperty(globalThis, ROOT_TRANSPORT_REGISTRY_KEY, {
27
+ value: registry,
28
+ configurable: true,
29
+ })
30
+ }
31
+ return registry
32
+ }
33
+
34
+ function rootOf(ctx) {
35
+ try {
36
+ if (objectLike(ctx?.root)) return ctx.root
37
+ } catch {}
38
+ return ctx
39
+ }
40
+
41
+ function stateFor(root) {
42
+ const registry = transportRegistry()
43
+ let state = registry.get(root)
44
+ if (!state) {
45
+ state = {
46
+ current: undefined,
47
+ routeOwner: undefined,
48
+ routeInstalling: false,
49
+ generationSequence: 0,
50
+ requestSequence: 0,
51
+ }
52
+ registry.set(root, state)
53
+ }
54
+ return state
55
+ }
56
+
57
+ function serviceOf(ctx, name) {
58
+ try {
59
+ const value = typeof ctx?.get === 'function' ? ctx.get(name) : undefined
60
+ if (value !== undefined && value !== null) return value
61
+ } catch {}
62
+ try {
63
+ const value = ctx?.[name]
64
+ return value === undefined || value === null ? undefined : value
65
+ } catch {
66
+ return undefined
67
+ }
68
+ }
69
+
70
+ function compatibleSettings(ctx) {
71
+ const settings = serviceOf(ctx, 'settings')
72
+ if (settings?.[DSH_017_SETTINGS_COMPAT_MARK] !== true) return undefined
73
+ if (typeof settings.describe !== 'function' || typeof settings.mutate !== 'function') return undefined
74
+ return settings
75
+ }
76
+
77
+ function namespaceDescriptor(settings) {
78
+ const descriptors = settings.describe({ redactSecrets: true })
79
+ return Array.isArray(descriptors) ? descriptors.find((entry) => entry?.ns === 'vision-router') : undefined
80
+ }
81
+
82
+ function sendJson(res, status, body) {
83
+ res.writeHead(status, {
84
+ 'content-type': 'application/json; charset=utf-8',
85
+ 'cache-control': 'no-store',
86
+ })
87
+ res.end(JSON.stringify(body))
88
+ }
89
+
90
+ async function readJson(req) {
91
+ let size = 0
92
+ const chunks = []
93
+ for await (const chunk of req) {
94
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
95
+ size += bytes.length
96
+ if (size > BODY_LIMIT_BYTES) throw Object.assign(new Error('request body too large'), { statusCode: 413 })
97
+ chunks.push(bytes)
98
+ }
99
+ if (chunks.length === 0) return {}
100
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'))
101
+ }
102
+
103
+ function currentSettings(state) {
104
+ const generation = state.current
105
+ if (!generation || generation.active === false) return undefined
106
+ try {
107
+ return generation.resolve()
108
+ } catch {
109
+ return undefined
110
+ }
111
+ }
112
+
113
+ function flattenEffectLabels(effects) {
114
+ const labels = []
115
+ const visit = (rows) => {
116
+ if (!Array.isArray(rows)) return
117
+ for (const row of rows) {
118
+ if (!row || typeof row !== 'object') continue
119
+ if (typeof row.label === 'string') labels.push(row.label)
120
+ visit(row.children)
121
+ }
122
+ }
123
+ visit(effects)
124
+ return labels
125
+ }
126
+
127
+ function slowMutationSnapshot(root) {
128
+ const loader = serviceOf(root, 'loader')
129
+ let entries = []
130
+ try { entries = typeof loader?.entries === 'function' ? [...loader.entries()] : [] } catch {}
131
+ return entries.flatMap((entry) => {
132
+ const fiber = entry?.fiber
133
+ if (!fiber || (entry?.options?.id !== 'vision-router' && !fiber.inertia)) return []
134
+ let effects = []
135
+ try { effects = typeof fiber.getEffects === 'function' ? flattenEffectLabels(fiber.getEffects()) : [] } catch {}
136
+ return [{
137
+ id: entry?.options?.id,
138
+ state: fiber.state,
139
+ inertia: Boolean(fiber.inertia),
140
+ effects,
141
+ }]
142
+ })
143
+ }
144
+
145
+ function beginSlowMutationWatch(root, requestId) {
146
+ return setTimeout(() => {
147
+ const snapshot = slowMutationSnapshot(root)
148
+ const message = `vision-router: DSH 0.1.7 settings mutation still reconciling after ${SLOW_MUTATION_MS}ms request=${requestId} ${JSON.stringify(snapshot)}`
149
+ try {
150
+ if (root?.logger && typeof root.logger.warn === 'function') root.logger.warn(message)
151
+ else console.warn(message)
152
+ } catch {
153
+ try { console.warn(message) } catch {}
154
+ }
155
+ }, SLOW_MUTATION_MS)
156
+ }
157
+
158
+ function routeFor(root, state) {
159
+ return {
160
+ kind: 'exact',
161
+ path: LOCAL_SETTINGS_PATH,
162
+ async handler(req, res) {
163
+ const requestId = ++state.requestSequence
164
+ trace('handler-enter', { requestId, method: req.method, generation: state.current?.id })
165
+ const settings = currentSettings(state)
166
+ if (!settings) {
167
+ trace('settings-unavailable', { requestId })
168
+ sendJson(res, 404, { ok: false, error: { code: 'settings-unavailable', message: 'Vision Router settings are unavailable' } })
169
+ return
170
+ }
171
+
172
+ if (req.method === 'GET') {
173
+ const descriptor = namespaceDescriptor(settings)
174
+ if (!descriptor) {
175
+ sendJson(res, 404, { ok: false, error: { code: 'settings-unavailable', message: 'Vision Router settings are unavailable' } })
176
+ return
177
+ }
178
+ sendJson(res, 200, { ok: true, value: { ...descriptor, writable: settings.writable === true } })
179
+ trace('response', { requestId, status: 200, revision: descriptor.revision })
180
+ return
181
+ }
182
+
183
+ if (req.method !== 'POST') {
184
+ res.setHeader('Allow', 'GET, POST')
185
+ sendJson(res, 405, { ok: false, error: { code: 'method-not-allowed', message: 'method not allowed' } })
186
+ return
187
+ }
188
+
189
+ let slowTimer
190
+ try {
191
+ trace('body-read-start', { requestId })
192
+ const payload = await readJson(req)
193
+ trace('body-read-done', { requestId, expectedRevision: payload?.expectedRevision, ops: payload?.ops?.length })
194
+ slowTimer = beginSlowMutationWatch(root, requestId)
195
+ trace('mutate-start', { requestId, generation: state.current?.id })
196
+ await settings.mutate('vision-router', payload?.ops, payload?.expectedRevision)
197
+ trace('mutate-done', { requestId, generation: state.current?.id })
198
+
199
+ // ConfigEditor.edit() reconciles the profile and can dispose the plugin
200
+ // generation that accepted this request. The route itself is owned by a
201
+ // root child fiber that depends only on WebServer, so finish the response
202
+ // from the newly-mounted DVR generation when present.
203
+ const afterSettings = currentSettings(state) ?? settings
204
+ const descriptor = namespaceDescriptor(afterSettings)
205
+ if (!descriptor) throw new Error('Vision Router settings disappeared after the write')
206
+ sendJson(res, 200, { ok: true, value: { ...descriptor, writable: afterSettings.writable === true } })
207
+ trace('response', { requestId, status: 200, revision: descriptor.revision })
208
+ } catch (error) {
209
+ const conflict = error?.code === 'SETTINGS_CONFLICT'
210
+ const status = error?.statusCode ?? (conflict ? 409 : 400)
211
+ trace('response-error', { requestId, status, code: error?.code, message: error?.message ?? String(error) })
212
+ sendJson(res, status, {
213
+ ok: false,
214
+ error: {
215
+ code: conflict ? 'settings-conflict' : 'settings-rejected',
216
+ message: error?.message ?? String(error),
217
+ ...(conflict ? { details: { expected: error.expected, actual: error.actual } } : {}),
218
+ },
219
+ })
220
+ } finally {
221
+ if (slowTimer) clearTimeout(slowTimer)
222
+ }
223
+ },
224
+ }
225
+ }
226
+
227
+ function ensureRootRoute(root, state) {
228
+ if (state.routeOwner || state.routeInstalling) return
229
+ if (typeof root?.inject !== 'function') {
230
+ throw new Error('Vision Router requires root dependency injection for the DSH 0.1.7 local settings route')
231
+ }
232
+
233
+ state.routeInstalling = true
234
+ try {
235
+ const owner = root.inject(['webServer'], (webCtx) => {
236
+ if (!webCtx?.webServer || typeof webCtx.webServer.register !== 'function' || typeof webCtx.effect !== 'function') {
237
+ throw new Error('Vision Router requires WebServer route registration on DSH 0.1.7')
238
+ }
239
+ trace('route-register', {})
240
+ webCtx.effect(
241
+ () => {
242
+ const dispose = webCtx.webServer.register(routeFor(root, state))
243
+ return () => {
244
+ trace('route-dispose', {})
245
+ dispose()
246
+ }
247
+ },
248
+ 'vision-router: DSH 0.1.7 root local settings transport',
249
+ )
250
+ })
251
+ // `root.inject()` creates a root child fiber. It is independent from the
252
+ // DVR plugin fiber and therefore survives ConfigEditor reconciliation of
253
+ // the `vision-router` entry. Keep the owner reachable for the process life.
254
+ state.routeOwner = owner ?? true
255
+ } catch (error) {
256
+ state.routeOwner = undefined
257
+ throw error
258
+ } finally {
259
+ state.routeInstalling = false
260
+ }
261
+ }
262
+
263
+ /**
264
+ * ConfigEditor.edit() reconciles the plugin configuration and therefore disposes
265
+ * the DVR plugin fiber while a Settings POST is still in flight. A dedicated
266
+ * root child fiber owns the HTTP route and depends only on WebServer; individual
267
+ * DVR generations only publish a resolver for their current settings facade.
268
+ */
269
+ export function installDsh017RootLocalSettingsTransport(ctx) {
270
+ if (!ctx || typeof ctx.inject !== 'function') return
271
+ const root = rootOf(ctx)
272
+ if (!objectLike(root)) return
273
+ const state = stateFor(root)
274
+
275
+ ctx.inject(['settings'], (settingsCtx) => {
276
+ const mounted = compatibleSettings(settingsCtx)
277
+ if (!mounted) return
278
+
279
+ const generation = {
280
+ id: ++state.generationSequence,
281
+ active: true,
282
+ resolve: () => compatibleSettings(ctx) ?? mounted,
283
+ }
284
+ state.current = generation
285
+ trace('generation-mount', { generation: generation.id })
286
+ ensureRootRoute(root, state)
287
+
288
+ if (typeof settingsCtx?.effect === 'function') {
289
+ settingsCtx.effect(
290
+ () => () => {
291
+ generation.active = false
292
+ if (state.current === generation) state.current = undefined
293
+ trace('generation-dispose', { generation: generation.id })
294
+ },
295
+ 'vision-router: DSH 0.1.7 settings generation',
296
+ )
297
+ }
298
+ })
299
+ }
@@ -1,8 +1,208 @@
1
+ import { htmlHasScriptMarker } from '../html-script-marker.js'
1
2
  import { installVisionRouterRemoteSettingsBridge } from '../remote-settings-bridge.js'
2
3
  import { installSettingsRc8ClientLifecycle } from '../settings-client-rc8-lifecycle.js'
3
4
 
5
+ const SETTINGS_CONFIG_FORMS_MARK = 'data-vision-router-settings-configforms-compat'
6
+
7
+ /**
8
+ * DSH 0.1.7 replaces the browser `settingsScope` service with `configForms`.
9
+ * Vision Router still supports older Hosts, so the browser bundle cannot hard
10
+ * depend on either service name. This prelude removes the legacy hard service
11
+ * edge at module-factory time and presents the old `settingsScope.bind()` face
12
+ * over whichever official settings service the active Host provides.
13
+ */
14
+ export const SETTINGS_CONFIG_FORMS_CLIENT_PRELUDE = String.raw`(function(){
15
+ 'use strict';
16
+ var TARGET = 'dsh-vision-router';
17
+ var FLAG = '__visionRouterSettingsConfigFormsCompat';
18
+ var contextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
19
+ var binderCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
20
+ var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
21
+
22
+ function safeGet(ctx, name) {
23
+ if (!ctx) return undefined;
24
+ try {
25
+ if (typeof ctx.get === 'function') {
26
+ var value = ctx.get(name);
27
+ if (value !== undefined && value !== null) return value;
28
+ }
29
+ } catch (_) {}
30
+ try { return ctx[name]; } catch (_) { return undefined; }
31
+ }
32
+
33
+ function isLoopbackLocation(locationLike) {
34
+ var hostname = locationLike && typeof locationLike.hostname === 'string'
35
+ ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
36
+ : '';
37
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true;
38
+ return /^127(?:\.\d{1,3}){3}$/.test(hostname);
39
+ }
40
+
41
+ function normalizeConnection(connection) {
42
+ if (!connection || typeof connection !== 'object') return connection;
43
+ var locationLike;
44
+ try { locationLike = window && window.location; } catch (_) { locationLike = undefined; }
45
+ if (!isLoopbackLocation(locationLike) || connection.isLoopback !== false) return connection;
46
+ if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
47
+ var wrapped = new Proxy(connection, {
48
+ get: function(target, property) {
49
+ if (property === 'isLoopback') return true;
50
+ var value = Reflect.get(target, property, target);
51
+ return typeof value === 'function' ? value.bind(target) : value;
52
+ }
53
+ });
54
+ if (connectionCache) connectionCache.set(connection, wrapped);
55
+ return wrapped;
56
+ }
57
+
58
+ function legacyBinder(ctx) {
59
+ var binder = safeGet(ctx, 'settingsScope');
60
+ return binder && typeof binder.bind === 'function' ? binder : undefined;
61
+ }
62
+
63
+ function configFormsBinder(ctx) {
64
+ var forms = safeGet(ctx, 'configForms');
65
+ if (!forms || typeof forms.get !== 'function') return undefined;
66
+ if (binderCache && binderCache.has(forms)) return binderCache.get(forms);
67
+ var binder = {
68
+ bind: function(spec) {
69
+ var namespace = spec && spec.namespace;
70
+ if (typeof namespace !== 'string' || namespace.length === 0) {
71
+ throw new TypeError('settings namespace must be a non-empty string');
72
+ }
73
+ return forms.get(namespace);
74
+ }
75
+ };
76
+ if (binderCache) binderCache.set(forms, binder);
77
+ return binder;
78
+ }
79
+
80
+ function resolveBinder(ctx) {
81
+ return legacyBinder(ctx) || configFormsBinder(ctx);
82
+ }
83
+
84
+ function wrapContext(ctx) {
85
+ if (!ctx || typeof ctx !== 'object') return ctx;
86
+ if (contextCache && contextCache.has(ctx)) return contextCache.get(ctx);
87
+ var wrapped = new Proxy(ctx, {
88
+ get: function(target, property) {
89
+ if (property === 'settingsScope') {
90
+ var binder = resolveBinder(target);
91
+ if (!binder) {
92
+ throw new Error('Vision Router requires DSH settingsScope or configForms');
93
+ }
94
+ return binder;
95
+ }
96
+ if (property === 'get') {
97
+ var getter = Reflect.get(target, property, target);
98
+ if (typeof getter !== 'function') return getter;
99
+ return function(name) {
100
+ var value = getter.call(target, name);
101
+ return name === 'connection' ? normalizeConnection(value) : value;
102
+ };
103
+ }
104
+ var value = Reflect.get(target, property, target);
105
+ return typeof value === 'function' ? value.bind(target) : value;
106
+ }
107
+ });
108
+ if (contextCache) contextCache.set(ctx, wrapped);
109
+ return wrapped;
110
+ }
111
+
112
+ function rewriteInject(exports) {
113
+ if (!exports || !Array.isArray(exports.inject) || exports.inject.indexOf('settingsScope') === -1) return;
114
+ exports.inject = exports.inject.filter(function(name){ return name !== 'settingsScope'; });
115
+ }
116
+
117
+ function patchLiveLoader(loader) {
118
+ if (!loader || typeof loader.load !== 'function' || loader.load[FLAG]) return;
119
+ var original = loader.load;
120
+ function load(spec) {
121
+ if (spec && spec.id === TARGET && typeof spec.factory === 'function') {
122
+ var factory = spec.factory;
123
+ spec = Object.assign({}, spec, {
124
+ factory: function(require) {
125
+ var exports = factory(require);
126
+ rewriteInject(exports);
127
+ if (exports && typeof exports.apply === 'function' && !exports.apply[FLAG]) {
128
+ var apply = exports.apply;
129
+ var wrappedApply = function(ctx) {
130
+ var rest = Array.prototype.slice.call(arguments, 1);
131
+ return apply.apply(exports, [wrapContext(ctx)].concat(rest));
132
+ };
133
+ Object.defineProperty(wrappedApply, FLAG, { value: true });
134
+ exports.apply = wrappedApply;
135
+ }
136
+ return exports;
137
+ }
138
+ });
139
+ }
140
+ return original.call(this, spec);
141
+ }
142
+ Object.defineProperty(load, FLAG, { value: true });
143
+ loader.load = load;
144
+ }
145
+
146
+ function patchCreate(loader) {
147
+ if (!loader) return;
148
+ patchLiveLoader(loader);
149
+ if (typeof loader.create !== 'function' || loader.create[FLAG]) return;
150
+ var originalCreate = loader.create;
151
+ function create() {
152
+ var result = originalCreate.apply(this, arguments);
153
+ patchLiveLoader(loader);
154
+ return result;
155
+ }
156
+ Object.defineProperty(create, FLAG, { value: true });
157
+ loader.create = create;
158
+ }
159
+
160
+ function install() {
161
+ if (window.__ModuleLoader__) {
162
+ patchCreate(window.__ModuleLoader__);
163
+ return;
164
+ }
165
+ var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
166
+ if (descriptor && descriptor.configurable === false) return;
167
+ var previousGet = descriptor && descriptor.get;
168
+ var previousSet = descriptor && descriptor.set;
169
+ var stored = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor.value : undefined;
170
+ Object.defineProperty(window, '__ModuleLoader__', {
171
+ configurable: true,
172
+ enumerable: !descriptor || descriptor.enumerable !== false,
173
+ get: function(){ return previousGet ? previousGet.call(window) : stored; },
174
+ set: function(value) {
175
+ if (previousSet) previousSet.call(window, value); else stored = value;
176
+ try { patchCreate(previousGet ? previousGet.call(window) : value); } catch (_) {}
177
+ }
178
+ });
179
+ if (stored) patchCreate(stored);
180
+ }
181
+
182
+ try { install(); } catch (_) {}
183
+ })();`
184
+
185
+ export function injectSettingsConfigFormsCompatPrelude(html) {
186
+ if (typeof html !== 'string' || htmlHasScriptMarker(html, SETTINGS_CONFIG_FORMS_MARK)) return html
187
+ const safe = SETTINGS_CONFIG_FORMS_CLIENT_PRELUDE.replace(/<\/script/gi, '<\\/script')
188
+ const script = `<script ${SETTINGS_CONFIG_FORMS_MARK}>${safe}</script>`
189
+ const closeHead = html.indexOf('</head>')
190
+ return closeHead === -1 ? `${html}${script}` : `${html.slice(0, closeHead)}${script}${html.slice(closeHead)}`
191
+ }
192
+
193
+ export function installSettingsConfigFormsCompat(ctx) {
194
+ if (!ctx || typeof ctx.inject !== 'function') return
195
+ ctx.inject(['webServer'], (webCtx) => {
196
+ webCtx.effect(
197
+ () => webCtx.webServer.tapIndex(injectSettingsConfigFormsCompatPrelude),
198
+ 'vision-router: settings configForms compatibility',
199
+ )
200
+ })
201
+ }
202
+
4
203
  export function installVisionRemoteSettingsClient(ctx, logger) {
5
204
  installVisionRouterRemoteSettingsBridge(ctx, logger)
205
+ installSettingsConfigFormsCompat(ctx)
6
206
  installSettingsRc8ClientLifecycle(ctx)
7
207
  return ctx
8
208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,12 +70,12 @@
70
70
  "sharp": "^0.35.4"
71
71
  },
72
72
  "scripts": {
73
- "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/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/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.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/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/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.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/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-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/pi-ai-bridge-wire-compat.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/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/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 && node --test tests/vision-backend-runtime-policy.test.js",
73
+ "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/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/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.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/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/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.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/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-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/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.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/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/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 && node --test tests/vision-backend-runtime-policy.test.js",
74
74
  "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",
75
75
  "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",
76
76
  "test:session": "node --test tests/session-vision-state.test.js tests/session-vision-state-integration.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",
77
77
  "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",
78
- "test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/rc6-rc7-compat.test.js tests/rc6-real-settings-persistence.test.js tests/android-attachment-compat.test.js tests/http-compat.test.js tests/pi-ai-bridge-wire-compat.test.js tests/native-image-coexistence.test.js tests/tesseract-node24-boot.test.js tests/dsh-host-capabilities.test.js",
78
+ "test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/rc6-rc7-compat.test.js tests/rc6-real-settings-persistence.test.js tests/android-attachment-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",
79
79
  "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",
80
80
  "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/rc6-rc7-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",
81
81
  "test:grounding": "node --test tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/vision-capability-grounding-proof-contract.test.js",