@jskit-ai/connectors-catalog 0.2.3 → 0.2.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.
Files changed (192) hide show
  1. package/README.md +4 -4
  2. package/docs/ai.md +3 -3
  3. package/docs/airtable.md +1 -1
  4. package/docs/algolia.md +2 -2
  5. package/docs/amazon-redshift.md +2 -2
  6. package/docs/apollo-io.md +2 -2
  7. package/docs/asana.md +2 -2
  8. package/docs/ashby.md +1 -1
  9. package/docs/attention.md +2 -2
  10. package/docs/aws-athena.md +1 -1
  11. package/docs/aws-credentials.md +3 -3
  12. package/docs/aws-s3.md +1 -1
  13. package/docs/bigquery.md +2 -2
  14. package/docs/brevo.md +2 -2
  15. package/docs/calendly.md +3 -3
  16. package/docs/canva.md +2 -2
  17. package/docs/chargebee.md +2 -2
  18. package/docs/clay.md +2 -2
  19. package/docs/clickhouse.md +1 -1
  20. package/docs/confidence-flags.md +1 -1
  21. package/docs/contentful.md +1 -1
  22. package/docs/dbt-semantic-layer.md +2 -2
  23. package/docs/elevenlabs.md +2 -2
  24. package/docs/firebase-cloud-messaging.md +3 -3
  25. package/docs/firecrawl.md +4 -4
  26. package/docs/fireflies.md +1 -1
  27. package/docs/gatewayapi.md +1 -1
  28. package/docs/gemini-enterprise.md +1 -1
  29. package/docs/github-api.md +1 -1
  30. package/docs/gitlab-api.md +1 -1
  31. package/docs/gmail.md +1 -1
  32. package/docs/gong.md +1 -1
  33. package/docs/google-ads.md +3 -3
  34. package/docs/google-analytics.md +2 -2
  35. package/docs/google-docs.md +1 -1
  36. package/docs/google-drive.md +1 -1
  37. package/docs/google-maps-platform.md +2 -2
  38. package/docs/google-search-console.md +1 -1
  39. package/docs/google-sheets.md +1 -1
  40. package/docs/google-slides.md +2 -2
  41. package/docs/granola.md +2 -2
  42. package/docs/hex.md +2 -2
  43. package/docs/heygen.md +2 -2
  44. package/docs/inngest.md +1 -1
  45. package/docs/klipy.md +1 -1
  46. package/docs/lightspeed.md +2 -2
  47. package/docs/linear.md +1 -1
  48. package/docs/linkedin.md +2 -2
  49. package/docs/mailgun.md +1 -1
  50. package/docs/microsoft-excel.md +2 -2
  51. package/docs/microsoft-fabric.md +1 -1
  52. package/docs/microsoft-outlook.md +1 -1
  53. package/docs/microsoft-powerpoint.md +1 -1
  54. package/docs/microsoft-teams.md +1 -1
  55. package/docs/microsoft-word.md +1 -1
  56. package/docs/notion.md +1 -1
  57. package/docs/oura.md +3 -3
  58. package/docs/paddle.md +3 -3
  59. package/docs/pipedrive.md +2 -2
  60. package/docs/prestashop.md +1 -1
  61. package/docs/replicate.md +1 -1
  62. package/docs/resend.md +1 -1
  63. package/docs/salesforce.md +1 -1
  64. package/docs/sanity.md +3 -3
  65. package/docs/semrush.md +2 -2
  66. package/docs/sentry.md +1 -1
  67. package/docs/slack.md +1 -1
  68. package/docs/snowflake.md +5 -5
  69. package/docs/storyblok.md +1 -1
  70. package/docs/stripe.md +2 -2
  71. package/docs/tally.md +1 -1
  72. package/docs/telegram.md +2 -2
  73. package/docs/tiktok.md +3 -3
  74. package/docs/twilio.md +3 -3
  75. package/docs/twitch.md +2 -2
  76. package/docs/wave.md +3 -3
  77. package/docs/wix.md +13 -13
  78. package/docs/wordpress-com.md +1 -1
  79. package/docs/wordpress-self-hosted.md +1 -1
  80. package/docs/workday.md +2 -2
  81. package/docs/x-twitter.md +3 -3
  82. package/docs/xero.md +3 -3
  83. package/docs/zoho-books.md +2 -2
  84. package/docs/zoho-crm.md +2 -2
  85. package/package.json +4 -3
  86. package/patterns/ai-connections/PATTERN.md +16 -1
  87. package/patterns/ai-connections/example/ai-model.js +12 -3
  88. package/patterns/ai-connections/example/perplexity-answer.js +15 -4
  89. package/patterns/api-key-connection/PATTERN.md +9 -1
  90. package/patterns/api-key-connection/example/connections.js +5 -2
  91. package/patterns/aws-storage-queries/PATTERN.md +13 -3
  92. package/patterns/aws-storage-queries/example/s3-transfer.js +14 -3
  93. package/patterns/event-delivery/PATTERN.md +1 -1
  94. package/patterns/firebase-messaging/PATTERN.md +10 -0
  95. package/patterns/firebase-messaging/example/server/notifications.js +8 -0
  96. package/patterns/google-ads-search/PATTERN.md +18 -7
  97. package/patterns/google-ads-search/example/ads-setup.js +13 -5
  98. package/patterns/oauth-connection/PATTERN.md +1 -1
  99. package/patterns/paddle-catalogue/PATTERN.md +11 -2
  100. package/patterns/paddle-catalogue/example/create-products.js +5 -2
  101. package/patterns/public-image/PATTERN.md +10 -1
  102. package/patterns/public-image/example/logo-url.js +4 -1
  103. package/patterns/redshift-queries/PATTERN.md +1 -1
  104. package/patterns/source-scanning/PATTERN.md +13 -2
  105. package/patterns/source-scanning/example/source-scanner.js +9 -0
  106. package/src/server/ai.js +19 -5
  107. package/src/server/apify.js +2 -2
  108. package/src/server/asana.js +6 -6
  109. package/src/server/ashby.js +2 -1
  110. package/src/server/attention.js +12 -11
  111. package/src/server/awsTransport.js +6 -0
  112. package/src/server/bigquery.js +5 -4
  113. package/src/server/brevo.js +2 -1
  114. package/src/server/clay.js +2 -1
  115. package/src/server/clickhouse.js +2 -1
  116. package/src/server/confidence.js +6 -1
  117. package/src/server/connectorValues.js +33 -0
  118. package/src/server/databricks.js +7 -7
  119. package/src/server/dbt-semantic-layer.js +5 -5
  120. package/src/server/elevenlabs.js +2 -2
  121. package/src/server/firebase-cloud-messaging.js +2 -2
  122. package/src/server/gemini-enterprise.js +5 -5
  123. package/src/server/google-ads-search.js +68 -48
  124. package/src/server/google-ads.js +15 -17
  125. package/src/server/google-drive.js +7 -7
  126. package/src/server/google-maps-platform.js +4 -2
  127. package/src/server/google-search-console.js +4 -3
  128. package/src/server/google.js +8 -4
  129. package/src/server/googleAdsApi.js +14 -0
  130. package/src/server/granola.js +2 -2
  131. package/src/server/hubspot.js +2 -1
  132. package/src/server/incident-io.js +3 -2
  133. package/src/server/lexware.js +5 -4
  134. package/src/server/lightspeed.js +7 -5
  135. package/src/server/linkedin.js +2 -2
  136. package/src/server/mcpTransport.js +8 -4
  137. package/src/server/microsoft-fabric.js +3 -3
  138. package/src/server/microsoft-onedrive.js +2 -2
  139. package/src/server/microsoft-onenote.js +4 -3
  140. package/src/server/microsoft-outlook.js +6 -6
  141. package/src/server/microsoft-sharepoint.js +4 -2
  142. package/src/server/microsoft-teams.js +2 -1
  143. package/src/server/microsoft.js +20 -13
  144. package/src/server/notion.js +2 -1
  145. package/src/server/pipedrive.js +2 -1
  146. package/src/server/salesforce.js +12 -12
  147. package/src/server/semrush.js +9 -8
  148. package/src/server/snowflake.js +20 -20
  149. package/src/server/storyblok.js +2 -1
  150. package/src/server/tally.js +2 -1
  151. package/src/server/telegram.js +2 -6
  152. package/src/server/tiktok.js +3 -3
  153. package/src/server/twilio.js +3 -2
  154. package/src/server/twitch.js +5 -3
  155. package/src/server/types.js +13 -0
  156. package/src/server/wix.js +20 -878
  157. package/src/server/wixBookings.js +449 -0
  158. package/src/server/wixCms.js +132 -0
  159. package/src/server/wixStores.js +296 -0
  160. package/src/server/wixValues.js +35 -0
  161. package/src/server/wiz.js +18 -4
  162. package/src/server/woocommerce.js +1 -1
  163. package/src/server/wordpress-com.js +2 -1
  164. package/src/server/workday.js +9 -9
  165. package/src/server/x-twitter.js +2 -1
  166. package/src/server/xero.js +40 -40
  167. package/src/server/zoho-books.js +6 -6
  168. package/src/server/zoho-crm.js +5 -5
  169. package/src/shared/confidence.js +6 -1
  170. package/src/shared/dbt-semantic-layer.js +1 -1
  171. package/src/shared/figma.js +1 -1
  172. package/src/shared/google.js +8 -4
  173. package/src/shared/googleScope.js +4 -0
  174. package/src/shared/microsoft-fabric.js +4 -5
  175. package/src/shared/microsoft.js +8 -3
  176. package/src/shared/salesforce.js +1 -1
  177. package/src/shared/tokens.js +7 -3
  178. package/src/shared/uuid.js +13 -0
  179. package/src/shared/wix.js +4 -3
  180. package/src/shared/zoho-books.js +3 -4
  181. package/src/shared/zoho-crm.js +1 -1
  182. package/src/shared/{zoho-regions.js → zoho.js} +4 -1
  183. package/test/ai.test.js +1 -1
  184. package/test/brevo.test.js +1 -1
  185. package/test/chargebee.test.js +1 -1
  186. package/test/google-ads.test.js +8 -4
  187. package/test/linkedin.test.js +1 -1
  188. package/test/resend.test.js +2 -2
  189. package/test/salesforce.test.js +1 -1
  190. package/test/snowflake.test.js +7 -7
  191. package/test/tiktok.test.js +1 -1
  192. package/test/wiz.test.js +1 -1
