@240xu/dsh-websearch 2.8.3 → 2.8.5

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.
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { throwIfSearchAborted, searchAborted, isAbortError } from "../util/abort.js";
19
19
  import { WebError } from "@deepseek-ai/dsh-web";
20
+ import { egressFetch } from "../util/egress-fetch.js";
20
21
 
21
22
  /** Attribution header; bumped with the package version. */
22
23
  const UA = "dsh-websearch/2.3";
@@ -111,7 +112,7 @@ export function createAnthropicLikeBackend(spec) {
111
112
 
112
113
  let response;
113
114
  try {
114
- response = await fetch(endpoint, {
115
+ response = await egressFetch(endpoint, {
115
116
  method: "POST",
116
117
  redirect: "error",
117
118
  headers: {
@@ -8,6 +8,7 @@
8
8
  import { WebError } from "@deepseek-ai/dsh-web";
9
9
  import { throwIfSearchAborted, searchAborted, isAbortError } from "../util/abort.js";
10
10
  import { applyBraveFilters, normalizeFilters, safeSearchParams } from "../util/filters.js";
11
+ import { egressFetch } from "../util/egress-fetch.js";
11
12
 
12
13
  const BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
13
14
 
@@ -46,7 +47,7 @@ export const braveBackend = {
46
47
 
47
48
  let response;
48
49
  try {
49
- response = await fetch(url, {
50
+ response = await egressFetch(url, {
50
51
  method: "GET",
51
52
  headers: {
52
53
  "X-Subscription-Token": apiKey,
@@ -8,6 +8,7 @@
8
8
  import { searchAborted, isAbortError, throwIfSearchAborted } from "../util/abort.js";
9
9
  import { applyDdgFilters, normalizeFilters } from "../util/filters.js";
10
10
  import { WebError } from "@deepseek-ai/dsh-web";
11
+ import { egressFetch } from "../util/egress-fetch.js";
11
12
 
12
13
  export const DDG_ENDPOINT = "https://html.duckduckgo.com/html/";
13
14
 
@@ -29,20 +30,21 @@ export const ddgBackend = {
29
30
  throwIfSearchAborted(signal);
30
31
  const params = new URLSearchParams({ q: request.query, b: Math.random().toString().slice(2) });
31
32
  applyDdgFilters(params, normalizeFilters(request.filters));
32
- const body = params.toString();
33
+ const target = `${DDG_ENDPOINT}?${params.toString()}`;
33
34
  hooks.recordRequest?.({ endpoint: DDG_ENDPOINT, toolName: "html", body: { q: request.query }, headers: {} });
34
35
 
35
36
  let text;
36
37
  try {
37
- const res = await fetch(DDG_ENDPOINT, {
38
- method: "POST",
38
+ // GET form: DDG's html endpoint still serves the same results page, and
39
+ // unlike the POST form it is not rejected by the endpoint's bot wall
40
+ // for non-browser clients (POST is JA3 + method double-gated).
41
+ const res = await egressFetch(target, {
42
+ method: "GET",
39
43
  redirect: "error",
40
44
  headers: {
41
- "Content-Type": "application/x-www-form-urlencoded",
42
45
  "Accept": "text/html",
43
46
  "User-Agent": "dsh-websearch/2.3",
44
47
  },
45
- body,
46
48
  ...(signal !== undefined ? { signal } : {}),
47
49
  });
48
50
  if (!res.ok) {
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { WebError } from "@deepseek-ai/dsh-web";
9
9
  import { throwIfSearchAborted, searchAborted, isAbortError } from "../util/abort.js";
10
+ import { egressFetch } from "../util/egress-fetch.js";
10
11
 
11
12
  const MOJEEK_ENDPOINT = "https://api.mojeek.com/v1/search";
12
13
 
@@ -52,7 +53,7 @@ export const mojeekBackend = {
52
53
 
53
54
  let response;
54
55
  try {
55
- response = await fetch(url, {
56
+ response = await egressFetch(url, {
56
57
  method: "GET",
57
58
  headers,
58
59
  redirect: "error",
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { throwIfSearchAborted, searchAborted, isAbortError } from "../util/abort.js";
14
14
  import { WebError } from "@deepseek-ai/dsh-web";
15
+ import { egressFetch } from "../util/egress-fetch.js";
15
16
 
16
17
  const UA = "dsh-websearch/2.3";
17
18
 
@@ -67,7 +68,7 @@ export const openaiBackend = {
67
68
 
68
69
  let response;
69
70
  try {
70
- response = await fetch(endpoint, {
71
+ response = await egressFetch(endpoint, {
71
72
  method: "POST",
72
73
  redirect: "error",
73
74
  headers: {
@@ -9,6 +9,7 @@
9
9
  import { WebError } from "@deepseek-ai/dsh-web";
10
10
  import { throwIfSearchAborted, searchAborted, isAbortError } from "../util/abort.js";
11
11
  import { applySearxngFilters, normalizeFilters, safeSearchParams } from "../util/filters.js";
12
+ import { egressFetch } from "../util/egress-fetch.js";
12
13
 
13
14
  const DEFAULT_SEARXNG_URL = "https://searx.be";
14
15
 
@@ -61,7 +62,7 @@ export const searxngBackend = {
61
62
 
62
63
  let response;
63
64
  try {
64
- response = await fetch(url, {
65
+ response = await egressFetch(url, {
65
66
  method: "GET",
66
67
  headers,
67
68
  redirect: "error",
@@ -8,6 +8,7 @@
8
8
  import { WebError } from "@deepseek-ai/dsh-web";
9
9
  import { throwIfSearchAborted, searchAborted, isAbortError } from "../util/abort.js";
10
10
  import { applySerperFilters, normalizeFilters } from "../util/filters.js";
11
+ import { egressFetch } from "../util/egress-fetch.js";
11
12
 
12
13
  const SERPER_ENDPOINT = "https://google.serper.dev/search";
13
14
 
@@ -46,7 +47,7 @@ export const serperBackend = {
46
47
 
47
48
  let response;
48
49
  try {
49
- response = await fetch(baseURL, {
50
+ response = await egressFetch(baseURL, {
50
51
  method: "POST",
51
52
  headers: {
52
53
  "X-API-KEY": apiKey,
@@ -8,6 +8,7 @@
8
8
  import { WebError } from "@deepseek-ai/dsh-web";
9
9
  import { throwIfSearchAborted, searchAborted, isAbortError } from "../util/abort.js";
10
10
  import { applyTavilyFilters, normalizeFilters } from "../util/filters.js";
11
+ import { egressFetch } from "../util/egress-fetch.js";
11
12
 
12
13
  const TAVILY_ENDPOINT = "https://api.tavily.com/search";
13
14
 
@@ -55,7 +56,7 @@ export const tavilyBackend = {
55
56
 
56
57
  let response;
57
58
  try {
58
- response = await fetch(baseURL, {
59
+ response = await egressFetch(baseURL, {
59
60
  method: "POST",
60
61
  headers: {
61
62
  "Authorization": `Bearer ${apiKey}`,
package/lib/client.js CHANGED
@@ -171,6 +171,78 @@ window.__ModuleLoader__.load({
171
171
  };
172
172
  }
173
173
 
174
+ /**
175
+ * Settings bridge for 0.2.x builds where the legacy `settingsScope`
176
+ * client service is gone: the same snapshot/subscribe/set/unset surface
177
+ * the card controller expects, backed by this plugin's fenced settings
178
+ * route. Writes land in the volatile profile entry the host provider
179
+ * resolves from on the next call -- no second settings store.
180
+ */
181
+ class SettingsRoutesBridge {
182
+ constructor() {
183
+ this.store = createStore({
184
+ status: "loading",
185
+ writable: false,
186
+ value: undefined,
187
+ base: undefined,
188
+ user: undefined,
189
+ revision: undefined,
190
+ mode: "routes",
191
+ });
192
+ }
193
+
194
+ getSnapshot() { return this.store.getSnapshot(); }
195
+ subscribe(listener) { return this.store.subscribe(listener); }
196
+
197
+ accept(payload) {
198
+ if (payload === null || typeof payload !== "object" || payload.ok !== true || payload.writable === false) {
199
+ // Keep the card rendered (status ready) but read-only, so the section
200
+ // explains itself instead of showing an endless loading ellipsis.
201
+ this.store.set({ status: "ready", writable: false, value: {}, base: undefined, user: undefined, revision: undefined, mode: "routes" });
202
+ return;
203
+ }
204
+ this.store.set({
205
+ status: "ready",
206
+ writable: true,
207
+ value: payload.value,
208
+ base: payload.base,
209
+ user: payload.user,
210
+ revision: payload.revision,
211
+ mode: "routes",
212
+ });
213
+ }
214
+
215
+ async load() {
216
+ try {
217
+ const response = await fetch("/api/unified-search/settings", { credentials: "same-origin", headers: { accept: "application/json" } });
218
+ const payload = await response.json().catch(() => undefined);
219
+ this.accept(response.status === 404 ? undefined : payload);
220
+ } catch (_noRoutes) {
221
+ this.accept(undefined);
222
+ }
223
+ }
224
+
225
+ async write(ops) {
226
+ const expectedRevision = this.store.getSnapshot().revision;
227
+ const response = await fetch("/api/unified-search/settings", {
228
+ method: "POST",
229
+ credentials: "same-origin",
230
+ headers: { "content-type": "application/json", accept: "application/json" },
231
+ // eslint-disable-next-line object-shorthand
232
+ body: JSON.stringify(expectedRevision === undefined ? { ops } : { ops, expectedRevision }),
233
+ });
234
+ const payload = await response.json().catch(() => undefined);
235
+ if (response.status === 409 || (payload !== undefined && payload.code === "SETTINGS_CONFLICT")) throw new Error("SETTINGS_CONFLICT");
236
+ if (!response.ok || payload === undefined || payload.ok !== true) {
237
+ throw new Error(payload !== undefined && payload.error !== undefined ? String(payload.error) : "settings request failed (" + response.status + ")");
238
+ }
239
+ this.accept(payload);
240
+ }
241
+
242
+ set(field, value) { return this.write([{ op: "set", path: [field], value }]); }
243
+ unset(field) { return this.write([{ op: "unset", path: [field] }]); }
244
+ }
245
+
174
246
  /**
175
247
  * Card state machine: reads the bound settings scope snapshot, stages edits,
176
248
  * writes section fields through scope.set/unset and secrets via credentials.
@@ -247,14 +319,37 @@ window.__ModuleLoader__.load({
247
319
 
248
320
  async readCredential(ref) {
249
321
  try {
250
- const res = await this.connection.api.credentials.describe({ refs: [ref] });
251
- if (!res.result.ok) return;
252
- const view = res.result.value.credentials[ref];
253
- this.keyState.set(ref, { configured: view?.configured ?? false, writable: view?.writable !== false });
322
+ let view;
323
+ if (this.remote !== undefined && this.remote.credentials !== undefined && typeof this.remote.credentials.describe === "function") {
324
+ // 0.2.x remote face: Record<ref, {configured, writable, source}> behind {ok, value}.
325
+ const res = await this.remote.credentials.describe([ref]);
326
+ const record = res !== null && typeof res === "object" && res.value !== undefined ? res.value : res;
327
+ const byRef = record !== null && typeof record === "object" && record.credentials !== undefined ? record.credentials : record;
328
+ view = byRef !== null && typeof byRef === "object" ? byRef[ref] : undefined;
329
+ }
330
+ if (view === undefined && this.connection !== undefined && this.connection.api !== undefined && this.connection.api.credentials !== undefined) {
331
+ // Older line: the connection controller's credentials API.
332
+ const res = await this.connection.api.credentials.describe({ refs: [ref] });
333
+ if (res?.result?.ok) view = res.result.value.credentials[ref];
334
+ }
335
+ if (view === undefined) return;
336
+ this.keyState.set(ref, { configured: view.configured === true, writable: view.writable !== false });
254
337
  } catch { /* ignore */ }
255
338
  this.republish();
256
339
  }
257
340
 
341
+ async writeCredential(ref, value) {
342
+ if (this.remote !== undefined && this.remote.credentials !== undefined && typeof this.remote.credentials.set === "function") {
343
+ await this.remote.credentials.set(ref, value);
344
+ return;
345
+ }
346
+ if (this.connection !== undefined && this.connection.api !== undefined && this.connection.api.credentials !== undefined && typeof this.connection.api.credentials.set === "function") {
347
+ await this.connection.api.credentials.set({ ref, value });
348
+ return;
349
+ }
350
+ throw new Error("credentials service unavailable");
351
+ }
352
+
258
353
  refreshAllCredentials() {
259
354
  for (const b of BACKENDS) if (b.keyEnv) this.readCredential(b.keyEnv);
260
355
  }
@@ -291,7 +386,7 @@ window.__ModuleLoader__.load({
291
386
  backendTimeoutMs: [1000, 120000],
292
387
  };
293
388
  const [lo, hi] = BOUNDS[field] ?? [-Infinity, Infinity];
294
- { this.failed = true; this.failedFields.add(field); continue; }
389
+ if (!Number.isFinite(num) || num < lo || num > hi) { this.failed = true; this.failedFields.add(field); continue; }
295
390
  await this.scope.set(field, num);
296
391
  doneFields.push(field);
297
392
  } else {
@@ -303,7 +398,7 @@ window.__ModuleLoader__.load({
303
398
  const value = staged.text.trim();
304
399
  if (value !== "") {
305
400
  try {
306
- await this.connection.api.credentials.set({ ref, value });
401
+ await this.writeCredential(ref, value);
307
402
  this.keyState.set(ref, { configured: true, writable: true });
308
403
  doneKeys.push(ref);
309
404
  this.failedKeys.delete(ref);
@@ -424,6 +519,7 @@ window.__ModuleLoader__.load({
424
519
  backend: b,
425
520
  value: state.value[toggleField(b.id)],
426
521
  keyConfigured: true,
522
+ t: props.t,
427
523
  onChange: (checked) => card.setToggle(b.id, checked),
428
524
  }, b.id)
429
525
  ) }),
@@ -433,6 +529,7 @@ window.__ModuleLoader__.load({
433
529
  backend: b,
434
530
  value: state.value[toggleField(b.id)],
435
531
  keyConfigured: !!state.keyState[b.keyEnv]?.configured,
532
+ t: props.t,
436
533
  onChange: (checked) => card.setToggle(b.id, checked),
437
534
  }, b.id)
438
535
  ) }),
@@ -545,27 +642,49 @@ window.__ModuleLoader__.load({
545
642
 
546
643
  /**
547
644
  * Plugin entry: register the card into the settings plugins page.
548
- * Compatibility (compat-audit P0): the settingsScope client service was
549
- * removed in DSH 0.1.7. It stays in `inject` so the fiber still
550
- * activates when the shim supplies it; every access is feature-detected.
645
+ * Compatibility: the legacy `settingsScope` client service was removed on
646
+ * the 0.2.x line; the card binds whichever data source the build offers
647
+ * (see selectBridge) and registers the section unconditionally.
551
648
  */
552
- const inject = ["slots", "locale", "connection", "remote", "settingsScope"];
649
+ const inject = ["slots", "locale", "connection", "remote"];
650
+ /**
651
+ * Resolve the settings data source for this build, in order:
652
+ * 1. the legacy `settingsScope` service (DSH <= 0.1.6 line, or a shim);
653
+ * 2. this plugin's own fenced settings routes (0.2.x line, where the
654
+ * client service was removed and the settings shell renders client
655
+ * slot sections only);
656
+ * 3. an unavailable bridge -- the section still registers and the card
657
+ * explains itself through its read-only banner.
658
+ */
659
+ function selectBridge(ctx) {
660
+ try {
661
+ const scope = ctx.get("settingsScope");
662
+ if (scope !== undefined && typeof scope.bind === "function") return scope.bind({ namespace: PLUGIN_NS });
663
+ } catch (_noSettingsScope) { /* fall through to the routes bridge */ }
664
+ return new SettingsRoutesBridge();
665
+ }
553
666
  function apply(ctx) {
554
667
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "websearch: copy dictionaries");
555
- const hasSettingsScope = ctx.get("settingsScope") !== undefined && typeof ctx.settingsScope?.bind === "function";
556
- if (!hasSettingsScope) {
557
- // DSH >= 0.1.7 without dsh-settings-scope-shim: the settings card
558
- // cannot bind its scope. Stop wiring the card instead of hard-
559
- // crashing the client fiber; the breadcrumb aids diagnosis.
560
- console.warn("[dsh-websearch] settingsScope service unavailable (DSH >= 0.1.7); unified-search settings card disabled. Configure via server settings/cordis config or install dsh-settings-scope-shim.");
561
- return;
562
- }
668
+ const bridge = selectBridge(ctx);
669
+ if (typeof bridge.load === "function") void bridge.load();
563
670
  const controller = new UnifiedSearchCardController(
564
- ctx.settingsScope.bind({ namespace: PLUGIN_NS }),
671
+ bridge,
565
672
  ctx.get("connection"),
566
673
  ctx.get("remote"),
567
674
  );
568
- ctx.effect(() => ctx.remote.$on("credentials/updated", () => controller.refreshAllCredentials()), "websearch: credential invalidations");
675
+ // Credential invalidations: 0.2.x forwards `credentials/reference-updated`;
676
+ // older lines forwarded `credentials/updated`. Subscribe to whichever
677
+ // event face this build exposes; a missing face must not abort apply().
678
+ for (const event of ["credentials/reference-updated", "credentials/updated"]) {
679
+ try {
680
+ ctx.effect(() => ctx.remote.$on(event, () => controller.refreshAllCredentials()), "websearch: credential invalidations");
681
+ } catch (_noEventFace) { /* this build does not forward that event */ }
682
+ }
683
+ // Re-read when another client saves this namespace (settings document
684
+ // revisions travel through the remote event face on 0.2.x).
685
+ try {
686
+ ctx.effect(() => ctx.remote.$on("settings/document-updated", () => { if (typeof bridge.load === "function") void bridge.load(); }), "websearch: settings document invalidations");
687
+ } catch (_noSettingsEvent) { /* settings events unavailable */ }
569
688
  ctx.slots.inject("settings.section", () => ctx.slots.register({
570
689
  name: "settings.section",
571
690
  id: "websearch",
package/lib/health.js CHANGED
@@ -7,6 +7,8 @@
7
7
  // provider's own telemetry truncation) and run through a defensive redaction
8
8
  // pass for token-like fragments.
9
9
 
10
+ import { rejectionFor } from "./trust.js";
11
+
10
12
  const MAX_ERROR_CHARS = 120;
11
13
 
12
14
  /** Token-like fragments that must never appear in a health payload. */
@@ -89,6 +91,11 @@ export function registerHealthRoute(ws, deps) {
89
91
  res.writeHead(code, { "content-type": "application/json" });
90
92
  res.end(JSON.stringify(body));
91
93
  };
94
+ const rejection = rejectionFor(req, typeof deps.getConnection === "function" ? deps.getConnection() : undefined);
95
+ if (rejection !== undefined) {
96
+ send(rejection, { ok: false, error: rejection === 503 ? "admission unavailable" : "untrusted request" });
97
+ return;
98
+ }
92
99
  if (req.method !== "GET" && req.method !== "HEAD") {
93
100
  send(405, { ok: false, error: "method not allowed (GET only)" });
94
101
  return;
package/lib/history.js CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
10
10
  import path from "node:path";
11
+ import { rejectionFor } from "./trust.js";
11
12
 
12
13
  /** Ring-buffer cap (spec: 50). */
13
14
  export const HISTORY_MAX_ENTRIES = 50;
@@ -143,18 +144,19 @@ export function toBackendObservability(healthMap, nowMs = Date.now()) {
143
144
  * backends: per-backend last real outcome/latency/breaker state (v2.8.0),
144
145
  * so fallback redundancy is observable without a separate endpoint.
145
146
  * POST /api/websearch/history/clear -> { ok, cleared: true }
146
- * Both routes sit behind isTrustedApiRequest (loopback Host + same-site).
147
+ * Both routes sit behind the host connection admission fence (the same
148
+ * decision the kernel applies to its own /api routes).
147
149
  */
148
- export function registerHistoryRoutes(ws, { getStore, getBackendHealth }) {
150
+ export function registerHistoryRoutes(ws, { getStore, getBackendHealth, getConnection }) {
149
151
  if (!ws || typeof ws.register !== "function") return;
150
152
  const send = (res, code, body) => {
151
153
  res.writeHead(code, { "content-type": "application/json" });
152
154
  res.end(JSON.stringify(body));
153
155
  };
154
156
  const guarded = (handler) => (req, res) => {
155
- const fence = isTrustedApiRequest(req);
156
- if (fence) {
157
- send(res, 403, { ok: false, error: `untrusted request (${fence})` });
157
+ const rejection = rejectionFor(req, typeof getConnection === "function" ? getConnection() : undefined);
158
+ if (rejection !== undefined) {
159
+ send(res, rejection, { ok: false, error: rejection === 503 ? "admission unavailable" : "untrusted request" });
158
160
  return;
159
161
  }
160
162
  handler(req, res);
package/lib/index.js CHANGED
@@ -37,6 +37,7 @@ import { createUnifiedSearchProvider, getLatestBackendHealth } from "./provider.
37
37
  import { registerHealthRoute } from "./health.js";
38
38
  import { createSearchCache, CACHE_DEFAULTS } from "./cache.js";
39
39
  import { createHistoryStore, registerHistoryRoutes, HISTORY_MAX_ENTRIES } from "./history.js";
40
+ import { registerSettingsRoutes } from "./settings-api.js";
40
41
  import { BREAKER_DEFAULTS } from "./breaker.js";
41
42
  import os from "node:os";
42
43
  import path from "node:path";
@@ -391,6 +392,8 @@ export function resolveStoreDir(env = process.env, fallbackHome = os.homedir())
391
392
  */
392
393
  export function apply(ctx, config) {
393
394
  let current = () => config;
395
+ /** 0.2.x SettingsForms instance, captured for the browser settings bridge. */
396
+ let settingsForms;
394
397
 
395
398
  ctx.inject(["settings"], (settingsCtx) => {
396
399
  const settings = settingsCtx.settings;
@@ -404,10 +407,11 @@ export function apply(ctx, config) {
404
407
  });
405
408
  return;
406
409
  }
407
- // 0.2.0 SettingsForms line: no installSection; the settings form is
408
- // auto-generated from the volatile-marked Config above — no action and no
409
- // warning here (the form IS the section; a warning would contradict the
410
- // visible UI). Config values still resolve via cordis config.
410
+ // 0.2.x SettingsForms line: no installSection. Keep the service so the
411
+ // browser settings card can read/write this profile entry through the
412
+ // fenced bridge in ./settings-api.js; values keep resolving via cordis
413
+ // config (the same volatile entry), so there is no second settings store.
414
+ if (typeof settings?.describe === "function" && typeof settings?.mutate === "function") settingsForms = settings;
411
415
  });
412
416
 
413
417
  // v2.7.0 stores: disk cache + append-only search history, both under
@@ -449,16 +453,33 @@ export function apply(ctx, config) {
449
453
  // only reads the settings snapshot + last per-search telemetry — it never
450
454
  // probes backends and never touches credential values.
451
455
  ctx.inject(["webServer"], (webCtx) => {
456
+ const getConnection = () => {
457
+ try {
458
+ return ctx.get("connection");
459
+ } catch {
460
+ return undefined;
461
+ }
462
+ };
452
463
  registerHealthRoute(webCtx.webServer, {
453
464
  resolveOptions: () => resolveOptions(ctx, current()),
454
465
  backends: BACKENDS,
455
466
  getBackendHealth: getLatestBackendHealth,
467
+ getConnection,
456
468
  });
457
469
  // v2.7.0 search history: read-only GET + explicit clear POST.
458
470
  // v2.8.0: GET also carries per-backend last-outcome observability.
459
471
  registerHistoryRoutes(webCtx.webServer, {
460
472
  getStore: () => historyStore,
461
473
  getBackendHealth: getLatestBackendHealth,
474
+ getConnection,
475
+ });
476
+ // Settings bridge for the browser card on 0.2.x builds (no legacy
477
+ // settingsScope service). Fenced by the host connection admission; writes
478
+ // land in the volatile config entry above.
479
+ registerSettingsRoutes(webCtx.webServer, {
480
+ getSettings: () => settingsForms,
481
+ getConfig: () => Config,
482
+ getConnection,
462
483
  });
463
484
  });
464
485
  }