@arbium/enterprise-agent-connect 0.1.1 → 0.1.2-dev.d2aa5ca

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 (217) hide show
  1. package/README.md +157 -10
  2. package/dist/connectors/dell-techdirect/index.d.ts +8 -0
  3. package/dist/connectors/dell-techdirect/tools.d.ts +2 -0
  4. package/dist/connectors/dell-techdirect/vendor.d.ts +6 -0
  5. package/dist/connectors/dynamics-365/connector.json +213 -0
  6. package/dist/connectors/dynamics-365/index.d.ts +11 -0
  7. package/dist/connectors/dynamics-365/index.js +10 -0
  8. package/dist/connectors/dynamics-365/index.js.map +1 -0
  9. package/dist/connectors/dynamics-365/tools.d.ts +35 -0
  10. package/dist/connectors/dynamics-365/tools.js +806 -0
  11. package/dist/connectors/dynamics-365/tools.js.map +1 -0
  12. package/dist/connectors/dynamics-365/vendor.d.ts +9 -0
  13. package/dist/connectors/dynamics-365/vendor.js +110 -0
  14. package/dist/connectors/dynamics-365/vendor.js.map +1 -0
  15. package/dist/connectors/entra/index.d.ts +2 -0
  16. package/dist/connectors/entra/tools.d.ts +14 -0
  17. package/dist/connectors/fedex/index.d.ts +8 -0
  18. package/dist/connectors/fedex/tools.d.ts +3 -0
  19. package/dist/connectors/fedex/vendor.d.ts +7 -0
  20. package/dist/connectors/gong/connector.json +127 -0
  21. package/dist/connectors/gong/index.d.ts +8 -0
  22. package/dist/connectors/gong/index.js +7 -0
  23. package/dist/connectors/gong/index.js.map +1 -0
  24. package/dist/connectors/gong/tools.d.ts +8 -0
  25. package/dist/connectors/gong/tools.js +424 -0
  26. package/dist/connectors/gong/tools.js.map +1 -0
  27. package/dist/connectors/gong/vendor.d.ts +4 -0
  28. package/dist/connectors/gong/vendor.js +97 -0
  29. package/dist/connectors/gong/vendor.js.map +1 -0
  30. package/dist/connectors/http/index.d.ts +38 -0
  31. package/dist/connectors/http/index.js +151 -64
  32. package/dist/connectors/http/index.js.map +1 -1
  33. package/dist/connectors/intune/index.d.ts +2 -0
  34. package/dist/connectors/intune/tools.d.ts +12 -0
  35. package/dist/connectors/jamf/connector.json +182 -0
  36. package/dist/connectors/jamf/index.d.ts +8 -0
  37. package/dist/connectors/jamf/index.js +7 -0
  38. package/dist/connectors/jamf/index.js.map +1 -0
  39. package/dist/connectors/jamf/tools.d.ts +7 -0
  40. package/dist/connectors/jamf/tools.js +659 -0
  41. package/dist/connectors/jamf/tools.js.map +1 -0
  42. package/dist/connectors/jamf/vendor.d.ts +5 -0
  43. package/dist/connectors/jamf/vendor.js +66 -0
  44. package/dist/connectors/jamf/vendor.js.map +1 -0
  45. package/dist/connectors/jira/connector.json +144 -1
  46. package/dist/connectors/jira/index.d.ts +8 -0
  47. package/dist/connectors/jira/jql.d.ts +4 -0
  48. package/dist/connectors/jira/tools.d.ts +3 -0
  49. package/dist/connectors/jira/vendor.d.ts +3 -0
  50. package/dist/connectors/microsoft/client.d.ts +58 -0
  51. package/dist/connectors/netsuite/connector.json +315 -0
  52. package/dist/connectors/netsuite/index.d.ts +8 -0
  53. package/dist/connectors/netsuite/index.js +7 -0
  54. package/dist/connectors/netsuite/index.js.map +1 -0
  55. package/dist/connectors/netsuite/tools.d.ts +24 -0
  56. package/dist/connectors/netsuite/tools.js +447 -0
  57. package/dist/connectors/netsuite/tools.js.map +1 -0
  58. package/dist/connectors/netsuite/vendor.d.ts +26 -0
  59. package/dist/connectors/netsuite/vendor.js +248 -0
  60. package/dist/connectors/netsuite/vendor.js.map +1 -0
  61. package/dist/connectors/odata/index.d.ts +36 -0
  62. package/dist/connectors/odata/index.js +460 -0
  63. package/dist/connectors/odata/index.js.map +1 -0
  64. package/dist/connectors/okta/connector.json +193 -0
  65. package/dist/connectors/okta/index.d.ts +8 -0
  66. package/dist/connectors/okta/index.js +7 -0
  67. package/dist/connectors/okta/index.js.map +1 -0
  68. package/dist/connectors/okta/tools.d.ts +6 -0
  69. package/dist/connectors/okta/tools.js +429 -0
  70. package/dist/connectors/okta/tools.js.map +1 -0
  71. package/dist/connectors/okta/vendor.d.ts +14 -0
  72. package/dist/connectors/okta/vendor.js +203 -0
  73. package/dist/connectors/okta/vendor.js.map +1 -0
  74. package/dist/connectors/outlook-calendar/index.d.ts +2 -0
  75. package/dist/connectors/outlook-calendar/tools.d.ts +65 -0
  76. package/dist/connectors/outlook-mail/index.d.ts +2 -0
  77. package/dist/connectors/outlook-mail/tools.d.ts +8 -0
  78. package/dist/connectors/salesforce/index.d.ts +8 -0
  79. package/dist/connectors/salesforce/soql.d.ts +3 -0
  80. package/dist/connectors/salesforce/tools.d.ts +9 -0
  81. package/dist/connectors/salesforce/vendor.d.ts +3 -0
  82. package/dist/connectors/sap-s4hana/connector.json +263 -0
  83. package/dist/connectors/sap-s4hana/index.d.ts +8 -0
  84. package/dist/connectors/sap-s4hana/index.js +7 -0
  85. package/dist/connectors/sap-s4hana/index.js.map +1 -0
  86. package/dist/connectors/sap-s4hana/tools.d.ts +4 -0
  87. package/dist/connectors/sap-s4hana/tools.js +761 -0
  88. package/dist/connectors/sap-s4hana/tools.js.map +1 -0
  89. package/dist/connectors/sap-s4hana/vendor.d.ts +5 -0
  90. package/dist/connectors/sap-s4hana/vendor.js +176 -0
  91. package/dist/connectors/sap-s4hana/vendor.js.map +1 -0
  92. package/dist/connectors/servicenow/connector.json +228 -0
  93. package/dist/connectors/servicenow/index.d.ts +8 -0
  94. package/dist/connectors/servicenow/index.js +7 -0
  95. package/dist/connectors/servicenow/index.js.map +1 -0
  96. package/dist/connectors/servicenow/tools.d.ts +4 -0
  97. package/dist/connectors/servicenow/tools.js +884 -0
  98. package/dist/connectors/servicenow/tools.js.map +1 -0
  99. package/dist/connectors/servicenow/vendor.d.ts +4 -0
  100. package/dist/connectors/servicenow/vendor.js +109 -0
  101. package/dist/connectors/servicenow/vendor.js.map +1 -0
  102. package/dist/connectors/sharepoint-files/excel.d.ts +16 -0
  103. package/dist/connectors/sharepoint-files/index.d.ts +11 -0
  104. package/dist/connectors/sharepoint-files/tools.d.ts +52 -0
  105. package/dist/connectors/sharepoint-files/vendor.d.ts +11 -0
  106. package/dist/connectors/slack/connector.json +148 -0
  107. package/dist/connectors/slack/index.d.ts +8 -0
  108. package/dist/connectors/slack/index.js +7 -0
  109. package/dist/connectors/slack/index.js.map +1 -0
  110. package/dist/connectors/slack/tools.d.ts +3 -0
  111. package/dist/connectors/slack/tools.js +355 -0
  112. package/dist/connectors/slack/tools.js.map +1 -0
  113. package/dist/connectors/slack/vendor.d.ts +17 -0
  114. package/dist/connectors/slack/vendor.js +195 -0
  115. package/dist/connectors/slack/vendor.js.map +1 -0
  116. package/dist/connectors/soap/index.d.ts +19 -0
  117. package/dist/connectors/soap/index.js +248 -0
  118. package/dist/connectors/soap/index.js.map +1 -0
  119. package/dist/connectors/soap/xml.d.ts +12 -0
  120. package/dist/connectors/soap/xml.js +126 -0
  121. package/dist/connectors/soap/xml.js.map +1 -0
  122. package/dist/connectors/sql/engines.d.ts +58 -0
  123. package/dist/connectors/sql/engines.js +292 -0
  124. package/dist/connectors/sql/engines.js.map +1 -0
  125. package/dist/connectors/sql/index.d.ts +19 -0
  126. package/dist/connectors/sql/index.js +212 -0
  127. package/dist/connectors/sql/index.js.map +1 -0
  128. package/dist/connectors/teams/index.d.ts +2 -0
  129. package/dist/connectors/teams/tools.d.ts +2 -0
  130. package/dist/connectors/workday/connector.json +186 -0
  131. package/dist/connectors/workday/index.d.ts +8 -0
  132. package/dist/connectors/workday/index.js +7 -0
  133. package/dist/connectors/workday/index.js.map +1 -0
  134. package/dist/connectors/workday/tools.d.ts +33 -0
  135. package/dist/connectors/workday/tools.js +410 -0
  136. package/dist/connectors/workday/tools.js.map +1 -0
  137. package/dist/connectors/workday/vendor.d.ts +18 -0
  138. package/dist/connectors/workday/vendor.js +195 -0
  139. package/dist/connectors/workday/vendor.js.map +1 -0
  140. package/dist/packages/connector-kit/src/branding.d.ts +57 -0
  141. package/dist/packages/connector-kit/src/certificate.d.ts +19 -0
  142. package/dist/packages/connector-kit/src/common.d.ts +40 -0
  143. package/dist/packages/connector-kit/src/connector.d.ts +1346 -0
  144. package/dist/packages/connector-kit/src/connector.js +586 -44
  145. package/dist/packages/connector-kit/src/connector.js.map +1 -1
  146. package/dist/packages/connector-kit/src/index.d.ts +5 -0
  147. package/dist/packages/connector-kit/src/index.js +5 -0
  148. package/dist/packages/connector-kit/src/index.js.map +1 -0
  149. package/dist/packages/connector-kit/src/oauth.d.ts +15 -0
  150. package/dist/packages/connector-kit/src/oauth.js +21 -4
  151. package/dist/packages/connector-kit/src/oauth.js.map +1 -1
  152. package/dist/packages/connector-kit/src/types.d.ts +85 -0
  153. package/dist/packages/connector-kit/src/types.js.map +1 -1
  154. package/dist/packages/server/migrations/0007_app_events.sql +110 -0
  155. package/dist/packages/server/migrations/rollback/to-legacy.sql +3 -0
  156. package/dist/packages/server/src/api.d.ts +3714 -0
  157. package/dist/packages/server/src/api.js +135 -1
  158. package/dist/packages/server/src/api.js.map +1 -1
  159. package/dist/packages/server/src/app.d.ts +23 -0
  160. package/dist/packages/server/src/app.js +24 -1
  161. package/dist/packages/server/src/app.js.map +1 -1
  162. package/dist/packages/server/src/builtin.d.ts +4 -0
  163. package/dist/packages/server/src/builtin.js +18 -0
  164. package/dist/packages/server/src/builtin.js.map +1 -1
  165. package/dist/packages/server/src/consent.d.ts +27 -0
  166. package/dist/packages/server/src/events/admin.d.ts +156 -0
  167. package/dist/packages/server/src/events/admin.js +255 -0
  168. package/dist/packages/server/src/events/admin.js.map +1 -0
  169. package/dist/packages/server/src/events/deliver.d.ts +43 -0
  170. package/dist/packages/server/src/events/deliver.js +129 -0
  171. package/dist/packages/server/src/events/deliver.js.map +1 -0
  172. package/dist/packages/server/src/events/runtime.d.ts +28 -0
  173. package/dist/packages/server/src/events/runtime.js +397 -0
  174. package/dist/packages/server/src/events/runtime.js.map +1 -0
  175. package/dist/packages/server/src/events/store.d.ts +172 -0
  176. package/dist/packages/server/src/events/store.js +275 -0
  177. package/dist/packages/server/src/events/store.js.map +1 -0
  178. package/dist/packages/server/src/index.d.ts +40 -0
  179. package/dist/packages/server/src/index.js +67 -11
  180. package/dist/packages/server/src/index.js.map +1 -1
  181. package/dist/packages/server/src/migrate.d.ts +11 -0
  182. package/dist/packages/server/src/openapi.d.ts +4 -0
  183. package/dist/packages/server/src/openapi.js +2 -2
  184. package/dist/packages/server/src/openapi.js.map +1 -1
  185. package/dist/packages/server/src/queue.d.ts +10 -0
  186. package/dist/packages/server/src/queue.js +45 -0
  187. package/dist/packages/server/src/queue.js.map +1 -0
  188. package/dist/packages/server/src/secrets.d.ts +19 -0
  189. package/dist/packages/server/src/server.d.ts +1 -0
  190. package/dist/packages/server/src/server.js +14 -1
  191. package/dist/packages/server/src/server.js.map +1 -1
  192. package/dist/packages/server/src/service.d.ts +760 -0
  193. package/dist/packages/server/src/service.js +10 -0
  194. package/dist/packages/server/src/service.js.map +1 -1
  195. package/dist/packages/server/src/store.d.ts +158 -0
  196. package/dist/packages/server/src/store.js +4 -0
  197. package/dist/packages/server/src/store.js.map +1 -1
  198. package/dist/packages/server/src/teams/activity.d.ts +123 -0
  199. package/dist/packages/server/src/teams/activity.js +6 -5
  200. package/dist/packages/server/src/teams/activity.js.map +1 -1
  201. package/dist/packages/server/src/teams/admin.d.ts +335 -0
  202. package/dist/packages/server/src/teams/admin.js +1 -1
  203. package/dist/packages/server/src/teams/admin.js.map +1 -1
  204. package/dist/packages/server/src/teams/approval.d.ts +57 -0
  205. package/dist/packages/server/src/teams/bot.d.ts +25 -0
  206. package/dist/packages/server/src/teams/claude.d.ts +46 -0
  207. package/dist/packages/server/src/teams/jwt.d.ts +17 -0
  208. package/dist/packages/server/src/teams/package.d.ts +79 -0
  209. package/dist/packages/server/src/teams/provision.d.ts +84 -0
  210. package/dist/packages/server/src/teams/runner.d.ts +80 -0
  211. package/dist/packages/server/src/teams/runner.js +50 -6
  212. package/dist/packages/server/src/teams/runner.js.map +1 -1
  213. package/dist/packages/server/src/teams/runtime.d.ts +91 -0
  214. package/dist/packages/server/src/teams/runtime.js +25 -47
  215. package/dist/packages/server/src/teams/runtime.js.map +1 -1
  216. package/dist/packages/server/src/teams/store.d.ts +233 -0
  217. package/package.json +28 -2
