@zackbart/connecta 0.3.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 (69) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/README.md +34 -14
  3. package/SECURITY.md +1 -1
  4. package/dist/catalog.d.ts.map +1 -1
  5. package/dist/catalog.js +62 -0
  6. package/dist/catalog.js.map +1 -1
  7. package/dist/connectors/api.d.ts +10 -0
  8. package/dist/connectors/api.d.ts.map +1 -1
  9. package/dist/connectors/api.js +16 -39
  10. package/dist/connectors/api.js.map +1 -1
  11. package/dist/connectors/remote-mcp.d.ts +14 -1
  12. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  13. package/dist/connectors/remote-mcp.js +29 -0
  14. package/dist/connectors/remote-mcp.js.map +1 -1
  15. package/dist/credentials.d.ts +2 -1
  16. package/dist/credentials.d.ts.map +1 -1
  17. package/dist/credentials.js +4 -2
  18. package/dist/credentials.js.map +1 -1
  19. package/dist/errors.d.ts +20 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +39 -1
  22. package/dist/errors.js.map +1 -1
  23. package/dist/executors/quickjs.d.ts.map +1 -1
  24. package/dist/executors/quickjs.js +32 -4
  25. package/dist/executors/quickjs.js.map +1 -1
  26. package/dist/index.d.ts +34 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +45 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/json-schema.d.ts +3 -0
  31. package/dist/json-schema.d.ts.map +1 -0
  32. package/dist/json-schema.js +6 -0
  33. package/dist/json-schema.js.map +1 -0
  34. package/dist/meta-tools.d.ts +34 -2
  35. package/dist/meta-tools.d.ts.map +1 -1
  36. package/dist/meta-tools.js +104 -15
  37. package/dist/meta-tools.js.map +1 -1
  38. package/dist/server.d.ts +4 -0
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/server.js +34 -6
  41. package/dist/server.js.map +1 -1
  42. package/dist/storage/file.d.ts.map +1 -1
  43. package/dist/storage/file.js +19 -3
  44. package/dist/storage/file.js.map +1 -1
  45. package/dist/ui.d.ts +1 -1
  46. package/dist/ui.d.ts.map +1 -1
  47. package/dist/ui.js +13 -9
  48. package/dist/ui.js.map +1 -1
  49. package/dist/validate.d.ts +71 -0
  50. package/dist/validate.d.ts.map +1 -0
  51. package/dist/validate.js +96 -0
  52. package/dist/validate.js.map +1 -0
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +5 -1
  56. package/src/catalog.ts +61 -0
  57. package/src/connectors/api.ts +25 -50
  58. package/src/connectors/remote-mcp.ts +52 -0
  59. package/src/credentials.ts +6 -3
  60. package/src/errors.ts +45 -2
  61. package/src/executors/quickjs.ts +32 -4
  62. package/src/index.ts +94 -1
  63. package/src/json-schema.ts +11 -0
  64. package/src/meta-tools.ts +156 -28
  65. package/src/server.ts +39 -6
  66. package/src/storage/file.ts +18 -2
  67. package/src/ui.ts +13 -8
  68. package/src/validate.ts +154 -0
  69. package/src/version.ts +1 -1
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()};
@@ -0,0 +1,154 @@
1
+ import { Validator } from "@cfworker/json-schema";
2
+ import { ConnectorCallError } from "./errors.js";
3
+ import type { JsonSchema, Logger } from "./types.js";
4
+
5
+ export interface ValidateToolInputOptions {
6
+ /**
7
+ * Tool address used in the error and warning text, conventionally
8
+ * `"connectorId.toolName"`.
9
+ */
10
+ address: string;
11
+ /**
12
+ * Destination for the one-time warning emitted when a schema turns out to be
13
+ * unusable. Default console.
14
+ */
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;
38
+ }
39
+
40
+ // Lazy validator cache keyed by the schema object itself; null marks a schema
41
+ // the validator rejected (warned once, then passed through rather than
42
+ // breaking a working tool). A WeakMap so schemas belonging to a discarded
43
+ // connector are collectable, the same pattern compactSchema uses.
44
+ const validators = new WeakMap<JsonSchema, Validator | null>();
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
+
53
+ function disableValidation(
54
+ schema: JsonSchema,
55
+ address: string,
56
+ logger: Logger,
57
+ err: unknown,
58
+ ): void {
59
+ validators.set(schema, null);
60
+ logger.warn(
61
+ `[connecta] tool "${address}" has an inputSchema the validator cannot use (${
62
+ err instanceof Error ? err.message : String(err)
63
+ }) — arguments are not validated`,
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Validate call arguments against a tool's JSON Schema.
69
+ *
70
+ * Returns a non-retryable `invalid_args` ConnectorCallError describing the
71
+ * mismatch, or null when the arguments are acceptable. It deliberately returns
72
+ * rather than throws: the caller decides what to do with the failure, which is
73
+ * what lets a connector own its error prose, or strip connector-wide
74
+ * convention arguments (a `confirm` flag on writes, say) that individual tool
75
+ * schemas do not declare before deciding the call is really invalid.
76
+ *
77
+ * A schema the validator cannot compile (or that only fails on first use, e.g.
78
+ * an unresolvable `$ref`) is warned about once and then passed through — a
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.
82
+ *
83
+ * The compiled validator is cached by **schema object identity**, so pass a
84
+ * stable object: hold the parsed manifest and hand the same schema back on
85
+ * every call. A schema rebuilt per call is a cache miss every time — it still
86
+ * validates correctly, but recompiles the validator on each call, silently and
87
+ * with nothing to show for it but latency.
88
+ *
89
+ * `api()` uses this internally; it is exported for connectors that implement
90
+ * the `Connector` interface directly.
91
+ */
92
+ export function validateToolInput(
93
+ schema: JsonSchema,
94
+ args: unknown,
95
+ opts: ValidateToolInputOptions,
96
+ ): ConnectorCallError | null {
97
+ const logger = opts.logger ?? console;
98
+ let validator = validators.get(schema);
99
+ if (validator === undefined) {
100
+ try {
101
+ validator = new Validator(schema as never, "2020-12", false);
102
+ validators.set(schema, validator);
103
+ } catch (err) {
104
+ disableValidation(schema, opts.address, logger, err);
105
+ validator = null;
106
+ }
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
+ }
113
+ let result;
114
+ try {
115
+ result = validator.validate(args);
116
+ } catch (err) {
117
+ // e.g. an unresolvable $ref — surfaces on first validate, not compile.
118
+ disableValidation(schema, opts.address, logger, err);
119
+ return opts.failClosed ? unevaluableSchema(opts.address) : null;
120
+ }
121
+ if (result && !result.valid) {
122
+ const units = result.errors.filter((u) => u.instanceLocation !== "#");
123
+ const detail = (units.length > 0 ? units : result.errors)
124
+ .slice(0, 3)
125
+ .map((u) => `${u.instanceLocation}: ${u.error}`)
126
+ .join("; ");
127
+ return new ConnectorCallError(
128
+ "invalid_args",
129
+ `Invalid arguments for "${opts.address}": ${detail || "input does not match the tool's inputSchema"}`,
130
+ );
131
+ }
132
+ return null;
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.3.0";
7
+ export const CONNECTA_VERSION = "0.4.1";