dsh-vision-router 2.2.8 → 2.3.0

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.
Files changed (49) hide show
  1. package/README.md +7 -7
  2. package/README.zh.md +7 -7
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/compat-inventory.md +26 -34
  8. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  9. package/docs/architecture/dsh-support-window.md +27 -21
  10. package/docs/architecture/final-closure-audit.md +2 -0
  11. package/docs/releases/v2.3.0.md +53 -0
  12. package/entry.js +8 -56
  13. package/index.js +161 -691
  14. package/lib/catalog-corrections.js +1 -2
  15. package/lib/client-maintenance-actions.js +145 -0
  16. package/lib/client-presentation-boundary-main.js +75 -17
  17. package/lib/client-presentation-boundary.js +53 -28
  18. package/lib/client.js +246 -144
  19. package/lib/core-apply-lifecycle.js +47 -0
  20. package/lib/core-primitives.js +88 -135
  21. package/lib/desktop-screenshot-tool.js +262 -0
  22. package/lib/dsh-contract-compat.js +11 -26
  23. package/lib/dsh-support-window.js +3 -3
  24. package/lib/http-compat.js +1 -2
  25. package/lib/local-remote-settings-permission.js +33 -7
  26. package/lib/maintenance-routes.js +136 -0
  27. package/lib/public-config.js +57 -0
  28. package/lib/public-entry.js +2 -7
  29. package/lib/remote-settings-bridge.js +11 -0
  30. package/lib/remote-settings-risk-client-boundary.js +155 -0
  31. package/lib/remote-settings-risk-confirmation.js +17 -105
  32. package/lib/runtime-composition.js +105 -30
  33. package/lib/runtime-i18n-boundary.js +10 -11
  34. package/lib/session-message-source-compat.js +7 -3
  35. package/lib/session-vision-index.js +154 -9
  36. package/lib/settings-client-loader-lifecycle.js +129 -0
  37. package/lib/sharp-runtime.js +0 -7
  38. package/lib/structured-flow-hardening.js +3 -2
  39. package/lib/vision-backend-runtime-policy.js +3 -2
  40. package/lib/vision-capability-benchmark-service.js +1 -0
  41. package/lib/vision-diagnostics-routes.js +216 -0
  42. package/lib/vision-evidence-guidance.js +3 -1
  43. package/lib/vision-provider-transport.js +0 -29
  44. package/lib/vision-toggle-root-hardening.js +22 -5
  45. package/lib/vision-tool-runtime-boundary.js +5 -1
  46. package/lib/web/remote-settings-client.js +2 -2
  47. package/package.json +23 -13
  48. package/lib/android-attachment-compat.js +0 -244
  49. package/lib/settings-client-rc8-lifecycle.js +0 -397
@@ -2,12 +2,17 @@ import { inspectDshHostCapabilities } from './dsh-host-capabilities.js'
2
2
 
3
3
  /**
4
4
  * Compatibility inventory
5
- * Reason: preserve Vision Router's released behavior across the supported DSH Host window.
6
- * Host gap: attachment ownership, settings-section and adapter-ownership seams differ across supported Hosts.
7
- * First needed for: rc.6 minimum support and the later batch-attachment/settings migration.
8
- * Feature detection: concrete attachment/settings/LLM methods only; never a DSH version string.
9
- * Removal condition: the minimum supported Host natively provides every seam used below and the legacy profile overlay is outside the support window.
10
- * Tests: rc6-rc7-compat, rc6-real-settings-persistence, attachment-admission-policy, DSH contract CI.
5
+ * Reason: preserve the remaining Settings/session/provider ownership and durable
6
+ * profile migrations across DVR 2.3's supported DSH 0.1.5+ Host window.
7
+ * Host gap: current supported Hosts still span multiple Settings/session
8
+ * lifecycle shapes, while historical persisted attachment policy overlays may
9
+ * survive an in-place Host upgrade.
10
+ * Feature detection: concrete attachment/settings/session/LLM methods only;
11
+ * never a DSH version string.
12
+ * Removal condition: each retained capability bridge has a current-Host or
13
+ * durable-data consumer; pre-floor Host-only branches are removed in R2.3.
14
+ * Tests: compatibility contracts, minimum Host Settings persistence,
15
+ * attachment-admission policy, and DSH contract CI.
11
16
  */