@@ -72,7 +72,8 @@ const clickRule = [
72
72
  (s) => !s.clickUnavailable || (!s.click && Boolean(s.copy)),
73
73
  { message: "A step with clickUnavailable has commands (copy) and no click" },
74
74
  ];
75
- const step = stepSchema.refine(...clickRule);
75
+ export const setupStep = stepSchema.refine(...clickRule);
76
+ const step = setupStep;
76
77
  const agentStep = agentStepSchema.refine(...clickRule);
77
78
  // A field with `options` is a choice (the first is the default), stored in the app's config
78
79
  // under its id. An option's `scopes` are vendor permissions the app then needs too, e.g.
@@ -88,7 +89,8 @@ const fieldSchema = z
88
89
  placeholder: text.optional(),
89
90
  secret: z.boolean(),
90
91
  options: z.array(optionSchema).min(2).optional(),
91
- // Asked only while an earlier choice is this option; otherwise its first option holds.
92
+ // Asked only while an earlier choice is this option; otherwise a choice's first option
93
+ // holds and a text field is not asked (e.g. a password only for that sign-in method).
92
94
  requires: chosenSchema.optional(),
93
95
  })
94
96
  .strict();
@@ -133,32 +135,122 @@ const vendorProbeSchema = z
133
135
  const probeSchema = z.union([httpProbeSchema, vendorProbeSchema]);
