@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.
- package/README.md +157 -10
- package/dist/connectors/dell-techdirect/index.d.ts +8 -0
- package/dist/connectors/dell-techdirect/tools.d.ts +2 -0
- package/dist/connectors/dell-techdirect/vendor.d.ts +6 -0
- package/dist/connectors/dynamics-365/connector.json +213 -0
- package/dist/connectors/dynamics-365/index.d.ts +11 -0
- package/dist/connectors/dynamics-365/index.js +10 -0
- package/dist/connectors/dynamics-365/index.js.map +1 -0
- package/dist/connectors/dynamics-365/tools.d.ts +35 -0
- package/dist/connectors/dynamics-365/tools.js +806 -0
- package/dist/connectors/dynamics-365/tools.js.map +1 -0
- package/dist/connectors/dynamics-365/vendor.d.ts +9 -0
- package/dist/connectors/dynamics-365/vendor.js +110 -0
- package/dist/connectors/dynamics-365/vendor.js.map +1 -0
- package/dist/connectors/entra/index.d.ts +2 -0
- package/dist/connectors/entra/tools.d.ts +14 -0
- package/dist/connectors/fedex/index.d.ts +8 -0
- package/dist/connectors/fedex/tools.d.ts +3 -0
- package/dist/connectors/fedex/vendor.d.ts +7 -0
- package/dist/connectors/gong/connector.json +127 -0
- package/dist/connectors/gong/index.d.ts +8 -0
- package/dist/connectors/gong/index.js +7 -0
- package/dist/connectors/gong/index.js.map +1 -0
- package/dist/connectors/gong/tools.d.ts +8 -0
- package/dist/connectors/gong/tools.js +424 -0
- package/dist/connectors/gong/tools.js.map +1 -0
- package/dist/connectors/gong/vendor.d.ts +4 -0
- package/dist/connectors/gong/vendor.js +97 -0
- package/dist/connectors/gong/vendor.js.map +1 -0
- package/dist/connectors/http/index.d.ts +38 -0
- package/dist/connectors/http/index.js +151 -64
- package/dist/connectors/http/index.js.map +1 -1
- package/dist/connectors/intune/index.d.ts +2 -0
- package/dist/connectors/intune/tools.d.ts +12 -0
- package/dist/connectors/jamf/connector.json +182 -0
- package/dist/connectors/jamf/index.d.ts +8 -0
- package/dist/connectors/jamf/index.js +7 -0
- package/dist/connectors/jamf/index.js.map +1 -0
- package/dist/connectors/jamf/tools.d.ts +7 -0
- package/dist/connectors/jamf/tools.js +659 -0
- package/dist/connectors/jamf/tools.js.map +1 -0
- package/dist/connectors/jamf/vendor.d.ts +5 -0
- package/dist/connectors/jamf/vendor.js +66 -0
- package/dist/connectors/jamf/vendor.js.map +1 -0
- package/dist/connectors/jira/connector.json +144 -1
- package/dist/connectors/jira/index.d.ts +8 -0
- package/dist/connectors/jira/jql.d.ts +4 -0
- package/dist/connectors/jira/tools.d.ts +3 -0
- package/dist/connectors/jira/vendor.d.ts +3 -0
- package/dist/connectors/microsoft/client.d.ts +58 -0
- package/dist/connectors/netsuite/connector.json +315 -0
- package/dist/connectors/netsuite/index.d.ts +8 -0
- package/dist/connectors/netsuite/index.js +7 -0
- package/dist/connectors/netsuite/index.js.map +1 -0
- package/dist/connectors/netsuite/tools.d.ts +24 -0
- package/dist/connectors/netsuite/tools.js +447 -0
- package/dist/connectors/netsuite/tools.js.map +1 -0
- package/dist/connectors/netsuite/vendor.d.ts +26 -0
- package/dist/connectors/netsuite/vendor.js +248 -0
- package/dist/connectors/netsuite/vendor.js.map +1 -0
- package/dist/connectors/odata/index.d.ts +36 -0
- package/dist/connectors/odata/index.js +460 -0
- package/dist/connectors/odata/index.js.map +1 -0
- package/dist/connectors/okta/connector.json +193 -0
- package/dist/connectors/okta/index.d.ts +8 -0
- package/dist/connectors/okta/index.js +7 -0
- package/dist/connectors/okta/index.js.map +1 -0
- package/dist/connectors/okta/tools.d.ts +6 -0
- package/dist/connectors/okta/tools.js +429 -0
- package/dist/connectors/okta/tools.js.map +1 -0
- package/dist/connectors/okta/vendor.d.ts +14 -0
- package/dist/connectors/okta/vendor.js +203 -0
- package/dist/connectors/okta/vendor.js.map +1 -0
- package/dist/connectors/outlook-calendar/index.d.ts +2 -0
- package/dist/connectors/outlook-calendar/tools.d.ts +65 -0
- package/dist/connectors/outlook-mail/index.d.ts +2 -0
- package/dist/connectors/outlook-mail/tools.d.ts +8 -0
- package/dist/connectors/salesforce/index.d.ts +8 -0
- package/dist/connectors/salesforce/soql.d.ts +3 -0
- package/dist/connectors/salesforce/tools.d.ts +9 -0
- package/dist/connectors/salesforce/vendor.d.ts +3 -0
- package/dist/connectors/sap-s4hana/connector.json +263 -0
- package/dist/connectors/sap-s4hana/index.d.ts +8 -0
- package/dist/connectors/sap-s4hana/index.js +7 -0
- package/dist/connectors/sap-s4hana/index.js.map +1 -0
- package/dist/connectors/sap-s4hana/tools.d.ts +4 -0
- package/dist/connectors/sap-s4hana/tools.js +761 -0
- package/dist/connectors/sap-s4hana/tools.js.map +1 -0
- package/dist/connectors/sap-s4hana/vendor.d.ts +5 -0
- package/dist/connectors/sap-s4hana/vendor.js +176 -0
- package/dist/connectors/sap-s4hana/vendor.js.map +1 -0
- package/dist/connectors/servicenow/connector.json +228 -0
- package/dist/connectors/servicenow/index.d.ts +8 -0
- package/dist/connectors/servicenow/index.js +7 -0
- package/dist/connectors/servicenow/index.js.map +1 -0
- package/dist/connectors/servicenow/tools.d.ts +4 -0
- package/dist/connectors/servicenow/tools.js +884 -0
- package/dist/connectors/servicenow/tools.js.map +1 -0
- package/dist/connectors/servicenow/vendor.d.ts +4 -0
- package/dist/connectors/servicenow/vendor.js +109 -0
- package/dist/connectors/servicenow/vendor.js.map +1 -0
- package/dist/connectors/sharepoint-files/excel.d.ts +16 -0
- package/dist/connectors/sharepoint-files/index.d.ts +11 -0
- package/dist/connectors/sharepoint-files/tools.d.ts +52 -0
- package/dist/connectors/sharepoint-files/vendor.d.ts +11 -0
- package/dist/connectors/slack/connector.json +148 -0
- package/dist/connectors/slack/index.d.ts +8 -0
- package/dist/connectors/slack/index.js +7 -0
- package/dist/connectors/slack/index.js.map +1 -0
- package/dist/connectors/slack/tools.d.ts +3 -0
- package/dist/connectors/slack/tools.js +355 -0
- package/dist/connectors/slack/tools.js.map +1 -0
- package/dist/connectors/slack/vendor.d.ts +17 -0
- package/dist/connectors/slack/vendor.js +195 -0
- package/dist/connectors/slack/vendor.js.map +1 -0
- package/dist/connectors/soap/index.d.ts +19 -0
- package/dist/connectors/soap/index.js +248 -0
- package/dist/connectors/soap/index.js.map +1 -0
- package/dist/connectors/soap/xml.d.ts +12 -0
- package/dist/connectors/soap/xml.js +126 -0
- package/dist/connectors/soap/xml.js.map +1 -0
- package/dist/connectors/sql/engines.d.ts +58 -0
- package/dist/connectors/sql/engines.js +292 -0
- package/dist/connectors/sql/engines.js.map +1 -0
- package/dist/connectors/sql/index.d.ts +19 -0
- package/dist/connectors/sql/index.js +212 -0
- package/dist/connectors/sql/index.js.map +1 -0
- package/dist/connectors/teams/index.d.ts +2 -0
- package/dist/connectors/teams/tools.d.ts +2 -0
- package/dist/connectors/workday/connector.json +186 -0
- package/dist/connectors/workday/index.d.ts +8 -0
- package/dist/connectors/workday/index.js +7 -0
- package/dist/connectors/workday/index.js.map +1 -0
- package/dist/connectors/workday/tools.d.ts +33 -0
- package/dist/connectors/workday/tools.js +410 -0
- package/dist/connectors/workday/tools.js.map +1 -0
- package/dist/connectors/workday/vendor.d.ts +18 -0
- package/dist/connectors/workday/vendor.js +195 -0
- package/dist/connectors/workday/vendor.js.map +1 -0
- package/dist/packages/connector-kit/src/branding.d.ts +57 -0
- package/dist/packages/connector-kit/src/certificate.d.ts +19 -0
- package/dist/packages/connector-kit/src/common.d.ts +40 -0
- package/dist/packages/connector-kit/src/connector.d.ts +1346 -0
- package/dist/packages/connector-kit/src/connector.js +586 -44
- package/dist/packages/connector-kit/src/connector.js.map +1 -1
- package/dist/packages/connector-kit/src/index.d.ts +5 -0
- package/dist/packages/connector-kit/src/index.js +5 -0
- package/dist/packages/connector-kit/src/index.js.map +1 -0
- package/dist/packages/connector-kit/src/oauth.d.ts +15 -0
- package/dist/packages/connector-kit/src/oauth.js +21 -4
- package/dist/packages/connector-kit/src/oauth.js.map +1 -1
- package/dist/packages/connector-kit/src/types.d.ts +85 -0
- package/dist/packages/connector-kit/src/types.js.map +1 -1
- package/dist/packages/server/migrations/0007_app_events.sql +110 -0
- package/dist/packages/server/migrations/rollback/to-legacy.sql +3 -0
- package/dist/packages/server/src/api.d.ts +3714 -0
- package/dist/packages/server/src/api.js +135 -1
- package/dist/packages/server/src/api.js.map +1 -1
- package/dist/packages/server/src/app.d.ts +23 -0
- package/dist/packages/server/src/app.js +24 -1
- package/dist/packages/server/src/app.js.map +1 -1
- package/dist/packages/server/src/builtin.d.ts +4 -0
- package/dist/packages/server/src/builtin.js +18 -0
- package/dist/packages/server/src/builtin.js.map +1 -1
- package/dist/packages/server/src/consent.d.ts +27 -0
- package/dist/packages/server/src/events/admin.d.ts +156 -0
- package/dist/packages/server/src/events/admin.js +255 -0
- package/dist/packages/server/src/events/admin.js.map +1 -0
- package/dist/packages/server/src/events/deliver.d.ts +43 -0
- package/dist/packages/server/src/events/deliver.js +129 -0
- package/dist/packages/server/src/events/deliver.js.map +1 -0
- package/dist/packages/server/src/events/runtime.d.ts +28 -0
- package/dist/packages/server/src/events/runtime.js +397 -0
- package/dist/packages/server/src/events/runtime.js.map +1 -0
- package/dist/packages/server/src/events/store.d.ts +172 -0
- package/dist/packages/server/src/events/store.js +275 -0
- package/dist/packages/server/src/events/store.js.map +1 -0
- package/dist/packages/server/src/index.d.ts +40 -0
- package/dist/packages/server/src/index.js +67 -11
- package/dist/packages/server/src/index.js.map +1 -1
- package/dist/packages/server/src/migrate.d.ts +11 -0
- package/dist/packages/server/src/openapi.d.ts +4 -0
- package/dist/packages/server/src/openapi.js +2 -2
- package/dist/packages/server/src/openapi.js.map +1 -1
- package/dist/packages/server/src/queue.d.ts +10 -0
- package/dist/packages/server/src/queue.js +45 -0
- package/dist/packages/server/src/queue.js.map +1 -0
- package/dist/packages/server/src/secrets.d.ts +19 -0
- package/dist/packages/server/src/server.d.ts +1 -0
- package/dist/packages/server/src/server.js +14 -1
- package/dist/packages/server/src/server.js.map +1 -1
- package/dist/packages/server/src/service.d.ts +760 -0
- package/dist/packages/server/src/service.js +10 -0
- package/dist/packages/server/src/service.js.map +1 -1
- package/dist/packages/server/src/store.d.ts +158 -0
- package/dist/packages/server/src/store.js +4 -0
- package/dist/packages/server/src/store.js.map +1 -1
- package/dist/packages/server/src/teams/activity.d.ts +123 -0
- package/dist/packages/server/src/teams/activity.js +6 -5
- package/dist/packages/server/src/teams/activity.js.map +1 -1
- package/dist/packages/server/src/teams/admin.d.ts +335 -0
- package/dist/packages/server/src/teams/admin.js +1 -1
- package/dist/packages/server/src/teams/admin.js.map +1 -1
- package/dist/packages/server/src/teams/approval.d.ts +57 -0
- package/dist/packages/server/src/teams/bot.d.ts +25 -0
- package/dist/packages/server/src/teams/claude.d.ts +46 -0
- package/dist/packages/server/src/teams/jwt.d.ts +17 -0
- package/dist/packages/server/src/teams/package.d.ts +79 -0
- package/dist/packages/server/src/teams/provision.d.ts +84 -0
- package/dist/packages/server/src/teams/runner.d.ts +80 -0
- package/dist/packages/server/src/teams/runner.js +50 -6
- package/dist/packages/server/src/teams/runner.js.map +1 -1
- package/dist/packages/server/src/teams/runtime.d.ts +91 -0
- package/dist/packages/server/src/teams/runtime.js +25 -47
- package/dist/packages/server/src/teams/runtime.js.map +1 -1
- package/dist/packages/server/src/teams/store.d.ts +233 -0
- 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
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
|
|
177
|
-
|
|
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" &&
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
405
|
-
|
|
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
|
|
959
|
+
return data;
|
|
418
960
|
});
|
|
419
961
|
}
|
|
420
962
|
export function loadConnectors(dir = CONNECTORS_DIR) {
|