@@ -38,6 +38,12 @@ the ordinary runtime for each request, preserving authorization and the
38
38
  connection's configured sandbox/live environment. The sandbox setting used for
39
39
  local schema validation sends no request and does not override runtime routing.
40
40
 
41
+ Each collaborator the composition receives (the connection `runtime`) is a
42
+ required parameter typed in a JSDoc `@param` and called directly, without a
43
+ `typeof` probe, a "requires x" guard or a no-op default. A server feature that
44
+ uses it follows
45
+ [typed features](https://github.com/mobily-enterprises/jskit-ai/blob/main/packages/agent-docs/site/guide/framework/typed-features.md).
46
+
41
47
  ## Product decisions
42
48
 
43
49
  For assistant use, authorize the complete cloned request with
@@ -58,12 +64,15 @@ termination can prevent the returned report from being saved, so an application
58
64
  needing crash recovery must record each completed step durably. This small
59
65
  recipe does not promise crash recovery, rollback or exactly-once execution.
60
66
 
67
+ Do not probe a collaborator, default one to `null`, `{}` or a no-op, fill a slot
68
+ with a setter after construction, or keep a module-level registry.
69
+
61
70
  ## Variation points
62
71
 
63
- CLI users can call this same JavaScript recipe with their own runtime. Vibe64
72
+ CLI users can call this same JavaScript recipe with their own runtime. An editor
64
73
  configures the project; it does not execute catalogue writes on configuration
65
74
  save. Laravel applications implement this composition with their own framework
66
- and Paddle client, without importing JavaScript or depending on Vibe64.
75
+ and Paddle client, without importing JavaScript or depending on an editor.
67
76
 
68
77
  ## Example files
69
78
 
@@ -1,7 +1,10 @@
1
1
  import { paddleProvider } from "@jskit-ai/connectors-catalog/server/paddle";
2
2
 
3
- // Application-owned composition: pass an application-mode connection runtime.
4
- // An assistant host must approve the complete batch before calling this recipe.
3
+ /**
4
+ * Application-owned composition: pass an application-mode connection runtime.
5
+ * An assistant host must approve the complete batch before calling this recipe.
6
+ * @param {{ runtime: import("@jskit-ai/connectors-core/server/types").ConnectorsCore, context: object, integrationId: string, items: object[], signal?: AbortSignal }} request
7
+ */
5
8
  async function createProductsWithPrices({ runtime, context, integrationId, items, signal }) {
6
9
  if (!Array.isArray(items) || !items.length || items.length > 100) {
7
10
  throw new TypeError("Supply between 1 and 100 products with prices.");
@@ -29,12 +29,18 @@ existing access policy applies before exposing its configured resources.
29
29
 
30
30
  ## Invariants
31
31
 
32
- - CLI and Vibe64 edit the same reference-only `integrations.json`.
32
+ - The CLI and a hosted editor edit the same reference-only `integrations.json`.
33
33
  - Only a `pk_` key enters an image URL; a private key fails validation.
34
34
  - URL creation is local and does not establish a verified account connection.
35
35
  - Browser load/error events report delivery. A monogram is not proof of a brand logo.
36
36
  - Domain restrictions concern the app that loads the image, including previews
37
37
  and custom domains. The editor VM may have a different origin.
38
+ - Each collaborator the composition receives (`resolveReference`) is a required
39
+ parameter typed in a JSDoc `@param` and called directly, without a `typeof`
40
+ probe, a "requires x" guard or a no-op default. It is typed with
41
+ `ResolveReference` from `@jskit-ai/connectors-core/server/types`. A server
42
+ feature that uses it follows
43
+ [typed features](https://github.com/mobily-enterprises/jskit-ai/blob/main/packages/agent-docs/site/guide/framework/typed-features.md).
38
44
 
39
45
  ## Framework APIs
40
46
 
@@ -82,3 +88,6 @@ These checks need neither live credentials nor generated sample applications.
82
88
  Do not call server `connectApiKey` to manufacture a Connected receipt for this
83
89
  image flow. Do not send a secret key, an environment dump or a caller-selected
84
90
  service origin to the browser. Do not treat key naming as provider quota isolation.
91
+
92
+ Do not probe a collaborator, default one to `null`, `{}` or a no-op, fill a slot
93
+ with a setter after construction, or keep a module-level registry.
@@ -2,7 +2,10 @@ import { parseIntegrationConfiguration } from "@jskit-ai/connectors-core/shared/
2
2
  import { logoDevDefinition } from "@jskit-ai/connectors-catalog/shared";
3
3
  import { createLogoDevImageUrl } from "@jskit-ai/connectors-catalog/client/logo-dev";
4
4
 
5
- // The application authorizes access before passing its configuration and resolver.
5
+ /**
6
+ * The application authorizes access before passing its configuration and resolver.
7
+ * @param {{ configurationText: string, integrationId: string, resolveReference: import("@jskit-ai/connectors-core/server/types").ResolveReference, image: object }} request
8
+ */
6
9
  async function logoUrlFromConfiguration({ configurationText, integrationId, resolveReference, image }) {
7
10
  const configuration = parseIntegrationConfiguration(configurationText, { providers: [logoDevDefinition], allowUnknownProviders: true });
8
11
  const integration = configuration.integrations[integrationId];
@@ -24,7 +24,7 @@ SQL can modify data; read-only behavior requires appropriate database grants.
24
24
 
25
25
  ## Product decisions
26
26
 
27
- Select serverless or provisioned in the same JSON edited by Vibe64. Serverless
27
+ Select serverless or provisioned in the same JSON the editor edits. Serverless
28
28
  requires a workgroup name; provisioned requires a cluster identifier and may use
29
29
  an explicit database user. Both require a database and region. Store AWS secrets
30
30
  outside the file. Temporary credentials additionally need `sessionTokenRef` and
@@ -18,7 +18,7 @@ The example wires that library to application-owned authority and snapshot looku
18
18
  ## Do not use when
19
19
 
20
20
  This is not an HTTP application-data connector, user login, a managed gateway,
21
- or an implementation of Vibe64's coding agents. It does not generate an app.
21
+ or an implementation of a hosted editor's coding agents. It does not generate an app.
22
22
  Auth0 and native Windows execution are not implemented in the current runner.
23
23
 
24
24
  ## Product decisions
@@ -39,7 +39,15 @@ and whether a blocking verdict prevents a later operation.
39
39
  - Missing/invalid reports, cancellation and failed scans never become clean scans.
40
40
  - Provider credentials are isolated from coding-agent sessions and credentials.
41
41
  - The host supplies its own authorized provider credentials; editor edition does
42
- not select a shared Vibe64 provider account.
42
+ not select a shared hosted provider account.
43
+ - Each collaborator the composition receives (`authorize`, `resolveReference`
44
+ and `resolveScanTarget`) is a required parameter typed in a JSDoc `@param` and
45
+ called directly, without a `typeof` probe, a "requires x" guard or a no-op
46
+ default. `authorize` and `resolveReference` are typed with `ConnectorAuthorize`
47
+ and `ResolveReference` from `@jskit-ai/connectors-core/server/types`, and
48
+ `resolveScanTarget` with `WizScanTargetResolver` from
49
+ `@jskit-ai/connectors-catalog/server/types`. A server feature that uses it follows
50
+ [typed features](https://github.com/mobily-enterprises/jskit-ai/blob/main/packages/agent-docs/site/guide/framework/typed-features.md).
43
51
 
44
52
  ## Framework APIs
45
53
 
@@ -81,3 +89,6 @@ and findings require separately authorised provider acceptance.
81
89
  Do not infer workspace grants from a shared slot, return raw provider stderr,
82
90
  replay scans automatically, pass arbitrary CLI flags, use a mutable working tree
83
91
  as if it were a fixed revision, or substitute a generic API reader for source scans.
92
+
93
+ Do not probe a collaborator, default one to `null`, `{}` or a no-op, fill a slot
94
+ with a setter after construction, or keep a module-level registry.
@@ -3,6 +3,15 @@ import { parseIntegrationConfiguration } from "@jskit-ai/connectors-core/shared/
3
3
  import { wizDefinition } from "@jskit-ai/connectors-catalog/shared";
4
4
  import { createWizScanner } from "@jskit-ai/connectors-catalog/server/wiz";
5
5
 
6
+ /**
7
+ * @param {object} dependencies
8
+ * @param {string} [dependencies.configFile]
9
+ * @param {string} dependencies.installedWizCliPath
10
+ * @param {import("@jskit-ai/connectors-core/server/types").ConnectorAuthorize} dependencies.authorize The application's access policy.
11
+ * @param {import("@jskit-ai/connectors-core/server/types").ResolveReference} dependencies.resolveReference
12
+ * @param {import("@jskit-ai/connectors-catalog/server/types").WizScanTargetResolver} dependencies.resolveScanTarget
13
+ * @param {Record<string, string>} [dependencies.environment]
14
+ */
6
15
  export async function projectSourceScanner({ configFile = "integrations.json", installedWizCliPath,
7
16
  authorize, resolveReference, resolveScanTarget, environment }) {
8
17
  const configuration = parseIntegrationConfiguration(await readFile(configFile, "utf8"), {
package/src/server/ai.js CHANGED
@@ -2,9 +2,17 @@ import { ConnectorError, createEnvironmentReferenceResolver } from "@jskit-ai/co
2
2
  import { validateIntegrationConfiguration } from "@jskit-ai/connectors-core/shared/configuration";
3
3
  import { aiCatalogue, aiDefinition, getAiModel } from "../shared/ai.js";
4
4
 
5
- // A server-only credential boundary. The consuming application's SDK owns all
6
- // inference, transport, streaming, tool execution and error/fallback policy.
7
- function createAiConnectionResolver({ configuration, authorize, resolveReference = createEnvironmentReferenceResolver() }) {
5
+ /**
6
+ * A server-only credential boundary. The consuming application's SDK owns all
7
+ * inference, transport, streaming, tool execution and error/fallback policy.
8
+ * @param {object} options
9
+ * @param {Record<string, any>} options.configuration The integration configuration.
10
+ * @param {import("@jskit-ai/connectors-core/server/types").ConnectorAuthorize} options.authorize
11
+ * @param {import("@jskit-ai/connectors-core/server/types").ResolveReference} [options.resolveReference]
12
+ * Reads `env:NAME` references from the environment unless the application resolves its own.
13
+ */
14
+ function createAiConnectionResolver({ configuration, authorize, resolveReference = createEnvironmentReferenceResolver(process.env) }) {
15
+ // readable-check: allow method-probe - input validation: the application passes its access policy
8
16
  if (typeof authorize !== "function") throw new TypeError("AI connections require application authorization.");
9
17
  const config = validateIntegrationConfiguration(configuration, { providers: [aiDefinition], allowUnknownProviders: true });
10
18
 
@@ -37,9 +45,15 @@ function createAiConnectionResolver({ configuration, authorize, resolveReference
37
45
  }
38
46
  // Never expose this result through a browser API or include it in logs.
39
47
  // Missing baseURL means use the named provider SDK's own default endpoint.
40
- return Object.freeze({ providerId: provider.id, model: model.id, sdkPackage: model.sdkPackage,
48
+ return Object.freeze({
49
+ providerId: provider.id,
50
+ model: model.id,
51
+ sdkPackage: model.sdkPackage,
41
52
  modelLimits: Object.freeze({ ...model.limit }),
42
- ...(provider.baseURL ? { baseURL: provider.baseURL } : {}), apiKey, access: model.access });
53
+ ...(provider.baseURL ? { baseURL: provider.baseURL } : {}),
54
+ apiKey,
55
+ access: model.access
56
+ });
43
57
  }
44
58
  return Object.freeze({ resolve });
45
59
  }
@@ -1,4 +1,4 @@
1
- import { ConnectorError } from "@jskit-ai/connectors-core/server";
1
+ import { ConnectorError, providerStatusError } from "@jskit-ai/connectors-core/server";
2
2
  import { apifyDefinition } from "../shared/tokens.js";
3
3
  import { jsonOperation, validatedOperation } from "./jsonOperation.js";
4
4
 
@@ -13,7 +13,7 @@ const apifyProvider = Object.freeze({
13
13
  const response = await fetchImpl(url, { ...options, credentials: "omit", redirect: "error" });
14
14
  if (!response.ok) {
15
15
  await response.body?.cancel();
16
- throw Object.assign(new Error("Apify storage request failed."), { status: response.status });
16
+ throw providerStatusError(response.status);
17
17
  }
18
18
  const reader = response.body?.getReader();
19
19
  const chunks = [];
@@ -1,6 +1,7 @@
1
1
  import { ConnectorError } from "@jskit-ai/connectors-core/server";
2
2
  import { asanaDefinition } from "../shared/tokens.js";
3
3
  import { jsonOperation, validatedOperation } from "./jsonOperation.js";
4
+ import { hasDataList } from "./connectorValues.js";
4
5
 
5
6
  const gid = { type: "string", minLength: 1, maxLength: 100, pattern: "^[0-9]+$" };
6
7
  const requiredGid = { ...gid, required: true };
@@ -17,7 +18,6 @@ const project = { ...common, archived: { type: "boolean" }, owner: { ...gid, nul
17
18
  const taskFields = "gid,name,notes,completed,assignee.gid,assignee.name,due_on,start_on,projects.gid,parent.gid";
18
19
  const projectFields = "gid,name,notes,archived,owner.gid,privacy_setting,due_on,start_on,workspace.gid";
19
20
  const record = result => typeof result?.data?.gid === "string";
20
- const list = result => Array.isArray(result?.data);
21
21
  function read(endpoint, input = {}, fields) {
22
22
  const url = new URL(`https://app.asana.com/api/1.0/${endpoint}`);
23
23
  for (const [key, value] of Object.entries(input)) url.searchParams.set(key, String(value));
@@ -40,21 +40,21 @@ const asanaProvider = Object.freeze({
40
40
  apiKey: { headers: (key) => ({ Authorization: `Bearer ${key}` }) },
41
41
  checkOperation: "workspaces.list",
42
42
  operations: {
43
- "workspaces.list": jsonOperation("https://app.asana.com/api/1.0/workspaces", page, list),
43
+ "workspaces.list": jsonOperation("https://app.asana.com/api/1.0/workspaces", page, hasDataList),
44
44
  "projects.list": validatedOperation({ workspace: requiredGid, ...page, archived: { type: "boolean" } },
45
- input => read("projects", input, projectFields), list),
45
+ input => read("projects", input, projectFields), hasDataList),
46
46
  "projects.get": validatedOperation({ gid: requiredGid }, ({ gid }) => read(`projects/${gid}`, {}, projectFields), record),
47
47
  "projects.create": write("POST", "projects", { ...project, name: { ...common.name, required: true },
48
48
  workspace: requiredGid, privacy_setting: { ...project.privacy_setting, required: true } }, projectFields),
49
49
  "projects.update": write("PUT", "projects", project, projectFields),
50
50
  "tasks.list": validatedOperation({ project: requiredGid, ...page, completed_since: { type: "string", maxLength: 100 } },
51
- ({ project, ...input }) => read(`projects/${project}/tasks`, input, taskFields), list),
51
+ ({ project, ...input }) => read(`projects/${project}/tasks`, input, taskFields), hasDataList),
52
52
  "tasks.get": validatedOperation({ gid: requiredGid }, ({ gid }) => read(`tasks/${gid}`, {}, taskFields), record),
53
53
  "tasks.create": write("POST", "tasks", { ...task, name: { ...common.name, required: true }, workspace: requiredGid,
54
54
  projects: { type: "array", items: gid, validator: values => values.length <= 100 || "Choose at most 100 projects." } }, taskFields),
55
55
  "tasks.update": write("PUT", "tasks", task, taskFields),
56
- "users.list": validatedOperation({ workspace: requiredGid, ...page }, ({ workspace, ...input }) => read(`workspaces/${workspace}/users`, input), list),
57
- "teams.list": validatedOperation({ workspace: requiredGid, ...page }, ({ workspace, ...input }) => read(`workspaces/${workspace}/teams`, input), list),
56
+ "users.list": validatedOperation({ workspace: requiredGid, ...page }, ({ workspace, ...input }) => read(`workspaces/${workspace}/users`, input), hasDataList),
57
+ "teams.list": validatedOperation({ workspace: requiredGid, ...page }, ({ workspace, ...input }) => read(`workspaces/${workspace}/teams`, input), hasDataList),
58
58
  "projectMembers.add": validatedOperation({ project: requiredGid, member: requiredGid,
59
59
  access_level: { type: "string", required: true, enum: ["admin", "editor", "commenter"] } },
60
60
  ({ project, member, access_level }) => ({ method: "POST", url: "https://app.asana.com/api/1.0/memberships",
@@ -1,10 +1,11 @@
1
1
  import { ConnectorError } from "@jskit-ai/connectors-core/server";
2
2
  import { ashbyDefinition } from "../shared/tokens.js";
3
3
  import { jsonOperation } from "./jsonOperation.js";
4
+ import { UUID_SOURCE } from "../shared/uuid.js";
4
5
 
5
6
  const text = { type: "string", minLength: 1, maxLength: 256 };
6
7
  const uuid = { type: "string", required: true,
7
- pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" };
8
+ pattern: `^${UUID_SOURCE}$` };
8
9
  const page = { limit: { type: "integer", min: 1, max: 100, defaultTo: 25 },
9
10
  cursor: { type: "string", minLength: 1, maxLength: 8192 }, syncToken: { type: "string", minLength: 1, maxLength: 8192 } };
10
11
  const candidate = { name: text, email: { ...text, pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" }, phoneNumber: text };
@@ -2,6 +2,8 @@ import { ConnectorError } from "@jskit-ai/connectors-core/server";
2
2
  import { createSchema } from "json-rest-schema";
3
3
  import { attentionDefinition } from "../shared/tokens.js";
4
4
  import { jsonOperation, validatedOperation, withQuery } from "./jsonOperation.js";
5
+ import { hasDataList } from "./connectorValues.js";
6
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
5
7
 
6
8
  const text = { type: "string", minLength: 1, maxLength: 2000 };
7
9
  const id = { type: "string", minLength: 1, maxLength: 100, pattern: "^[A-Za-z0-9_-]+$" };
@@ -10,9 +12,8 @@ const ids = { type: "array", items: id, validator: value => value.length <= 100
10
12
  const bool = { type: "boolean" };
11
13
  const page = { page: { type: "integer", min: 1, defaultTo: 1 }, size: { type: "integer", min: 1, max: 200, defaultTo: 50 } };
12
14
  const dateTime = { ...text, validator: value => /^\d{4}-\d{2}-\d{2}T/.test(value) && Number.isFinite(Date.parse(value)) || "Use an ISO timestamp." };
13
- const list = result => Array.isArray(result?.data);
14
- const record = result => result?.data !== null && typeof result?.data === "object" && !Array.isArray(result.data) && typeof (result.data.uuid ?? result.data.id) === "string";
15
- const conversation = result => result?.type === "conversations" && typeof result.id === "string" && result.attributes !== null && typeof result.attributes === "object" && !Array.isArray(result.attributes);
15
+ const record = result => isRecord(result?.data) && typeof (result.data.uuid ?? result.data.id) === "string";
16
+ const conversation = result => result?.type === "conversations" && typeof result.id === "string" && isRecord(result.attributes);
16
17
  function operation(method, endpoint, fields, validateResult, prepare = value => value) {
17
18
  return validatedOperation(fields, values => {
18
19
  const { id: resource, ...body } = prepare(values);
@@ -49,19 +50,19 @@ const attentionProvider = Object.freeze({
49
50
  "analysis.ask": operation("POST", "ask_attention/v2", { conversations_ids: { ...ids, required: true },
50
51
  deal_id: { type: "string", required: true, maxLength: 100 }, prompt: { ...text, required: true, maxLength: 20000 }
51
52
  }, result => Array.isArray(result) && result.every(item => typeof item?.output === "string" && typeof item.error === "string" && typeof item.conversation_id === "string")),
52
- "scorecards.list": operation("GET", "scorecards", page, list),
53
+ "scorecards.list": operation("GET", "scorecards", page, hasDataList),
53
54
  "scorecards.get": operation("GET", "scorecards/{id}", { id: requiredId }, record),
54
- "scorecardItems.list": operation("GET", "scorecards/{id}/items", { id: requiredId, ...page }, list),
55
+ "scorecardItems.list": operation("GET", "scorecards/{id}/items", { id: requiredId, ...page }, hasDataList),
55
56
  "scorecards.summary": operation("POST", "scorecards/summary", { fromDateTime: { ...dateTime, required: true }, toDateTime: { ...dateTime, required: true },
56
57
  scorecardUUID: requiredId, scorecardsItemsUUIDs: { ...ids, required: true }, teamUUIDs: { ...ids, required: true }, userUUIDs: { ...ids, required: true }
57
- }, list, value => { if (Date.parse(value.fromDateTime) >= Date.parse(value.toDateTime)) throw new ConnectorError("connector_input_invalid", "End time must follow start time.", { statusCode: 422, messageCode: "connectors.attention.endTimeMustFollowStart" }); return value; }),
58
+ }, hasDataList, value => { if (Date.parse(value.fromDateTime) >= Date.parse(value.toDateTime)) throw new ConnectorError("connector_input_invalid", "End time must follow start time.", { statusCode: 422, messageCode: "connectors.attention.endTimeMustFollowStart" }); return value; }),
58
59
  "scorecardResults.create": operation("POST", "createScorecardResult", { scorecard_uuid: requiredId, conversation_uuid: requiredId,
59
60
  summary: { ...text, required: true, maxLength: 20000 }, items: { type: "array", required: true,
60
61
  items: { type: "object", schema: createSchema({ scorecard_item_uuid: requiredId, numeric_result: { type: "integer" }, description: { ...text, required: true } }) },
61
62
  validator: value => value.length > 0 && value.length <= 100 || "Supply 1–100 scoring items." }
62
63
  }, result => result?.success === true),
63
- "users.list": operation("GET", "organizations/users", { teamUUID: id }, list),
64
- "roles.list": operation("GET", "organizations/roles", {}, list),
64
+ "users.list": operation("GET", "organizations/users", { teamUUID: id }, hasDataList),
65
+ "roles.list": operation("GET", "organizations/roles", {}, hasDataList),
65
66
  "users.create": operation("POST", "organizations/users", { email: { ...text, required: true, pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" },
66
67
  first_name: { ...text, required: true }, last_name: { ...text, required: true }, roleUUID: requiredId,
67
68
  seat_type: { type: "string", required: true, enum: ["listener", "recording"] },
@@ -71,8 +72,8 @@ const attentionProvider = Object.freeze({
71
72
  roleUUID: id, seat_type: { type: "string", enum: ["listener", "recording"] }, teamUUIDsToRemove: ids,
72
73
  teamsToAdd: { type: "array", items: team, validator: value => value.length <= 100 || "Use at most 100 teams." }
73
74
  }, record),
74
- "users.delete": operation("DELETE", "organizations/users/{id}", { id: requiredId }, result => result === null || result !== null && typeof result === "object" && !Array.isArray(result) && Object.keys(result).length === 0),
75
- "teams.list": operation("GET", "organizations/teams", {}, list),
75
+ "users.delete": operation("DELETE", "organizations/users/{id}", { id: requiredId }, result => result === null || isRecord(result) && Object.keys(result).length === 0),
76
+ "teams.list": operation("GET", "organizations/teams", {}, hasDataList),
76
77
  "teams.create": operation("POST", "organizations/teams", { name: { ...text, required: true }, parentTeamUUID: id }, record),
77
78
  "teams.update": operation("PATCH", "organizations/teams/{id}", { id: requiredId, name: text, parentTeamUUID: id }, record),
78
79
  "conversations.list": jsonOperation("https://api.attention.tech/v2/conversations/list", {
@@ -82,7 +83,7 @@ const attentionProvider = Object.freeze({
82
83
  "filter[hide_internal]": { type: "boolean" },
83
84
  detailedTranscript: { type: "boolean", defaultTo: false }
84
85
  }, (result) => Array.isArray(result?.data) && (result.meta === undefined ||
85
- result.meta !== null && typeof result.meta === "object" && !Array.isArray(result.meta) &&
86
+ isRecord(result.meta) &&
86
87
  ["totalRecords", "pageCount", "pageNumber", "pageSize"].every((field) => result.meta[field] === undefined ||
87
88
  Number.isInteger(result.meta[field]) && result.meta[field] >= 0)))
88
89
  }
@@ -18,6 +18,12 @@ function awsOperation(service, command, fields, makeInput, validateResult, scope
18
18
  }
19
19
 
20
20
  function awsExchange(service, Client, execute) {
21
+ /**
22
+ * Sends one AWS request, signed with the connection's access key bindings.
23
+ * @param {string} endpoint
24
+ * @param {object} options
25
+ * @param {import("@jskit-ai/connectors-core/server/types").ConnectorExchangeTransport} transport
26
+ */
21
27
  return async (endpoint, options, { settings, apiKey, resolveReference, fetchImpl }) => {
22
28
  let credentials;
23
29
  try {
@@ -3,6 +3,7 @@ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
3
  import { jsonOperation, validatedOperation } from "./jsonOperation.js";
4
4
  import { bigqueryDefinition } from "../shared/google.js";
5
5
  import { googleRead, googleProvider, pageToken } from "./google.js";
6
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
6
7
  const origin = "https://bigquery.googleapis.com";
7
8
  const queryScopes = ["https://www.googleapis.com/auth/bigquery", "https://www.googleapis.com/auth/cloud-platform"];
8
9
  const resultFields = {
@@ -13,9 +14,9 @@ const resultFields = {
13
14
  const resultsSchema = createSchema({ ...resultFields,
14
15
  jobId: { type: "string", required: true, minLength: 1, maxLength: 1024, pattern: "^[a-zA-Z0-9_-]+$" }, pageToken: { ...pageToken, noTrim: true }
15
16
  });
16
- const validQueryResult = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value) && !value.error &&
17
+ const validQueryResult = (value) => Boolean(isRecord(value) && !value.error &&
17
18
  typeof value.jobComplete === "boolean" && (value.rows === undefined || Array.isArray(value.rows) && value.rows.every((row) =>
18
- Array.isArray(row?.f) && row.f.every((cell) => cell && typeof cell === "object" && !Array.isArray(cell) && Object.hasOwn(cell, "v")))) &&
19
+ Array.isArray(row?.f) && row.f.every((cell) => isRecord(cell) && Object.hasOwn(cell, "v")))) &&
19
20
  (value.schema === undefined || Array.isArray(value.schema?.fields)) &&
20
21
  (value.pageToken === undefined || typeof value.pageToken === "string") &&
21
22
  (value.totalRows === undefined || (typeof value.totalRows === "string" && /^\d+$/.test(value.totalRows))) &&
@@ -41,7 +42,7 @@ const metadataPage = { maxResults: resultFields.maxResults, pageToken: { ...page
41
42
  const jobFields = { jobId: resultsSchema.getFieldDefinitions().jobId, location: resultFields.location };
42
43
  const validJob = (job) => Boolean(typeof job?.jobReference?.jobId === "string" && job.jobReference.jobId.length > 0 && typeof job.jobReference.projectId === "string" &&
43
44
  ["PENDING", "RUNNING", "DONE"].includes(job.status?.state));
44
- const validPage = (result, field, reference) => Boolean(result && typeof result === "object" && !Array.isArray(result) && !result.error &&
45
+ const validPage = (result, field, reference) => Boolean(isRecord(result) && !result.error &&
45
46
  (result[field] === undefined || Array.isArray(result[field]) && result[field].every((item) => typeof item?.[reference]?.projectId === "string")) &&
46
47
  (result.nextPageToken === undefined || typeof result.nextPageToken === "string"));
47
48
 
@@ -86,7 +87,7 @@ const bigqueryProvider = googleProvider(bigqueryDefinition, origin, "projects.li
86
87
  "projects.list": googleRead(["bigquery.readonly", "bigquery", "cloud-platform.read-only", "cloud-platform"], {
87
88
  maxResults: { type: "integer", min: 1, max: 50, defaultTo: 50 }, pageToken
88
89
  }, (query) => ({ url: `${origin}/bigquery/v2/projects`, query }),
89
- (result) => Boolean(result && typeof result === "object" && !Array.isArray(result) && !result.error
90
+ (result) => Boolean(isRecord(result) && !result.error
90
91
  && Number.isInteger(result.totalItems) && result.totalItems >= 0
91
92
  && (Array.isArray(result.projects) || (result.totalItems === 0 && result.projects === undefined))))
92
93
  });
@@ -2,6 +2,7 @@ import { ConnectorError } from "@jskit-ai/connectors-core/server";
2
2
  import { createSchema } from "json-rest-schema";
3
3
  import { brevoDefinition } from "../shared/tokens.js";
4
4
  import { jsonOperation, validatedOperation, withQuery } from "./jsonOperation.js";
5
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
5
6
 
6
7
  const text = { type: "string", minLength: 1, maxLength: 1000 };
7
8
  const email = { ...text, maxLength: 320, pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" };
@@ -13,7 +14,7 @@ const mailbox = { type: "object", schema: createSchema({ email: { ...email, requ
13
14
  const ids = { type: "array", items: { type: "integer", min: 1 }, validator: value => value.length <= 100 || "Use at most 100 list IDs." };
14
15
  const emails = { type: "array", items: email, validator: value => value.length > 0 && value.length <= 150 || "Supply 1–150 email addresses." };
15
16
  const domain = { type: "string", minLength: 3, maxLength: 253, pattern: "^(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\\.)+[A-Za-z]{2,}$", required: true };
16
- const empty = result => result === null || result !== null && typeof result === "object" && !Array.isArray(result) && Object.keys(result).length === 0;
17
+ const empty = result => result === null || isRecord(result) && Object.keys(result).length === 0;
17
18
  const record = result => Number.isInteger(result?.id);
18
19
  function operation(method, endpoint, fields, validateResult, prepare = value => value) {
19
20
  return validatedOperation(fields, values => {
@@ -2,6 +2,7 @@ import { createSchema } from "json-rest-schema";
2
2
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
3
  import { clayDefinition } from "../shared/tokens.js";
4
4
  import { jsonOperation } from "./jsonOperation.js";
5
+ import { hasDataList } from "./connectorValues.js";
5
6
 
6
7
  const endpoint = "https://api.clay.com/public/v0";
7
8
  const pageSchema = createSchema({
@@ -57,7 +58,7 @@ const clayProvider = Object.freeze({
57
58
  group_by: array(text, 5), order_by: array(object, 3)
58
59
  }) },
59
60
  cursor: text, limit: { type: "integer", min: 1, max: 100, defaultTo: 50 }
60
- }, result => Array.isArray(result?.data), "POST"),
61
+ }, hasDataList, "POST"),
61
62
  "searches.create": jsonOperation(`${endpoint}/search/query-mode`, {
62
63
  query: { type: "string", required: true, minLength: 1, maxLength: 16000,
63
64
  validator: (value) => value.trim().length > 0 || "Enter a Clay search query." }
@@ -1,6 +1,7 @@
1
1
  import { createSchema } from "json-rest-schema";
2
2
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
3
  import { clickhouseDefinition } from "../shared/clickhouse.js";
4
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
4
5
 
5
6
  const name = { type: "string", required: true, minLength: 1, maxLength: 256,
6
7
  validator: (value) => !/[\p{Cc}]/u.test(value) || "Use a name without control characters." };
@@ -31,7 +32,7 @@ function queryOperation(query, fields, validateData, parameters = values => valu
31
32
  return Boolean(result && !result.exception && !result.error && Array.isArray(result.meta) &&
32
33
  result.meta.every((column) => typeof column?.name === "string" && typeof column.type === "string") &&
33
34
  Array.isArray(result.data) && result.data.length <= 100 && Number.isInteger(result.rows) && result.rows === result.data.length &&
34
- result.data.every((row) => row && typeof row === "object" && !Array.isArray(row)) && (!validateData || validateData(result.data)));
35
+ result.data.every(isRecord) && (!validateData || validateData(result.data)));
35
36
  }
36
37
  };
37
38
  }
@@ -10,7 +10,12 @@ const oauth = Object.freeze({ issuer: origin, authorization_endpoint: `${origin}
10
10
 
11
11
  function provider(definition, path) {
12
12
  return Object.freeze({
13
- ...definition, oauth, oauthResource: `${origin}/mcp`, apiOrigins: [origin], checkOperation: "tools.list", exchange: exchangeMcp,
13
+ ...definition,
14
+ oauth,
15
+ oauthResource: `${origin}/mcp`,
16
+ apiOrigins: [origin],
17
+ checkOperation: "tools.list",
18
+ exchange: exchangeMcp,
14
19
  operations: Object.fromEntries(Object.entries(mcpOperations(() => `${origin}/mcp/${path}`)).map(([name, operation]) => [name, { ...operation, scopes: ["openid"] }]))
15
20
  });
16
21
  }
@@ -0,0 +1,33 @@
1
+ // The value checks several providers share when they validate a provider's response or settings.
2
+
3
+ /**
4
+ * A safe integer that is zero or more, such as a count or a version number.
5
+ * @param {unknown} value
6
+ */
7
+ const isCount = (value) => Number.isSafeInteger(value) && value >= 0;
8
+
9
+ /**
10
+ * A non-empty string.
11
+ * @param {unknown} value
12
+ */
13
+ const isText = (value) => typeof value === "string" && value.length > 0;
14
+
15
+ /**
16
+ * A response record that carries a string `id`.
17
+ * @param {any} value
18
+ */
19
+ const hasStringId = (value) => typeof value?.id === "string";
20
+
21
+ /**
22
+ * A response whose `data` is a list.
23
+ * @param {any} result
24
+ */
25
+ const hasDataList = (result) => Array.isArray(result?.data);
26
+
27
+ /**
28
+ * The origin of the account URL a connection's settings hold.
29
+ * @param {{ accountUrl: string }} settings
30
+ */
31
+ const accountOrigin = (settings) => new URL(settings.accountUrl).origin;
32
+
33
+ export { isCount, isText, hasStringId, hasDataList, accountOrigin };
@@ -1,19 +1,19 @@
1
- import { ConnectorError } from "@jskit-ai/connectors-core/server";
1
+ import { ConnectorError, providerStatusError } from "@jskit-ai/connectors-core/server";
2
2
  import { databricksDefinition } from "../shared/databricks.js";
3
3
  import { jsonOperation } from "./jsonOperation.js";
4
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
4
5
 
5
6
  const origin = (settings) => new URL(settings.workspaceUrl).origin;
6
- const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
7
7
  const identifier = (value) => Number.isSafeInteger(value) && value > 0;
8
8
  const pageToken = { type: "string", minLength: 1, maxLength: 8192 };
9
9
  const pagination = (value) => ["next_page_token", "prev_page_token"].every((key) => value[key] === undefined ||
10
10
  typeof value[key] === "string" && value[key].length > 0 && value[key].length <= 8192);
11
- const job = (value) => object(value) && identifier(value.job_id) &&
12
- (value.settings === undefined || object(value.settings)) && (value.has_more === undefined || typeof value.has_more === "boolean");
11
+ const job = (value) => isRecord(value) && identifier(value.job_id) &&
12
+ (value.settings === undefined || isRecord(value.settings)) && (value.has_more === undefined || typeof value.has_more === "boolean");
13
13
  const jobsList = jsonOperation((settings) => `${origin(settings)}/api/2.2/jobs/list`, {
14
14
  limit: { type: "integer", min: 1, max: 100, defaultTo: 20 },
15
15
  expand_tasks: { type: "boolean", defaultTo: false }, name: { type: "string", minLength: 1, maxLength: 4096 }, page_token: pageToken
16
- }, (value) => object(value) && pagination(value) && (value.jobs === undefined || Array.isArray(value.jobs) && value.jobs.every(job)));
16
+ }, (value) => isRecord(value) && pagination(value) && (value.jobs === undefined || Array.isArray(value.jobs) && value.jobs.every(job)));
17
17
  const jobsGet = jsonOperation((settings) => `${origin(settings)}/api/2.2/jobs/get`, {
18
18
  job_id: { type: "integer", required: true, min: 1, max: Number.MAX_SAFE_INTEGER },
19
19
  include_trigger_state: { type: "boolean", defaultTo: false }, page_token: pageToken
@@ -41,10 +41,10 @@ const databricksProvider = Object.freeze({
41
41
  const url = new URL(address);
42
42
  // An empty jobs list is {}, but JSON null or an unparseable body is not a verified list.
43
43
  const response = await fetchImpl(address, { ...options, credentials: "omit" });
44
- if (!response.ok) throw Object.assign(new Error("Databricks request failed."), { status: response.status });
44
+ if (!response.ok) throw providerStatusError(response.status);
45
45
  let result;
46
46
  try { result = await response.json(); } catch { /* checked below */ }
47
- if (response.status !== 200 || !object(result) || result.error_code || result.error ||
47
+ if (response.status !== 200 || !isRecord(result) || result.error_code || result.error ||
48
48
  (url.pathname.endsWith("/list") && result?.jobs?.length > Number(url.searchParams.get("limit"))) ||
49
49
  (url.pathname.endsWith("/get") && result?.job_id !== Number(url.searchParams.get("job_id")))) {
50
50
  throw new ConnectorError("connector_response_invalid", "Databricks returned an unexpected job response.", { statusCode: 502, messageCode: "connectors.databricks.returnedUnexpectedJobResponse" });
@@ -2,10 +2,10 @@ import { createSchema } from "json-rest-schema";
2
2
  import { ConnectorError } from "@jskit-ai/connectors-core/server";
3
3
  import { dbtSemanticLayerDefinition } from "../shared/dbt-semantic-layer.js";
4
4
  import { graphqlOperation } from "./graphqlOperation.js";
5
+ import { isText } from "./connectorValues.js";
5
6
 
6
7
  const origin = (settings) => `https://${settings.host}`;
7
8
  const endpoint = (settings) => `${origin(settings)}/api/graphql`;
8
- const text = (value) => typeof value === "string" && value.length > 0;
9
9
  const description = (value) => value === null || typeof value === "string";
10
10
  const paging = {
11
11
  pageNum: { type: "integer", min: 1, max: 2_147_483_647, defaultTo: 1 },
@@ -14,7 +14,7 @@ const paging = {
14
14
  };
15
15
  const pageFields = "pageNum pageSize totalItems totalPages items { name description type }";
16
16
  const validPage = (page, typed = true) => Array.isArray(page?.items) && page.items.length <= 100 &&
17
- page.items.every((item) => text(item?.name) && description(item.description) && (!typed || text(item.type))) &&
17
+ page.items.every((item) => isText(item?.name) && description(item.description) && (!typed || isText(item.type))) &&
18
18
  Number.isSafeInteger(page.pageNum) && page.pageNum >= 1 && Number.isSafeInteger(page.pageSize) && page.pageSize >= 1 && page.pageSize <= 100 &&
19
19
  Number.isSafeInteger(page.totalItems) && page.totalItems >= page.items.length && Number.isSafeInteger(page.totalPages) && page.totalPages >= 0;
20
20
 
@@ -43,15 +43,15 @@ const dbtSemanticLayerProvider = Object.freeze({
43
43
  where: { type: "array", validator: (value) => value.length <= 20 || "Use at most 20 filters.",
44
44
  items: { type: "object", schema: createSchema({ sql: { type: "string", required: true, minLength: 1, maxLength: 4000 } }) } },
45
45
  limit: { type: "integer", min: 1, max: 10000, defaultTo: 100 }
46
- }, (data) => text(data?.createQuery?.queryId)),
46
+ }, (data) => isText(data?.createQuery?.queryId)),
47
47
  "queries.get": environmentOperation("query ConnectorDbtQueryResult($environmentId: BigInt!, $queryId: String!, $pageNum: Int!) { query(environmentId: $environmentId, queryId: $queryId, pageNum: $pageNum) { status error totalPages jsonResult(orient: TABLE, encoded: false) } }", {
48
48
  queryId: { type: "string", required: true, minLength: 1, maxLength: 512 },
49
49
  pageNum: { type: "integer", min: 1, max: 2_147_483_647, defaultTo: 1 }
50
- }, (data) => text(data?.query?.status) && description(data.query.error) &&
50
+ }, (data) => isText(data?.query?.status) && description(data.query.error) &&
51
51
  (data.query.totalPages === null || (Number.isSafeInteger(data.query.totalPages) && data.query.totalPages >= 0)) &&
52
52
  (data.query.jsonResult === null || (typeof data.query.jsonResult === "object" && Array.isArray(data.query.jsonResult?.data) && Array.isArray(data.query.jsonResult?.schema?.fields)))),
53
53
  "environment.read": environmentOperation("query ConnectorDbtEnvironment($environmentId: BigInt!) { environmentInfo(environmentId: $environmentId) { dialect } }", {},
54
- (data) => text(data?.environmentInfo?.dialect)),
54
+ (data) => isText(data?.environmentInfo?.dialect)),
55
55
  "metrics.list": environmentOperation(`query ConnectorDbtMetrics($environmentId: BigInt!, $pageNum: Int!, $pageSize: Int!, $search: String) { metricsPaginated(environmentId: $environmentId, pageNum: $pageNum, pageSize: $pageSize, search: $search) { ${pageFields} } }`, paging,
56
56
  (data) => validPage(data?.metricsPaginated)),
57
57
  "dimensions.list": environmentOperation(`query ConnectorDbtDimensions($environmentId: BigInt!, $metrics: [MetricInput!]!, $pageNum: Int!, $pageSize: Int!, $search: String) { dimensionsPaginated(environmentId: $environmentId, metrics: $metrics, pageNum: $pageNum, pageSize: $pageSize, search: $search) { ${pageFields} } }`, {
@@ -1,6 +1,6 @@
1
1
  import { createSchema } from "json-rest-schema";
2
2
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
- import { ConnectorError } from "@jskit-ai/connectors-core/server";
3
+ import { ConnectorError, providerStatusError } from "@jskit-ai/connectors-core/server";
4
4
  import { elevenlabsDefinition } from "../shared/tokens.js";
5
5
  import { jsonOperation } from "./jsonOperation.js";
6
6
 
@@ -36,7 +36,7 @@ const elevenlabsProvider = Object.freeze({
36
36
  if (!new URL(url).pathname.startsWith("/v1/text-to-speech/")) return request(url, options);
37
37
  const response = await fetchImpl(url, { ...options, body: JSON.stringify(options.body),
38
38
  headers: { ...options.headers, "Content-Type": "application/json", Accept: "audio/*" }, credentials: "omit", redirect: "error" });
39
- if (!response.ok) { await response.body?.cancel(); throw Object.assign(new Error("ElevenLabs speech request failed."), { status: response.status }); }
39
+ if (!response.ok) { await response.body?.cancel(); throw providerStatusError(response.status); }
40
40
  const contentType = response.headers.get("content-type")?.split(";")[0] || "application/octet-stream";
41
41
  if (!contentType.startsWith("audio/") && contentType !== "application/octet-stream") {
42
42
  await response.body?.cancel(); throw new ConnectorError("connector_response_invalid", "ElevenLabs did not return audio.", { statusCode: 502, messageCode: "connectors.elevenlabs.didNotReturnAudio" });
@@ -1,7 +1,7 @@
1
1
  import { importPKCS8, SignJWT } from "jose";
2
2
  import { createSchema } from "json-rest-schema";
3
3
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
4
- import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { ConnectorError, providerStatusError } from "@jskit-ai/connectors-core/server";
5
5
  import { firebaseCloudMessagingDefinition, firebaseMessagingScope, firebaseProjectId } from "../shared/firebase-cloud-messaging.js";
6
6
  import { jsonOperation } from "./jsonOperation.js";
7
7
 
@@ -99,7 +99,7 @@ const firebaseCloudMessagingProvider = Object.freeze({
99
99
  if (response.status === 400) {
100
100
  throw new ConnectorError("connector_message_invalid", "Firebase rejected the message or target. Do not delete a registration unless the payload is known to be valid.", { statusCode: 422, messageCode: "connectors.firebaseCloudMessaging.rejectedMessageTargetDoNot" });
101
101
  }
102
- throw Object.assign(new Error("Firebase message request failed."), { status: response.status });
102
+ throw providerStatusError(response.status);
103
103
  }
104
104
  if (!receipt(result) || !result.name.startsWith(`projects/${settings.projectId}/messages/`)) {
105
105
  throw new ConnectorError("connector_response_invalid", "Firebase returned an unexpected message receipt.", { statusCode: 502, messageCode: "connectors.firebaseCloudMessaging.returnedUnexpectedMessageReceipt" });