@arbium/enterprise-agent-connect 0.0.0-stage → 0.1.2-dev.9022f22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +400 -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,924 @@
|
|
|
1
|
+
import { brand } from "../../connector-kit/src/branding.js";
|
|
2
|
+
import { describeCertificate, newCertificate } from "../../connector-kit/src/certificate.js";
|
|
3
|
+
import { signInNonce, signState, verifyState } from "./consent.js";
|
|
4
|
+
import { effectiveCapabilities, pickDisplay, pickLabels, providerSettings, tokenRolesFor, } from "../../connector-kit/src/connector.js";
|
|
5
|
+
import { credentialAad, hashAgentKey, looksLikeAgentKey } from "./secrets.js";
|
|
6
|
+
import { ProviderChanged } from "./store.js";
|
|
7
|
+
import { configSites } from "../../../connectors/sharepoint-files/vendor.js";
|
|
8
|
+
import { SHARED_APP_AUTH } from "../../../connectors/microsoft/client.js";
|
|
9
|
+
import { argumentsTarget } from "../../connector-kit/src/common.js";
|
|
10
|
+
import { ConnectError, } from "../../connector-kit/src/types.js";
|
|
11
|
+
export class InvalidInput extends Error {
|
|
12
|
+
}
|
|
13
|
+
// fetch rejects with TypeError("fetch failed") when the app's address does not answer, e.g.
|
|
14
|
+
// a mistyped site or domain. That is the admin's to fix, not a server fault.
|
|
15
|
+
async function reaching(name, run) {
|
|
16
|
+
try {
|
|
17
|
+
return await run();
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof TypeError && error.message === "fetch failed")
|
|
21
|
+
throw new ConnectError(`Could not reach ${name}. Check the address and try again.`);
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Validates ticked capabilities and returns them in connector order, with the required ones
|
|
26
|
+
// added: they are always on.
|
|
27
|
+
export function validateCapabilities(connector, raw) {
|
|
28
|
+
if (!Array.isArray(raw))
|
|
29
|
+
throw new InvalidInput("capabilities must be a list");
|
|
30
|
+
for (const id of raw)
|
|
31
|
+
if (typeof id !== "string" || !connector.capabilities.some((c) => c.id === id))
|
|
32
|
+
throw new InvalidInput(`${connector.name} has no capability ${String(id)}`);
|
|
33
|
+
const ids = connector.capabilities
|
|
34
|
+
.filter((c) => c.required || raw.includes(c.id))
|
|
35
|
+
.map((c) => c.id);
|
|
36
|
+
if (!ids.length)
|
|
37
|
+
throw new InvalidInput(`Choose at least one thing ${connector.name} may do`);
|
|
38
|
+
return ids;
|
|
39
|
+
}
|
|
40
|
+
export function publicConnection(row) {
|
|
41
|
+
const { credential: _c, credentialKeyId: _k, ...rest } = row;
|
|
42
|
+
return rest;
|
|
43
|
+
}
|
|
44
|
+
const certificateInfo = (pem) => (pem ? describeCertificate(pem) : null);
|
|
45
|
+
// `sharedClientId`: the shared app's id, for rows approved before the id was stored with them
|
|
46
|
+
// (moved from microsoft-365), so their setup steps still show it.
|
|
47
|
+
export function publicProvider(row, connections, sharedClientId = null) {
|
|
48
|
+
const { credential: _c, credentialKeyId: _k, credentialScope: _s, ...rest } = row;
|
|
49
|
+
const shared = row.config.auth === SHARED_APP_AUTH && !row.config.clientId && sharedClientId;
|
|
50
|
+
return {
|
|
51
|
+
...rest,
|
|
52
|
+
...(shared ? { config: { ...row.config, clientId: sharedClientId } } : {}),
|
|
53
|
+
certificate: certificateInfo(row.config.certificate),
|
|
54
|
+
nextCertificate: certificateInfo(row.config.nextCertificate),
|
|
55
|
+
apps: connections.filter((c) => c.providerId === row.id).map((c) => c.connectorId),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Same account, reached by the same app. A principal key missing on either side (rows moved
|
|
59
|
+
// from before it was stored) is not counted as a change.
|
|
60
|
+
export function sameSignIn(provider, before, after) {
|
|
61
|
+
if (before[provider.accountKey] !== after[provider.accountKey])
|
|
62
|
+
return false;
|
|
63
|
+
return provider.principalKeys.every((k) => !before[k] || !after[k] || before[k] === after[k]);
|
|
64
|
+
}
|
|
65
|
+
// What a provider's credential is bound to. Rows moved from an app connection keep that
|
|
66
|
+
// app's id until re-sealed.
|
|
67
|
+
const providerScope = (provider) => `provider:${provider}`;
|
|
68
|
+
// Every tool a connector's capabilities name must exist in its toolset, and every tool in
|
|
69
|
+
// the toolset must belong to a capability (otherwise no agent could ever be given it). Every
|
|
70
|
+
// vendor check a probe names must exist too.
|
|
71
|
+
export function assertToolsImplemented(apps) {
|
|
72
|
+
for (const { connector, toolset, vendor } of apps) {
|
|
73
|
+
for (const p of probes(connector))
|
|
74
|
+
if ("check" in p && !vendor.checks?.[p.check])
|
|
75
|
+
throw new Error(`${connector.id}: check not implemented: ${p.check}`);
|
|
76
|
+
if (connector.siteAccess && !vendor.sites)
|
|
77
|
+
throw new Error(`${connector.id}: sites are added, but its client cannot check them`);
|
|
78
|
+
const named = new Set(connector.capabilities.flatMap((l) => l.tools));
|
|
79
|
+
const missing = [...named].filter((t) => !toolset.names.includes(t));
|
|
80
|
+
if (missing.length)
|
|
81
|
+
throw new Error(`${connector.id}: tools not implemented: ${missing.join(", ")}`);
|
|
82
|
+
const orphans = toolset.names.filter((t) => !named.has(t));
|
|
83
|
+
if (orphans.length)
|
|
84
|
+
throw new Error(`${connector.id}: tools in no capability: ${orphans.join(", ")}`);
|
|
85
|
+
const destructive = destructiveTools(toolset);
|
|
86
|
+
for (const c of connector.capabilities) {
|
|
87
|
+
const tool = c.tools.find((t) => destructive.has(t));
|
|
88
|
+
if (tool && !c.destructive)
|
|
89
|
+
throw new Error(`${connector.id}: capability ${c.id} has ${tool}, which can destroy or overwrite, so it must be destructive`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// The tools a toolset declares destructive (MCP's destructiveHint), read by registering them
|
|
94
|
+
// all on a server that only records.
|
|
95
|
+
function destructiveTools(toolset) {
|
|
96
|
+
const found = new Set();
|
|
97
|
+
const recorder = {
|
|
98
|
+
registerTool(name, config) {
|
|
99
|
+
if (config.annotations?.destructiveHint)
|
|
100
|
+
found.add(name);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
toolset.register(recorder, { limits: {}, config: {} }, new Set(toolset.names));
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
const probes = (connector) => [
|
|
107
|
+
connector.verify,
|
|
108
|
+
...(connector.discover ? [connector.discover] : []),
|
|
109
|
+
];
|
|
110
|
+
export function validateLimits(connector, raw) {
|
|
111
|
+
if (raw === undefined || raw === null)
|
|
112
|
+
raw = {};
|
|
113
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
114
|
+
throw new InvalidInput("limits must be an object");
|
|
115
|
+
const input = raw;
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const key of Object.keys(input))
|
|
118
|
+
if (!connector.limits.some((l) => l.id === key))
|
|
119
|
+
throw new InvalidInput(`${connector.name} has no limit ${key}`);
|
|
120
|
+
for (const limit of connector.limits) {
|
|
121
|
+
const value = input[limit.id];
|
|
122
|
+
const pattern = limit.pattern ? new RegExp(limit.pattern) : null;
|
|
123
|
+
if (limit.kind === "map") {
|
|
124
|
+
// Keyed by values of another list limit (checked above, in connector order).
|
|
125
|
+
const keys = out[limit.keys] ?? [];
|
|
126
|
+
const map = value ?? {};
|
|
127
|
+
if (typeof map !== "object" || Array.isArray(map))
|
|
128
|
+
throw new InvalidInput(`${limit.label} must name a value for each choice`);
|
|
129
|
+
const kept = {};
|
|
130
|
+
for (const [k, v] of Object.entries(map)) {
|
|
131
|
+
if (!keys.includes(k))
|
|
132
|
+
throw new InvalidInput(`${limit.label}: ${k} is not one of the chosen values`);
|
|
133
|
+
const text = typeof v === "string" ? v.trim() : v;
|
|
134
|
+
if (typeof text !== "string" || text.length > 400)
|
|
135
|
+
throw new InvalidInput(`${limit.label} must be text`);
|
|
136
|
+
if (text && pattern && !pattern.test(text))
|
|
137
|
+
throw new InvalidInput(`${limit.label}: ${text} is not valid`);
|
|
138
|
+
if (text)
|
|
139
|
+
kept[k] = text;
|
|
140
|
+
}
|
|
141
|
+
if (limit.required && Object.keys(kept).length === 0)
|
|
142
|
+
throw new InvalidInput(`${limit.label} is required`);
|
|
143
|
+
if (Object.keys(kept).length)
|
|
144
|
+
out[limit.id] = kept;
|
|
145
|
+
}
|
|
146
|
+
else if (limit.kind === "list") {
|
|
147
|
+
const list = value ?? [];
|
|
148
|
+
if (!Array.isArray(list) || list.some((v) => typeof v !== "string" || !v || v.length > 255))
|
|
149
|
+
throw new InvalidInput(`${limit.label} must be a list of values`);
|
|
150
|
+
if (list.length > 500)
|
|
151
|
+
throw new InvalidInput(`${limit.label} has too many values`);
|
|
152
|
+
const bad = pattern && list.find((v) => !pattern.test(v));
|
|
153
|
+
if (bad)
|
|
154
|
+
throw new InvalidInput(`${limit.label}: ${bad} is not valid`);
|
|
155
|
+
if (limit.required && list.length === 0)
|
|
156
|
+
throw new InvalidInput(`${limit.label} is required`);
|
|
157
|
+
if (list.length)
|
|
158
|
+
out[limit.id] = [...new Set(list)];
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const text = typeof value === "string" ? value.trim() : value;
|
|
162
|
+
if (text !== undefined && (typeof text !== "string" || text.length > 320))
|
|
163
|
+
throw new InvalidInput(`${limit.label} must be text`);
|
|
164
|
+
if (text && pattern && !pattern.test(text))
|
|
165
|
+
throw new InvalidInput(`${limit.label}: ${text} is not valid`);
|
|
166
|
+
if (limit.required && !text)
|
|
167
|
+
throw new InvalidInput(`${limit.label} is required`);
|
|
168
|
+
if (text)
|
|
169
|
+
out[limit.id] = text;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
// A choice field's value must be one of its options; left empty, it is the first.
|
|
175
|
+
export function validateChoices(connector, fields) {
|
|
176
|
+
const out = { ...fields };
|
|
177
|
+
for (const field of connector.fields) {
|
|
178
|
+
if (!field.options)
|
|
179
|
+
continue;
|
|
180
|
+
const value = out[field.id]?.trim() || field.options[0].value;
|
|
181
|
+
if (!field.options.some((o) => o.value === value))
|
|
182
|
+
throw new InvalidInput(`${field.label}: ${value} is not one of the choices`);
|
|
183
|
+
out[field.id] = value;
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
// One user by id or sign-in name, its id and name only.
|
|
188
|
+
const DIRECTORY_LOOKUP = /^\/v1\.0\/users\/[^/?#]+\?\$select=id,displayName$/;
|
|
189
|
+
export function createService(deps) {
|
|
190
|
+
const { store, key } = deps;
|
|
191
|
+
const stateKey = key.derive("consent-state");
|
|
192
|
+
const apps = new Map(deps.apps.map((a) => [a.connector.id, a]));
|
|
193
|
+
const providers = new Map((deps.providers ?? []).map((p) => [p.provider.id, p]));
|
|
194
|
+
for (const { connector } of deps.apps)
|
|
195
|
+
if (connector.provider && !providers.has(connector.provider))
|
|
196
|
+
throw new Error(`${connector.id}: provider ${connector.provider} is not loaded`);
|
|
197
|
+
for (const { provider } of providers.values())
|
|
198
|
+
for (const step of provider.addAgent)
|
|
199
|
+
for (const id of step.apps)
|
|
200
|
+
if (apps.get(id)?.connector.provider !== provider.id)
|
|
201
|
+
throw new Error(`${provider.id}: add-agent step "${step.title}" names ${id}, not one of its apps`);
|
|
202
|
+
// Each confirmable step id and who owns it (for its audit row): the provider when one of
|
|
203
|
+
// its steps carries the id, else the first app that does.
|
|
204
|
+
const confirmable = new Map();
|
|
205
|
+
for (const { connector } of deps.apps)
|
|
206
|
+
for (const s of [...connector.setup, ...connector.afterConnect])
|
|
207
|
+
if (s.confirm && !confirmable.has(s.confirm))
|
|
208
|
+
confirmable.set(s.confirm, connector.id);
|
|
209
|
+
for (const { provider } of providers.values())
|
|
210
|
+
for (const s of provider.setup)
|
|
211
|
+
if (s.confirm)
|
|
212
|
+
confirmable.set(s.confirm, provider.id);
|
|
213
|
+
const sharedClientId = (id) => providers.get(id)?.client.approval?.clientId ?? null;
|
|
214
|
+
function app(connectorId) {
|
|
215
|
+
const found = apps.get(connectorId);
|
|
216
|
+
if (!found)
|
|
217
|
+
throw new InvalidInput(`Unknown app ${connectorId}`);
|
|
218
|
+
return found;
|
|
219
|
+
}
|
|
220
|
+
function providerApp(id) {
|
|
221
|
+
const found = providers.get(id);
|
|
222
|
+
if (!found)
|
|
223
|
+
throw new InvalidInput(`Unknown connection ${id}`);
|
|
224
|
+
return found;
|
|
225
|
+
}
|
|
226
|
+
function approvalFor(connectorId) {
|
|
227
|
+
const target = app(connectorId);
|
|
228
|
+
const approval = target.connector.provider
|
|
229
|
+
? providerApp(target.connector.provider).client.approval
|
|
230
|
+
: undefined;
|
|
231
|
+
if (!approval)
|
|
232
|
+
throw new InvalidInput(`${target.connector.name} is connected with a form, not an approval`);
|
|
233
|
+
return { target, approval, provider: providerApp(target.connector.provider) };
|
|
234
|
+
}
|
|
235
|
+
function openProvider(row) {
|
|
236
|
+
return key.open({ ciphertext: row.credential, keyId: row.credentialKeyId }, credentialAad(row.organizationId, row.credentialScope));
|
|
237
|
+
}
|
|
238
|
+
// Moves a provider credential sealed with the previous key, or under a former app's id,
|
|
239
|
+
// onto the current key and its own scope.
|
|
240
|
+
async function freshen(row, secret) {
|
|
241
|
+
const scope = providerScope(row.provider);
|
|
242
|
+
if (row.credentialKeyId === key.id && row.credentialScope === scope)
|
|
243
|
+
return;
|
|
244
|
+
const sealed = key.seal(secret, credentialAad(row.organizationId, scope));
|
|
245
|
+
// False when an admin changed the credential meanwhile: theirs is sealed afresh already.
|
|
246
|
+
await store.updateProviderCredential(row.id, row.config, sealed.ciphertext, sealed.keyId, scope, row.credential);
|
|
247
|
+
}
|
|
248
|
+
// Changes to a provider's sign-in, kept with the agents' tool calls (no agent; `by` is the
|
|
249
|
+
// admin). Shown nowhere yet, but there for an audit.
|
|
250
|
+
async function logAdmin(org, by, provider, action, target) {
|
|
251
|
+
await store.audit({
|
|
252
|
+
organizationId: org,
|
|
253
|
+
agentId: null,
|
|
254
|
+
agentName: by,
|
|
255
|
+
connectorId: provider,
|
|
256
|
+
tool: `admin:${action}`,
|
|
257
|
+
target,
|
|
258
|
+
decision: "allowed",
|
|
259
|
+
reason: null,
|
|
260
|
+
vendorStatus: null,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// How the admin can tell sign-ins apart: the app, the method, and the certificate.
|
|
264
|
+
function signInLabel(config) {
|
|
265
|
+
const cert = config.certificate
|
|
266
|
+
? ` certificate ${describeCertificate(config.certificate).thumbprint}`
|
|
267
|
+
: "";
|
|
268
|
+
return `${config.auth ?? "none"} app ${config.clientId ?? "-"} tenant ${config.tenantId ?? "-"}${cert}`;
|
|
269
|
+
}
|
|
270
|
+
// The connection an app's calls go through: its own credential, or its provider's with the
|
|
271
|
+
// provider's settings under its own.
|
|
272
|
+
function resolve(row, providerRows) {
|
|
273
|
+
if (!row.providerId)
|
|
274
|
+
return {
|
|
275
|
+
config: row.config,
|
|
276
|
+
secret: key.open({ ciphertext: row.credential, keyId: row.credentialKeyId }, credentialAad(row.organizationId, row.connectorId)),
|
|
277
|
+
};
|
|
278
|
+
const provider = providerRows.find((p) => p.id === row.providerId);
|
|
279
|
+
if (!provider)
|
|
280
|
+
throw new Error(`${row.connectorId} connection has no provider row`);
|
|
281
|
+
return { config: { ...provider.config, ...row.config }, secret: openProvider(provider) };
|
|
282
|
+
}
|
|
283
|
+
async function save(target, input) {
|
|
284
|
+
const org = input.organizationId;
|
|
285
|
+
const capabilities = validateCapabilities(target.connector, input.capabilities);
|
|
286
|
+
let provider = null;
|
|
287
|
+
let account;
|
|
288
|
+
if (target.connector.provider) {
|
|
289
|
+
const { provider: def } = providerApp(target.connector.provider);
|
|
290
|
+
provider = await store.getProvider(org, def.id);
|
|
291
|
+
if (!provider || provider.status === "pending")
|
|
292
|
+
throw new ConnectError(`Connect ${def.name} first`);
|
|
293
|
+
account = {
|
|
294
|
+
config: provider.config,
|
|
295
|
+
secret: openProvider(provider),
|
|
296
|
+
accountLabel: provider.accountLabel ?? "",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const fields = validateChoices(target.connector, input.fields);
|
|
300
|
+
const previous = await store.getConnection(org, input.connectorId);
|
|
301
|
+
const { conn, checks } = await reaching(target.connector.name, async () => {
|
|
302
|
+
const conn = await target.vendor.connect(fields, account, previous?.config);
|
|
303
|
+
const full = account
|
|
304
|
+
? { config: { ...account.config, ...conn.config }, secret: account.secret }
|
|
305
|
+
: conn;
|
|
306
|
+
return { conn, checks: await probe(target, full, capabilities) };
|
|
307
|
+
});
|
|
308
|
+
const accountKey = target.connector.accountKey;
|
|
309
|
+
const accountOf = (config) => ({ ...provider?.config, ...config })[accountKey];
|
|
310
|
+
if (!accountOf(conn.config))
|
|
311
|
+
throw new Error(`${target.connector.id} connection has no ${accountKey}`);
|
|
312
|
+
const sealed = provider ? null : key.seal(conn.secret, credentialAad(org, input.connectorId));
|
|
313
|
+
await store.saveConnection({
|
|
314
|
+
clearGrants: previous !== null && accountOf(previous.config) !== accountOf(conn.config),
|
|
315
|
+
organizationId: org,
|
|
316
|
+
connectorId: input.connectorId,
|
|
317
|
+
capabilities,
|
|
318
|
+
config: conn.config,
|
|
319
|
+
credential: sealed?.ciphertext ?? null,
|
|
320
|
+
credentialKeyId: sealed?.keyId ?? null,
|
|
321
|
+
providerId: provider?.id ?? null,
|
|
322
|
+
accountLabel: conn.accountLabel,
|
|
323
|
+
connectedBy: input.connectedBy,
|
|
324
|
+
});
|
|
325
|
+
const row = await store.getConnection(org, input.connectorId);
|
|
326
|
+
if (!row)
|
|
327
|
+
throw new Error("Connection vanished after saving");
|
|
328
|
+
return { connection: publicConnection(row), checks };
|
|
329
|
+
}
|
|
330
|
+
async function saveProvider(org, { provider }, conn, connectedBy, existing) {
|
|
331
|
+
const accountKey = provider.accountKey;
|
|
332
|
+
if (!conn.config[accountKey])
|
|
333
|
+
throw new Error(`${provider.id} connection has no ${accountKey}`);
|
|
334
|
+
const scope = providerScope(provider.id);
|
|
335
|
+
const sealed = key.seal(conn.secret, credentialAad(org, scope));
|
|
336
|
+
const removeApps = existing !== null &&
|
|
337
|
+
existing.status !== "pending" &&
|
|
338
|
+
!sameSignIn(provider, existing.config, conn.config);
|
|
339
|
+
// What the admin entered in the apps' steps (e.g. the Azure resource group for Teams bots)
|
|
340
|
+
// stays with the same sign-in.
|
|
341
|
+
const settings = providerSettings(deps.apps.map((a) => a.connector), provider.id);
|
|
342
|
+
const kept = existing && !removeApps
|
|
343
|
+
? Object.fromEntries(Object.entries(existing.config).filter(([k]) => settings.has(k) && !(k in conn.config)))
|
|
344
|
+
: {};
|
|
345
|
+
const removed = removeApps
|
|
346
|
+
? (await store.listConnections(org))
|
|
347
|
+
.filter((c) => c.providerId === existing.id)
|
|
348
|
+
.map((c) => c.connectorId)
|
|
349
|
+
: [];
|
|
350
|
+
await store.saveProvider({
|
|
351
|
+
organizationId: org,
|
|
352
|
+
provider: provider.id,
|
|
353
|
+
config: { ...kept, ...conn.config },
|
|
354
|
+
credential: sealed.ciphertext,
|
|
355
|
+
credentialKeyId: sealed.keyId,
|
|
356
|
+
credentialScope: scope,
|
|
357
|
+
accountLabel: conn.accountLabel,
|
|
358
|
+
connectedBy,
|
|
359
|
+
removeApps,
|
|
360
|
+
replaces: existing?.credential ?? null,
|
|
361
|
+
});
|
|
362
|
+
const before = existing && existing.status !== "pending" ? signInLabel(existing.config) : "none";
|
|
363
|
+
await logAdmin(org, connectedBy, provider.id, "connect", `${before} -> ${signInLabel(conn.config)}${removed.length ? `; disconnected ${removed.join(", ")}` : ""}`);
|
|
364
|
+
return providerView(org, provider.id);
|
|
365
|
+
}
|
|
366
|
+
async function providerView(org, id) {
|
|
367
|
+
const [row, connections] = await Promise.all([
|
|
368
|
+
store.getProvider(org, id),
|
|
369
|
+
store.listConnections(org),
|
|
370
|
+
]);
|
|
371
|
+
if (!row)
|
|
372
|
+
throw new Error("Provider connection vanished after saving");
|
|
373
|
+
return publicProvider(row, connections, sharedClientId(id));
|
|
374
|
+
}
|
|
375
|
+
async function probe({ connector, vendor }, conn, ceiling) {
|
|
376
|
+
const results = [];
|
|
377
|
+
for (const p of probes(connector)) {
|
|
378
|
+
if ("check" in p) {
|
|
379
|
+
const { values, warning, labels } = await vendor.checks[p.check](conn, {
|
|
380
|
+
expected: tokenRolesFor(connector, ceiling, conn.config),
|
|
381
|
+
});
|
|
382
|
+
// The vendor's own warning says more than the generic "nothing yet".
|
|
383
|
+
const shown = warning || (values.length === 0 ? p.ifEmpty : undefined);
|
|
384
|
+
results.push({
|
|
385
|
+
label: p.label,
|
|
386
|
+
values,
|
|
387
|
+
...(shown ? { warning: shown } : {}),
|
|
388
|
+
...(labels ? { labels } : {}),
|
|
389
|
+
});
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
// {name} in a probe path is the connection's config value, e.g. the signed-in user's id.
|
|
393
|
+
const path = p.path.replace(/\{([A-Za-z_]+)\}/g, (_, name) => {
|
|
394
|
+
const value = conn.config[name];
|
|
395
|
+
if (!value)
|
|
396
|
+
throw new Error(`${connector.id} probe needs config ${name}`);
|
|
397
|
+
return encodeURIComponent(value);
|
|
398
|
+
});
|
|
399
|
+
const res = await vendor.call(conn, { method: p.method, path, body: p.body });
|
|
400
|
+
if (res.status < 200 || res.status >= 300)
|
|
401
|
+
throw new ConnectError(`${p.label}: ${connector.name} answered ${res.status}`);
|
|
402
|
+
const values = pickDisplay(res.data, p.show, p.match);
|
|
403
|
+
results.push({
|
|
404
|
+
label: p.label,
|
|
405
|
+
values,
|
|
406
|
+
...(values.length === 0 && p.ifEmpty ? { warning: p.ifEmpty } : {}),
|
|
407
|
+
...(p.name ? { labels: pickLabels(res.data, p.show, p.name, p.match) } : {}),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return results;
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
connectors: () => deps.apps.map(({ connector }) => connector),
|
|
414
|
+
providers: () => [...providers.values()].map(({ provider, client }) => ({
|
|
415
|
+
...provider,
|
|
416
|
+
// The deployment offers approving its own shared app instead of registering one.
|
|
417
|
+
approval: Boolean(client.approval),
|
|
418
|
+
})),
|
|
419
|
+
async listConnections(org) {
|
|
420
|
+
return (await store.listConnections(org))
|
|
421
|
+
.filter((r) => apps.has(r.connectorId))
|
|
422
|
+
.map(publicConnection);
|
|
423
|
+
},
|
|
424
|
+
async listProviders(org) {
|
|
425
|
+
const [rows, connections] = await Promise.all([
|
|
426
|
+
store.listProviders(org),
|
|
427
|
+
store.listConnections(org),
|
|
428
|
+
]);
|
|
429
|
+
return rows
|
|
430
|
+
.filter((r) => providers.has(r.provider))
|
|
431
|
+
.map((r) => publicProvider(r, connections, sharedClientId(r.provider)));
|
|
432
|
+
},
|
|
433
|
+
// Setup steps the admin marked done, shared by every admin of the organization.
|
|
434
|
+
async listConfirmed(org) {
|
|
435
|
+
return store.listConfirmed(org);
|
|
436
|
+
},
|
|
437
|
+
async setConfirmed(org, step, by, on) {
|
|
438
|
+
const owner = confirmable.get(step);
|
|
439
|
+
if (!owner)
|
|
440
|
+
throw new InvalidInput(`No setup step ${step} to confirm`);
|
|
441
|
+
await store.setConfirmed(org, step, by, on);
|
|
442
|
+
await logAdmin(org, by, owner, on ? "confirm_step" : "unconfirm_step", step);
|
|
443
|
+
},
|
|
444
|
+
// Guided apps: the admin's form fields. Verified before anything is stored.
|
|
445
|
+
async connect(input) {
|
|
446
|
+
return save(app(input.connectorId), input);
|
|
447
|
+
},
|
|
448
|
+
// The provider's own sign-in, shared by its apps. Proven before it is stored. Reaching
|
|
449
|
+
// another account than before disconnects its apps.
|
|
450
|
+
async connectProvider(input) {
|
|
451
|
+
const target = providerApp(input.provider);
|
|
452
|
+
const existing = await store.getProvider(input.organizationId, input.provider);
|
|
453
|
+
const conn = await reaching(target.provider.name, () => target.client.connect(input.fields, existing ? { config: existing.config, secret: openProvider(existing) } : null));
|
|
454
|
+
return saveProvider(input.organizationId, target, conn, input.connectedBy, existing);
|
|
455
|
+
},
|
|
456
|
+
// A new key pair for the provider. Before it is connected, the certificate to upload
|
|
457
|
+
// first; after, one to rotate to, kept apart until the admin switches to it.
|
|
458
|
+
async newCertificate(org, id, by) {
|
|
459
|
+
const { provider } = providerApp(id);
|
|
460
|
+
if (!provider.certificate)
|
|
461
|
+
throw new InvalidInput(`${provider.name} does not sign in with a certificate`);
|
|
462
|
+
const made = newCertificate(brand().microsoft.appName);
|
|
463
|
+
const scope = providerScope(id);
|
|
464
|
+
const row = await store.getProvider(org, id);
|
|
465
|
+
if (!row || row.status === "pending") {
|
|
466
|
+
const sealed = key.seal({ private_key: made.privateKey }, credentialAad(org, scope));
|
|
467
|
+
const saved = await store.savePendingProvider({
|
|
468
|
+
organizationId: org,
|
|
469
|
+
provider: id,
|
|
470
|
+
config: { certificate: made.certificate },
|
|
471
|
+
credential: sealed.ciphertext,
|
|
472
|
+
credentialKeyId: sealed.keyId,
|
|
473
|
+
credentialScope: scope,
|
|
474
|
+
});
|
|
475
|
+
if (!saved)
|
|
476
|
+
throw new ConnectError(`${provider.name} was connected meanwhile. Reload the page.`);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
if (row.config.auth === SHARED_APP_AUTH)
|
|
480
|
+
throw new InvalidInput(`This connection uses ${brand().productName}'s shared app, which has no certificate of yours`);
|
|
481
|
+
const secret = { ...openProvider(row), next_private_key: made.privateKey };
|
|
482
|
+
const sealed = key.seal(secret, credentialAad(org, scope));
|
|
483
|
+
const saved = await store.updateProviderCredential(row.id, { ...row.config, nextCertificate: made.certificate }, sealed.ciphertext, sealed.keyId, scope, row.credential);
|
|
484
|
+
if (!saved)
|
|
485
|
+
throw new ProviderChanged();
|
|
486
|
+
}
|
|
487
|
+
await logAdmin(org, by, id, "create_certificate", `${!row || row.status === "pending" ? "first" : "next"} certificate ${describeCertificate(made.certificate).thumbprint}`);
|
|
488
|
+
return providerView(org, id);
|
|
489
|
+
},
|
|
490
|
+
// Signs in with the new certificate and, only if that works, makes it the one used. The
|
|
491
|
+
// old one keeps working in the app until the admin removes it there.
|
|
492
|
+
async activateCertificate(org, id, by) {
|
|
493
|
+
const { provider, client } = providerApp(id);
|
|
494
|
+
const row = await store.getProvider(org, id);
|
|
495
|
+
if (!row || row.status === "pending")
|
|
496
|
+
throw new InvalidInput(`${provider.name} is not connected`);
|
|
497
|
+
const { nextCertificate, ...config } = row.config;
|
|
498
|
+
const { next_private_key, client_secret: _old, ...secret } = openProvider(row);
|
|
499
|
+
if (!nextCertificate || !next_private_key)
|
|
500
|
+
throw new InvalidInput("There is no new certificate to switch to");
|
|
501
|
+
const candidate = {
|
|
502
|
+
config: { ...config, auth: "certificate", certificate: nextCertificate },
|
|
503
|
+
secret: { ...secret, private_key: next_private_key },
|
|
504
|
+
};
|
|
505
|
+
await reaching(provider.name, () => client.verify(candidate));
|
|
506
|
+
const scope = providerScope(id);
|
|
507
|
+
const sealed = key.seal(candidate.secret, credentialAad(org, scope));
|
|
508
|
+
const saved = await store.updateProviderCredential(row.id, candidate.config, sealed.ciphertext, sealed.keyId, scope, row.credential, true);
|
|
509
|
+
if (!saved)
|
|
510
|
+
throw new ProviderChanged();
|
|
511
|
+
await logAdmin(org, by, id, "switch_certificate", `${signInLabel(row.config)} -> ${signInLabel(candidate.config)}`);
|
|
512
|
+
return providerView(org, id);
|
|
513
|
+
},
|
|
514
|
+
async checkProvider(org, id) {
|
|
515
|
+
const { client } = providerApp(id);
|
|
516
|
+
const row = await store.getProvider(org, id);
|
|
517
|
+
if (!row)
|
|
518
|
+
return null;
|
|
519
|
+
if (row.status === "pending")
|
|
520
|
+
return { ok: false, error: "Not connected yet" };
|
|
521
|
+
const secret = openProvider(row);
|
|
522
|
+
try {
|
|
523
|
+
await reaching(providerApp(id).provider.name, () => client.verify({ config: row.config, secret }));
|
|
524
|
+
await store.markProviderChecked(row.id, true, row.credential);
|
|
525
|
+
await freshen(row, secret);
|
|
526
|
+
return { ok: true };
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
if (!(error instanceof ConnectError))
|
|
530
|
+
throw error;
|
|
531
|
+
await store.markProviderChecked(row.id, false, row.credential);
|
|
532
|
+
return { ok: false, error: error.message };
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
// Values the provider's apps ask for in their steps (connector `settings`), kept in the
|
|
536
|
+
// provider's config. Only over the credential read: a reconnect meanwhile wins.
|
|
537
|
+
async setProviderSettings(org, id, values, by) {
|
|
538
|
+
const { provider } = providerApp(id);
|
|
539
|
+
const allowed = providerSettings(deps.apps.map((a) => a.connector), provider.id);
|
|
540
|
+
const clean = {};
|
|
541
|
+
for (const [k, raw] of Object.entries(values)) {
|
|
542
|
+
const setting = allowed.get(k);
|
|
543
|
+
if (!setting)
|
|
544
|
+
throw new InvalidInput(`${provider.name} has no setting ${k}`);
|
|
545
|
+
const value = raw.trim();
|
|
546
|
+
if (!new RegExp(`^(?:${setting.pattern})$`).test(value))
|
|
547
|
+
throw new InvalidInput(`${setting.label}: ${setting.help ?? "not a valid value"}`);
|
|
548
|
+
clean[k] = value;
|
|
549
|
+
}
|
|
550
|
+
const row = await store.getProvider(org, provider.id);
|
|
551
|
+
if (!row || row.status === "pending")
|
|
552
|
+
throw new ConnectError(`Connect ${provider.name} first`);
|
|
553
|
+
const saved = await store.updateProviderCredential(row.id, { ...row.config, ...clean }, row.credential, row.credentialKeyId, row.credentialScope, row.credential);
|
|
554
|
+
if (!saved)
|
|
555
|
+
throw new ProviderChanged();
|
|
556
|
+
await logAdmin(org, by, provider.id, "settings", Object.entries(clean)
|
|
557
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
558
|
+
.join(" "));
|
|
559
|
+
return { settings: clean };
|
|
560
|
+
},
|
|
561
|
+
async disconnectProvider(org, id, by) {
|
|
562
|
+
const { provider } = providerApp(id);
|
|
563
|
+
const row = await store.getProvider(org, provider.id);
|
|
564
|
+
const apps = row
|
|
565
|
+
? (await store.listConnections(org))
|
|
566
|
+
.filter((c) => c.providerId === row.id)
|
|
567
|
+
.map((c) => c.connectorId)
|
|
568
|
+
: [];
|
|
569
|
+
const removed = await store.removeProvider(org, provider.id);
|
|
570
|
+
if (removed && row)
|
|
571
|
+
await logAdmin(org, by, provider.id, "disconnect", `${signInLabel(row.config)}; disconnected ${apps.join(", ") || "no apps"}`);
|
|
572
|
+
return removed;
|
|
573
|
+
},
|
|
574
|
+
// Approval of the deployment's shared app, where offered: where the admin goes to approve. The
|
|
575
|
+
// nonce goes back to the admin app, which keeps it in the admin's browser until the
|
|
576
|
+
// approval returns.
|
|
577
|
+
startConsent(input) {
|
|
578
|
+
const { target, approval } = approvalFor(input.connectorId);
|
|
579
|
+
const { state, nonce } = signState(stateKey, {
|
|
580
|
+
org: input.organizationId,
|
|
581
|
+
app: input.connectorId,
|
|
582
|
+
ceiling: validateCapabilities(target.connector, input.capabilities),
|
|
583
|
+
redirectUri: input.redirectUri,
|
|
584
|
+
});
|
|
585
|
+
return { url: approval.consentUrl({ state, redirectUri: input.redirectUri }), nonce };
|
|
586
|
+
},
|
|
587
|
+
// What the app sent back, twice. First the approval, which names the organization that
|
|
588
|
+
// approved but is unsigned, so the admin is sent on to sign in there (`redirect`). Then
|
|
589
|
+
// that sign-in, which proves the organization; only then is the provider connection
|
|
590
|
+
// saved, and the app too when it needs no settings of its own. The ceiling and the
|
|
591
|
+
// organization come from the signed state, never from the callback.
|
|
592
|
+
async finishConsent(input) {
|
|
593
|
+
const { target, approval, provider } = approvalFor(input.connectorId);
|
|
594
|
+
const state = verifyState(stateKey, input.state, {
|
|
595
|
+
org: input.organizationId,
|
|
596
|
+
app: input.connectorId,
|
|
597
|
+
nonce: input.nonce,
|
|
598
|
+
});
|
|
599
|
+
const name = provider.provider.name;
|
|
600
|
+
const { error, error_description: description } = input.params;
|
|
601
|
+
if (error)
|
|
602
|
+
throw new ConnectError(`${name} did not approve ${brand().productName}: ${(description ?? error).split(/\r?\n/)[0].slice(0, 300)}`);
|
|
603
|
+
if (!state.tenant) {
|
|
604
|
+
if (input.params.admin_consent?.toLowerCase() !== "true")
|
|
605
|
+
throw new ConnectError(`${name} did not approve ${brand().productName}`);
|
|
606
|
+
const tenant = (input.params.tenant ?? "").trim().toLowerCase();
|
|
607
|
+
const next = signState(stateKey, {
|
|
608
|
+
org: state.org,
|
|
609
|
+
app: state.app,
|
|
610
|
+
ceiling: state.ceiling,
|
|
611
|
+
redirectUri: state.redirectUri,
|
|
612
|
+
tenant,
|
|
613
|
+
}, Date.now(), input.nonce);
|
|
614
|
+
return {
|
|
615
|
+
redirect: approval.signInUrl({
|
|
616
|
+
tenant,
|
|
617
|
+
state: next.state,
|
|
618
|
+
redirectUri: state.redirectUri,
|
|
619
|
+
nonce: signInNonce(input.nonce),
|
|
620
|
+
}),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const code = input.params.code;
|
|
624
|
+
if (!code)
|
|
625
|
+
throw new ConnectError(`${name} did not confirm the sign-in. Start again.`);
|
|
626
|
+
const conn = await reaching(name, async () => {
|
|
627
|
+
await approval.proveTenant({
|
|
628
|
+
tenant: state.tenant,
|
|
629
|
+
code,
|
|
630
|
+
redirectUri: state.redirectUri,
|
|
631
|
+
nonce: signInNonce(input.nonce),
|
|
632
|
+
});
|
|
633
|
+
return approval.connect(state.tenant);
|
|
634
|
+
});
|
|
635
|
+
const existing = await store.getProvider(input.organizationId, provider.provider.id);
|
|
636
|
+
const saved = await saveProvider(input.organizationId, provider, conn, input.connectedBy, existing);
|
|
637
|
+
if (target.connector.fields.length)
|
|
638
|
+
return { provider: saved };
|
|
639
|
+
return save(target, {
|
|
640
|
+
organizationId: input.organizationId,
|
|
641
|
+
connectorId: input.connectorId,
|
|
642
|
+
capabilities: state.ceiling,
|
|
643
|
+
fields: {},
|
|
644
|
+
connectedBy: input.connectedBy,
|
|
645
|
+
});
|
|
646
|
+
},
|
|
647
|
+
// `record: false` only reads (e.g. the choices for a key's limits): a failure there must
|
|
648
|
+
// not mark the connection broken and cut every agent off. A recorded check that changes the
|
|
649
|
+
// connection's status is audited under `by`, the admin who asked for it.
|
|
650
|
+
async check(org, connectorId, { record = true, by } = {}) {
|
|
651
|
+
const target = app(connectorId);
|
|
652
|
+
const row = await store.getConnection(org, connectorId);
|
|
653
|
+
if (!row)
|
|
654
|
+
return null;
|
|
655
|
+
const providerRows = row.providerId ? await store.listProviders(org) : [];
|
|
656
|
+
const before = row.status;
|
|
657
|
+
const recordCheck = async (ok, target) => {
|
|
658
|
+
await store.markChecked(row.id, ok);
|
|
659
|
+
if (by && before !== (ok ? "connected" : "needs_reconnect"))
|
|
660
|
+
await logAdmin(org, by, connectorId, "check_app", target);
|
|
661
|
+
};
|
|
662
|
+
try {
|
|
663
|
+
const conn = resolve(row, providerRows);
|
|
664
|
+
const checks = await reaching(target.connector.name, () => probe(target, conn, row.capabilities));
|
|
665
|
+
// Moves a row sealed with the previous key onto the current one, read only or not
|
|
666
|
+
// (pages only read): it changes no status.
|
|
667
|
+
const provider = providerRows.find((p) => p.id === row.providerId);
|
|
668
|
+
if (provider)
|
|
669
|
+
await freshen(provider, conn.secret);
|
|
670
|
+
else if (row.credentialKeyId !== key.id) {
|
|
671
|
+
const sealed = key.seal(conn.secret, credentialAad(org, connectorId));
|
|
672
|
+
// Only over the credential read: one saved by a reconnect meanwhile stays.
|
|
673
|
+
await store.reseal(row.id, row.credential, sealed.ciphertext, sealed.keyId);
|
|
674
|
+
}
|
|
675
|
+
if (record)
|
|
676
|
+
await recordCheck(true, "ok");
|
|
677
|
+
return { ok: true, checks };
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
if (!(error instanceof ConnectError))
|
|
681
|
+
throw error;
|
|
682
|
+
if (record)
|
|
683
|
+
await recordCheck(false, `failed: ${error.message}`);
|
|
684
|
+
return { ok: false, error: error.message };
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
// Sites added after connecting, the whole list at once (adding or removing one).
|
|
688
|
+
async setSites(org, connectorId, raw) {
|
|
689
|
+
const { connector, vendor } = app(connectorId);
|
|
690
|
+
if (!connector.siteAccess || !vendor.sites)
|
|
691
|
+
throw new InvalidInput(`${connector.name} has no sites to add`);
|
|
692
|
+
const row = await store.getConnection(org, connectorId);
|
|
693
|
+
if (!row)
|
|
694
|
+
return null;
|
|
695
|
+
const sites = vendor.sites.normalize(raw);
|
|
696
|
+
const { sites: _old, ...rest } = row.config;
|
|
697
|
+
const config = sites.length ? { ...rest, sites: sites.join("\n") } : rest;
|
|
698
|
+
// Only over the config read here: a reconnect meanwhile must not get the old one back.
|
|
699
|
+
if (!(await store.updateConfig(org, connectorId, config, row.config)))
|
|
700
|
+
throw new ProviderChanged();
|
|
701
|
+
return { sites };
|
|
702
|
+
},
|
|
703
|
+
// Whether Microsoft gave the app each added site (or the one asked about), read live.
|
|
704
|
+
async siteAccess(org, connectorId, only) {
|
|
705
|
+
const { connector, vendor } = app(connectorId);
|
|
706
|
+
if (!connector.siteAccess || !vendor.sites)
|
|
707
|
+
throw new InvalidInput(`${connector.name} has no sites to check`);
|
|
708
|
+
const row = await store.getConnection(org, connectorId);
|
|
709
|
+
if (!row)
|
|
710
|
+
return null;
|
|
711
|
+
const added = configSites(row.config);
|
|
712
|
+
const sites = only === undefined ? added : vendor.sites.normalize([only]);
|
|
713
|
+
if (only !== undefined && !added.includes(sites[0]))
|
|
714
|
+
throw new InvalidInput(`${only} is not one of the added sites`);
|
|
715
|
+
const conn = resolve(row, row.providerId ? await store.listProviders(org) : []);
|
|
716
|
+
const write = connector.siteAccess.write.some((id) => row.capabilities.includes(id));
|
|
717
|
+
const results = [];
|
|
718
|
+
for (const site of sites)
|
|
719
|
+
results.push(await reaching(connector.name, () => vendor.sites.access(conn, site, write)));
|
|
720
|
+
return { sites: results };
|
|
721
|
+
},
|
|
722
|
+
// Whether the app can reach one agent's own address (its mailbox) yet, read live.
|
|
723
|
+
// A check of an address no key holds yet is recorded, so lookups of other mailboxes in the
|
|
724
|
+
// tenant leave a trace.
|
|
725
|
+
async agentAccess(org, connectorId, value, by) {
|
|
726
|
+
const { connector, vendor } = app(connectorId);
|
|
727
|
+
const access = connector.agentAccess;
|
|
728
|
+
if (!access)
|
|
729
|
+
throw new InvalidInput(`${connector.name} has no agent address to check`);
|
|
730
|
+
const limit = connector.limits.find((l) => l.id === access.limit);
|
|
731
|
+
if (limit.pattern && !new RegExp(limit.pattern).test(value))
|
|
732
|
+
throw new InvalidInput(`${value} is not a valid ${limit.label.toLowerCase()}`);
|
|
733
|
+
const row = await store.getConnection(org, connectorId);
|
|
734
|
+
if (!row)
|
|
735
|
+
return null;
|
|
736
|
+
const conn = resolve(row, row.providerId ? await store.listProviders(org) : []);
|
|
737
|
+
const held = (await store.listAgents(org)).some((a) => a.grants.some((g) => g.connectorId === connectorId &&
|
|
738
|
+
String(g.limits[access.limit] ?? "").toLowerCase() === value.toLowerCase()));
|
|
739
|
+
if (!held)
|
|
740
|
+
await logAdmin(org, by, connectorId, "agent_access_check", value);
|
|
741
|
+
const res = await reaching(connector.name, () => vendor.call(conn, {
|
|
742
|
+
method: "GET",
|
|
743
|
+
path: access.path.replace("{value}", encodeURIComponent(value)),
|
|
744
|
+
}));
|
|
745
|
+
const status = res.status >= 200 && res.status < 300
|
|
746
|
+
? "granted"
|
|
747
|
+
: res.status === 401 || res.status === 403
|
|
748
|
+
? "not_granted"
|
|
749
|
+
: res.status === 404
|
|
750
|
+
? "not_found"
|
|
751
|
+
: "failed";
|
|
752
|
+
const message = {
|
|
753
|
+
granted: access.granted,
|
|
754
|
+
not_granted: access.notGranted,
|
|
755
|
+
not_found: access.notFound,
|
|
756
|
+
failed: `${connector.name} answered ${res.status}. Check again later.`,
|
|
757
|
+
}[status];
|
|
758
|
+
if (status !== "granted")
|
|
759
|
+
return { value, status, message };
|
|
760
|
+
// Opening it is not enough: each ticked capability's own role, proven by a call that
|
|
761
|
+
// changes nothing.
|
|
762
|
+
const missing = [];
|
|
763
|
+
for (const capability of connector.capabilities) {
|
|
764
|
+
const probe = access.probes[capability.id];
|
|
765
|
+
if (!probe || !row.capabilities.includes(capability.id))
|
|
766
|
+
continue;
|
|
767
|
+
const answer = await reaching(connector.name, () => vendor.call(conn, {
|
|
768
|
+
method: probe.method,
|
|
769
|
+
path: probe.path.replace("{value}", encodeURIComponent(value)),
|
|
770
|
+
body: probe.body,
|
|
771
|
+
}));
|
|
772
|
+
if (answer.status === 401 || answer.status === 403)
|
|
773
|
+
missing.push(`${capability.label} (${capability.scopes.join(", ")})`);
|
|
774
|
+
else if (!probe.allowed.includes(answer.status))
|
|
775
|
+
return {
|
|
776
|
+
value,
|
|
777
|
+
status: "failed",
|
|
778
|
+
message: `${connector.name} answered ${answer.status} when checking ${capability.label}. Check again later.`,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
if (missing.length)
|
|
782
|
+
return {
|
|
783
|
+
value,
|
|
784
|
+
status: "not_granted",
|
|
785
|
+
message: access.missing.replace("{missing}", missing.join(", ")),
|
|
786
|
+
};
|
|
787
|
+
return { value, status, message };
|
|
788
|
+
},
|
|
789
|
+
// The saved ceiling, or null when the app is not connected.
|
|
790
|
+
async setCeiling(org, connectorId, ceiling) {
|
|
791
|
+
const caps = validateCapabilities(app(connectorId).connector, ceiling);
|
|
792
|
+
return (await store.setCeiling(org, connectorId, caps)) ? caps : null;
|
|
793
|
+
},
|
|
794
|
+
async setGrant(org, agentId, connectorId, capabilities, limits) {
|
|
795
|
+
const { connector, vendor } = app(connectorId);
|
|
796
|
+
const caps = validateCapabilities(connector, capabilities);
|
|
797
|
+
const checked = validateLimits(connector, limits);
|
|
798
|
+
const row = await store.getConnection(org, connectorId);
|
|
799
|
+
if (!row)
|
|
800
|
+
throw new InvalidInput(`Connect ${connector.name} before giving a key access to it`);
|
|
801
|
+
// Never stored above the app's ceiling: raising the ceiling later must not turn it on.
|
|
802
|
+
const above = (ceiling) => {
|
|
803
|
+
const over = caps.filter((c) => !ceiling.includes(c));
|
|
804
|
+
if (over.length)
|
|
805
|
+
throw new InvalidInput(`${connector.name} does not allow ${over
|
|
806
|
+
.map((id) => connector.capabilities.find((c) => c.id === id).label)
|
|
807
|
+
.join(", ")}. Turn it on for the app first, or untick it.`);
|
|
808
|
+
};
|
|
809
|
+
above(row.capabilities);
|
|
810
|
+
if (connector.siteAccess && vendor.sites) {
|
|
811
|
+
const added = configSites(row.config);
|
|
812
|
+
const missing = (checked.sites ?? []).find((s) => !added.includes(vendor.sites.normalize([s])[0]));
|
|
813
|
+
if (missing)
|
|
814
|
+
throw new InvalidInput(`${missing} is not one of the sites added to ${connector.name}`);
|
|
815
|
+
}
|
|
816
|
+
const saved = await store.setGrant(org, agentId, connectorId, caps, checked);
|
|
817
|
+
// Lost to a ceiling lowered meanwhile: say so rather than "not found".
|
|
818
|
+
if (!saved) {
|
|
819
|
+
const now = await store.getConnection(org, connectorId);
|
|
820
|
+
if (!now)
|
|
821
|
+
throw new InvalidInput(`Connect ${connector.name} before giving a key access to it`);
|
|
822
|
+
above(now.capabilities);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
return { capabilities: caps, limits: checked };
|
|
826
|
+
},
|
|
827
|
+
/** A call with the organization's own connection, no agent's (an event poll or a webhook
|
|
828
|
+
* registration). Null when the app is not connected. */
|
|
829
|
+
async appCall(org, connectorId, request) {
|
|
830
|
+
const target = app(connectorId);
|
|
831
|
+
const row = await store.getConnection(org, connectorId);
|
|
832
|
+
if (!row || row.status !== "connected")
|
|
833
|
+
return null;
|
|
834
|
+
const providerRows = row.providerId ? await store.listProviders(org) : [];
|
|
835
|
+
return target.vendor.call(resolve(row, providerRows), request);
|
|
836
|
+
},
|
|
837
|
+
// Resolves an agent key to the tools that agent may call right now.
|
|
838
|
+
async agentSession(rawKey) {
|
|
839
|
+
if (!looksLikeAgentKey(rawKey))
|
|
840
|
+
return null;
|
|
841
|
+
const agent = await store.agentByKeyHash(hashAgentKey(rawKey));
|
|
842
|
+
if (!agent || agent.status !== "active")
|
|
843
|
+
return null;
|
|
844
|
+
const [grants, connections, providerRows] = await Promise.all([
|
|
845
|
+
store.grantsFor(agent.id),
|
|
846
|
+
store.listConnections(agent.organizationId),
|
|
847
|
+
store.listProviders(agent.organizationId),
|
|
848
|
+
]);
|
|
849
|
+
const needsBot = grants.some((g) => apps.get(g.connectorId)?.connector.signsInAs);
|
|
850
|
+
if (needsBot && !deps.bots)
|
|
851
|
+
throw new Error("An app signs in as the agent's bot, but bots are not loaded");
|
|
852
|
+
const bot = needsBot ? await deps.bots.botForAgent(agent.organizationId, agent.id) : null;
|
|
853
|
+
const toolsets = sessionToolsets(agent, grants, connections, providerRows, bot);
|
|
854
|
+
const granted = new Set(toolsets.flatMap((t) => [...t.allowed]));
|
|
855
|
+
return {
|
|
856
|
+
agent,
|
|
857
|
+
toolsets,
|
|
858
|
+
// A call to one of our tools the agent was not given never reaches a handler (the MCP
|
|
859
|
+
// server answers "not found"), so it is recorded here.
|
|
860
|
+
async refuseUngranted(tool, args) {
|
|
861
|
+
if (granted.has(tool))
|
|
862
|
+
return;
|
|
863
|
+
const owner = deps.apps.find((a) => a.toolset.names.includes(tool));
|
|
864
|
+
if (!owner)
|
|
865
|
+
return;
|
|
866
|
+
await store.audit({
|
|
867
|
+
organizationId: agent.organizationId,
|
|
868
|
+
agentId: agent.id,
|
|
869
|
+
agentName: agent.name,
|
|
870
|
+
connectorId: owner.connector.id,
|
|
871
|
+
tool,
|
|
872
|
+
target: argumentsTarget(args),
|
|
873
|
+
decision: "denied",
|
|
874
|
+
reason: "This agent is not allowed to do this",
|
|
875
|
+
vendorStatus: null,
|
|
876
|
+
});
|
|
877
|
+
},
|
|
878
|
+
};
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
function sessionToolsets(agent, grants, connections, providerRows, bot) {
|
|
882
|
+
return grants.flatMap((grant) => {
|
|
883
|
+
const target = apps.get(grant.connectorId);
|
|
884
|
+
const row = connections.find((c) => c.connectorId === grant.connectorId);
|
|
885
|
+
if (!target || !row || row.status !== "connected")
|
|
886
|
+
return [];
|
|
887
|
+
const provider = row.providerId ? providerRows.find((p) => p.id === row.providerId) : null;
|
|
888
|
+
if (provider !== null && provider?.status !== "connected")
|
|
889
|
+
return [];
|
|
890
|
+
const capabilities = effectiveCapabilities(target.connector, grant.capabilities, row.capabilities);
|
|
891
|
+
// Its own bot, once that is set up; until then the agent gets none of these tools.
|
|
892
|
+
const asBot = target.connector.signsInAs === "agentBot";
|
|
893
|
+
if (asBot && (bot?.status !== "connected" || !bot.botAppId))
|
|
894
|
+
return [];
|
|
895
|
+
let conn = null;
|
|
896
|
+
const ctx = {
|
|
897
|
+
limits: grant.limits,
|
|
898
|
+
config: provider ? { ...provider.config, ...row.config } : row.config,
|
|
899
|
+
call: (request) => {
|
|
900
|
+
// An app signing in as the agent's bot reaches the Microsoft connection only to look
|
|
901
|
+
// a person up in the directory (Teams' approver), never to read or write anything else.
|
|
902
|
+
if (asBot && !(request.method === "GET" && DIRECTORY_LOOKUP.test(request.path)))
|
|
903
|
+
throw new Error(`${grant.connectorId} calls go through the agent's bot`);
|
|
904
|
+
return target.vendor.call((conn ??= resolve(row, providerRows)), request);
|
|
905
|
+
},
|
|
906
|
+
forbidden: Object.fromEntries(capabilities.flatMap((c) => c.forbidden
|
|
907
|
+
? c.tools.map((t) => [t, c.forbidden.replaceAll("{scopes}", c.scopes.join(", "))])
|
|
908
|
+
: [])),
|
|
909
|
+
...(asBot ? { teams: deps.bots.access(bot) } : {}),
|
|
910
|
+
audit: (entry) => store.audit({
|
|
911
|
+
organizationId: agent.organizationId,
|
|
912
|
+
agentId: agent.id,
|
|
913
|
+
agentName: agent.name,
|
|
914
|
+
connectorId: grant.connectorId,
|
|
915
|
+
...entry,
|
|
916
|
+
}),
|
|
917
|
+
};
|
|
918
|
+
return [
|
|
919
|
+
{ toolset: target.toolset, ctx, allowed: new Set(capabilities.flatMap((c) => c.tools)) },
|
|
920
|
+
];
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
//# sourceMappingURL=service.js.map
|