@zackbart/connecta 0.4.0 → 0.4.1

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 (55) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +34 -14
  3. package/SECURITY.md +1 -1
  4. package/dist/connectors/api.d.ts +10 -0
  5. package/dist/connectors/api.d.ts.map +1 -1
  6. package/dist/connectors/api.js +11 -1
  7. package/dist/connectors/api.js.map +1 -1
  8. package/dist/connectors/remote-mcp.d.ts +14 -1
  9. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  10. package/dist/connectors/remote-mcp.js +29 -0
  11. package/dist/connectors/remote-mcp.js.map +1 -1
  12. package/dist/credentials.d.ts +2 -1
  13. package/dist/credentials.d.ts.map +1 -1
  14. package/dist/credentials.js +4 -2
  15. package/dist/credentials.js.map +1 -1
  16. package/dist/executors/quickjs.d.ts.map +1 -1
  17. package/dist/executors/quickjs.js +32 -4
  18. package/dist/executors/quickjs.js.map +1 -1
  19. package/dist/index.d.ts +16 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +40 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/meta-tools.d.ts +3 -0
  24. package/dist/meta-tools.d.ts.map +1 -1
  25. package/dist/meta-tools.js +47 -6
  26. package/dist/meta-tools.js.map +1 -1
  27. package/dist/server.d.ts +2 -0
  28. package/dist/server.d.ts.map +1 -1
  29. package/dist/server.js +29 -5
  30. package/dist/server.js.map +1 -1
  31. package/dist/storage/file.d.ts.map +1 -1
  32. package/dist/storage/file.js +19 -3
  33. package/dist/storage/file.js.map +1 -1
  34. package/dist/ui.d.ts +1 -1
  35. package/dist/ui.d.ts.map +1 -1
  36. package/dist/ui.js +13 -9
  37. package/dist/ui.js.map +1 -1
  38. package/dist/validate.d.ts +33 -1
  39. package/dist/validate.d.ts.map +1 -1
  40. package/dist/validate.js +32 -2
  41. package/dist/validate.js.map +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +1 -1
  45. package/src/connectors/api.ts +21 -1
  46. package/src/connectors/remote-mcp.ts +52 -0
  47. package/src/credentials.ts +6 -3
  48. package/src/executors/quickjs.ts +32 -4
  49. package/src/index.ts +72 -1
  50. package/src/meta-tools.ts +90 -16
  51. package/src/server.ts +32 -5
  52. package/src/storage/file.ts +18 -2
  53. package/src/ui.ts +13 -8
  54. package/src/validate.ts +60 -2
  55. package/src/version.ts +1 -1
package/src/meta-tools.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  } from "./errors.js";
16
16
  import type { Registry } from "./registry.js";
17
17
  import { AVAILABLE_SKILLS } from "./skills.js";
18
- import type { KVStorage, ToolDef } from "./types.js";
18
+ import type { ConnectorStatus, KVStorage, ToolDef } from "./types.js";
19
19
 