134
136
  // An app described entirely here, e.g. a company's internal REST API: the generic http module
135
137
  // (connectors/http) runs it, so it needs no code. Auth values are the ids of connect form fields.
136
- const httpAuthSchema = z.discriminatedUnion("type", [
138
+ const headerName = z.string().regex(/^[A-Za-z0-9-]+$/);
139
+ // Printable ASCII only: a header value can never carry a line break into the request.
140
+ const headerValue = z.string().regex(/^[\x20-\x7E]*$/);
141
+ const keyAuths = [
137
142
  z
138
- .object({ type: z.literal("header"), header: z.string().regex(/^[A-Za-z0-9-]+$/), value: slug })
143
+ .object({
144
+ type: z.literal("header"),
145
+ header: headerName,
146
+ value: slug,
147
+ // Written before the value, e.g. "SSWS " (Okta) or "Token token=" (PagerDuty).
148
+ prefix: headerValue.optional(),
149
+ })
139
150
  .strict(),
140
151
  z.object({ type: z.literal("bearer"), token: slug }).strict(),
141
152
  z.object({ type: z.literal("basic"), username: slug, password: slug }).strict(),
153
+ ];
154
+ const httpAuthSchema = z.discriminatedUnion("type", [
155
+ ...keyAuths,
142
156
  z
143
157
  .object({
144
158
  type: z.literal("oauth2"),
145
159
  tokenUrl: z.url({ protocol: /^https?$/ }),
146
160
  clientId: slug,
147
161
  clientSecret: slug,
162
+ // Sent with the token request, e.g. https://<org>.crm.dynamics.com/.default.
163
+ scope: z
164
+ .string()
165
+ .regex(/^[\x21-\x7e]+( [\x21-\x7e]+)*$/)
166
+ .optional(),
167
+ })
168
+ .strict(),
169
+ // Added to every call's query string, e.g. ?api_key=…
170
+ z
171
+ .object({
172
+ type: z.literal("query"),
173
+ param: z.string().regex(/^[A-Za-z0-9_.-]+$/),
174
+ value: slug,
148
175
  })
149
176
  .strict(),
150
177
  ]);
178
+ // The connect form fields an auth names, each with whether it must be a secret field.
179
+ const authFields = (a) => a.type === "header" || a.type === "query"
180
+ ? [[a.value, true]]
181
+ : a.type === "bearer"
182
+ ? [[a.token, true]]
183
+ : a.type === "basic"
184
+ ? [
185
+ [a.username, false],
186
+ [a.password, true],
187
+ ]
188
+ : [
189
+ [a.clientId, false],
190
+ [a.clientSecret, true],
191
+ ];
192
+ const scalarType = z.enum(["string", "number", "integer", "boolean"]);
193
+ const scalar = z.union([z.string(), z.number(), z.boolean()]);
194
+ const fitsType = (type, v) => type === "integer"
195
+ ? Number.isInteger(v)
196
+ : type === "number" || type === "string" || type === "boolean"
197
+ ? typeof v === type
198
+ : false;
151
199
  const httpParamSchema = z
152
200
  .object({
153
- type: z.enum(["string", "number", "integer", "boolean"]),
201
+ type: z.enum(["string", "number", "integer", "boolean", "array"]),
154
202
  description: text,
155
203
  // Where it goes; a {name} in the path template is always "path".
156
204
  in: z.enum(["path", "query", "body"]),
205
+ // The name the app knows it by, when it is not this param's own (e.g. pageToken).
206
+ name: z
207
+ .string()
208
+ .regex(/^[A-Za-z0-9_.\-[\]]+$/)
209
+ .optional(),
157
210
  required: z.boolean().default(true),
211
+ // Sent when the agent leaves the param out; such a param is never required.
212
+ default: z.union([scalar, z.array(scalar)]).optional(),
213
+ // The only values the agent may send (each item's, for an array).
214
+ enum: z.array(scalar).min(1).optional(),
215
+ // An array's item type.
216
+ items: scalarType.optional(),
217
+ // An array in the query: one `name=` per item, unless joined with this, e.g. ",".
218
+ join: z.string().min(1).max(3).optional(),
158
219
  // The value must be one of the agent's values of this list limit, while it has any.
159
220
  limit: slug.optional(),
160
221
  })
161
- .strict();
222
+ .strict()
223
+ .refine((p) => (p.type === "array") === Boolean(p.items), {
224
+ message: "An array param names its item type (items), and only an array param",
225
+ })
226
+ .refine((p) => !p.join || (p.type === "array" && p.in === "query"), {
227
+ message: "join is for array params in the query",
228
+ })
229
+ .refine((p) => {
230
+ const itemType = p.items ?? p.type;
231
+ const fits = (v) => fitsType(itemType, v) && (!p.enum || p.enum.some((e) => e === v));
232
+ return ((p.enum ?? []).every((v) => fitsType(itemType, v)) &&
233
+ (p.default === undefined ||
234
+ (p.type === "array"
235
+ ? Array.isArray(p.default) && p.default.every(fits)
236
+ : fits(p.default))));
237
+ }, { message: "A param's enum and default match its type, and the default is one of its enum" });
238
+ // Picks what the agent sees of a successful answer. `items`: a dot path to the list in the
239
+ // answer, e.g. "data.results". `fields`: dot paths kept of each item (of the answer itself,
240
+ // without `items`); a path through a list keeps that field of each of its items.
241
+ const dotPath = z.string().regex(/^[A-Za-z0-9_$@-]+(\.[A-Za-z0-9_$@-]+)*$/);
242
+ const httpResultSchema = z
243
+ .object({ items: dotPath.optional(), fields: z.array(dotPath).min(1).optional() })
244
+ .strict()
245
+ .refine((r) => r.items || r.fields, { message: "A result names items, fields or both" });
246
+ // One page per call: `param` takes the cursor in, and the answer gives the next one from a
247
+ // dot path in its body, a header, or the rel="next" address of its Link header (the value of
248
+ // `param` there). The agent gets {items, next}.
249
+ const httpPagingSchema = z.union([
250
+ z.object({ param: slug, body: dotPath }).strict(),
251
+ z.object({ param: slug, header: headerName }).strict(),
252
+ z.object({ param: slug, link: z.literal(true) }).strict(),
253
+ ]);
162
254
  const httpToolSchema = z
163
255
  .object({
164
256
  name: slug,
@@ -172,38 +264,426 @@ const httpToolSchema = z
172
264
  // Checked in the answer, for calls whose params cannot be limited (e.g. an order by id):
173
265
  // while the agent has values of `limit`, the answer's `field` (a dot path; every item of a
174
266
  // list) must be one of them, else nothing is returned. A missing field is refused.
175
- answerLimits: z
176
- .array(z
177
- .object({ field: z.string().regex(/^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/), limit: slug })
178
- .strict())
179
- .default([]),
267
+ answerLimits: z.array(z.object({ field: dotPath, limit: slug }).strict()).default([]),
268
+ result: httpResultSchema.optional(),
269
+ paging: httpPagingSchema.optional(),
180
270
  })
181
271
  .strict()
182
272
  .refine((t) => {
183
273
  const inPath = [...t.path.matchAll(/\{([a-z][a-z0-9_]*)\}/g)].map((m) => m[1]);
184
274
  const declared = Object.entries(t.params).filter(([, p]) => p.in === "path");
185
- return (inPath.every((n) => t.params[n]?.in === "path" && t.params[n].required) &&
186
- declared.every(([n]) => inPath.includes(n)));
187
- }, { message: "Each {param} in a tool's path is a required path param, and only those" })
275
+ return (inPath.every((n) => t.params[n]?.in === "path" &&
276
+ t.params[n].required &&
277
+ t.params[n].default === undefined &&
278
+ t.params[n].type !== "array") && declared.every(([n]) => inPath.includes(n)));
279
+ }, {
280
+ message: "Each {param} in a tool's path is a required path param with one value and no default, and only those",
281
+ })
188
282
  .refine((t) => !Object.values(t.params).some((p) => p.in === "body") || t.method !== "GET", {
189
283
  message: "A GET tool has no body params",
190
284
  })
