@tangle-network/agent-integrations 0.53.53 → 0.53.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/data/bundled-adapter-manifests.json +1 -0
- package/dist/audit-DMTzJQJU.d.ts +51 -0
- package/dist/catalog.d.ts +3 -3
- package/dist/catalog.js +2 -12
- package/dist/chunk-A4OJ5EQT.js +133156 -0
- package/dist/chunk-A4OJ5EQT.js.map +1 -0
- package/dist/chunk-ABA4HWIQ.js +58 -0
- package/dist/chunk-ABA4HWIQ.js.map +1 -0
- package/dist/chunk-DED3J7F6.js +1 -0
- package/dist/chunk-ESY2FBBM.js +33 -0
- package/dist/chunk-ESY2FBBM.js.map +1 -0
- package/dist/chunk-GIK6EXU2.js +121 -0
- package/dist/chunk-GIK6EXU2.js.map +1 -0
- package/dist/{chunk-TWR2KAZG.js → chunk-MH4IEJCL.js} +1 -1
- package/dist/chunk-MH4IEJCL.js.map +1 -0
- package/dist/chunk-QVWTBQXP.js +267 -0
- package/dist/chunk-QVWTBQXP.js.map +1 -0
- package/dist/{chunk-2GEGKSL5.js → chunk-QWOX5SIM.js} +6 -33
- package/dist/chunk-QWOX5SIM.js.map +1 -0
- package/dist/chunk-RSJRICPU.js +394 -0
- package/dist/chunk-RSJRICPU.js.map +1 -0
- package/dist/chunk-WPQJK4S4.js +400 -0
- package/dist/chunk-WPQJK4S4.js.map +1 -0
- package/dist/connect/index.d.ts +2 -2
- package/dist/connectors/adapters/index.d.ts +2 -2
- package/dist/connectors/index.d.ts +10 -22
- package/dist/connectors/index.js +12 -10
- package/dist/{consumer-BbQWkIzU.d.ts → consumer-CIq2D22y.d.ts} +4 -51
- package/dist/consumer.d.ts +5 -4
- package/dist/{core-types-A-xIaTzb.d.ts → core-types-DZJQRBya.d.ts} +2 -2
- package/dist/coverage-catalog.d.ts +2 -2
- package/dist/coverage-catalog.js +1 -1
- package/dist/{importers-BMigcDVf.d.ts → importers-Cyk5osDs.d.ts} +1 -1
- package/dist/index.d.ts +17 -104
- package/dist/index.js +72 -62
- package/dist/internal/bundled-manifest-runtime.d.ts +15 -0
- package/dist/internal/bundled-manifest-runtime.js +13 -0
- package/dist/internal/bundled-manifest-runtime.js.map +1 -0
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +10 -5
- package/dist/mcp.js.map +1 -1
- package/dist/middleware/index.d.ts +2 -2
- package/dist/registry-core-DqdQjTkh.d.ts +49 -0
- package/dist/registry.d.ts +6 -49
- package/dist/registry.js +16 -10
- package/dist/runtime.d.ts +3 -3
- package/dist/runtime.js +3 -12
- package/dist/specs.d.ts +2 -2
- package/dist/specs.js +4 -6
- package/dist/{tangle-id-gFOGXe4Y.d.ts → tangle-id-Oy2z4W3p.d.ts} +1 -1
- package/dist/{types-DpKEBiX6.d.ts → types-DIis6QtV.d.ts} +1 -1
- package/dist/worker.d.ts +99 -0
- package/dist/worker.js +28 -0
- package/dist/worker.js.map +1 -0
- package/docs/repo-structure.md +29 -6
- package/package.json +10 -1
- package/dist/chunk-2GEGKSL5.js.map +0 -1
- package/dist/chunk-E2QYXDJQ.js +0 -147
- package/dist/chunk-E2QYXDJQ.js.map +0 -1
- package/dist/chunk-P4FFI752.js +0 -1
- package/dist/chunk-TWR2KAZG.js.map +0 -1
- package/dist/chunk-ZC3ZE7TN.js +0 -4849
- package/dist/chunk-ZC3ZE7TN.js.map +0 -1
- /package/dist/{chunk-P4FFI752.js.map → chunk-DED3J7F6.js.map} +0 -0
package/dist/chunk-ZC3ZE7TN.js
DELETED
|
@@ -1,4849 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IntegrationRuntimeError,
|
|
3
|
-
normalizeIntegrationError
|
|
4
|
-
} from "./chunk-H4XYLS7T.js";
|
|
5
|
-
import {
|
|
6
|
-
INTEGRATION_KIND_ALIASES,
|
|
7
|
-
canonicalIntegrationKind,
|
|
8
|
-
integrationSpecToConnector,
|
|
9
|
-
listIntegrationSpecs
|
|
10
|
-
} from "./chunk-2GEGKSL5.js";
|
|
11
|
-
import {
|
|
12
|
-
listBundledConnectorAdapters
|
|
13
|
-
} from "./chunk-E2QYXDJQ.js";
|
|
14
|
-
import {
|
|
15
|
-
CONNECTOR_ADAPTER_FACTORIES,
|
|
16
|
-
createOAuthBasicAuthorizationHeader,
|
|
17
|
-
createOAuthTokenRequestHeaders,
|
|
18
|
-
oauthClientCredentialRedactionValues,
|
|
19
|
-
redactOAuthSensitiveText
|
|
20
|
-
} from "./chunk-QL6CRJ3E.js";
|
|
21
|
-
|
|
22
|
-
// src/activepieces-catalog.ts
|
|
23
|
-
import { readFileSync } from "fs";
|
|
24
|
-
import { fileURLToPath } from "url";
|
|
25
|
-
import { dirname, resolve } from "path";
|
|
26
|
-
|
|
27
|
-
// src/activepieces-overrides.ts
|
|
28
|
-
var ACTIVEPIECES_OVERRIDES = {
|
|
29
|
-
slack: {
|
|
30
|
-
category: "chat",
|
|
31
|
-
actionRisks: {
|
|
32
|
-
"slack.send.message": "write",
|
|
33
|
-
"slack.send.direct.message": "write",
|
|
34
|
-
"request.action.message": "write",
|
|
35
|
-
"request.action.direct.message": "write",
|
|
36
|
-
"request.approval.direct.message": "write",
|
|
37
|
-
"request.send.approval.message": "write",
|
|
38
|
-
"upload.file": "write",
|
|
39
|
-
"search.messages": "read"
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
discord: { category: "chat" },
|
|
43
|
-
"microsoft-teams": { category: "chat" },
|
|
44
|
-
whatsapp: { category: "chat" },
|
|
45
|
-
telegram: { category: "chat" },
|
|
46
|
-
gmail: {
|
|
47
|
-
category: "email",
|
|
48
|
-
actionRisks: {
|
|
49
|
-
"gmail.send.email": "write",
|
|
50
|
-
"gmail.reply.to.email": "write",
|
|
51
|
-
"gmail.create.draft.reply": "write",
|
|
52
|
-
"gmail.search.mail": "read",
|
|
53
|
-
"gmail.get.email": "read",
|
|
54
|
-
"request.approval.in.email": "write"
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
"microsoft-outlook": { category: "email" },
|
|
58
|
-
sendgrid: { category: "email" },
|
|
59
|
-
postmark: { category: "email" },
|
|
60
|
-
mailchimp: { category: "email" },
|
|
61
|
-
resend: { category: "email" },
|
|
62
|
-
"google-calendar": { category: "calendar" },
|
|
63
|
-
"microsoft-outlook-calendar": { category: "calendar" },
|
|
64
|
-
cal: { category: "calendar" },
|
|
65
|
-
calendly: { category: "calendar" },
|
|
66
|
-
zoom: { category: "calendar" },
|
|
67
|
-
"google-drive": { category: "storage" },
|
|
68
|
-
dropbox: { category: "storage" },
|
|
69
|
-
onedrive: { category: "storage" },
|
|
70
|
-
"google-sheets": { category: "database" },
|
|
71
|
-
"google-docs": { category: "docs" },
|
|
72
|
-
airtable: { category: "database" },
|
|
73
|
-
notion: { category: "docs" },
|
|
74
|
-
hubspot: { category: "crm" },
|
|
75
|
-
salesforce: { category: "crm" },
|
|
76
|
-
pipedrive: { category: "crm" },
|
|
77
|
-
intercom: { category: "crm" },
|
|
78
|
-
zendesk: { category: "crm" },
|
|
79
|
-
neverbounce: { category: "crm" },
|
|
80
|
-
stripe: {
|
|
81
|
-
category: "crm",
|
|
82
|
-
actionRisks: {
|
|
83
|
-
"stripe.create.customer": "write",
|
|
84
|
-
"stripe.update.customer": "write",
|
|
85
|
-
"stripe.retrieve.customer": "read",
|
|
86
|
-
"stripe.search.customer": "read",
|
|
87
|
-
"stripe.search.subscriptions": "read",
|
|
88
|
-
"stripe.create.invoice": "write",
|
|
89
|
-
"stripe.retrieve.invoice": "read",
|
|
90
|
-
"stripe.find.invoice": "read",
|
|
91
|
-
"stripe.create.subscription": "write",
|
|
92
|
-
"stripe.cancel.subscription": "destructive",
|
|
93
|
-
"stripe.create.payment.intent": "write",
|
|
94
|
-
"stripe.retrieve.payment.intent": "read",
|
|
95
|
-
"stripe.create.refund": "destructive",
|
|
96
|
-
"stripe.create.product": "write",
|
|
97
|
-
"stripe.create.price": "write",
|
|
98
|
-
"stripe.create.payment.link": "write",
|
|
99
|
-
"stripe.deactivate.payment.link": "destructive",
|
|
100
|
-
"stripe.retrieve.payout": "read"
|
|
101
|
-
},
|
|
102
|
-
approvalRequired: {
|
|
103
|
-
"stripe.create.refund": true,
|
|
104
|
-
"stripe.cancel.subscription": true,
|
|
105
|
-
"stripe.deactivate.payment.link": true
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
twilio: {
|
|
109
|
-
category: "chat",
|
|
110
|
-
actionRisks: {
|
|
111
|
-
"send.sms": "write",
|
|
112
|
-
"send.whatsapp.message": "write",
|
|
113
|
-
"make.phone.call": "write"
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
shopify: { category: "crm" },
|
|
117
|
-
square: { category: "crm" }
|
|
118
|
-
};
|
|
119
|
-
function getActivepiecesOverride(id) {
|
|
120
|
-
return ACTIVEPIECES_OVERRIDES[id];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// src/activepieces-catalog.ts
|
|
124
|
-
var CATALOG_RESOURCE_RELATIVE = "../data/activepieces-catalog.json";
|
|
125
|
-
var CACHED_CATALOG;
|
|
126
|
-
function loadCatalog() {
|
|
127
|
-
if (CACHED_CATALOG) return CACHED_CATALOG;
|
|
128
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
129
|
-
const path = resolve(here, CATALOG_RESOURCE_RELATIVE);
|
|
130
|
-
const raw = readFileSync(path, "utf8");
|
|
131
|
-
const parsed = JSON.parse(raw);
|
|
132
|
-
CACHED_CATALOG = parsed;
|
|
133
|
-
return parsed;
|
|
134
|
-
}
|
|
135
|
-
function listActivepiecesCatalogEntries() {
|
|
136
|
-
return loadCatalog().map((entry) => ({
|
|
137
|
-
...entry,
|
|
138
|
-
actions: [...entry.actions],
|
|
139
|
-
triggers: [...entry.triggers],
|
|
140
|
-
domains: [...entry.domains],
|
|
141
|
-
source: { ...entry.source }
|
|
142
|
-
}));
|
|
143
|
-
}
|
|
144
|
-
function buildActivepiecesConnectors(options = {}) {
|
|
145
|
-
const providerId = options.providerId ?? "activepieces";
|
|
146
|
-
const executable = options.executable === true;
|
|
147
|
-
return listActivepiecesCatalogEntries().map((entry) => {
|
|
148
|
-
const override = getActivepiecesOverride(entry.id);
|
|
149
|
-
const category = override?.category ?? entry.category;
|
|
150
|
-
const scopes = [`${entry.id}.read`, `${entry.id}.write`];
|
|
151
|
-
const catalogActions = entry.actions.map((action) => toAction(applyActionOverride(action, override), scopes, dataClassFor(category)));
|
|
152
|
-
const catalogTriggers = entry.triggers.map((trigger2) => toTrigger(trigger2, scopes, dataClassFor(category)));
|
|
153
|
-
const entryExecutable = executable && catalogActions.length > 0;
|
|
154
|
-
return {
|
|
155
|
-
id: entry.id,
|
|
156
|
-
providerId,
|
|
157
|
-
title: entry.title,
|
|
158
|
-
category,
|
|
159
|
-
auth: entry.auth,
|
|
160
|
-
scopes: options.includeCatalogActions ? scopes : [],
|
|
161
|
-
actions: options.includeCatalogActions ? catalogActions : [],
|
|
162
|
-
triggers: options.includeCatalogActions ? catalogTriggers : void 0,
|
|
163
|
-
metadata: {
|
|
164
|
-
source: "activepieces-community",
|
|
165
|
-
executable: entryExecutable,
|
|
166
|
-
runtime: "activepieces-piece",
|
|
167
|
-
catalogOnly: !entryExecutable,
|
|
168
|
-
supportTier: entryExecutable ? "gatewayExecutable" : "catalogOnly",
|
|
169
|
-
catalogActionCount: catalogActions.length,
|
|
170
|
-
catalogTriggerCount: catalogTriggers.length,
|
|
171
|
-
npmPackage: entry.npmPackage,
|
|
172
|
-
version: entry.version,
|
|
173
|
-
license: entry.source.license,
|
|
174
|
-
sourcePath: entry.source.path,
|
|
175
|
-
domains: entry.domains,
|
|
176
|
-
authFields: entry.authFields,
|
|
177
|
-
...override ? { overridden: true } : {}
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
function applyActionOverride(action, override) {
|
|
183
|
-
if (!override) return action;
|
|
184
|
-
const risk = override.actionRisks?.[action.id] ?? action.risk;
|
|
185
|
-
return { ...action, risk };
|
|
186
|
-
}
|
|
187
|
-
function toAction(action, scopes, dataClass) {
|
|
188
|
-
return {
|
|
189
|
-
id: action.id,
|
|
190
|
-
title: action.title,
|
|
191
|
-
risk: action.risk,
|
|
192
|
-
requiredScopes: action.risk === "read" ? [scopes[0]] : [scopes[1]],
|
|
193
|
-
dataClass,
|
|
194
|
-
approvalRequired: action.risk !== "read",
|
|
195
|
-
inputSchema: { type: "object", additionalProperties: true, properties: {} }
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
function toTrigger(trigger2, scopes, dataClass) {
|
|
199
|
-
return {
|
|
200
|
-
id: trigger2.id,
|
|
201
|
-
title: trigger2.title,
|
|
202
|
-
requiredScopes: [scopes[0]],
|
|
203
|
-
dataClass,
|
|
204
|
-
payloadSchema: { type: "object", additionalProperties: true, properties: {} }
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
function dataClassFor(category) {
|
|
208
|
-
if (category === "database" || category === "storage" || category === "email") return "private";
|
|
209
|
-
if (category === "crm" || category === "chat" || category === "docs") return "private";
|
|
210
|
-
if (category === "internal") return "sensitive";
|
|
211
|
-
return "internal";
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// src/index.ts
|
|
215
|
-
import { createHash as createHash3, createHmac, randomBytes, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
|
|
216
|
-
|
|
217
|
-
// src/core-error.ts
|
|
218
|
-
var IntegrationError = class extends Error {
|
|
219
|
-
constructor(message, code) {
|
|
220
|
-
super(message);
|
|
221
|
-
this.code = code;
|
|
222
|
-
this.name = "IntegrationError";
|
|
223
|
-
}
|
|
224
|
-
code;
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
// src/adapter-provider.ts
|
|
228
|
-
function createConnectorAdapterProvider(options) {
|
|
229
|
-
const providerId = options.id ?? "first-party";
|
|
230
|
-
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
231
|
-
const fetchImpl = options.fetchImpl ?? globalThis.fetch?.bind(globalThis);
|
|
232
|
-
const adapters = /* @__PURE__ */ new Map();
|
|
233
|
-
for (const adapter of options.adapters) {
|
|
234
|
-
adapters.set(adapter.manifest.kind, adapter);
|
|
235
|
-
}
|
|
236
|
-
return {
|
|
237
|
-
id: providerId,
|
|
238
|
-
kind: options.kind ?? "first_party",
|
|
239
|
-
listConnectors: () => [...adapters.values()].map((adapter) => manifestToConnector(providerId, adapter)),
|
|
240
|
-
async startAuth(request) {
|
|
241
|
-
const adapter = adapters.get(request.connectorId);
|
|
242
|
-
if (!adapter) {
|
|
243
|
-
throw new IntegrationError(
|
|
244
|
-
`Connector adapter ${request.connectorId} not found.`,
|
|
245
|
-
"connector_not_found"
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
const auth = oauthManifestAuth(adapter.manifest.auth);
|
|
249
|
-
if (!auth) {
|
|
250
|
-
throw new IntegrationError(
|
|
251
|
-
`Connector ${request.connectorId} does not support OAuth2 authorization (auth kind: ${adapter.manifest.auth.kind}).`,
|
|
252
|
-
"auth_not_supported"
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
if ((auth.grantType ?? "authorization_code") !== "authorization_code") {
|
|
256
|
-
throw new IntegrationError(
|
|
257
|
-
`Connector ${request.connectorId} uses client_credentials and requires a machine connection path.`,
|
|
258
|
-
"auth_not_supported"
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
const pkce = resolveOAuthPkceMode(auth.pkce, request.connectorId);
|
|
262
|
-
const tokenClientAuthMethod = resolveTokenClientAuthMethod(
|
|
263
|
-
auth.tokenClientAuthMethod,
|
|
264
|
-
request.connectorId
|
|
265
|
-
);
|
|
266
|
-
if (!options.resolveOAuthClient) {
|
|
267
|
-
throw new IntegrationError(
|
|
268
|
-
`OAuth client resolver missing on adapter provider; cannot start auth for ${request.connectorId}.`,
|
|
269
|
-
"config_missing"
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
const client = await options.resolveOAuthClient({ connectorId: request.connectorId });
|
|
273
|
-
if (!client || !client.clientId || tokenClientAuthMethod !== "none" && !client.clientSecret) {
|
|
274
|
-
throw new IntegrationError(
|
|
275
|
-
`OAuth client credentials unavailable for ${request.connectorId}.`,
|
|
276
|
-
"config_missing"
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
if (!auth.authorizationUrl) {
|
|
280
|
-
throw new IntegrationError(
|
|
281
|
-
`Connector ${request.connectorId} uses the ${auth.grantType ?? "authorization_code"} grant and has no authorization URL to redirect to.`,
|
|
282
|
-
"auth_not_supported"
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
const scopes = request.requestedScopes && request.requestedScopes.length > 0 ? request.requestedScopes : auth.scopes;
|
|
286
|
-
const url = new URL(resolveOAuthUrlTemplate(
|
|
287
|
-
auth.authorizationUrl,
|
|
288
|
-
request.metadata,
|
|
289
|
-
request.connectorId,
|
|
290
|
-
auth.urlTemplateMetadata
|
|
291
|
-
));
|
|
292
|
-
url.searchParams.set("response_type", "code");
|
|
293
|
-
url.searchParams.set(
|
|
294
|
-
resolveOAuthParameterName(
|
|
295
|
-
auth.authorizationClientIdParam,
|
|
296
|
-
"client_id",
|
|
297
|
-
request.connectorId
|
|
298
|
-
),
|
|
299
|
-
client.clientId
|
|
300
|
-
);
|
|
301
|
-
url.searchParams.set("redirect_uri", request.redirectUri);
|
|
302
|
-
if (auth.sendScopeParam !== false && scopes.length > 0) {
|
|
303
|
-
url.searchParams.set("scope", scopes.join(auth.scopeSeparator ?? " "));
|
|
304
|
-
}
|
|
305
|
-
const state = request.state ?? randomState();
|
|
306
|
-
url.searchParams.set("state", state);
|
|
307
|
-
if (auth.extraAuthParams) {
|
|
308
|
-
for (const [key, value] of Object.entries(auth.extraAuthParams)) {
|
|
309
|
-
url.searchParams.set(key, value);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (pkce === "unsupported") {
|
|
313
|
-
url.searchParams.delete("code_challenge");
|
|
314
|
-
url.searchParams.delete("code_challenge_method");
|
|
315
|
-
} else {
|
|
316
|
-
url.searchParams.set(
|
|
317
|
-
"code_challenge",
|
|
318
|
-
requirePkceChallenge(request.codeChallenge, request.connectorId)
|
|
319
|
-
);
|
|
320
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
providerId,
|
|
324
|
-
connectorId: request.connectorId,
|
|
325
|
-
authUrl: url.toString(),
|
|
326
|
-
state,
|
|
327
|
-
metadata: request.metadata
|
|
328
|
-
};
|
|
329
|
-
},
|
|
330
|
-
async completeAuth(request) {
|
|
331
|
-
const adapter = adapters.get(request.connectorId);
|
|
332
|
-
if (!adapter) {
|
|
333
|
-
throw new IntegrationError(
|
|
334
|
-
`Connector adapter ${request.connectorId} not found.`,
|
|
335
|
-
"connector_not_found"
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
const auth = oauthManifestAuth(adapter.manifest.auth);
|
|
339
|
-
if (!auth) {
|
|
340
|
-
throw new IntegrationError(
|
|
341
|
-
`Connector ${request.connectorId} does not support OAuth2 authorization (auth kind: ${adapter.manifest.auth.kind}).`,
|
|
342
|
-
"auth_not_supported"
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
if ((auth.grantType ?? "authorization_code") !== "authorization_code") {
|
|
346
|
-
throw new IntegrationError(
|
|
347
|
-
`Connector ${request.connectorId} uses client_credentials and requires a machine connection path.`,
|
|
348
|
-
"auth_not_supported"
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
const tokenClientAuthMethod = resolveTokenClientAuthMethod(
|
|
352
|
-
auth.tokenClientAuthMethod,
|
|
353
|
-
request.connectorId
|
|
354
|
-
);
|
|
355
|
-
const pkce = resolveOAuthPkceMode(auth.pkce, request.connectorId);
|
|
356
|
-
if (!request.code) {
|
|
357
|
-
throw new IntegrationError(
|
|
358
|
-
`Authorization code missing on completeAuth for ${request.connectorId}.`,
|
|
359
|
-
"config_missing"
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
if (!options.resolveOAuthClient) {
|
|
363
|
-
throw new IntegrationError(
|
|
364
|
-
`OAuth client resolver missing on adapter provider; cannot complete auth for ${request.connectorId}.`,
|
|
365
|
-
"config_missing"
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
const client = await options.resolveOAuthClient({ connectorId: request.connectorId });
|
|
369
|
-
if (!client || !client.clientId || tokenClientAuthMethod !== "none" && !client.clientSecret) {
|
|
370
|
-
throw new IntegrationError(
|
|
371
|
-
`OAuth client credentials unavailable for ${request.connectorId}.`,
|
|
372
|
-
"config_missing"
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
if (!fetchImpl) {
|
|
376
|
-
throw new IntegrationError(
|
|
377
|
-
"No fetch implementation available; inject fetchImpl into createConnectorAdapterProvider.",
|
|
378
|
-
"config_missing"
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
const body = new URLSearchParams({
|
|
382
|
-
grant_type: "authorization_code",
|
|
383
|
-
code: request.code,
|
|
384
|
-
redirect_uri: request.redirectUri
|
|
385
|
-
});
|
|
386
|
-
const codeVerifier = pkce === "unsupported" ? void 0 : requirePkceVerifier(request.codeVerifier, request.connectorId);
|
|
387
|
-
if (codeVerifier) body.set("code_verifier", codeVerifier);
|
|
388
|
-
const headers = createOAuthTokenRequestHeaders(auth.tokenRequestHeaders);
|
|
389
|
-
if (tokenClientAuthMethod === "none") {
|
|
390
|
-
body.set(
|
|
391
|
-
resolveOAuthParameterName(
|
|
392
|
-
auth.tokenClientIdParam,
|
|
393
|
-
"client_id",
|
|
394
|
-
request.connectorId
|
|
395
|
-
),
|
|
396
|
-
client.clientId
|
|
397
|
-
);
|
|
398
|
-
} else if (tokenClientAuthMethod === "client_secret_post") {
|
|
399
|
-
body.set(
|
|
400
|
-
resolveOAuthParameterName(
|
|
401
|
-
auth.tokenClientIdParam,
|
|
402
|
-
"client_id",
|
|
403
|
-
request.connectorId
|
|
404
|
-
),
|
|
405
|
-
client.clientId
|
|
406
|
-
);
|
|
407
|
-
body.set(
|
|
408
|
-
resolveOAuthParameterName(
|
|
409
|
-
auth.tokenClientSecretParam,
|
|
410
|
-
"client_secret",
|
|
411
|
-
request.connectorId
|
|
412
|
-
),
|
|
413
|
-
client.clientSecret
|
|
414
|
-
);
|
|
415
|
-
} else {
|
|
416
|
-
const authorization = createOAuthBasicAuthorizationHeader(
|
|
417
|
-
client.clientId,
|
|
418
|
-
client.clientSecret,
|
|
419
|
-
base64Encode
|
|
420
|
-
);
|
|
421
|
-
headers.authorization = authorization;
|
|
422
|
-
}
|
|
423
|
-
const redactionValues = [
|
|
424
|
-
request.code,
|
|
425
|
-
...codeVerifier ? [codeVerifier] : [],
|
|
426
|
-
...oauthClientCredentialRedactionValues(
|
|
427
|
-
client.clientId,
|
|
428
|
-
client.clientSecret,
|
|
429
|
-
headers.authorization
|
|
430
|
-
)
|
|
431
|
-
];
|
|
432
|
-
const tokenUrl = resolveOAuthUrlTemplate(
|
|
433
|
-
auth.tokenUrl,
|
|
434
|
-
request.metadata,
|
|
435
|
-
request.connectorId,
|
|
436
|
-
auth.urlTemplateMetadata
|
|
437
|
-
);
|
|
438
|
-
let res;
|
|
439
|
-
try {
|
|
440
|
-
res = await fetchImpl(tokenUrl, {
|
|
441
|
-
method: "POST",
|
|
442
|
-
headers,
|
|
443
|
-
body
|
|
444
|
-
});
|
|
445
|
-
} catch (cause) {
|
|
446
|
-
const detail = redactOAuthSensitiveText(
|
|
447
|
-
cause?.message ?? "unknown",
|
|
448
|
-
redactionValues
|
|
449
|
-
);
|
|
450
|
-
throw new IntegrationError(
|
|
451
|
-
`OAuth token exchange transport error for ${request.connectorId}: ${detail}`,
|
|
452
|
-
"provider_failure"
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
if (!res.ok) {
|
|
456
|
-
const text = await res.text().catch(() => "");
|
|
457
|
-
const statusText = redactOAuthSensitiveText(res.statusText, redactionValues);
|
|
458
|
-
const redactedText = redactOAuthSensitiveText(text, redactionValues);
|
|
459
|
-
const detail = `${res.status} ${statusText} \u2014 ${redactedText.slice(0, 200)}`;
|
|
460
|
-
throw new IntegrationError(
|
|
461
|
-
`OAuth token exchange failed for ${request.connectorId}: ${detail}`,
|
|
462
|
-
"provider_failure"
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
let json;
|
|
466
|
-
try {
|
|
467
|
-
json = await res.json();
|
|
468
|
-
} catch {
|
|
469
|
-
throw new IntegrationError(
|
|
470
|
-
`OAuth token exchange returned non-JSON body for ${request.connectorId}.`,
|
|
471
|
-
"provider_failure"
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
if (!json.access_token) {
|
|
475
|
-
throw new IntegrationError(
|
|
476
|
-
`OAuth token exchange returned no access_token for ${request.connectorId}.`,
|
|
477
|
-
"provider_failure"
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
const grantedScopes = typeof json.scope === "string" && json.scope.length > 0 ? json.scope.split(/[\s,]+/).filter(Boolean) : [];
|
|
481
|
-
const issued = now();
|
|
482
|
-
const issuedIso = issued.toISOString();
|
|
483
|
-
const expiresAt = typeof json.expires_in === "number" && json.expires_in > 0 ? new Date(issued.getTime() + json.expires_in * 1e3).toISOString() : void 0;
|
|
484
|
-
const capturedMetadata = captureTokenMetadata(auth.tokenMetadata, json, request.connectorId);
|
|
485
|
-
return {
|
|
486
|
-
id: randomConnectionId(),
|
|
487
|
-
owner: request.owner,
|
|
488
|
-
providerId,
|
|
489
|
-
connectorId: request.connectorId,
|
|
490
|
-
status: "active",
|
|
491
|
-
grantedScopes,
|
|
492
|
-
createdAt: issuedIso,
|
|
493
|
-
updatedAt: issuedIso,
|
|
494
|
-
expiresAt,
|
|
495
|
-
metadata: { ...request.metadata, ...capturedMetadata }
|
|
496
|
-
};
|
|
497
|
-
},
|
|
498
|
-
async invokeAction(connection, request) {
|
|
499
|
-
const adapter = adapters.get(connection.connectorId);
|
|
500
|
-
if (!adapter) {
|
|
501
|
-
throw new IntegrationError(`Connector adapter ${connection.connectorId} not found.`, "connector_not_found");
|
|
502
|
-
}
|
|
503
|
-
const capability = adapter.manifest.capabilities.find((candidate) => candidate.name === request.action);
|
|
504
|
-
if (!capability) {
|
|
505
|
-
throw new IntegrationError(`Capability ${request.action} is not defined by ${connection.connectorId}.`, "action_not_found");
|
|
506
|
-
}
|
|
507
|
-
const source = await options.resolveDataSource(connection);
|
|
508
|
-
let rotationPersistence = Promise.resolve();
|
|
509
|
-
const persistRotation = (credentials) => {
|
|
510
|
-
if (!options.onCredentialsRotated) return Promise.resolve();
|
|
511
|
-
rotationPersistence = rotationPersistence.then(
|
|
512
|
-
() => options.onCredentialsRotated({ connection, credentials })
|
|
513
|
-
);
|
|
514
|
-
void rotationPersistence.catch(() => {
|
|
515
|
-
});
|
|
516
|
-
return rotationPersistence;
|
|
517
|
-
};
|
|
518
|
-
const invocation = {
|
|
519
|
-
source,
|
|
520
|
-
capabilityName: request.action,
|
|
521
|
-
args: toRecord(request.input),
|
|
522
|
-
idempotencyKey: request.idempotencyKey ?? `idem_${connection.id}_${request.action}_${now().getTime()}`,
|
|
523
|
-
expectedEtag: typeof request.metadata?.expectedEtag === "string" ? request.metadata.expectedEtag : void 0,
|
|
524
|
-
callSessionId: typeof request.metadata?.callSessionId === "string" ? request.metadata.callSessionId : void 0,
|
|
525
|
-
onCredentialsRotated: options.onCredentialsRotated ? persistRotation : void 0
|
|
526
|
-
};
|
|
527
|
-
try {
|
|
528
|
-
if (capability.class === "read") {
|
|
529
|
-
if (!adapter.executeRead) {
|
|
530
|
-
throw new IntegrationError(`Connector ${connection.connectorId} does not implement reads.`, "action_not_found");
|
|
531
|
-
}
|
|
532
|
-
const result = await adapter.executeRead(invocation);
|
|
533
|
-
return readResultToAction(request, result);
|
|
534
|
-
}
|
|
535
|
-
if (capability.class === "mutation") {
|
|
536
|
-
if (!adapter.executeMutation) {
|
|
537
|
-
throw new IntegrationError(`Connector ${connection.connectorId} does not implement mutations.`, "action_not_found");
|
|
538
|
-
}
|
|
539
|
-
const result = await adapter.executeMutation(invocation);
|
|
540
|
-
return mutationResultToAction(request, result);
|
|
541
|
-
}
|
|
542
|
-
throw new IntegrationError(`Capability ${request.action} is not invokable as an action.`, "action_not_found");
|
|
543
|
-
} finally {
|
|
544
|
-
await rotationPersistence;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
function adapterManifestsToConnectors(adapters, providerId = "first-party") {
|
|
550
|
-
return adapters.map((adapter) => manifestToConnector(providerId, adapter));
|
|
551
|
-
}
|
|
552
|
-
function createConnectorAdapterCatalogSource(options) {
|
|
553
|
-
const sourceId = options.id ?? "first-party";
|
|
554
|
-
return {
|
|
555
|
-
id: sourceId,
|
|
556
|
-
precedence: options.precedence,
|
|
557
|
-
connectors: adapterManifestsToConnectors(options.adapters, options.providerId ?? sourceId)
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
function manifestToConnector(providerId, adapter) {
|
|
561
|
-
const manifest = adapter.manifest;
|
|
562
|
-
const primaryAuth = primaryManifestAuth(manifest.auth);
|
|
563
|
-
const oauthAuth = oauthManifestAuth(manifest.auth);
|
|
564
|
-
return {
|
|
565
|
-
id: manifest.kind,
|
|
566
|
-
providerId,
|
|
567
|
-
title: manifest.displayName,
|
|
568
|
-
category: mapCategory(manifest.category),
|
|
569
|
-
auth: mapAuth(primaryAuth.kind),
|
|
570
|
-
scopes: primaryAuth.kind === "oauth2" ? primaryAuth.scopes : [],
|
|
571
|
-
actions: manifest.capabilities.filter((capability) => capability.class === "read" || capability.class === "mutation").map((capability) => ({
|
|
572
|
-
id: capability.name,
|
|
573
|
-
title: titleFromName(capability.name),
|
|
574
|
-
risk: capability.class === "read" ? "read" : capability.externalEffect ? "destructive" : "write",
|
|
575
|
-
requiredScopes: capability.requiredScopes ?? [],
|
|
576
|
-
dataClass: inferDataClass(manifest.category),
|
|
577
|
-
description: capability.description,
|
|
578
|
-
approvalRequired: capability.class === "mutation",
|
|
579
|
-
inputSchema: capability.parameters
|
|
580
|
-
})),
|
|
581
|
-
metadata: {
|
|
582
|
-
source: "first-party-adapter",
|
|
583
|
-
supportTier: "firstPartyExecutable",
|
|
584
|
-
executable: true,
|
|
585
|
-
oauthPkce: oauthAuth ? resolveOAuthPkceMode(oauthAuth.pkce, manifest.kind) : void 0,
|
|
586
|
-
authOptions: manifest.auth.kind === "one_of" ? manifest.auth.options.map((auth) => auth.kind) : [manifest.auth.kind],
|
|
587
|
-
preferredAuth: manifest.auth.kind === "one_of" ? manifest.auth.preferred : manifest.auth.kind
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
function readResultToAction(request, result) {
|
|
592
|
-
return {
|
|
593
|
-
ok: true,
|
|
594
|
-
action: request.action,
|
|
595
|
-
output: result.data,
|
|
596
|
-
metadata: {
|
|
597
|
-
etag: result.etag,
|
|
598
|
-
fetchedAt: result.fetchedAt
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
function mutationResultToAction(request, result) {
|
|
603
|
-
if (result.status === "committed") {
|
|
604
|
-
return {
|
|
605
|
-
ok: true,
|
|
606
|
-
action: request.action,
|
|
607
|
-
output: result.data,
|
|
608
|
-
metadata: {
|
|
609
|
-
etagAfter: result.etagAfter,
|
|
610
|
-
committedAt: result.committedAt,
|
|
611
|
-
idempotentReplay: result.idempotentReplay
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
if (result.status === "conflict") {
|
|
616
|
-
return {
|
|
617
|
-
ok: false,
|
|
618
|
-
action: request.action,
|
|
619
|
-
output: {
|
|
620
|
-
conflict: true,
|
|
621
|
-
message: result.message,
|
|
622
|
-
alternatives: result.alternatives,
|
|
623
|
-
currentState: result.currentState
|
|
624
|
-
}
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
return {
|
|
628
|
-
ok: false,
|
|
629
|
-
action: request.action,
|
|
630
|
-
output: {
|
|
631
|
-
rateLimited: true,
|
|
632
|
-
retryAfterMs: result.retryAfterMs,
|
|
633
|
-
message: result.message
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
function mapAuth(kind) {
|
|
638
|
-
if (kind === "one_of") return "custom";
|
|
639
|
-
if (kind === "oauth2") return "oauth2";
|
|
640
|
-
if (kind === "api-key") return "api_key";
|
|
641
|
-
if (kind === "none") return "none";
|
|
642
|
-
return "custom";
|
|
643
|
-
}
|
|
644
|
-
function primaryManifestAuth(auth) {
|
|
645
|
-
if (auth.kind !== "one_of") return auth;
|
|
646
|
-
return auth.options.find((option) => option.kind === auth.preferred) ?? auth.options[0];
|
|
647
|
-
}
|
|
648
|
-
function oauthManifestAuth(auth) {
|
|
649
|
-
if (auth.kind === "oauth2") return auth;
|
|
650
|
-
if (auth.kind !== "one_of") return void 0;
|
|
651
|
-
return auth.options.find((option) => option.kind === "oauth2");
|
|
652
|
-
}
|
|
653
|
-
function resolveTokenClientAuthMethod(value, connectorId) {
|
|
654
|
-
if (value === void 0 || value === "client_secret_post") {
|
|
655
|
-
return "client_secret_post";
|
|
656
|
-
}
|
|
657
|
-
if (value === "none" || value === "client_secret_basic") return value;
|
|
658
|
-
throw new IntegrationError(
|
|
659
|
-
`Connector ${connectorId} has an unsupported OAuth token client authentication method.`,
|
|
660
|
-
"config_missing"
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
function resolveOAuthPkceMode(value, connectorId) {
|
|
664
|
-
if (value === void 0 || value === "required") return "required";
|
|
665
|
-
if (value === "supported" || value === "unsupported") return value;
|
|
666
|
-
throw new IntegrationError(
|
|
667
|
-
`Connector ${connectorId} has an invalid OAuth PKCE posture.`,
|
|
668
|
-
"config_missing"
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
function requirePkceChallenge(value, connectorId) {
|
|
672
|
-
if (!value || !/^[A-Za-z0-9_-]{43}$/.test(value)) {
|
|
673
|
-
throw new IntegrationError(
|
|
674
|
-
`OAuth PKCE S256 challenge missing or invalid for ${connectorId}.`,
|
|
675
|
-
"config_missing"
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
return value;
|
|
679
|
-
}
|
|
680
|
-
function requirePkceVerifier(value, connectorId) {
|
|
681
|
-
if (!value || !/^[A-Za-z0-9._~-]{43,128}$/.test(value)) {
|
|
682
|
-
throw new IntegrationError(
|
|
683
|
-
`OAuth PKCE verifier missing or invalid for ${connectorId}.`,
|
|
684
|
-
"config_missing"
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
return value;
|
|
688
|
-
}
|
|
689
|
-
function resolveOAuthParameterName(value, fallback, connectorId) {
|
|
690
|
-
const name = value ?? fallback;
|
|
691
|
-
if (!/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(name)) {
|
|
692
|
-
throw new IntegrationError(
|
|
693
|
-
`Connector ${connectorId} declares an invalid OAuth parameter name.`,
|
|
694
|
-
"config_missing"
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
return name;
|
|
698
|
-
}
|
|
699
|
-
function base64Encode(value) {
|
|
700
|
-
const btoa2 = globalThis.btoa;
|
|
701
|
-
if (typeof btoa2 !== "function") {
|
|
702
|
-
throw new IntegrationError(
|
|
703
|
-
"OAuth client_secret_basic requires a base64 encoder in this runtime.",
|
|
704
|
-
"config_missing"
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
const bytes = new TextEncoder().encode(value);
|
|
708
|
-
let binary = "";
|
|
709
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
710
|
-
return btoa2(binary);
|
|
711
|
-
}
|
|
712
|
-
function resolveOAuthUrlTemplate(template, metadata, connectorId, metadataSpecs) {
|
|
713
|
-
const placeholders = [...template.matchAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g)];
|
|
714
|
-
let resolved = template;
|
|
715
|
-
for (const match of placeholders) {
|
|
716
|
-
const key = match[1];
|
|
717
|
-
const raw = metadata?.[key];
|
|
718
|
-
const metadataSpec = metadataSpecs?.[key];
|
|
719
|
-
if (metadataSpec?.kind === "base-url") {
|
|
720
|
-
if (typeof raw !== "string") {
|
|
721
|
-
throw new IntegrationError(
|
|
722
|
-
`OAuth URL for ${connectorId} requires metadata.${key} as a provider base URL.`,
|
|
723
|
-
"config_missing"
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
resolved = resolved.replaceAll(
|
|
727
|
-
match[0],
|
|
728
|
-
resolveOAuthMetadataBaseUrl(raw, metadataSpec, connectorId, key)
|
|
729
|
-
);
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
if (metadataSpec?.kind === "path-segment") {
|
|
733
|
-
if (!isOAuthPathSegmentPlaceholder(template, match.index ?? -1, match[0])) {
|
|
734
|
-
throw new IntegrationError(
|
|
735
|
-
`OAuth URL for ${connectorId} declares metadata.${key} outside a complete path segment.`,
|
|
736
|
-
"config_missing"
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
if (typeof raw !== "string" || !isOAuthPathSegment(raw.trim())) {
|
|
740
|
-
throw new IntegrationError(
|
|
741
|
-
`OAuth URL for ${connectorId} requires metadata.${key} as a valid path segment.`,
|
|
742
|
-
"config_missing"
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
resolved = resolved.replaceAll(match[0], encodeURIComponent(raw.trim()));
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
if (typeof raw !== "string" || !isDnsLabel(raw.trim())) {
|
|
749
|
-
throw new IntegrationError(
|
|
750
|
-
`OAuth URL for ${connectorId} requires metadata.${key} as a valid tenant label.`,
|
|
751
|
-
"config_missing"
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
resolved = resolved.replaceAll(match[0], raw.trim().toLowerCase());
|
|
755
|
-
}
|
|
756
|
-
let url;
|
|
757
|
-
try {
|
|
758
|
-
url = new URL(resolved);
|
|
759
|
-
} catch {
|
|
760
|
-
throw new IntegrationError(
|
|
761
|
-
`OAuth URL for ${connectorId} is invalid.`,
|
|
762
|
-
"config_missing"
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
if (url.protocol !== "https:" || url.username || url.password) {
|
|
766
|
-
throw new IntegrationError(
|
|
767
|
-
`OAuth URL for ${connectorId} must be HTTPS without embedded credentials.`,
|
|
768
|
-
"config_missing"
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
return url.toString();
|
|
772
|
-
}
|
|
773
|
-
function isOAuthPathSegment(value) {
|
|
774
|
-
return value !== "." && value !== ".." && /^[A-Za-z0-9._~-]{1,128}$/.test(value);
|
|
775
|
-
}
|
|
776
|
-
function isOAuthPathSegmentPlaceholder(template, index, placeholder) {
|
|
777
|
-
if (index < 0) return false;
|
|
778
|
-
const schemeIndex = template.indexOf("://");
|
|
779
|
-
const rootEnd = schemeIndex >= 0 ? schemeIndex + 3 : template.startsWith("{") ? template.indexOf("}") + 1 : 0;
|
|
780
|
-
const pathStart = template.indexOf("/", rootEnd);
|
|
781
|
-
const end = index + placeholder.length;
|
|
782
|
-
const queryStart = template.search(/[?#]/);
|
|
783
|
-
return pathStart >= 0 && index >= pathStart && (queryStart < 0 || index < queryStart) && template[index - 1] === "/" && (end === template.length || template[end] === "/" || template[end] === "?" || template[end] === "#");
|
|
784
|
-
}
|
|
785
|
-
function resolveOAuthMetadataBaseUrl(value, spec, connectorId, metadataKey) {
|
|
786
|
-
let url;
|
|
787
|
-
try {
|
|
788
|
-
url = new URL(value.trim());
|
|
789
|
-
} catch {
|
|
790
|
-
throw invalidOAuthMetadataBaseUrl(connectorId, metadataKey);
|
|
791
|
-
}
|
|
792
|
-
if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
793
|
-
throw invalidOAuthMetadataBaseUrl(connectorId, metadataKey);
|
|
794
|
-
}
|
|
795
|
-
const exactAllowed = spec.allowedBaseUrls?.some((candidate) => sameOrigin(candidate, url)) ?? false;
|
|
796
|
-
const suffixAllowed = spec.allowedBaseUrlSuffixes?.some(
|
|
797
|
-
(suffix) => !url.port && suffix.startsWith(".") && url.hostname.toLowerCase().endsWith(suffix.toLowerCase())
|
|
798
|
-
) ?? false;
|
|
799
|
-
const hasExplicitAllowlist = Boolean(
|
|
800
|
-
spec.allowedBaseUrls?.length || spec.allowedBaseUrlSuffixes?.length
|
|
801
|
-
);
|
|
802
|
-
if (hasExplicitAllowlist && !exactAllowed && !suffixAllowed) {
|
|
803
|
-
throw invalidOAuthMetadataBaseUrl(connectorId, metadataKey);
|
|
804
|
-
}
|
|
805
|
-
if (spec.requirePublicHttps && !isPublicDnsHostname(url.hostname)) {
|
|
806
|
-
throw invalidOAuthMetadataBaseUrl(connectorId, metadataKey);
|
|
807
|
-
}
|
|
808
|
-
return url.origin;
|
|
809
|
-
}
|
|
810
|
-
function invalidOAuthMetadataBaseUrl(connectorId, metadataKey) {
|
|
811
|
-
return new IntegrationError(
|
|
812
|
-
`OAuth URL for ${connectorId} requires metadata.${metadataKey} as an allowed HTTPS provider root.`,
|
|
813
|
-
"config_missing"
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
function sameOrigin(candidate, actual) {
|
|
817
|
-
try {
|
|
818
|
-
const allowed = new URL(candidate);
|
|
819
|
-
return allowed.protocol === "https:" && !allowed.username && !allowed.password && allowed.pathname === "/" && !allowed.search && !allowed.hash && allowed.origin === actual.origin;
|
|
820
|
-
} catch {
|
|
821
|
-
return false;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
function isPublicDnsHostname(hostname) {
|
|
825
|
-
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
826
|
-
if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal") || normalized.endsWith(".home.arpa")) {
|
|
827
|
-
return false;
|
|
828
|
-
}
|
|
829
|
-
if (/^[0-9.]+$/.test(normalized) || normalized.includes(":")) return false;
|
|
830
|
-
return normalized.includes(".") && normalized.split(".").every(isDnsLabel);
|
|
831
|
-
}
|
|
832
|
-
function isDnsLabel(value) {
|
|
833
|
-
return value.length > 0 && value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(value);
|
|
834
|
-
}
|
|
835
|
-
function captureTokenMetadata(tokenMetadata, json, connectorId) {
|
|
836
|
-
if (!tokenMetadata) return {};
|
|
837
|
-
const captured = {};
|
|
838
|
-
for (const [metaKey, source] of Object.entries(tokenMetadata)) {
|
|
839
|
-
const field = typeof source === "string" ? source : source.field;
|
|
840
|
-
const required = typeof source === "string" ? false : source.required === true;
|
|
841
|
-
const raw = json[field];
|
|
842
|
-
const value = typeof raw === "string" ? raw.trim() : raw;
|
|
843
|
-
if (value === void 0 || value === null || value === "") {
|
|
844
|
-
if (required) {
|
|
845
|
-
throw new IntegrationError(
|
|
846
|
-
`OAuth token exchange for ${connectorId} did not return required field "${field}" for metadata.${metaKey}.`,
|
|
847
|
-
"provider_failure"
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
captured[metaKey] = String(value);
|
|
853
|
-
}
|
|
854
|
-
return captured;
|
|
855
|
-
}
|
|
856
|
-
function mapCategory(category) {
|
|
857
|
-
if (category === "comms") return "chat";
|
|
858
|
-
if (category === "spreadsheet") return "database";
|
|
859
|
-
if (category === "doc") return "docs";
|
|
860
|
-
if (category === "commerce") return "workflow";
|
|
861
|
-
if (category === "sales-intelligence") return "crm";
|
|
862
|
-
if (category === "market-intelligence" || category === "hr") return "other";
|
|
863
|
-
return category === "other" ? "other" : category;
|
|
864
|
-
}
|
|
865
|
-
function inferDataClass(category) {
|
|
866
|
-
if (category === "commerce") return "sensitive";
|
|
867
|
-
if (category === "webhook") return "internal";
|
|
868
|
-
return "private";
|
|
869
|
-
}
|
|
870
|
-
function titleFromName(name) {
|
|
871
|
-
return name.split(/[._-]/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
872
|
-
}
|
|
873
|
-
function toRecord(input) {
|
|
874
|
-
if (input && typeof input === "object" && !Array.isArray(input)) return input;
|
|
875
|
-
return {};
|
|
876
|
-
}
|
|
877
|
-
function randomState() {
|
|
878
|
-
const bytes = new Uint8Array(24);
|
|
879
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
880
|
-
return base64UrlEncode(bytes);
|
|
881
|
-
}
|
|
882
|
-
function randomConnectionId() {
|
|
883
|
-
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
884
|
-
return `conn_${globalThis.crypto.randomUUID().replace(/-/g, "")}`;
|
|
885
|
-
}
|
|
886
|
-
const bytes = new Uint8Array(16);
|
|
887
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
888
|
-
return `conn_${base64UrlEncode(bytes)}`;
|
|
889
|
-
}
|
|
890
|
-
function base64UrlEncode(bytes) {
|
|
891
|
-
let bin = "";
|
|
892
|
-
for (const b of bytes) bin += String.fromCharCode(b);
|
|
893
|
-
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
|
|
894
|
-
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// src/credentials.ts
|
|
898
|
-
var CredentialRefreshRevokedError = class extends Error {
|
|
899
|
-
constructor() {
|
|
900
|
-
super("Connection was revoked during credential refresh.");
|
|
901
|
-
this.name = "CredentialRefreshRevokedError";
|
|
902
|
-
}
|
|
903
|
-
};
|
|
904
|
-
var credentialRefreshes = /* @__PURE__ */ new WeakMap();
|
|
905
|
-
var credentialPersistences = /* @__PURE__ */ new WeakMap();
|
|
906
|
-
var credentialRevocations = /* @__PURE__ */ new WeakMap();
|
|
907
|
-
var InMemoryIntegrationSecretStore = class {
|
|
908
|
-
secrets = /* @__PURE__ */ new Map();
|
|
909
|
-
get(ref) {
|
|
910
|
-
return this.secrets.get(secretKey(ref));
|
|
911
|
-
}
|
|
912
|
-
put(ref, credentials) {
|
|
913
|
-
this.secrets.set(secretKey(ref), credentials);
|
|
914
|
-
}
|
|
915
|
-
delete(ref) {
|
|
916
|
-
this.secrets.delete(secretKey(ref));
|
|
917
|
-
}
|
|
918
|
-
};
|
|
919
|
-
var InMemoryIntegrationOAuthStateStore = class {
|
|
920
|
-
states = /* @__PURE__ */ new Map();
|
|
921
|
-
put(state) {
|
|
922
|
-
this.states.set(state.state, state);
|
|
923
|
-
}
|
|
924
|
-
consume(state) {
|
|
925
|
-
const record = this.states.get(state);
|
|
926
|
-
this.states.delete(state);
|
|
927
|
-
if (!record) return { ok: false, reason: "unknown" };
|
|
928
|
-
if (record.expiresAt <= Date.now()) return { ok: false, reason: "expired" };
|
|
929
|
-
return { ok: true, state: record };
|
|
930
|
-
}
|
|
931
|
-
sweep(now) {
|
|
932
|
-
for (const [key, record] of this.states) {
|
|
933
|
-
if (record.expiresAt <= now) this.states.delete(key);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
};
|
|
937
|
-
function createConnectionCredentialResolver(options) {
|
|
938
|
-
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
939
|
-
return async function resolveDataSource(connection) {
|
|
940
|
-
const credentials = await resolveConnectionCredentials(connection, {
|
|
941
|
-
secrets: options.secrets,
|
|
942
|
-
connections: options.connections,
|
|
943
|
-
adapters: options.adapters,
|
|
944
|
-
now,
|
|
945
|
-
markConnectionError: options.markConnectionError
|
|
946
|
-
});
|
|
947
|
-
return {
|
|
948
|
-
id: connection.id,
|
|
949
|
-
projectId: String(connection.metadata?.projectId ?? connection.owner.id),
|
|
950
|
-
publishedAgentId: typeof connection.metadata?.publishedAgentId === "string" ? connection.metadata.publishedAgentId : null,
|
|
951
|
-
kind: connection.connectorId,
|
|
952
|
-
label: connection.account?.displayName ?? connection.account?.email ?? connection.connectorId,
|
|
953
|
-
consistencyModel: typeof connection.metadata?.consistencyModel === "string" ? connection.metadata.consistencyModel : "authoritative",
|
|
954
|
-
scopes: connection.grantedScopes,
|
|
955
|
-
metadata: connection.metadata ?? {},
|
|
956
|
-
credentials,
|
|
957
|
-
status: connection.status === "active" ? "active" : connection.status === "revoked" ? "revoked" : "error"
|
|
958
|
-
};
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
async function resolveConnectionCredentials(input, options) {
|
|
962
|
-
if (input.status !== "active") throw new Error(`Connection ${input.id} is ${input.status}.`);
|
|
963
|
-
if (!input.secretRef) return { kind: "none" };
|
|
964
|
-
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
965
|
-
const current = await options.secrets.get(input.secretRef);
|
|
966
|
-
if (!current) throw new Error(`Secret ${input.secretRef.provider}/${input.secretRef.id} not found.`);
|
|
967
|
-
if (!isExpiredOauth(current, now)) return current;
|
|
968
|
-
const adapter = options.adapters?.find((candidate) => candidate.manifest.kind === input.connectorId);
|
|
969
|
-
if (!adapter?.refreshToken) return current;
|
|
970
|
-
let refresh;
|
|
971
|
-
try {
|
|
972
|
-
await assertCredentialRefreshActive(input, options, now);
|
|
973
|
-
refresh = acquireCredentialRefresh({
|
|
974
|
-
connectorId: input.connectorId,
|
|
975
|
-
secretRef: input.secretRef,
|
|
976
|
-
secrets: options.secrets,
|
|
977
|
-
now,
|
|
978
|
-
refreshToken: adapter.refreshToken.bind(adapter)
|
|
979
|
-
});
|
|
980
|
-
const refreshed = await refresh.promise;
|
|
981
|
-
await assertCredentialRefreshActive(input, options, now, refresh);
|
|
982
|
-
if (options.connections) {
|
|
983
|
-
await options.connections.put({
|
|
984
|
-
...input,
|
|
985
|
-
status: "active",
|
|
986
|
-
updatedAt: now().toISOString(),
|
|
987
|
-
expiresAt: refreshed.kind === "oauth2" && refreshed.expiresAt ? new Date(refreshed.expiresAt).toISOString() : input.expiresAt
|
|
988
|
-
});
|
|
989
|
-
await assertCredentialRefreshActive(input, options, now, refresh);
|
|
990
|
-
}
|
|
991
|
-
return refreshed;
|
|
992
|
-
} catch (error) {
|
|
993
|
-
const err = error instanceof Error ? error : new Error("Credential refresh failed.");
|
|
994
|
-
if (err instanceof CredentialRefreshRevokedError) throw err;
|
|
995
|
-
await options.markConnectionError?.(input, err);
|
|
996
|
-
if (options.connections) {
|
|
997
|
-
await options.connections.put({
|
|
998
|
-
...input,
|
|
999
|
-
status: "expired",
|
|
1000
|
-
updatedAt: now().toISOString()
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
throw err;
|
|
1004
|
-
} finally {
|
|
1005
|
-
if (refresh) {
|
|
1006
|
-
releaseCredentialRefresh(
|
|
1007
|
-
options.secrets,
|
|
1008
|
-
input.connectorId,
|
|
1009
|
-
input.secretRef,
|
|
1010
|
-
refresh
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
function acquireCredentialRefresh(input) {
|
|
1016
|
-
let refreshes = credentialRefreshes.get(input.secrets);
|
|
1017
|
-
if (!refreshes) {
|
|
1018
|
-
refreshes = /* @__PURE__ */ new Map();
|
|
1019
|
-
credentialRefreshes.set(input.secrets, refreshes);
|
|
1020
|
-
}
|
|
1021
|
-
const key = credentialRefreshKey(input.connectorId, input.secretRef);
|
|
1022
|
-
if (credentialRevocations.get(input.secrets)?.has(key)) {
|
|
1023
|
-
throw new CredentialRefreshRevokedError();
|
|
1024
|
-
}
|
|
1025
|
-
const existing = refreshes.get(key);
|
|
1026
|
-
if (existing) {
|
|
1027
|
-
existing.waiters += 1;
|
|
1028
|
-
return existing;
|
|
1029
|
-
}
|
|
1030
|
-
let flight;
|
|
1031
|
-
const promise = Promise.resolve().then(async () => {
|
|
1032
|
-
const latest = await input.secrets.get(input.secretRef);
|
|
1033
|
-
if (!latest) {
|
|
1034
|
-
throw new Error(
|
|
1035
|
-
`Secret ${input.secretRef.provider}/${input.secretRef.id} not found.`
|
|
1036
|
-
);
|
|
1037
|
-
}
|
|
1038
|
-
if (!isExpiredOauth(latest, input.now)) return latest;
|
|
1039
|
-
const refreshed = await input.refreshToken(latest);
|
|
1040
|
-
if (flight.cancelled) throw new CredentialRefreshRevokedError();
|
|
1041
|
-
await input.secrets.put(input.secretRef, refreshed);
|
|
1042
|
-
if (flight.cancelled) {
|
|
1043
|
-
await input.secrets.delete?.(input.secretRef);
|
|
1044
|
-
throw new CredentialRefreshRevokedError();
|
|
1045
|
-
}
|
|
1046
|
-
return refreshed;
|
|
1047
|
-
});
|
|
1048
|
-
flight = {
|
|
1049
|
-
promise,
|
|
1050
|
-
cancelled: false,
|
|
1051
|
-
settled: false,
|
|
1052
|
-
waiters: 1
|
|
1053
|
-
};
|
|
1054
|
-
refreshes.set(key, flight);
|
|
1055
|
-
const settle = () => {
|
|
1056
|
-
flight.settled = true;
|
|
1057
|
-
clearCredentialRefresh(input.secrets, key, flight);
|
|
1058
|
-
};
|
|
1059
|
-
void promise.then(settle, settle);
|
|
1060
|
-
return flight;
|
|
1061
|
-
}
|
|
1062
|
-
function releaseCredentialRefresh(secrets, connectorId, secretRef, flight) {
|
|
1063
|
-
flight.waiters -= 1;
|
|
1064
|
-
clearCredentialRefresh(
|
|
1065
|
-
secrets,
|
|
1066
|
-
credentialRefreshKey(connectorId, secretRef),
|
|
1067
|
-
flight
|
|
1068
|
-
);
|
|
1069
|
-
}
|
|
1070
|
-
function clearCredentialRefresh(secrets, key, flight) {
|
|
1071
|
-
if (!flight.settled || flight.waiters !== 0) return;
|
|
1072
|
-
const refreshes = credentialRefreshes.get(secrets);
|
|
1073
|
-
if (refreshes?.get(key) !== flight) return;
|
|
1074
|
-
refreshes.delete(key);
|
|
1075
|
-
if (refreshes.size === 0) credentialRefreshes.delete(secrets);
|
|
1076
|
-
}
|
|
1077
|
-
async function assertCredentialRefreshActive(input, options, now, operation) {
|
|
1078
|
-
const key = input.secretRef ? credentialRefreshKey(input.connectorId, input.secretRef) : void 0;
|
|
1079
|
-
let revoked = operation?.cancelled === true || key !== void 0 && credentialRevocations.get(options.secrets)?.has(key) === true;
|
|
1080
|
-
let stored;
|
|
1081
|
-
if (options.connections) {
|
|
1082
|
-
stored = await options.connections.get(input.id);
|
|
1083
|
-
revoked ||= stored?.status === "revoked";
|
|
1084
|
-
}
|
|
1085
|
-
if (!revoked && input.secretRef) {
|
|
1086
|
-
revoked = await options.secrets.get(input.secretRef) === void 0;
|
|
1087
|
-
}
|
|
1088
|
-
if (!revoked) return;
|
|
1089
|
-
if (operation) operation.cancelled = true;
|
|
1090
|
-
if (input.secretRef) await options.secrets.delete?.(input.secretRef);
|
|
1091
|
-
if (options.connections && stored?.status !== "revoked") {
|
|
1092
|
-
await options.connections.put({
|
|
1093
|
-
...stored ?? input,
|
|
1094
|
-
status: "revoked",
|
|
1095
|
-
updatedAt: now().toISOString()
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
throw new CredentialRefreshRevokedError();
|
|
1099
|
-
}
|
|
1100
|
-
function createCredentialBackedAdapterProvider(options) {
|
|
1101
|
-
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1102
|
-
return createConnectorAdapterProvider({
|
|
1103
|
-
...options,
|
|
1104
|
-
resolveDataSource: createConnectionCredentialResolver(options),
|
|
1105
|
-
onCredentialsRotated: async ({ connection, credentials }) => {
|
|
1106
|
-
const persistence = connection.secretRef ? acquireCredentialPersistence(
|
|
1107
|
-
options.secrets,
|
|
1108
|
-
connection.connectorId,
|
|
1109
|
-
connection.secretRef
|
|
1110
|
-
) : void 0;
|
|
1111
|
-
try {
|
|
1112
|
-
await assertCredentialRefreshActive(connection, options, now, persistence);
|
|
1113
|
-
if (connection.secretRef) {
|
|
1114
|
-
await options.secrets.put(connection.secretRef, credentials);
|
|
1115
|
-
}
|
|
1116
|
-
await assertCredentialRefreshActive(connection, options, now, persistence);
|
|
1117
|
-
if (options.connections) {
|
|
1118
|
-
await options.connections.put({
|
|
1119
|
-
...connection,
|
|
1120
|
-
status: "active",
|
|
1121
|
-
updatedAt: now().toISOString(),
|
|
1122
|
-
expiresAt: credentials.kind === "oauth2" && credentials.expiresAt ? new Date(credentials.expiresAt).toISOString() : connection.expiresAt
|
|
1123
|
-
});
|
|
1124
|
-
await assertCredentialRefreshActive(connection, options, now, persistence);
|
|
1125
|
-
}
|
|
1126
|
-
await options.onCredentialsRotated?.({ connection, secretRef: connection.secretRef, credentials });
|
|
1127
|
-
await assertCredentialRefreshActive(connection, options, now, persistence);
|
|
1128
|
-
} finally {
|
|
1129
|
-
if (persistence && connection.secretRef) {
|
|
1130
|
-
releaseCredentialPersistence(
|
|
1131
|
-
options.secrets,
|
|
1132
|
-
connection.connectorId,
|
|
1133
|
-
connection.secretRef,
|
|
1134
|
-
persistence
|
|
1135
|
-
);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
function acquireCredentialPersistence(secrets, connectorId, secretRef) {
|
|
1142
|
-
let persistences = credentialPersistences.get(secrets);
|
|
1143
|
-
if (!persistences) {
|
|
1144
|
-
persistences = /* @__PURE__ */ new Map();
|
|
1145
|
-
credentialPersistences.set(secrets, persistences);
|
|
1146
|
-
}
|
|
1147
|
-
const key = credentialRefreshKey(connectorId, secretRef);
|
|
1148
|
-
let leases = persistences.get(key);
|
|
1149
|
-
if (!leases) {
|
|
1150
|
-
leases = /* @__PURE__ */ new Set();
|
|
1151
|
-
persistences.set(key, leases);
|
|
1152
|
-
}
|
|
1153
|
-
const lease = {
|
|
1154
|
-
cancelled: credentialRevocations.get(secrets)?.has(key) === true
|
|
1155
|
-
};
|
|
1156
|
-
leases.add(lease);
|
|
1157
|
-
return lease;
|
|
1158
|
-
}
|
|
1159
|
-
function releaseCredentialPersistence(secrets, connectorId, secretRef, lease) {
|
|
1160
|
-
const persistences = credentialPersistences.get(secrets);
|
|
1161
|
-
if (!persistences) return;
|
|
1162
|
-
const key = credentialRefreshKey(connectorId, secretRef);
|
|
1163
|
-
const leases = persistences.get(key);
|
|
1164
|
-
leases?.delete(lease);
|
|
1165
|
-
if (leases?.size === 0) persistences.delete(key);
|
|
1166
|
-
if (persistences.size === 0) credentialPersistences.delete(secrets);
|
|
1167
|
-
}
|
|
1168
|
-
async function revokeConnection(input) {
|
|
1169
|
-
const finishRevocation = input.connection.secretRef && input.secrets ? beginCredentialRevocation(
|
|
1170
|
-
input.secrets,
|
|
1171
|
-
input.connection.connectorId,
|
|
1172
|
-
input.connection.secretRef
|
|
1173
|
-
) : void 0;
|
|
1174
|
-
try {
|
|
1175
|
-
if (input.connection.secretRef) await input.secrets?.delete?.(input.connection.secretRef);
|
|
1176
|
-
const revoked = {
|
|
1177
|
-
...input.connection,
|
|
1178
|
-
status: "revoked",
|
|
1179
|
-
updatedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
1180
|
-
};
|
|
1181
|
-
await input.connections?.put(revoked);
|
|
1182
|
-
return revoked;
|
|
1183
|
-
} finally {
|
|
1184
|
-
finishRevocation?.();
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
function beginCredentialRevocation(secrets, connectorId, secretRef) {
|
|
1188
|
-
const key = credentialRefreshKey(connectorId, secretRef);
|
|
1189
|
-
let revocations = credentialRevocations.get(secrets);
|
|
1190
|
-
if (!revocations) {
|
|
1191
|
-
revocations = /* @__PURE__ */ new Map();
|
|
1192
|
-
credentialRevocations.set(secrets, revocations);
|
|
1193
|
-
}
|
|
1194
|
-
revocations.set(key, (revocations.get(key) ?? 0) + 1);
|
|
1195
|
-
const flight = credentialRefreshes.get(secrets)?.get(key);
|
|
1196
|
-
if (flight) flight.cancelled = true;
|
|
1197
|
-
for (const persistence of credentialPersistences.get(secrets)?.get(key) ?? []) {
|
|
1198
|
-
persistence.cancelled = true;
|
|
1199
|
-
}
|
|
1200
|
-
return () => {
|
|
1201
|
-
const remaining = (revocations?.get(key) ?? 1) - 1;
|
|
1202
|
-
if (remaining > 0) revocations?.set(key, remaining);
|
|
1203
|
-
else revocations?.delete(key);
|
|
1204
|
-
if (revocations?.size === 0) credentialRevocations.delete(secrets);
|
|
1205
|
-
};
|
|
1206
|
-
}
|
|
1207
|
-
function isExpiredOauth(credentials, now) {
|
|
1208
|
-
return credentials.kind === "oauth2" && typeof credentials.expiresAt === "number" && credentials.expiresAt <= now().getTime() && Boolean(credentials.refreshToken);
|
|
1209
|
-
}
|
|
1210
|
-
function secretKey(ref) {
|
|
1211
|
-
return `${ref.provider}:${ref.id}`;
|
|
1212
|
-
}
|
|
1213
|
-
function credentialRefreshKey(connectorId, ref) {
|
|
1214
|
-
return JSON.stringify([connectorId, ref.provider, ref.id]);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
// src/audit.ts
|
|
1218
|
-
import { randomUUID } from "crypto";
|
|
1219
|
-
var InMemoryIntegrationAuditStore = class {
|
|
1220
|
-
events = [];
|
|
1221
|
-
record(event) {
|
|
1222
|
-
this.events.push(event);
|
|
1223
|
-
}
|
|
1224
|
-
list(filter = {}) {
|
|
1225
|
-
return this.events.filter((event) => matchesFilter(event, filter));
|
|
1226
|
-
}
|
|
1227
|
-
};
|
|
1228
|
-
function createIntegrationAuditEvent(input) {
|
|
1229
|
-
const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : input.occurredAt ?? (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1230
|
-
return {
|
|
1231
|
-
...input,
|
|
1232
|
-
id: input.id ?? `audit_${randomUUID()}`,
|
|
1233
|
-
occurredAt,
|
|
1234
|
-
metadata: input.metadata ? redactUnknown(input.metadata) : void 0
|
|
1235
|
-
};
|
|
1236
|
-
}
|
|
1237
|
-
function createAuditingActionGuard(options) {
|
|
1238
|
-
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1239
|
-
return {
|
|
1240
|
-
async invokeAction(ctx, proceed) {
|
|
1241
|
-
const startedAt = now();
|
|
1242
|
-
try {
|
|
1243
|
-
const result = await proceed();
|
|
1244
|
-
await options.sink.record(actionEvent({
|
|
1245
|
-
ctx,
|
|
1246
|
-
request: ctx.request,
|
|
1247
|
-
result,
|
|
1248
|
-
type: result.ok ? "action.invoked" : "action.failed",
|
|
1249
|
-
subject: options.subject,
|
|
1250
|
-
occurredAt: startedAt,
|
|
1251
|
-
includeInputPreview: options.includeInputPreview
|
|
1252
|
-
}));
|
|
1253
|
-
return result;
|
|
1254
|
-
} catch (error) {
|
|
1255
|
-
await options.sink.record(actionEvent({
|
|
1256
|
-
ctx,
|
|
1257
|
-
request: ctx.request,
|
|
1258
|
-
type: "action.failed",
|
|
1259
|
-
subject: options.subject,
|
|
1260
|
-
occurredAt: startedAt,
|
|
1261
|
-
includeInputPreview: options.includeInputPreview,
|
|
1262
|
-
message: error instanceof Error ? error.message : "Integration action failed."
|
|
1263
|
-
}));
|
|
1264
|
-
throw error;
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
function sanitizeAuditConnection(connection) {
|
|
1270
|
-
return {
|
|
1271
|
-
id: connection.id,
|
|
1272
|
-
owner: connection.owner,
|
|
1273
|
-
providerId: connection.providerId,
|
|
1274
|
-
connectorId: connection.connectorId,
|
|
1275
|
-
status: connection.status,
|
|
1276
|
-
grantedScopes: connection.grantedScopes,
|
|
1277
|
-
account: connection.account,
|
|
1278
|
-
hasSecretRef: Boolean(connection.secretRef),
|
|
1279
|
-
createdAt: connection.createdAt,
|
|
1280
|
-
updatedAt: connection.updatedAt,
|
|
1281
|
-
expiresAt: connection.expiresAt,
|
|
1282
|
-
lastUsedAt: connection.lastUsedAt
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
function actionEvent(input) {
|
|
1286
|
-
return createIntegrationAuditEvent({
|
|
1287
|
-
type: input.type,
|
|
1288
|
-
occurredAt: input.occurredAt,
|
|
1289
|
-
actor: input.subject ?? input.ctx.connection.owner,
|
|
1290
|
-
connectionId: input.ctx.connection.id,
|
|
1291
|
-
providerId: input.ctx.connection.providerId,
|
|
1292
|
-
connectorId: input.ctx.connection.connectorId,
|
|
1293
|
-
action: input.request.action,
|
|
1294
|
-
risk: input.ctx.action?.risk,
|
|
1295
|
-
dataClass: input.ctx.action?.dataClass,
|
|
1296
|
-
ok: input.result?.ok ?? false,
|
|
1297
|
-
message: input.message,
|
|
1298
|
-
metadata: {
|
|
1299
|
-
idempotencyKey: input.request.idempotencyKey,
|
|
1300
|
-
dryRun: input.request.dryRun,
|
|
1301
|
-
externalId: input.result?.externalId,
|
|
1302
|
-
warnings: input.result?.warnings,
|
|
1303
|
-
inputPreview: input.includeInputPreview ? redactUnknown(input.request.input) : void 0
|
|
1304
|
-
}
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
1307
|
-
function matchesFilter(event, filter) {
|
|
1308
|
-
if (filter.type && event.type !== filter.type) return false;
|
|
1309
|
-
if (filter.actor && (!event.actor || event.actor.type !== filter.actor.type || event.actor.id !== filter.actor.id)) return false;
|
|
1310
|
-
if (filter.connectionId && event.connectionId !== filter.connectionId) return false;
|
|
1311
|
-
if (filter.providerId && event.providerId !== filter.providerId) return false;
|
|
1312
|
-
if (filter.connectorId && event.connectorId !== filter.connectorId) return false;
|
|
1313
|
-
if (filter.action && event.action !== filter.action) return false;
|
|
1314
|
-
return true;
|
|
1315
|
-
}
|
|
1316
|
-
function redactUnknown(value) {
|
|
1317
|
-
if (Array.isArray(value)) return value.map(redactUnknown);
|
|
1318
|
-
if (!value || typeof value !== "object") return value;
|
|
1319
|
-
const out = {};
|
|
1320
|
-
for (const [key, child] of Object.entries(value)) {
|
|
1321
|
-
if (/token|secret|password|authorization|api[_-]?key|credential|refresh/i.test(key)) {
|
|
1322
|
-
out[key] = "[REDACTED]";
|
|
1323
|
-
} else {
|
|
1324
|
-
out[key] = redactUnknown(child);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
return out;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
// src/approval.ts
|
|
1331
|
-
import { createHash } from "crypto";
|
|
1332
|
-
var InMemoryIntegrationApprovalStore = class {
|
|
1333
|
-
records = /* @__PURE__ */ new Map();
|
|
1334
|
-
get(approvalId) {
|
|
1335
|
-
return this.records.get(approvalId);
|
|
1336
|
-
}
|
|
1337
|
-
put(record) {
|
|
1338
|
-
this.records.set(record.id, record);
|
|
1339
|
-
}
|
|
1340
|
-
list(filter = {}) {
|
|
1341
|
-
return [...this.records.values()].filter((record) => matchesFilter2(record, filter));
|
|
1342
|
-
}
|
|
1343
|
-
};
|
|
1344
|
-
var ApprovalBackedPolicyEngine = class {
|
|
1345
|
-
base;
|
|
1346
|
-
store;
|
|
1347
|
-
audit;
|
|
1348
|
-
now;
|
|
1349
|
-
approvalTtlMs;
|
|
1350
|
-
constructor(options) {
|
|
1351
|
-
this.base = options.base;
|
|
1352
|
-
this.store = options.store;
|
|
1353
|
-
this.audit = options.audit;
|
|
1354
|
-
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1355
|
-
this.approvalTtlMs = options.approvalTtlMs;
|
|
1356
|
-
}
|
|
1357
|
-
async decide(ctx) {
|
|
1358
|
-
const approved = await this.findApprovedRecord(ctx);
|
|
1359
|
-
if (approved) return { decision: "allow", reason: `Approved by ${approved.resolvedBy?.type ?? "actor"} ${approved.resolvedBy?.id ?? "unknown"}.`, metadata: { approvalId: approved.id } };
|
|
1360
|
-
const decision = await this.base.decide(ctx);
|
|
1361
|
-
if (decision.decision !== "require_approval") return decision;
|
|
1362
|
-
const requestedAt = decision.approval.requestedAt;
|
|
1363
|
-
const expiresAt = this.approvalTtlMs ? new Date(Date.parse(requestedAt) + this.approvalTtlMs).toISOString() : void 0;
|
|
1364
|
-
const record = {
|
|
1365
|
-
id: decision.approval.id,
|
|
1366
|
-
request: decision.approval,
|
|
1367
|
-
status: "pending",
|
|
1368
|
-
requestedAt,
|
|
1369
|
-
expiresAt,
|
|
1370
|
-
metadata: { ...decision.metadata ?? {}, inputHash: approvalInputHash(ctx.request.input) }
|
|
1371
|
-
};
|
|
1372
|
-
await this.store.put(record);
|
|
1373
|
-
await this.audit?.record(createIntegrationAuditEvent({
|
|
1374
|
-
type: "approval.requested",
|
|
1375
|
-
actor: ctx.subject,
|
|
1376
|
-
connectionId: ctx.connection.id,
|
|
1377
|
-
providerId: ctx.connection.providerId,
|
|
1378
|
-
connectorId: ctx.connection.connectorId,
|
|
1379
|
-
action: ctx.request.action,
|
|
1380
|
-
risk: ctx.action?.risk,
|
|
1381
|
-
dataClass: ctx.action?.dataClass,
|
|
1382
|
-
message: decision.reason,
|
|
1383
|
-
metadata: { approvalId: record.id },
|
|
1384
|
-
now: this.now
|
|
1385
|
-
}));
|
|
1386
|
-
return decision;
|
|
1387
|
-
}
|
|
1388
|
-
async findApprovedRecord(ctx) {
|
|
1389
|
-
const approvalId = typeof ctx.request.metadata?.approvalId === "string" ? ctx.request.metadata.approvalId : void 0;
|
|
1390
|
-
if (!approvalId) return void 0;
|
|
1391
|
-
const record = await this.store.get(approvalId);
|
|
1392
|
-
if (!record || record.status !== "approved") return void 0;
|
|
1393
|
-
if (record.expiresAt && Date.parse(record.expiresAt) <= this.now().getTime()) {
|
|
1394
|
-
await this.store.put({ ...record, status: "expired" });
|
|
1395
|
-
return void 0;
|
|
1396
|
-
}
|
|
1397
|
-
if (!approvalMatches(record, ctx)) return void 0;
|
|
1398
|
-
return record;
|
|
1399
|
-
}
|
|
1400
|
-
};
|
|
1401
|
-
function createApprovalBackedPolicyEngine(options) {
|
|
1402
|
-
return new ApprovalBackedPolicyEngine(options);
|
|
1403
|
-
}
|
|
1404
|
-
async function resolveIntegrationApproval(input) {
|
|
1405
|
-
const record = await input.store.get(input.approvalId);
|
|
1406
|
-
if (!record) throw new Error(`Approval ${input.approvalId} not found.`);
|
|
1407
|
-
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
1408
|
-
const next = {
|
|
1409
|
-
...record,
|
|
1410
|
-
status: input.approved ? "approved" : "denied",
|
|
1411
|
-
resolvedAt: now().toISOString(),
|
|
1412
|
-
resolvedBy: input.resolvedBy,
|
|
1413
|
-
reason: input.reason,
|
|
1414
|
-
metadata: { ...record.metadata ?? {}, ...input.metadata ?? {} }
|
|
1415
|
-
};
|
|
1416
|
-
await input.store.put(next);
|
|
1417
|
-
await input.audit?.record(createIntegrationAuditEvent({
|
|
1418
|
-
type: "approval.resolved",
|
|
1419
|
-
actor: input.resolvedBy,
|
|
1420
|
-
connectionId: record.request.connectionId,
|
|
1421
|
-
providerId: record.request.providerId,
|
|
1422
|
-
connectorId: record.request.connectorId,
|
|
1423
|
-
action: record.request.action,
|
|
1424
|
-
risk: record.request.risk,
|
|
1425
|
-
dataClass: record.request.dataClass,
|
|
1426
|
-
ok: input.approved,
|
|
1427
|
-
message: input.reason,
|
|
1428
|
-
metadata: { approvalId: record.id, status: next.status },
|
|
1429
|
-
now
|
|
1430
|
-
}));
|
|
1431
|
-
return next;
|
|
1432
|
-
}
|
|
1433
|
-
function approvalMatches(record, ctx) {
|
|
1434
|
-
return record.request.connectionId === ctx.connection.id && record.request.providerId === ctx.connection.providerId && record.request.connectorId === ctx.connection.connectorId && record.request.action === ctx.request.action && record.request.actor.type === ctx.subject.type && record.request.actor.id === ctx.subject.id && record.metadata?.inputHash === approvalInputHash(ctx.request.input);
|
|
1435
|
-
}
|
|
1436
|
-
function matchesFilter2(record, filter) {
|
|
1437
|
-
if (filter.status && record.status !== filter.status) return false;
|
|
1438
|
-
if (filter.connectionId && record.request.connectionId !== filter.connectionId) return false;
|
|
1439
|
-
if (filter.connectorId && record.request.connectorId !== filter.connectorId) return false;
|
|
1440
|
-
if (filter.action && record.request.action !== filter.action) return false;
|
|
1441
|
-
if (filter.actor && (record.request.actor.type !== filter.actor.type || record.request.actor.id !== filter.actor.id)) return false;
|
|
1442
|
-
return true;
|
|
1443
|
-
}
|
|
1444
|
-
function approvalInputHash(input) {
|
|
1445
|
-
return createHash("sha256").update(JSON.stringify(input ?? null)).digest("base64url");
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
// src/actions.ts
|
|
1449
|
-
var CANONICAL_INTEGRATION_ACTIONS = {
|
|
1450
|
-
googleCalendarEventsList: "google-calendar.events.list",
|
|
1451
|
-
googleCalendarEventsCreate: "google-calendar.events.create",
|
|
1452
|
-
gmailMessagesSearch: "gmail.messages.search",
|
|
1453
|
-
gmailMessagesSend: "gmail.messages.send",
|
|
1454
|
-
googleDriveFilesSearch: "google-drive.files.search",
|
|
1455
|
-
googleDriveFilesRead: "google-drive.files.read",
|
|
1456
|
-
githubRepositoriesGet: "github.repositories.get",
|
|
1457
|
-
githubIssuesSearch: "github.issues.search",
|
|
1458
|
-
githubIssuesCreate: "github.issues.create",
|
|
1459
|
-
githubPullRequestsComment: "github.pull-requests.comment",
|
|
1460
|
-
slackChannelsList: "slack.channels.list",
|
|
1461
|
-
slackMessagesSearch: "slack.messages.search",
|
|
1462
|
-
slackMessagesPost: "slack.messages.post",
|
|
1463
|
-
providerHttpRequest: "provider.http.request"
|
|
1464
|
-
};
|
|
1465
|
-
function buildCanonicalLaunchConnectors(options = {}) {
|
|
1466
|
-
const providerId = options.providerId ?? "tangle-platform";
|
|
1467
|
-
const connectors = [
|
|
1468
|
-
googleCalendarConnector(providerId),
|
|
1469
|
-
gmailConnector(providerId),
|
|
1470
|
-
googleDriveConnector(providerId),
|
|
1471
|
-
githubConnector(providerId),
|
|
1472
|
-
slackConnector(providerId)
|
|
1473
|
-
];
|
|
1474
|
-
if (!options.includeProviderPassthrough) return connectors;
|
|
1475
|
-
return connectors.map((connector) => ({
|
|
1476
|
-
...connector,
|
|
1477
|
-
actions: [...connector.actions, providerPassthroughAction(connector.id)]
|
|
1478
|
-
}));
|
|
1479
|
-
}
|
|
1480
|
-
function canonicalActionConnectorId(actionId) {
|
|
1481
|
-
if (actionId.startsWith("google-calendar.")) return "google-calendar";
|
|
1482
|
-
if (actionId.startsWith("gmail.")) return "gmail";
|
|
1483
|
-
if (actionId.startsWith("google-drive.")) return "google-drive";
|
|
1484
|
-
if (actionId.startsWith("github.")) return "github";
|
|
1485
|
-
if (actionId.startsWith("slack.")) return "slack";
|
|
1486
|
-
if (actionId === CANONICAL_INTEGRATION_ACTIONS.providerHttpRequest) return void 0;
|
|
1487
|
-
return actionId.split(".")[0];
|
|
1488
|
-
}
|
|
1489
|
-
function googleCalendarConnector(providerId) {
|
|
1490
|
-
return {
|
|
1491
|
-
id: "google-calendar",
|
|
1492
|
-
providerId,
|
|
1493
|
-
title: "Google Calendar",
|
|
1494
|
-
category: "calendar",
|
|
1495
|
-
auth: "oauth2",
|
|
1496
|
-
scopes: ["https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/calendar.events"],
|
|
1497
|
-
actions: [
|
|
1498
|
-
{
|
|
1499
|
-
id: CANONICAL_INTEGRATION_ACTIONS.googleCalendarEventsList,
|
|
1500
|
-
title: "List calendar events",
|
|
1501
|
-
risk: "read",
|
|
1502
|
-
requiredScopes: ["https://www.googleapis.com/auth/calendar.readonly"],
|
|
1503
|
-
dataClass: "private",
|
|
1504
|
-
description: "Read events from a Google Calendar over a bounded time range.",
|
|
1505
|
-
inputSchema: objectSchema({
|
|
1506
|
-
calendarId: { type: "string", default: "primary" },
|
|
1507
|
-
timeMin: { type: "string", description: "RFC3339 lower bound." },
|
|
1508
|
-
timeMax: { type: "string", description: "RFC3339 upper bound." }
|
|
1509
|
-
}, ["timeMin", "timeMax"])
|
|
1510
|
-
},
|
|
1511
|
-
{
|
|
1512
|
-
id: CANONICAL_INTEGRATION_ACTIONS.googleCalendarEventsCreate,
|
|
1513
|
-
title: "Create calendar event",
|
|
1514
|
-
risk: "write",
|
|
1515
|
-
requiredScopes: ["https://www.googleapis.com/auth/calendar.events"],
|
|
1516
|
-
dataClass: "private",
|
|
1517
|
-
approvalRequired: true,
|
|
1518
|
-
description: "Create an event on a Google Calendar after user approval.",
|
|
1519
|
-
inputSchema: objectSchema({
|
|
1520
|
-
calendarId: { type: "string", default: "primary" },
|
|
1521
|
-
start: { type: "string", description: "RFC3339 start time." },
|
|
1522
|
-
end: { type: "string", description: "RFC3339 end time." },
|
|
1523
|
-
summary: { type: "string" },
|
|
1524
|
-
description: { type: "string" },
|
|
1525
|
-
attendees: { type: "array", items: { type: "string" } }
|
|
1526
|
-
}, ["start", "end", "summary"])
|
|
1527
|
-
}
|
|
1528
|
-
],
|
|
1529
|
-
metadata: { source: "canonical-launch", supportTier: "setupReady" }
|
|
1530
|
-
};
|
|
1531
|
-
}
|
|
1532
|
-
function gmailConnector(providerId) {
|
|
1533
|
-
return {
|
|
1534
|
-
id: "gmail",
|
|
1535
|
-
providerId,
|
|
1536
|
-
title: "Gmail",
|
|
1537
|
-
category: "email",
|
|
1538
|
-
auth: "oauth2",
|
|
1539
|
-
scopes: ["https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/gmail.send"],
|
|
1540
|
-
actions: [
|
|
1541
|
-
{
|
|
1542
|
-
id: CANONICAL_INTEGRATION_ACTIONS.gmailMessagesSearch,
|
|
1543
|
-
title: "Search Gmail messages",
|
|
1544
|
-
risk: "read",
|
|
1545
|
-
requiredScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
|
|
1546
|
-
dataClass: "private",
|
|
1547
|
-
description: "Search user Gmail messages and return bounded message metadata/snippets.",
|
|
1548
|
-
inputSchema: objectSchema({ query: { type: "string" }, maxResults: { type: "integer", minimum: 1, maximum: 50 } }, ["query"])
|
|
1549
|
-
},
|
|
1550
|
-
{
|
|
1551
|
-
id: CANONICAL_INTEGRATION_ACTIONS.gmailMessagesSend,
|
|
1552
|
-
title: "Send Gmail message",
|
|
1553
|
-
risk: "write",
|
|
1554
|
-
requiredScopes: ["https://www.googleapis.com/auth/gmail.send"],
|
|
1555
|
-
dataClass: "private",
|
|
1556
|
-
approvalRequired: true,
|
|
1557
|
-
description: "Send an email from the user account after approval.",
|
|
1558
|
-
inputSchema: objectSchema({
|
|
1559
|
-
to: { type: "array", items: { type: "string" } },
|
|
1560
|
-
subject: { type: "string" },
|
|
1561
|
-
body: { type: "string" }
|
|
1562
|
-
}, ["to", "subject", "body"])
|
|
1563
|
-
}
|
|
1564
|
-
],
|
|
1565
|
-
triggers: [{
|
|
1566
|
-
id: "gmail.messages.received",
|
|
1567
|
-
title: "Gmail message received",
|
|
1568
|
-
requiredScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
|
|
1569
|
-
dataClass: "private",
|
|
1570
|
-
description: "Triggered when a new matching Gmail message is received."
|
|
1571
|
-
}],
|
|
1572
|
-
metadata: { source: "canonical-launch", supportTier: "setupReady" }
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
function googleDriveConnector(providerId) {
|
|
1576
|
-
return {
|
|
1577
|
-
id: "google-drive",
|
|
1578
|
-
providerId,
|
|
1579
|
-
title: "Google Drive",
|
|
1580
|
-
category: "storage",
|
|
1581
|
-
auth: "oauth2",
|
|
1582
|
-
scopes: ["https://www.googleapis.com/auth/drive.readonly", "https://www.googleapis.com/auth/drive.file"],
|
|
1583
|
-
actions: [
|
|
1584
|
-
{
|
|
1585
|
-
id: CANONICAL_INTEGRATION_ACTIONS.googleDriveFilesSearch,
|
|
1586
|
-
title: "Search Drive files",
|
|
1587
|
-
risk: "read",
|
|
1588
|
-
requiredScopes: ["https://www.googleapis.com/auth/drive.readonly"],
|
|
1589
|
-
dataClass: "private",
|
|
1590
|
-
description: "Search user-visible Google Drive files.",
|
|
1591
|
-
inputSchema: objectSchema({ query: { type: "string" }, maxResults: { type: "integer", minimum: 1, maximum: 50 } }, ["query"])
|
|
1592
|
-
},
|
|
1593
|
-
{
|
|
1594
|
-
id: CANONICAL_INTEGRATION_ACTIONS.googleDriveFilesRead,
|
|
1595
|
-
title: "Read Drive file",
|
|
1596
|
-
risk: "read",
|
|
1597
|
-
requiredScopes: ["https://www.googleapis.com/auth/drive.readonly"],
|
|
1598
|
-
dataClass: "private",
|
|
1599
|
-
description: "Read metadata and content for an authorized Drive file.",
|
|
1600
|
-
inputSchema: objectSchema({ fileId: { type: "string" } }, ["fileId"])
|
|
1601
|
-
}
|
|
1602
|
-
],
|
|
1603
|
-
metadata: { source: "canonical-launch", supportTier: "setupReady" }
|
|
1604
|
-
};
|
|
1605
|
-
}
|
|
1606
|
-
function githubConnector(providerId) {
|
|
1607
|
-
return {
|
|
1608
|
-
id: "github",
|
|
1609
|
-
providerId,
|
|
1610
|
-
title: "GitHub",
|
|
1611
|
-
category: "workflow",
|
|
1612
|
-
auth: "oauth2",
|
|
1613
|
-
scopes: ["repo", "read:user"],
|
|
1614
|
-
actions: [
|
|
1615
|
-
readAction(CANONICAL_INTEGRATION_ACTIONS.githubRepositoriesGet, "Read repository metadata", ["repo"], objectSchema({ owner: { type: "string" }, repo: { type: "string" } }, ["owner", "repo"])),
|
|
1616
|
-
readAction(CANONICAL_INTEGRATION_ACTIONS.githubIssuesSearch, "Search issues and pull requests", ["repo"], objectSchema({ query: { type: "string" }, limit: { type: "integer", minimum: 1, maximum: 50 } }, ["query"])),
|
|
1617
|
-
writeAction(CANONICAL_INTEGRATION_ACTIONS.githubIssuesCreate, "Create issue", ["repo"], objectSchema({ owner: { type: "string" }, repo: { type: "string" }, title: { type: "string" }, body: { type: "string" } }, ["owner", "repo", "title"])),
|
|
1618
|
-
writeAction(CANONICAL_INTEGRATION_ACTIONS.githubPullRequestsComment, "Comment on pull request", ["repo"], objectSchema({ owner: { type: "string" }, repo: { type: "string" }, pullNumber: { type: "integer" }, body: { type: "string" } }, ["owner", "repo", "pullNumber", "body"]))
|
|
1619
|
-
],
|
|
1620
|
-
metadata: { source: "canonical-launch", supportTier: "setupReady" }
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
function slackConnector(providerId) {
|
|
1624
|
-
return {
|
|
1625
|
-
id: "slack",
|
|
1626
|
-
providerId,
|
|
1627
|
-
title: "Slack",
|
|
1628
|
-
category: "chat",
|
|
1629
|
-
auth: "oauth2",
|
|
1630
|
-
scopes: ["channels:read", "search:read", "chat:write"],
|
|
1631
|
-
actions: [
|
|
1632
|
-
readAction(CANONICAL_INTEGRATION_ACTIONS.slackChannelsList, "List Slack channels", ["channels:read"], objectSchema({ limit: { type: "integer", minimum: 1, maximum: 200 } })),
|
|
1633
|
-
readAction(CANONICAL_INTEGRATION_ACTIONS.slackMessagesSearch, "Search Slack messages", ["search:read"], objectSchema({ query: { type: "string" }, count: { type: "integer", minimum: 1, maximum: 50 } }, ["query"])),
|
|
1634
|
-
writeAction(CANONICAL_INTEGRATION_ACTIONS.slackMessagesPost, "Post Slack message", ["chat:write"], objectSchema({ channel: { type: "string" }, text: { type: "string" }, blocks: { type: "array" } }, ["channel", "text"]))
|
|
1635
|
-
],
|
|
1636
|
-
triggers: [trigger("slack.message.posted", "Slack message posted", ["channels:read"])],
|
|
1637
|
-
metadata: { source: "canonical-launch", supportTier: "setupReady" }
|
|
1638
|
-
};
|
|
1639
|
-
}
|
|
1640
|
-
function readAction(id, title, scopes, inputSchema) {
|
|
1641
|
-
return { id, title, risk: "read", requiredScopes: scopes, dataClass: "private", inputSchema };
|
|
1642
|
-
}
|
|
1643
|
-
function writeAction(id, title, scopes, inputSchema) {
|
|
1644
|
-
return { id, title, risk: "write", requiredScopes: scopes, dataClass: "private", approvalRequired: true, inputSchema };
|
|
1645
|
-
}
|
|
1646
|
-
function trigger(id, title, scopes) {
|
|
1647
|
-
return { id, title, requiredScopes: scopes, dataClass: "private" };
|
|
1648
|
-
}
|
|
1649
|
-
function providerPassthroughAction(connectorId) {
|
|
1650
|
-
return {
|
|
1651
|
-
id: CANONICAL_INTEGRATION_ACTIONS.providerHttpRequest,
|
|
1652
|
-
title: "Provider HTTP request",
|
|
1653
|
-
risk: "write",
|
|
1654
|
-
requiredScopes: [],
|
|
1655
|
-
dataClass: "sensitive",
|
|
1656
|
-
approvalRequired: true,
|
|
1657
|
-
description: `Controlled provider-native passthrough for ${connectorId}. Disabled by default by platform policy.`,
|
|
1658
|
-
inputSchema: objectSchema({
|
|
1659
|
-
method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
|
|
1660
|
-
path: { type: "string" },
|
|
1661
|
-
query: { type: "object" },
|
|
1662
|
-
body: { type: "object" }
|
|
1663
|
-
}, ["method", "path"])
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
function objectSchema(properties, required = []) {
|
|
1667
|
-
return { type: "object", additionalProperties: false, properties, required };
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
// src/bridge.ts
|
|
1671
|
-
var DEFAULT_INTEGRATION_BRIDGE_ENV = "TANGLE_INTEGRATION_BUNDLE";
|
|
1672
|
-
function buildIntegrationBridgePayload(bundle) {
|
|
1673
|
-
return {
|
|
1674
|
-
version: 1,
|
|
1675
|
-
manifestId: bundle.manifestId,
|
|
1676
|
-
subject: bundle.subject,
|
|
1677
|
-
expiresAt: bundle.expiresAt,
|
|
1678
|
-
tools: bundle.tools.flatMap((tool) => {
|
|
1679
|
-
const binding = bundle.capabilities.find(
|
|
1680
|
-
(candidate) => candidate.connectorId === tool.connectorId && candidate.connectionId && candidate.allowedActions.includes(tool.action.id)
|
|
1681
|
-
);
|
|
1682
|
-
if (!binding) return [];
|
|
1683
|
-
return [{
|
|
1684
|
-
name: tool.name,
|
|
1685
|
-
title: tool.title,
|
|
1686
|
-
connectorId: tool.connectorId,
|
|
1687
|
-
connectionId: binding.connectionId,
|
|
1688
|
-
action: tool.action.id,
|
|
1689
|
-
risk: tool.risk,
|
|
1690
|
-
dataClass: tool.dataClass,
|
|
1691
|
-
requiredScopes: tool.requiredScopes,
|
|
1692
|
-
capabilityToken: binding.capability.token
|
|
1693
|
-
}];
|
|
1694
|
-
})
|
|
1695
|
-
};
|
|
1696
|
-
}
|
|
1697
|
-
function encodeIntegrationBridgePayload(payload) {
|
|
1698
|
-
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
1699
|
-
}
|
|
1700
|
-
function decodeIntegrationBridgePayload(encoded) {
|
|
1701
|
-
const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
1702
|
-
assertBridgePayload(parsed);
|
|
1703
|
-
return parsed;
|
|
1704
|
-
}
|
|
1705
|
-
function buildIntegrationBridgeEnvironment(bundle, options = {}) {
|
|
1706
|
-
const envVar = options.envVar ?? DEFAULT_INTEGRATION_BRIDGE_ENV;
|
|
1707
|
-
return {
|
|
1708
|
-
[envVar]: encodeIntegrationBridgePayload(buildIntegrationBridgePayload(bundle))
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
|
-
function parseIntegrationBridgeEnvironment(env, options = {}) {
|
|
1712
|
-
const envVar = options.envVar ?? DEFAULT_INTEGRATION_BRIDGE_ENV;
|
|
1713
|
-
const encoded = env[envVar];
|
|
1714
|
-
if (!encoded) throw new Error(`Missing ${envVar}.`);
|
|
1715
|
-
return decodeIntegrationBridgePayload(encoded);
|
|
1716
|
-
}
|
|
1717
|
-
function redactIntegrationBridgePayload(payload) {
|
|
1718
|
-
return {
|
|
1719
|
-
...payload,
|
|
1720
|
-
tools: payload.tools.map((tool) => ({
|
|
1721
|
-
...tool,
|
|
1722
|
-
capabilityToken: "[REDACTED]"
|
|
1723
|
-
}))
|
|
1724
|
-
};
|
|
1725
|
-
}
|
|
1726
|
-
function assertBridgePayload(value) {
|
|
1727
|
-
if (!value || typeof value !== "object") throw new Error("Invalid integration bridge payload.");
|
|
1728
|
-
const payload = value;
|
|
1729
|
-
if (payload.version !== 1) throw new Error("Unsupported integration bridge payload version.");
|
|
1730
|
-
if (typeof payload.manifestId !== "string") throw new Error("Invalid integration bridge manifestId.");
|
|
1731
|
-
if (typeof payload.expiresAt !== "string") throw new Error("Invalid integration bridge expiresAt.");
|
|
1732
|
-
if (!Array.isArray(payload.tools)) throw new Error("Invalid integration bridge tools.");
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
// src/apps.ts
|
|
1736
|
-
var TangleAppsClient = class {
|
|
1737
|
-
endpoint;
|
|
1738
|
-
fetchImpl;
|
|
1739
|
-
constructor(options) {
|
|
1740
|
-
this.endpoint = options.endpoint.replace(/\/$/, "");
|
|
1741
|
-
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
1742
|
-
}
|
|
1743
|
-
/**
|
|
1744
|
-
* Register a product as an app (ONE-TIME, with the owner's bearer — a Tangle
|
|
1745
|
-
* session or `sk-tan-*` key). Returns the client_id + the once-shown
|
|
1746
|
-
* client_secret; persist the secret immediately (never retrievable again).
|
|
1747
|
-
*/
|
|
1748
|
-
async registerApp(input, ownerBearer) {
|
|
1749
|
-
const data = await this.request(
|
|
1750
|
-
"POST",
|
|
1751
|
-
"/v1/apps",
|
|
1752
|
-
input,
|
|
1753
|
-
ownerBearer
|
|
1754
|
-
);
|
|
1755
|
-
return { ...data.app, clientSecret: data.clientSecret };
|
|
1756
|
-
}
|
|
1757
|
-
/** List the caller's registered apps (no secrets). */
|
|
1758
|
-
async listApps(ownerBearer) {
|
|
1759
|
-
const data = await this.request("GET", "/v1/apps", void 0, ownerBearer);
|
|
1760
|
-
return data.apps ?? [];
|
|
1761
|
-
}
|
|
1762
|
-
/** Revoke an app and cascade-kill its grants + tokens. */
|
|
1763
|
-
revokeApp(appId, ownerBearer) {
|
|
1764
|
-
return this.request("POST", `/v1/apps/${encodeURIComponent(appId)}/revoke`, {}, ownerBearer);
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Durable re-mint: mint a fresh single-use `sk-tan-broker-` token against an
|
|
1768
|
-
* existing consented grant using ONLY the app credentials — no user session,
|
|
1769
|
-
* no `agc_` code. The runtime path: one call per `/v1/hub/exec`.
|
|
1770
|
-
*/
|
|
1771
|
-
async mintBrokerToken(input) {
|
|
1772
|
-
const data = await this.request(
|
|
1773
|
-
"POST",
|
|
1774
|
-
`/v1/apps/grants/${encodeURIComponent(input.grantId)}/mint-broker-token`,
|
|
1775
|
-
{
|
|
1776
|
-
client_id: input.clientId,
|
|
1777
|
-
client_secret: input.clientSecret,
|
|
1778
|
-
grant_id: input.grantId,
|
|
1779
|
-
...input.ttlSeconds ? { ttl_seconds: input.ttlSeconds } : {}
|
|
1780
|
-
}
|
|
1781
|
-
);
|
|
1782
|
-
return toBrokerToken(data);
|
|
1783
|
-
}
|
|
1784
|
-
/**
|
|
1785
|
-
* Exchange an `agc_` authorization code (from the user's one-time consent)
|
|
1786
|
-
* for the first broker token + the durable grant. Use on the consent
|
|
1787
|
-
* callback; afterward `mintBrokerToken` is enough.
|
|
1788
|
-
*/
|
|
1789
|
-
async exchangeAuthCode(input) {
|
|
1790
|
-
const data = await this.request("POST", "/v1/apps/oauth/token", {
|
|
1791
|
-
grant_type: "authorization_code",
|
|
1792
|
-
client_id: input.clientId,
|
|
1793
|
-
client_secret: input.clientSecret,
|
|
1794
|
-
code: input.code,
|
|
1795
|
-
redirect_uri: input.redirectUri,
|
|
1796
|
-
...input.connectionId ? { connection_id: input.connectionId } : {}
|
|
1797
|
-
});
|
|
1798
|
-
return toBrokerToken(data);
|
|
1799
|
-
}
|
|
1800
|
-
async request(method, path, body, bearer) {
|
|
1801
|
-
try {
|
|
1802
|
-
const headers = { accept: "application/json" };
|
|
1803
|
-
if (bearer) headers.authorization = `Bearer ${bearer}`;
|
|
1804
|
-
if (body !== void 0) headers["content-type"] = "application/json";
|
|
1805
|
-
const response = await this.fetchImpl(`${this.endpoint}${path}`, {
|
|
1806
|
-
method,
|
|
1807
|
-
headers,
|
|
1808
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1809
|
-
});
|
|
1810
|
-
const json = await response.json().catch(() => void 0);
|
|
1811
|
-
if (!response.ok || json && json.success === false) {
|
|
1812
|
-
const platformCode = json?.error && typeof json.error === "object" ? json.error.code : void 0;
|
|
1813
|
-
const message = json?.error && typeof json.error === "object" && json.error.message || (typeof json?.error === "string" ? json.error : `Tangle apps request failed (HTTP ${response.status})`);
|
|
1814
|
-
throw new IntegrationRuntimeError({
|
|
1815
|
-
code: platformCode === "BROKER_DISABLED" ? "passthrough_disabled" : "unknown",
|
|
1816
|
-
message,
|
|
1817
|
-
status: response.status,
|
|
1818
|
-
metadata: { platformCode, path }
|
|
1819
|
-
});
|
|
1820
|
-
}
|
|
1821
|
-
return (json && "data" in json ? json.data : json) ?? {};
|
|
1822
|
-
} catch (error) {
|
|
1823
|
-
if (error instanceof IntegrationRuntimeError) throw error;
|
|
1824
|
-
const normalized = normalizeIntegrationError(error);
|
|
1825
|
-
throw new IntegrationRuntimeError({
|
|
1826
|
-
code: normalized.code,
|
|
1827
|
-
message: normalized.message,
|
|
1828
|
-
metadata: { path, userAction: normalized.userAction }
|
|
1829
|
-
});
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
};
|
|
1833
|
-
function createTangleAppsClient(options) {
|
|
1834
|
-
return new TangleAppsClient(options);
|
|
1835
|
-
}
|
|
1836
|
-
function toBrokerToken(data) {
|
|
1837
|
-
return {
|
|
1838
|
-
accessToken: data.access_token,
|
|
1839
|
-
expiresIn: data.expires_in,
|
|
1840
|
-
scope: data.scope,
|
|
1841
|
-
connectionId: data.connection_id
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
// src/client.ts
|
|
1846
|
-
var TangleIntegrationsClient = class {
|
|
1847
|
-
endpoint;
|
|
1848
|
-
bridge;
|
|
1849
|
-
fetchImpl;
|
|
1850
|
-
getCapabilityToken;
|
|
1851
|
-
constructor(options) {
|
|
1852
|
-
this.endpoint = options.endpoint.replace(/\/$/, "");
|
|
1853
|
-
this.bridge = options.bridge ?? parseIntegrationBridgeEnvironment(
|
|
1854
|
-
options.env ?? readProcessEnv(),
|
|
1855
|
-
{ envVar: options.envVar ?? DEFAULT_INTEGRATION_BRIDGE_ENV }
|
|
1856
|
-
);
|
|
1857
|
-
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
1858
|
-
this.getCapabilityToken = options.getCapabilityToken ?? ((tool) => tool.capabilityToken);
|
|
1859
|
-
}
|
|
1860
|
-
tools() {
|
|
1861
|
-
return [...this.bridge.tools];
|
|
1862
|
-
}
|
|
1863
|
-
findTool(toolOrAction) {
|
|
1864
|
-
const found = this.bridge.tools.find(
|
|
1865
|
-
(tool) => tool.name === toolOrAction || tool.action === toolOrAction || `${tool.connectorId}.${tool.action}` === toolOrAction
|
|
1866
|
-
);
|
|
1867
|
-
if (!found) {
|
|
1868
|
-
throw new IntegrationRuntimeError({
|
|
1869
|
-
code: "action_not_found",
|
|
1870
|
-
message: `Integration tool ${toolOrAction} is not available in this runtime.`,
|
|
1871
|
-
metadata: { available: this.bridge.tools.map((tool) => ({ name: tool.name, action: tool.action, connectorId: tool.connectorId })) }
|
|
1872
|
-
});
|
|
1873
|
-
}
|
|
1874
|
-
return found;
|
|
1875
|
-
}
|
|
1876
|
-
async invoke(input) {
|
|
1877
|
-
try {
|
|
1878
|
-
const tool = this.findTool(input.tool);
|
|
1879
|
-
const token = await this.getCapabilityToken(tool);
|
|
1880
|
-
const response = await this.fetchImpl(`${this.endpoint}/v1/integrations/invoke`, {
|
|
1881
|
-
method: "POST",
|
|
1882
|
-
headers: {
|
|
1883
|
-
"content-type": "application/json",
|
|
1884
|
-
authorization: `Bearer ${token}`
|
|
1885
|
-
},
|
|
1886
|
-
body: JSON.stringify({
|
|
1887
|
-
action: tool.action,
|
|
1888
|
-
input: input.input,
|
|
1889
|
-
idempotencyKey: input.idempotencyKey ?? defaultIdempotencyKey(tool.action),
|
|
1890
|
-
dryRun: input.dryRun,
|
|
1891
|
-
metadata: input.metadata
|
|
1892
|
-
})
|
|
1893
|
-
});
|
|
1894
|
-
const json = await response.json().catch(() => void 0);
|
|
1895
|
-
if (!response.ok && !json) {
|
|
1896
|
-
return { status: "failed", action: tool.action, error: `Integration invoke failed with HTTP ${response.status}` };
|
|
1897
|
-
}
|
|
1898
|
-
return json ?? { status: "failed", action: tool.action, error: "Integration invoke returned an empty response." };
|
|
1899
|
-
} catch (error) {
|
|
1900
|
-
const normalized = normalizeIntegrationError(error);
|
|
1901
|
-
return { status: "failed", action: input.tool, error: normalized.message, metadata: { code: normalized.code, userAction: normalized.userAction } };
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
};
|
|
1905
|
-
function createTangleIntegrationsClient(options) {
|
|
1906
|
-
return new TangleIntegrationsClient(options);
|
|
1907
|
-
}
|
|
1908
|
-
function defaultIdempotencyKey(action) {
|
|
1909
|
-
return `${action}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
1910
|
-
}
|
|
1911
|
-
function readProcessEnv() {
|
|
1912
|
-
if (typeof process !== "undefined" && process.env) return process.env;
|
|
1913
|
-
return {};
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
// src/consent.ts
|
|
1917
|
-
function renderConsentSummary(manifestOrResolution, options = {}) {
|
|
1918
|
-
const manifest = "manifest" in manifestOrResolution ? manifestOrResolution.manifest : manifestOrResolution;
|
|
1919
|
-
const appName = options.appName ?? manifest.title ?? manifest.id;
|
|
1920
|
-
const requirements = manifest.requirements;
|
|
1921
|
-
const risk = aggregateRisk(requirements, options.connectors);
|
|
1922
|
-
const connectorIds = unique(requirements.map((requirement) => requirement.connectorId));
|
|
1923
|
-
const first = requirements[0];
|
|
1924
|
-
const body = first ? sentenceForRequirement(appName, first) : `${appName} does not request integrations.`;
|
|
1925
|
-
return {
|
|
1926
|
-
title: `${appName} wants to use ${humanList(connectorIds.map(titleize))}`,
|
|
1927
|
-
body,
|
|
1928
|
-
bullets: requirements.map((requirement) => bulletForRequirement(requirement, options.connectors)),
|
|
1929
|
-
primaryAction: risk === "read" ? "Allow access" : risk === "write" ? "Review and allow" : "Review destructive access",
|
|
1930
|
-
risk,
|
|
1931
|
-
connectorIds
|
|
1932
|
-
};
|
|
1933
|
-
}
|
|
1934
|
-
function renderApprovalCopy(input) {
|
|
1935
|
-
return {
|
|
1936
|
-
title: `${input.appName} wants to ${input.action.title.toLowerCase()}`,
|
|
1937
|
-
body: `${input.appName} is requesting permission to run "${input.action.title}" on ${input.connectorTitle}.`,
|
|
1938
|
-
bullets: [
|
|
1939
|
-
`Risk: ${input.action.risk}`,
|
|
1940
|
-
`Data: ${input.action.dataClass}`,
|
|
1941
|
-
...input.approvalId ? [`Approval id: ${input.approvalId}`] : []
|
|
1942
|
-
],
|
|
1943
|
-
primaryAction: input.action.risk === "read" ? "Allow" : "Approve action",
|
|
1944
|
-
risk: input.action.risk,
|
|
1945
|
-
connectorIds: []
|
|
1946
|
-
};
|
|
1947
|
-
}
|
|
1948
|
-
function sentenceForRequirement(appName, requirement) {
|
|
1949
|
-
if (requirement.connectorId === "google-calendar" && requirement.mode === "read") {
|
|
1950
|
-
return `${appName} wants to read your Google Calendar to find schedule-aware recommendations.`;
|
|
1951
|
-
}
|
|
1952
|
-
if (requirement.connectorId === "google-calendar" && requirement.mode === "write") {
|
|
1953
|
-
return `${appName} wants to create or update Google Calendar events after your approval.`;
|
|
1954
|
-
}
|
|
1955
|
-
if (requirement.mode === "read") return `${appName} wants to read ${titleize(requirement.connectorId)} data.`;
|
|
1956
|
-
if (requirement.mode === "write") return `${appName} wants to write ${titleize(requirement.connectorId)} data after approval.`;
|
|
1957
|
-
return `${appName} wants to subscribe to ${titleize(requirement.connectorId)} events.`;
|
|
1958
|
-
}
|
|
1959
|
-
function bulletForRequirement(requirement, connectors = []) {
|
|
1960
|
-
const connector = connectors.find((candidate) => candidate.id === requirement.connectorId);
|
|
1961
|
-
const actions = requirement.requiredActions?.length ? requirement.requiredActions.map((id) => connector?.actions.find((action) => action.id === id)?.title ?? id) : requirement.requiredTriggers ?? [];
|
|
1962
|
-
return `${titleize(requirement.connectorId)}: ${requirement.reason}${actions.length ? ` (${actions.join(", ")})` : ""}`;
|
|
1963
|
-
}
|
|
1964
|
-
function aggregateRisk(requirements, connectors = []) {
|
|
1965
|
-
let rank = 0;
|
|
1966
|
-
for (const requirement of requirements) {
|
|
1967
|
-
if (requirement.mode === "write") rank = Math.max(rank, 1);
|
|
1968
|
-
const connector = connectors.find((candidate) => candidate.id === requirement.connectorId);
|
|
1969
|
-
for (const actionId of requirement.requiredActions ?? []) {
|
|
1970
|
-
const risk = connector?.actions.find((action) => action.id === actionId)?.risk;
|
|
1971
|
-
if (risk === "write") rank = Math.max(rank, 1);
|
|
1972
|
-
if (risk === "destructive") rank = Math.max(rank, 2);
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
return rank === 2 ? "destructive" : rank === 1 ? "write" : "read";
|
|
1976
|
-
}
|
|
1977
|
-
function humanList(values) {
|
|
1978
|
-
if (values.length <= 1) return values[0] ?? "integrations";
|
|
1979
|
-
if (values.length === 2) return `${values[0]} and ${values[1]}`;
|
|
1980
|
-
return `${values.slice(0, -1).join(", ")}, and ${values.at(-1)}`;
|
|
1981
|
-
}
|
|
1982
|
-
function titleize(value) {
|
|
1983
|
-
return value.split(/[-_.]/g).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
1984
|
-
}
|
|
1985
|
-
function unique(values) {
|
|
1986
|
-
return [...new Set(values)];
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
// src/discovery.ts
|
|
1990
|
-
async function discoverWorkspaceCapabilities(input) {
|
|
1991
|
-
const connections = await resolveConnections(input);
|
|
1992
|
-
const connectors = await resolveConnectors(input);
|
|
1993
|
-
const activeConnectionsByConnector = /* @__PURE__ */ new Map();
|
|
1994
|
-
for (const conn of connections) {
|
|
1995
|
-
if (conn.status !== "active") continue;
|
|
1996
|
-
if (!activeConnectionsByConnector.has(conn.connectorId)) {
|
|
1997
|
-
activeConnectionsByConnector.set(conn.connectorId, conn);
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
const capabilities = [];
|
|
2001
|
-
const triggers = [];
|
|
2002
|
-
const countsByConnector = {};
|
|
2003
|
-
const unreachableConnectors = [];
|
|
2004
|
-
for (const connector of connectors) {
|
|
2005
|
-
const connection = activeConnectionsByConnector.get(connector.id);
|
|
2006
|
-
const connected = Boolean(connection);
|
|
2007
|
-
if (!connected && !input.includeUnconnected) continue;
|
|
2008
|
-
const grantedScopes = new Set(connection?.grantedScopes ?? []);
|
|
2009
|
-
let actionsAdded = 0;
|
|
2010
|
-
for (const action of connector.actions) {
|
|
2011
|
-
const missing2 = action.requiredScopes.filter((scope) => !grantedScopes.has(scope));
|
|
2012
|
-
if (connected && missing2.length > 0 && !input.includeMissingScopes) continue;
|
|
2013
|
-
capabilities.push(toCapability(connector, action, connection));
|
|
2014
|
-
actionsAdded += 1;
|
|
2015
|
-
}
|
|
2016
|
-
for (const trigger2 of connector.triggers ?? []) {
|
|
2017
|
-
const missing2 = trigger2.requiredScopes.filter((scope) => !grantedScopes.has(scope));
|
|
2018
|
-
if (connected && missing2.length > 0 && !input.includeMissingScopes) continue;
|
|
2019
|
-
triggers.push(toTrigger2(connector, trigger2, connection));
|
|
2020
|
-
}
|
|
2021
|
-
countsByConnector[connector.id] = actionsAdded;
|
|
2022
|
-
if (connected && actionsAdded === 0 && connector.actions.length > 0) {
|
|
2023
|
-
unreachableConnectors.push({
|
|
2024
|
-
connectorId: connector.id,
|
|
2025
|
-
reason: "all_actions_missing_scope"
|
|
2026
|
-
});
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
return { capabilities, triggers, countsByConnector, unreachableConnectors };
|
|
2030
|
-
}
|
|
2031
|
-
async function resolveConnections(input) {
|
|
2032
|
-
if (input.connections) return input.connections;
|
|
2033
|
-
if (input.store) return await input.store.listByOwner(input.owner);
|
|
2034
|
-
throw new Error("discoverWorkspaceCapabilities: provide either connections or store");
|
|
2035
|
-
}
|
|
2036
|
-
async function resolveConnectors(input) {
|
|
2037
|
-
if (input.connectors) return input.connectors;
|
|
2038
|
-
if (input.providers) {
|
|
2039
|
-
const lists = await Promise.all(input.providers.map((p) => Promise.resolve(p.listConnectors())));
|
|
2040
|
-
return lists.flat();
|
|
2041
|
-
}
|
|
2042
|
-
throw new Error("discoverWorkspaceCapabilities: provide either connectors or providers");
|
|
2043
|
-
}
|
|
2044
|
-
function toCapability(connector, action, connection) {
|
|
2045
|
-
return {
|
|
2046
|
-
id: `${connector.id}.${action.id}`,
|
|
2047
|
-
title: action.title,
|
|
2048
|
-
description: action.description,
|
|
2049
|
-
category: connector.category,
|
|
2050
|
-
connectorId: connector.id,
|
|
2051
|
-
providerId: connector.providerId,
|
|
2052
|
-
actionId: action.id,
|
|
2053
|
-
scopes: action.requiredScopes,
|
|
2054
|
-
risk: action.risk,
|
|
2055
|
-
dataClass: action.dataClass,
|
|
2056
|
-
toolSchema: {
|
|
2057
|
-
name: `${connector.id}.${action.id}`,
|
|
2058
|
-
description: action.description,
|
|
2059
|
-
inputSchema: action.inputSchema,
|
|
2060
|
-
outputSchema: action.outputSchema
|
|
2061
|
-
},
|
|
2062
|
-
connected: Boolean(connection),
|
|
2063
|
-
connectionId: connection?.id,
|
|
2064
|
-
approvalRequired: action.approvalRequired
|
|
2065
|
-
};
|
|
2066
|
-
}
|
|
2067
|
-
function toTrigger2(connector, trigger2, connection) {
|
|
2068
|
-
return {
|
|
2069
|
-
id: `${connector.id}.${trigger2.id}`,
|
|
2070
|
-
title: trigger2.title,
|
|
2071
|
-
description: trigger2.description,
|
|
2072
|
-
category: connector.category,
|
|
2073
|
-
connectorId: connector.id,
|
|
2074
|
-
providerId: connector.providerId,
|
|
2075
|
-
triggerId: trigger2.id,
|
|
2076
|
-
scopes: trigger2.requiredScopes,
|
|
2077
|
-
dataClass: trigger2.dataClass,
|
|
2078
|
-
connected: Boolean(connection),
|
|
2079
|
-
connectionId: connection?.id
|
|
2080
|
-
};
|
|
2081
|
-
}
|
|
2082
|
-
function filterDiscoveryByWorkspaceScopes(discovery, workspaceScopes, opts = {}) {
|
|
2083
|
-
if (workspaceScopes.length === 0 && !opts.denyByDefault) return discovery;
|
|
2084
|
-
const granted = new Set(workspaceScopes);
|
|
2085
|
-
const hasWildcard = granted.has("tangle:*");
|
|
2086
|
-
function allowed(connectorId, scopes) {
|
|
2087
|
-
if (hasWildcard) return true;
|
|
2088
|
-
if (granted.has(`${connectorId}:*`)) return true;
|
|
2089
|
-
for (const scope of scopes) {
|
|
2090
|
-
if (!granted.has(scope)) return false;
|
|
2091
|
-
}
|
|
2092
|
-
return true;
|
|
2093
|
-
}
|
|
2094
|
-
const capabilities = discovery.capabilities.filter((cap) => allowed(cap.connectorId, cap.scopes));
|
|
2095
|
-
const triggers = discovery.triggers.filter((t) => allowed(t.connectorId, t.scopes));
|
|
2096
|
-
const countsByConnector = {};
|
|
2097
|
-
for (const cap of capabilities) {
|
|
2098
|
-
countsByConnector[cap.connectorId] = (countsByConnector[cap.connectorId] ?? 0) + 1;
|
|
2099
|
-
}
|
|
2100
|
-
return {
|
|
2101
|
-
capabilities,
|
|
2102
|
-
triggers,
|
|
2103
|
-
countsByConnector,
|
|
2104
|
-
unreachableConnectors: discovery.unreachableConnectors
|
|
2105
|
-
};
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
// src/events.ts
|
|
2109
|
-
var InMemoryIntegrationEventStore = class {
|
|
2110
|
-
events = /* @__PURE__ */ new Map();
|
|
2111
|
-
providerIds = /* @__PURE__ */ new Set();
|
|
2112
|
-
put(event) {
|
|
2113
|
-
this.events.set(event.id, event);
|
|
2114
|
-
if (event.providerEventId) this.providerIds.add(providerKey(event.sourceId, event.providerEventId));
|
|
2115
|
-
}
|
|
2116
|
-
hasProviderEvent(sourceId, providerEventId) {
|
|
2117
|
-
return this.providerIds.has(providerKey(sourceId, providerEventId));
|
|
2118
|
-
}
|
|
2119
|
-
list() {
|
|
2120
|
-
return [...this.events.values()];
|
|
2121
|
-
}
|
|
2122
|
-
};
|
|
2123
|
-
async function receiveIntegrationWebhook(input) {
|
|
2124
|
-
if (!input.adapter.handleInboundEvent) {
|
|
2125
|
-
return { status: 405, body: { ok: false, error: "Connector does not support inbound webhooks." }, received: [], duplicates: [] };
|
|
2126
|
-
}
|
|
2127
|
-
if (!input.adapter.verifySignature && !input.allowUnsignedWebhook) {
|
|
2128
|
-
return { status: 401, body: { ok: false, error: "Webhook signature verification is required." }, received: [], duplicates: [] };
|
|
2129
|
-
}
|
|
2130
|
-
const signature = input.adapter.verifySignature?.({
|
|
2131
|
-
rawBody: input.rawBody,
|
|
2132
|
-
headers: input.headers,
|
|
2133
|
-
source: input.source
|
|
2134
|
-
});
|
|
2135
|
-
if (signature && !signature.valid) {
|
|
2136
|
-
return { status: 401, body: { ok: false, error: signature.reason ?? "Invalid webhook signature." }, received: [], duplicates: [] };
|
|
2137
|
-
}
|
|
2138
|
-
const handled = await input.adapter.handleInboundEvent({
|
|
2139
|
-
source: input.source,
|
|
2140
|
-
rawBody: input.rawBody,
|
|
2141
|
-
headers: input.headers
|
|
2142
|
-
});
|
|
2143
|
-
const received = [];
|
|
2144
|
-
const duplicates = [];
|
|
2145
|
-
for (const inbound of handled.events) {
|
|
2146
|
-
const event = storedEvent(input.source, inbound, input.now ?? (() => /* @__PURE__ */ new Date()));
|
|
2147
|
-
if (event.providerEventId && await input.store.hasProviderEvent(event.sourceId, event.providerEventId)) {
|
|
2148
|
-
duplicates.push(event);
|
|
2149
|
-
continue;
|
|
2150
|
-
}
|
|
2151
|
-
await input.store.put(event);
|
|
2152
|
-
received.push(event);
|
|
2153
|
-
await dispatchStoredEvent(event, input.source, input.workflowRuntime);
|
|
2154
|
-
}
|
|
2155
|
-
return {
|
|
2156
|
-
status: handled.response?.status ?? 200,
|
|
2157
|
-
body: handled.response?.body ?? { received: true, count: received.length, duplicateCount: duplicates.length },
|
|
2158
|
-
headers: handled.response?.headers,
|
|
2159
|
-
received,
|
|
2160
|
-
duplicates
|
|
2161
|
-
};
|
|
2162
|
-
}
|
|
2163
|
-
function storedEventToTriggerEvent(event, source) {
|
|
2164
|
-
return {
|
|
2165
|
-
id: event.id,
|
|
2166
|
-
providerId: String(source.metadata.providerId ?? "first-party"),
|
|
2167
|
-
connectorId: event.connectorId,
|
|
2168
|
-
connectionId: source.id,
|
|
2169
|
-
trigger: event.eventType,
|
|
2170
|
-
occurredAt: event.receivedAt,
|
|
2171
|
-
payload: event.payload,
|
|
2172
|
-
metadata: {
|
|
2173
|
-
providerEventId: event.providerEventId,
|
|
2174
|
-
sourceId: event.sourceId,
|
|
2175
|
-
...event.metadata
|
|
2176
|
-
}
|
|
2177
|
-
};
|
|
2178
|
-
}
|
|
2179
|
-
async function dispatchStoredEvent(event, source, workflowRuntime) {
|
|
2180
|
-
if (!workflowRuntime) return;
|
|
2181
|
-
await workflowRuntime.dispatchEvent(storedEventToTriggerEvent(event, source), () => void 0);
|
|
2182
|
-
}
|
|
2183
|
-
function storedEvent(source, event, now) {
|
|
2184
|
-
return {
|
|
2185
|
-
id: `evt_${source.id}_${event.providerEventId ?? `${event.eventType}_${now().getTime()}`}`,
|
|
2186
|
-
sourceId: source.id,
|
|
2187
|
-
connectorId: source.kind,
|
|
2188
|
-
eventType: event.eventType,
|
|
2189
|
-
providerEventId: event.providerEventId,
|
|
2190
|
-
receivedAt: now().toISOString(),
|
|
2191
|
-
payload: event.payload
|
|
2192
|
-
};
|
|
2193
|
-
}
|
|
2194
|
-
function providerKey(sourceId, providerEventId) {
|
|
2195
|
-
return `${sourceId}:${providerEventId}`;
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
// src/guard.ts
|
|
2199
|
-
import { createHash as createHash2 } from "crypto";
|
|
2200
|
-
var InMemoryIntegrationIdempotencyStore = class {
|
|
2201
|
-
records = /* @__PURE__ */ new Map();
|
|
2202
|
-
get(key) {
|
|
2203
|
-
return this.records.get(key);
|
|
2204
|
-
}
|
|
2205
|
-
put(record) {
|
|
2206
|
-
this.records.set(record.key, record);
|
|
2207
|
-
}
|
|
2208
|
-
};
|
|
2209
|
-
var DefaultIntegrationActionGuard = class {
|
|
2210
|
-
idempotency;
|
|
2211
|
-
audit;
|
|
2212
|
-
rateLimiter;
|
|
2213
|
-
requireIdempotencyForMutations;
|
|
2214
|
-
now;
|
|
2215
|
-
constructor(options = {}) {
|
|
2216
|
-
this.idempotency = options.idempotency;
|
|
2217
|
-
this.audit = options.audit;
|
|
2218
|
-
this.rateLimiter = options.rateLimiter;
|
|
2219
|
-
this.requireIdempotencyForMutations = options.requireIdempotencyForMutations ?? false;
|
|
2220
|
-
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2221
|
-
}
|
|
2222
|
-
async invokeAction(ctx, proceed) {
|
|
2223
|
-
const idempotencyKey = ctx.request.idempotencyKey;
|
|
2224
|
-
const requestHash = hashRequest(ctx);
|
|
2225
|
-
if (this.requireIdempotencyForMutations && ctx.action?.risk !== "read" && !idempotencyKey) {
|
|
2226
|
-
return {
|
|
2227
|
-
ok: false,
|
|
2228
|
-
action: ctx.request.action,
|
|
2229
|
-
output: {
|
|
2230
|
-
idempotencyRequired: true,
|
|
2231
|
-
message: "State-changing integration actions require an idempotency key."
|
|
2232
|
-
}
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2235
|
-
if (idempotencyKey && this.idempotency) {
|
|
2236
|
-
const existing = await this.idempotency.get(idempotencyKey);
|
|
2237
|
-
if (existing) {
|
|
2238
|
-
if (existing.requestHash !== requestHash) {
|
|
2239
|
-
return {
|
|
2240
|
-
ok: false,
|
|
2241
|
-
action: ctx.request.action,
|
|
2242
|
-
output: { idempotencyConflict: true, message: "Idempotency key was reused with different integration input." }
|
|
2243
|
-
};
|
|
2244
|
-
}
|
|
2245
|
-
return {
|
|
2246
|
-
...existing.result,
|
|
2247
|
-
metadata: { ...existing.result.metadata ?? {}, idempotentReplay: true }
|
|
2248
|
-
};
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
if (ctx.request.dryRun && ctx.action?.risk !== "read") {
|
|
2252
|
-
const result = {
|
|
2253
|
-
ok: true,
|
|
2254
|
-
action: ctx.request.action,
|
|
2255
|
-
output: { dryRun: true },
|
|
2256
|
-
metadata: { dryRun: true }
|
|
2257
|
-
};
|
|
2258
|
-
await this.writeIdempotency(idempotencyKey, requestHash, result);
|
|
2259
|
-
return result;
|
|
2260
|
-
}
|
|
2261
|
-
const rateLimit = await this.rateLimiter?.check(ctx);
|
|
2262
|
-
if (rateLimit && !rateLimit.allowed) {
|
|
2263
|
-
return {
|
|
2264
|
-
ok: false,
|
|
2265
|
-
action: ctx.request.action,
|
|
2266
|
-
output: { rateLimited: true, retryAfterMs: rateLimit.retryAfterMs, message: rateLimit.reason ?? "Integration rate limit exceeded." }
|
|
2267
|
-
};
|
|
2268
|
-
}
|
|
2269
|
-
try {
|
|
2270
|
-
const result = await proceed();
|
|
2271
|
-
if (result.ok) {
|
|
2272
|
-
await this.writeIdempotency(idempotencyKey, requestHash, result);
|
|
2273
|
-
}
|
|
2274
|
-
await this.audit?.record(createIntegrationAuditEvent({
|
|
2275
|
-
type: result.ok ? "action.invoked" : "action.failed",
|
|
2276
|
-
actor: ctx.connection.owner,
|
|
2277
|
-
connectionId: ctx.connection.id,
|
|
2278
|
-
providerId: ctx.connection.providerId,
|
|
2279
|
-
connectorId: ctx.connection.connectorId,
|
|
2280
|
-
action: ctx.request.action,
|
|
2281
|
-
risk: ctx.action?.risk,
|
|
2282
|
-
dataClass: ctx.action?.dataClass,
|
|
2283
|
-
ok: result.ok,
|
|
2284
|
-
metadata: { idempotencyKey, externalId: result.externalId, warnings: result.warnings },
|
|
2285
|
-
now: this.now
|
|
2286
|
-
}));
|
|
2287
|
-
return result;
|
|
2288
|
-
} catch (error) {
|
|
2289
|
-
await this.audit?.record(createIntegrationAuditEvent({
|
|
2290
|
-
type: "action.failed",
|
|
2291
|
-
actor: ctx.connection.owner,
|
|
2292
|
-
connectionId: ctx.connection.id,
|
|
2293
|
-
providerId: ctx.connection.providerId,
|
|
2294
|
-
connectorId: ctx.connection.connectorId,
|
|
2295
|
-
action: ctx.request.action,
|
|
2296
|
-
risk: ctx.action?.risk,
|
|
2297
|
-
dataClass: ctx.action?.dataClass,
|
|
2298
|
-
ok: false,
|
|
2299
|
-
message: error instanceof Error ? error.message : "Integration action failed.",
|
|
2300
|
-
metadata: { idempotencyKey },
|
|
2301
|
-
now: this.now
|
|
2302
|
-
}));
|
|
2303
|
-
throw error;
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
async writeIdempotency(key, requestHash, result) {
|
|
2307
|
-
if (!key || !this.idempotency) return;
|
|
2308
|
-
await this.idempotency.put({
|
|
2309
|
-
key,
|
|
2310
|
-
requestHash,
|
|
2311
|
-
result,
|
|
2312
|
-
createdAt: this.now().toISOString()
|
|
2313
|
-
});
|
|
2314
|
-
}
|
|
2315
|
-
};
|
|
2316
|
-
function createDefaultIntegrationActionGuard(options = {}) {
|
|
2317
|
-
return new DefaultIntegrationActionGuard(options);
|
|
2318
|
-
}
|
|
2319
|
-
function hashRequest(ctx) {
|
|
2320
|
-
return createHash2("sha256").update(JSON.stringify({
|
|
2321
|
-
connectionId: ctx.connection.id,
|
|
2322
|
-
action: ctx.request.action,
|
|
2323
|
-
input: ctx.request.input ?? null,
|
|
2324
|
-
dryRun: ctx.request.dryRun ?? false
|
|
2325
|
-
})).digest("base64url");
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
// src/healthcheck.ts
|
|
2329
|
-
var InMemoryIntegrationHealthcheckStore = class {
|
|
2330
|
-
results = /* @__PURE__ */ new Map();
|
|
2331
|
-
put(result) {
|
|
2332
|
-
this.results.set(result.connectionId, result);
|
|
2333
|
-
}
|
|
2334
|
-
get(connectionId) {
|
|
2335
|
-
return this.results.get(connectionId);
|
|
2336
|
-
}
|
|
2337
|
-
list() {
|
|
2338
|
-
return [...this.results.values()];
|
|
2339
|
-
}
|
|
2340
|
-
};
|
|
2341
|
-
async function runIntegrationHealthcheck(input) {
|
|
2342
|
-
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
2343
|
-
const checkedAt = now().toISOString();
|
|
2344
|
-
const connector = input.connector ?? input.registry?.byId.get(input.connection.connectorId)?.connector;
|
|
2345
|
-
const checks = [];
|
|
2346
|
-
checks.push(connectionStatusCheck(input.connection, now));
|
|
2347
|
-
if (!connector) {
|
|
2348
|
-
checks.push({ id: "connector-known", status: "unknown", message: `Connector ${input.connection.connectorId} is not in the registry.` });
|
|
2349
|
-
} else {
|
|
2350
|
-
checks.push(connectorExecutableCheck(connector));
|
|
2351
|
-
checks.push(scopeShapeCheck(input.connection, connector));
|
|
2352
|
-
if (input.test && input.connection.status === "active") {
|
|
2353
|
-
checks.push(await liveHealthcheck(input.connection, connector, input.test));
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
const result = {
|
|
2357
|
-
connectionId: input.connection.id,
|
|
2358
|
-
providerId: input.connection.providerId,
|
|
2359
|
-
connectorId: input.connection.connectorId,
|
|
2360
|
-
status: rollupHealthStatus(checks),
|
|
2361
|
-
checkedAt,
|
|
2362
|
-
checks
|
|
2363
|
-
};
|
|
2364
|
-
await input.audit?.record(createIntegrationAuditEvent({
|
|
2365
|
-
type: "healthcheck.completed",
|
|
2366
|
-
actor: input.connection.owner,
|
|
2367
|
-
connectionId: input.connection.id,
|
|
2368
|
-
providerId: input.connection.providerId,
|
|
2369
|
-
connectorId: input.connection.connectorId,
|
|
2370
|
-
ok: result.status === "healthy",
|
|
2371
|
-
message: result.status,
|
|
2372
|
-
metadata: { checks: checks.map((check) => ({ id: check.id, status: check.status, message: check.message })) },
|
|
2373
|
-
occurredAt: checkedAt
|
|
2374
|
-
}));
|
|
2375
|
-
return result;
|
|
2376
|
-
}
|
|
2377
|
-
async function runIntegrationHealthchecks(input) {
|
|
2378
|
-
const results = [];
|
|
2379
|
-
for (const connection of input.connections) {
|
|
2380
|
-
const result = await runIntegrationHealthcheck({
|
|
2381
|
-
connection,
|
|
2382
|
-
registry: input.registry,
|
|
2383
|
-
test: input.test,
|
|
2384
|
-
audit: input.audit,
|
|
2385
|
-
now: input.now
|
|
2386
|
-
});
|
|
2387
|
-
await input.store?.put(result);
|
|
2388
|
-
results.push(result);
|
|
2389
|
-
}
|
|
2390
|
-
return results;
|
|
2391
|
-
}
|
|
2392
|
-
function healthcheckRequest(action = "healthcheck") {
|
|
2393
|
-
return {
|
|
2394
|
-
connectionId: "__healthcheck__",
|
|
2395
|
-
action,
|
|
2396
|
-
input: {},
|
|
2397
|
-
dryRun: true,
|
|
2398
|
-
metadata: { healthcheck: true }
|
|
2399
|
-
};
|
|
2400
|
-
}
|
|
2401
|
-
function connectionStatusCheck(connection, now) {
|
|
2402
|
-
if (connection.status !== "active") {
|
|
2403
|
-
return { id: "connection-active", status: "unhealthy", message: `Connection is ${connection.status}.` };
|
|
2404
|
-
}
|
|
2405
|
-
if (connection.expiresAt && Date.parse(connection.expiresAt) <= now().getTime()) {
|
|
2406
|
-
return { id: "connection-active", status: "unhealthy", message: "Connection credentials are expired." };
|
|
2407
|
-
}
|
|
2408
|
-
return { id: "connection-active", status: "healthy", message: "Connection is active." };
|
|
2409
|
-
}
|
|
2410
|
-
function connectorExecutableCheck(connector) {
|
|
2411
|
-
const executable = connector.actions.length > 0 || (connector.triggers?.length ?? 0) > 0;
|
|
2412
|
-
if (!executable) {
|
|
2413
|
-
return { id: "connector-executable", status: "degraded", message: `${connector.title} is catalog-only.` };
|
|
2414
|
-
}
|
|
2415
|
-
return { id: "connector-executable", status: "healthy", message: `${connector.title} has executable actions or triggers.` };
|
|
2416
|
-
}
|
|
2417
|
-
function scopeShapeCheck(connection, connector) {
|
|
2418
|
-
const declaredScopes = new Set(connector.scopes);
|
|
2419
|
-
const undeclared = connection.grantedScopes.filter((scope) => !declaredScopes.has(scope));
|
|
2420
|
-
if (connector.scopes.length === 0 && connection.grantedScopes.length > 0) {
|
|
2421
|
-
return { id: "scope-shape", status: "unknown", message: "Connector does not declare a scope catalog.", metadata: { grantedScopes: connection.grantedScopes } };
|
|
2422
|
-
}
|
|
2423
|
-
if (undeclared.length > 0) {
|
|
2424
|
-
return { id: "scope-shape", status: "degraded", message: "Connection has scopes not declared by the connector.", metadata: { undeclared } };
|
|
2425
|
-
}
|
|
2426
|
-
return { id: "scope-shape", status: "healthy", message: "Granted scopes match the connector shape." };
|
|
2427
|
-
}
|
|
2428
|
-
async function liveHealthcheck(connection, connector, test) {
|
|
2429
|
-
try {
|
|
2430
|
-
const result = await test(connection, connector);
|
|
2431
|
-
const ok = typeof result === "boolean" ? result : result.ok;
|
|
2432
|
-
return {
|
|
2433
|
-
id: "provider-live-test",
|
|
2434
|
-
status: ok ? "healthy" : "unhealthy",
|
|
2435
|
-
message: ok ? "Provider live test passed." : "Provider live test failed.",
|
|
2436
|
-
metadata: typeof result === "boolean" ? void 0 : { action: result.action, warnings: result.warnings }
|
|
2437
|
-
};
|
|
2438
|
-
} catch (error) {
|
|
2439
|
-
return {
|
|
2440
|
-
id: "provider-live-test",
|
|
2441
|
-
status: "unhealthy",
|
|
2442
|
-
message: error instanceof Error ? error.message : "Provider live test failed."
|
|
2443
|
-
};
|
|
2444
|
-
}
|
|
2445
|
-
}
|
|
2446
|
-
function rollupHealthStatus(checks) {
|
|
2447
|
-
if (checks.some((check) => check.status === "unhealthy")) return "unhealthy";
|
|
2448
|
-
if (checks.some((check) => check.status === "degraded")) return "degraded";
|
|
2449
|
-
if (checks.some((check) => check.status === "unknown")) return "unknown";
|
|
2450
|
-
return "healthy";
|
|
2451
|
-
}
|
|
2452
|
-
|
|
2453
|
-
// src/manifest.ts
|
|
2454
|
-
function validateIntegrationManifest(manifest) {
|
|
2455
|
-
const issues = [];
|
|
2456
|
-
if (!manifest.id?.trim()) issues.push({ path: "id", message: "Manifest id is required." });
|
|
2457
|
-
if (!Array.isArray(manifest.requirements)) issues.push({ path: "requirements", message: "Requirements must be an array." });
|
|
2458
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2459
|
-
for (const [index, requirement] of (manifest.requirements ?? []).entries()) {
|
|
2460
|
-
const path = `requirements[${index}]`;
|
|
2461
|
-
if (!requirement.id?.trim()) issues.push({ path: `${path}.id`, message: "Requirement id is required." });
|
|
2462
|
-
if (ids.has(requirement.id)) issues.push({ path: `${path}.id`, message: `Duplicate requirement id ${requirement.id}.` });
|
|
2463
|
-
ids.add(requirement.id);
|
|
2464
|
-
if (!requirement.connectorId?.trim()) issues.push({ path: `${path}.connectorId`, message: "Connector id is required." });
|
|
2465
|
-
if (!["read", "write", "trigger"].includes(requirement.mode)) issues.push({ path: `${path}.mode`, message: "Mode must be read, write, or trigger." });
|
|
2466
|
-
if (!requirement.reason?.trim()) issues.push({ path: `${path}.reason`, message: "Human-readable reason is required." });
|
|
2467
|
-
if (requirement.mode !== "trigger" && !requirement.requiredActions?.length) {
|
|
2468
|
-
issues.push({ path: `${path}.requiredActions`, message: "Non-trigger requirements should list required actions." });
|
|
2469
|
-
}
|
|
2470
|
-
if (requirement.mode === "trigger" && !requirement.requiredTriggers?.length) {
|
|
2471
|
-
issues.push({ path: `${path}.requiredTriggers`, message: "Trigger requirements should list required triggers." });
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
return { ok: issues.length === 0, issues };
|
|
2475
|
-
}
|
|
2476
|
-
function assertValidIntegrationManifest(manifest) {
|
|
2477
|
-
const result = validateIntegrationManifest(manifest);
|
|
2478
|
-
if (!result.ok) {
|
|
2479
|
-
throw new Error(`Invalid integration manifest: ${result.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
function inferIntegrationManifestFromTools(options) {
|
|
2483
|
-
const byConnector = /* @__PURE__ */ new Map();
|
|
2484
|
-
for (const item of options.tools) {
|
|
2485
|
-
const action = typeof item === "string" ? item : item.action;
|
|
2486
|
-
const connectorId = typeof item === "string" ? canonicalActionConnectorId(action) : item.connectorId ?? canonicalActionConnectorId(action);
|
|
2487
|
-
if (!connectorId) continue;
|
|
2488
|
-
const mode = typeof item === "string" ? inferMode(action) : item.mode ?? inferMode(action);
|
|
2489
|
-
const id = `${connectorId}-${mode}`;
|
|
2490
|
-
const existing = byConnector.get(id);
|
|
2491
|
-
const reason = typeof item === "string" ? defaultReason(connectorId, mode) : item.reason ?? defaultReason(connectorId, mode);
|
|
2492
|
-
if (existing) {
|
|
2493
|
-
byConnector.set(id, {
|
|
2494
|
-
...existing,
|
|
2495
|
-
requiredActions: unique2([...existing.requiredActions ?? [], action]),
|
|
2496
|
-
requiredScopes: unique2([...existing.requiredScopes ?? [], ...typeof item === "string" ? [] : item.scopes ?? []])
|
|
2497
|
-
});
|
|
2498
|
-
} else {
|
|
2499
|
-
byConnector.set(id, {
|
|
2500
|
-
id,
|
|
2501
|
-
connectorId,
|
|
2502
|
-
mode,
|
|
2503
|
-
reason,
|
|
2504
|
-
requiredActions: mode === "trigger" ? void 0 : [action],
|
|
2505
|
-
requiredScopes: typeof item === "string" ? void 0 : item.scopes
|
|
2506
|
-
});
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
const manifest = {
|
|
2510
|
-
id: options.manifestId,
|
|
2511
|
-
title: options.title,
|
|
2512
|
-
requirements: [...byConnector.values()],
|
|
2513
|
-
metadata: options.metadata
|
|
2514
|
-
};
|
|
2515
|
-
assertValidIntegrationManifest(manifest);
|
|
2516
|
-
return manifest;
|
|
2517
|
-
}
|
|
2518
|
-
function explainMissingRequirements(resolution) {
|
|
2519
|
-
return [...resolution.missing, ...resolution.optionalMissing].map((item) => ({
|
|
2520
|
-
requirementId: item.requirement.id,
|
|
2521
|
-
connectorId: item.requirement.connectorId,
|
|
2522
|
-
status: item.status,
|
|
2523
|
-
message: item.message,
|
|
2524
|
-
userAction: item.requirement.optional ? "ignore_optional" : item.status === "not_executable" ? "enable" : "connect"
|
|
2525
|
-
}));
|
|
2526
|
-
}
|
|
2527
|
-
function calendarExercisePlannerManifest(id = "exercise-calendar-planner") {
|
|
2528
|
-
return {
|
|
2529
|
-
id,
|
|
2530
|
-
title: "Exercise Calendar Planner",
|
|
2531
|
-
requirements: [{
|
|
2532
|
-
id: "calendar-read",
|
|
2533
|
-
connectorId: "google-calendar",
|
|
2534
|
-
mode: "read",
|
|
2535
|
-
reason: "Read busy and free calendar windows to recommend exercise sessions.",
|
|
2536
|
-
requiredActions: [CANONICAL_INTEGRATION_ACTIONS.googleCalendarEventsList],
|
|
2537
|
-
requiredScopes: ["https://www.googleapis.com/auth/calendar.readonly"]
|
|
2538
|
-
}]
|
|
2539
|
-
};
|
|
2540
|
-
}
|
|
2541
|
-
function inferMode(action) {
|
|
2542
|
-
if (/(create|send|post|update|delete|write|comment|request)$/i.test(action)) return "write";
|
|
2543
|
-
return "read";
|
|
2544
|
-
}
|
|
2545
|
-
function defaultReason(connectorId, mode) {
|
|
2546
|
-
if (connectorId === "google-calendar" && mode === "read") return "Read calendar availability for the generated app.";
|
|
2547
|
-
if (connectorId === "google-calendar" && mode === "write") return "Create or update calendar events after user approval.";
|
|
2548
|
-
return `${mode === "read" ? "Read from" : mode === "write" ? "Write to" : "Subscribe to"} ${connectorId} for this app.`;
|
|
2549
|
-
}
|
|
2550
|
-
function unique2(values) {
|
|
2551
|
-
return [...new Set(values)];
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
// src/passthrough.ts
|
|
2555
|
-
var PROVIDER_PASSTHROUGH_ACTION = CANONICAL_INTEGRATION_ACTIONS.providerHttpRequest;
|
|
2556
|
-
function validateProviderPassthroughRequest(input, policy) {
|
|
2557
|
-
if (!policy.enabled) {
|
|
2558
|
-
throw new IntegrationRuntimeError({
|
|
2559
|
-
code: "passthrough_disabled",
|
|
2560
|
-
message: "Provider-native passthrough is disabled for this connector."
|
|
2561
|
-
});
|
|
2562
|
-
}
|
|
2563
|
-
if (!input.path.startsWith("/")) {
|
|
2564
|
-
throw new IntegrationRuntimeError({ code: "input_invalid", message: "Provider passthrough path must start with /." });
|
|
2565
|
-
}
|
|
2566
|
-
if (policy.allowedMethods?.length && !policy.allowedMethods.includes(input.method)) {
|
|
2567
|
-
throw new IntegrationRuntimeError({ code: "action_denied", message: `Provider passthrough method ${input.method} is not allowed.` });
|
|
2568
|
-
}
|
|
2569
|
-
if (policy.allowedPathPrefixes?.length && !policy.allowedPathPrefixes.some((prefix) => input.path.startsWith(prefix))) {
|
|
2570
|
-
throw new IntegrationRuntimeError({ code: "action_denied", message: `Provider passthrough path ${input.path} is not allowed.` });
|
|
2571
|
-
}
|
|
2572
|
-
const maxBodyBytes = policy.maxBodyBytes ?? 64 * 1024;
|
|
2573
|
-
const bodyBytes = Buffer.byteLength(JSON.stringify(input.body ?? null), "utf8");
|
|
2574
|
-
if (bodyBytes > maxBodyBytes) {
|
|
2575
|
-
throw new IntegrationRuntimeError({ code: "input_invalid", message: `Provider passthrough body exceeds ${maxBodyBytes} bytes.` });
|
|
2576
|
-
}
|
|
2577
|
-
for (const key of Object.keys(input.headers ?? {})) {
|
|
2578
|
-
if (/authorization|cookie|token|secret|api[_-]?key/i.test(key)) {
|
|
2579
|
-
throw new IntegrationRuntimeError({ code: "input_invalid", message: `Provider passthrough header ${key} is not caller-settable.` });
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
// src/policy.ts
|
|
2585
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
2586
|
-
var StaticIntegrationPolicyEngine = class {
|
|
2587
|
-
rules;
|
|
2588
|
-
defaultReadEffect;
|
|
2589
|
-
defaultWriteEffect;
|
|
2590
|
-
defaultDestructiveEffect;
|
|
2591
|
-
now;
|
|
2592
|
-
constructor(options = {}) {
|
|
2593
|
-
this.rules = options.rules ?? [];
|
|
2594
|
-
this.defaultReadEffect = options.defaultReadEffect ?? "allow";
|
|
2595
|
-
this.defaultWriteEffect = options.defaultWriteEffect ?? "require_approval";
|
|
2596
|
-
this.defaultDestructiveEffect = options.defaultDestructiveEffect ?? "deny";
|
|
2597
|
-
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2598
|
-
}
|
|
2599
|
-
decide(ctx) {
|
|
2600
|
-
const action = ctx.action;
|
|
2601
|
-
if (!action) return { decision: "deny", reason: "Integration action is missing from connector catalog." };
|
|
2602
|
-
const matched = this.rules.find((rule) => ruleMatches(rule, ctx));
|
|
2603
|
-
const effect = matched?.effect ?? this.defaultEffect(action.risk);
|
|
2604
|
-
const reason = matched?.reason ?? defaultReason2(effect, action.risk);
|
|
2605
|
-
if (effect === "allow") return { decision: "allow", reason, metadata: matched ? { ruleId: matched.id } : void 0 };
|
|
2606
|
-
if (effect === "deny") return { decision: "deny", reason, metadata: matched ? { ruleId: matched.id } : void 0 };
|
|
2607
|
-
return {
|
|
2608
|
-
decision: "require_approval",
|
|
2609
|
-
reason,
|
|
2610
|
-
approval: buildApprovalRequest(ctx, reason, this.now()),
|
|
2611
|
-
metadata: matched ? { ruleId: matched.id } : void 0
|
|
2612
|
-
};
|
|
2613
|
-
}
|
|
2614
|
-
defaultEffect(risk) {
|
|
2615
|
-
if (risk === "read") return this.defaultReadEffect;
|
|
2616
|
-
if (risk === "write") return this.defaultWriteEffect;
|
|
2617
|
-
return this.defaultDestructiveEffect;
|
|
2618
|
-
}
|
|
2619
|
-
};
|
|
2620
|
-
function createDefaultIntegrationPolicyEngine(options = {}) {
|
|
2621
|
-
return new StaticIntegrationPolicyEngine(options);
|
|
2622
|
-
}
|
|
2623
|
-
function buildApprovalRequest(ctx, reason, requestedAt) {
|
|
2624
|
-
if (!ctx.action) {
|
|
2625
|
-
throw new Error("Cannot build approval request without an action descriptor.");
|
|
2626
|
-
}
|
|
2627
|
-
return {
|
|
2628
|
-
id: `approval_${randomUUID2()}`,
|
|
2629
|
-
connectionId: ctx.connection.id,
|
|
2630
|
-
providerId: ctx.connection.providerId,
|
|
2631
|
-
connectorId: ctx.connection.connectorId,
|
|
2632
|
-
action: ctx.request.action,
|
|
2633
|
-
actor: { type: ctx.subject.type, id: ctx.subject.id },
|
|
2634
|
-
risk: ctx.action.risk,
|
|
2635
|
-
dataClass: ctx.action.dataClass,
|
|
2636
|
-
reason,
|
|
2637
|
-
requestedAt: requestedAt.toISOString(),
|
|
2638
|
-
inputPreview: previewInput(ctx.request.input)
|
|
2639
|
-
};
|
|
2640
|
-
}
|
|
2641
|
-
function redactApprovalRequest(request) {
|
|
2642
|
-
return {
|
|
2643
|
-
...request,
|
|
2644
|
-
inputPreview: redactUnknown2(request.inputPreview)
|
|
2645
|
-
};
|
|
2646
|
-
}
|
|
2647
|
-
function ruleMatches(rule, ctx) {
|
|
2648
|
-
if (!ctx.action) return false;
|
|
2649
|
-
if (rule.providerId && rule.providerId !== ctx.connection.providerId) return false;
|
|
2650
|
-
if (rule.connectorId && rule.connectorId !== ctx.connection.connectorId) return false;
|
|
2651
|
-
if (rule.action && rule.action !== ctx.request.action) return false;
|
|
2652
|
-
if (rule.risk && rule.risk !== ctx.action.risk) return false;
|
|
2653
|
-
if (rule.maxRisk && riskRank(ctx.action.risk) > riskRank(rule.maxRisk)) return false;
|
|
2654
|
-
if (rule.dataClass && rule.dataClass !== ctx.action.dataClass) return false;
|
|
2655
|
-
return true;
|
|
2656
|
-
}
|
|
2657
|
-
function riskRank(risk) {
|
|
2658
|
-
if (risk === "read") return 0;
|
|
2659
|
-
if (risk === "write") return 1;
|
|
2660
|
-
return 2;
|
|
2661
|
-
}
|
|
2662
|
-
function defaultReason2(effect, risk) {
|
|
2663
|
-
if (effect === "allow") return `${risk} integration action allowed by default policy.`;
|
|
2664
|
-
if (effect === "deny") return `${risk} integration action denied by default policy.`;
|
|
2665
|
-
return `${risk} integration action requires approval by default policy.`;
|
|
2666
|
-
}
|
|
2667
|
-
function previewInput(input) {
|
|
2668
|
-
return redactUnknown2(input);
|
|
2669
|
-
}
|
|
2670
|
-
function redactUnknown2(value) {
|
|
2671
|
-
if (Array.isArray(value)) return value.map(redactUnknown2);
|
|
2672
|
-
if (!value || typeof value !== "object") return value;
|
|
2673
|
-
const out = {};
|
|
2674
|
-
for (const [key, child] of Object.entries(value)) {
|
|
2675
|
-
if (/token|secret|password|authorization|api[_-]?key|credential/i.test(key)) {
|
|
2676
|
-
out[key] = "[REDACTED]";
|
|
2677
|
-
} else {
|
|
2678
|
-
out[key] = redactUnknown2(child);
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
return out;
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
// src/presets.ts
|
|
2685
|
-
function createPlatformIntegrationPolicyPreset(options = {}) {
|
|
2686
|
-
return new StaticIntegrationPolicyEngine({
|
|
2687
|
-
...options,
|
|
2688
|
-
defaultReadEffect: "allow",
|
|
2689
|
-
defaultWriteEffect: options.allowWritesWithoutApproval ? "allow" : "require_approval",
|
|
2690
|
-
defaultDestructiveEffect: options.allowDestructiveActions ? "require_approval" : "deny",
|
|
2691
|
-
rules: [
|
|
2692
|
-
...options.allowProviderPassthrough ? [] : [{
|
|
2693
|
-
id: "deny-provider-native-passthrough",
|
|
2694
|
-
action: "provider.http.request",
|
|
2695
|
-
effect: "deny",
|
|
2696
|
-
reason: "Provider-native passthrough is disabled by default. Promote the connector action or enable passthrough explicitly."
|
|
2697
|
-
}],
|
|
2698
|
-
...options.rules ?? []
|
|
2699
|
-
]
|
|
2700
|
-
});
|
|
2701
|
-
}
|
|
2702
|
-
|
|
2703
|
-
// src/sandbox.ts
|
|
2704
|
-
function buildIntegrationInvocationEnvelope(input) {
|
|
2705
|
-
const parsed = parseIntegrationToolName(input.toolName);
|
|
2706
|
-
const envelope = {
|
|
2707
|
-
kind: "integration.invocation",
|
|
2708
|
-
capabilityToken: input.capabilityToken,
|
|
2709
|
-
toolName: input.toolName,
|
|
2710
|
-
action: parsed.actionId,
|
|
2711
|
-
input: input.args,
|
|
2712
|
-
idempotencyKey: input.idempotencyKey,
|
|
2713
|
-
dryRun: input.dryRun,
|
|
2714
|
-
metadata: input.metadata
|
|
2715
|
-
};
|
|
2716
|
-
validateIntegrationInvocationEnvelope(envelope);
|
|
2717
|
-
return envelope;
|
|
2718
|
-
}
|
|
2719
|
-
function invocationRequestFromEnvelope(envelope) {
|
|
2720
|
-
validateIntegrationInvocationEnvelope(envelope);
|
|
2721
|
-
return {
|
|
2722
|
-
action: envelope.action,
|
|
2723
|
-
input: envelope.input,
|
|
2724
|
-
idempotencyKey: envelope.idempotencyKey,
|
|
2725
|
-
dryRun: envelope.dryRun,
|
|
2726
|
-
metadata: envelope.metadata
|
|
2727
|
-
};
|
|
2728
|
-
}
|
|
2729
|
-
function validateIntegrationInvocationEnvelope(envelope, options = {}) {
|
|
2730
|
-
if (!envelope || typeof envelope !== "object") throw new Error("Integration invocation envelope is required.");
|
|
2731
|
-
if (envelope.kind !== "integration.invocation") throw new Error("Invalid integration invocation envelope kind.");
|
|
2732
|
-
if (!isNonEmptyString(envelope.capabilityToken)) throw new Error("Integration invocation envelope is missing capabilityToken.");
|
|
2733
|
-
if (!isNonEmptyString(envelope.toolName)) throw new Error("Integration invocation envelope is missing toolName.");
|
|
2734
|
-
if (!isNonEmptyString(envelope.action)) throw new Error("Integration invocation envelope is missing action.");
|
|
2735
|
-
if (!isNonEmptyString(envelope.idempotencyKey)) throw new Error("Integration invocation envelope is missing idempotencyKey.");
|
|
2736
|
-
if (envelope.metadata !== void 0 && !isPlainRecord(envelope.metadata)) {
|
|
2737
|
-
throw new Error("Integration invocation envelope metadata must be an object.");
|
|
2738
|
-
}
|
|
2739
|
-
const parsed = parseIntegrationToolName(envelope.toolName);
|
|
2740
|
-
if (parsed.actionId !== envelope.action) {
|
|
2741
|
-
throw new Error(`Integration invocation action ${envelope.action} does not match tool ${parsed.actionId}.`);
|
|
2742
|
-
}
|
|
2743
|
-
const inputBytes = Buffer.byteLength(JSON.stringify(envelope.input ?? null), "utf8");
|
|
2744
|
-
const maxInputBytes = options.maxInputBytes ?? 256 * 1024;
|
|
2745
|
-
if (inputBytes > maxInputBytes) {
|
|
2746
|
-
throw new Error(`Integration invocation input exceeds ${maxInputBytes} bytes.`);
|
|
2747
|
-
}
|
|
2748
|
-
if (options.requireKnownTool || options.connectors) {
|
|
2749
|
-
if (!options.connectors) throw new Error("connectors are required when requireKnownTool is true.");
|
|
2750
|
-
const connector = options.connectors.find(
|
|
2751
|
-
(candidate) => candidate.providerId === parsed.providerId && candidate.id === parsed.connectorId
|
|
2752
|
-
);
|
|
2753
|
-
const action = connector?.actions.find((candidate) => candidate.id === parsed.actionId);
|
|
2754
|
-
if (!connector || !action) throw new Error(`Unknown integration tool ${envelope.toolName}.`);
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
function redactInvocationEnvelope(envelope) {
|
|
2758
|
-
return {
|
|
2759
|
-
...envelope,
|
|
2760
|
-
capabilityToken: "[REDACTED]",
|
|
2761
|
-
input: redactUnknown3(envelope.input)
|
|
2762
|
-
};
|
|
2763
|
-
}
|
|
2764
|
-
function redactCapability(capability) {
|
|
2765
|
-
return {
|
|
2766
|
-
...capability,
|
|
2767
|
-
metadata: redactUnknown3(capability.metadata)
|
|
2768
|
-
};
|
|
2769
|
-
}
|
|
2770
|
-
function normalizeIntegrationResult(result) {
|
|
2771
|
-
const output = result.output;
|
|
2772
|
-
if (!result.ok && output?.approvalRequired === true && output.approval) {
|
|
2773
|
-
return {
|
|
2774
|
-
status: "approval_required",
|
|
2775
|
-
action: result.action,
|
|
2776
|
-
approval: output.approval,
|
|
2777
|
-
metadata: result.metadata
|
|
2778
|
-
};
|
|
2779
|
-
}
|
|
2780
|
-
if (!result.ok) {
|
|
2781
|
-
return {
|
|
2782
|
-
status: "failed",
|
|
2783
|
-
action: result.action,
|
|
2784
|
-
error: String(result.output ?? result.warnings?.[0] ?? "integration action failed"),
|
|
2785
|
-
metadata: result.metadata
|
|
2786
|
-
};
|
|
2787
|
-
}
|
|
2788
|
-
return {
|
|
2789
|
-
status: "ok",
|
|
2790
|
-
action: result.action,
|
|
2791
|
-
output: result.output,
|
|
2792
|
-
metadata: result.metadata
|
|
2793
|
-
};
|
|
2794
|
-
}
|
|
2795
|
-
async function dispatchIntegrationInvocation(envelope, options) {
|
|
2796
|
-
try {
|
|
2797
|
-
validateIntegrationInvocationEnvelope(envelope, options);
|
|
2798
|
-
const result = await options.hub.invokeWithCapability(
|
|
2799
|
-
envelope.capabilityToken,
|
|
2800
|
-
invocationRequestFromEnvelope(envelope)
|
|
2801
|
-
);
|
|
2802
|
-
return normalizeIntegrationResult(result);
|
|
2803
|
-
} catch (error) {
|
|
2804
|
-
return {
|
|
2805
|
-
status: "failed",
|
|
2806
|
-
action: typeof envelope?.action === "string" ? envelope.action : "unknown",
|
|
2807
|
-
error: error instanceof Error ? error.message : "Integration invocation failed."
|
|
2808
|
-
};
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
var IntegrationSandboxHost = class {
|
|
2812
|
-
options;
|
|
2813
|
-
constructor(options) {
|
|
2814
|
-
this.options = options;
|
|
2815
|
-
}
|
|
2816
|
-
dispatch(envelope) {
|
|
2817
|
-
return dispatchIntegrationInvocation(envelope, this.options);
|
|
2818
|
-
}
|
|
2819
|
-
};
|
|
2820
|
-
function redactUnknown3(value) {
|
|
2821
|
-
if (Array.isArray(value)) return value.map(redactUnknown3);
|
|
2822
|
-
if (!value || typeof value !== "object") return value;
|
|
2823
|
-
const out = {};
|
|
2824
|
-
for (const [key, child] of Object.entries(value)) {
|
|
2825
|
-
if (/token|secret|password|authorization|api[_-]?key|credential/i.test(key)) {
|
|
2826
|
-
out[key] = "[REDACTED]";
|
|
2827
|
-
} else {
|
|
2828
|
-
out[key] = redactUnknown3(child);
|
|
2829
|
-
}
|
|
2830
|
-
}
|
|
2831
|
-
return out;
|
|
2832
|
-
}
|
|
2833
|
-
function isNonEmptyString(value) {
|
|
2834
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
2835
|
-
}
|
|
2836
|
-
function isPlainRecord(value) {
|
|
2837
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
// src/importers.ts
|
|
2841
|
-
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete"]);
|
|
2842
|
-
function importOpenApiConnector(document, options) {
|
|
2843
|
-
const actions = [];
|
|
2844
|
-
for (const [path, methods] of Object.entries(document.paths ?? {})) {
|
|
2845
|
-
for (const [method, rawOperation] of Object.entries(methods)) {
|
|
2846
|
-
const normalizedMethod = method.toLowerCase();
|
|
2847
|
-
if (!HTTP_METHODS.has(normalizedMethod) || !isObject(rawOperation)) continue;
|
|
2848
|
-
const operation = rawOperation;
|
|
2849
|
-
const operationId = operation.operationId ?? `${normalizedMethod}_${path.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
|
|
2850
|
-
actions.push({
|
|
2851
|
-
id: operationId,
|
|
2852
|
-
title: operation.summary ?? titleFromId(operationId),
|
|
2853
|
-
risk: riskFromHttpMethod(normalizedMethod, operation, options.defaultRisk),
|
|
2854
|
-
requiredScopes: scopesFromOpenApiOperation(operation, options.scopes ?? []),
|
|
2855
|
-
dataClass: options.dataClass ?? "private",
|
|
2856
|
-
description: operation.description ?? operation.summary ?? `${normalizedMethod.toUpperCase()} ${path}`,
|
|
2857
|
-
approvalRequired: riskFromHttpMethod(normalizedMethod, operation, options.defaultRisk) !== "read",
|
|
2858
|
-
inputSchema: openApiInputSchema(path, normalizedMethod, operation),
|
|
2859
|
-
outputSchema: operation.responses
|
|
2860
|
-
});
|
|
2861
|
-
}
|
|
2862
|
-
}
|
|
2863
|
-
return connectorFromActions(options, actions);
|
|
2864
|
-
}
|
|
2865
|
-
function importGraphqlConnector(operations, options) {
|
|
2866
|
-
return connectorFromActions(options, operations.map((operation) => ({
|
|
2867
|
-
id: operation.name,
|
|
2868
|
-
title: titleFromId(operation.name),
|
|
2869
|
-
risk: operation.kind === "query" ? "read" : options.defaultRisk ?? "write",
|
|
2870
|
-
requiredScopes: operation.requiredScopes ?? options.scopes ?? [],
|
|
2871
|
-
dataClass: options.dataClass ?? "private",
|
|
2872
|
-
description: operation.description,
|
|
2873
|
-
approvalRequired: operation.kind === "mutation",
|
|
2874
|
-
inputSchema: operation.inputSchema,
|
|
2875
|
-
outputSchema: operation.outputSchema
|
|
2876
|
-
})));
|
|
2877
|
-
}
|
|
2878
|
-
function importMcpConnector(catalog, options) {
|
|
2879
|
-
return connectorFromActions(options, catalog.tools.map((tool) => {
|
|
2880
|
-
const risk = riskFromMcpTool(tool, options.defaultRisk);
|
|
2881
|
-
return {
|
|
2882
|
-
id: tool.name,
|
|
2883
|
-
title: tool.annotations?.title ?? titleFromId(tool.name),
|
|
2884
|
-
risk,
|
|
2885
|
-
requiredScopes: options.scopes ?? [],
|
|
2886
|
-
dataClass: options.dataClass ?? "private",
|
|
2887
|
-
description: tool.description,
|
|
2888
|
-
approvalRequired: risk !== "read",
|
|
2889
|
-
inputSchema: tool.inputSchema
|
|
2890
|
-
};
|
|
2891
|
-
}));
|
|
2892
|
-
}
|
|
2893
|
-
function connectorFromActions(options, actions) {
|
|
2894
|
-
const scopes = unique3([
|
|
2895
|
-
...options.scopes ?? [],
|
|
2896
|
-
...actions.flatMap((action) => action.requiredScopes)
|
|
2897
|
-
]);
|
|
2898
|
-
return {
|
|
2899
|
-
id: options.connectorId,
|
|
2900
|
-
providerId: options.providerId,
|
|
2901
|
-
title: options.connectorTitle,
|
|
2902
|
-
category: options.category ?? "other",
|
|
2903
|
-
auth: options.auth ?? "custom",
|
|
2904
|
-
scopes,
|
|
2905
|
-
actions,
|
|
2906
|
-
metadata: { source: "catalog-importer" }
|
|
2907
|
-
};
|
|
2908
|
-
}
|
|
2909
|
-
function riskFromHttpMethod(method, operation, fallback) {
|
|
2910
|
-
if (method === "get") return "read";
|
|
2911
|
-
if (method === "delete") return "destructive";
|
|
2912
|
-
const text = `${operation.operationId ?? ""} ${operation.summary ?? ""} ${operation.description ?? ""}`.toLowerCase();
|
|
2913
|
-
if (/\b(delete|remove|destroy|cancel|void|revoke|drop)\b/.test(text)) return "destructive";
|
|
2914
|
-
return fallback && fallback !== "read" ? fallback : "write";
|
|
2915
|
-
}
|
|
2916
|
-
function riskFromMcpTool(tool, fallback) {
|
|
2917
|
-
if (tool.annotations?.destructiveHint) return "destructive";
|
|
2918
|
-
if (tool.annotations?.readOnlyHint) return "read";
|
|
2919
|
-
const text = `${tool.name} ${tool.description ?? ""}`.toLowerCase();
|
|
2920
|
-
if (/\b(delete|remove|destroy|cancel|void|revoke|drop)\b/.test(text)) return "destructive";
|
|
2921
|
-
if (/\b(get|list|read|search|find|fetch|query)\b/.test(text)) return "read";
|
|
2922
|
-
return fallback ?? "write";
|
|
2923
|
-
}
|
|
2924
|
-
function scopesFromOpenApiOperation(operation, fallback) {
|
|
2925
|
-
const scopes = (operation.security ?? []).flatMap((entry) => Object.values(entry).flat());
|
|
2926
|
-
return unique3(scopes.length > 0 ? scopes : fallback);
|
|
2927
|
-
}
|
|
2928
|
-
function openApiInputSchema(path, method, operation) {
|
|
2929
|
-
return {
|
|
2930
|
-
type: "object",
|
|
2931
|
-
additionalProperties: true,
|
|
2932
|
-
properties: {
|
|
2933
|
-
path: { const: path },
|
|
2934
|
-
method: { const: method.toUpperCase() },
|
|
2935
|
-
parameters: { type: "object", additionalProperties: true },
|
|
2936
|
-
body: operation.requestBody ?? { type: "object", additionalProperties: true }
|
|
2937
|
-
}
|
|
2938
|
-
};
|
|
2939
|
-
}
|
|
2940
|
-
function titleFromId(id) {
|
|
2941
|
-
return id.replace(/([a-z])([A-Z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
2942
|
-
}
|
|
2943
|
-
function isObject(value) {
|
|
2944
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2945
|
-
}
|
|
2946
|
-
function unique3(values) {
|
|
2947
|
-
return [...new Set(values)];
|
|
2948
|
-
}
|
|
2949
|
-
|
|
2950
|
-
// src/gateway-catalog.ts
|
|
2951
|
-
function createGatewayCatalogProvider(options) {
|
|
2952
|
-
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2953
|
-
let cachedAt = 0;
|
|
2954
|
-
let cached;
|
|
2955
|
-
async function listConnectors() {
|
|
2956
|
-
const ttl = options.cacheTtlMs ?? 6e4;
|
|
2957
|
-
const current = now().getTime();
|
|
2958
|
-
if (cached && current - cachedAt < ttl) return cached;
|
|
2959
|
-
const entries = await options.fetchCatalog();
|
|
2960
|
-
cached = normalizeGatewayCatalog(entries, {
|
|
2961
|
-
providerId: options.id,
|
|
2962
|
-
providerKind: options.kind
|
|
2963
|
-
});
|
|
2964
|
-
cachedAt = current;
|
|
2965
|
-
return cached;
|
|
2966
|
-
}
|
|
2967
|
-
return {
|
|
2968
|
-
id: options.id,
|
|
2969
|
-
kind: options.kind,
|
|
2970
|
-
listConnectors,
|
|
2971
|
-
startAuth: options.startAuth,
|
|
2972
|
-
completeAuth: options.completeAuth,
|
|
2973
|
-
async invokeAction(connection, request) {
|
|
2974
|
-
if (!options.invokeAction) {
|
|
2975
|
-
throw new IntegrationError(`Gateway provider ${options.id} does not implement action invocation.`, "action_not_found");
|
|
2976
|
-
}
|
|
2977
|
-
await assertKnownGatewayAction(await listConnectors(), connection.connectorId, request.action);
|
|
2978
|
-
return options.invokeAction(connection, request);
|
|
2979
|
-
}
|
|
2980
|
-
};
|
|
2981
|
-
}
|
|
2982
|
-
function normalizeGatewayCatalog(entries, options) {
|
|
2983
|
-
const out = [];
|
|
2984
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2985
|
-
for (const entry of entries) {
|
|
2986
|
-
const id = slug(entry.id ?? entry.key ?? entry.name ?? entry.title ?? "");
|
|
2987
|
-
if (!id || seen.has(id)) continue;
|
|
2988
|
-
seen.add(id);
|
|
2989
|
-
const title = entry.title ?? entry.name ?? titleFromId2(id);
|
|
2990
|
-
const actions = normalizeActions(entry.actions ?? [], entry.scopes ?? []);
|
|
2991
|
-
out.push({
|
|
2992
|
-
id,
|
|
2993
|
-
providerId: options.providerId,
|
|
2994
|
-
title,
|
|
2995
|
-
category: normalizeCategory(entry.category),
|
|
2996
|
-
auth: normalizeAuth(entry.auth),
|
|
2997
|
-
scopes: unique4([
|
|
2998
|
-
...entry.scopes ?? [],
|
|
2999
|
-
...actions.flatMap((action) => action.requiredScopes)
|
|
3000
|
-
]),
|
|
3001
|
-
actions: actions.length > 0 ? actions : defaultActionsFor(entry.category, entry.scopes ?? []),
|
|
3002
|
-
triggers: normalizeTriggers(entry.triggers ?? [], entry.scopes ?? []),
|
|
3003
|
-
metadata: {
|
|
3004
|
-
...entry.metadata ?? {},
|
|
3005
|
-
source: "gateway-catalog",
|
|
3006
|
-
providerKind: options.providerKind,
|
|
3007
|
-
executable: true
|
|
3008
|
-
}
|
|
3009
|
-
});
|
|
3010
|
-
}
|
|
3011
|
-
return out;
|
|
3012
|
-
}
|
|
3013
|
-
async function assertKnownGatewayAction(connectors, connectorId, actionId) {
|
|
3014
|
-
const connector = connectors.find((candidate) => candidate.id === connectorId);
|
|
3015
|
-
if (!connector) throw new IntegrationError(`Connector ${connectorId} not found.`, "connector_not_found");
|
|
3016
|
-
if (!connector.actions.some((action) => action.id === actionId)) {
|
|
3017
|
-
throw new IntegrationError(`Action ${actionId} is not defined by connector ${connectorId}.`, "action_not_found");
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
3020
|
-
function normalizeActions(actions, fallbackScopes) {
|
|
3021
|
-
return actions.map((action) => {
|
|
3022
|
-
const id = slug(action.id ?? action.key ?? action.name ?? action.title ?? "");
|
|
3023
|
-
return {
|
|
3024
|
-
id,
|
|
3025
|
-
title: action.title ?? action.name ?? titleFromId2(id),
|
|
3026
|
-
risk: normalizeRisk(action.risk),
|
|
3027
|
-
requiredScopes: unique4([
|
|
3028
|
-
...action.requiredScopes ?? [],
|
|
3029
|
-
...action.scopes ?? [],
|
|
3030
|
-
...action.requiredScopes?.length || action.scopes?.length ? [] : fallbackScopes
|
|
3031
|
-
]),
|
|
3032
|
-
dataClass: normalizeDataClass(action.dataClass),
|
|
3033
|
-
description: action.description,
|
|
3034
|
-
approvalRequired: action.approvalRequired ?? normalizeRisk(action.risk) !== "read",
|
|
3035
|
-
inputSchema: action.inputSchema,
|
|
3036
|
-
outputSchema: action.outputSchema
|
|
3037
|
-
};
|
|
3038
|
-
}).filter((action) => action.id);
|
|
3039
|
-
}
|
|
3040
|
-
function normalizeTriggers(triggers, fallbackScopes) {
|
|
3041
|
-
const normalized = triggers.map((trigger2) => {
|
|
3042
|
-
const id = slug(trigger2.id ?? trigger2.key ?? trigger2.name ?? trigger2.title ?? "");
|
|
3043
|
-
return {
|
|
3044
|
-
id,
|
|
3045
|
-
title: trigger2.title ?? trigger2.name ?? titleFromId2(id),
|
|
3046
|
-
requiredScopes: unique4([
|
|
3047
|
-
...trigger2.requiredScopes ?? [],
|
|
3048
|
-
...trigger2.scopes ?? [],
|
|
3049
|
-
...trigger2.requiredScopes?.length || trigger2.scopes?.length ? [] : fallbackScopes
|
|
3050
|
-
]),
|
|
3051
|
-
dataClass: normalizeDataClass(trigger2.dataClass),
|
|
3052
|
-
description: trigger2.description,
|
|
3053
|
-
payloadSchema: trigger2.payloadSchema
|
|
3054
|
-
};
|
|
3055
|
-
}).filter((trigger2) => trigger2.id);
|
|
3056
|
-
return normalized.length > 0 ? normalized : void 0;
|
|
3057
|
-
}
|
|
3058
|
-
function defaultActionsFor(category, scopes) {
|
|
3059
|
-
const readScope = scopes.find((scope) => scope.endsWith(".read")) ?? scopes[0];
|
|
3060
|
-
const writeScope = scopes.find((scope) => scope.endsWith(".write")) ?? scopes[1] ?? readScope;
|
|
3061
|
-
const requiredRead = readScope ? [readScope] : [];
|
|
3062
|
-
const requiredWrite = writeScope ? [writeScope] : [];
|
|
3063
|
-
const dataClass = normalizeDataClass(category === "finance" || category === "commerce" || category === "hr" ? "sensitive" : "private");
|
|
3064
|
-
return [
|
|
3065
|
-
{
|
|
3066
|
-
id: "records.search",
|
|
3067
|
-
title: "Search records",
|
|
3068
|
-
risk: "read",
|
|
3069
|
-
requiredScopes: requiredRead,
|
|
3070
|
-
dataClass,
|
|
3071
|
-
description: "Search provider records."
|
|
3072
|
-
},
|
|
3073
|
-
{
|
|
3074
|
-
id: "records.read",
|
|
3075
|
-
title: "Read record",
|
|
3076
|
-
risk: "read",
|
|
3077
|
-
requiredScopes: requiredRead,
|
|
3078
|
-
dataClass,
|
|
3079
|
-
description: "Read a provider record."
|
|
3080
|
-
},
|
|
3081
|
-
{
|
|
3082
|
-
id: "records.upsert",
|
|
3083
|
-
title: "Upsert record",
|
|
3084
|
-
risk: "write",
|
|
3085
|
-
requiredScopes: requiredWrite,
|
|
3086
|
-
dataClass,
|
|
3087
|
-
approvalRequired: true,
|
|
3088
|
-
description: "Create or update a provider record."
|
|
3089
|
-
}
|
|
3090
|
-
];
|
|
3091
|
-
}
|
|
3092
|
-
function normalizeCategory(category) {
|
|
3093
|
-
const value = slug(category ?? "");
|
|
3094
|
-
if (value === "mail") return "email";
|
|
3095
|
-
if (value === "messaging" || value === "communication" || value === "communications") return "chat";
|
|
3096
|
-
if (value === "file" || value === "files") return "storage";
|
|
3097
|
-
if (value === "project-management" || value === "automation") return "workflow";
|
|
3098
|
-
if (value === "developer" || value === "devops") return "workflow";
|
|
3099
|
-
if (value === "support") return "crm";
|
|
3100
|
-
if ([
|
|
3101
|
-
"email",
|
|
3102
|
-
"calendar",
|
|
3103
|
-
"chat",
|
|
3104
|
-
"crm",
|
|
3105
|
-
"storage",
|
|
3106
|
-
"docs",
|
|
3107
|
-
"database",
|
|
3108
|
-
"webhook",
|
|
3109
|
-
"workflow",
|
|
3110
|
-
"internal",
|
|
3111
|
-
"other"
|
|
3112
|
-
].includes(value)) return value;
|
|
3113
|
-
return "other";
|
|
3114
|
-
}
|
|
3115
|
-
function normalizeAuth(auth) {
|
|
3116
|
-
if (auth === "oauth2") return "oauth2";
|
|
3117
|
-
if (auth === "api_key" || auth === "api-key" || auth === "apikey") return "api_key";
|
|
3118
|
-
if (auth === "none") return "none";
|
|
3119
|
-
return "custom";
|
|
3120
|
-
}
|
|
3121
|
-
function normalizeRisk(risk) {
|
|
3122
|
-
if (risk === "read" || risk === "write" || risk === "destructive") return risk;
|
|
3123
|
-
return "write";
|
|
3124
|
-
}
|
|
3125
|
-
function normalizeDataClass(dataClass) {
|
|
3126
|
-
if (dataClass === "public" || dataClass === "internal" || dataClass === "private" || dataClass === "sensitive" || dataClass === "secret") return dataClass;
|
|
3127
|
-
return "private";
|
|
3128
|
-
}
|
|
3129
|
-
function slug(value) {
|
|
3130
|
-
return value.trim().toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3131
|
-
}
|
|
3132
|
-
function titleFromId2(id) {
|
|
3133
|
-
return id.split("-").filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
3134
|
-
}
|
|
3135
|
-
function unique4(values) {
|
|
3136
|
-
return [...new Set(values)];
|
|
3137
|
-
}
|
|
3138
|
-
|
|
3139
|
-
// src/activepieces-provider.ts
|
|
3140
|
-
function createActivepiecesExecutorProvider(options) {
|
|
3141
|
-
const providerId = options.id ?? "activepieces";
|
|
3142
|
-
const connectors = options.connectors ?? buildActivepiecesConnectors({
|
|
3143
|
-
providerId,
|
|
3144
|
-
includeCatalogActions: true,
|
|
3145
|
-
executable: true
|
|
3146
|
-
});
|
|
3147
|
-
const byEntry = new Map(listActivepiecesCatalogEntries().map((entry) => [entry.id, entry]));
|
|
3148
|
-
return createCatalogExecutorProvider({
|
|
3149
|
-
id: providerId,
|
|
3150
|
-
kind: "activepieces",
|
|
3151
|
-
connectors,
|
|
3152
|
-
startAuth: options.startAuth,
|
|
3153
|
-
completeAuth: options.completeAuth,
|
|
3154
|
-
executeAction: async ({ connection, request, connector, action }) => {
|
|
3155
|
-
const catalogEntry = byEntry.get(connector.id);
|
|
3156
|
-
if (!catalogEntry) {
|
|
3157
|
-
throw new IntegrationError(`Activepieces catalog entry ${connector.id} not found.`, "connector_not_found");
|
|
3158
|
-
}
|
|
3159
|
-
const catalogAction = catalogEntry.actions.find((candidate) => candidate.id === action.id);
|
|
3160
|
-
return options.executeAction({
|
|
3161
|
-
connection,
|
|
3162
|
-
request,
|
|
3163
|
-
connector,
|
|
3164
|
-
catalogEntry,
|
|
3165
|
-
piece: {
|
|
3166
|
-
id: catalogEntry.id,
|
|
3167
|
-
npmPackage: catalogEntry.npmPackage,
|
|
3168
|
-
version: catalogEntry.version,
|
|
3169
|
-
actionId: action.id,
|
|
3170
|
-
upstreamActionName: catalogAction?.upstreamName
|
|
3171
|
-
}
|
|
3172
|
-
});
|
|
3173
|
-
}
|
|
3174
|
-
});
|
|
3175
|
-
}
|
|
3176
|
-
|
|
3177
|
-
// src/catalog-freshness.ts
|
|
3178
|
-
var ACTIVEPIECES_PUBLIC_CATALOG_URL = "https://www.activepieces.com/pieces";
|
|
3179
|
-
function parseCount(value) {
|
|
3180
|
-
if (!value) return void 0;
|
|
3181
|
-
const parsed = Number.parseInt(value.replace(/,/g, ""), 10);
|
|
3182
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3183
|
-
}
|
|
3184
|
-
function extractActivepiecesPublicPieceCount(html) {
|
|
3185
|
-
const showingMatch = html.match(/Showing\s+([0-9,]+)\s+pieces/i);
|
|
3186
|
-
const integrationMatch = html.match(/([0-9,]+)\+?\s+Integrations/i);
|
|
3187
|
-
return parseCount(showingMatch?.[1]) ?? parseCount(integrationMatch?.[1]);
|
|
3188
|
-
}
|
|
3189
|
-
async function auditIntegrationCatalogFreshness(options = {}) {
|
|
3190
|
-
const minActivepiecesConnectors = options.minActivepiecesConnectors ?? 600;
|
|
3191
|
-
const staleConnectorDelta = options.staleConnectorDelta ?? 25;
|
|
3192
|
-
const activepiecesEntries = listActivepiecesCatalogEntries();
|
|
3193
|
-
const activepiecesConnectors = buildActivepiecesConnectors({
|
|
3194
|
-
includeCatalogActions: true
|
|
3195
|
-
});
|
|
3196
|
-
const executableActivepiecesProvider = createActivepiecesExecutorProvider({
|
|
3197
|
-
executeAction: () => ({ ok: true, action: "audit.noop" })
|
|
3198
|
-
});
|
|
3199
|
-
const executableActivepiecesConnectors = await executableActivepiecesProvider.listConnectors();
|
|
3200
|
-
const executableRegistry = composeIntegrationRegistry([
|
|
3201
|
-
{
|
|
3202
|
-
id: executableActivepiecesProvider.id,
|
|
3203
|
-
connectors: executableActivepiecesConnectors
|
|
3204
|
-
}
|
|
3205
|
-
]);
|
|
3206
|
-
const executableTools = buildIntegrationToolCatalog(executableRegistry.connectors);
|
|
3207
|
-
const unsupportedExecutableConnectorIds = executableActivepiecesConnectors.filter((connector) => connector.actions.length === 0).map((connector) => connector.id);
|
|
3208
|
-
const registry = buildDefaultIntegrationRegistry({
|
|
3209
|
-
includeSpecs: true,
|
|
3210
|
-
includeActivepieces: true
|
|
3211
|
-
});
|
|
3212
|
-
const warnings = [];
|
|
3213
|
-
if (activepiecesConnectors.length < minActivepiecesConnectors) {
|
|
3214
|
-
warnings.push(
|
|
3215
|
-
`Activepieces catalog has ${activepiecesConnectors.length} connectors, below floor ${minActivepiecesConnectors}.`
|
|
3216
|
-
);
|
|
3217
|
-
}
|
|
3218
|
-
let upstream;
|
|
3219
|
-
if (options.liveActivepieces) {
|
|
3220
|
-
upstream = await checkActivepiecesPublicCatalog({
|
|
3221
|
-
localConnectorCount: activepiecesConnectors.length,
|
|
3222
|
-
staleConnectorDelta,
|
|
3223
|
-
fetchImpl: options.fetchImpl,
|
|
3224
|
-
warnings
|
|
3225
|
-
});
|
|
3226
|
-
}
|
|
3227
|
-
return {
|
|
3228
|
-
ok: warnings.length === 0,
|
|
3229
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3230
|
-
local: {
|
|
3231
|
-
activepiecesEntries: activepiecesEntries.length,
|
|
3232
|
-
activepiecesConnectors: activepiecesConnectors.length,
|
|
3233
|
-
activepiecesActions: activepiecesConnectors.reduce(
|
|
3234
|
-
(sum, connector) => sum + connector.actions.length,
|
|
3235
|
-
0
|
|
3236
|
-
),
|
|
3237
|
-
activepiecesTriggers: activepiecesConnectors.reduce(
|
|
3238
|
-
(sum, connector) => sum + (connector.triggers?.length ?? 0),
|
|
3239
|
-
0
|
|
3240
|
-
),
|
|
3241
|
-
executableActivepiecesConnectors: executableActivepiecesConnectors.length,
|
|
3242
|
-
executableActivepiecesActions: executableActivepiecesConnectors.reduce(
|
|
3243
|
-
(sum, connector) => sum + connector.actions.length,
|
|
3244
|
-
0
|
|
3245
|
-
),
|
|
3246
|
-
executableActivepiecesTriggers: executableActivepiecesConnectors.reduce(
|
|
3247
|
-
(sum, connector) => sum + (connector.triggers?.length ?? 0),
|
|
3248
|
-
0
|
|
3249
|
-
),
|
|
3250
|
-
executableToolDefinitions: executableTools.length,
|
|
3251
|
-
unsupportedExecutableConnectorIds,
|
|
3252
|
-
registryEntries: registry.entries.length,
|
|
3253
|
-
registrySummary: summarizeIntegrationRegistry(registry),
|
|
3254
|
-
conflictSamples: registry.entries.flatMap((entry) => entry.conflicts).slice(0, 10)
|
|
3255
|
-
},
|
|
3256
|
-
upstream,
|
|
3257
|
-
warnings
|
|
3258
|
-
};
|
|
3259
|
-
}
|
|
3260
|
-
async function checkActivepiecesPublicCatalog(input) {
|
|
3261
|
-
try {
|
|
3262
|
-
const res = await (input.fetchImpl ?? fetch)(ACTIVEPIECES_PUBLIC_CATALOG_URL, {
|
|
3263
|
-
headers: { accept: "text/html" },
|
|
3264
|
-
signal: AbortSignal.timeout(15e3)
|
|
3265
|
-
});
|
|
3266
|
-
if (!res.ok) {
|
|
3267
|
-
const warning = `Activepieces freshness request failed with HTTP ${res.status}.`;
|
|
3268
|
-
input.warnings.push(warning);
|
|
3269
|
-
return {
|
|
3270
|
-
checkedUrl: ACTIVEPIECES_PUBLIC_CATALOG_URL,
|
|
3271
|
-
warning
|
|
3272
|
-
};
|
|
3273
|
-
}
|
|
3274
|
-
const activepiecesPieces = extractActivepiecesPublicPieceCount(await res.text());
|
|
3275
|
-
const activepiecesDelta = activepiecesPieces === void 0 ? void 0 : activepiecesPieces - input.localConnectorCount;
|
|
3276
|
-
if (activepiecesDelta !== void 0 && activepiecesDelta > input.staleConnectorDelta) {
|
|
3277
|
-
input.warnings.push(
|
|
3278
|
-
`Activepieces upstream appears ${activepiecesDelta} connectors ahead of the vendored package catalog.`
|
|
3279
|
-
);
|
|
3280
|
-
}
|
|
3281
|
-
return {
|
|
3282
|
-
activepiecesPieces,
|
|
3283
|
-
activepiecesDelta,
|
|
3284
|
-
checkedUrl: ACTIVEPIECES_PUBLIC_CATALOG_URL,
|
|
3285
|
-
warning: activepiecesPieces === void 0 ? "Could not parse upstream piece count." : void 0
|
|
3286
|
-
};
|
|
3287
|
-
} catch (error) {
|
|
3288
|
-
const warning = error instanceof Error ? error.message : "Activepieces freshness request failed.";
|
|
3289
|
-
input.warnings.push(warning);
|
|
3290
|
-
return {
|
|
3291
|
-
checkedUrl: ACTIVEPIECES_PUBLIC_CATALOG_URL,
|
|
3292
|
-
warning
|
|
3293
|
-
};
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
// src/runtime.ts
|
|
3298
|
-
var InMemoryIntegrationGrantStore = class {
|
|
3299
|
-
grants = /* @__PURE__ */ new Map();
|
|
3300
|
-
get(grantId) {
|
|
3301
|
-
return this.grants.get(grantId);
|
|
3302
|
-
}
|
|
3303
|
-
put(grant) {
|
|
3304
|
-
this.grants.set(grant.id, grant);
|
|
3305
|
-
}
|
|
3306
|
-
listByManifest(manifestId, grantee) {
|
|
3307
|
-
return [...this.grants.values()].filter(
|
|
3308
|
-
(grant) => grant.manifestId === manifestId && (!grantee || sameActor(grant.grantee, grantee))
|
|
3309
|
-
);
|
|
3310
|
-
}
|
|
3311
|
-
listByGrantee(grantee) {
|
|
3312
|
-
return [...this.grants.values()].filter((grant) => sameActor(grant.grantee, grantee));
|
|
3313
|
-
}
|
|
3314
|
-
listByIds(grantIds) {
|
|
3315
|
-
const wanted = new Set(grantIds);
|
|
3316
|
-
return [...this.grants.values()].filter((grant) => wanted.has(grant.id));
|
|
3317
|
-
}
|
|
3318
|
-
delete(grantId) {
|
|
3319
|
-
this.grants.delete(grantId);
|
|
3320
|
-
}
|
|
3321
|
-
};
|
|
3322
|
-
var IntegrationRuntime = class {
|
|
3323
|
-
hub;
|
|
3324
|
-
grants;
|
|
3325
|
-
now;
|
|
3326
|
-
constructor(options) {
|
|
3327
|
-
this.hub = options.hub;
|
|
3328
|
-
this.grants = options.grants ?? new InMemoryIntegrationGrantStore();
|
|
3329
|
-
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3330
|
-
}
|
|
3331
|
-
async registry() {
|
|
3332
|
-
return this.hub.listRegistry();
|
|
3333
|
-
}
|
|
3334
|
-
async resolveManifest(manifest, owner) {
|
|
3335
|
-
const registry = await this.registry();
|
|
3336
|
-
const connections = await this.hub.listConnections(owner);
|
|
3337
|
-
const resolutions = manifest.requirements.map(
|
|
3338
|
-
(requirement) => resolveRequirement(requirement, owner, registry, connections)
|
|
3339
|
-
);
|
|
3340
|
-
return {
|
|
3341
|
-
manifest,
|
|
3342
|
-
owner,
|
|
3343
|
-
ready: resolutions.filter((resolution) => resolution.status === "ready"),
|
|
3344
|
-
missing: resolutions.filter((resolution) => resolution.status !== "ready" && !resolution.requirement.optional),
|
|
3345
|
-
optionalMissing: resolutions.filter((resolution) => resolution.status !== "ready" && resolution.requirement.optional === true)
|
|
3346
|
-
};
|
|
3347
|
-
}
|
|
3348
|
-
async createGrants(input) {
|
|
3349
|
-
const resolution = await this.resolveManifest(input.manifest, input.owner);
|
|
3350
|
-
if (resolution.missing.length > 0) {
|
|
3351
|
-
throw new Error(`Cannot create integration grants; missing requirements: ${resolution.missing.map((r) => r.requirement.id).join(", ")}`);
|
|
3352
|
-
}
|
|
3353
|
-
const now = this.now().toISOString();
|
|
3354
|
-
const grants = resolution.ready.map((ready) => ({
|
|
3355
|
-
id: `grant_${input.manifest.id}_${ready.requirement.id}_${ready.connection.id}`,
|
|
3356
|
-
manifestId: input.manifest.id,
|
|
3357
|
-
requirementId: ready.requirement.id,
|
|
3358
|
-
owner: input.owner,
|
|
3359
|
-
grantee: input.grantee,
|
|
3360
|
-
connectionId: ready.connection.id,
|
|
3361
|
-
connectorId: ready.connector.id,
|
|
3362
|
-
scopes: requiredScopes(ready.requirement, ready.connector),
|
|
3363
|
-
allowedActions: requiredActions(ready.requirement, ready.connector),
|
|
3364
|
-
allowedTriggers: requiredTriggers(ready.requirement, ready.connector),
|
|
3365
|
-
status: "active",
|
|
3366
|
-
createdAt: now,
|
|
3367
|
-
updatedAt: now,
|
|
3368
|
-
metadata: input.metadata
|
|
3369
|
-
}));
|
|
3370
|
-
for (const grant of grants) await this.grants.put(grant);
|
|
3371
|
-
return grants;
|
|
3372
|
-
}
|
|
3373
|
-
async buildSandboxBundle(input) {
|
|
3374
|
-
const grants = await this.resolveBundleGrants(input);
|
|
3375
|
-
if (grants.length === 0) {
|
|
3376
|
-
throw new Error("Cannot build integration bundle; no active integration grants matched the request.");
|
|
3377
|
-
}
|
|
3378
|
-
const registry = await this.registry();
|
|
3379
|
-
const bindings = [];
|
|
3380
|
-
const connectors = [];
|
|
3381
|
-
let expiresAt = "";
|
|
3382
|
-
for (const grant of grants) {
|
|
3383
|
-
const connection = await this.hub.getConnection(grant.connectionId);
|
|
3384
|
-
if (!connection) {
|
|
3385
|
-
throw new Error(`Cannot build integration bundle; connection ${grant.connectionId} not found.`);
|
|
3386
|
-
}
|
|
3387
|
-
if (!sameActor(connection.owner, input.owner)) {
|
|
3388
|
-
throw new Error(`Cannot build integration bundle; connection ${connection.id} belongs to a different owner.`);
|
|
3389
|
-
}
|
|
3390
|
-
if (connection.status !== "active") {
|
|
3391
|
-
throw new Error(`Cannot build integration bundle; connection ${connection.id} is ${connection.status}.`);
|
|
3392
|
-
}
|
|
3393
|
-
const entry = registry.byId.get(grant.connectorId);
|
|
3394
|
-
if (!entry) continue;
|
|
3395
|
-
const connector = {
|
|
3396
|
-
...entry.connector,
|
|
3397
|
-
actions: entry.connector.actions.filter((action) => grant.allowedActions.includes(action.id)),
|
|
3398
|
-
triggers: entry.connector.triggers?.filter((trigger2) => grant.allowedTriggers.includes(trigger2.id)),
|
|
3399
|
-
scopes: entry.connector.scopes.filter((scope) => grant.scopes.includes(scope))
|
|
3400
|
-
};
|
|
3401
|
-
const capability = await this.hub.issueCapability({
|
|
3402
|
-
subject: input.subject,
|
|
3403
|
-
connectionId: grant.connectionId,
|
|
3404
|
-
scopes: grant.scopes,
|
|
3405
|
-
allowedActions: grant.allowedActions,
|
|
3406
|
-
ttlMs: input.ttlMs,
|
|
3407
|
-
metadata: {
|
|
3408
|
-
manifestId: grant.manifestId,
|
|
3409
|
-
grantId: grant.id,
|
|
3410
|
-
requirementId: grant.requirementId
|
|
3411
|
-
}
|
|
3412
|
-
});
|
|
3413
|
-
bindings.push({
|
|
3414
|
-
requirementId: grant.requirementId,
|
|
3415
|
-
connectorId: grant.connectorId,
|
|
3416
|
-
connectionId: grant.connectionId,
|
|
3417
|
-
grantId: grant.id,
|
|
3418
|
-
scopes: grant.scopes,
|
|
3419
|
-
allowedActions: grant.allowedActions,
|
|
3420
|
-
allowedTriggers: grant.allowedTriggers,
|
|
3421
|
-
capability
|
|
3422
|
-
});
|
|
3423
|
-
connectors.push(connector);
|
|
3424
|
-
expiresAt = capability.capability.expiresAt;
|
|
3425
|
-
}
|
|
3426
|
-
return {
|
|
3427
|
-
manifestId: input.manifestId ?? grants[0]?.manifestId ?? "explicit-grants",
|
|
3428
|
-
subject: input.subject,
|
|
3429
|
-
capabilities: bindings,
|
|
3430
|
-
connectors,
|
|
3431
|
-
tools: buildIntegrationToolCatalog(connectors),
|
|
3432
|
-
expiresAt
|
|
3433
|
-
};
|
|
3434
|
-
}
|
|
3435
|
-
async resolveBundleGrants(input) {
|
|
3436
|
-
if (input.grantIds?.length) {
|
|
3437
|
-
const uniqueGrantIds = unique5(input.grantIds);
|
|
3438
|
-
const grants = this.grants.listByIds ? await this.grants.listByIds(uniqueGrantIds) : await Promise.all(uniqueGrantIds.map((grantId) => this.grants.get(grantId)));
|
|
3439
|
-
const found = grants.filter((grant) => Boolean(grant));
|
|
3440
|
-
const foundIds = new Set(found.map((grant) => grant.id));
|
|
3441
|
-
const missing2 = uniqueGrantIds.filter((grantId) => !foundIds.has(grantId));
|
|
3442
|
-
if (missing2.length > 0) {
|
|
3443
|
-
throw new Error(`Cannot build integration bundle; unknown grant id(s): ${missing2.join(", ")}`);
|
|
3444
|
-
}
|
|
3445
|
-
const badOwner = found.find((grant) => !sameActor(grant.owner, input.owner));
|
|
3446
|
-
if (badOwner) {
|
|
3447
|
-
throw new Error(`Cannot build integration bundle; grant ${badOwner.id} belongs to a different owner.`);
|
|
3448
|
-
}
|
|
3449
|
-
const badGrantee = input.grantee ? found.find((grant) => !sameActor(grant.grantee, input.grantee)) : void 0;
|
|
3450
|
-
if (badGrantee) {
|
|
3451
|
-
throw new Error(`Cannot build integration bundle; grant ${badGrantee.id} belongs to a different grantee.`);
|
|
3452
|
-
}
|
|
3453
|
-
const badManifest = input.manifestId ? found.find((grant) => grant.manifestId !== input.manifestId) : void 0;
|
|
3454
|
-
if (badManifest) {
|
|
3455
|
-
throw new Error(`Cannot build integration bundle; grant ${badManifest.id} is not for manifest ${input.manifestId}.`);
|
|
3456
|
-
}
|
|
3457
|
-
const inactive = found.find((grant) => grant.status !== "active");
|
|
3458
|
-
if (inactive) {
|
|
3459
|
-
throw new Error(`Cannot build integration bundle; grant ${inactive.id} is ${inactive.status}.`);
|
|
3460
|
-
}
|
|
3461
|
-
return found;
|
|
3462
|
-
}
|
|
3463
|
-
if (!input.manifestId) {
|
|
3464
|
-
throw new Error("Cannot build integration bundle; manifestId or grantIds is required.");
|
|
3465
|
-
}
|
|
3466
|
-
return (await this.grants.listByManifest(input.manifestId, input.grantee)).filter((grant) => sameActor(grant.owner, input.owner)).filter((grant) => grant.status === "active");
|
|
3467
|
-
}
|
|
3468
|
-
};
|
|
3469
|
-
function createIntegrationRuntime(options) {
|
|
3470
|
-
return new IntegrationRuntime(options);
|
|
3471
|
-
}
|
|
3472
|
-
function resolveRequirement(requirement, owner, registry, connections) {
|
|
3473
|
-
const entry = registry.byId.get(requirement.connectorId);
|
|
3474
|
-
if (!entry) {
|
|
3475
|
-
return missing(requirement, "unknown_connector", `Unknown connector ${requirement.connectorId}.`);
|
|
3476
|
-
}
|
|
3477
|
-
const connector = entry.connector;
|
|
3478
|
-
if (connector.actions.length === 0 && (connector.triggers?.length ?? 0) === 0) {
|
|
3479
|
-
return missing(requirement, "not_executable", `${connector.title} is catalog-only and cannot be invoked yet.`, connector, entry);
|
|
3480
|
-
}
|
|
3481
|
-
const scopes = requiredScopes(requirement, connector);
|
|
3482
|
-
const actions = requiredActions(requirement, connector);
|
|
3483
|
-
const triggers = requiredTriggers(requirement, connector);
|
|
3484
|
-
const connection = connections.find(
|
|
3485
|
-
(candidate) => sameActor(candidate.owner, owner) && candidate.status === "active" && (candidate.connectorId === connector.id || entry.aliases.includes(candidate.connectorId)) && scopes.every((scope) => candidate.grantedScopes.includes(scope))
|
|
3486
|
-
);
|
|
3487
|
-
if (!connection) {
|
|
3488
|
-
return {
|
|
3489
|
-
requirement,
|
|
3490
|
-
status: "missing_connection",
|
|
3491
|
-
connector,
|
|
3492
|
-
registryEntry: entry,
|
|
3493
|
-
missingScopes: scopes,
|
|
3494
|
-
missingActions: actions,
|
|
3495
|
-
missingTriggers: triggers,
|
|
3496
|
-
message: `${connector.title} needs an active user connection with the required scopes.`
|
|
3497
|
-
};
|
|
3498
|
-
}
|
|
3499
|
-
return {
|
|
3500
|
-
requirement,
|
|
3501
|
-
status: "ready",
|
|
3502
|
-
connector,
|
|
3503
|
-
registryEntry: entry,
|
|
3504
|
-
connection,
|
|
3505
|
-
missingScopes: [],
|
|
3506
|
-
missingActions: [],
|
|
3507
|
-
missingTriggers: [],
|
|
3508
|
-
message: `${connector.title} is ready.`
|
|
3509
|
-
};
|
|
3510
|
-
}
|
|
3511
|
-
function missing(requirement, status, message, connector, registryEntry2) {
|
|
3512
|
-
return {
|
|
3513
|
-
requirement,
|
|
3514
|
-
status,
|
|
3515
|
-
connector,
|
|
3516
|
-
registryEntry: registryEntry2,
|
|
3517
|
-
missingScopes: [],
|
|
3518
|
-
missingActions: [],
|
|
3519
|
-
missingTriggers: [],
|
|
3520
|
-
message
|
|
3521
|
-
};
|
|
3522
|
-
}
|
|
3523
|
-
function requiredActions(requirement, connector) {
|
|
3524
|
-
if (requirement.mode === "trigger") return [];
|
|
3525
|
-
if (requirement.requiredActions?.length) return unique5(requirement.requiredActions);
|
|
3526
|
-
const actions = connector.actions.filter((action) => {
|
|
3527
|
-
if (requirement.mode === "read") return action.risk === "read";
|
|
3528
|
-
if (requirement.mode === "write") return action.risk === "write";
|
|
3529
|
-
return false;
|
|
3530
|
-
});
|
|
3531
|
-
return unique5(actions.map((action) => action.id));
|
|
3532
|
-
}
|
|
3533
|
-
function requiredTriggers(requirement, connector) {
|
|
3534
|
-
if (requirement.requiredTriggers?.length) return unique5(requirement.requiredTriggers);
|
|
3535
|
-
if (requirement.mode !== "trigger") return [];
|
|
3536
|
-
return unique5((connector.triggers ?? []).map((trigger2) => trigger2.id));
|
|
3537
|
-
}
|
|
3538
|
-
function requiredScopes(requirement, connector) {
|
|
3539
|
-
if (requirement.requiredScopes?.length) return unique5(requirement.requiredScopes);
|
|
3540
|
-
const actionIds = new Set(requiredActions(requirement, connector));
|
|
3541
|
-
const triggerIds = new Set(requiredTriggers(requirement, connector));
|
|
3542
|
-
return unique5([
|
|
3543
|
-
...connector.actions.filter((action) => actionIds.has(action.id)).flatMap((action) => action.requiredScopes),
|
|
3544
|
-
...(connector.triggers ?? []).filter((trigger2) => triggerIds.has(trigger2.id)).flatMap((trigger2) => trigger2.requiredScopes)
|
|
3545
|
-
]);
|
|
3546
|
-
}
|
|
3547
|
-
function sameActor(a, b) {
|
|
3548
|
-
return a.type === b.type && a.id === b.id;
|
|
3549
|
-
}
|
|
3550
|
-
function unique5(values) {
|
|
3551
|
-
return [...new Set(values)];
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
// src/workflow.ts
|
|
3555
|
-
var InMemoryIntegrationWorkflowStore = class {
|
|
3556
|
-
workflows = /* @__PURE__ */ new Map();
|
|
3557
|
-
put(workflow) {
|
|
3558
|
-
this.workflows.set(workflow.id, workflow);
|
|
3559
|
-
}
|
|
3560
|
-
get(id) {
|
|
3561
|
-
return this.workflows.get(id);
|
|
3562
|
-
}
|
|
3563
|
-
list() {
|
|
3564
|
-
return [...this.workflows.values()];
|
|
3565
|
-
}
|
|
3566
|
-
listByWorkflow(workflowId) {
|
|
3567
|
-
return [...this.workflows.values()].filter((workflow) => workflow.workflowId === workflowId);
|
|
3568
|
-
}
|
|
3569
|
-
listByOwner(owner) {
|
|
3570
|
-
return [...this.workflows.values()].filter((workflow) => sameActor2(workflow.owner, owner));
|
|
3571
|
-
}
|
|
3572
|
-
};
|
|
3573
|
-
var IntegrationWorkflowRuntime = class {
|
|
3574
|
-
runtime;
|
|
3575
|
-
hub;
|
|
3576
|
-
grants;
|
|
3577
|
-
store;
|
|
3578
|
-
now;
|
|
3579
|
-
constructor(options) {
|
|
3580
|
-
this.runtime = options.runtime;
|
|
3581
|
-
this.hub = options.hub;
|
|
3582
|
-
this.grants = options.grants;
|
|
3583
|
-
this.store = options.store ?? new InMemoryIntegrationWorkflowStore();
|
|
3584
|
-
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3585
|
-
}
|
|
3586
|
-
async install(input) {
|
|
3587
|
-
const grants = await this.runtime.createGrants({
|
|
3588
|
-
manifest: input.workflow.manifest,
|
|
3589
|
-
owner: input.owner,
|
|
3590
|
-
grantee: input.grantee,
|
|
3591
|
-
metadata: { workflowId: input.workflow.id }
|
|
3592
|
-
});
|
|
3593
|
-
const triggerGrant = findTriggerGrant(grants, input.workflow.trigger.requirementId, input.workflow.trigger.triggerId);
|
|
3594
|
-
const subscription = await this.hub.subscribeTrigger(
|
|
3595
|
-
triggerGrant.connectionId,
|
|
3596
|
-
input.workflow.trigger.triggerId,
|
|
3597
|
-
input.workflow.trigger.targetUrl
|
|
3598
|
-
);
|
|
3599
|
-
const installed = {
|
|
3600
|
-
id: `workflow_${input.workflow.id}_${triggerGrant.id}`,
|
|
3601
|
-
workflowId: input.workflow.id,
|
|
3602
|
-
manifestId: input.workflow.manifest.id,
|
|
3603
|
-
owner: input.owner,
|
|
3604
|
-
grantee: input.grantee,
|
|
3605
|
-
triggerGrantId: triggerGrant.id,
|
|
3606
|
-
subscription,
|
|
3607
|
-
status: "active",
|
|
3608
|
-
createdAt: this.now().toISOString(),
|
|
3609
|
-
metadata: input.workflow.metadata
|
|
3610
|
-
};
|
|
3611
|
-
await this.store.put(installed);
|
|
3612
|
-
return installed;
|
|
3613
|
-
}
|
|
3614
|
-
async dispatchEvent(event, handler) {
|
|
3615
|
-
const workflows = (await this.store.list()).filter(
|
|
3616
|
-
(workflow) => workflow.status === "active" && workflow.subscription.connectionId === event.connectionId && workflow.subscription.trigger === event.trigger
|
|
3617
|
-
);
|
|
3618
|
-
await handler({ event, workflows });
|
|
3619
|
-
return { matched: workflows };
|
|
3620
|
-
}
|
|
3621
|
-
};
|
|
3622
|
-
function createIntegrationWorkflowRuntime(options) {
|
|
3623
|
-
return new IntegrationWorkflowRuntime(options);
|
|
3624
|
-
}
|
|
3625
|
-
function findTriggerGrant(grants, requirementId, triggerId) {
|
|
3626
|
-
const grant = grants.find(
|
|
3627
|
-
(candidate) => candidate.requirementId === requirementId && candidate.allowedTriggers.includes(triggerId)
|
|
3628
|
-
);
|
|
3629
|
-
if (!grant) throw new Error(`Missing trigger grant ${requirementId}/${triggerId}.`);
|
|
3630
|
-
return grant;
|
|
3631
|
-
}
|
|
3632
|
-
function sameActor2(a, b) {
|
|
3633
|
-
return a.type === b.type && a.id === b.id;
|
|
3634
|
-
}
|
|
3635
|
-
|
|
3636
|
-
// src/index.ts
|
|
3637
|
-
var InMemoryConnectionStore = class {
|
|
3638
|
-
connections = /* @__PURE__ */ new Map();
|
|
3639
|
-
get(connectionId) {
|
|
3640
|
-
return this.connections.get(connectionId);
|
|
3641
|
-
}
|
|
3642
|
-
put(connection) {
|
|
3643
|
-
this.connections.set(connection.id, connection);
|
|
3644
|
-
}
|
|
3645
|
-
listByOwner(owner) {
|
|
3646
|
-
return [...this.connections.values()].filter(
|
|
3647
|
-
(connection) => connection.owner.type === owner.type && connection.owner.id === owner.id
|
|
3648
|
-
);
|
|
3649
|
-
}
|
|
3650
|
-
delete(connectionId) {
|
|
3651
|
-
this.connections.delete(connectionId);
|
|
3652
|
-
}
|
|
3653
|
-
};
|
|
3654
|
-
var IntegrationHub = class {
|
|
3655
|
-
providers = /* @__PURE__ */ new Map();
|
|
3656
|
-
store;
|
|
3657
|
-
capabilitySecret;
|
|
3658
|
-
guard;
|
|
3659
|
-
policy;
|
|
3660
|
-
/** Host-injected (or in-memory default) secret store. The hub re-persists
|
|
3661
|
-
* rotated credentials here when a connection carries a secretRef. */
|
|
3662
|
-
secretStore;
|
|
3663
|
-
oauthStateStore;
|
|
3664
|
-
oauthStateTtlMs;
|
|
3665
|
-
credentialsRotated;
|
|
3666
|
-
now;
|
|
3667
|
-
constructor(options) {
|
|
3668
|
-
if (!options.capabilitySecret) {
|
|
3669
|
-
throw new IntegrationError("capabilitySecret is required.", "capability_invalid");
|
|
3670
|
-
}
|
|
3671
|
-
for (const provider of options.providers) this.providers.set(provider.id, provider);
|
|
3672
|
-
this.store = options.store;
|
|
3673
|
-
this.capabilitySecret = options.capabilitySecret;
|
|
3674
|
-
this.guard = options.guard;
|
|
3675
|
-
this.policy = options.policy;
|
|
3676
|
-
this.secretStore = options.secretStore ?? new InMemoryIntegrationSecretStore();
|
|
3677
|
-
this.oauthStateStore = options.oauthStateStore ?? new InMemoryIntegrationOAuthStateStore();
|
|
3678
|
-
this.oauthStateTtlMs = options.oauthStateTtlMs ?? 10 * 60 * 1e3;
|
|
3679
|
-
this.credentialsRotated = options.credentialsRotated;
|
|
3680
|
-
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3681
|
-
}
|
|
3682
|
-
async listConnectors() {
|
|
3683
|
-
const catalogs = await Promise.all([...this.providers.values()].map((provider) => provider.listConnectors()));
|
|
3684
|
-
return catalogs.flat();
|
|
3685
|
-
}
|
|
3686
|
-
async listRegistry(options = {}) {
|
|
3687
|
-
const sources = await Promise.all([...this.providers.values()].map(async (provider) => ({
|
|
3688
|
-
id: provider.id,
|
|
3689
|
-
connectors: await provider.listConnectors()
|
|
3690
|
-
})));
|
|
3691
|
-
return composeIntegrationRegistry(sources, options);
|
|
3692
|
-
}
|
|
3693
|
-
async startAuth(providerId, request) {
|
|
3694
|
-
const provider = this.requireProvider(providerId);
|
|
3695
|
-
if (!provider.startAuth) throw new IntegrationError(`Provider ${providerId} does not support auth start.`, "auth_not_supported");
|
|
3696
|
-
const connector = await this.requireConnector(provider, request.connectorId);
|
|
3697
|
-
const pkce = connectorOAuthPkceMode(connector);
|
|
3698
|
-
const pkcePair = pkce === "unsupported" ? void 0 : createPkcePair();
|
|
3699
|
-
const result = await provider.startAuth({
|
|
3700
|
-
...request,
|
|
3701
|
-
codeChallenge: pkcePair?.challenge
|
|
3702
|
-
});
|
|
3703
|
-
const record = {
|
|
3704
|
-
state: result.state,
|
|
3705
|
-
providerId,
|
|
3706
|
-
connectorId: request.connectorId,
|
|
3707
|
-
owner: request.owner,
|
|
3708
|
-
requestedScopes: request.requestedScopes,
|
|
3709
|
-
redirectUri: request.redirectUri,
|
|
3710
|
-
codeVerifier: pkcePair?.verifier,
|
|
3711
|
-
expiresAt: this.now().getTime() + this.oauthStateTtlMs,
|
|
3712
|
-
metadata: request.metadata
|
|
3713
|
-
};
|
|
3714
|
-
await this.oauthStateStore.put(record);
|
|
3715
|
-
return result;
|
|
3716
|
-
}
|
|
3717
|
-
async completeAuth(providerId, request) {
|
|
3718
|
-
const provider = this.requireProvider(providerId);
|
|
3719
|
-
if (!provider.completeAuth) throw new IntegrationError(`Provider ${providerId} does not support auth completion.`, "auth_not_supported");
|
|
3720
|
-
const outcome = await this.oauthStateStore.consume(request.state);
|
|
3721
|
-
if (!outcome.ok) {
|
|
3722
|
-
throw new IntegrationError(`Integration OAuth state ${outcome.reason}: possible CSRF, replay, or stale flow.`, "capability_invalid");
|
|
3723
|
-
}
|
|
3724
|
-
if (outcome.state.providerId !== providerId || outcome.state.connectorId !== request.connectorId) {
|
|
3725
|
-
throw new IntegrationError("Integration OAuth state does not match completion request.", "capability_invalid");
|
|
3726
|
-
}
|
|
3727
|
-
if (outcome.state.redirectUri !== request.redirectUri) {
|
|
3728
|
-
throw new IntegrationError("Integration OAuth redirect URI does not match the start request.", "capability_invalid");
|
|
3729
|
-
}
|
|
3730
|
-
const pinnedMetadata = request.metadata || outcome.state.metadata ? { ...request.metadata, ...outcome.state.metadata } : void 0;
|
|
3731
|
-
const connection = await provider.completeAuth({
|
|
3732
|
-
...request,
|
|
3733
|
-
connectorId: outcome.state.connectorId,
|
|
3734
|
-
owner: outcome.state.owner,
|
|
3735
|
-
state: outcome.state.state,
|
|
3736
|
-
redirectUri: outcome.state.redirectUri,
|
|
3737
|
-
codeVerifier: outcome.state.codeVerifier,
|
|
3738
|
-
metadata: pinnedMetadata
|
|
3739
|
-
});
|
|
3740
|
-
await this.store.put(connection);
|
|
3741
|
-
return connection;
|
|
3742
|
-
}
|
|
3743
|
-
async upsertConnection(connection) {
|
|
3744
|
-
await this.store.put(connection);
|
|
3745
|
-
return connection;
|
|
3746
|
-
}
|
|
3747
|
-
async listConnections(owner) {
|
|
3748
|
-
return this.store.listByOwner(owner);
|
|
3749
|
-
}
|
|
3750
|
-
async getConnection(connectionId) {
|
|
3751
|
-
return this.store.get(connectionId);
|
|
3752
|
-
}
|
|
3753
|
-
async issueCapability(request) {
|
|
3754
|
-
const connection = await this.requireConnection(request.connectionId);
|
|
3755
|
-
this.assertConnectionActive(connection);
|
|
3756
|
-
assertScopes(connection, request.scopes);
|
|
3757
|
-
const now = this.now();
|
|
3758
|
-
const capability = {
|
|
3759
|
-
id: `cap_${randomUUID3()}`,
|
|
3760
|
-
subject: request.subject,
|
|
3761
|
-
connectionId: request.connectionId,
|
|
3762
|
-
scopes: unique6(request.scopes),
|
|
3763
|
-
allowedActions: unique6(request.allowedActions),
|
|
3764
|
-
issuedAt: now.toISOString(),
|
|
3765
|
-
expiresAt: new Date(now.getTime() + request.ttlMs).toISOString(),
|
|
3766
|
-
metadata: request.metadata
|
|
3767
|
-
};
|
|
3768
|
-
return { capability, token: signCapability(capability, this.capabilitySecret) };
|
|
3769
|
-
}
|
|
3770
|
-
verifyCapability(token) {
|
|
3771
|
-
const capability = verifyCapabilityToken(token, this.capabilitySecret);
|
|
3772
|
-
if (Date.parse(capability.expiresAt) <= this.now().getTime()) {
|
|
3773
|
-
throw new IntegrationError("Integration capability expired.", "capability_expired");
|
|
3774
|
-
}
|
|
3775
|
-
return capability;
|
|
3776
|
-
}
|
|
3777
|
-
async invokeWithCapability(token, request) {
|
|
3778
|
-
const capability = this.verifyCapability(token);
|
|
3779
|
-
if (!capability.allowedActions.includes(request.action)) {
|
|
3780
|
-
throw new IntegrationError(`Capability does not allow action ${request.action}.`, "action_denied");
|
|
3781
|
-
}
|
|
3782
|
-
const connection = await this.requireConnection(capability.connectionId);
|
|
3783
|
-
this.assertConnectionActive(connection);
|
|
3784
|
-
const provider = this.requireProvider(connection.providerId);
|
|
3785
|
-
const connector = await this.requireConnector(provider, connection.connectorId);
|
|
3786
|
-
const action = connector.actions.find((candidate) => candidate.id === request.action);
|
|
3787
|
-
if (!action) throw new IntegrationError(`Action ${request.action} is not defined by connector ${connector.id}.`, "action_not_found");
|
|
3788
|
-
assertScopes(connection, action.requiredScopes);
|
|
3789
|
-
assertScopes({ ...connection, grantedScopes: capability.scopes }, action.requiredScopes);
|
|
3790
|
-
const fullRequest = { ...request, connectionId: connection.id };
|
|
3791
|
-
const proceed = async () => {
|
|
3792
|
-
if (!this.policy) return provider.invokeAction(connection, fullRequest);
|
|
3793
|
-
const decision = await this.policy.decide({
|
|
3794
|
-
connection,
|
|
3795
|
-
request: fullRequest,
|
|
3796
|
-
action,
|
|
3797
|
-
subject: capability.subject
|
|
3798
|
-
});
|
|
3799
|
-
if (decision.decision === "deny") {
|
|
3800
|
-
throw new IntegrationError(decision.reason, "policy_denied");
|
|
3801
|
-
}
|
|
3802
|
-
if (decision.decision === "require_approval") {
|
|
3803
|
-
return {
|
|
3804
|
-
ok: false,
|
|
3805
|
-
action: request.action,
|
|
3806
|
-
output: { approvalRequired: true, approval: decision.approval },
|
|
3807
|
-
metadata: { policyDecision: decision.decision, reason: decision.reason, ...decision.metadata }
|
|
3808
|
-
};
|
|
3809
|
-
}
|
|
3810
|
-
return provider.invokeAction(connection, fullRequest);
|
|
3811
|
-
};
|
|
3812
|
-
if (this.guard) {
|
|
3813
|
-
return this.guard.invokeAction({ connection, request: fullRequest, action }, proceed);
|
|
3814
|
-
}
|
|
3815
|
-
return proceed();
|
|
3816
|
-
}
|
|
3817
|
-
async subscribeTrigger(connectionId, trigger2, targetUrl) {
|
|
3818
|
-
const connection = await this.requireConnection(connectionId);
|
|
3819
|
-
this.assertConnectionActive(connection);
|
|
3820
|
-
const provider = this.requireProvider(connection.providerId);
|
|
3821
|
-
const connector = await this.requireConnector(provider, connection.connectorId);
|
|
3822
|
-
const spec = connector.triggers?.find((candidate) => candidate.id === trigger2);
|
|
3823
|
-
if (!spec) throw new IntegrationError(`Trigger ${trigger2} is not defined by connector ${connector.id}.`, "trigger_not_found");
|
|
3824
|
-
assertScopes(connection, spec.requiredScopes);
|
|
3825
|
-
if (!provider.subscribeTrigger) {
|
|
3826
|
-
throw new IntegrationError(`Provider ${provider.id} does not support triggers.`, "auth_not_supported");
|
|
3827
|
-
}
|
|
3828
|
-
return provider.subscribeTrigger(connection, trigger2, targetUrl);
|
|
3829
|
-
}
|
|
3830
|
-
requireProvider(providerId) {
|
|
3831
|
-
const provider = this.providers.get(providerId);
|
|
3832
|
-
if (!provider) throw new IntegrationError(`Provider ${providerId} not found.`, "provider_not_found");
|
|
3833
|
-
return provider;
|
|
3834
|
-
}
|
|
3835
|
-
async requireConnector(provider, connectorId) {
|
|
3836
|
-
const connector = (await provider.listConnectors()).find((candidate) => candidate.id === connectorId);
|
|
3837
|
-
if (!connector) throw new IntegrationError(`Connector ${connectorId} not found.`, "connector_not_found");
|
|
3838
|
-
return connector;
|
|
3839
|
-
}
|
|
3840
|
-
async requireConnection(connectionId) {
|
|
3841
|
-
const connection = await this.store.get(connectionId);
|
|
3842
|
-
if (!connection) throw new IntegrationError(`Connection ${connectionId} not found.`, "connection_not_found");
|
|
3843
|
-
return connection;
|
|
3844
|
-
}
|
|
3845
|
-
assertConnectionActive(connection) {
|
|
3846
|
-
if (connection.status !== "active") {
|
|
3847
|
-
throw new IntegrationError(`Connection ${connection.id} is ${connection.status}.`, "connection_not_active");
|
|
3848
|
-
}
|
|
3849
|
-
if (connection.expiresAt && Date.parse(connection.expiresAt) <= this.now().getTime()) {
|
|
3850
|
-
throw new IntegrationError(`Connection ${connection.id} is expired.`, "connection_not_active");
|
|
3851
|
-
}
|
|
3852
|
-
}
|
|
3853
|
-
};
|
|
3854
|
-
function sanitizeConnection(connection) {
|
|
3855
|
-
return {
|
|
3856
|
-
id: connection.id,
|
|
3857
|
-
owner: connection.owner,
|
|
3858
|
-
providerId: connection.providerId,
|
|
3859
|
-
connectorId: connection.connectorId,
|
|
3860
|
-
status: connection.status,
|
|
3861
|
-
grantedScopes: connection.grantedScopes,
|
|
3862
|
-
account: connection.account,
|
|
3863
|
-
hasSecretRef: Boolean(connection.secretRef),
|
|
3864
|
-
createdAt: connection.createdAt,
|
|
3865
|
-
updatedAt: connection.updatedAt,
|
|
3866
|
-
expiresAt: connection.expiresAt,
|
|
3867
|
-
lastUsedAt: connection.lastUsedAt
|
|
3868
|
-
};
|
|
3869
|
-
}
|
|
3870
|
-
function createMockIntegrationProvider(options = {}) {
|
|
3871
|
-
const providerId = options.id ?? "mock";
|
|
3872
|
-
const connectors = options.connectors ?? [{
|
|
3873
|
-
id: "gmail",
|
|
3874
|
-
providerId,
|
|
3875
|
-
title: "Gmail",
|
|
3876
|
-
category: "email",
|
|
3877
|
-
auth: "oauth2",
|
|
3878
|
-
scopes: ["email.read", "email.write"],
|
|
3879
|
-
actions: [
|
|
3880
|
-
{ id: "messages.search", title: "Search messages", risk: "read", requiredScopes: ["email.read"], dataClass: "private" },
|
|
3881
|
-
{ id: "drafts.create", title: "Create draft", risk: "write", requiredScopes: ["email.write"], dataClass: "private", approvalRequired: true }
|
|
3882
|
-
],
|
|
3883
|
-
triggers: [
|
|
3884
|
-
{ id: "message.received", title: "Message received", requiredScopes: ["email.read"], dataClass: "private" }
|
|
3885
|
-
]
|
|
3886
|
-
}];
|
|
3887
|
-
return {
|
|
3888
|
-
id: providerId,
|
|
3889
|
-
kind: "custom",
|
|
3890
|
-
listConnectors: () => connectors,
|
|
3891
|
-
startAuth: (request) => ({
|
|
3892
|
-
providerId,
|
|
3893
|
-
connectorId: request.connectorId,
|
|
3894
|
-
authUrl: `https://auth.example.test/${request.connectorId}?state=${encodeURIComponent(request.state ?? "state")}`,
|
|
3895
|
-
state: request.state ?? "state"
|
|
3896
|
-
}),
|
|
3897
|
-
completeAuth: (request) => ({
|
|
3898
|
-
id: `conn_${request.connectorId}_${request.owner.id}`,
|
|
3899
|
-
owner: request.owner,
|
|
3900
|
-
providerId,
|
|
3901
|
-
connectorId: request.connectorId,
|
|
3902
|
-
status: "active",
|
|
3903
|
-
grantedScopes: connectors.find((connector) => connector.id === request.connectorId)?.scopes ?? [],
|
|
3904
|
-
secretRef: { provider: providerId, id: `secret_${request.owner.id}` },
|
|
3905
|
-
createdAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3906
|
-
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
3907
|
-
}),
|
|
3908
|
-
invokeAction: async (connection, request) => options.onInvoke?.(connection, request) ?? {
|
|
3909
|
-
ok: true,
|
|
3910
|
-
action: request.action,
|
|
3911
|
-
output: { echo: request.input ?? null }
|
|
3912
|
-
},
|
|
3913
|
-
subscribeTrigger: (connection, trigger2, targetUrl) => ({
|
|
3914
|
-
id: `sub_${connection.id}_${trigger2}`,
|
|
3915
|
-
connectionId: connection.id,
|
|
3916
|
-
trigger: trigger2,
|
|
3917
|
-
targetUrl,
|
|
3918
|
-
status: "active",
|
|
3919
|
-
createdAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
3920
|
-
})
|
|
3921
|
-
};
|
|
3922
|
-
}
|
|
3923
|
-
function createHttpIntegrationProvider(options) {
|
|
3924
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
3925
|
-
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
3926
|
-
return {
|
|
3927
|
-
id: options.id,
|
|
3928
|
-
kind: options.kind ?? "custom",
|
|
3929
|
-
listConnectors: () => options.connectors,
|
|
3930
|
-
async startAuth(request) {
|
|
3931
|
-
const response = await postJson(fetcher, `${baseUrl}/auth/start`, request, options.bearer);
|
|
3932
|
-
return response;
|
|
3933
|
-
},
|
|
3934
|
-
async completeAuth(request) {
|
|
3935
|
-
const response = await postJson(fetcher, `${baseUrl}/auth/complete`, request, options.bearer);
|
|
3936
|
-
return response;
|
|
3937
|
-
},
|
|
3938
|
-
async invokeAction(connection, request) {
|
|
3939
|
-
return postJson(fetcher, `${baseUrl}/actions/invoke`, {
|
|
3940
|
-
connection,
|
|
3941
|
-
request
|
|
3942
|
-
}, options.bearer);
|
|
3943
|
-
},
|
|
3944
|
-
async subscribeTrigger(connection, trigger2, targetUrl) {
|
|
3945
|
-
return postJson(fetcher, `${baseUrl}/triggers/subscribe`, {
|
|
3946
|
-
connection,
|
|
3947
|
-
trigger: trigger2,
|
|
3948
|
-
targetUrl
|
|
3949
|
-
}, options.bearer);
|
|
3950
|
-
},
|
|
3951
|
-
async unsubscribeTrigger(subscriptionId) {
|
|
3952
|
-
await postJson(fetcher, `${baseUrl}/triggers/unsubscribe`, { subscriptionId }, options.bearer);
|
|
3953
|
-
},
|
|
3954
|
-
async normalizeTriggerEvent(raw) {
|
|
3955
|
-
return postJson(fetcher, `${baseUrl}/triggers/normalize`, { raw }, options.bearer);
|
|
3956
|
-
}
|
|
3957
|
-
};
|
|
3958
|
-
}
|
|
3959
|
-
function signCapability(capability, secret) {
|
|
3960
|
-
const payload = base64UrlEncode2(JSON.stringify(capability));
|
|
3961
|
-
const signature = hmac(payload, secret);
|
|
3962
|
-
return `${payload}.${signature}`;
|
|
3963
|
-
}
|
|
3964
|
-
function verifyCapabilityToken(token, secret) {
|
|
3965
|
-
const [payload, signature] = token.split(".");
|
|
3966
|
-
if (!payload || !signature) throw new IntegrationError("Malformed integration capability.", "capability_invalid");
|
|
3967
|
-
const expected = hmac(payload, secret);
|
|
3968
|
-
if (!constantTimeEqual(signature, expected)) throw new IntegrationError("Invalid integration capability signature.", "capability_invalid");
|
|
3969
|
-
let parsed;
|
|
3970
|
-
try {
|
|
3971
|
-
parsed = JSON.parse(base64UrlDecode(payload));
|
|
3972
|
-
} catch {
|
|
3973
|
-
throw new IntegrationError("Invalid integration capability payload.", "capability_invalid");
|
|
3974
|
-
}
|
|
3975
|
-
if (!parsed.id || !parsed.connectionId || !Array.isArray(parsed.scopes) || !Array.isArray(parsed.allowedActions)) {
|
|
3976
|
-
throw new IntegrationError("Invalid integration capability payload.", "capability_invalid");
|
|
3977
|
-
}
|
|
3978
|
-
return parsed;
|
|
3979
|
-
}
|
|
3980
|
-
async function postJson(fetcher, url, body, bearer) {
|
|
3981
|
-
const response = await fetcher(url, {
|
|
3982
|
-
method: "POST",
|
|
3983
|
-
headers: {
|
|
3984
|
-
"Content-Type": "application/json",
|
|
3985
|
-
...bearer ? { Authorization: `Bearer ${bearer}` } : {}
|
|
3986
|
-
},
|
|
3987
|
-
body: JSON.stringify(body)
|
|
3988
|
-
});
|
|
3989
|
-
if (!response.ok) throw new IntegrationError(`Integration provider returned HTTP ${response.status}.`, "provider_not_found");
|
|
3990
|
-
return response.json();
|
|
3991
|
-
}
|
|
3992
|
-
function assertScopes(connection, requiredScopes2) {
|
|
3993
|
-
const missing2 = requiredScopes2.filter((scope) => !connection.grantedScopes.includes(scope));
|
|
3994
|
-
if (missing2.length > 0) throw new IntegrationError(`Missing integration scopes: ${missing2.join(", ")}`, "scope_denied");
|
|
3995
|
-
}
|
|
3996
|
-
function connectorOAuthPkceMode(connector) {
|
|
3997
|
-
const value = connector.metadata?.oauthPkce;
|
|
3998
|
-
if (value === void 0) return "required";
|
|
3999
|
-
if (value === "required" || value === "supported" || value === "unsupported") return value;
|
|
4000
|
-
throw new IntegrationError(
|
|
4001
|
-
`Connector ${connector.id} declares an invalid OAuth PKCE posture.`,
|
|
4002
|
-
"config_missing"
|
|
4003
|
-
);
|
|
4004
|
-
}
|
|
4005
|
-
function createPkcePair() {
|
|
4006
|
-
const verifier = randomBytes(48).toString("base64url");
|
|
4007
|
-
const challenge = createHash3("sha256").update(verifier).digest("base64url");
|
|
4008
|
-
return { verifier, challenge };
|
|
4009
|
-
}
|
|
4010
|
-
function hmac(payload, secret) {
|
|
4011
|
-
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
4012
|
-
}
|
|
4013
|
-
function constantTimeEqual(a, b) {
|
|
4014
|
-
const left = Buffer.from(a);
|
|
4015
|
-
const right = Buffer.from(b);
|
|
4016
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
4017
|
-
}
|
|
4018
|
-
function base64UrlEncode2(value) {
|
|
4019
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
4020
|
-
}
|
|
4021
|
-
function base64UrlDecode(value) {
|
|
4022
|
-
return Buffer.from(value, "base64url").toString("utf8");
|
|
4023
|
-
}
|
|
4024
|
-
function unique6(values) {
|
|
4025
|
-
return [...new Set(values)];
|
|
4026
|
-
}
|
|
4027
|
-
|
|
4028
|
-
// src/catalog-executor.ts
|
|
4029
|
-
function createCatalogExecutorProvider(options) {
|
|
4030
|
-
const byConnector = new Map(options.connectors.map((connector) => [connector.id, connector]));
|
|
4031
|
-
return {
|
|
4032
|
-
id: options.id,
|
|
4033
|
-
kind: options.kind,
|
|
4034
|
-
listConnectors: () => options.connectors,
|
|
4035
|
-
startAuth: options.startAuth,
|
|
4036
|
-
completeAuth: options.completeAuth,
|
|
4037
|
-
async invokeAction(connection, request) {
|
|
4038
|
-
const connector = byConnector.get(connection.connectorId);
|
|
4039
|
-
if (!connector) {
|
|
4040
|
-
throw new IntegrationError(`Connector ${connection.connectorId} not found.`, "connector_not_found");
|
|
4041
|
-
}
|
|
4042
|
-
const action = connector.actions.find((candidate) => candidate.id === request.action);
|
|
4043
|
-
if (!action) {
|
|
4044
|
-
throw new IntegrationError(`Action ${request.action} is not defined by connector ${connector.id}.`, "action_not_found");
|
|
4045
|
-
}
|
|
4046
|
-
return options.executeAction({ connection, request, connector, action });
|
|
4047
|
-
},
|
|
4048
|
-
async subscribeTrigger(connection, triggerId, targetUrl) {
|
|
4049
|
-
if (!options.subscribeTrigger) {
|
|
4050
|
-
throw new IntegrationError(`Provider ${options.id} does not support trigger subscriptions.`, "trigger_not_found");
|
|
4051
|
-
}
|
|
4052
|
-
const connector = byConnector.get(connection.connectorId);
|
|
4053
|
-
if (!connector) {
|
|
4054
|
-
throw new IntegrationError(`Connector ${connection.connectorId} not found.`, "connector_not_found");
|
|
4055
|
-
}
|
|
4056
|
-
const trigger2 = connector.triggers?.find((candidate) => candidate.id === triggerId);
|
|
4057
|
-
if (!trigger2) {
|
|
4058
|
-
throw new IntegrationError(`Trigger ${triggerId} is not defined by connector ${connector.id}.`, "trigger_not_found");
|
|
4059
|
-
}
|
|
4060
|
-
return options.subscribeTrigger(connection, trigger2, targetUrl);
|
|
4061
|
-
},
|
|
4062
|
-
unsubscribeTrigger: options.unsubscribeTrigger,
|
|
4063
|
-
normalizeTriggerEvent: options.normalizeTriggerEvent
|
|
4064
|
-
};
|
|
4065
|
-
}
|
|
4066
|
-
|
|
4067
|
-
// src/tangle-catalog.ts
|
|
4068
|
-
var TANGLE_INTEGRATIONS_CATALOG_PROVIDER_ID = "tangle-catalog";
|
|
4069
|
-
var TANGLE_INTEGRATIONS_CATALOG_SOURCE = "tangle-integrations-catalog";
|
|
4070
|
-
function listTangleNativeAdapterIds() {
|
|
4071
|
-
const ids = new Set(
|
|
4072
|
-
CONNECTOR_ADAPTER_FACTORIES.map(
|
|
4073
|
-
(definition) => definition.kind
|
|
4074
|
-
)
|
|
4075
|
-
);
|
|
4076
|
-
for (const adapter of listBundledConnectorAdapters()) {
|
|
4077
|
-
ids.add(adapter.manifest.kind);
|
|
4078
|
-
}
|
|
4079
|
-
return [...ids].sort();
|
|
4080
|
-
}
|
|
4081
|
-
function listTangleIntegrationCatalogEntries() {
|
|
4082
|
-
return listActivepiecesCatalogEntries().map((entry) => sanitizeEntry(entry));
|
|
4083
|
-
}
|
|
4084
|
-
function listTangleIntegrationContracts() {
|
|
4085
|
-
const nativeAdapterIds = new Set(listTangleNativeAdapterIds().map(canonicalIntegrationKind));
|
|
4086
|
-
return listActivepiecesCatalogEntries().map((entry) => {
|
|
4087
|
-
const nativeAdapter = nativeAdapterIds.has(canonicalIntegrationKind(entry.id));
|
|
4088
|
-
return {
|
|
4089
|
-
id: entry.id,
|
|
4090
|
-
title: entry.title,
|
|
4091
|
-
description: entry.description,
|
|
4092
|
-
category: entry.category,
|
|
4093
|
-
auth: entry.auth,
|
|
4094
|
-
authFields: entry.authFields ?? [],
|
|
4095
|
-
actions: entry.actions.map((action) => ({
|
|
4096
|
-
id: action.id,
|
|
4097
|
-
title: action.title,
|
|
4098
|
-
risk: action.risk,
|
|
4099
|
-
upstreamName: action.upstreamName ?? action.id
|
|
4100
|
-
})),
|
|
4101
|
-
triggers: entry.triggers.map((trigger2) => ({
|
|
4102
|
-
id: trigger2.id,
|
|
4103
|
-
title: trigger2.title,
|
|
4104
|
-
upstreamName: trigger2.upstreamName ?? trigger2.id
|
|
4105
|
-
})),
|
|
4106
|
-
implementation: {
|
|
4107
|
-
kind: nativeAdapter ? "native_adapter" : "package_runtime",
|
|
4108
|
-
runtimePackage: entry.npmPackage,
|
|
4109
|
-
version: entry.version
|
|
4110
|
-
},
|
|
4111
|
-
status: nativeAdapter ? "native_backed" : entry.npmPackage ? "runtime_backed" : "contract_ready",
|
|
4112
|
-
quality: {
|
|
4113
|
-
tangleContract: true,
|
|
4114
|
-
authFieldsMapped: entry.auth === "none" || Boolean(entry.authFields?.length),
|
|
4115
|
-
actionNamesMapped: entry.actions.every((action) => Boolean(action.upstreamName)),
|
|
4116
|
-
triggerNamesMapped: entry.triggers.every((trigger2) => Boolean(trigger2.upstreamName)),
|
|
4117
|
-
runtimePackageMapped: Boolean(entry.npmPackage),
|
|
4118
|
-
nativeAdapter
|
|
4119
|
-
}
|
|
4120
|
-
};
|
|
4121
|
-
});
|
|
4122
|
-
}
|
|
4123
|
-
function listTangleIntegrationCatalogRuntimePackages() {
|
|
4124
|
-
const nativeAdapterIds = new Set(listTangleNativeAdapterIds());
|
|
4125
|
-
return listActivepiecesCatalogEntries().filter((entry) => Boolean(entry.npmPackage) && !nativeAdapterIds.has(entry.id)).map((entry) => ({
|
|
4126
|
-
connectorId: entry.id,
|
|
4127
|
-
packageName: entry.npmPackage,
|
|
4128
|
-
version: entry.version
|
|
4129
|
-
}));
|
|
4130
|
-
}
|
|
4131
|
-
function buildTangleCatalogRuntimePackageManifest(options = {}) {
|
|
4132
|
-
const dependencies = {};
|
|
4133
|
-
if (options.includeAgentIntegrationsPackage ?? true) {
|
|
4134
|
-
dependencies["@tangle-network/agent-integrations"] = options.agentIntegrationsVersion ?? "latest";
|
|
4135
|
-
}
|
|
4136
|
-
for (const pkg of listTangleIntegrationCatalogRuntimePackages()) {
|
|
4137
|
-
dependencies[pkg.packageName] = pkg.version ?? "latest";
|
|
4138
|
-
}
|
|
4139
|
-
Object.assign(dependencies, options.additionalDependencies);
|
|
4140
|
-
return {
|
|
4141
|
-
name: options.name ?? "@tangle-network/agent-integrations-runtime-bundle",
|
|
4142
|
-
private: true,
|
|
4143
|
-
type: "module",
|
|
4144
|
-
dependencies: Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))),
|
|
4145
|
-
tangle: {
|
|
4146
|
-
integrationContracts: listTangleIntegrationContracts().length,
|
|
4147
|
-
packageRuntimeBackends: listTangleIntegrationContracts().filter((contract) => contract.implementation.kind === "package_runtime").length,
|
|
4148
|
-
generatedFrom: TANGLE_INTEGRATIONS_CATALOG_SOURCE
|
|
4149
|
-
}
|
|
4150
|
-
};
|
|
4151
|
-
}
|
|
4152
|
-
function renderTangleCatalogRuntimePnpmAddCommand(options = {}) {
|
|
4153
|
-
const manifest = buildTangleCatalogRuntimePackageManifest({
|
|
4154
|
-
includeAgentIntegrationsPackage: options.includeAgentIntegrationsPackage,
|
|
4155
|
-
agentIntegrationsVersion: options.agentIntegrationsVersion
|
|
4156
|
-
});
|
|
4157
|
-
return [
|
|
4158
|
-
"pnpm",
|
|
4159
|
-
"add",
|
|
4160
|
-
...Object.entries(manifest.dependencies).map(([name, version]) => `${name}@${version}`)
|
|
4161
|
-
].join(" ");
|
|
4162
|
-
}
|
|
4163
|
-
function buildTangleIntegrationCatalogConnectors(options = {}) {
|
|
4164
|
-
const providerId = options.providerId ?? TANGLE_INTEGRATIONS_CATALOG_PROVIDER_ID;
|
|
4165
|
-
return buildActivepiecesConnectors({
|
|
4166
|
-
...options,
|
|
4167
|
-
providerId
|
|
4168
|
-
}).map((connector) => sanitizeConnector(connector, providerId));
|
|
4169
|
-
}
|
|
4170
|
-
function createTangleCatalogExecutorProvider(options) {
|
|
4171
|
-
const providerId = options.id ?? TANGLE_INTEGRATIONS_CATALOG_PROVIDER_ID;
|
|
4172
|
-
const connectors = options.connectors ?? buildTangleIntegrationCatalogConnectors({
|
|
4173
|
-
providerId,
|
|
4174
|
-
includeCatalogActions: true,
|
|
4175
|
-
executable: true
|
|
4176
|
-
});
|
|
4177
|
-
const byEntry = new Map(listActivepiecesCatalogEntries().map((entry) => [entry.id, entry]));
|
|
4178
|
-
return createCatalogExecutorProvider({
|
|
4179
|
-
id: providerId,
|
|
4180
|
-
kind: "tangle_catalog",
|
|
4181
|
-
connectors,
|
|
4182
|
-
startAuth: options.startAuth,
|
|
4183
|
-
completeAuth: options.completeAuth,
|
|
4184
|
-
executeAction: async ({ connection, request, connector, action }) => {
|
|
4185
|
-
const importedEntry = byEntry.get(connector.id);
|
|
4186
|
-
if (!importedEntry) {
|
|
4187
|
-
throw new IntegrationError(`Tangle catalog entry ${connector.id} not found.`, "connector_not_found");
|
|
4188
|
-
}
|
|
4189
|
-
const catalogAction = importedEntry.actions.find((candidate) => candidate.id === action.id);
|
|
4190
|
-
return options.executeAction({
|
|
4191
|
-
connection,
|
|
4192
|
-
request,
|
|
4193
|
-
connector,
|
|
4194
|
-
catalogEntry: sanitizeEntry(importedEntry),
|
|
4195
|
-
piece: {
|
|
4196
|
-
id: importedEntry.id,
|
|
4197
|
-
packageName: importedEntry.npmPackage,
|
|
4198
|
-
version: importedEntry.version,
|
|
4199
|
-
actionId: action.id,
|
|
4200
|
-
upstreamActionName: catalogAction?.upstreamName
|
|
4201
|
-
}
|
|
4202
|
-
});
|
|
4203
|
-
},
|
|
4204
|
-
subscribeTrigger: options.subscribeTrigger ? async (connection, trigger2, targetUrl) => {
|
|
4205
|
-
const connector = connectors.find((candidate) => candidate.id === connection.connectorId);
|
|
4206
|
-
const importedEntry = byEntry.get(connection.connectorId);
|
|
4207
|
-
if (!connector || !importedEntry) {
|
|
4208
|
-
throw new IntegrationError(`Tangle catalog entry ${connection.connectorId} not found.`, "connector_not_found");
|
|
4209
|
-
}
|
|
4210
|
-
const catalogTrigger = importedEntry.triggers.find((candidate) => candidate.id === trigger2.id);
|
|
4211
|
-
return options.subscribeTrigger({
|
|
4212
|
-
connection,
|
|
4213
|
-
connector,
|
|
4214
|
-
catalogEntry: sanitizeEntry(importedEntry),
|
|
4215
|
-
trigger: trigger2,
|
|
4216
|
-
targetUrl,
|
|
4217
|
-
piece: {
|
|
4218
|
-
id: importedEntry.id,
|
|
4219
|
-
packageName: importedEntry.npmPackage,
|
|
4220
|
-
version: importedEntry.version,
|
|
4221
|
-
triggerId: trigger2.id,
|
|
4222
|
-
upstreamTriggerName: catalogTrigger?.upstreamName
|
|
4223
|
-
}
|
|
4224
|
-
});
|
|
4225
|
-
} : void 0,
|
|
4226
|
-
unsubscribeTrigger: options.unsubscribeTrigger,
|
|
4227
|
-
normalizeTriggerEvent: options.normalizeTriggerEvent
|
|
4228
|
-
});
|
|
4229
|
-
}
|
|
4230
|
-
var extractExternalCatalogPublicCount = extractActivepiecesPublicPieceCount;
|
|
4231
|
-
async function auditTangleIntegrationCatalogFreshness(options = {}) {
|
|
4232
|
-
const result = await auditIntegrationCatalogFreshness(options);
|
|
4233
|
-
return {
|
|
4234
|
-
ok: result.ok,
|
|
4235
|
-
generatedAt: result.generatedAt,
|
|
4236
|
-
local: {
|
|
4237
|
-
catalogEntries: result.local.activepiecesEntries,
|
|
4238
|
-
catalogConnectors: result.local.activepiecesConnectors,
|
|
4239
|
-
catalogActions: result.local.activepiecesActions,
|
|
4240
|
-
catalogTriggers: result.local.activepiecesTriggers,
|
|
4241
|
-
executableCatalogConnectors: result.local.executableActivepiecesConnectors,
|
|
4242
|
-
executableCatalogActions: result.local.executableActivepiecesActions,
|
|
4243
|
-
executableCatalogTriggers: result.local.executableActivepiecesTriggers,
|
|
4244
|
-
executableToolDefinitions: result.local.executableToolDefinitions,
|
|
4245
|
-
unsupportedExecutableConnectorIds: result.local.unsupportedExecutableConnectorIds,
|
|
4246
|
-
registryEntries: result.local.registryEntries,
|
|
4247
|
-
registrySummary: result.local.registrySummary,
|
|
4248
|
-
conflictSamples: result.local.conflictSamples
|
|
4249
|
-
},
|
|
4250
|
-
upstream: result.upstream ? {
|
|
4251
|
-
externalEntries: result.upstream.activepiecesPieces,
|
|
4252
|
-
externalDelta: result.upstream.activepiecesDelta,
|
|
4253
|
-
checkedUrl: result.upstream.checkedUrl,
|
|
4254
|
-
warning: result.upstream.warning
|
|
4255
|
-
} : void 0,
|
|
4256
|
-
warnings: result.warnings.map((warning) => warning.replaceAll("Activepieces", "Tangle Integrations Catalog"))
|
|
4257
|
-
};
|
|
4258
|
-
}
|
|
4259
|
-
function sanitizeEntry(entry) {
|
|
4260
|
-
return {
|
|
4261
|
-
id: entry.id,
|
|
4262
|
-
title: entry.title,
|
|
4263
|
-
description: entry.description,
|
|
4264
|
-
category: entry.category,
|
|
4265
|
-
auth: entry.auth,
|
|
4266
|
-
authFields: entry.authFields,
|
|
4267
|
-
domains: entry.domains.filter((domain) => !domain.toLowerCase().includes("activepieces")),
|
|
4268
|
-
actions: entry.actions.map((action) => ({
|
|
4269
|
-
id: action.id,
|
|
4270
|
-
title: action.title,
|
|
4271
|
-
risk: action.risk,
|
|
4272
|
-
upstreamName: action.upstreamName
|
|
4273
|
-
})),
|
|
4274
|
-
triggers: entry.triggers.map((trigger2) => ({
|
|
4275
|
-
id: trigger2.id,
|
|
4276
|
-
title: trigger2.title,
|
|
4277
|
-
upstreamName: trigger2.upstreamName
|
|
4278
|
-
}))
|
|
4279
|
-
};
|
|
4280
|
-
}
|
|
4281
|
-
function sanitizeConnector(connector, providerId) {
|
|
4282
|
-
const metadata = connector.metadata ?? {};
|
|
4283
|
-
return {
|
|
4284
|
-
...connector,
|
|
4285
|
-
providerId,
|
|
4286
|
-
metadata: {
|
|
4287
|
-
source: TANGLE_INTEGRATIONS_CATALOG_SOURCE,
|
|
4288
|
-
providerId,
|
|
4289
|
-
executable: metadata.executable,
|
|
4290
|
-
runtime: "native-adapter-backlog",
|
|
4291
|
-
catalogOnly: metadata.catalogOnly,
|
|
4292
|
-
supportTier: metadata.supportTier,
|
|
4293
|
-
catalogActionCount: metadata.catalogActionCount,
|
|
4294
|
-
catalogTriggerCount: metadata.catalogTriggerCount,
|
|
4295
|
-
license: metadata.license,
|
|
4296
|
-
version: metadata.version,
|
|
4297
|
-
domains: Array.isArray(metadata.domains) ? metadata.domains.filter((domain) => typeof domain === "string" && !domain.toLowerCase().includes("activepieces")) : void 0,
|
|
4298
|
-
...metadata.overridden ? { overridden: true } : {}
|
|
4299
|
-
}
|
|
4300
|
-
};
|
|
4301
|
-
}
|
|
4302
|
-
|
|
4303
|
-
// src/registry.ts
|
|
4304
|
-
var DEFAULT_ALIASES = INTEGRATION_KIND_ALIASES;
|
|
4305
|
-
var DEFAULT_SOURCE_PRECEDENCE = {
|
|
4306
|
-
"first-party": 500,
|
|
4307
|
-
spec: 400,
|
|
4308
|
-
gateway: 300,
|
|
4309
|
-
"tangle-catalog": 100,
|
|
4310
|
-
activepieces: 100,
|
|
4311
|
-
coverage: 50
|
|
4312
|
-
};
|
|
4313
|
-
var SUPPORT_RANK = {
|
|
4314
|
-
catalogOnly: 0,
|
|
4315
|
-
setupReady: 1,
|
|
4316
|
-
gatewayExecutable: 2,
|
|
4317
|
-
firstPartyExecutable: 3,
|
|
4318
|
-
sandboxExecutable: 4
|
|
4319
|
-
};
|
|
4320
|
-
function buildDefaultIntegrationRegistry(options = {}) {
|
|
4321
|
-
const includeSpecs = options.includeSpecs ?? true;
|
|
4322
|
-
const includeTangleCatalog = options.includeTangleCatalog ?? options.includeActivepieces ?? true;
|
|
4323
|
-
const sources = [];
|
|
4324
|
-
if (includeSpecs) {
|
|
4325
|
-
sources.push({
|
|
4326
|
-
id: "spec",
|
|
4327
|
-
connectors: listIntegrationSpecs().map((spec) => integrationSpecToConnector(spec, "spec"))
|
|
4328
|
-
});
|
|
4329
|
-
}
|
|
4330
|
-
if (includeTangleCatalog) {
|
|
4331
|
-
const tangleConnectors = options.tangleCatalogRuntimeExecutable ? buildTangleIntegrationCatalogConnectors({
|
|
4332
|
-
providerId: "tangle-catalog",
|
|
4333
|
-
includeCatalogActions: true,
|
|
4334
|
-
executable: true
|
|
4335
|
-
}) : buildActivepiecesConnectors({ providerId: "tangle-catalog" }).map((connector) => ({
|
|
4336
|
-
...connector,
|
|
4337
|
-
providerId: "tangle-catalog",
|
|
4338
|
-
metadata: {
|
|
4339
|
-
source: "tangle-integrations-catalog",
|
|
4340
|
-
providerId: "tangle-catalog",
|
|
4341
|
-
executable: connector.metadata?.executable,
|
|
4342
|
-
runtime: "native-adapter-backlog",
|
|
4343
|
-
catalogOnly: connector.metadata?.catalogOnly,
|
|
4344
|
-
supportTier: connector.metadata?.supportTier,
|
|
4345
|
-
catalogActionCount: connector.metadata?.catalogActionCount,
|
|
4346
|
-
catalogTriggerCount: connector.metadata?.catalogTriggerCount,
|
|
4347
|
-
license: connector.metadata?.license,
|
|
4348
|
-
version: connector.metadata?.version,
|
|
4349
|
-
domains: Array.isArray(connector.metadata?.domains) ? connector.metadata.domains.filter((domain) => typeof domain === "string" && !domain.toLowerCase().includes("activepieces")) : void 0,
|
|
4350
|
-
...connector.metadata?.overridden ? { overridden: true } : {}
|
|
4351
|
-
}
|
|
4352
|
-
}));
|
|
4353
|
-
sources.push({
|
|
4354
|
-
id: "tangle-catalog",
|
|
4355
|
-
connectors: tangleConnectors
|
|
4356
|
-
});
|
|
4357
|
-
}
|
|
4358
|
-
return composeIntegrationRegistry(sources);
|
|
4359
|
-
}
|
|
4360
|
-
function classifyIntegrationCatalogExecutability(registry = buildDefaultIntegrationRegistry()) {
|
|
4361
|
-
const packageByConnector = new Map(
|
|
4362
|
-
listActivepiecesCatalogEntries().filter((entry) => entry.npmPackage).map((entry) => [entry.id, entry.npmPackage])
|
|
4363
|
-
);
|
|
4364
|
-
return registry.entries.map((entry) => {
|
|
4365
|
-
const runtimePackage = packageByConnector.get(entry.connector.id);
|
|
4366
|
-
const tierExecutable = entry.supportTier === "firstPartyExecutable" || entry.supportTier === "sandboxExecutable" || entry.supportTier === "gatewayExecutable";
|
|
4367
|
-
return {
|
|
4368
|
-
canonicalId: entry.canonicalId,
|
|
4369
|
-
executable: tierExecutable && entry.connector.actions.length > 0,
|
|
4370
|
-
supportTier: entry.supportTier,
|
|
4371
|
-
authKind: entry.connector.auth,
|
|
4372
|
-
runtimePackage,
|
|
4373
|
-
actionCount: entry.connector.actions.length,
|
|
4374
|
-
triggerCount: entry.connector.triggers?.length ?? 0
|
|
4375
|
-
};
|
|
4376
|
-
});
|
|
4377
|
-
}
|
|
4378
|
-
function composeIntegrationRegistry(sources, options = {}) {
|
|
4379
|
-
const aliases = { ...DEFAULT_ALIASES, ...options.aliases ?? {} };
|
|
4380
|
-
const precedence = { ...DEFAULT_SOURCE_PRECEDENCE, ...options.sourcePrecedence ?? {} };
|
|
4381
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
4382
|
-
for (const source of sources) {
|
|
4383
|
-
for (const connector of source.connectors) {
|
|
4384
|
-
const canonicalId = canonicalConnectorId(connector.id, aliases);
|
|
4385
|
-
const candidates = grouped.get(canonicalId) ?? [];
|
|
4386
|
-
candidates.push({
|
|
4387
|
-
source,
|
|
4388
|
-
connector,
|
|
4389
|
-
supportTier: inferIntegrationSupportTier(connector)
|
|
4390
|
-
});
|
|
4391
|
-
grouped.set(canonicalId, candidates);
|
|
4392
|
-
}
|
|
4393
|
-
}
|
|
4394
|
-
const entries = [...grouped.entries()].map(([canonicalId, candidates]) => registryEntry(canonicalId, candidates, precedence, aliases)).sort((a, b) => a.canonicalId.localeCompare(b.canonicalId));
|
|
4395
|
-
const byId = /* @__PURE__ */ new Map();
|
|
4396
|
-
for (const entry of entries) {
|
|
4397
|
-
byId.set(entry.canonicalId, entry);
|
|
4398
|
-
for (const alias of entry.aliases) byId.set(alias, entry);
|
|
4399
|
-
}
|
|
4400
|
-
return {
|
|
4401
|
-
entries,
|
|
4402
|
-
connectors: entries.map((entry) => entry.connector),
|
|
4403
|
-
byId
|
|
4404
|
-
};
|
|
4405
|
-
}
|
|
4406
|
-
function summarizeIntegrationRegistry(registry) {
|
|
4407
|
-
const bySupportTier = {
|
|
4408
|
-
catalogOnly: 0,
|
|
4409
|
-
setupReady: 0,
|
|
4410
|
-
gatewayExecutable: 0,
|
|
4411
|
-
firstPartyExecutable: 0,
|
|
4412
|
-
sandboxExecutable: 0
|
|
4413
|
-
};
|
|
4414
|
-
for (const entry of registry.entries) bySupportTier[entry.supportTier] += 1;
|
|
4415
|
-
return {
|
|
4416
|
-
totalEntries: registry.entries.length,
|
|
4417
|
-
totalSources: registry.entries.reduce((sum, entry) => sum + entry.sources.length, 0),
|
|
4418
|
-
toolBindableEntries: registry.entries.filter((entry) => entry.connector.actions.length > 0).length,
|
|
4419
|
-
conflictEntries: registry.entries.filter((entry) => entry.conflicts.length > 0).length,
|
|
4420
|
-
bySupportTier
|
|
4421
|
-
};
|
|
4422
|
-
}
|
|
4423
|
-
function canonicalConnectorId(id, aliases = DEFAULT_ALIASES) {
|
|
4424
|
-
const normalized = slug2(id);
|
|
4425
|
-
let current = normalized;
|
|
4426
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4427
|
-
while (aliases[current] && !seen.has(current)) {
|
|
4428
|
-
seen.add(current);
|
|
4429
|
-
current = aliases[current];
|
|
4430
|
-
}
|
|
4431
|
-
return current;
|
|
4432
|
-
}
|
|
4433
|
-
function inferIntegrationSupportTier(connector) {
|
|
4434
|
-
const metadata = connector.metadata ?? {};
|
|
4435
|
-
const explicit = metadata.supportTier;
|
|
4436
|
-
if (isSupportTier(explicit)) return explicit;
|
|
4437
|
-
if (metadata.sandboxExecutable === true) return "sandboxExecutable";
|
|
4438
|
-
if (metadata.source === "first-party-adapter" || connector.providerId === "first-party") return "firstPartyExecutable";
|
|
4439
|
-
if (metadata.source === "gateway-catalog" && metadata.executable === true) return "gatewayExecutable";
|
|
4440
|
-
if (metadata.source === "integration-spec") return "setupReady";
|
|
4441
|
-
if (metadata.source === "coverage-catalog" || metadata.source === "activepieces-community" || metadata.source === "tangle-integrations-catalog" || metadata.catalogOnly === true) return "catalogOnly";
|
|
4442
|
-
if (connector.actions.length > 0) return "gatewayExecutable";
|
|
4443
|
-
return "catalogOnly";
|
|
4444
|
-
}
|
|
4445
|
-
function registryEntry(canonicalId, candidates, precedence, aliases) {
|
|
4446
|
-
const ordered = [...candidates].sort((a, b) => compareCandidates(a, b, precedence));
|
|
4447
|
-
const primary = ordered[0];
|
|
4448
|
-
const actions = mergeActions(ordered);
|
|
4449
|
-
const triggers = mergeTriggers(ordered);
|
|
4450
|
-
const scopes = unique7(toolBindableCandidates(ordered).flatMap((candidate) => candidate.connector.scopes ?? []));
|
|
4451
|
-
const supportTier = ordered.reduce(
|
|
4452
|
-
(best, candidate) => SUPPORT_RANK[candidate.supportTier] > SUPPORT_RANK[best] ? candidate.supportTier : best,
|
|
4453
|
-
primary.supportTier
|
|
4454
|
-
);
|
|
4455
|
-
const aliasesForEntry = unique7([
|
|
4456
|
-
...ordered.map((candidate) => candidate.connector.id),
|
|
4457
|
-
...Object.entries(aliases).filter(([, target]) => canonicalConnectorId(target, aliases) === canonicalId).map(([alias]) => alias)
|
|
4458
|
-
].map(slug2).filter((id) => id && id !== canonicalId)).sort();
|
|
4459
|
-
const sources = ordered.map((candidate) => ({
|
|
4460
|
-
sourceId: candidate.source.id,
|
|
4461
|
-
providerId: candidate.connector.providerId,
|
|
4462
|
-
connectorId: candidate.connector.id,
|
|
4463
|
-
supportTier: candidate.supportTier,
|
|
4464
|
-
actionCount: candidate.connector.actions.length,
|
|
4465
|
-
triggerCount: candidate.connector.triggers?.length ?? 0
|
|
4466
|
-
}));
|
|
4467
|
-
const conflicts = conflictDiagnostics(ordered);
|
|
4468
|
-
return {
|
|
4469
|
-
canonicalId,
|
|
4470
|
-
aliases: aliasesForEntry,
|
|
4471
|
-
supportTier,
|
|
4472
|
-
sources,
|
|
4473
|
-
conflicts,
|
|
4474
|
-
connector: {
|
|
4475
|
-
...primary.connector,
|
|
4476
|
-
id: canonicalId,
|
|
4477
|
-
scopes,
|
|
4478
|
-
actions,
|
|
4479
|
-
triggers,
|
|
4480
|
-
metadata: {
|
|
4481
|
-
...primary.connector.metadata ?? {},
|
|
4482
|
-
registry: {
|
|
4483
|
-
canonicalId,
|
|
4484
|
-
aliases: aliasesForEntry,
|
|
4485
|
-
supportTier,
|
|
4486
|
-
sources,
|
|
4487
|
-
conflicts,
|
|
4488
|
-
toolBindable: actions.length > 0,
|
|
4489
|
-
catalogOnlyActionCount: ordered.filter((candidate) => candidate.supportTier === "catalogOnly").reduce((sum, candidate) => sum + catalogActionCount(candidate.connector), 0)
|
|
4490
|
-
}
|
|
4491
|
-
}
|
|
4492
|
-
}
|
|
4493
|
-
};
|
|
4494
|
-
}
|
|
4495
|
-
function compareCandidates(a, b, precedence) {
|
|
4496
|
-
return SUPPORT_RANK[b.supportTier] - SUPPORT_RANK[a.supportTier] || (b.source.precedence ?? precedence[b.source.id] ?? 0) - (a.source.precedence ?? precedence[a.source.id] ?? 0) || b.connector.actions.length - a.connector.actions.length || a.connector.id.localeCompare(b.connector.id);
|
|
4497
|
-
}
|
|
4498
|
-
function mergeActions(candidates) {
|
|
4499
|
-
const out = /* @__PURE__ */ new Map();
|
|
4500
|
-
for (const candidate of toolBindableCandidates(candidates)) {
|
|
4501
|
-
for (const action of candidate.connector.actions) {
|
|
4502
|
-
if (!out.has(action.id)) out.set(action.id, action);
|
|
4503
|
-
}
|
|
4504
|
-
}
|
|
4505
|
-
return [...out.values()];
|
|
4506
|
-
}
|
|
4507
|
-
function mergeTriggers(candidates) {
|
|
4508
|
-
const out = /* @__PURE__ */ new Map();
|
|
4509
|
-
for (const candidate of toolBindableCandidates(candidates)) {
|
|
4510
|
-
for (const trigger2 of candidate.connector.triggers ?? []) {
|
|
4511
|
-
if (!out.has(trigger2.id)) out.set(trigger2.id, trigger2);
|
|
4512
|
-
}
|
|
4513
|
-
}
|
|
4514
|
-
return out.size > 0 ? [...out.values()] : void 0;
|
|
4515
|
-
}
|
|
4516
|
-
function toolBindableCandidates(candidates) {
|
|
4517
|
-
const bindable = candidates.filter((candidate) => candidate.supportTier !== "catalogOnly");
|
|
4518
|
-
if (bindable.length === 0) return [];
|
|
4519
|
-
const maxRank = Math.max(...bindable.map((candidate) => SUPPORT_RANK[candidate.supportTier]));
|
|
4520
|
-
return bindable.filter((candidate) => SUPPORT_RANK[candidate.supportTier] === maxRank);
|
|
4521
|
-
}
|
|
4522
|
-
function catalogActionCount(connector) {
|
|
4523
|
-
const value = connector.metadata?.catalogActionCount;
|
|
4524
|
-
return typeof value === "number" ? value : connector.actions.length;
|
|
4525
|
-
}
|
|
4526
|
-
function conflictDiagnostics(candidates) {
|
|
4527
|
-
return [
|
|
4528
|
-
conflictFor("auth", candidates.map((candidate) => ({
|
|
4529
|
-
value: candidate.connector.auth,
|
|
4530
|
-
sourceId: candidate.source.id,
|
|
4531
|
-
connectorId: candidate.connector.id
|
|
4532
|
-
}))),
|
|
4533
|
-
conflictFor("category", candidates.map((candidate) => ({
|
|
4534
|
-
value: candidate.connector.category,
|
|
4535
|
-
sourceId: candidate.source.id,
|
|
4536
|
-
connectorId: candidate.connector.id
|
|
4537
|
-
})))
|
|
4538
|
-
].filter((conflict) => Boolean(conflict));
|
|
4539
|
-
}
|
|
4540
|
-
function conflictFor(field, values) {
|
|
4541
|
-
const uniqueValues = new Set(values.map((entry) => entry.value));
|
|
4542
|
-
if (uniqueValues.size <= 1) return void 0;
|
|
4543
|
-
return { field, values };
|
|
4544
|
-
}
|
|
4545
|
-
function isSupportTier(value) {
|
|
4546
|
-
return value === "catalogOnly" || value === "setupReady" || value === "gatewayExecutable" || value === "firstPartyExecutable" || value === "sandboxExecutable";
|
|
4547
|
-
}
|
|
4548
|
-
function slug2(value) {
|
|
4549
|
-
return value.trim().toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4550
|
-
}
|
|
4551
|
-
function unique7(values) {
|
|
4552
|
-
return [...new Set(values)];
|
|
4553
|
-
}
|
|
4554
|
-
|
|
4555
|
-
// src/catalog.ts
|
|
4556
|
-
var riskRank2 = {
|
|
4557
|
-
read: 0,
|
|
4558
|
-
write: 1,
|
|
4559
|
-
destructive: 2
|
|
4560
|
-
};
|
|
4561
|
-
function integrationToolName(providerId, connectorId, actionId) {
|
|
4562
|
-
return `int_${encodeToolPart(providerId)}_${encodeToolPart(connectorId)}_${encodeToolPart(actionId)}`;
|
|
4563
|
-
}
|
|
4564
|
-
function parseIntegrationToolName(name) {
|
|
4565
|
-
const parts = name.split("_");
|
|
4566
|
-
if (parts.length !== 4 || parts[0] !== "int") {
|
|
4567
|
-
throw new Error(`Invalid integration tool name: ${name}`);
|
|
4568
|
-
}
|
|
4569
|
-
return {
|
|
4570
|
-
providerId: decodeToolPart(parts[1]),
|
|
4571
|
-
connectorId: decodeToolPart(parts[2]),
|
|
4572
|
-
actionId: decodeToolPart(parts[3])
|
|
4573
|
-
};
|
|
4574
|
-
}
|
|
4575
|
-
function buildIntegrationToolCatalog(connectors) {
|
|
4576
|
-
const tools = [];
|
|
4577
|
-
for (const connector of connectors) {
|
|
4578
|
-
for (const action of connector.actions) {
|
|
4579
|
-
tools.push(flattenIntegrationToolDefinition(connector, action));
|
|
4580
|
-
}
|
|
4581
|
-
}
|
|
4582
|
-
return tools;
|
|
4583
|
-
}
|
|
4584
|
-
function flattenIntegrationToolDefinition(connector, action) {
|
|
4585
|
-
const tags = unique8([
|
|
4586
|
-
connector.id,
|
|
4587
|
-
connector.providerId,
|
|
4588
|
-
connector.title,
|
|
4589
|
-
connector.category,
|
|
4590
|
-
action.id,
|
|
4591
|
-
action.title,
|
|
4592
|
-
action.risk,
|
|
4593
|
-
action.dataClass,
|
|
4594
|
-
...connector.scopes ?? [],
|
|
4595
|
-
...action.requiredScopes ?? []
|
|
4596
|
-
].flatMap(tokenize));
|
|
4597
|
-
return {
|
|
4598
|
-
name: integrationToolName(connector.providerId, connector.id, action.id),
|
|
4599
|
-
title: `${connector.title}: ${action.title}`,
|
|
4600
|
-
description: action.description ?? `${action.risk} action ${action.id} on ${connector.title}`,
|
|
4601
|
-
providerId: connector.providerId,
|
|
4602
|
-
connectorId: connector.id,
|
|
4603
|
-
connectorTitle: connector.title,
|
|
4604
|
-
category: connector.category,
|
|
4605
|
-
action,
|
|
4606
|
-
risk: action.risk,
|
|
4607
|
-
dataClass: action.dataClass,
|
|
4608
|
-
requiredScopes: action.requiredScopes,
|
|
4609
|
-
inputSchema: action.inputSchema,
|
|
4610
|
-
outputSchema: action.outputSchema,
|
|
4611
|
-
tags,
|
|
4612
|
-
supportTier: toolSupportTier(connector),
|
|
4613
|
-
runnable: toolRunnable(connector)
|
|
4614
|
-
};
|
|
4615
|
-
}
|
|
4616
|
-
function describeIntegrationTool(registry, toolName) {
|
|
4617
|
-
let parsed;
|
|
4618
|
-
try {
|
|
4619
|
-
parsed = parseIntegrationToolName(toolName);
|
|
4620
|
-
} catch {
|
|
4621
|
-
return void 0;
|
|
4622
|
-
}
|
|
4623
|
-
const entry = registry.byId.get(parsed.connectorId);
|
|
4624
|
-
if (!entry) return void 0;
|
|
4625
|
-
const action = entry.connector.actions.find((candidate) => candidate.id === parsed.actionId);
|
|
4626
|
-
if (!action) return void 0;
|
|
4627
|
-
return flattenIntegrationToolDefinition(entry.connector, action);
|
|
4628
|
-
}
|
|
4629
|
-
function buildIntegrationCatalogView(input) {
|
|
4630
|
-
const runtimeConnectors = input.executableRegistry?.connectors ?? input.discoveryRegistry.connectors.filter((connector) => toolRunnable(connector));
|
|
4631
|
-
const runtimeTools = buildIntegrationToolCatalog(runtimeConnectors);
|
|
4632
|
-
const discoveryTools = buildIntegrationToolCatalog(input.discoveryRegistry.connectors);
|
|
4633
|
-
return {
|
|
4634
|
-
connectors: input.discoveryRegistry.connectors,
|
|
4635
|
-
conflicts: input.discoveryRegistry.entries.flatMap((entry) => entry.conflicts),
|
|
4636
|
-
summary: summarizeIntegrationRegistry(input.discoveryRegistry),
|
|
4637
|
-
tools: runtimeTools,
|
|
4638
|
-
runtimeTools,
|
|
4639
|
-
discoveryTools
|
|
4640
|
-
};
|
|
4641
|
-
}
|
|
4642
|
-
function searchIntegrationTools(catalog, query, filters = {}) {
|
|
4643
|
-
const terms = tokenize(query);
|
|
4644
|
-
const filtered = catalog.filter((tool) => {
|
|
4645
|
-
if (filters.providerId && tool.providerId !== filters.providerId) return false;
|
|
4646
|
-
if (filters.connectorId && tool.connectorId !== filters.connectorId) return false;
|
|
4647
|
-
if (filters.category && tool.category !== filters.category) return false;
|
|
4648
|
-
if (filters.dataClass && tool.dataClass !== filters.dataClass) return false;
|
|
4649
|
-
if (filters.maxRisk && riskRank2[tool.risk] > riskRank2[filters.maxRisk]) return false;
|
|
4650
|
-
return true;
|
|
4651
|
-
});
|
|
4652
|
-
const scored = filtered.map((tool) => scoreTool(tool, terms));
|
|
4653
|
-
return scored.filter((result) => terms.length === 0 || result.score > 0).sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name)).slice(0, filters.limit ?? 20);
|
|
4654
|
-
}
|
|
4655
|
-
function toMcpTools(tools) {
|
|
4656
|
-
return tools.map((tool) => ({
|
|
4657
|
-
name: tool.name,
|
|
4658
|
-
description: `${tool.title}. ${tool.description}`,
|
|
4659
|
-
inputSchema: tool.inputSchema ?? {
|
|
4660
|
-
type: "object",
|
|
4661
|
-
additionalProperties: true,
|
|
4662
|
-
properties: {}
|
|
4663
|
-
}
|
|
4664
|
-
}));
|
|
4665
|
-
}
|
|
4666
|
-
function scoreTool(tool, terms) {
|
|
4667
|
-
if (terms.length === 0) return { tool, score: 1, matched: [] };
|
|
4668
|
-
const haystack = new Set(tool.tags);
|
|
4669
|
-
const identity = new Set(
|
|
4670
|
-
[tool.connectorId, tool.connectorTitle, tool.providerId].flatMap(tokenize)
|
|
4671
|
-
);
|
|
4672
|
-
const matched = [];
|
|
4673
|
-
let score = 0;
|
|
4674
|
-
for (const term of terms) {
|
|
4675
|
-
if (identity.has(term)) {
|
|
4676
|
-
matched.push(term);
|
|
4677
|
-
score += 6;
|
|
4678
|
-
continue;
|
|
4679
|
-
}
|
|
4680
|
-
if (haystack.has(term)) {
|
|
4681
|
-
matched.push(term);
|
|
4682
|
-
score += 4;
|
|
4683
|
-
continue;
|
|
4684
|
-
}
|
|
4685
|
-
if (tool.tags.some((tag) => tag.includes(term))) {
|
|
4686
|
-
matched.push(term);
|
|
4687
|
-
score += 1;
|
|
4688
|
-
}
|
|
4689
|
-
}
|
|
4690
|
-
if (tool.risk === "read") score += 0.25;
|
|
4691
|
-
return { tool, score, matched: unique8(matched) };
|
|
4692
|
-
}
|
|
4693
|
-
function tokenize(value) {
|
|
4694
|
-
return value.toLowerCase().split(/[^a-z0-9]+/g).map((part) => part.trim()).filter(Boolean);
|
|
4695
|
-
}
|
|
4696
|
-
function encodeToolPart(value) {
|
|
4697
|
-
return Buffer.from(value, "utf8").toString("base64url").replace(/_/g, ".");
|
|
4698
|
-
}
|
|
4699
|
-
function decodeToolPart(value) {
|
|
4700
|
-
return Buffer.from(value.replace(/\./g, "_"), "base64url").toString("utf8");
|
|
4701
|
-
}
|
|
4702
|
-
function unique8(values) {
|
|
4703
|
-
return [...new Set(values)];
|
|
4704
|
-
}
|
|
4705
|
-
function toolSupportTier(connector) {
|
|
4706
|
-
const registry = connector.metadata?.registry;
|
|
4707
|
-
if (registry && typeof registry === "object" && !Array.isArray(registry)) {
|
|
4708
|
-
const supportTier2 = registry.supportTier;
|
|
4709
|
-
return isIntegrationSupportTier(supportTier2) ? supportTier2 : void 0;
|
|
4710
|
-
}
|
|
4711
|
-
const supportTier = connector.metadata?.supportTier;
|
|
4712
|
-
return isIntegrationSupportTier(supportTier) ? supportTier : void 0;
|
|
4713
|
-
}
|
|
4714
|
-
function toolRunnable(connector) {
|
|
4715
|
-
const registry = connector.metadata?.registry;
|
|
4716
|
-
if (registry && typeof registry === "object" && !Array.isArray(registry)) {
|
|
4717
|
-
const toolBindable = registry.toolBindable;
|
|
4718
|
-
if (typeof toolBindable === "boolean") return toolBindable;
|
|
4719
|
-
}
|
|
4720
|
-
const tier = toolSupportTier(connector);
|
|
4721
|
-
return tier === "gatewayExecutable" || tier === "firstPartyExecutable" || tier === "sandboxExecutable";
|
|
4722
|
-
}
|
|
4723
|
-
function isIntegrationSupportTier(value) {
|
|
4724
|
-
return value === "catalogOnly" || value === "setupReady" || value === "gatewayExecutable" || value === "firstPartyExecutable" || value === "sandboxExecutable";
|
|
4725
|
-
}
|
|
4726
|
-
|
|
4727
|
-
export {
|
|
4728
|
-
ACTIVEPIECES_OVERRIDES,
|
|
4729
|
-
getActivepiecesOverride,
|
|
4730
|
-
listActivepiecesCatalogEntries,
|
|
4731
|
-
buildActivepiecesConnectors,
|
|
4732
|
-
createCatalogExecutorProvider,
|
|
4733
|
-
createActivepiecesExecutorProvider,
|
|
4734
|
-
integrationToolName,
|
|
4735
|
-
parseIntegrationToolName,
|
|
4736
|
-
buildIntegrationToolCatalog,
|
|
4737
|
-
flattenIntegrationToolDefinition,
|
|
4738
|
-
describeIntegrationTool,
|
|
4739
|
-
buildIntegrationCatalogView,
|
|
4740
|
-
searchIntegrationTools,
|
|
4741
|
-
toMcpTools,
|
|
4742
|
-
ACTIVEPIECES_PUBLIC_CATALOG_URL,
|
|
4743
|
-
extractActivepiecesPublicPieceCount,
|
|
4744
|
-
auditIntegrationCatalogFreshness,
|
|
4745
|
-
TANGLE_INTEGRATIONS_CATALOG_PROVIDER_ID,
|
|
4746
|
-
TANGLE_INTEGRATIONS_CATALOG_SOURCE,
|
|
4747
|
-
listTangleNativeAdapterIds,
|
|
4748
|
-
listTangleIntegrationCatalogEntries,
|
|
4749
|
-
listTangleIntegrationContracts,
|
|
4750
|
-
listTangleIntegrationCatalogRuntimePackages,
|
|
4751
|
-
buildTangleCatalogRuntimePackageManifest,
|
|
4752
|
-
renderTangleCatalogRuntimePnpmAddCommand,
|
|
4753
|
-
buildTangleIntegrationCatalogConnectors,
|
|
4754
|
-
createTangleCatalogExecutorProvider,
|
|
4755
|
-
extractExternalCatalogPublicCount,
|
|
4756
|
-
auditTangleIntegrationCatalogFreshness,
|
|
4757
|
-
buildDefaultIntegrationRegistry,
|
|
4758
|
-
classifyIntegrationCatalogExecutability,
|
|
4759
|
-
composeIntegrationRegistry,
|
|
4760
|
-
summarizeIntegrationRegistry,
|
|
4761
|
-
canonicalConnectorId,
|
|
4762
|
-
inferIntegrationSupportTier,
|
|
4763
|
-
IntegrationError,
|
|
4764
|
-
createConnectorAdapterProvider,
|
|
4765
|
-
adapterManifestsToConnectors,
|
|
4766
|
-
createConnectorAdapterCatalogSource,
|
|
4767
|
-
manifestToConnector,
|
|
4768
|
-
InMemoryIntegrationSecretStore,
|
|
4769
|
-
InMemoryIntegrationOAuthStateStore,
|
|
4770
|
-
createConnectionCredentialResolver,
|
|
4771
|
-
resolveConnectionCredentials,
|
|
4772
|
-
createCredentialBackedAdapterProvider,
|
|
4773
|
-
revokeConnection,
|
|
4774
|
-
InMemoryIntegrationAuditStore,
|
|
4775
|
-
createIntegrationAuditEvent,
|
|
4776
|
-
createAuditingActionGuard,
|
|
4777
|
-
sanitizeAuditConnection,
|
|
4778
|
-
InMemoryIntegrationApprovalStore,
|
|
4779
|
-
ApprovalBackedPolicyEngine,
|
|
4780
|
-
createApprovalBackedPolicyEngine,
|
|
4781
|
-
resolveIntegrationApproval,
|
|
4782
|
-
CANONICAL_INTEGRATION_ACTIONS,
|
|
4783
|
-
buildCanonicalLaunchConnectors,
|
|
4784
|
-
canonicalActionConnectorId,
|
|
4785
|
-
DEFAULT_INTEGRATION_BRIDGE_ENV,
|
|
4786
|
-
buildIntegrationBridgePayload,
|
|
4787
|
-
encodeIntegrationBridgePayload,
|
|
4788
|
-
decodeIntegrationBridgePayload,
|
|
4789
|
-
buildIntegrationBridgeEnvironment,
|
|
4790
|
-
parseIntegrationBridgeEnvironment,
|
|
4791
|
-
redactIntegrationBridgePayload,
|
|
4792
|
-
TangleAppsClient,
|
|
4793
|
-
createTangleAppsClient,
|
|
4794
|
-
TangleIntegrationsClient,
|
|
4795
|
-
createTangleIntegrationsClient,
|
|
4796
|
-
renderConsentSummary,
|
|
4797
|
-
renderApprovalCopy,
|
|
4798
|
-
discoverWorkspaceCapabilities,
|
|
4799
|
-
filterDiscoveryByWorkspaceScopes,
|
|
4800
|
-
InMemoryIntegrationEventStore,
|
|
4801
|
-
receiveIntegrationWebhook,
|
|
4802
|
-
storedEventToTriggerEvent,
|
|
4803
|
-
InMemoryIntegrationIdempotencyStore,
|
|
4804
|
-
DefaultIntegrationActionGuard,
|
|
4805
|
-
createDefaultIntegrationActionGuard,
|
|
4806
|
-
InMemoryIntegrationHealthcheckStore,
|
|
4807
|
-
runIntegrationHealthcheck,
|
|
4808
|
-
runIntegrationHealthchecks,
|
|
4809
|
-
healthcheckRequest,
|
|
4810
|
-
validateIntegrationManifest,
|
|
4811
|
-
assertValidIntegrationManifest,
|
|
4812
|
-
inferIntegrationManifestFromTools,
|
|
4813
|
-
explainMissingRequirements,
|
|
4814
|
-
calendarExercisePlannerManifest,
|
|
4815
|
-
PROVIDER_PASSTHROUGH_ACTION,
|
|
4816
|
-
validateProviderPassthroughRequest,
|
|
4817
|
-
StaticIntegrationPolicyEngine,
|
|
4818
|
-
createDefaultIntegrationPolicyEngine,
|
|
4819
|
-
buildApprovalRequest,
|
|
4820
|
-
redactApprovalRequest,
|
|
4821
|
-
createPlatformIntegrationPolicyPreset,
|
|
4822
|
-
buildIntegrationInvocationEnvelope,
|
|
4823
|
-
invocationRequestFromEnvelope,
|
|
4824
|
-
validateIntegrationInvocationEnvelope,
|
|
4825
|
-
redactInvocationEnvelope,
|
|
4826
|
-
redactCapability,
|
|
4827
|
-
normalizeIntegrationResult,
|
|
4828
|
-
dispatchIntegrationInvocation,
|
|
4829
|
-
IntegrationSandboxHost,
|
|
4830
|
-
importOpenApiConnector,
|
|
4831
|
-
importGraphqlConnector,
|
|
4832
|
-
importMcpConnector,
|
|
4833
|
-
createGatewayCatalogProvider,
|
|
4834
|
-
normalizeGatewayCatalog,
|
|
4835
|
-
InMemoryIntegrationGrantStore,
|
|
4836
|
-
IntegrationRuntime,
|
|
4837
|
-
createIntegrationRuntime,
|
|
4838
|
-
InMemoryIntegrationWorkflowStore,
|
|
4839
|
-
IntegrationWorkflowRuntime,
|
|
4840
|
-
createIntegrationWorkflowRuntime,
|
|
4841
|
-
InMemoryConnectionStore,
|
|
4842
|
-
IntegrationHub,
|
|
4843
|
-
sanitizeConnection,
|
|
4844
|
-
createMockIntegrationProvider,
|
|
4845
|
-
createHttpIntegrationProvider,
|
|
4846
|
-
signCapability,
|
|
4847
|
-
verifyCapabilityToken
|
|
4848
|
-
};
|
|
4849
|
-
//# sourceMappingURL=chunk-ZC3ZE7TN.js.map
|