20
20
  interface TextContent {
21
21
  type: "text";
@@ -72,6 +72,42 @@ function normalizeTimeoutMs(value: number | undefined): number | undefined {
72
72
  return Math.max(1, Math.trunc(value));
73
73
  }
74
74
 
75
+ /**
76
+ * Generous default bound for a single downstream probe/catalog call in the
77
+ * list/search/describe fan-out. High enough to trip only on a pathological
78
+ * hang, not a realistically slow probe.
79
+ */
80
+ const DEFAULT_PROBE_TIMEOUT_MS = 30_000;
81
+
82
+ /**
83
+ * Reject `promise` after `ms` if it has not settled, so one hung downstream
84
+ * cannot stall a whole fan-out. NOTE: this bounds only the caller-facing wait —
85
+ * the registry probe methods take no AbortSignal, so the underlying fetch is
86
+ * NOT cancelled and keeps running in the background. Real cancellation
87
+ * (AbortSignal plumbed through the registry) is a deferred follow-up.
88
+ */
89
+ function withTimeout<T>(
90
+ promise: Promise<T>,
91
+ ms: number,
92
+ label: string,
93
+ ): Promise<T> {
94
+ return new Promise<T>((resolve, reject) => {
95
+ const timer = setTimeout(() => {
96
+ reject(new Error(`${label} timed out after ${ms}ms`));
97
+ }, ms);
98
+ promise.then(
99
+ (value) => {
100
+ clearTimeout(timer);
101
+ resolve(value);
102
+ },
103
+ (err) => {
104
+ clearTimeout(timer);
105
+ reject(err);
106
+ },
107
+ );
108
+ });
109
+ }
110
+
75
111
  /**
76
112
  * How long to wait before the next attempt, or `undefined` for "don't retry".
77
113
  *
@@ -314,11 +350,15 @@ export function createMetaTools(
314
350
  maxResultBytes?: number;
315
351
  /** Deadline applied when a call passes no `timeoutMs`. Off when unset. */
316
352
  defaultToolTimeoutMs?: number;
353
+ /** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
354
+ probeTimeoutMs?: number;
317
355
  activity?: ActivityRequestContext;
318
356
  } = {},
319
357
  ) {
320
358
  const cap = opts.maxResultBytes ?? registry.maxResultBytes;
321
359
  const defaultToolTimeoutMs = normalizeTimeoutMs(opts.defaultToolTimeoutMs);
360
+ const probeTimeoutMs =
361
+ normalizeTimeoutMs(opts.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
322
362
  // createMetaTools() is called once per inbound MCP request. Sharing this
323
363
  // identity lets remote connectors reuse one downstream client inside that
324
364
  // request without leaking request-bound I/O into the next one.
@@ -622,25 +662,45 @@ export function createMetaTools(
622
662
  const checkedAt = new Date().toISOString();
623
663
  const statusStarted = Date.now();
624
664
  const observed = registry.healthFor(c.id);
625
- let status = probe
626
- ? await registry.statusFor(c.id, baseUrl, requestScope)
627
- : {
628
- state:
629
- observed?.consecutiveFailures &&
630
- observed.consecutiveFailures > 0
631
- ? ("error" as const)
632
- : observed?.lastSuccessAt || c.kind === "api"
633
- ? ("ok" as const)
634
- : ("unknown" as const),
635
- ...(observed?.lastError ? { message: observed.lastError } : {}),
636
- };
665
+ let status:
666
+ | ConnectorStatus
667
+ | { state: "ok" | "error" | "unknown"; message?: string };
668
+ if (probe) {
669
+ try {
670
+ status = await withTimeout(
671
+ registry.statusFor(c.id, baseUrl, requestScope),
672
+ probeTimeoutMs,
673
+ `list_connectors probe of "${c.id}"`,
674
+ );
675
+ } catch (err) {
676
+ // A probe that outran probeTimeoutMs (or otherwise threw)
677
+ // degrades this connector to an error status rather than
678
+ // hanging the whole list_connectors call.
679
+ status = { state: "error", message: msg(err) };
680
+ }
681
+ } else {
682
+ status = {
683
+ state:
684
+ observed?.consecutiveFailures &&
685
+ observed.consecutiveFailures > 0
686
+ ? ("error" as const)
687
+ : observed?.lastSuccessAt || c.kind === "api"
688
+ ? ("ok" as const)
689
+ : ("unknown" as const),
690
+ ...(observed?.lastError ? { message: observed.lastError } : {}),
691
+ };
692
+ }
637
693
  let tools = registry.peekTools(c.id);
638
694
  // An auth_required status may have just started OAuth. A second
639
695
  // listTools probe would overwrite its state/verifier while returning
640
696
  // the first (now stale) authorization URL.
641
697
  if (probe && status.state === "ok") {
642
698
  try {
643
- tools = await registry.refreshTools(c.id, baseUrl, requestScope);
699
+ tools = await withTimeout(
700
+ registry.refreshTools(c.id, baseUrl, requestScope),
701
+ probeTimeoutMs,
702
+ `list_connectors catalog refresh of "${c.id}"`,
703
+ );
644
704
  registry.recordSuccess(c.id, Date.now() - statusStarted);
645
705
  } catch (err) {
646
706
  status = { state: "error" as const, message: msg(err) };
@@ -687,7 +747,13 @@ export function createMetaTools(
687
747
  order: number;
688
748
  }> = [];
689
749
  const catalogs = await Promise.allSettled(
690
- conns.map((c) => registry.getTools(c.id, baseUrl, requestScope)),
750
+ conns.map((c) =>
751
+ withTimeout(
752
+ registry.getTools(c.id, baseUrl, requestScope),
753
+ probeTimeoutMs,
754
+ `search_tools probe of "${c.id}"`,
755
+ ),
756
+ ),
691
757
  );
692
758
  let orderBase = 0;
693
759
  catalogs.forEach((catalog, connectorIndex) => {
@@ -791,7 +857,13 @@ export function createMetaTools(
791
857
  ),
792
858
  ];
793
859
  const loaded = await Promise.allSettled(
794
- connectorIds.map((id) => registry.getTools(id, baseUrl, requestScope)),
860
+ connectorIds.map((id) =>
861
+ withTimeout(
862
+ registry.getTools(id, baseUrl, requestScope),
863
+ probeTimeoutMs,
864
+ `describe_tools probe of "${id}"`,
865
+ ),
866
+ ),
795
867
  );
796
868
  const catalogs = new Map<string, ToolDef[] | Error>();
797
869
  loaded.forEach((result, index) => {
@@ -1040,12 +1112,14 @@ export function registerMetaTools(
1040
1112
  baseUrl: string;
1041
1113
  maxResultBytes?: number;
1042
1114
  defaultToolTimeoutMs?: number;
1115
+ probeTimeoutMs?: number;
1043
1116
  activity?: ActivityRequestContext;
1044
1117
  },
1045
1118
  ): void {
1046
1119
  const mt = createMetaTools(registry, ctx.baseUrl, {
1047
1120
  maxResultBytes: ctx.maxResultBytes,
1048
1121
  defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
1122
+ probeTimeoutMs: ctx.probeTimeoutMs,
1049
1123
  activity: ctx.activity,
1050
1124
  });
1051
1125
 
package/src/server.ts CHANGED
@@ -49,6 +49,8 @@ export interface ServerOptions {
49
49
  deploymentInfo?: Record<string, unknown>;
50
50
  /** Deadline for call_tool/batch_call calls that pass no timeoutMs. Off when unset. */
51
51
  defaultToolTimeoutMs?: number;
52
+ /** Per-connector deadline for the list/search/describe probe fan-out. Default 30_000. */
53
+ probeTimeoutMs?: number;
52
54
  /** When set, the execute_code meta-tool is registered on top of the nine. */
53
55
  executor?: Executor;
54
56
  /** Encrypted connector-credential storage backing the authenticated /ui controls. */
@@ -61,6 +63,14 @@ function msg(err: unknown): string {
61
63
  return err instanceof Error ? err.message : String(err);
62
64
  }
63
65
 
66
+ /** Per-request base64 nonce for the /ui page's inline scripts (Node 20+ and Workers). */
67
+ function uiScriptNonce(): string {
68
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
69
+ let binary = "";
70
+ for (const byte of bytes) binary += String.fromCharCode(byte);
71
+ return btoa(binary);
72
+ }
73
+
64
74
  function escapeHtml(s: string): string {
65
75
  return s
66
76
  .replaceAll("&", "&amp;")
@@ -231,9 +241,12 @@ function withSecurityHeaders(
231
241
  headers.set("Strict-Transport-Security", "max-age=31536000");
232
242
  }
233
243
  if (path === "/ui") {
234
- // A directive-only CSP does not interfere with the UI's existing scripts,
235
- // while preventing the authenticated operator surface from being framed.
236
- headers.set("Content-Security-Policy", "frame-ancestors 'none'");
244
+ // The /ui GET response ships its own nonce-based script CSP (which already
245
+ // includes frame-ancestors 'none'); only fall back to the framing-only
246
+ // directive when no CSP is present (e.g. HTTPS redirects, error responses).
247
+ if (!headers.has("Content-Security-Policy")) {
248
+ headers.set("Content-Security-Policy", "frame-ancestors 'none'");
249
+ }
237
250
  headers.set("X-Frame-Options", "DENY");
238
251
  }
239
252
  return new Response(response.body, {
@@ -523,6 +536,7 @@ async function serveMcp(
523
536
  baseUrl,
524
537
  activity,
525
538
  defaultToolTimeoutMs: opts.defaultToolTimeoutMs,
539
+ probeTimeoutMs: opts.probeTimeoutMs,
526
540
  });
527
541
  if (opts.executor) {
528
542
  registerExecuteTool(server, opts.registry, {
@@ -693,9 +707,22 @@ export function createFetchHandler(
693
707
  // Open shell — carries no data; data comes only from the gated /ui/data.
694
708
  const uiAuth = auth.find((provider) => provider.uiAuth)?.uiAuth;
695
709
  const mcpUrl = new URL("/mcp", baseUrl).toString();
696
- return new Response(renderUiHtml(uiAuth, mcpUrl, opts.branding), {
710
+ // Nonce the page's inline script (and the Clerk loader). 'strict-dynamic'
711
+ // lets scripts the nonced Clerk loader injects at runtime execute; the
712
+ // https:/'unsafe-inline' fallbacks are ignored by CSP3 browsers that
713
+ // honour the nonce and only cover legacy ones. No default-src, so Clerk's
714
+ // style/font/network needs and the page's inline <style> stay unrestricted
715
+ // — only script execution, the XSS sink, is gated.
716
+ const nonce = uiScriptNonce();
717
+ return new Response(renderUiHtml(uiAuth, mcpUrl, opts.branding, nonce), {
697
718
  status: 200,
698
- headers: { "Content-Type": "text/html; charset=utf-8" },
719
+ headers: {
720
+ "Content-Type": "text/html; charset=utf-8",
721
+ "Content-Security-Policy":
722
+ `script-src 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline'; ` +
723
+ "object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
724
+ "X-Content-Type-Options": "nosniff",
725
+ },
699
726
  });
700
727
  }
701
728
 
@@ -1,4 +1,5 @@
1
1
  import {
2
+ chmodSync,
2
3
  existsSync,
3
4
  mkdirSync,
4
5
  readFileSync,
@@ -28,8 +29,20 @@ export function fileStorage(
28
29
  opts: FileStorageOptions = {},
29
30
  ): KVStorage {
30
31
  const logger: Logger = opts.logger ?? console;
32
+ // The state file holds downstream OAuth access/refresh tokens in cleartext,
33
+ // so keep it owner-only. Repair is best-effort: chmod is a no-op or throws on
34
+ // non-POSIX filesystems, and a loose mode must never keep the store from
35
+ // starting.
36
+ const tighten = () => {
37
+ try {
38
+ chmodSync(path, 0o600);
39
+ } catch {
40
+ // Non-POSIX filesystem or a race on the file — leave the mode as-is.
41
+ }
42
+ };
31
43
  let data: Record<string, Entry> = {};
32
44
  if (existsSync(path)) {
45
+ tighten();
33
46
  try {
34
47
  data = JSON.parse(readFileSync(path, "utf8")) as Record<string, Entry>;
35
48
  } catch (error) {
@@ -60,10 +73,13 @@ export function fileStorage(
60
73
  }
61
74
  const persist = () => {
62
75
  const dir = dirname(path);
63
- if (dir) mkdirSync(dir, { recursive: true });
76
+ if (dir) mkdirSync(dir, { recursive: true, mode: 0o700 });
64
77
  const tmp = `${path}.tmp`;
65
- writeFileSync(tmp, JSON.stringify(data));
78
+ // 0o600 on the tmp file; the atomic rename below preserves it, so the live
79
+ // state file is never briefly world-readable.
80
+ writeFileSync(tmp, JSON.stringify(data), { mode: 0o600 });
66
81
  renameSync(tmp, path);
82
+ tighten();
67
83
  };
68
84
  const fresh = (key: string): Entry | null => {
69
85
  const e = data[key];
package/src/ui.ts CHANGED
@@ -29,15 +29,16 @@ export function resolveBranding(
29
29
  ): ResolvedBranding {
30
30
  const productName = branding?.productName?.trim() || "Connecta";
31
31
  const ownerName = branding?.ownerName?.trim();
32
+ // Operator branding URLs become masthead/callback hrefs, so a non-http(s)
33
+ // scheme (javascript:, data:) is dropped the same as an unset URL — the
34
+ // callers already render a <span> instead of an <a> when it is absent.
35
+ const productUrl = branding?.productUrl?.trim();
36
+ const ownerUrl = branding?.ownerUrl?.trim();
32
37
  return {
33
38
  productName,
34
- ...(branding?.productUrl?.trim()
35
- ? { productUrl: branding.productUrl.trim() }
36
- : {}),
39
+ ...(productUrl && isSafeHttpUrl(productUrl) ? { productUrl } : {}),
37
40
  ...(ownerName ? { ownerName } : {}),
38
- ...(branding?.ownerUrl?.trim()
39
- ? { ownerUrl: branding.ownerUrl.trim() }
40
- : {}),
41
+ ...(ownerUrl && isSafeHttpUrl(ownerUrl) ? { ownerUrl } : {}),
41
42
  description:
42
43
  branding?.description?.trim() ||
43
44
  `Manage the services this ${productName} instance makes available to agents.`,
@@ -302,10 +303,14 @@ export function renderUiHtml(
302
303
  uiAuth?: UiAuthConfig,
303
304
  mcpUrl = "/mcp",
304
305
  branding?: ConnectaBranding,
306
+ nonce?: string,
305
307
  ): string {
306
308
  const auth = uiAuth ?? { kind: "bearer" as const };
307
309
  const brand = resolveBranding(branding);
308
310
  const title = brand.pageTitle;
311
+ // When the /ui response ships a nonce-based CSP, every <script> it emits must
312
+ // carry that nonce to run; without a nonce the markup is unchanged.
313
+ const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
309
314
  // Top-left corner. With an owner set it reads "<owner> <product>"; without
310
315
  // one the product label stands alone. Either half links out when the
311
316
  // matching URL is configured.
@@ -323,7 +328,7 @@ export function renderUiHtml(
323
328
  : "";
324
329
  const clerkScript =
325
330
  uiAuth?.kind === "clerk"
326
- ? `<script defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(uiAuth.publishableKey)}" src="${escapeHtmlAttr(uiAuth.frontendApiUrl)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
331
+ ? `<script${nonceAttr} defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(uiAuth.publishableKey)}" src="${escapeHtmlAttr(uiAuth.frontendApiUrl)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
327
332
  : "";
328
333
 
329
334
  return `<!doctype html>
@@ -683,7 +688,7 @@ ${clerkScript}
683
688
  </section>
684
689
  </main>
685
690
 
686
- <script>
691
+ <script${nonceAttr}>
687
692
  const AUTH = ${jsonForInlineScript(auth)};
688
693
  const MCP_URL = ${jsonForInlineScript(mcpUrl)};
689
694
  const filterUiConnectors = ${filterUiConnectors.toString()};
package/src/validate.ts CHANGED
@@ -13,6 +13,28 @@ export interface ValidateToolInputOptions {
13
13
  * unusable. Default console.
14
14
  */
15
15
  logger?: Logger;
16
+ /**
17
+ * Fail-closed on a schema the validator cannot evaluate (default false =
18
+ * today's fail-open behavior). When true, a schema that cannot be compiled —
19
+ * or that only fails on first use, e.g. an unresolvable `$ref` — yields a
20
+ * non-retryable `invalid_args` error instead of passing the raw arguments
21
+ * through, so unvalidated input is never silently admitted. The happy path
22
+ * (a schema that compiles and validates) is unaffected.
23
+ */
24
+ failClosed?: boolean;
25
+ }
26
+
27
+ export interface PrecompileValidatorOptions {
28
+ /**
29
+ * Tool address used in the warning text, conventionally
30
+ * `"connectorId.toolName"`.
31
+ */
32
+ address: string;
33
+ /**
34
+ * Destination for the warning emitted when the schema cannot be compiled.
35
+ * Default console.
36
+ */
37
+ logger?: Logger;
16
38
  }
17
39
 
18
40
  // Lazy validator cache keyed by the schema object itself; null marks a schema
@@ -21,6 +43,13 @@ export interface ValidateToolInputOptions {
21
43
  // connector are collectable, the same pattern compactSchema uses.
22
44
  const validators = new WeakMap<JsonSchema, Validator | null>();
23
45
 
46
+ function unevaluableSchema(address: string): ConnectorCallError {
47
+ return new ConnectorCallError(
48
+ "invalid_args",
49
+ `Cannot validate arguments for "${address}": its inputSchema could not be evaluated`,
50
+ );
51
+ }
52
+
24
53
  function disableValidation(
25
54
  schema: JsonSchema,
26
55
  address: string,
@@ -47,7 +76,9 @@ function disableValidation(
47
76
  *
48
77
  * A schema the validator cannot compile (or that only fails on first use, e.g.
49
78
  * an unresolvable `$ref`) is warned about once and then passed through — a
50
- * broken schema should not break an otherwise working tool.
79
+ * broken schema should not break an otherwise working tool. Pass
80
+ * `failClosed: true` to instead reject such calls with `invalid_args`, for
81
+ * callers that would rather refuse a call than forward unvalidated arguments.
51
82
  *
52
83
  * The compiled validator is cached by **schema object identity**, so pass a
53
84
  * stable object: hold the parsed manifest and hand the same schema back on
@@ -74,12 +105,18 @@ export function validateToolInput(
74
105
  validator = null;
75
106
  }
76
107
  }
108
+ // A schema the validator could not compile (or that a prior call disabled):
109
+ // pass through by default, refuse when the caller opted into fail-closed.
110
+ if (validator === null) {
111
+ return opts.failClosed ? unevaluableSchema(opts.address) : null;
112
+ }
77
113
  let result;
78
114
  try {
79
- result = validator?.validate(args);
115
+ result = validator.validate(args);
80
116
  } catch (err) {
81
117
  // e.g. an unresolvable $ref — surfaces on first validate, not compile.
82
118
  disableValidation(schema, opts.address, logger, err);
119
+ return opts.failClosed ? unevaluableSchema(opts.address) : null;
83
120
  }
84
121
  if (result && !result.valid) {
85
122
  const units = result.errors.filter((u) => u.instanceLocation !== "#");
@@ -94,3 +131,24 @@ export function validateToolInput(
94
131
  }
95
132
  return null;
96
133
  }
134
+
135
+ /**
136
+ * Eagerly compile and cache a tool's inputSchema so a schema the validator
137
+ * cannot use surfaces once at connector construction rather than silently on
138
+ * the first call. Reuses the same module-level cache `validateToolInput` reads,
139
+ * so the runtime path hits the cache. Warning-only: it never throws and never
140
+ * changes call behavior. A schema that only fails on first `validate()` (e.g.
141
+ * an unresolvable `$ref`) still slips through here and is caught at call time.
142
+ */
143
+ export function precompileValidator(
144
+ schema: JsonSchema,
145
+ opts: PrecompileValidatorOptions,
146
+ ): void {
147
+ if (validators.has(schema)) return;
148
+ const logger = opts.logger ?? console;
149
+ try {
150
+ validators.set(schema, new Validator(schema as never, "2020-12", false));
151
+ } catch (err) {
152
+ disableValidation(schema, opts.address, logger, err);
153
+ }
154
+ }
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.4.0";
7
+ export const CONNECTA_VERSION = "0.4.1";