12
17
 
13
18
  const HOST_OWNED_ROUTES = new Set(['deepseek-official', 'deepseek-official-native'])
@@ -654,23 +659,3 @@ export function installHostSettingsCompatibility(ctx, entryConfig, options = {})
654
659
  },
655
660
  })
656
661
  }
657
-
658
- /**
659
- * The minimum-supported host keeps the narrow Termux fallback. A
660
- * batch-capable attachment service keeps AttachmentId store-owned, so that
661
- * host context passes through untouched.
662
- */
663
- export function attachmentContextForContract(ctx, logger, options = {}) {
664
- installDshHostCapabilityDiagnostics(ctx)
665
- if (hasBatchAttachmentContract(ctx)) return ctx
666
- const install = options.installAndroidAttachmentCompat
667
- if (typeof install !== 'function') return ctx
668
- return install(ctx, logger, options.android)
669
- }
670
-
671
- // Transitional aliases for external/tests written against the rc.7-era names.
672
- // New code must consume the capability-named exports above so future DSH
673
- // releases do not accrete `if (rc8) / if (rc9)` branches.
674
- export const isRc7ContractRuntime = hasBatchAttachmentContract
675
- export const protectRc7ProviderOwnership = protectHostProviderOwnership
676
- export const installRc7SettingsCompatibility = installHostSettingsCompatibility
@@ -1,6 +1,6 @@
1
1
  export const DSH_SUPPORT_WINDOW = Object.freeze({
2
- dvrTrain: '2.2.x',
3
- minimum: '0.1.0-rc.8',
2
+ dvrTrain: '2.3.x',
3
+ minimum: '0.1.5',
4
4
  currentStable: '0.1.5-rc.3',
5
5
  })
6
6
 
