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

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