191
285
  // Checked after the call, so only for reads: a write would already have happened.
192
286
  .refine((t) => !t.answerLimits.length || t.method === "GET", {
193
287
  message: "answerLimits are for GET tools only",
194
- });
288
+ })
289
+ .refine((t) => {
290
+ const p = t.paging && t.params[t.paging.param];
291
+ return !t.paging || (p && p.in !== "path" && p.type !== "array" && p.type !== "boolean");
292
+ }, { message: "paging.param is one of the tool's query or body params, a string or number" })
293
+ .refine((t) => {
294
+ const wire = Object.entries(t.params).map(([k, p]) => `${p.in}:${p.name ?? k}`);
295
+ return new Set(wire).size === wire.length;
296
+ }, { message: "Two params of a tool are sent under the same name" });
297
+ // Headers no `headers` entry may set: credentials go through `auth` (secret fields), and the
298
+ // rest the service sets itself.
299
+ const RESERVED_HEADERS = new Set([
300
+ "authorization",
301
+ "proxy-authorization",
302
+ "cookie",
303
+ "host",
304
+ "content-type",
305
+ "content-length",
306
+ "transfer-encoding",
307
+ "connection",
308
+ ]);
309
+ const CREDENTIAL_LIKE = /auth|token|secret|key|password|session|cookie/i;
195
310
  const httpSchema = z