@@ -49,7 +49,7 @@ export function formatDshSupportWindowLines(capabilities = {}) {
49
49
  ` support floor: DVR ${DSH_SUPPORT_WINDOW.dvrTrain} -> DSH ${DSH_SUPPORT_WINDOW.minimum}`,
50
50
  'DSH compatibility verification evidence:',
51
51
  ` exact stable: ${DSH_VERIFICATION_EVIDENCE.exactStable}`,
52
- ` exact next/rc (not a support claim): ${DSH_VERIFICATION_EVIDENCE.exactPreview}`,
52
+ ` exact supported 0.2.x boundary: ${DSH_VERIFICATION_EVIDENCE.exactPreview}`,
53
53
  ` scheduled stable canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.stableCanaryDistTag}`,
54
54
  ` scheduled next/rc canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.nextCanaryDistTag}`,
55
55
  ` scheduled alpha/pre-release canary: npm dist-tag ${DSH_VERIFICATION_EVIDENCE.previewCanaryDistTag}`,
@@ -1,4 +1,3 @@
1
- import { currentVisionProviderTransport } from './vision-provider-transport.js'
2
1
 
3
2
  const GLM_4V_FLASH = /^glm-4v-flash(?:$|[-_.])/i
4
3
  export const DEFAULT_HTTP_ERROR_DETAIL_MAX_BYTES = 64 * 1024
@@ -278,7 +277,7 @@ function nextCompatibilityBody(body, detail) {
278
277
  */
279
278
  export async function fetchWithOpenAICompatibility(fetchImpl, input, init, context = {}) {
280
279
  if (typeof fetchImpl !== 'function') throw new TypeError('fetch implementation is required')
281
- const transport = context.active === true ? currentVisionProviderTransport() : undefined
280
+ const transport = context.active === true ? context.providerTransport : undefined
282
281
  const requestFetch = transport
283
282
  ? (nextInput, nextInit) => transport.fetch(nextInput, nextInit, {
284
283
  providerName: context.providerName,
@@ -173,10 +173,16 @@ export function createVisionRouterLocalPermissionHttpHandler(settings, logger) {
173
173
  }
174
174
  }
175
175
 
176
- export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
177
- 'use strict';
178
- var ENDPOINT = '/_dsh/vision-router/remote-settings-permission';
179
- var FIELD = 'allowRemoteSettings';
176
+ /**
177
+ * Current-product browser boundary for the local allowRemoteSettings control.
178
+ * Host loader lifecycle compatibility stays outside this owner.
179
+ */
180
+ export function createLocalRemoteSettingsPermissionClientBoundary(options = {}) {
181
+ var fetchImpl = options.fetchImpl
182
+ var ENDPOINT = options.endpoint
183
+ var FIELD = options.field
184
+ if (typeof ENDPOINT !== 'string' || ENDPOINT === '') throw new TypeError('local permission boundary requires endpoint')
185
+ if (typeof FIELD !== 'string' || FIELD === '') throw new TypeError('local permission boundary requires field')
180
186
  var scopeCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
181
187
  var binderCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
182
188
  var contextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
@@ -189,11 +195,11 @@ export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
189
195
  }
190
196
 
191
197
  async function writePermission(operation, value, revision) {
192
- if (typeof fetch !== 'function') throw new Error('Vision Router local settings transport is unavailable');
198
+ if (typeof fetchImpl !== 'function') throw new Error('Vision Router local settings transport is unavailable');
193
199
  var payload = { operation: operation };
194
200
  if (operation === 'set') payload.value = value;
195
201
  if (Number.isInteger(revision) && revision >= 0) payload.expectedRevision = revision;
196
- var response = await fetch(ENDPOINT, {
202
+ var response = await fetchImpl(ENDPOINT, {
197
203
  method: 'POST',
198
204
  headers: { 'content-type': 'application/json', accept: 'application/json' },
199
205
  body: JSON.stringify(payload)
@@ -366,6 +372,26 @@ export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
366
372
  return wrapped;
367
373
  }
368
374
 
375
+
376
+ return { wrapContext: wrapContext }
377
+ }
378
+
379
+ export const LOCAL_PERMISSION_CLIENT_BOUNDARY_SOURCE =
380
+ 'var createLocalRemoteSettingsPermissionClientBoundary = (' +
381
+ createLocalRemoteSettingsPermissionClientBoundary.toString() +
382
+ ');'
383
+
384
+ export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
385
+ 'use strict';
386
+ ${LOCAL_PERMISSION_CLIENT_BOUNDARY_SOURCE}
387
+ var localPermissionBoundary = createLocalRemoteSettingsPermissionClientBoundary({
388
+ endpoint: '${LOCAL_REMOTE_SETTINGS_PERMISSION_PATH}',
389
+ field: '${LOCAL_REMOTE_SETTINGS_PERMISSION_FIELD}',
390
+ fetchImpl: typeof fetch === 'function'
391
+ ? function(url, options) { return fetch(url, options); }
392
+ : undefined
393
+ });
394
+
369
395
  function patchLoader(loader) {
370
396
  if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterLocalPermission) return;
371
397
  var original = loader.load;
@@ -379,7 +405,7 @@ export const LOCAL_PERMISSION_CLIENT_PRELUDE = String.raw`(function(){
379
405
  var apply = exports.apply;
380
406
  var wrappedApply = function(ctx) {
381
407
  var rest = Array.prototype.slice.call(arguments, 1);
382
- return apply.apply(exports, [wrapContext(ctx)].concat(rest));
408
+ return apply.apply(exports, [localPermissionBoundary.wrapContext(ctx)].concat(rest));
383
409
  };
384
410
  Object.defineProperty(wrappedApply, '__visionRouterLocalPermission', { value: true });
385
411
  exports.apply = wrappedApply;
@@ -0,0 +1,136 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { createCachedUpdateChecker } from './update-check.js'
3
+ import { detectDshSelfUpdatePlan, runDshPluginUpdate } from './self-update.js'
4
+
5
+ function json(res, status, body, headers = {}) {
6
+ res.writeHead(status, {
7
+ 'content-type': 'application/json',
8
+ ...headers,
9
+ })
10
+ res.end(JSON.stringify(body))
11
+ }
12
+
13
+ /**
14
+ * Product-maintenance Web routes kept outside the visual Core.
15
+ *
16
+ * The supplied context must already carry DVR's Web capability/auth boundary;
17
+ * this installer deliberately owns only update-product state and the narrow
18
+ * per-route CSRF/replay checks that existed in Core before extraction.
19
+ */
20
+ export function installVisionMaintenanceRoutes(ctx, options = {}) {
21
+ if (!ctx || typeof ctx.inject !== 'function') return
22
+ const logger = options.logger ?? ctx.logger
23
+ const updateChecker = options.updateChecker ?? createCachedUpdateChecker({
24
+ fetchImpl: (...args) => globalThis.fetch(...args),
25
+ })
26
+ const selfUpdatePlan = options.selfUpdatePlan ?? detectDshSelfUpdatePlan()
27
+ const updateRunner = options.runUpdate ?? runDshPluginUpdate
28
+ const tokenFactory = options.tokenFactory ?? (() => randomBytes(24).toString('base64url'))
29
+ let selfUpdateToken = tokenFactory()
30
+ let selfUpdateInFlight
31
+
32
+ const updateResultForClient = (result) => ({
33
+ ...result,
34
+ autoUpdate: {
35
+ supported: selfUpdatePlan.available === true,
36
+ method: selfUpdatePlan.available === true ? selfUpdatePlan.method : undefined,
37
+ profile: selfUpdatePlan.profile,
38
+ reason: selfUpdatePlan.available === true ? undefined : selfUpdatePlan.reason,
39
+ token:
40
+ selfUpdatePlan.available === true &&
41
+ result &&
42
+ result.ok === true &&
43
+ result.updateAvailable === true
44
+ ? selfUpdateToken
45
+ : undefined,
46
+ },
47
+ })
48
+
49
+ void updateChecker.check(false).then((result) => {
50
+ if (result && result.ok === true && result.updateAvailable === true) {
51
+ logger?.info?.(
52
+ 'vision-router: update available %s -> %s',
53
+ result.currentVersion,
54
+ result.latestVersion,
55
+ )
56
+ }
57
+ })
58
+
59
+ ctx.inject(['webServer'], (webCtx) => {
60
+ webCtx.effect(
61
+ () => webCtx.webServer.register({
62
+ kind: 'exact',
63
+ path: '/_dsh/vision-router/update-check',
64
+ handler: async (req, res) => {
65
+ if (req.method !== 'GET') {
66
+ res.setHeader('Allow', 'GET')
67
+ res.writeHead(405)
68
+ res.end()
69
+ return
70
+ }
71
+ const force = /(?:[?&])force=1(?:&|$)/.test(String(req.url ?? ''))
72
+ const result = await updateChecker.check(force)
73
+ json(res, 200, updateResultForClient(result), { 'cache-control': 'no-store' })
74
+ },
75
+ }),
76
+ 'vision-router: update-check route',
77
+ )
78
+
79
+ webCtx.effect(
80
+ () => webCtx.webServer.register({
81
+ kind: 'exact',
82
+ path: '/_dsh/vision-router/self-update',
83
+ handler: async (req, res) => {
84
+ if (req.method !== 'POST') {
85
+ res.setHeader('Allow', 'POST')
86
+ res.writeHead(405)
87
+ res.end()
88
+ return
89
+ }
90
+ const fetchSite = String(req.headers?.['sec-fetch-site'] ?? '')
91
+ if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') {
92
+ json(res, 403, { ok: false, error: 'cross-origin update request rejected' })
93
+ return
94
+ }
95
+ const token = String(req.headers?.['x-dsh-vision-router-update-token'] ?? '')
96
+ if (!token || token !== selfUpdateToken) {
97
+ json(res, 403, { ok: false, error: 'invalid update token' })
98
+ return
99
+ }
100
+ if (selfUpdatePlan.available !== true) {
101
+ json(res, 409, { ok: false, error: 'automatic update is not safe for this DSH launch' })
102
+ return
103
+ }
104
+ try {
105
+ const fresh = await updateChecker.check(true)
106
+ if (!fresh || fresh.ok !== true) {
107
+ json(res, 502, { ok: false, error: fresh?.error || 'could not refresh update metadata' })
108
+ return
109
+ }
110
+ if (fresh.updateAvailable !== true) {
111
+ json(res, 409, { ok: false, error: 'no newer version is currently available' })
112
+ return
113
+ }
114
+ if (!selfUpdateInFlight) {
115
+ const pending = updateRunner(selfUpdatePlan, { targetVersion: fresh.latestVersion })
116
+ selfUpdateInFlight = pending
117
+ void pending.then(
118
+ () => { if (selfUpdateInFlight === pending) selfUpdateInFlight = undefined },
119
+ () => { if (selfUpdateInFlight === pending) selfUpdateInFlight = undefined },
120
+ )
121
+ }
122
+ const result = await selfUpdateInFlight
123
+ selfUpdateToken = tokenFactory()
124
+ json(res, 200, result, { 'cache-control': 'no-store' })
125
+ } catch (error) {
126
+ json(res, 500, {
127
+ ok: false,
128
+ error: error && error.message ? error.message : String(error),
129
+ })
130
+ }
131
+ },
132
+ }),
133
+ 'vision-router: self-update route',
134
+ )
135
+ })
136
+ }
@@ -0,0 +1,57 @@
1
+ import z from '@deepseek-ai/schemastery'
2
+
3
+ // Increment whenever the browser-visible settings contract gains a field whose
4
+ // absence changes write semantics. The resolved schema also publishes this as
5
+ // a read-only-by-convention handshake for browser/Host compatibility checks.
6
+ export const SETTINGS_CONTRACT_REVISION = 7
7
+
8
+ /**
9
+ * Compose DVR's public product settings onto the mature Core schema.
10
+ *
11
+ * Schemastery object schemas expose set() as the supported replacement seam.
12
+ * Mutating the supplied object is deliberate: Core later registers this exact
13
+ * Config identity with Settings, so a cloned/public-only schema would let the
14
+ * exported contract and runtime validation drift apart.
15
+ */
16
+ export function composePublicVisionConfig(coreConfig) {
17
+ if (!coreConfig || typeof coreConfig.set !== 'function') {
18
+ throw new TypeError('Vision Router public config requires a Schemastery object schema')
19
+ }
20
+
21
+ coreConfig.set('progressiveTools', z.boolean().default(false))
22
+
23
+ // Keep provider/task/turn timeout layers coherent. The whole-turn budget is
24
+ // an optional safety cap: 0 remains unlimited for long-running agent turns.
25
+ coreConfig.set(
26
+ 'visionTaskTimeoutMs',
27
+ z.number().step(1000).min(1000).max(180000).default(120000),
28
+ )
29
+ coreConfig.set('visionTurnBudgetMs', z.number().step(1000).min(0).max(600000).default(0))
30
+
31
+ // Both visible settings entry points edit the same Host-owned namespace.
32
+ coreConfig.set('visionDepth', z.union(['fast', 'standard', 'deep', 'custom']).default('standard'))
33
+ coreConfig.set('visionDepthMaxCalls', z.number().step(1).min(0).max(100).default(0))
34
+
35
+ // Product semantics: ordered remains the safe default. Auto execution and
36
+ // background measurement each require explicit live user authority.
37
+ coreConfig.set('routingMode', z.union(['ordered', 'auto']).default('ordered'))
38
+ coreConfig.set(
39
+ 'routingPreference',
40
+ z.union(['balanced', 'quality', 'speed', 'local']).default('balanced'),
41
+ )
42
+ coreConfig.set(
43
+ 'backgroundBenchmarking',
44
+ z.union(['local-free', 'all', 'off']).default('off'),
45
+ )
46
+
47
+ // Remote Settings remains an explicit opt-in product permission. The remote
48
+ // risk dialog is a reminder/acknowledgement, not a server-side auth proof.
49
+ coreConfig.set('allowRemoteSettings', z.boolean().default(false))
50
+
51
+ coreConfig.set(
52
+ 'settingsContractRevision',
53
+ z.number().step(1).min(1).max(SETTINGS_CONTRACT_REVISION).default(SETTINGS_CONTRACT_REVISION),
54
+ )
55
+
56
+ return coreConfig
57
+ }
@@ -2,10 +2,7 @@ import './abort-signal-compat.js'
2
2
  import * as base from '../entry.js'
3
3
  import { createVisionToggleRootHardening } from './vision-toggle-root-hardening.js'
4
4
  import { contextWithVisionRoutingTopologyRefresh } from './vision-routing-topology-refresh.js'
5
- import {
6
- createVisionProviderTransport,
7
- installVisionProviderTransport,
8
- } from './vision-provider-transport.js'
5
+ import { createVisionProviderTransport } from './vision-provider-transport.js'
9
6
  import { installLegacyGlobalProxyBoundary } from './legacy-global-proxy-boundary.js'
10
7
  import { installDsh017SettingsCompatibility } from './dsh-settings-017-compat.js'
11
8
  import { installDsh017RootLocalSettingsTransport } from './web/dsh-settings-017-root-transport.js'
@@ -59,12 +56,10 @@ export function apply(ctx, config = {}) {
59
56
  ctx: hardening.ctx,
60
57
  config: () => liveVisionConfig(hardening.ctx, config),
61
58
  })
62
- const releaseTransportRegistry = installVisionProviderTransport(transport)
63
59
  let transportReleased = false
64
60
  const releaseTransport = () => {
65
61
  if (transportReleased) return
66
62
  transportReleased = true
67
- releaseTransportRegistry()
68
63
  void transport.dispose()
69
64
  }
70
65
  try {
@@ -72,7 +67,7 @@ export function apply(ctx, config = {}) {
72
67
  () => releaseTransport,
73
68
  'vision-router: provider transport',
74
69
  )
75
- const result = base.apply(runtimeCtx, hardening.config)
70
+ const result = base.apply(runtimeCtx, hardening.config, { providerTransport: transport })
76
71
  installLegacyGlobalProxyBoundary(runtimeCtx, hardening.config)
77
72
  hardening.installClientBoundary()
78
73
  return result
@@ -138,6 +138,17 @@ function conflictResult(error) {
138
138
  }
139
139
  }
140
140
 
141
+ /**
142
+ * PRODUCT CONTRACT (2.3+): remote-settings enablement is reminder-only.
143
+ *
144
+ * `acceptedRisk === true` is an acknowledgement bit for the UX flow, not an
145
+ * authentication/authorization credential, proof of local presence, challenge
146
+ * response, one-time secret, or administrator approval. Do not strengthen this
147
+ * endpoint into a second security gate unless the product policy is explicitly
148
+ * changed. The real capability boundary is the narrow remote mutable-field
149
+ * allow-list plus the fields that remain local-only. The historical RPC/function
150
+ * name uses "authorize"; do not infer security-auth semantics from that name.
151
+ */
141
152
  export async function authorizeRemoteSettingsAfterRiskConfirmation(settings, payload, logger) {
142
153
  if (!payload || typeof payload !== 'object' || Array.isArray(payload) || payload.acceptedRisk !== true) {
143
154
  return badRequest('acceptedRisk must be true')
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Current-product remote Settings risk behavior shared by every browser
3
+ * attachment lifecycle. Host loader/version compatibility stays outside this owner.
4
+ *
5
+ * PRODUCT CONTRACT (2.3+): acceptedRisk is reminder/friction only. It is not
6
+ * authentication, authorization, local-presence proof, or a credential.
7
+ */
8
+ export function createRemoteSettingsRiskClientBoundary(options = {}) {
9
+ var confirmImpl = options.confirmImpl
10
+ var alertImpl = options.alertImpl
11
+ var locale = options.locale
12
+ var location = options.location
13
+ var normalizeLoopback = options.normalizeLoopback === true
14
+ var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined
15
+ var rpcCache = typeof WeakMap === 'function' ? new WeakMap() : undefined
16
+ var contextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined
17
+ var authorizationByRpc = typeof WeakMap === 'function' ? new WeakMap() : undefined
18
+ var CHANNEL = '/vision-router-settings'
19
+ var AUTHORIZE_ENDPOINT = 'authorize'
20
+
21
+ function localeValue() {
22
+ try {
23
+ var value = typeof locale === 'function' ? locale() : locale
24
+ return String(value || '').toLowerCase()
25
+ } catch (_) {
26
+ return ''
27
+ }
28
+ }
29
+
30
+ function chineseLocale() {
31
+ var lang = localeValue()
32
+ return lang === '' || lang.indexOf('zh') === 0
33
+ }
34
+
35
+ function riskMessage() {
36
+ return chineseLocale()
37
+ ? '启用远程设置?\n\n当前页面通过远程地址访问 DSH。启用后,能够访问此 trusted host 的客户端可以修改 Vision Router 已开放的设置。\n\nDSH trustedHosts 不是身份认证,请仅在你信任当前网络和访问者时继续。API Key、HTTP Provider 凭据、本地 Ollama / LM Studio、产物路径等未列入远程白名单的敏感配置仍不会开放。\n\n确定要允许远程修改设置吗?'
38
+ : 'Enable remote settings?\n\nThis page is accessing DSH through a remote address. After enabling this, clients that can reach this trusted host can change the Vision Router settings exposed by the remote allow-list.\n\nDSH trustedHosts is not authentication. Continue only if you trust the current network and its users. Sensitive settings outside the remote allow-list, including API keys, HTTP-provider credentials, local Ollama / LM Studio, and artifact paths, remain unavailable remotely.\n\nAllow remote settings?'
39
+ }
40
+
41
+ function isLoopbackLocation(locationLike) {
42
+ var hostname = locationLike && typeof locationLike.hostname === 'string'
43
+ ? locationLike.hostname.toLowerCase().replace(/^\[|\]$/g, '')
44
+ : ''
45
+ var protocol = locationLike && typeof locationLike.protocol === 'string'
46
+ ? locationLike.protocol.toLowerCase()
47
+ : ''
48
+ if (protocol === 'dsh-app:' && hostname === 'app') return true
49
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return true
50
+ return /^127(?:\.\d{1,3}){3}$/.test(hostname)
51
+ }
52
+
53
+ function currentLocation() {
54
+ try { return typeof location === 'function' ? location() : location }
55
+ catch (_) { return undefined }
56
+ }
57
+
58
+ function isPermissionDisabledDescribe(channel, endpoint, result) {
59
+ return channel === CHANNEL
60
+ && endpoint === 'describe'
61
+ && result && result.ok === true
62
+ && result.value && result.value.enabled !== true
63
+ && result.value.reason === 'permission-disabled'
64
+ }
65
+
66
+ async function authorizeAndRefresh(target, call) {
67
+ var existing = authorizationByRpc && authorizationByRpc.get(target)
68
+ if (existing) return existing
69
+ var pending = (async function() {
70
+ if (typeof confirmImpl !== 'function' || confirmImpl(riskMessage()) !== true) return undefined
71
+ var authorized = await call.call(target, CHANNEL, AUTHORIZE_ENDPOINT, { acceptedRisk: true })
72
+ if (!authorized || authorized.ok !== true) {
73
+ var message = authorized && authorized.error && authorized.error.message
74
+ ? authorized.error.message
75
+ : 'Vision Router could not enable remote settings.'
76
+ try { if (typeof alertImpl === 'function') alertImpl(message) } catch (_) {}
77
+ return undefined
78
+ }
79
+ return call.call(target, CHANNEL, 'describe', {})
80
+ })()
81
+ if (authorizationByRpc) authorizationByRpc.set(target, pending)
82
+ try { return await pending }
83
+ finally {
84
+ if (authorizationByRpc && authorizationByRpc.get(target) === pending) {
85
+ authorizationByRpc.delete(target)
86
+ }
87
+ }
88
+ }
89
+
90
+ function wrapRpc(rpc) {
91
+ if (!rpc || (typeof rpc !== 'object' && typeof rpc !== 'function')) return rpc
92
+ if (rpcCache && rpcCache.has(rpc)) return rpcCache.get(rpc)
93
+ var wrapped = new Proxy(rpc, {
94
+ get: function(target, property) {
95
+ if (property === 'call') {
96
+ var call = Reflect.get(target, property, target)
97
+ if (typeof call !== 'function') return call
98
+ return async function(channel, endpoint) {
99
+ var result = await call.apply(target, arguments)
100
+ if (!isPermissionDisabledDescribe(channel, endpoint, result)) return result
101
+ var refreshed = await authorizeAndRefresh(target, call)
102
+ return refreshed || result
103
+ }
104
+ }
105
+ var value = Reflect.get(target, property, target)
106
+ return typeof value === 'function' ? value.bind(target) : value
107
+ }
108
+ })
109
+ if (rpcCache) rpcCache.set(rpc, wrapped)
110
+ return wrapped
111
+ }
112
+
113
+ function wrapConnection(connection) {
114
+ if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection
115
+ if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection)
116
+ var wrapped = new Proxy(connection, {
117
+ get: function(target, property) {
118
+ if (property === 'isLoopback' && normalizeLoopback && isLoopbackLocation(currentLocation())) return true
119
+ if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target))
120
+ var value = Reflect.get(target, property, target)
121
+ return typeof value === 'function' ? value.bind(target) : value
122
+ }
123
+ })
124
+ if (connectionCache) connectionCache.set(connection, wrapped)
125
+ return wrapped
126
+ }
127
+
128
+ function wrapContext(ctx) {
129
+ if (!ctx || typeof ctx !== 'object') return ctx
130
+ if (contextCache && contextCache.has(ctx)) return contextCache.get(ctx)
131
+ var wrapped = new Proxy(ctx, {
132
+ get: function(target, property) {
133
+ if (property === 'get') {
134
+ var get = Reflect.get(target, property, target)
135
+ if (typeof get !== 'function') return get
136
+ return function(name) {
137
+ var value = get.apply(target, arguments)
138
+ return name === 'connection' ? wrapConnection(value) : value
139
+ }
140
+ }
141
+ var value = Reflect.get(target, property, target)
142
+ return typeof value === 'function' ? value.bind(target) : value
143
+ }
144
+ })
145
+ if (contextCache) contextCache.set(ctx, wrapped)
146
+ return wrapped
147
+ }
148
+
149
+ return { wrapContext: wrapContext }
150
+ }
151
+
152
+ export const REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE =
153
+ 'var createRemoteSettingsRiskClientBoundary = (' +
154
+ createRemoteSettingsRiskClientBoundary.toString() +
155
+ ');'