@arbium/enterprise-agent-connect 0.1.1 → 0.1.2-dev.cba5f68
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 +164 -12
- 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 +228 -0
- package/dist/connectors/dynamics-365/index.d.ts +8 -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 +89 -0
- package/dist/connectors/gong/vendor.js.map +1 -0
- package/dist/connectors/http/index.d.ts +34 -0
- package/dist/connectors/http/index.js +154 -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 +70 -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/jira/vendor.js +8 -2
- package/dist/connectors/jira/vendor.js.map +1 -1
- package/dist/connectors/microsoft/client.d.ts +66 -0
- package/dist/connectors/microsoft/client.js +26 -8
- package/dist/connectors/microsoft/client.js.map +1 -1
- package/dist/connectors/netsuite/connector.json +317 -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 +247 -0
- package/dist/connectors/netsuite/vendor.js.map +1 -0
- package/dist/connectors/odata/index.d.ts +33 -0
- package/dist/connectors/odata/index.js +460 -0
- package/dist/connectors/odata/index.js.map +1 -0
- package/dist/connectors/okta/connector.json +194 -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 +204 -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/salesforce/vendor.js +9 -3
- package/dist/connectors/salesforce/vendor.js.map +1 -1
- package/dist/connectors/sap-s4hana/connector.json +264 -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 +3 -0
- package/dist/connectors/servicenow/vendor.js +106 -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 +8 -0
- package/dist/connectors/sharepoint-files/index.js.map +1 -1
- 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 +189 -0
- package/dist/connectors/slack/vendor.js.map +1 -0
- package/dist/connectors/soap/index.d.ts +16 -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 +16 -0
- package/dist/connectors/sql/index.js +214 -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 +7 -0
- package/dist/connectors/teams/tools.js +3 -2
- package/dist/connectors/teams/tools.js.map +1 -1
- package/dist/connectors/workday/connector.json +188 -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 +199 -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 +36 -0
- package/dist/packages/connector-kit/src/common.js +4 -1
- package/dist/packages/connector-kit/src/common.js.map +1 -1
- package/dist/packages/connector-kit/src/connector.d.ts +1356 -0
- package/dist/packages/connector-kit/src/connector.js +632 -53
- package/dist/packages/connector-kit/src/connector.js.map +1 -1
- package/dist/packages/connector-kit/src/fetch.d.ts +6 -0
- package/dist/packages/connector-kit/src/fetch.js +44 -0
- package/dist/packages/connector-kit/src/fetch.js.map +1 -0
- package/dist/packages/connector-kit/src/index.d.ts +5 -0
- package/dist/packages/connector-kit/src/index.js +6 -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 +25 -6
- package/dist/packages/connector-kit/src/oauth.js.map +1 -1
- package/dist/packages/connector-kit/src/types.d.ts +87 -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 +3730 -0
- package/dist/packages/server/src/api.js +137 -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 +5 -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 +68 -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 +767 -0
- package/dist/packages/server/src/service.js +19 -3
- package/dist/packages/server/src/service.js.map +1 -1
- package/dist/packages/server/src/store.d.ts +157 -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,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
|
|
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
|
|
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
|
|
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({
|
|
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,426 @@ 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
|
-
|
|
177
|
-
|
|
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" &&
|
|
186
|
-
|
|
187
|
-
|
|
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 `same` paths are equal (or that has none).
|
|
683
|
+
poll: z
|
|
684
|
+
.object({
|
|
685
|
+
request: eventRequest,
|
|
686
|
+
filter: z.string().includes("{value}").optional(),
|
|
687
|
+
items: dotPath,
|
|
688
|
+
wrap: slug.optional(),
|
|
689
|
+
cursor: eventPath,
|
|
690
|
+
deliveryId: z.array(eventPath).min(1),
|
|
691
|
+
types: z
|
|
692
|
+
.array(z.object({ type: eventId, same: z.tuple([eventPath, eventPath]).optional() }).strict())
|
|
693
|
+
.min(1),
|
|
694
|
+
})
|
|
695
|
+
.strict()
|
|
696
|
+
.optional(),
|
|
697
|
+
})
|
|
698
|
+
.strict()
|
|
699
|
+
.refine((e) => e.webhook || e.poll, { message: "Events come from a webhook, a poll or both" })
|
|
700
|
+
.refine((e) => Object.values(e.limits).every((f) => f in e.fields) &&
|
|
701
|
+
(e.self?.fields ?? []).every((f) => f in e.fields) &&
|
|
702
|
+
(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
703
|
export const connectorSchema = z
|
|
208
704
|
.object({
|
|
209
705
|
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
@@ -230,8 +726,8 @@ export const connectorSchema = z
|
|
|
230
726
|
// Vendor scopes every ceiling needs, whatever is ticked (e.g. what adding agents' Teams
|
|
231
727
|
// bots takes), filled into {scope}/{scopes} with the capabilities' own.
|
|
232
728
|
scopes: z.array(text).optional(),
|
|
233
|
-
setup: z.array(
|
|
234
|
-
afterConnect: z.array(
|
|
729
|
+
setup: z.array(appStep),
|
|
730
|
+
afterConnect: z.array(appStep),
|
|
235
731
|
fields: z.array(fieldSchema),
|
|
236
732
|
capabilities: z
|
|
237
733
|
.array(z
|
|
@@ -320,32 +816,38 @@ export const connectorSchema = z
|
|
|
320
816
|
discover: probeSchema.optional(),
|
|
321
817
|
revoke: text,
|
|
322
818
|
http: httpSchema.optional(),
|
|
819
|
+
soap: soapSchema.optional(),
|
|
820
|
+
odata: odataSchema.optional(),
|
|
821
|
+
sql: sqlSchema.optional(),
|
|
822
|
+
events: eventsSchema.optional(),
|
|
323
823
|
})
|
|
324
824
|
.strict()
|
|
825
|
+
.refine((c) => [c.http, c.soap, c.odata, c.sql].filter(Boolean).length <= 1, {
|
|
826
|
+
message: "A connector has at most one of http, soap, odata and sql",
|
|
827
|
+
})
|
|
325
828
|
.refine((c) => {
|
|
326
|
-
|
|
829
|
+
const app = described(c);
|
|
830
|
+
if (!app)
|
|
327
831
|
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
832
|
const lists = new Set(c.limits.filter((l) => l.kind === "list").map((l) => l.id));
|
|
338
|
-
return (
|
|
833
|
+
return (app.fields.every(([id, secret]) => c.fields.some((f) => f.id === id && f.secret === secret && !f.options)) &&
|
|
339
834
|
!c.provider &&
|
|
340
835
|
(c.accountKey === "host" || c.fields.some((f) => f.id === c.accountKey && !f.secret)) &&
|
|
341
|
-
|
|
342
|
-
t.answerLimits.every((a) => lists.has(a.limit))));
|
|
836
|
+
app.limits.every((l) => lists.has(l)));
|
|
343
837
|
}, {
|
|
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",
|
|
838
|
+
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
839
|
})
|
|
346
840
|
.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" })
|
|
841
|
+
.refine((c) => {
|
|
842
|
+
if (!c.sql)
|
|
843
|
+
return true;
|
|
844
|
+
const writes = new Set(c.sql.tools.filter((t) => t.write).map((t) => t.name));
|
|
845
|
+
return ([c.verify, ...(c.discover ? [c.discover] : [])].every((p) => "check" in p) &&
|
|
846
|
+
c.capabilities.every((cap) => !cap.required || !cap.tools.some((t) => writes.has(t))));
|
|
847
|
+
}, { message: "An sql app's probes are checks, and a write tool is in no required capability" })
|
|
347
848
|
.refine((c) => [...Object.keys(c.agentAccess?.probes ?? {}), ...(c.siteAccess?.write ?? [])].every((id) => c.capabilities.some((x) => x.id === id)) &&
|
|
348
849
|
(!Object.keys(c.agentAccess?.probes ?? {}).length || c.agentAccess?.missing !== undefined), { message: "agentAccess probes and siteAccess.write name capabilities; probes need missing" })
|
|
850
|
+
.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
851
|
.refine((c) => new Set(c.fields.map((f) => f.id)).size === c.fields.length, {
|
|
350
852
|
message: "Field ids must be unique",
|
|
351
853
|
})
|
|
@@ -360,12 +862,64 @@ export const connectorSchema = z
|
|
|
360
862
|
.refine((c) => {
|
|
361
863
|
const isOption = (o, fields) => fields.some((f) => f.id === o.field && f.options?.some((x) => x.value === o.value));
|
|
362
864
|
return ([...c.setup, ...c.afterConnect].every((s) => !s.option || isOption(s.option, c.fields)) &&
|
|
363
|
-
c.fields.every((f, i) => !f.requires ||
|
|
865
|
+
c.fields.every((f, i) => !f.requires || isOption(f.requires, c.fields.slice(0, i))));
|
|
364
866
|
}, { message: "A step's option and a field's requires name an option of an earlier choice" })
|
|
867
|
+
.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
868
|
.refine((c) => c.provider !== undefined || c.fields.length > 0, {
|
|
366
869
|
message: "A guided connector needs connect form fields",
|
|
367
870
|
})
|
|
368
871
|
.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" });
|
|
872
|
+
// An app described in its connector.json (http, soap, odata or sql): the connect form fields
|
|
873
|
+
// its sign-in names, and the list limits its tools name.
|
|
874
|
+
function described(c) {
|
|
875
|
+
const limitsOf = (...sets) => sets.flatMap((s) => Object.values(s).flatMap((p) => (p.limit ? [p.limit] : [])));
|
|
876
|
+
if (c.http)
|
|
877
|
+
return {
|
|
878
|
+
fields: authFields(c.http.auth),
|
|
879
|
+
limits: c.http.tools.flatMap((t) => [
|
|
880
|
+
...limitsOf(t.params),
|
|
881
|
+
...t.answerLimits.map((a) => a.limit),
|
|
882
|
+
]),
|
|
883
|
+
};
|
|
884
|
+
if (c.soap) {
|
|
885
|
+
const ws = c.soap.wsSecurity;
|
|
886
|
+
return {
|
|
887
|
+
fields: [
|
|
888
|
+
...(c.soap.auth ? authFields(c.soap.auth) : []),
|
|
889
|
+
...(ws
|
|
890
|
+
? [
|
|
891
|
+
[ws.username, false],
|
|
892
|
+
[ws.password, true],
|
|
893
|
+
]
|
|
894
|
+
: []),
|
|
895
|
+
],
|
|
896
|
+
limits: c.soap.tools.flatMap((t) => [
|
|
897
|
+
...limitsOf(t.params),
|
|
898
|
+
...t.answerLimits.map((a) => a.limit),
|
|
899
|
+
]),
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
if (c.odata)
|
|
903
|
+
return {
|
|
904
|
+
fields: authFields(c.odata.auth),
|
|
905
|
+
limits: c.odata.tools.flatMap((t) => [
|
|
906
|
+
...limitsOf(t.key, t.filters, t.fields, t.params),
|
|
907
|
+
...t.answerLimits.map((a) => a.limit),
|
|
908
|
+
]),
|
|
909
|
+
};
|
|
910
|
+
if (c.sql)
|
|
911
|
+
return {
|
|
912
|
+
fields: [
|
|
913
|
+
[c.sql.auth.user, false],
|
|
914
|
+
[c.sql.auth.password, true],
|
|
915
|
+
],
|
|
916
|
+
limits: c.sql.tools.flatMap((t) => [
|
|
917
|
+
...limitsOf(t.params),
|
|
918
|
+
...t.answerLimits.map((a) => a.limit),
|
|
919
|
+
]),
|
|
920
|
+
};
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
369
923
|
// One sign-in shared by several apps, e.g. a customer's Microsoft Entra app used by mail and
|
|
370
924
|
// files. Its setup steps are shown with the first of its apps the admin connects.
|
|
371
925
|
export const providerSchema = z
|
|
@@ -384,7 +938,7 @@ export const providerSchema = z
|
|
|
384
938
|
// The service can make a key pair whose certificate the admin uploads to their app.
|
|
385
939
|
certificate: z.boolean(),
|
|
386
940
|
setup: z.array(step),
|
|
387
|
-
fields: z.array(fieldSchema
|
|
941
|
+
fields: z.array(fieldSchema).min(1),
|
|
388
942
|
// What one more agent needs outside the service (e.g. its mailbox), shown when its API key is
|
|
389
943
|
// made with one of the apps each step names.
|
|
390
944
|
addAgent: z.array(agentStep).default([]),
|
|
@@ -393,32 +947,38 @@ export const providerSchema = z
|
|
|
393
947
|
.strict();
|
|
394
948
|
/** The repo's connectors/ folder, beside packages/ (and so beside dist/packages/ once built). */
|
|
395
949
|
export const CONNECTORS_DIR = new URL("../../../connectors/", import.meta.url);
|
|
950
|
+
function parse(raw, where, schema) {
|
|
951
|
+
let filled;
|
|
952
|
+
try {
|
|
953
|
+
filled = fillBrandDeep(raw);
|
|
954
|
+
}
|
|
955
|
+
catch (error) {
|
|
956
|
+
throw new Error(`${where}: ${error.message}`);
|
|
957
|
+
}
|
|
958
|
+
const parsed = schema.safeParse(filled);
|
|
959
|
+
if (!parsed.success)
|
|
960
|
+
throw new Error(`${where}: ${z.prettifyError(parsed.error)}`);
|
|
961
|
+
return parsed.data;
|
|
962
|
+
}
|
|
963
|
+
/** A definition given inline rather than in a folder, checked as loadConnectors checks one. */
|
|
964
|
+
export const parseConnector = (raw, where) => parse(raw, where, connectorSchema);
|
|
396
965
|
// Each subfolder of dir holding `file` is one definition, whose id is the folder's name.
|
|
397
966
|
// Subfolders may be links: Kubernetes mounts ConfigMap and Secret folders that way.
|
|
398
|
-
function loadDir(dir, file, schema) {
|
|
967
|
+
function loadDir(dir, file, schema, only = () => true) {
|
|
399
968
|
return readdirSync(dir)
|
|
400
|
-
.filter((name) => existsSync(new URL(`${name}/${file}`, dir)))
|
|
969
|
+
.filter((name) => only(name) && existsSync(new URL(`${name}/${file}`, dir)))
|
|
401
970
|
.sort()
|
|
402
971
|
.map((name) => {
|
|
403
972
|
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)
|
|
973
|
+
const data = parse(JSON.parse(readFileSync(new URL(where, dir), "utf8")), where, schema);
|
|
974
|
+
if (data.id !== name)
|
|
416
975
|
throw new Error(`${where}: id must match the folder name`);
|
|
417
|
-
return
|
|
976
|
+
return data;
|
|
418
977
|
});
|
|
419
978
|
}
|
|
420
|
-
|
|
421
|
-
|
|
979
|
+
/** `only`: the folder ids to read, so an app not offered is never parsed. */
|
|
980
|
+
export function loadConnectors(dir = CONNECTORS_DIR, only) {
|
|
981
|
+
return loadDir(dir, "connector.json", connectorSchema, only);
|
|
422
982
|
}
|
|
423
983
|
export function loadProviders(dir = CONNECTORS_DIR) {
|
|
424
984
|
return loadDir(dir, "provider.json", providerSchema);
|
|
@@ -477,6 +1037,25 @@ export function expandCopy(lines, scopes) {
|
|
|
477
1037
|
}
|
|
478
1038
|
// Reads `a.b` / `values[].name` paths out of a JSON response for display.
|
|
479
1039
|
export function pickDisplay(data, path, match = {}) {
|
|
1040
|
+
return pick(data, path, match)
|
|
1041
|
+
.filter((v) => v !== undefined && v !== null)
|
|
1042
|
+
.map(String);
|
|
1043
|
+
}
|
|
1044
|
+
/** Each value `show` picks, with its label `name` picks from the same list item. */
|
|
1045
|
+
export function pickLabels(data, show, name, match = {}) {
|
|
1046
|
+
const prefix = listPrefix(show);
|
|
1047
|
+
const items = prefix ? pick(data, prefix, match) : [data];
|
|
1048
|
+
const rest = (path) => path.slice(prefix.length).replace(/^\./, "");
|
|
1049
|
+
const out = {};
|
|
1050
|
+
for (const item of items) {
|
|
1051
|
+
const [value] = rest(show) ? pick(item, rest(show)) : [item];
|
|
1052
|
+
const [label] = rest(name) ? pick(item, rest(name)) : [item];
|
|
1053
|
+
if (value != null && label != null)
|
|
1054
|
+
out[String(value)] = String(label);
|
|
1055
|
+
}
|
|
1056
|
+
return out;
|
|
1057
|
+
}
|
|
1058
|
+
function pick(data, path, match = {}) {
|
|
480
1059
|
const matches = (v) => Object.entries(match).every(([k, want]) => v && typeof v === "object" && v[k] === want);
|
|
481
1060
|
let values = [data];
|
|
482
1061
|
for (const part of path.split(".")) {
|
|
@@ -487,6 +1066,6 @@ export function pickDisplay(data, path, match = {}) {
|
|
|
487
1066
|
return many ? (Array.isArray(next) ? next.filter(matches) : []) : [next];
|
|
488
1067
|
});
|
|
489
1068
|
}
|
|
490
|
-
return values
|
|
1069
|
+
return values;
|
|
491
1070
|
}
|
|
492
1071
|
//# sourceMappingURL=connector.js.map
|