@arbium/enterprise-agent-connect 0.0.0-stage → 0.1.2-dev.75145ef
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/LICENSE +202 -0
- package/README.md +269 -2
- package/dist/connectors/dell-techdirect/connector.json +39 -0
- package/dist/connectors/dell-techdirect/index.d.ts +8 -0
- package/dist/connectors/dell-techdirect/index.js +7 -0
- package/dist/connectors/dell-techdirect/index.js.map +1 -0
- package/dist/connectors/dell-techdirect/tools.d.ts +2 -0
- package/dist/connectors/dell-techdirect/tools.js +49 -0
- package/dist/connectors/dell-techdirect/tools.js.map +1 -0
- package/dist/connectors/dell-techdirect/vendor.d.ts +6 -0
- package/dist/connectors/dell-techdirect/vendor.js +57 -0
- package/dist/connectors/dell-techdirect/vendor.js.map +1 -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/connector.json +85 -0
- package/dist/connectors/entra/index.d.ts +2 -0
- package/dist/connectors/entra/index.js +4 -0
- package/dist/connectors/entra/index.js.map +1 -0
- package/dist/connectors/entra/tools.d.ts +14 -0
- package/dist/connectors/entra/tools.js +262 -0
- package/dist/connectors/entra/tools.js.map +1 -0
- package/dist/connectors/fedex/connector.json +118 -0
- package/dist/connectors/fedex/index.d.ts +8 -0
- package/dist/connectors/fedex/index.js +7 -0
- package/dist/connectors/fedex/index.js.map +1 -0
- package/dist/connectors/fedex/tools.d.ts +3 -0
- package/dist/connectors/fedex/tools.js +152 -0
- package/dist/connectors/fedex/tools.js.map +1 -0
- package/dist/connectors/fedex/vendor.d.ts +7 -0
- package/dist/connectors/fedex/vendor.js +98 -0
- package/dist/connectors/fedex/vendor.js.map +1 -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 +325 -0
- package/dist/connectors/http/index.js.map +1 -0
- package/dist/connectors/intune/connector.json +106 -0
- package/dist/connectors/intune/index.d.ts +2 -0
- package/dist/connectors/intune/index.js +4 -0
- package/dist/connectors/intune/index.js.map +1 -0
- package/dist/connectors/intune/tools.d.ts +12 -0
- package/dist/connectors/intune/tools.js +416 -0
- package/dist/connectors/intune/tools.js.map +1 -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 +272 -0
- package/dist/connectors/jira/index.d.ts +8 -0
- package/dist/connectors/jira/index.js +7 -0
- package/dist/connectors/jira/index.js.map +1 -0
- package/dist/connectors/jira/jql.d.ts +4 -0
- package/dist/connectors/jira/jql.js +67 -0
- package/dist/connectors/jira/jql.js.map +1 -0
- package/dist/connectors/jira/tools.d.ts +3 -0
- package/dist/connectors/jira/tools.js +464 -0
- package/dist/connectors/jira/tools.js.map +1 -0
- package/dist/connectors/jira/vendor.d.ts +3 -0
- package/dist/connectors/jira/vendor.js +100 -0
- package/dist/connectors/jira/vendor.js.map +1 -0
- package/dist/connectors/microsoft/client.d.ts +66 -0
- package/dist/connectors/microsoft/client.js +452 -0
- package/dist/connectors/microsoft/client.js.map +1 -0
- package/dist/connectors/microsoft/provider.json +118 -0
- 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/connector.json +136 -0
- package/dist/connectors/outlook-calendar/index.d.ts +2 -0
- package/dist/connectors/outlook-calendar/index.js +4 -0
- package/dist/connectors/outlook-calendar/index.js.map +1 -0
- package/dist/connectors/outlook-calendar/tools.d.ts +65 -0
- package/dist/connectors/outlook-calendar/tools.js +398 -0
- package/dist/connectors/outlook-calendar/tools.js.map +1 -0
- package/dist/connectors/outlook-mail/connector.json +154 -0
- package/dist/connectors/outlook-mail/index.d.ts +2 -0
- package/dist/connectors/outlook-mail/index.js +4 -0
- package/dist/connectors/outlook-mail/index.js.map +1 -0
- package/dist/connectors/outlook-mail/tools.d.ts +8 -0
- package/dist/connectors/outlook-mail/tools.js +236 -0
- package/dist/connectors/outlook-mail/tools.js.map +1 -0
- package/dist/connectors/salesforce/connector.json +122 -0
- package/dist/connectors/salesforce/index.d.ts +8 -0
- package/dist/connectors/salesforce/index.js +7 -0
- package/dist/connectors/salesforce/index.js.map +1 -0
- package/dist/connectors/salesforce/soql.d.ts +3 -0
- package/dist/connectors/salesforce/soql.js +60 -0
- package/dist/connectors/salesforce/soql.js.map +1 -0
- package/dist/connectors/salesforce/tools.d.ts +9 -0
- package/dist/connectors/salesforce/tools.js +234 -0
- package/dist/connectors/salesforce/tools.js.map +1 -0
- package/dist/connectors/salesforce/vendor.d.ts +3 -0
- package/dist/connectors/salesforce/vendor.js +113 -0
- package/dist/connectors/salesforce/vendor.js.map +1 -0
- 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/connector.json +194 -0
- package/dist/connectors/sharepoint-files/excel.d.ts +16 -0
- package/dist/connectors/sharepoint-files/excel.js +300 -0
- package/dist/connectors/sharepoint-files/excel.js.map +1 -0
- package/dist/connectors/sharepoint-files/index.d.ts +8 -0
- package/dist/connectors/sharepoint-files/index.js +12 -0
- package/dist/connectors/sharepoint-files/index.js.map +1 -0
- package/dist/connectors/sharepoint-files/tools.d.ts +52 -0
- package/dist/connectors/sharepoint-files/tools.js +365 -0
- package/dist/connectors/sharepoint-files/tools.js.map +1 -0
- package/dist/connectors/sharepoint-files/vendor.d.ts +11 -0
- package/dist/connectors/sharepoint-files/vendor.js +167 -0
- package/dist/connectors/sharepoint-files/vendor.js.map +1 -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/connector.json +298 -0
- package/dist/connectors/teams/index.d.ts +2 -0
- package/dist/connectors/teams/index.js +5 -0
- package/dist/connectors/teams/index.js.map +1 -0
- package/dist/connectors/teams/tools.d.ts +7 -0
- package/dist/connectors/teams/tools.js +392 -0
- package/dist/connectors/teams/tools.js.map +1 -0
- 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/branding.js +102 -0
- package/dist/packages/connector-kit/src/branding.js.map +1 -0
- package/dist/packages/connector-kit/src/certificate.d.ts +19 -0
- package/dist/packages/connector-kit/src/certificate.js +84 -0
- package/dist/packages/connector-kit/src/certificate.js.map +1 -0
- package/dist/packages/connector-kit/src/common.d.ts +36 -0
- package/dist/packages/connector-kit/src/common.js +110 -0
- package/dist/packages/connector-kit/src/common.js.map +1 -0
- package/dist/packages/connector-kit/src/connector.d.ts +1356 -0
- package/dist/packages/connector-kit/src/connector.js +1073 -0
- package/dist/packages/connector-kit/src/connector.js.map +1 -0
- 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 +98 -0
- package/dist/packages/connector-kit/src/oauth.js.map +1 -0
- package/dist/packages/connector-kit/src/types.d.ts +87 -0
- package/dist/packages/connector-kit/src/types.js +20 -0
- package/dist/packages/connector-kit/src/types.js.map +1 -0
- package/dist/packages/server/migrations/0001_tables.sql +268 -0
- package/dist/packages/server/migrations/0002_organizations.sql +72 -0
- package/dist/packages/server/migrations/0003_drop_unused.sql +3 -0
- package/dist/packages/server/migrations/0004_trigger_runners.sql +15 -0
- package/dist/packages/server/migrations/0005_run_runner.sql +3 -0
- package/dist/packages/server/migrations/0006_approvals.sql +36 -0
- package/dist/packages/server/migrations/0007_app_events.sql +110 -0
- package/dist/packages/server/migrations/rollback/to-legacy.sql +90 -0
- package/dist/packages/server/src/api.d.ts +3730 -0
- package/dist/packages/server/src/api.js +820 -0
- package/dist/packages/server/src/api.js.map +1 -0
- package/dist/packages/server/src/app.d.ts +23 -0
- package/dist/packages/server/src/app.js +467 -0
- package/dist/packages/server/src/app.js.map +1 -0
- package/dist/packages/server/src/builtin.d.ts +5 -0
- package/dist/packages/server/src/builtin.js +42 -0
- package/dist/packages/server/src/builtin.js.map +1 -0
- package/dist/packages/server/src/consent.d.ts +27 -0
- package/dist/packages/server/src/consent.js +36 -0
- package/dist/packages/server/src/consent.js.map +1 -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 +404 -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 +186 -0
- package/dist/packages/server/src/index.js.map +1 -0
- package/dist/packages/server/src/migrate.d.ts +11 -0
- package/dist/packages/server/src/migrate.js +134 -0
- package/dist/packages/server/src/migrate.js.map +1 -0
- package/dist/packages/server/src/openapi.d.ts +4 -0
- package/dist/packages/server/src/openapi.js +101 -0
- package/dist/packages/server/src/openapi.js.map +1 -0
- 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/secrets.js +76 -0
- package/dist/packages/server/src/secrets.js.map +1 -0
- package/dist/packages/server/src/server.d.ts +1 -0
- package/dist/packages/server/src/server.js +82 -0
- package/dist/packages/server/src/server.js.map +1 -0
- package/dist/packages/server/src/service.d.ts +767 -0
- package/dist/packages/server/src/service.js +924 -0
- package/dist/packages/server/src/service.js.map +1 -0
- package/dist/packages/server/src/store.d.ts +157 -0
- package/dist/packages/server/src/store.js +415 -0
- package/dist/packages/server/src/store.js.map +1 -0
- package/dist/packages/server/src/teams/activity.d.ts +123 -0
- package/dist/packages/server/src/teams/activity.js +188 -0
- package/dist/packages/server/src/teams/activity.js.map +1 -0
- package/dist/packages/server/src/teams/admin.d.ts +335 -0
- package/dist/packages/server/src/teams/admin.js +699 -0
- package/dist/packages/server/src/teams/admin.js.map +1 -0
- package/dist/packages/server/src/teams/approval.d.ts +57 -0
- package/dist/packages/server/src/teams/approval.js +118 -0
- package/dist/packages/server/src/teams/approval.js.map +1 -0
- package/dist/packages/server/src/teams/bot.d.ts +25 -0
- package/dist/packages/server/src/teams/bot.js +124 -0
- package/dist/packages/server/src/teams/bot.js.map +1 -0
- package/dist/packages/server/src/teams/claude.d.ts +46 -0
- package/dist/packages/server/src/teams/claude.js +165 -0
- package/dist/packages/server/src/teams/claude.js.map +1 -0
- package/dist/packages/server/src/teams/jwt.d.ts +17 -0
- package/dist/packages/server/src/teams/jwt.js +121 -0
- package/dist/packages/server/src/teams/jwt.js.map +1 -0
- package/dist/packages/server/src/teams/package.d.ts +79 -0
- package/dist/packages/server/src/teams/package.js +319 -0
- package/dist/packages/server/src/teams/package.js.map +1 -0
- package/dist/packages/server/src/teams/provision.d.ts +84 -0
- package/dist/packages/server/src/teams/provision.js +505 -0
- package/dist/packages/server/src/teams/provision.js.map +1 -0
- package/dist/packages/server/src/teams/runner.d.ts +80 -0
- package/dist/packages/server/src/teams/runner.js +187 -0
- package/dist/packages/server/src/teams/runner.js.map +1 -0
- package/dist/packages/server/src/teams/runtime.d.ts +91 -0
- package/dist/packages/server/src/teams/runtime.js +679 -0
- package/dist/packages/server/src/teams/runtime.js.map +1 -0
- package/dist/packages/server/src/teams/store.d.ts +233 -0
- package/dist/packages/server/src/teams/store.js +507 -0
- package/dist/packages/server/src/teams/store.js.map +1 -0
- package/package.json +68 -4
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { fillBrandDeep } from "./branding.js";
|
|
4
|
+
// A connector describes one app agents can use: how an admin connects it, the capabilities
|
|
5
|
+
// they can grant (each a set of tools), the value limits a grant can carry, and how a
|
|
6
|
+
// connection is checked. Each app is a folder in connectors/: its connector.json, and the
|
|
7
|
+
// ConnectorModule (index.ts) with its vendor client and tools.
|
|
8
|
+
const text = z.string().min(1);
|
|
9
|
+
const slug = z.string().regex(/^[a-z][a-z0-9_]*$/);
|
|
10
|
+
const settingSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
id: z.string().regex(/^[a-z][A-Za-z]+$/),
|
|
13
|
+
label: text,
|
|
14
|
+
help: text.optional(),
|
|
15
|
+
placeholder: text.optional(),
|
|
16
|
+
// The whole value must match.
|
|
17
|
+
pattern: text,
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
// One option of a choice field (`options`), e.g. add_to_teams = on.
|
|
21
|
+
const chosenSchema = z.object({ field: slug, value: slug }).strict();
|
|
22
|
+
const stepSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
title: text,
|
|
25
|
+
body: text,
|
|
26
|
+
link: z.object({ label: text, href: z.url() }).strict().optional(),
|
|
27
|
+
// A line containing {scope} repeats once per scope; {scopes} becomes them space-separated.
|
|
28
|
+
copy: z.array(text).optional(),
|
|
29
|
+
// The shell `copy` runs in, named above the commands.
|
|
30
|
+
shell: z.enum(["bash", "powershell"]).optional(),
|
|
31
|
+
// Lists the in-app permissions the ticked capabilities need, e.g. Jira project permissions.
|
|
32
|
+
permissions: z.boolean().optional(),
|
|
33
|
+
// Shows the certificate the service made for the provider connection, to download.
|
|
34
|
+
certificate: z.boolean().optional(),
|
|
35
|
+
// The same step in the vendor's admin portals, one screen or choice per item. A step with
|
|
36
|
+
// both `click` and `copy` is offered both ways.
|
|
37
|
+
click: z.array(text).min(1).optional(),
|
|
38
|
+
// The vendor has no portal page for this step, only commands (`copy`): the admin page says
|
|
39
|
+
// so in place of clicks. Never with `click`, so a step cannot pass as clickable by a
|
|
40
|
+
// "use the commands" line.
|
|
41
|
+
clickUnavailable: z.literal(true).optional(),
|
|
42
|
+
// A site grant step shown only while the site role the ceiling needs is this one, e.g.
|
|
43
|
+
// raising a read grant to write.
|
|
44
|
+
siteRole: z.enum(["read", "write"]).optional(),
|
|
45
|
+
// Shows the live check of the agent's own address (`agentAccess`) after this step.
|
|
46
|
+
agentCheck: z.boolean().optional(),
|
|
47
|
+
// The service cannot check this step live, so the admin marks it done. Steps of different apps
|
|
48
|
+
// with the same id share that mark (e.g. one group both Outlook apps use).
|
|
49
|
+
confirm: z
|
|
50
|
+
.string()
|
|
51
|
+
.regex(/^[a-z0-9-]+$/)
|
|
52
|
+
.optional(),
|
|
53
|
+
// Checked live by the app's own setup check under this id (e.g. Teams' Azure role), not by
|
|
54
|
+
// the permissions check.
|
|
55
|
+
live: z
|
|
56
|
+
.string()
|
|
57
|
+
.regex(/^[a-z0-9-]+$/)
|
|
58
|
+
.optional(),
|
|
59
|
+
// Values the admin enters at this step, kept in the provider connection's config under
|
|
60
|
+
// their ids and filled into steps as {id} (e.g. the Azure subscription for Teams bots).
|
|
61
|
+
settings: z.array(settingSchema).min(1).optional(),
|
|
62
|
+
// Shown, and holding back turning on, only while this option is chosen (e.g. Teams' Azure
|
|
63
|
+
// steps only while the service makes the bots).
|
|
64
|
+
option: chosenSchema.optional(),
|
|
65
|
+
})
|
|
66
|
+
.strict();
|
|
67
|
+
// A step of adding one more agent on a provider's page, shown while one of `apps` is on.
|
|
68
|
+
const agentStepSchema = stepSchema
|
|
69
|
+
.extend({ apps: z.array(z.string().regex(/^[a-z0-9-]+$/)).min(1) })
|
|
70
|
+
.strict();
|
|
71
|
+
const clickRule = [
|
|
72
|
+
(s) => !s.clickUnavailable || (!s.click && Boolean(s.copy)),
|
|
73
|
+
{ message: "A step with clickUnavailable has commands (copy) and no click" },
|
|
74
|
+
];
|
|
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);
|
|
83
|
+
const agentStep = agentStepSchema.refine(...clickRule);
|
|
84
|
+
// A field with `options` is a choice (the first is the default), stored in the app's config
|
|
85
|
+
// under its id. An option's `scopes` are vendor permissions the app then needs too, e.g.
|
|
86
|
+
// reading every site.
|
|
87
|
+
const optionSchema = z
|
|
88
|
+
.object({ value: slug, label: text, description: text, scopes: z.array(text).optional() })
|
|
89
|
+
.strict();
|
|
90
|
+
const fieldSchema = z
|
|
91
|
+
.object({
|
|
92
|
+
id: slug,
|
|
93
|
+
label: text,
|
|
94
|
+
help: text.optional(),
|
|
95
|
+
placeholder: text.optional(),
|
|
96
|
+
secret: z.boolean(),
|
|
97
|
+
options: z.array(optionSchema).min(2).optional(),
|
|
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).
|
|
100
|
+
requires: chosenSchema.optional(),
|
|
101
|
+
// May be left empty.
|
|
102
|
+
optional: z.boolean().default(false),
|
|
103
|
+
})
|
|
104
|
+
.strict();
|
|
105
|
+
// "map": one optional value per value of the list limit `keys`, e.g. a folder per site.
|
|
106
|
+
const limitSchema = z
|
|
107
|
+
.object({
|
|
108
|
+
id: slug,
|
|
109
|
+
label: text,
|
|
110
|
+
help: text,
|
|
111
|
+
kind: z.enum(["list", "text", "map"]),
|
|
112
|
+
required: z.boolean(),
|
|
113
|
+
// Each value must match this, e.g. a Jira project key or a mailbox address.
|
|
114
|
+
pattern: z.string().optional(),
|
|
115
|
+
keys: slug.optional(),
|
|
116
|
+
})
|
|
117
|
+
.strict()
|
|
118
|
+
.refine((l) => (l.kind === "map") === Boolean(l.keys), {
|
|
119
|
+
message: "A map limit names the list limit it is keyed by (keys), and only it",
|
|
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) : "";
|
|
123
|
+
const httpProbeSchema = z
|
|
124
|
+
.object({
|
|
125
|
+
method: z.enum(["GET", "POST"]),
|
|
126
|
+
path: z.string().startsWith("/"),
|
|
127
|
+
label: text,
|
|
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(),
|
|
132
|
+
// Keeps only list items whose fields equal these values, e.g. objects agents can query.
|
|
133
|
+
match: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
134
|
+
body: z.record(z.string(), z.unknown()).optional(),
|
|
135
|
+
// Shown to the admin when the probe finds nothing, e.g. a missing project permission.
|
|
136
|
+
ifEmpty: text.optional(),
|
|
137
|
+
})
|
|
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
|
+
});
|
|
142
|
+
// A check no single API call answers, e.g. the permissions inside a token. The vendor client
|
|
143
|
+
// implements it under this name (Vendor.checks).
|
|
144
|
+
const vendorProbeSchema = z
|
|
145
|
+
.object({
|
|
146
|
+
label: text,
|
|
147
|
+
check: slug,
|
|
148
|
+
ifEmpty: text.optional(),
|
|
149
|
+
})
|
|
150
|
+
.strict();
|
|
151
|
+
const probeSchema = z.union([httpProbeSchema, vendorProbeSchema]);
|
|
152
|
+
// An app described entirely here, e.g. a company's internal REST API: the generic http module
|
|
153
|
+
// (connectors/http) runs it, so it needs no code. Auth values are the ids of connect form fields.
|
|
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 = [
|
|
158
|
+
z
|
|
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
|
+
})
|
|
166
|
+
.strict(),
|
|
167
|
+
z.object({ type: z.literal("bearer"), token: slug }).strict(),
|
|
168
|
+
z.object({ type: z.literal("basic"), username: slug, password: slug }).strict(),
|
|
169
|
+
];
|
|
170
|
+
const httpAuthSchema = z.discriminatedUnion("type", [
|
|
171
|
+
...keyAuths,
|
|
172
|
+
z
|
|
173
|
+
.object({
|
|
174
|
+
type: z.literal("oauth2"),
|
|
175
|
+
tokenUrl: z.url({ protocol: /^https?$/ }),
|
|
176
|
+
clientId: slug,
|
|
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,
|
|
191
|
+
})
|
|
192
|
+
.strict(),
|
|
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;
|
|
215
|
+
const httpParamSchema = z
|
|
216
|
+
.object({
|
|
217
|
+
type: z.enum(["string", "number", "integer", "boolean", "array"]),
|
|
218
|
+
description: text,
|
|
219
|
+
// Where it goes; a {name} in the path template is always "path".
|
|
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(),
|
|
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(),
|
|
235
|
+
// The value must be one of the agent's values of this list limit, while it has any.
|
|
236
|
+
limit: slug.optional(),
|
|
237
|
+
})
|
|
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
|
+
]);
|
|
270
|
+
const httpToolSchema = z
|
|
271
|
+
.object({
|
|
272
|
+
name: slug,
|
|
273
|
+
description: text,
|
|
274
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
275
|
+
// Under baseUrl, with {param} for path params, e.g. /orders/{id}.
|
|
276
|
+
path: z.string().regex(/^\/[^?#]*$/),
|
|
277
|
+
params: z.record(slug, httpParamSchema).default({}),
|
|
278
|
+
// Deletes or overwrites: its capability must be destructive. DELETE always is.
|
|
279
|
+
destructive: z.boolean().default(false),
|
|
280
|
+
// Checked in the answer, for calls whose params cannot be limited (e.g. an order by id):
|
|
281
|
+
// while the agent has values of `limit`, the answer's `field` (a dot path; every item of a
|
|
282
|
+
// list) must be one of them, else nothing is returned. A missing field is refused.
|
|
283
|
+
answerLimits: z.array(z.object({ field: dotPath, limit: slug }).strict()).default([]),
|
|
284
|
+
result: httpResultSchema.optional(),
|
|
285
|
+
paging: httpPagingSchema.optional(),
|
|
286
|
+
})
|
|
287
|
+
.strict()
|
|
288
|
+
.refine((t) => {
|
|
289
|
+
const inPath = [...t.path.matchAll(/\{([a-z][a-z0-9_]*)\}/g)].map((m) => m[1]);
|
|
290
|
+
const declared = Object.entries(t.params).filter(([, p]) => p.in === "path");
|
|
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
|
+
})
|
|
298
|
+
.refine((t) => !Object.values(t.params).some((p) => p.in === "body") || t.method !== "GET", {
|
|
299
|
+
message: "A GET tool has no body params",
|
|
300
|
+
})
|
|
301
|
+
// Checked after the call, so only for reads: a write would already have happened.
|
|
302
|
+
.refine((t) => !t.answerLimits.length || t.method === "GET", {
|
|
303
|
+
message: "answerLimits are for GET tools only",
|
|
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;
|
|
326
|
+
const httpSchema = z
|
|
327
|
+
.object({
|
|
328
|
+
// Every call goes to this origin, under this path, and nowhere else: redirects are refused.
|
|
329
|
+
baseUrl: z.url({ protocol: /^https?$/ }).refine((u) => !/[?#]/.test(u), "No query or fragment"),
|
|
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({}),
|
|
333
|
+
tools: z.array(httpToolSchema).min(1),
|
|
334
|
+
// Plain http for baseUrl and the token URL, e.g. an app inside the cluster. Off by default.
|
|
335
|
+
allowInsecureHttp: z.boolean().default(false),
|
|
336
|
+
})
|
|
337
|
+
.strict()
|
|
338
|
+
.refine((h) => h.allowInsecureHttp ||
|
|
339
|
+
[h.baseUrl, ...(h.auth.type === "oauth2" ? [h.auth.tokenUrl] : [])].every((u) => u.startsWith("https://")), { message: "baseUrl and tokenUrl must be https unless allowInsecureHttp is true" })
|
|
340
|
+
.refine((h) => Object.keys(h.headers).every((name) => !RESERVED_HEADERS.has(name.toLowerCase()) &&
|
|
341
|
+
!CREDENTIAL_LIKE.test(name) &&
|
|
342
|
+
!(h.auth.type === "header" && h.auth.header.toLowerCase() === name.toLowerCase())), {
|
|
343
|
+
message: "headers are not secret, so they cannot carry credentials (use auth) or headers the service sets",
|
|
344
|
+
})
|
|
345
|
+
.refine((h) => h.auth.type !== "query" ||
|
|
346
|
+
h.tools.every((t) => Object.entries(t.params).every(([k, p]) => p.in !== "query" || (p.name ?? k) !== h.auth.param)), { message: "No tool param may be sent as the query auth param" });
|
|
347
|
+
const answerLimitsSchema = z.array(z.object({ field: dotPath, limit: slug }).strict()).default([]);
|
|
348
|
+
const secure = (allowInsecure, ...urls) => allowInsecure || urls.every((u) => u.startsWith("https://"));
|
|
349
|
+
// A SOAP service described here, run by connectors/soap. Each tool is one operation: its Body
|
|
350
|
+
// content is a template whose {param} placeholders are filled with XML-escaped values.
|
|
351
|
+
const soapParamSchema = z
|
|
352
|
+
.object({
|
|
353
|
+
type: z.enum(["string", "number", "integer", "boolean"]),
|
|
354
|
+
description: text,
|
|
355
|
+
// Left out, an optional param's element (`<x>{param}</x>`) is left out of the body.
|
|
356
|
+
required: z.boolean().default(true),
|
|
357
|
+
limit: slug.optional(),
|
|
358
|
+
})
|
|
359
|
+
.strict();
|
|
360
|
+
const PLACEHOLDER = /\{([a-z][a-z0-9_]*)\}/g;
|
|
361
|
+
const xmlName = z.string().regex(/^[A-Za-z_][\w.-]*$/);
|
|
362
|
+
const soapToolSchema = z
|
|
363
|
+
.object({
|
|
364
|
+
name: slug,
|
|
365
|
+
description: text,
|
|
366
|
+
operation: xmlName,
|
|
367
|
+
// Sent as the SOAPAction header (1.1) or the content type's action (1.2).
|
|
368
|
+
soapAction: z
|
|
369
|
+
.string()
|
|
370
|
+
.regex(/^[^"\r\n]*$/)
|
|
371
|
+
.optional(),
|
|
372
|
+
body: text,
|
|
373
|
+
params: z.record(slug, soapParamSchema).default({}),
|
|
374
|
+
// Dot path in the Body's content to the answer, default `<operation>Response`; ending `[]`
|
|
375
|
+
// makes it a list even when the service answers one item.
|
|
376
|
+
result: z
|
|
377
|
+
.string()
|
|
378
|
+
.regex(/^[A-Za-z_][\w.-]*(\.[A-Za-z_][\w.-]*)*(\[\])?$/)
|
|
379
|
+
.optional(),
|
|
380
|
+
// Only these fields of the answer (of each item, for a list) reach the agent.
|
|
381
|
+
fields: z.array(xmlName).min(1).optional(),
|
|
382
|
+
// Changes nothing (a lookup): only these can have answerLimits.
|
|
383
|
+
readOnly: z.boolean().default(false),
|
|
384
|
+
destructive: z.boolean().default(false),
|
|
385
|
+
answerLimits: answerLimitsSchema,
|
|
386
|
+
})
|
|
387
|
+
.strict()
|
|
388
|
+
.refine((t) => {
|
|
389
|
+
const used = [...t.body.matchAll(PLACEHOLDER)].map((m) => m[1]);
|
|
390
|
+
return (new Set(used).size === used.length &&
|
|
391
|
+
used.every((n) => n in t.params) &&
|
|
392
|
+
Object.keys(t.params).every((n) => used.includes(n)));
|
|
393
|
+
}, { message: "Each param is a {param} placeholder in the tool's body, once, and only those" })
|
|
394
|
+
.refine((t) => Object.entries(t.params).every(([n, p]) => p.required || new RegExp(`<([\\w.:-]+)(\\s[^>]*)?>\\{${n}\\}</\\1>`).test(t.body)), { message: "An optional param's placeholder is the whole content of an element" })
|
|
395
|
+
.refine((t) => !t.answerLimits.length || t.readOnly, {
|
|
396
|
+
message: "answerLimits are for readOnly tools only",
|
|
397
|
+
})
|
|
398
|
+
.refine((t) => !(t.readOnly && t.destructive), {
|
|
399
|
+
message: "A readOnly tool cannot be destructive",
|
|
400
|
+
});
|
|
401
|
+
const soapSchema = z
|
|
402
|
+
.object({
|
|
403
|
+
endpoint: z.url({ protocol: /^https?$/ }).refine((u) => !u.includes("#"), "No fragment"),
|
|
404
|
+
version: z.enum(["1.1", "1.2"]).default("1.1"),
|
|
405
|
+
// Prefixes declared on the Envelope for the tools' bodies, e.g. {"hr": "urn:example:hr"}.
|
|
406
|
+
namespaces: z
|
|
407
|
+
.record(xmlName.refine((p) => !["soap", "wsse", "wsu", "xml", "xmlns"].includes(p), "Reserved"), z.string().regex(/^[^"<>&\s]+$/))
|
|
408
|
+
.default({}),
|
|
409
|
+
auth: z.discriminatedUnion("type", [...keyAuths]).optional(),
|
|
410
|
+
// WS-Security UsernameToken in the SOAP header, with or without other auth.
|
|
411
|
+
wsSecurity: z
|
|
412
|
+
.object({ username: slug, password: slug, digest: z.boolean().default(false) })
|
|
413
|
+
.strict()
|
|
414
|
+
.optional(),
|
|
415
|
+
// The call the connector's `operation` check makes: a tool, its arguments, what to show.
|
|
416
|
+
verify: z
|
|
417
|
+
.object({
|
|
418
|
+
tool: slug,
|
|
419
|
+
args: z.record(slug, z.union([z.string(), z.number(), z.boolean()])).default({}),
|
|
420
|
+
show: text,
|
|
421
|
+
})
|
|
422
|
+
.strict()
|
|
423
|
+
.optional(),
|
|
424
|
+
tools: z.array(soapToolSchema).min(1),
|
|
425
|
+
allowInsecureHttp: z.boolean().default(false),
|
|
426
|
+
})
|
|
427
|
+
.strict()
|
|
428
|
+
.refine((s) => s.auth || s.wsSecurity, { message: "A soap app needs auth or wsSecurity" })
|
|
429
|
+
.refine((s) => secure(s.allowInsecureHttp, s.endpoint), {
|
|
430
|
+
message: "endpoint must be https unless allowInsecureHttp is true",
|
|
431
|
+
})
|
|
432
|
+
.refine((s) => !s.verify || s.tools.some((t) => t.name === s.verify.tool), {
|
|
433
|
+
message: "verify.tool names one of the soap tools",
|
|
434
|
+
});
|
|
435
|
+
// An OData service (v2 or v4: SAP Gateway, Dynamics, SuccessFactors) described here, run by
|
|
436
|
+
// connectors/odata. Agents never write OData syntax: values are typed and made into literals.
|
|
437
|
+
const odataName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_.]*$/);
|
|
438
|
+
const odataValueSchema = z
|
|
439
|
+
.object({
|
|
440
|
+
type: z.enum([
|
|
441
|
+
"string",
|
|
442
|
+
"integer",
|
|
443
|
+
"number",
|
|
444
|
+
"decimal",
|
|
445
|
+
"boolean",
|
|
446
|
+
"guid",
|
|
447
|
+
"datetime",
|
|
448
|
+
"datetimeoffset",
|
|
449
|
+
]),
|
|
450
|
+
description: text,
|
|
451
|
+
// The OData property (or function parameter) name, default the param's own. A field can
|
|
452
|
+
// bind a lookup, e.g. Dynamics' parentaccountid@odata.bind.
|
|
453
|
+
property: z
|
|
454
|
+
.string()
|
|
455
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_.]*(@odata\.bind)?$/)
|
|
456
|
+
.optional(),
|
|
457
|
+
// Keys are always required; fields default to required on create, never on update.
|
|
458
|
+
required: z.boolean().optional(),
|
|
459
|
+
limit: slug.optional(),
|
|
460
|
+
})
|
|
461
|
+
.strict();
|
|
462
|
+
const odataFilterSchema = odataValueSchema
|
|
463
|
+
.extend({
|
|
464
|
+
op: z.enum(["eq", "ne", "gt", "ge", "lt", "le", "contains", "startswith"]).default("eq"),
|
|
465
|
+
})
|
|
466
|
+
.strict()
|
|
467
|
+
.refine((f) => !f.limit || f.op === "eq", { message: "Only an eq filter can have a limit" })
|
|
468
|
+
.refine((f) => !["contains", "startswith"].includes(f.op) || f.type === "string", {
|
|
469
|
+
message: "contains and startswith filter strings",
|
|
470
|
+
});
|
|
471
|
+
const ODATA_KINDS = {
|
|
472
|
+
list: { on: "entitySet", parts: ["filters"] },
|
|
473
|
+
get: { on: "entitySet", parts: ["key"] },
|
|
474
|
+
create: { on: "entitySet", parts: ["fields"] },
|
|
475
|
+
update: { on: "entitySet", parts: ["key", "fields"] },
|
|
476
|
+
function: { on: "function", parts: ["params"] },
|
|
477
|
+
action: { on: "function", parts: ["params"] },
|
|
478
|
+
};
|
|
479
|
+
const odataToolSchema = z
|
|
480
|
+
.object({
|
|
481
|
+
name: slug,
|
|
482
|
+
description: text,
|
|
483
|
+
kind: z.enum(["list", "get", "create", "update", "function", "action"]),
|
|
484
|
+
entitySet: odataName.optional(),
|
|
485
|
+
// A function or action import's name.
|
|
486
|
+
function: odataName.optional(),
|
|
487
|
+
key: z.record(slug, odataValueSchema).default({}),
|
|
488
|
+
filters: z.record(slug, odataFilterSchema).default({}),
|
|
489
|
+
fields: z.record(slug, odataValueSchema).default({}),
|
|
490
|
+
params: z.record(slug, odataValueSchema).default({}),
|
|
491
|
+
// $select, and the only properties passed back.
|
|
492
|
+
select: z.array(odataName).default([]),
|
|
493
|
+
orderBy: z
|
|
494
|
+
.string()
|
|
495
|
+
.regex(/^[A-Za-z_][\w./]*( (asc|desc))?(,[A-Za-z_][\w./]*( (asc|desc))?)*$/)
|
|
496
|
+
.optional(),
|
|
497
|
+
// Most items per page: the agent's max_results is capped at it.
|
|
498
|
+
top: z.number().int().min(1).max(1000).default(50),
|
|
499
|
+
destructive: z.boolean().default(false),
|
|
500
|
+
answerLimits: answerLimitsSchema,
|
|
501
|
+
})
|
|
502
|
+
.strict()
|
|
503
|
+
.refine((t) => {
|
|
504
|
+
const kind = ODATA_KINDS[t.kind];
|
|
505
|
+
const parts = ["key", "filters", "fields", "params"];
|
|
506
|
+
return (Boolean(t[kind.on]) &&
|
|
507
|
+
!t[kind.on === "entitySet" ? "function" : "entitySet"] &&
|
|
508
|
+
parts.every((p) => kind.parts.includes(p) || !Object.keys(t[p]).length) &&
|
|
509
|
+
(!kind.parts.includes("key") || Object.keys(t.key).length > 0) &&
|
|
510
|
+
(t.kind !== "create" || Object.keys(t.fields).length > 0) &&
|
|
511
|
+
(t.kind === "list" || !t.orderBy) &&
|
|
512
|
+
(kind.on !== "function" || !t.select.length));
|
|
513
|
+
}, {
|
|
514
|
+
message: "A list names an entitySet and filters, get a key, create fields, update a key and fields, function and action a function and params; orderBy is for lists, select not for functions",
|
|
515
|
+
})
|
|
516
|
+
.refine((t) => !["create", "action"].includes(t.kind) || !t.answerLimits.length, {
|
|
517
|
+
message: "answerLimits are for reads and updates (checked before the change)",
|
|
518
|
+
})
|
|
519
|
+
.refine((t) => {
|
|
520
|
+
const names = [t.key, t.filters, t.fields, t.params].flatMap(Object.keys);
|
|
521
|
+
return (new Set(names).size === names.length &&
|
|
522
|
+
!names.some((n) => n === "cursor" || n === "max_results"));
|
|
523
|
+
}, { message: "Param names are unique in a tool, and cursor and max_results are taken" })
|
|
524
|
+
.refine((t) => !t.select.length ||
|
|
525
|
+
Object.entries(t.filters).every(([n, f]) => !f.limit || t.select.includes(f.property ?? n)), { message: "A limited filter's property must be selected, so answers can be checked" });
|
|
526
|
+
const odataSchema = z
|
|
527
|
+
.object({
|
|
528
|
+
// The service root, e.g. https://host/sap/opu/odata/sap/API_BUSINESS_PARTNER.
|
|
529
|
+
serviceRoot: z
|
|
530
|
+
.url({ protocol: /^https?$/ })
|
|
531
|
+
.refine((u) => !/[?#]/.test(u), "No query or fragment"),
|
|
532
|
+
version: z.enum(["2", "4"]),
|
|
533
|
+
auth: httpAuthSchema,
|
|
534
|
+
// Sent on every call, e.g. SAP's sap-client. Not secret: never credentials.
|
|
535
|
+
headers: z
|
|
536
|
+
.record(headerName.refine((h) => !RESERVED_HEADERS.has(h.toLowerCase()) &&
|
|
537
|
+
!CREDENTIAL_LIKE.test(h) &&
|
|
538
|
+
!/^(accept|if-match|x-http-method|odata-.*|(max)?dataserviceversion|prefer)$/i.test(h), "Set by the service, or a credential (use auth)"), headerValue)
|
|
539
|
+
.default({}),
|
|
540
|
+
// Writes first fetch an x-csrf-token (SAP Gateway).
|
|
541
|
+
csrf: z.boolean().default(false),
|
|
542
|
+
// "skip": $top and $skip; "server": the service's own pages (odata.maxpagesize, v4), e.g.
|
|
543
|
+
// Dynamics, which has no $skip.
|
|
544
|
+
paging: z.enum(["skip", "server"]).default("skip"),
|
|
545
|
+
tools: z.array(odataToolSchema).min(1),
|
|
546
|
+
allowInsecureHttp: z.boolean().default(false),
|
|
547
|
+
})
|
|
548
|
+
.strict()
|
|
549
|
+
.refine((o) => secure(o.allowInsecureHttp, o.serviceRoot, ...(o.auth.type === "oauth2" ? [o.auth.tokenUrl] : [])), { message: "serviceRoot and tokenUrl must be https unless allowInsecureHttp is true" })
|
|
550
|
+
.refine((o) => o.paging === "skip" || o.version === "4", {
|
|
551
|
+
message: "Server paging is for OData v4",
|
|
552
|
+
});
|
|
553
|
+
// An app reached as a database, described entirely here: the generic sql module
|
|
554
|
+
// (connectors/sql) runs it. Each tool is one statement the company's developer wrote, with
|
|
555
|
+
// :name params bound by the driver, never pasted into the text.
|
|
556
|
+
const sqlParamSchema = z
|
|
557
|
+
.object({
|
|
558
|
+
type: z.enum(["string", "number", "integer", "boolean"]),
|
|
559
|
+
description: text,
|
|
560
|
+
// Left out, an optional param is bound as null.
|
|
561
|
+
required: z.boolean().default(true),
|
|
562
|
+
// The value must be one of the agent's values of this list limit, while it has any.
|
|
563
|
+
limit: slug.optional(),
|
|
564
|
+
})
|
|
565
|
+
.strict();
|
|
566
|
+
const sqlToolSchema = z
|
|
567
|
+
.object({
|
|
568
|
+
name: slug,
|
|
569
|
+
description: text,
|
|
570
|
+
sql: text,
|
|
571
|
+
params: z.record(slug, sqlParamSchema).default({}),
|
|
572
|
+
// Changes data: run read-write and committed, and never in a required capability. Any
|
|
573
|
+
// other tool runs in a read-only transaction that is rolled back.
|
|
574
|
+
write: z.boolean().default(false),
|
|
575
|
+
// Deletes or overwrites: its capability must be destructive.
|
|
576
|
+
destructive: z.boolean().default(false),
|
|
577
|
+
// While the agent has values of `limit`, every row's `column` must be one of them, else
|
|
578
|
+
// nothing is returned (a write is rolled back). A missing column is refused.
|
|
579
|
+
answerLimits: z.array(z.object({ column: text, limit: slug }).strict()).default([]),
|
|
580
|
+
})
|
|
581
|
+
.strict()
|
|
582
|
+
.refine((t) => !t.destructive || t.write, { message: "A destructive tool is a write" });
|
|
583
|
+
const sqlSchema = z
|
|
584
|
+
.object({
|
|
585
|
+
engine: z.enum(["postgres", "sqlserver", "mysql", "oracle"]),
|
|
586
|
+
host: z.string().regex(/^[A-Za-z0-9._-]+$/),
|
|
587
|
+
// The engine's usual port when left out.
|
|
588
|
+
port: z.number().int().min(1).max(65535).optional(),
|
|
589
|
+
// Oracle: the service name.
|
|
590
|
+
database: text,
|
|
591
|
+
// Ids of connect form fields: user a plain one, password a secret one.
|
|
592
|
+
auth: z.object({ user: slug, password: slug }).strict(),
|
|
593
|
+
// "verify": encrypted, the server's certificate checked; "insecure": encrypted, not
|
|
594
|
+
// checked; "off": plain, e.g. a database on the same private network.
|
|
595
|
+
tls: z.enum(["verify", "insecure", "off"]).default("verify"),
|
|
596
|
+
timeoutSeconds: z.number().int().min(1).max(300).default(30),
|
|
597
|
+
// Rows returned per call; more are cut off and the answer says so.
|
|
598
|
+
maxRows: z.number().int().min(1).max(1000).default(200),
|
|
599
|
+
tools: z.array(sqlToolSchema).min(1),
|
|
600
|
+
})
|
|
601
|
+
.strict();
|
|
602
|
+
// Events the app can send (or a poll can read) that start agents (packages/server/src/events,
|
|
603
|
+
// docs/triggers.md). A path is `a.b`, or `a[].b` for every item of a list.
|
|
604
|
+
const eventPath = z.string().regex(/^[A-Za-z0-9_$@-]+(\[\])?(\.[A-Za-z0-9_$@-]+(\[\])?)*$/);
|
|
605
|
+
const eventId = z.string().regex(/^[A-Za-z0-9_.:-]{1,100}$/);
|
|
606
|
+
// Where a value is in a delivery: a path in its JSON body, or a request header.
|
|
607
|
+
const locator = z.union([
|
|
608
|
+
z.object({ field: eventPath }).strict(),
|
|
609
|
+
z.object({ header: headerName }).strict(),
|
|
610
|
+
]);
|
|
611
|
+
const eventRequest = z
|
|
612
|
+
.object({
|
|
613
|
+
method: z.enum(["GET", "POST", "PUT"]),
|
|
614
|
+
path: z.string().startsWith("/"),
|
|
615
|
+
body: z.unknown().optional(),
|
|
616
|
+
})
|
|
617
|
+
.strict();
|
|
618
|
+
const eventsSchema = z
|
|
619
|
+
.object({
|
|
620
|
+
types: z.array(z.object({ id: eventId, label: text }).strict()).min(1),
|
|
621
|
+
// What an agent is given of each event, and what conditions and limits read: name → path.
|
|
622
|
+
fields: z.record(slug, eventPath),
|
|
623
|
+
// A limit's id → the field holding the event's value for it. An agent with that limit set
|
|
624
|
+
// is started only by events whose value is within it.
|
|
625
|
+
limits: z.record(slug, slug).default({}),
|
|
626
|
+
webhook: z
|
|
627
|
+
.object({
|
|
628
|
+
// How a delivery proves it came from the app, with the subscription's secret: an
|
|
629
|
+
// HMAC-SHA256 of the body (hex, after `prefix`), the secret itself in a header, or
|
|
630
|
+
// nothing at all, only with allowInsecure.
|
|
631
|
+
verify: z.discriminatedUnion("type", [
|
|
632
|
+
z
|
|
633
|
+
.object({
|
|
634
|
+
type: z.literal("hmac-sha256"),
|
|
635
|
+
header: headerName,
|
|
636
|
+
prefix: z.string().max(20).default(""),
|
|
637
|
+
})
|
|
638
|
+
.strict(),
|
|
639
|
+
z.object({ type: z.literal("token"), header: headerName }).strict(),
|
|
640
|
+
z.object({ type: z.literal("none") }).strict(),
|
|
641
|
+
]),
|
|
642
|
+
allowInsecure: z.boolean().default(false),
|
|
643
|
+
type: locator,
|
|
644
|
+
// Same for every retry of one delivery; without one, the body's hash is used.
|
|
645
|
+
deliveryId: locator.optional(),
|
|
646
|
+
// When the app stamps deliveries: older (or newer) than the tolerance is refused.
|
|
647
|
+
timestamp: z
|
|
648
|
+
.object({
|
|
649
|
+
at: locator,
|
|
650
|
+
unit: z.enum(["s", "ms", "iso"]),
|
|
651
|
+
toleranceSeconds: z
|
|
652
|
+
.number()
|
|
653
|
+
.int()
|
|
654
|
+
.min(60)
|
|
655
|
+
.max(7 * 86400)
|
|
656
|
+
.default(300),
|
|
657
|
+
})
|
|
658
|
+
.strict()
|
|
659
|
+
.optional(),
|
|
660
|
+
// Where the admin pastes the address ({url}) and the secret.
|
|
661
|
+
setup: z.array(step).min(1),
|
|
662
|
+
// Registers the webhook through the app's API with the connection's credentials, filled
|
|
663
|
+
// with {name} {url} {secret} {events} {filter}. `location`: where the answer gives the
|
|
664
|
+
// webhook's own address.
|
|
665
|
+
register: eventRequest.extend({ location: dotPath }).strict().optional(),
|
|
666
|
+
})
|
|
667
|
+
.strict()
|
|
668
|
+
.refine((w) => w.verify.type !== "none" || w.allowInsecure, {
|
|
669
|
+
message: "A webhook verified by nothing needs allowInsecure",
|
|
670
|
+
})
|
|
671
|
+
.optional(),
|
|
672
|
+
// The account the app's connection acts as, so its own changes (an agent's comment) never
|
|
673
|
+
// start agents again: `request` answers its id at `id`; `fields` hold who made the change.
|
|
674
|
+
self: z
|
|
675
|
+
.object({ request: eventRequest, id: dotPath, fields: z.array(slug).min(1) })
|
|
676
|
+
.strict()
|
|
677
|
+
.optional(),
|
|
678
|
+
// For apps without webhooks: `request` read every interval, filled with {minutes} (since
|
|
679
|
+
// the cursor, rounded up, plus one) {cursor} and {filter} (the subscription's filter put
|
|
680
|
+
// into `filter`'s {value}, or nothing); `items` the list in the answer, each kept under
|
|
681
|
+
// `wrap` so the webhook's paths fit; `cursor` the item's change time; `deliveryId` the
|
|
682
|
+
// paths joined into an id; `types` the first whose `since` time is after the cursor the poll
|
|
683
|
+
// started from (or that has none): a record created since the last poll is new, whatever
|
|
684
|
+
// its change time says (Jira stamps an issue's update just after its creation).
|
|
685
|
+
poll: z
|
|
686
|
+
.object({
|
|
687
|
+
request: eventRequest,
|
|
688
|
+
filter: z.string().includes("{value}").optional(),
|
|
689
|
+
items: dotPath,
|
|
690
|
+
wrap: slug.optional(),
|
|
691
|
+
cursor: eventPath,
|
|
692
|
+
deliveryId: z.array(eventPath).min(1),
|
|
693
|
+
types: z
|
|
694
|
+
.array(z.object({ type: eventId, since: eventPath.optional() }).strict())
|
|
695
|
+
.min(1),
|
|
696
|
+
})
|
|
697
|
+
.strict()
|
|
698
|
+
.optional(),
|
|
699
|
+
})
|
|
700
|
+
.strict()
|
|
701
|
+
.refine((e) => e.webhook || e.poll, { message: "Events come from a webhook, a poll or both" })
|
|
702
|
+
.refine((e) => Object.values(e.limits).every((f) => f in e.fields) &&
|
|
703
|
+
(e.self?.fields ?? []).every((f) => f in e.fields) &&
|
|
704
|
+
(e.poll?.types ?? []).every((t) => e.types.some((x) => x.id === t.type)), { message: "Event limits and self name fields, and a poll's types are declared types" });
|
|
705
|
+
export const connectorSchema = z
|
|
706
|
+
.object({
|
|
707
|
+
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
708
|
+
name: text,
|
|
709
|
+
description: text,
|
|
710
|
+
// A path under the admin app's public folder.
|
|
711
|
+
logo: z.string().regex(/^\/brand\/[a-z0-9/-]+\.svg$/),
|
|
712
|
+
identity: text,
|
|
713
|
+
// "agentBot": calls go out as the agent's own identity (its Teams bot), not the provider's
|
|
714
|
+
// shared sign-in. A grant only gives tools once that agent has a connected bot.
|
|
715
|
+
signsInAs: z.enum(["agentBot"]).optional(),
|
|
716
|
+
// A provider connection (providers/<id>.json) this app signs in through, shared with the
|
|
717
|
+
// provider's other apps: its credentials are entered once, not per app.
|
|
718
|
+
provider: z
|
|
719
|
+
.string()
|
|
720
|
+
.regex(/^[a-z0-9-]+$/)
|
|
721
|
+
.optional(),
|
|
722
|
+
// Position among its provider's apps on the Integrations tab, where they are grouped.
|
|
723
|
+
order: z.number().int().optional(),
|
|
724
|
+
connectionKind: z.enum(["guided"]),
|
|
725
|
+
// The connection config value naming the account reached (site, org, tenant). Agents'
|
|
726
|
+
// grants are cleared when a reconnect reaches a different one.
|
|
727
|
+
accountKey: z.string().regex(/^[A-Za-z]+$/),
|
|
728
|
+
// Vendor scopes every ceiling needs, whatever is ticked (e.g. what adding agents' Teams
|
|
729
|
+
// bots takes), filled into {scope}/{scopes} with the capabilities' own.
|
|
730
|
+
scopes: z.array(text).optional(),
|
|
731
|
+
setup: z.array(appStep),
|
|
732
|
+
afterConnect: z.array(appStep),
|
|
733
|
+
fields: z.array(fieldSchema),
|
|
734
|
+
capabilities: z
|
|
735
|
+
.array(z
|
|
736
|
+
.object({
|
|
737
|
+
id: slug,
|
|
738
|
+
label: text,
|
|
739
|
+
description: text,
|
|
740
|
+
// Always on: every connection and every agent has it.
|
|
741
|
+
required: z.boolean().default(false),
|
|
742
|
+
// Off unless the admin confirms a warning.
|
|
743
|
+
destructive: z.boolean().default(false),
|
|
744
|
+
// Spends money on the account (e.g. buys shipping labels): shown as that instead
|
|
745
|
+
// of "Deletes data". Destructive too, so it still asks first.
|
|
746
|
+
costs: z.boolean().optional(),
|
|
747
|
+
// Vendor scopes or roles, filled into {scope}/{scopes} in setup steps.
|
|
748
|
+
scopes: z.array(text),
|
|
749
|
+
// The permissions a provider's access token must carry for this capability, when
|
|
750
|
+
// they are not `scopes` (e.g. mail scopes are Exchange roles, not token roles).
|
|
751
|
+
roles: z.array(text).optional(),
|
|
752
|
+
// Permissions granted inside the app, listed by steps with `permissions`.
|
|
753
|
+
permissions: z.array(text),
|
|
754
|
+
// Values for {name} placeholders in setup steps while this capability is ticked;
|
|
755
|
+
// a later ticked capability overrides an earlier one (e.g. site role read → write).
|
|
756
|
+
fill: z.record(z.string().regex(/^[a-z_]+$/), text).optional(),
|
|
757
|
+
// A caution shown with the checkbox, e.g. that it reaches the whole organization.
|
|
758
|
+
note: text.optional(),
|
|
759
|
+
// Added to the agent's error when the app answers 403 to one of its tools: what
|
|
760
|
+
// Microsoft (or the vendor) still needs, with {scopes} filled.
|
|
761
|
+
forbidden: text.optional(),
|
|
762
|
+
// Its answers use the organization's Claude API key, a setup step while ticked.
|
|
763
|
+
claudeKey: z.boolean().optional(),
|
|
764
|
+
tools: z.array(slug).min(1),
|
|
765
|
+
})
|
|
766
|
+
.strict()
|
|
767
|
+
.refine((c) => !(c.required && c.destructive), {
|
|
768
|
+
message: "A required capability cannot be destructive",
|
|
769
|
+
})
|
|
770
|
+
.refine((c) => !c.costs || c.destructive, {
|
|
771
|
+
message: "A capability that spends money must be destructive, so it asks first",
|
|
772
|
+
}))
|
|
773
|
+
.min(1),
|
|
774
|
+
limits: z.array(limitSchema),
|
|
775
|
+
// Sites the admin adds one at a time after connecting (config.sites), each given to the
|
|
776
|
+
// app outside the service. `grant`: steps showing how, with {site}, {siteHost} and {sitePath}
|
|
777
|
+
// filled per site; `request`: a note to send IT asking for it.
|
|
778
|
+
// `write`: the capabilities that need write access to each site, so Check access also
|
|
779
|
+
// proves the app can change files there while one of them is in the ceiling.
|
|
780
|
+
siteAccess: z
|
|
781
|
+
.object({
|
|
782
|
+
grant: z.array(step).min(1),
|
|
783
|
+
request: text,
|
|
784
|
+
write: z.array(slug).default([]),
|
|
785
|
+
})
|
|
786
|
+
.strict()
|
|
787
|
+
.optional(),
|
|
788
|
+
// Each agent's own address in this app (e.g. its mailbox), held in the text limit `limit`.
|
|
789
|
+
// `path` (with {value}) is read with the app's sign-in to show whether the app can reach
|
|
790
|
+
// it yet; `label` names that check; the messages answer 2xx, 401/403, 404.
|
|
791
|
+
agentAccess: z
|
|
792
|
+
.object({
|
|
793
|
+
limit: slug,
|
|
794
|
+
label: text,
|
|
795
|
+
path: z.string().startsWith("/").includes("{value}"),
|
|
796
|
+
granted: text,
|
|
797
|
+
notGranted: text,
|
|
798
|
+
notFound: text,
|
|
799
|
+
// Per capability: a call that proves the app may also do that there, changing
|
|
800
|
+
// nothing (Exchange refuses a missing role with 401/403 before reading the body).
|
|
801
|
+
// `allowed`: the statuses that mean it may. `missing` names those it may not yet,
|
|
802
|
+
// {missing} filled with their labels and roles.
|
|
803
|
+
probes: z
|
|
804
|
+
.record(slug, z
|
|
805
|
+
.object({
|
|
806
|
+
method: z.enum(["GET", "POST", "PATCH"]),
|
|
807
|
+
path: z.string().startsWith("/").includes("{value}"),
|
|
808
|
+
body: z.unknown().optional(),
|
|
809
|
+
allowed: z.array(z.number().int()).min(1),
|
|
810
|
+
})
|
|
811
|
+
.strict())
|
|
812
|
+
.default({}),
|
|
813
|
+
missing: text.optional(),
|
|
814
|
+
})
|
|
815
|
+
.strict()
|
|
816
|
+
.optional(),
|
|
817
|
+
verify: probeSchema,
|
|
818
|
+
discover: probeSchema.optional(),
|
|
819
|
+
revoke: text,
|
|
820
|
+
http: httpSchema.optional(),
|
|
821
|
+
soap: soapSchema.optional(),
|
|
822
|
+
odata: odataSchema.optional(),
|
|
823
|
+
sql: sqlSchema.optional(),
|
|
824
|
+
events: eventsSchema.optional(),
|
|
825
|
+
})
|
|
826
|
+
.strict()
|
|
827
|
+
.refine((c) => [c.http, c.soap, c.odata, c.sql].filter(Boolean).length <= 1, {
|
|
828
|
+
message: "A connector has at most one of http, soap, odata and sql",
|
|
829
|
+
})
|
|
830
|
+
.refine((c) => {
|
|
831
|
+
const app = described(c);
|
|
832
|
+
if (!app)
|
|
833
|
+
return true;
|
|
834
|
+
const lists = new Set(c.limits.filter((l) => l.kind === "list").map((l) => l.id));
|
|
835
|
+
return (app.fields.every(([id, secret]) => c.fields.some((f) => f.id === id && f.secret === secret && !f.options)) &&
|
|
836
|
+
!c.provider &&
|
|
837
|
+
(c.accountKey === "host" || c.fields.some((f) => f.id === c.accountKey && !f.secret)) &&
|
|
838
|
+
app.limits.every((l) => lists.has(l)));
|
|
839
|
+
}, {
|
|
840
|
+
message: "An http, soap, odata or sql app's auth names its fields (secrets as secret fields), its accountKey is host or a plain field, a param's or answer's limit names a list limit, and it has no provider",
|
|
841
|
+
})
|
|
842
|
+
.refine((c) => !c.agentAccess || c.limits.some((l) => l.id === c.agentAccess.limit && l.kind === "text"), { message: "agentAccess.limit must be one of the connector's text limits" })
|
|
843
|
+
.refine((c) => {
|
|
844
|
+
if (!c.sql)
|
|
845
|
+
return true;
|
|
846
|
+
const writes = new Set(c.sql.tools.filter((t) => t.write).map((t) => t.name));
|
|
847
|
+
return ([c.verify, ...(c.discover ? [c.discover] : [])].every((p) => "check" in p) &&
|
|
848
|
+
c.capabilities.every((cap) => !cap.required || !cap.tools.some((t) => writes.has(t))));
|
|
849
|
+
}, { message: "An sql app's probes are checks, and a write tool is in no required capability" })
|
|
850
|
+
.refine((c) => [...Object.keys(c.agentAccess?.probes ?? {}), ...(c.siteAccess?.write ?? [])].every((id) => c.capabilities.some((x) => x.id === id)) &&
|
|
851
|
+
(!Object.keys(c.agentAccess?.probes ?? {}).length || c.agentAccess?.missing !== undefined), { message: "agentAccess probes and siteAccess.write name capabilities; probes need missing" })
|
|
852
|
+
.refine((c) => Object.keys(c.events?.limits ?? {}).every((id) => c.limits.some((l) => l.id === id && l.kind !== "map")), { message: "Event limits name the connector's list or text limits" })
|
|
853
|
+
.refine((c) => new Set(c.fields.map((f) => f.id)).size === c.fields.length, {
|
|
854
|
+
message: "Field ids must be unique",
|
|
855
|
+
})
|
|
856
|
+
.refine((c) => new Set(c.capabilities.map((l) => l.id)).size === c.capabilities.length, {
|
|
857
|
+
message: "Capability ids must be unique",
|
|
858
|
+
})
|
|
859
|
+
.refine((c) => {
|
|
860
|
+
const tools = c.capabilities.flatMap((l) => l.tools);
|
|
861
|
+
return new Set(tools).size === tools.length;
|
|
862
|
+
}, { message: "A tool belongs to one capability only" })
|
|
863
|
+
.refine((c) => c.provider !== undefined || ![...c.setup, ...c.afterConnect].some((s) => s.settings), { message: "Step settings are kept on a provider connection: only a provider's app has them" })
|
|
864
|
+
.refine((c) => {
|
|
865
|
+
const isOption = (o, fields) => fields.some((f) => f.id === o.field && f.options?.some((x) => x.value === o.value));
|
|
866
|
+
return ([...c.setup, ...c.afterConnect].every((s) => !s.option || isOption(s.option, c.fields)) &&
|
|
867
|
+
c.fields.every((f, i) => !f.requires || isOption(f.requires, c.fields.slice(0, i))));
|
|
868
|
+
}, { message: "A step's option and a field's requires name an option of an earlier choice" })
|
|
869
|
+
.refine((c) => [...c.setup, ...c.afterConnect].every((s) => !s.capabilities || s.capabilities.every((id) => c.capabilities.some((x) => x.id === id))), { message: "A step's capabilities name capabilities of its connector" })
|
|
870
|
+
.refine((c) => c.provider !== undefined || c.fields.length > 0, {
|
|
871
|
+
message: "A guided connector needs connect form fields",
|
|
872
|
+
})
|
|
873
|
+
.refine((c) => c.limits.every((l, i) => !l.keys || c.limits.slice(0, i).some((k) => k.id === l.keys && k.kind === "list")), { message: "A map limit's keys must be a list limit listed before it" });
|
|
874
|
+
// An app described in its connector.json (http, soap, odata or sql): the connect form fields
|
|
875
|
+
// its sign-in names, and the list limits its tools name.
|
|
876
|
+
function described(c) {
|
|
877
|
+
const limitsOf = (...sets) => sets.flatMap((s) => Object.values(s).flatMap((p) => (p.limit ? [p.limit] : [])));
|
|
878
|
+
if (c.http)
|
|
879
|
+
return {
|
|
880
|
+
fields: authFields(c.http.auth),
|
|
881
|
+
limits: c.http.tools.flatMap((t) => [
|
|
882
|
+
...limitsOf(t.params),
|
|
883
|
+
...t.answerLimits.map((a) => a.limit),
|
|
884
|
+
]),
|
|
885
|
+
};
|
|
886
|
+
if (c.soap) {
|
|
887
|
+
const ws = c.soap.wsSecurity;
|
|
888
|
+
return {
|
|
889
|
+
fields: [
|
|
890
|
+
...(c.soap.auth ? authFields(c.soap.auth) : []),
|
|
891
|
+
...(ws
|
|
892
|
+
? [
|
|
893
|
+
[ws.username, false],
|
|
894
|
+
[ws.password, true],
|
|
895
|
+
]
|
|
896
|
+
: []),
|
|
897
|
+
],
|
|
898
|
+
limits: c.soap.tools.flatMap((t) => [
|
|
899
|
+
...limitsOf(t.params),
|
|
900
|
+
...t.answerLimits.map((a) => a.limit),
|
|
901
|
+
]),
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
if (c.odata)
|
|
905
|
+
return {
|
|
906
|
+
fields: authFields(c.odata.auth),
|
|
907
|
+
limits: c.odata.tools.flatMap((t) => [
|
|
908
|
+
...limitsOf(t.key, t.filters, t.fields, t.params),
|
|
909
|
+
...t.answerLimits.map((a) => a.limit),
|
|
910
|
+
]),
|
|
911
|
+
};
|
|
912
|
+
if (c.sql)
|
|
913
|
+
return {
|
|
914
|
+
fields: [
|
|
915
|
+
[c.sql.auth.user, false],
|
|
916
|
+
[c.sql.auth.password, true],
|
|
917
|
+
],
|
|
918
|
+
limits: c.sql.tools.flatMap((t) => [
|
|
919
|
+
...limitsOf(t.params),
|
|
920
|
+
...t.answerLimits.map((a) => a.limit),
|
|
921
|
+
]),
|
|
922
|
+
};
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
// One sign-in shared by several apps, e.g. a customer's Microsoft Entra app used by mail and
|
|
926
|
+
// files. Its setup steps are shown with the first of its apps the admin connects.
|
|
927
|
+
export const providerSchema = z
|
|
928
|
+
.object({
|
|
929
|
+
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
930
|
+
name: text,
|
|
931
|
+
logo: z.string().regex(/^\/brand\/[a-z0-9/-]+\.svg$/),
|
|
932
|
+
identity: text,
|
|
933
|
+
// `identity` while the connection uses the deployment's shared app instead of the customer's own.
|
|
934
|
+
sharedIdentity: text.optional(),
|
|
935
|
+
accountKey: z.string().regex(/^[A-Za-z]+$/),
|
|
936
|
+
// Config values naming who signs in (e.g. the app and its service principal). A change in
|
|
937
|
+
// any, like a change of account, disconnects the provider's apps: what the admin granted
|
|
938
|
+
// outside the service (mailbox roles, site grants) belongs to the old one.
|
|
939
|
+
principalKeys: z.array(z.string().regex(/^[A-Za-z]+$/)).default([]),
|
|
940
|
+
// The service can make a key pair whose certificate the admin uploads to their app.
|
|
941
|
+
certificate: z.boolean(),
|
|
942
|
+
setup: z.array(step),
|
|
943
|
+
fields: z.array(fieldSchema).min(1),
|
|
944
|
+
// What one more agent needs outside the service (e.g. its mailbox), shown when its API key is
|
|
945
|
+
// made with one of the apps each step names.
|
|
946
|
+
addAgent: z.array(agentStep).default([]),
|
|
947
|
+
revoke: text,
|
|
948
|
+
})
|
|
949
|
+
.strict();
|
|
950
|
+
/** The repo's connectors/ folder, beside packages/ (and so beside dist/packages/ once built). */
|
|
951
|
+
export const CONNECTORS_DIR = new URL("../../../connectors/", import.meta.url);
|
|
952
|
+
function parse(raw, where, schema) {
|
|
953
|
+
let filled;
|
|
954
|
+
try {
|
|
955
|
+
filled = fillBrandDeep(raw);
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
throw new Error(`${where}: ${error.message}`);
|
|
959
|
+
}
|
|
960
|
+
const parsed = schema.safeParse(filled);
|
|
961
|
+
if (!parsed.success)
|
|
962
|
+
throw new Error(`${where}: ${z.prettifyError(parsed.error)}`);
|
|
963
|
+
return parsed.data;
|
|
964
|
+
}
|
|
965
|
+
/** A definition given inline rather than in a folder, checked as loadConnectors checks one. */
|
|
966
|
+
export const parseConnector = (raw, where) => parse(raw, where, connectorSchema);
|
|
967
|
+
// Each subfolder of dir holding `file` is one definition, whose id is the folder's name.
|
|
968
|
+
// Subfolders may be links: Kubernetes mounts ConfigMap and Secret folders that way.
|
|
969
|
+
function loadDir(dir, file, schema, only = () => true) {
|
|
970
|
+
return readdirSync(dir)
|
|
971
|
+
.filter((name) => only(name) && existsSync(new URL(`${name}/${file}`, dir)))
|
|
972
|
+
.sort()
|
|
973
|
+
.map((name) => {
|
|
974
|
+
const where = `${name}/${file}`;
|
|
975
|
+
const data = parse(JSON.parse(readFileSync(new URL(where, dir), "utf8")), where, schema);
|
|
976
|
+
if (data.id !== name)
|
|
977
|
+
throw new Error(`${where}: id must match the folder name`);
|
|
978
|
+
return data;
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
/** `only`: the folder ids to read, so an app not offered is never parsed. */
|
|
982
|
+
export function loadConnectors(dir = CONNECTORS_DIR, only) {
|
|
983
|
+
return loadDir(dir, "connector.json", connectorSchema, only);
|
|
984
|
+
}
|
|
985
|
+
export function loadProviders(dir = CONNECTORS_DIR) {
|
|
986
|
+
return loadDir(dir, "provider.json", providerSchema);
|
|
987
|
+
}
|
|
988
|
+
/** The ids of an app's choice fields: what its connection keeps from the admin's form. */
|
|
989
|
+
export const choiceFields = (connector) => connector.fields.filter((f) => f.options).map((f) => f.id);
|
|
990
|
+
/** The option chosen for each choice field: the stored one, else the first, which also holds
|
|
991
|
+
* while the earlier choice it `requires` is not made. */
|
|
992
|
+
export function chosenOptions(connector, config = {}) {
|
|
993
|
+
const chosen = {};
|
|
994
|
+
return connector.fields.flatMap((f) => {
|
|
995
|
+
if (!f.options)
|
|
996
|
+
return [];
|
|
997
|
+
const asked = !f.requires || chosen[f.requires.field] === f.requires.value;
|
|
998
|
+
const option = (asked && f.options.find((o) => o.value === config[f.id])) || f.options[0];
|
|
999
|
+
chosen[f.id] = option.value;
|
|
1000
|
+
return [option];
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
/** The permissions a provider's token needs for these capabilities and the chosen options. */
|
|
1004
|
+
export function tokenRolesFor(connector, capabilities, config = {}) {
|
|
1005
|
+
return [
|
|
1006
|
+
...new Set([
|
|
1007
|
+
...(connector.scopes ?? []),
|
|
1008
|
+
...connector.capabilities
|
|
1009
|
+
.filter((c) => capabilities.includes(c.id))
|
|
1010
|
+
.flatMap((c) => c.roles ?? c.scopes),
|
|
1011
|
+
...chosenOptions(connector, config).flatMap((o) => o.scopes ?? []),
|
|
1012
|
+
]),
|
|
1013
|
+
];
|
|
1014
|
+
}
|
|
1015
|
+
// What an agent may actually do: its capabilities that the connection also allows.
|
|
1016
|
+
export function effectiveCapabilities(connector, grant, ceiling) {
|
|
1017
|
+
return connector.capabilities.filter((c) => grant.includes(c.id) && ceiling.includes(c.id));
|
|
1018
|
+
}
|
|
1019
|
+
// Keys of a provider connection's own config, which a setting saved beside them must not replace.
|
|
1020
|
+
const RESERVED_SETTINGS = new Set(["tenantId", "clientId", "auth", "certificate", "sites"]);
|
|
1021
|
+
/** The settings a provider's apps ask for in their steps, by id. */
|
|
1022
|
+
export function providerSettings(connectors, provider) {
|
|
1023
|
+
const out = new Map();
|
|
1024
|
+
for (const c of connectors)
|
|
1025
|
+
if (c.provider === provider)
|
|
1026
|
+
for (const s of [...c.setup, ...c.afterConnect])
|
|
1027
|
+
for (const setting of s.settings ?? []) {
|
|
1028
|
+
if (RESERVED_SETTINGS.has(setting.id))
|
|
1029
|
+
throw new Error(`${c.id}: setting id ${setting.id} is the connection's own`);
|
|
1030
|
+
out.set(setting.id, setting);
|
|
1031
|
+
}
|
|
1032
|
+
return out;
|
|
1033
|
+
}
|
|
1034
|
+
export function expandCopy(lines, scopes) {
|
|
1035
|
+
return lines
|
|
1036
|
+
.flatMap((line) => line.includes("{scope}") ? scopes.map((s) => line.replaceAll("{scope}", s)) : [line])
|
|
1037
|
+
.map((line) => line.replaceAll("{scopes}", scopes.join(" ")))
|
|
1038
|
+
.join("\n");
|
|
1039
|
+
}
|
|
1040
|
+
// Reads `a.b` / `values[].name` paths out of a JSON response for display.
|
|
1041
|
+
export function pickDisplay(data, path, match = {}) {
|
|
1042
|
+
return pick(data, path, match)
|
|
1043
|
+
.filter((v) => v !== undefined && v !== null)
|
|
1044
|
+
.map(String);
|
|
1045
|
+
}
|
|
1046
|
+
/** Each value `show` picks, with its label `name` picks from the same list item. */
|
|
1047
|
+
export function pickLabels(data, show, name, match = {}) {
|
|
1048
|
+
const prefix = listPrefix(show);
|
|
1049
|
+
const items = prefix ? pick(data, prefix, match) : [data];
|
|
1050
|
+
const rest = (path) => path.slice(prefix.length).replace(/^\./, "");
|
|
1051
|
+
const out = {};
|
|
1052
|
+
for (const item of items) {
|
|
1053
|
+
const [value] = rest(show) ? pick(item, rest(show)) : [item];
|
|
1054
|
+
const [label] = rest(name) ? pick(item, rest(name)) : [item];
|
|
1055
|
+
if (value != null && label != null)
|
|
1056
|
+
out[String(value)] = String(label);
|
|
1057
|
+
}
|
|
1058
|
+
return out;
|
|
1059
|
+
}
|
|
1060
|
+
function pick(data, path, match = {}) {
|
|
1061
|
+
const matches = (v) => Object.entries(match).every(([k, want]) => v && typeof v === "object" && v[k] === want);
|
|
1062
|
+
let values = [data];
|
|
1063
|
+
for (const part of path.split(".")) {
|
|
1064
|
+
const many = part.endsWith("[]");
|
|
1065
|
+
const key = many ? part.slice(0, -2) : part;
|
|
1066
|
+
values = values.flatMap((v) => {
|
|
1067
|
+
const next = v && typeof v === "object" ? v[key] : undefined;
|
|
1068
|
+
return many ? (Array.isArray(next) ? next.filter(matches) : []) : [next];
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
return values;
|
|
1072
|
+
}
|
|
1073
|
+
//# sourceMappingURL=connector.js.map
|