196
311
  .object({
197
312
  // Every call goes to this origin, under this path, and nowhere else: redirects are refused.
198
313
  baseUrl: z.url({ protocol: /^https?$/ }).refine((u) => !/[?#]/.test(u), "No query or fragment"),
199
314
  auth: httpAuthSchema,
315
+ // Sent with every call, e.g. an API version or Accept. Not secret: never credentials.
316
+ headers: z.record(headerName, headerValue).default({}),
200
317
  tools: z.array(httpToolSchema).min(1),
201
318
  // Plain http for baseUrl and the token URL, e.g. an app inside the cluster. Off by default.
202
319
  allowInsecureHttp: z.boolean().default(false),
203
320
  })
204
321
  .strict()
205
322
  .refine((h) => h.allowInsecureHttp ||
206
- [h.baseUrl, ...(h.auth.type === "oauth2" ? [h.auth.tokenUrl] : [])].every((u) => u.startsWith("https://")), { message: "baseUrl and tokenUrl must be https unless allowInsecureHttp is true" });
323
+ [h.baseUrl, ...(h.auth.type === "oauth2" ? [h.auth.tokenUrl] : [])].every((u) => u.startsWith("https://")), { message: "baseUrl and tokenUrl must be https unless allowInsecureHttp is true" })
324
+ .refine((h) => Object.keys(h.headers).every((name) => !RESERVED_HEADERS.has(name.toLowerCase()) &&
325
+ !CREDENTIAL_LIKE.test(name) &&
326
+ !(h.auth.type === "header" && h.auth.header.toLowerCase() === name.toLowerCase())), {
327
+ message: "headers are not secret, so they cannot carry credentials (use auth) or headers the service sets",
328
+ })
329
+ .refine((h) => h.auth.type !== "query" ||
330
+ h.tools.every((t) => Object.entries(t.params).every(([k, p]) => p.in !== "query" || (p.name ?? k) !== h.auth.param)), { message: "No tool param may be sent as the query auth param" });
331
+ const answerLimitsSchema = z.array(z.object({ field: dotPath, limit: slug }).strict()).default([]);
332
+ const secure = (allowInsecure, ...urls) => allowInsecure || urls.every((u) => u.startsWith("https://"));
333
+ // A SOAP service described here, run by connectors/soap. Each tool is one operation: its Body
334
+ // content is a template whose {param} placeholders are filled with XML-escaped values.
335
+ const soapParamSchema = z
336
+ .object({
337
+ type: z.enum(["string", "number", "integer", "boolean"]),
338
+ description: text,
339
+ // Left out, an optional param's element (`<x>{param}</x>`) is left out of the body.
340
+ required: z.boolean().default(true),
341
+ limit: slug.optional(),
342
+ })
343
+ .strict();
344
+ const PLACEHOLDER = /\{([a-z][a-z0-9_]*)\}/g;
345
+ const xmlName = z.string().regex(/^[A-Za-z_][\w.-]*$/);
346
+ const soapToolSchema = z
347
+ .object({
348
+ name: slug,
349
+ description: text,
350
+ operation: xmlName,
351
+ // Sent as the SOAPAction header (1.1) or the content type's action (1.2).
352
+ soapAction: z
353
+ .string()
354
+ .regex(/^[^"\r\n]*$/)
355
+ .optional(),
356
+ body: text,
357
+ params: z.record(slug, soapParamSchema).default({}),
358
+ // Dot path in the Body's content to the answer, default `<operation>Response`; ending `[]`
359
+ // makes it a list even when the service answers one item.
360
+ result: z
361
+ .string()
362
+ .regex(/^[A-Za-z_][\w.-]*(\.[A-Za-z_][\w.-]*)*(\[\])?$/)
363
+ .optional(),
364
+ // Only these fields of the answer (of each item, for a list) reach the agent.
365
+ fields: z.array(xmlName).min(1).optional(),
366
+ // Changes nothing (a lookup): only these can have answerLimits.
367
+ readOnly: z.boolean().default(false),
368
+ destructive: z.boolean().default(false),
369
+ answerLimits: answerLimitsSchema,
370
+ })
371
+ .strict()
372
+ .refine((t) => {
373
+ const used = [...t.body.matchAll(PLACEHOLDER)].map((m) => m[1]);
374
+ return (new Set(used).size === used.length &&
375
+ used.every((n) => n in t.params) &&
376
+ Object.keys(t.params).every((n) => used.includes(n)));
377
+ }, { message: "Each param is a {param} placeholder in the tool's body, once, and only those" })
378
+ .refine((t) => Object.entries(t.params).every(([n, p]) => p.required || new RegExp(`<([\\w.:-]+)(\\s[^>]*)?>\\{${n}\\}</\\1>`).test(t.body)), { message: "An optional param's placeholder is the whole content of an element" })
379
+ .refine((t) => !t.answerLimits.length || t.readOnly, {
380
+ message: "answerLimits are for readOnly tools only",
381
+ })
382
+ .refine((t) => !(t.readOnly && t.destructive), {
383
+ message: "A readOnly tool cannot be destructive",
384
+ });
385
+ const soapSchema = z
386
+ .object({
387
+ endpoint: z.url({ protocol: /^https?$/ }).refine((u) => !u.includes("#"), "No fragment"),
388
+ version: z.enum(["1.1", "1.2"]).default("1.1"),
389
+ // Prefixes declared on the Envelope for the tools' bodies, e.g. {"hr": "urn:example:hr"}.
390
+ namespaces: z
391
+ .record(xmlName.refine((p) => !["soap", "wsse", "wsu", "xml", "xmlns"].includes(p), "Reserved"), z.string().regex(/^[^"<>&\s]+$/))
392
+ .default({}),
393
+ auth: z.discriminatedUnion("type", [...keyAuths]).optional(),
394
+ // WS-Security UsernameToken in the SOAP header, with or without other auth.
395
+ wsSecurity: z
396
+ .object({ username: slug, password: slug, digest: z.boolean().default(false) })
397
+ .strict()
398
+ .optional(),
399
+ // The call the connector's `operation` check makes: a tool, its arguments, what to show.
400
+ verify: z
401
+ .object({
402
+ tool: slug,
403
+ args: z.record(slug, z.union([z.string(), z.number(), z.boolean()])).default({}),
404
+ show: text,
405
+ })
406
+ .strict()
407
+ .optional(),
408
+ tools: z.array(soapToolSchema).min(1),
409
+ allowInsecureHttp: z.boolean().default(false),
410
+ })
411
+ .strict()
412
+ .refine((s) => s.auth || s.wsSecurity, { message: "A soap app needs auth or wsSecurity" })
413
+ .refine((s) => secure(s.allowInsecureHttp, s.endpoint), {
414
+ message: "endpoint must be https unless allowInsecureHttp is true",
415
+ })
416
+ .refine((s) => !s.verify || s.tools.some((t) => t.name === s.verify.tool), {
417
+ message: "verify.tool names one of the soap tools",
418
+ });
419
+ // An OData service (v2 or v4: SAP Gateway, Dynamics, SuccessFactors) described here, run by
420
+ // connectors/odata. Agents never write OData syntax: values are typed and made into literals.
421
+ const odataName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_.]*$/);
422
+ const odataValueSchema = z
423
+ .object({
424
+ type: z.enum([
425
+ "string",
426
+ "integer",
427
+ "number",
428
+ "decimal",
429
+ "boolean",
430
+ "guid",
431
+ "datetime",
432
+ "datetimeoffset",
433
+ ]),
434
+ description: text,
435
+ // The OData property (or function parameter) name, default the param's own. A field can
436
+ // bind a lookup, e.g. Dynamics' parentaccountid@odata.bind.
437
+ property: z
438
+ .string()
439
+ .regex(/^[A-Za-z_][A-Za-z0-9_.]*(@odata\.bind)?$/)
440
+ .optional(),
441
+ // Keys are always required; fields default to required on create, never on update.
442
+ required: z.boolean().optional(),
443
+ limit: slug.optional(),
444
+ })
445
+ .strict();
446
+ const odataFilterSchema = odataValueSchema
447
+ .extend({
448
+ op: z.enum(["eq", "ne", "gt", "ge", "lt", "le", "contains", "startswith"]).default("eq"),
449
+ })
450
+ .strict()
451
+ .refine((f) => !f.limit || f.op === "eq", { message: "Only an eq filter can have a limit" })
452
+ .refine((f) => !["contains", "startswith"].includes(f.op) || f.type === "string", {
453
+ message: "contains and startswith filter strings",
454
+ });
455
+ const ODATA_KINDS = {
456
+ list: { on: "entitySet", parts: ["filters"] },
457
+ get: { on: "entitySet", parts: ["key"] },
458
+ create: { on: "entitySet", parts: ["fields"] },
459
+ update: { on: "entitySet", parts: ["key", "fields"] },
460
+ function: { on: "function", parts: ["params"] },
461
+ action: { on: "function", parts: ["params"] },
462
+ };
463
+ const odataToolSchema = z
464
+ .object({
465
+ name: slug,
466
+ description: text,
467
+ kind: z.enum(["list", "get", "create", "update", "function", "action"]),
468
+ entitySet: odataName.optional(),
469
+ // A function or action import's name.
470
+ function: odataName.optional(),
471
+ key: z.record(slug, odataValueSchema).default({}),
472
+ filters: z.record(slug, odataFilterSchema).default({}),
473
+ fields: z.record(slug, odataValueSchema).default({}),
474
+ params: z.record(slug, odataValueSchema).default({}),
475
+ // $select, and the only properties passed back.
476
+ select: z.array(odataName).default([]),
477
+ orderBy: z
478
+ .string()
479
+ .regex(/^[A-Za-z_][\w./]*( (asc|desc))?(,[A-Za-z_][\w./]*( (asc|desc))?)*$/)
480
+ .optional(),
481
+ // Most items per page: the agent's max_results is capped at it.
482
+ top: z.number().int().min(1).max(1000).default(50),
483
+ destructive: z.boolean().default(false),
484
+ answerLimits: answerLimitsSchema,
485
+ })
486
+ .strict()
487
+ .refine((t) => {
488
+ const kind = ODATA_KINDS[t.kind];
489
+ const parts = ["key", "filters", "fields", "params"];
490
+ return (Boolean(t[kind.on]) &&
491
+ !t[kind.on === "entitySet" ? "function" : "entitySet"] &&
492
+ parts.every((p) => kind.parts.includes(p) || !Object.keys(t[p]).length) &&
493
+ (!kind.parts.includes("key") || Object.keys(t.key).length > 0) &&
494
+ (t.kind !== "create" || Object.keys(t.fields).length > 0) &&
495
+ (t.kind === "list" || !t.orderBy) &&
496
+ (kind.on !== "function" || !t.select.length));
497
+ }, {
498
+ message: "A list names an entitySet and filters, get a key, create fields, update a key and fields, function and action a function and params; orderBy is for lists, select not for functions",
499
+ })
500
+ .refine((t) => !["create", "action"].includes(t.kind) || !t.answerLimits.length, {
501
+ message: "answerLimits are for reads and updates (checked before the change)",
502
+ })
503
+ .refine((t) => {
504
+ const names = [t.key, t.filters, t.fields, t.params].flatMap(Object.keys);
505
+ return (new Set(names).size === names.length &&
506
+ !names.some((n) => n === "cursor" || n === "max_results"));
507
+ }, { message: "Param names are unique in a tool, and cursor and max_results are taken" })
508
+ .refine((t) => !t.select.length ||
509
+ Object.entries(t.filters).every(([n, f]) => !f.limit || t.select.includes(f.property ?? n)), { message: "A limited filter's property must be selected, so answers can be checked" });
510
+ const odataSchema = z
511
+ .object({
512
+ // The service root, e.g. https://host/sap/opu/odata/sap/API_BUSINESS_PARTNER.
513
+ serviceRoot: z
514
+ .url({ protocol: /^https?$/ })
515
+ .refine((u) => !/[?#]/.test(u), "No query or fragment"),
516
+ version: z.enum(["2", "4"]),
517
+ auth: httpAuthSchema,
518
+ // Sent on every call, e.g. SAP's sap-client. Not secret: never credentials.
519
+ headers: z
520
+ .record(headerName.refine((h) => !RESERVED_HEADERS.has(h.toLowerCase()) &&
521
+ !CREDENTIAL_LIKE.test(h) &&
522
+ !/^(accept|if-match|x-http-method|odata-.*|(max)?dataserviceversion|prefer)$/i.test(h), "Set by the service, or a credential (use auth)"), headerValue)
523
+ .default({}),
524
+ // Writes first fetch an x-csrf-token (SAP Gateway).
525
+ csrf: z.boolean().default(false),
526
+ // "skip": $top and $skip; "server": the service's own pages (odata.maxpagesize, v4), e.g.
527
+ // Dynamics, which has no $skip.
528
+ paging: z.enum(["skip", "server"]).default("skip"),
529
+ tools: z.array(odataToolSchema).min(1),
530
+ allowInsecureHttp: z.boolean().default(false),
531
+ })
532
+ .strict()
533
+ .refine((o) => secure(o.allowInsecureHttp, o.serviceRoot, ...(o.auth.type === "oauth2" ? [o.auth.tokenUrl] : [])), { message: "serviceRoot and tokenUrl must be https unless allowInsecureHttp is true" })
534
+ .refine((o) => o.paging === "skip" || o.version === "4", {
535
+ message: "Server paging is for OData v4",
536
+ });
537
+ // An app reached as a database, described entirely here: the generic sql module
538
+ // (connectors/sql) runs it. Each tool is one statement the company's developer wrote, with
539
+ // :name params bound by the driver, never pasted into the text.
540
+ const sqlParamSchema = z
541
+ .object({
542
+ type: z.enum(["string", "number", "integer", "boolean"]),
543
+ description: text,
544
+ // Left out, an optional param is bound as null.
545
+ required: z.boolean().default(true),
546
+ // The value must be one of the agent's values of this list limit, while it has any.
547
+ limit: slug.optional(),
548
+ })
549
+ .strict();
550
+ const sqlToolSchema = z
551
+ .object({
552
+ name: slug,
553
+ description: text,
554
+ sql: text,
555
+ params: z.record(slug, sqlParamSchema).default({}),
556
+ // Changes data: run read-write and committed, and never in a required capability. Any
557
+ // other tool runs in a read-only transaction that is rolled back.
558
+ write: z.boolean().default(false),
559
+ // Deletes or overwrites: its capability must be destructive.
560
+ destructive: z.boolean().default(false),
561
+ // While the agent has values of `limit`, every row's `column` must be one of them, else
562
+ // nothing is returned (a write is rolled back). A missing column is refused.
563
+ answerLimits: z.array(z.object({ column: text, limit: slug }).strict()).default([]),
564
+ })
565
+ .strict()
566
+ .refine((t) => !t.destructive || t.write, { message: "A destructive tool is a write" });
567
+ const sqlSchema = z
568
+ .object({
569
+ engine: z.enum(["postgres", "sqlserver", "mysql", "oracle"]),
570
+ host: z.string().regex(/^[A-Za-z0-9._-]+$/),
571
+ // The engine's usual port when left out.
572
+ port: z.number().int().min(1).max(65535).optional(),
573
+ // Oracle: the service name.
574
+ database: text,
575
+ // Ids of connect form fields: user a plain one, password a secret one.
576
+ auth: z.object({ user: slug, password: slug }).strict(),
577
+ // "verify": encrypted, the server's certificate checked; "insecure": encrypted, not
578
+ // checked; "off": plain, e.g. a database on the same private network.
579
+ tls: z.enum(["verify", "insecure", "off"]).default("verify"),
580
+ timeoutSeconds: z.number().int().min(1).max(300).default(30),
581
+ // Rows returned per call; more are cut off and the answer says so.
582
+ maxRows: z.number().int().min(1).max(1000).default(200),
583
+ tools: z.array(sqlToolSchema).min(1),
584
+ })
585
+ .strict();
586
+ // Events the app can send (or a poll can read) that start agents (packages/server/src/events,
587
+ // docs/triggers.md). A path is `a.b`, or `a[].b` for every item of a list.
588
+ const eventPath = z.string().regex(/^[A-Za-z0-9_$@-]+(\[\])?(\.[A-Za-z0-9_$@-]+(\[\])?)*$/);
589
+ const eventId = z.string().regex(/^[A-Za-z0-9_.:-]{1,100}$/);
590
+ // Where a value is in a delivery: a path in its JSON body, or a request header.
591
+ const locator = z.union([
592
+ z.object({ field: eventPath }).strict(),
593
+ z.object({ header: headerName }).strict(),
594
+ ]);
595
+ const eventRequest = z
596
+ .object({
597
+ method: z.enum(["GET", "POST", "PUT"]),
598
+ path: z.string().startsWith("/"),
599
+ body: z.unknown().optional(),
600
+ })
601
+ .strict();
602
+ const eventsSchema = z
603
+ .object({
604
+ types: z.array(z.object({ id: eventId, label: text }).strict()).min(1),
605
+ // What an agent is given of each event, and what conditions and limits read: name → path.
606
+ fields: z.record(slug, eventPath),
607
+ // A limit's id → the field holding the event's value for it. An agent with that limit set
608
+ // is started only by events whose value is within it.
609
+ limits: z.record(slug, slug).default({}),
610
+ webhook: z
611
+ .object({
612
+ // How a delivery proves it came from the app, with the subscription's secret: an
613
+ // HMAC-SHA256 of the body (hex, after `prefix`), the secret itself in a header, or
614
+ // nothing at all, only with allowInsecure.
615
+ verify: z.discriminatedUnion("type", [
616
+ z
617
+ .object({
618
+ type: z.literal("hmac-sha256"),
619
+ header: headerName,
620
+ prefix: z.string().max(20).default(""),
621
+ })
622
+ .strict(),
623
+ z.object({ type: z.literal("token"), header: headerName }).strict(),
624
+ z.object({ type: z.literal("none") }).strict(),
625
+ ]),
626
+ allowInsecure: z.boolean().default(false),
627
+ type: locator,
628
+ // Same for every retry of one delivery; without one, the body's hash is used.
629
+ deliveryId: locator.optional(),
630
+ // When the app stamps deliveries: older (or newer) than the tolerance is refused.
631
+ timestamp: z
632
+ .object({
633
+ at: locator,
634
+ unit: z.enum(["s", "ms", "iso"]),
635
+ toleranceSeconds: z
636
+ .number()
637
+ .int()
638
+ .min(60)
639
+ .max(7 * 86400)
640
+ .default(300),
641
+ })
642
+ .strict()
643
+ .optional(),
644
+ // Where the admin pastes the address ({url}) and the secret.
645
+ setup: z.array(step).min(1),
646
+ // Registers the webhook through the app's API with the connection's credentials, filled
647
+ // with {name} {url} {secret} {events} {filter}. `location`: where the answer gives the
648
+ // webhook's own address.
649
+ register: eventRequest.extend({ location: dotPath }).strict().optional(),
650
+ })
651
+ .strict()
652
+ .refine((w) => w.verify.type !== "none" || w.allowInsecure, {
653
+ message: "A webhook verified by nothing needs allowInsecure",
654
+ })
655
+ .optional(),
656
+ // The account the app's connection acts as, so its own changes (an agent's comment) never
657
+ // start agents again: `request` answers its id at `id`; `fields` hold who made the change.
658
+ self: z
659
+ .object({ request: eventRequest, id: dotPath, fields: z.array(slug).min(1) })
660
+ .strict()
661
+ .optional(),
662
+ // For apps without webhooks: `request` read every interval, filled with {minutes} (since
663
+ // the cursor, rounded up, plus one) {cursor} and {filter} (the subscription's filter put
664
+ // into `filter`'s {value}, or nothing); `items` the list in the answer, each kept under
665
+ // `wrap` so the webhook's paths fit; `cursor` the item's change time; `deliveryId` the
666
+ // paths joined into an id; `types` the first whose `same` paths are equal (or that has none).
667
+ poll: z
668
+ .object({
669
+ request: eventRequest,
670
+ filter: z.string().includes("{value}").optional(),
671
+ items: dotPath,
672
+ wrap: slug.optional(),
673
+ cursor: eventPath,
674
+ deliveryId: z.array(eventPath).min(1),
675
+ types: z
676
+ .array(z.object({ type: eventId, same: z.tuple([eventPath, eventPath]).optional() }).strict())
677
+ .min(1),
678
+ })
679
+ .strict()
680
+ .optional(),
681
+ })
682
+ .strict()
683
+ .refine((e) => e.webhook || e.poll, { message: "Events come from a webhook, a poll or both" })
684
+ .refine((e) => Object.values(e.limits).every((f) => f in e.fields) &&
685
+ (e.self?.fields ?? []).every((f) => f in e.fields) &&
686
+ (e.poll?.types ?? []).every((t) => e.types.some((x) => x.id === t.type)), { message: "Event limits and self name fields, and a poll's types are declared types" });
207
687
  export const connectorSchema = z
208
688
  .object({
209
689
  id: z.string().regex(/^[a-z0-9-]+$/),
@@ -320,32 +800,38 @@ export const connectorSchema = z
320
800
  discover: probeSchema.optional(),
321
801
  revoke: text,
322
802
  http: httpSchema.optional(),
803
+ soap: soapSchema.optional(),
804
+ odata: odataSchema.optional(),
805
+ sql: sqlSchema.optional(),
806
+ events: eventsSchema.optional(),
323
807
  })
324
808
  .strict()
809
+ .refine((c) => [c.http, c.soap, c.odata, c.sql].filter(Boolean).length <= 1, {
810
+ message: "A connector has at most one of http, soap, odata and sql",
811
+ })
325
812
  .refine((c) => {
326
- if (!c.http)
813
+ const app = described(c);
814
+ if (!app)
327
815
  return true;
328
- const field = (id, secret) => c.fields.some((f) => f.id === id && f.secret === secret && !f.options);
329
- const a = c.http.auth;
330
- const secretsOk = a.type === "header"
331
- ? field(a.value, true)
332
- : a.type === "bearer"
333
- ? field(a.token, true)
334
- : a.type === "basic"
335
- ? field(a.username, false) && field(a.password, true)
336
- : field(a.clientId, false) && field(a.clientSecret, true);
337
816
  const lists = new Set(c.limits.filter((l) => l.kind === "list").map((l) => l.id));
338
- return (secretsOk &&
817
+ return (app.fields.every(([id, secret]) => c.fields.some((f) => f.id === id && f.secret === secret && !f.options)) &&
339
818
  !c.provider &&
340
819
  (c.accountKey === "host" || c.fields.some((f) => f.id === c.accountKey && !f.secret)) &&
341
- c.http.tools.every((t) => Object.values(t.params).every((p) => !p.limit || lists.has(p.limit)) &&
342
- t.answerLimits.every((a) => lists.has(a.limit))));
820
+ app.limits.every((l) => lists.has(l)));
343
821
  }, {
344
- message: "An http app's auth names its fields (secrets as secret fields), its accountKey is host or a plain field, a param's or answer's limit names a list limit, and it has no provider",
822
+ message: "An http, soap, odata or sql app's auth names its fields (secrets as secret fields), its accountKey is host or a plain field, a param's or answer's limit names a list limit, and it has no provider",
345
823
  })
346
824
  .refine((c) => !c.agentAccess || c.limits.some((l) => l.id === c.agentAccess.limit && l.kind === "text"), { message: "agentAccess.limit must be one of the connector's text limits" })
825
+ .refine((c) => {
826
+ if (!c.sql)
827
+ return true;
828
+ const writes = new Set(c.sql.tools.filter((t) => t.write).map((t) => t.name));
829
+ return ([c.verify, ...(c.discover ? [c.discover] : [])].every((p) => "check" in p) &&
830
+ c.capabilities.every((cap) => !cap.required || !cap.tools.some((t) => writes.has(t))));
831
+ }, { message: "An sql app's probes are checks, and a write tool is in no required capability" })
347
832
  .refine((c) => [...Object.keys(c.agentAccess?.probes ?? {}), ...(c.siteAccess?.write ?? [])].every((id) => c.capabilities.some((x) => x.id === id)) &&
348
833
  (!Object.keys(c.agentAccess?.probes ?? {}).length || c.agentAccess?.missing !== undefined), { message: "agentAccess probes and siteAccess.write name capabilities; probes need missing" })
834
+ .refine((c) => Object.keys(c.events?.limits ?? {}).every((id) => c.limits.some((l) => l.id === id && l.kind !== "map")), { message: "Event limits name the connector's list or text limits" })
349
835
  .refine((c) => new Set(c.fields.map((f) => f.id)).size === c.fields.length, {
350
836
  message: "Field ids must be unique",
351
837
  })
@@ -360,12 +846,63 @@ export const connectorSchema = z
360
846
  .refine((c) => {
361
847
  const isOption = (o, fields) => fields.some((f) => f.id === o.field && f.options?.some((x) => x.value === o.value));
362
848
  return ([...c.setup, ...c.afterConnect].every((s) => !s.option || isOption(s.option, c.fields)) &&
363
- c.fields.every((f, i) => !f.requires || (f.options && isOption(f.requires, c.fields.slice(0, i)))));
849
+ c.fields.every((f, i) => !f.requires || isOption(f.requires, c.fields.slice(0, i))));
364
850
  }, { message: "A step's option and a field's requires name an option of an earlier choice" })
365
851
  .refine((c) => c.provider !== undefined || c.fields.length > 0, {
366
852
  message: "A guided connector needs connect form fields",
367
853
  })
368
854
  .refine((c) => c.limits.every((l, i) => !l.keys || c.limits.slice(0, i).some((k) => k.id === l.keys && k.kind === "list")), { message: "A map limit's keys must be a list limit listed before it" });
855
+ // An app described in its connector.json (http, soap, odata or sql): the connect form fields
856
+ // its sign-in names, and the list limits its tools name.
857
+ function described(c) {
858
+ const limitsOf = (...sets) => sets.flatMap((s) => Object.values(s).flatMap((p) => (p.limit ? [p.limit] : [])));
859
+ if (c.http)
860
+ return {
861
+ fields: authFields(c.http.auth),
862
+ limits: c.http.tools.flatMap((t) => [
863
+ ...limitsOf(t.params),
864
+ ...t.answerLimits.map((a) => a.limit),
865
+ ]),
866
+ };
867
+ if (c.soap) {
868
+ const ws = c.soap.wsSecurity;
869
+ return {
870
+ fields: [
871
+ ...(c.soap.auth ? authFields(c.soap.auth) : []),
872
+ ...(ws
873
+ ? [
874
+ [ws.username, false],
875
+ [ws.password, true],
876
+ ]
877
+ : []),
878
+ ],
879
+ limits: c.soap.tools.flatMap((t) => [
880
+ ...limitsOf(t.params),
881
+ ...t.answerLimits.map((a) => a.limit),
882
+ ]),
883
+ };
884
+ }
885
+ if (c.odata)
886
+ return {
887
+ fields: authFields(c.odata.auth),
888
+ limits: c.odata.tools.flatMap((t) => [
889
+ ...limitsOf(t.key, t.filters, t.fields, t.params),
890
+ ...t.answerLimits.map((a) => a.limit),
891
+ ]),
892
+ };
893
+ if (c.sql)
894
+ return {
895
+ fields: [
896
+ [c.sql.auth.user, false],
897
+ [c.sql.auth.password, true],
898
+ ],
899
+ limits: c.sql.tools.flatMap((t) => [
900
+ ...limitsOf(t.params),
901
+ ...t.answerLimits.map((a) => a.limit),
902
+ ]),
903
+ };
904
+ return null;
905
+ }
369
906
  // One sign-in shared by several apps, e.g. a customer's Microsoft Entra app used by mail and
370
907
  // files. Its setup steps are shown with the first of its apps the admin connects.
371
908
  export const providerSchema = z
@@ -393,6 +930,21 @@ export const providerSchema = z
393
930
  .strict();
394
931
  /** The repo's connectors/ folder, beside packages/ (and so beside dist/packages/ once built). */
395
932
  export const CONNECTORS_DIR = new URL("../../../connectors/", import.meta.url);
933
+ function parse(raw, where, schema) {
934
+ let filled;
935
+ try {
936
+ filled = fillBrandDeep(raw);
937
+ }
938
+ catch (error) {
939
+ throw new Error(`${where}: ${error.message}`);
940
+ }
941
+ const parsed = schema.safeParse(filled);
942
+ if (!parsed.success)
943
+ throw new Error(`${where}: ${z.prettifyError(parsed.error)}`);
944
+ return parsed.data;
945
+ }
946
+ /** A definition given inline rather than in a folder, checked as loadConnectors checks one. */
947
+ export const parseConnector = (raw, where) => parse(raw, where, connectorSchema);
396
948
  // Each subfolder of dir holding `file` is one definition, whose id is the folder's name.
397
949
  // Subfolders may be links: Kubernetes mounts ConfigMap and Secret folders that way.
398
950
  function loadDir(dir, file, schema) {
@@ -401,20 +953,10 @@ function loadDir(dir, file, schema) {
401
953
  .sort()
402
954
  .map((name) => {
403
955
  const where = `${name}/${file}`;
404
- const raw = JSON.parse(readFileSync(new URL(where, dir), "utf8"));
405
- let filled;
406
- try {
407
- filled = fillBrandDeep(raw);
408
- }
409
- catch (error) {
410
- throw new Error(`${where}: ${error.message}`);
411
- }
412
- const parsed = schema.safeParse(filled);
413
- if (!parsed.success)
414
- throw new Error(`${where}: ${z.prettifyError(parsed.error)}`);
415
- if (parsed.data.id !== name)
956
+ const data = parse(JSON.parse(readFileSync(new URL(where, dir), "utf8")), where, schema);
957
+ if (data.id !== name)
416
958
  throw new Error(`${where}: id must match the folder name`);
417
- return parsed.data;
959
+ return data;
418
960
  });
419
961
  }
420
962
  export function loadConnectors(dir = CONNECTORS_DIR) {