@arbium/enterprise-agent-connect-client 0.0.0-stage → 0.1.2-dev.cba5f68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.d.ts +1274 -0
- package/lib/index.js +235 -0
- package/lib/schema.d.ts +6448 -0
- package/lib/schema.js +5 -0
- package/lib/steps.d.ts +1448 -0
- package/lib/steps.js +725 -0
- package/lib/types.d.ts +62 -0
- package/lib/types.js +1 -0
- package/package.json +19 -4
- package/README.md +0 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { chosenSteps } from "./steps.js";
|
|
2
|
+
export * from "./steps.js";
|
|
3
|
+
/** A refusal the admin should read (4xx with an `error`): bad input, a failed connection
|
|
4
|
+
* check, a name in use. A 5xx or an answer without an `error` throws a plain Error. */
|
|
5
|
+
export class AgentConnectError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
constructor(message, status) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** The admin API (v1), server to server: the token must never reach a browser. */
|
|
13
|
+
export function createClient(options) {
|
|
14
|
+
const base = `${options.baseUrl.replace(/\/+$/, "")}/agent-connect/admin/v1`;
|
|
15
|
+
async function send(method, path, body) {
|
|
16
|
+
return (options.fetch ?? globalThis.fetch)(`${base}${path}`, {
|
|
17
|
+
method,
|
|
18
|
+
headers: {
|
|
19
|
+
authorization: `Bearer ${options.token}`,
|
|
20
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
21
|
+
},
|
|
22
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
23
|
+
cache: "no-store",
|
|
24
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 45_000),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function refusal(res, method, path) {
|
|
28
|
+
const data = await res.json().catch(() => null);
|
|
29
|
+
const message = data?.error;
|
|
30
|
+
if (res.status < 500 && typeof message === "string")
|
|
31
|
+
throw new AgentConnectError(message, res.status);
|
|
32
|
+
throw new Error(`agent-connect ${method} ${path} answered ${res.status}`);
|
|
33
|
+
}
|
|
34
|
+
async function call(method, path, body) {
|
|
35
|
+
const res = await send(method, path, body);
|
|
36
|
+
if (res.status === 204)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (res.ok)
|
|
39
|
+
return (await res.json());
|
|
40
|
+
return refusal(res, method, path);
|
|
41
|
+
}
|
|
42
|
+
/** A row that may not exist yet: 404 is null, not an error. */
|
|
43
|
+
async function maybe(path) {
|
|
44
|
+
const res = await send("GET", path);
|
|
45
|
+
if (res.status === 404)
|
|
46
|
+
return null;
|
|
47
|
+
if (res.ok)
|
|
48
|
+
return (await res.json());
|
|
49
|
+
return refusal(res, "GET", path);
|
|
50
|
+
}
|
|
51
|
+
const seg = encodeURIComponent;
|
|
52
|
+
const org = (id) => `/orgs/${seg(id)}`;
|
|
53
|
+
const app = (o, a) => `${org(o)}/connections/${seg(a)}`;
|
|
54
|
+
const provider = (o, p) => `${org(o)}/providers/${seg(p)}`;
|
|
55
|
+
const agent = (o, id) => `${org(o)}/agents/${seg(id)}`;
|
|
56
|
+
const bot = (o, id) => `${agent(o, id)}/teams-bot`;
|
|
57
|
+
const client = {
|
|
58
|
+
organizations: {
|
|
59
|
+
list: () => call("GET", "/orgs"),
|
|
60
|
+
get: (id) => maybe(org(id)),
|
|
61
|
+
/** Makes the organization with this id if missing; `name` replaces the kept one. */
|
|
62
|
+
ensure: (id, name) => call("PUT", org(id), name === undefined ? {} : { name }),
|
|
63
|
+
create: (name) => call("POST", "/orgs", { name }),
|
|
64
|
+
},
|
|
65
|
+
branding: () => call("GET", "/branding"),
|
|
66
|
+
connectors: () => call("GET", "/connectors"),
|
|
67
|
+
providers: () => call("GET", "/providers"),
|
|
68
|
+
providerConnections: (o) => call("GET", `${org(o)}/providers`),
|
|
69
|
+
connectProvider: (o, p, body) => call("PUT", provider(o, p), body),
|
|
70
|
+
checkProvider: (o, p) => call("POST", `${provider(o, p)}/check`),
|
|
71
|
+
// `by`: the admin, recorded in the service's audit log with each change.
|
|
72
|
+
/** Before connecting, the certificate to upload; after, one to rotate to. */
|
|
73
|
+
newCertificate: (o, p, by) => call("POST", `${provider(o, p)}/certificate`, { by }),
|
|
74
|
+
/** Signs in with the new certificate and, if that works, switches to it. */
|
|
75
|
+
activateCertificate: (o, p, by) => call("POST", `${provider(o, p)}/certificate/activate`, {
|
|
76
|
+
by,
|
|
77
|
+
}),
|
|
78
|
+
/** Disconnects the provider and every app signing in through it. */
|
|
79
|
+
disconnectProvider: (o, p, by) => call("DELETE", provider(o, p), { by }),
|
|
80
|
+
/** Values an app's steps ask for, kept on the provider connection. */
|
|
81
|
+
setProviderSettings: (o, p, values, by) => call("PUT", `${provider(o, p)}/settings`, { values, by }),
|
|
82
|
+
connections: (o) => call("GET", `${org(o)}/connections`),
|
|
83
|
+
connect: (o, a, body) => call("PUT", app(o, a), body),
|
|
84
|
+
startConsent: (o, a, body) => call("POST", `${app(o, a)}/consent`, body),
|
|
85
|
+
/** `redirect`: the approval came back; the admin signs in to that organization next.
|
|
86
|
+
* `provider`: the approval connected the provider; the app still needs its own settings. */
|
|
87
|
+
finishConsent: (o, a, body) => call("POST", `${app(o, a)}/consent/callback`, body),
|
|
88
|
+
setCeiling: (o, a, capabilities, by) => call("PATCH", app(o, a), { capabilities, by }),
|
|
89
|
+
/** Recorded: a failure marks the app as needing reconnecting, so it names the admin. */
|
|
90
|
+
check: (o, a, by) => call("POST", `${app(o, a)}/check`, { by }),
|
|
91
|
+
/** The same checks, read only: a failure here does not mark the app. */
|
|
92
|
+
peek: (o, a) => call("POST", `${app(o, a)}/check?record=false`),
|
|
93
|
+
/** The sites added to an app, the whole list at once. */
|
|
94
|
+
setSites: (o, a, sites, by) => call("PUT", `${app(o, a)}/sites`, { sites, by }),
|
|
95
|
+
/** Whether the app can open each added site, or just `site`. */
|
|
96
|
+
checkSites: (o, a, site) => call("POST", `${app(o, a)}/sites/check`, site === undefined ? {} : { site }),
|
|
97
|
+
/** Whether the app can reach one agent's own address (its mailbox) yet. */
|
|
98
|
+
agentAccess: (o, a, value, by) => call("POST", `${app(o, a)}/agent-access`, { value, by }),
|
|
99
|
+
disconnect: (o, a, by) => call("DELETE", app(o, a), { by }),
|
|
100
|
+
agents: (o) => call("GET", `${org(o)}/agents`),
|
|
101
|
+
createAgent: (o, body) => call("POST", `${org(o)}/agents`, body),
|
|
102
|
+
setAgentStatus: (o, id, status, by) => call("PATCH", agent(o, id), { status, by }),
|
|
103
|
+
newKey: (o, id, by) => call("POST", `${agent(o, id)}/key`, { by }),
|
|
104
|
+
deleteAgent: (o, id, by) => call("DELETE", agent(o, id), { by }),
|
|
105
|
+
setGrant: (o, id, a, capabilities, limits, by) => call("PUT", `${agent(o, id)}/grants/${seg(a)}`, { capabilities, limits, by }),
|
|
106
|
+
removeGrant: (o, id, a, by) => call("DELETE", `${agent(o, id)}/grants/${seg(a)}`, { by }),
|
|
107
|
+
/** Recent requests, newest first, of one key (`agent`), one app, or both. */
|
|
108
|
+
audit: (o, filter, limit = 50) => call("GET", `${org(o)}/audit?${new URLSearchParams({ limit: String(limit), ...filter })}`),
|
|
109
|
+
// Setup steps an admin marked done (they cannot be checked), shared by every admin.
|
|
110
|
+
confirmed: async (o) => (await call("GET", `${org(o)}/confirmed`)).map((c) => c.step),
|
|
111
|
+
setConfirmed: (o, step, on, by) => call(on ? "PUT" : "DELETE", `${org(o)}/confirmed/${seg(step)}`, { by }),
|
|
112
|
+
// ---- Teams: each agent's own bot ----
|
|
113
|
+
teamsBots: (o) => call("GET", `${org(o)}/teams/bots`),
|
|
114
|
+
createTeamsBot: (o, id, displayName, by) => call("POST", bot(o, id), { displayName, by }),
|
|
115
|
+
/** Proves the application ID the setup script printed by signing in as it. */
|
|
116
|
+
connectTeamsBot: (o, id, botAppId, by) => call("PUT", bot(o, id), { botAppId, by }),
|
|
117
|
+
checkTeamsBot: (o, id) => call("POST", `${bot(o, id)}/check`),
|
|
118
|
+
teamsTest: (o, id, conversationId, by) => call("POST", `${bot(o, id)}/test`, { conversationId, by }),
|
|
119
|
+
/** Forgets the bot. What Add to Teams made is deleted (`failed`: what could not be);
|
|
120
|
+
* `removal` is the commands for the rest. */
|
|
121
|
+
removeTeamsBot: (o, id, by) => call("DELETE", bot(o, id), { by }),
|
|
122
|
+
newTeamsCertificate: (o, id, by) => call("POST", `${bot(o, id)}/certificate`, { by }),
|
|
123
|
+
activateTeamsCertificate: (o, id, by) => call("POST", `${bot(o, id)}/certificate/activate`, {
|
|
124
|
+
by,
|
|
125
|
+
}),
|
|
126
|
+
/** The Teams page's set-up-once steps, checked live. */
|
|
127
|
+
teamsSetupState: (o) => call("GET", `${org(o)}/teams/setup`),
|
|
128
|
+
/** Makes the agent's bot in the tenant, each part only if missing. */
|
|
129
|
+
addToTeams: (o, id, displayName, by) => call("POST", `${bot(o, id)}/add`, { displayName, by }),
|
|
130
|
+
startTeamsPublish: (o, id, redirectUri) => call("POST", `${bot(o, id)}/publish`, { redirectUri }),
|
|
131
|
+
finishTeamsPublish: (o, body) => call("POST", `${org(o)}/teams/publish/callback`, body),
|
|
132
|
+
teamsTargets: (o, id, kind, q) => call("GET", `${bot(o, id)}/targets?${new URLSearchParams({ kind, q })}`),
|
|
133
|
+
installTeamsBot: (o, id, kind, target, by) => call("POST", `${bot(o, id)}/install`, { kind, id: target, by }),
|
|
134
|
+
/** A file the service makes (the setup script, certificate or Teams app package), as is. */
|
|
135
|
+
teamsFile: async (o, id, file) => {
|
|
136
|
+
const path = `${bot(o, id)}/${file}`;
|
|
137
|
+
const res = await send("GET", path);
|
|
138
|
+
return res.ok ? res : refusal(res, "GET", path);
|
|
139
|
+
},
|
|
140
|
+
teamsTrigger: (o, id) => maybe(`${agent(o, id)}/teams-trigger`),
|
|
141
|
+
setTeamsTrigger: (o, id, body) => call("PUT", `${agent(o, id)}/teams-trigger`, body),
|
|
142
|
+
teamsRuns: (o, id, limit = 20) => call("GET", `${agent(o, id)}/teams-runs?limit=${limit}`),
|
|
143
|
+
// App events that start agents (docs/triggers.md). A webhook's secret is in the answer to
|
|
144
|
+
// subscribeEvents and rotateEventSecret only.
|
|
145
|
+
eventSubscriptions: (o) => call("GET", `${org(o)}/event-subscriptions`),
|
|
146
|
+
subscribeEvents: (o, body) => call("POST", `${org(o)}/event-subscriptions`, body),
|
|
147
|
+
rotateEventSecret: (o, id, by) => call("POST", `${org(o)}/event-subscriptions/${seg(id)}/secret`, {
|
|
148
|
+
by,
|
|
149
|
+
}),
|
|
150
|
+
unsubscribeEvents: (o, id, by) => call("DELETE", `${org(o)}/event-subscriptions/${seg(id)}`, { by }),
|
|
151
|
+
eventTriggers: (o, id) => call("GET", `${agent(o, id)}/event-triggers`),
|
|
152
|
+
setEventTrigger: (o, id, a, body) => call("PUT", `${agent(o, id)}/event-triggers/${seg(a)}`, body),
|
|
153
|
+
removeEventTrigger: (o, id, a, by) => call("DELETE", `${agent(o, id)}/event-triggers/${seg(a)}`, { by }),
|
|
154
|
+
eventRuns: (o, id, limit = 20) => call("GET", `${agent(o, id)}/event-runs?limit=${limit}`),
|
|
155
|
+
// The organization's Claude API key: write-only, shown by its last four characters.
|
|
156
|
+
claudeKey: (o) => call("GET", `${org(o)}/claude-key`),
|
|
157
|
+
setClaudeKey: (o, apiKey, by) => call("PUT", `${org(o)}/claude-key`, { apiKey, by }),
|
|
158
|
+
removeClaudeKey: (o, by) => call("DELETE", `${org(o)}/claude-key`, { by }),
|
|
159
|
+
claudeAgents: (o) => call("GET", `${org(o)}/claude-key/agents`),
|
|
160
|
+
};
|
|
161
|
+
/** One app's live site check, or null when the app cannot run it (e.g. the vendor refuses
|
|
162
|
+
* the sign-in): the page then shows the app as needing attention instead of failing. */
|
|
163
|
+
async function checkedSites(o, a) {
|
|
164
|
+
try {
|
|
165
|
+
return (await client.checkSites(o, a)).sites;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (!(error instanceof AgentConnectError))
|
|
169
|
+
throw error;
|
|
170
|
+
console.error(`[agent-connect] site check for ${a}`, error.message);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const peeked = (o, a) => client.peek(o, a).catch((error) => {
|
|
175
|
+
if (!(error instanceof AgentConnectError))
|
|
176
|
+
throw error;
|
|
177
|
+
console.error(`[agent-connect] check for ${a}`, error.message);
|
|
178
|
+
return null;
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
...client,
|
|
182
|
+
checkedSites,
|
|
183
|
+
/** The read-only live check of each app of its own that is on, for warnings no stored
|
|
184
|
+
* status holds (e.g. an app can see nothing yet). Null for an app whose check could not
|
|
185
|
+
* run. */
|
|
186
|
+
async appChecks(o, connectors, connections) {
|
|
187
|
+
const apps = connectors.filter((c) => !c.provider &&
|
|
188
|
+
connections.some((x) => x.connectorId === c.id && x.status === "connected"));
|
|
189
|
+
const results = await Promise.all(apps.map((c) => peeked(o, c.id)));
|
|
190
|
+
return Object.fromEntries(apps.map((c, i) => [c.id, results[i]]));
|
|
191
|
+
},
|
|
192
|
+
/** Each provider app's live state for its steps (`appState`), read only: its own check
|
|
193
|
+
* and its sites while on, the organization's Claude key where a capability needs one,
|
|
194
|
+
* and its chosen steps' own live checks (none while those steps are not chosen, so no
|
|
195
|
+
* vendor call). An app's page, its provider's page and the Integrations tab all read it
|
|
196
|
+
* here, so they agree. The Claude key and the step checks are read once for all apps. */
|
|
197
|
+
async appStates(o, connectors, connections) {
|
|
198
|
+
const apps = connectors.map((connector) => {
|
|
199
|
+
const connection = connections.find((c) => c.connectorId === connector.id) ?? null;
|
|
200
|
+
const liveSteps = chosenSteps(connector, [...connector.setup, ...connector.afterConnect], connection?.config ?? {}, connection?.capabilities ?? []).filter((s) => s.live);
|
|
201
|
+
return { connector, connection, liveSteps };
|
|
202
|
+
});
|
|
203
|
+
const liveSteps = apps.flatMap((a) => a.liveSteps);
|
|
204
|
+
const [claudeKey, live, perApp] = await Promise.all([
|
|
205
|
+
apps.some((a) => a.connector.capabilities.some((c) => c.claudeKey))
|
|
206
|
+
? client.claudeKey(o)
|
|
207
|
+
: null,
|
|
208
|
+
liveSteps.length
|
|
209
|
+
? client.teamsSetupState(o).catch((error) => {
|
|
210
|
+
if (!(error instanceof AgentConnectError)) {
|
|
211
|
+
console.error("[agent-connect] step checks", error);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
// The service's own answer (e.g. what the connection lacks) goes on each step.
|
|
215
|
+
const failing = { state: "failing", message: error.message };
|
|
216
|
+
return Object.fromEntries(liveSteps.map((s) => [s.live, failing]));
|
|
217
|
+
})
|
|
218
|
+
: null,
|
|
219
|
+
Promise.all(apps.map(({ connector, connection }) => Promise.all([
|
|
220
|
+
connection ? peeked(o, connector.id) : null,
|
|
221
|
+
connection && connector.siteAccess ? checkedSites(o, connector.id) : null,
|
|
222
|
+
]))),
|
|
223
|
+
]);
|
|
224
|
+
return Object.fromEntries(apps.map((a, i) => [
|
|
225
|
+
a.connector.id,
|
|
226
|
+
{
|
|
227
|
+
health: perApp[i][0],
|
|
228
|
+
sites: perApp[i][1],
|
|
229
|
+
claudeKey: claudeKey?.set === true,
|
|
230
|
+
live: a.liveSteps.length ? live : null,
|
|
231
|
+
},
|
|
232
|
+
]));
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|