@tangle-network/agent-integrations 0.59.0 → 0.59.2
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 +31 -0
- package/data/bundled-adapter-manifests.json +1 -1
- package/dist/{adapters-ediOgE75.js → adapters-4-7nkFS3.js} +137 -383
- package/dist/{adapters-ediOgE75.js.map → adapters-4-7nkFS3.js.map} +1 -1
- package/dist/{bundled-manifest-data-DQXscV3-.js → bundled-manifest-data-ka9qth1e.js} +72 -51
- package/dist/{bundled-manifest-data-DQXscV3-.js.map → bundled-manifest-data-ka9qth1e.js.map} +1 -1
- package/dist/catalog-CFvKzwVj.js +201 -0
- package/dist/catalog-CFvKzwVj.js.map +1 -0
- package/dist/catalog.js +1 -1
- package/dist/connect/index.js +1 -1
- package/dist/connectors/adapters/index.js +1 -1
- package/dist/connectors/index.d.ts +2 -1
- package/dist/connectors/index.js +3 -2
- package/dist/connectors/oauth.d.ts +2 -0
- package/dist/connectors/oauth.js +282 -0
- package/dist/connectors/oauth.js.map +1 -0
- package/dist/{consumer-BC6pCUzI.d.ts → consumer-zHtoJixe.d.ts} +5 -37
- package/dist/{consumer-BC6pCUzI.d.ts.map → consumer-zHtoJixe.d.ts.map} +1 -1
- package/dist/consumer.d.ts +1 -1
- package/dist/hub-0Od2lgun.d.ts +38 -0
- package/dist/hub-0Od2lgun.d.ts.map +1 -0
- package/dist/hub-D13zPQGr.js +1004 -0
- package/dist/hub-D13zPQGr.js.map +1 -0
- package/dist/hub.d.ts +2 -0
- package/dist/hub.js +2 -0
- package/dist/index-BQXENE9p.d.ts +130 -0
- package/dist/index-BQXENE9p.d.ts.map +1 -0
- package/dist/index-CDVPZuIY.d.ts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +16 -12
- package/dist/internal/bundled-manifest-runtime.js +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +2 -1
- package/dist/mcp.js.map +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/oauth-C5pa5j3O.d.ts +136 -0
- package/dist/oauth-C5pa5j3O.d.ts.map +1 -0
- package/dist/registry-core-BZo69C37.js +186 -0
- package/dist/registry-core-BZo69C37.js.map +1 -0
- package/dist/registry.js +2 -2
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +114 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/{specs-BkhKJNjX.js → specs-C2qsaxDe.js} +2 -2
- package/dist/{specs-BkhKJNjX.js.map → specs-C2qsaxDe.js.map} +1 -1
- package/dist/specs.js +1 -1
- package/dist/{src-Bd4KMI-J.js → src-mzICXbwx.js} +84922 -86026
- package/dist/src-mzICXbwx.js.map +1 -0
- package/dist/stripe/index.js +1 -1
- package/dist/webhooks/index.js +1 -1
- package/package.json +17 -2
- package/dist/catalog-CBd0zbdD.js +0 -383
- package/dist/catalog-CBd0zbdD.js.map +0 -1
- package/dist/index-j2QziJA5.d.ts +0 -263
- package/dist/index-j2QziJA5.d.ts.map +0 -1
- package/dist/src-Bd4KMI-J.js.map +0 -1
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
import { createOAuthBasicAuthorizationHeader, createOAuthTokenRequestHeaders, oauthClientCredentialRedactionValues, redactOAuthSensitiveText } from "./connectors/oauth.js";
|
|
2
|
+
import { n as composeIntegrationRegistry } from "./registry-core-BZo69C37.js";
|
|
3
|
+
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
|
+
//#region src/core-error.ts
|
|
5
|
+
var IntegrationError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(message, code) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = "IntegrationError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/adapter-provider.ts
|
|
15
|
+
function createConnectorAdapterProvider(options) {
|
|
16
|
+
const providerId = options.id ?? "first-party";
|
|
17
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
18
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch?.bind(globalThis);
|
|
19
|
+
const adapters = /* @__PURE__ */ new Map();
|
|
20
|
+
for (const adapter of options.adapters) adapters.set(adapter.manifest.kind, adapter);
|
|
21
|
+
return {
|
|
22
|
+
id: providerId,
|
|
23
|
+
kind: options.kind ?? "first_party",
|
|
24
|
+
listConnectors: () => [...adapters.values()].map((adapter) => manifestToConnector(providerId, adapter)),
|
|
25
|
+
async startAuth(request) {
|
|
26
|
+
const adapter = adapters.get(request.connectorId);
|
|
27
|
+
if (!adapter) throw new IntegrationError(`Connector adapter ${request.connectorId} not found.`, "connector_not_found");
|
|
28
|
+
const auth = oauthManifestAuth(adapter.manifest.auth);
|
|
29
|
+
if (!auth) throw new IntegrationError(`Connector ${request.connectorId} does not support OAuth2 authorization (auth kind: ${adapter.manifest.auth.kind}).`, "auth_not_supported");
|
|
30
|
+
if ((auth.grantType ?? "authorization_code") !== "authorization_code") throw new IntegrationError(`Connector ${request.connectorId} uses client_credentials and requires a machine connection path.`, "auth_not_supported");
|
|
31
|
+
const pkce = resolveOAuthPkceMode(auth.pkce, request.connectorId);
|
|
32
|
+
const tokenClientAuthMethod = resolveTokenClientAuthMethod(auth.tokenClientAuthMethod, request.connectorId);
|
|
33
|
+
if (!options.resolveOAuthClient) throw new IntegrationError(`OAuth client resolver missing on adapter provider; cannot start auth for ${request.connectorId}.`, "config_missing");
|
|
34
|
+
const client = await options.resolveOAuthClient({ connectorId: request.connectorId });
|
|
35
|
+
if (!client || !client.clientId || tokenClientAuthMethod !== "none" && !client.clientSecret) throw new IntegrationError(`OAuth client credentials unavailable for ${request.connectorId}.`, "config_missing");
|
|
36
|
+
if (!auth.authorizationUrl) throw new IntegrationError(`Connector ${request.connectorId} uses the ${auth.grantType ?? "authorization_code"} grant and has no authorization URL to redirect to.`, "auth_not_supported");
|
|
37
|
+
const scopes = request.requestedScopes && request.requestedScopes.length > 0 ? request.requestedScopes : auth.scopes;
|
|
38
|
+
const url = new URL(resolveOAuthUrlTemplate(auth.authorizationUrl, request.metadata, request.connectorId, auth.urlTemplateMetadata));
|
|
39
|
+
url.searchParams.set("response_type", "code");
|
|
40
|
+
url.searchParams.set(resolveOAuthParameterName(auth.authorizationClientIdParam, "client_id", request.connectorId), client.clientId);
|
|
41
|
+
url.searchParams.set("redirect_uri", request.redirectUri);
|
|
42
|
+
if (auth.sendScopeParam !== false && scopes.length > 0) url.searchParams.set("scope", scopes.join(auth.scopeSeparator ?? " "));
|
|
43
|
+
const state = request.state ?? randomState();
|
|
44
|
+
url.searchParams.set("state", state);
|
|
45
|
+
if (auth.extraAuthParams) for (const [key, value] of Object.entries(auth.extraAuthParams)) url.searchParams.set(key, value);
|
|
46
|
+
if (pkce === "unsupported") {
|
|
47
|
+
url.searchParams.delete("code_challenge");
|
|
48
|
+
url.searchParams.delete("code_challenge_method");
|
|
49
|
+
} else {
|
|
50
|
+
url.searchParams.set("code_challenge", requirePkceChallenge(request.codeChallenge, request.connectorId));
|
|
51
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
providerId,
|
|
55
|
+
connectorId: request.connectorId,
|
|
56
|
+
authUrl: url.toString(),
|
|
57
|
+
state,
|
|
58
|
+
metadata: request.metadata
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
async completeAuth(request) {
|
|
62
|
+
const adapter = adapters.get(request.connectorId);
|
|
63
|
+
if (!adapter) throw new IntegrationError(`Connector adapter ${request.connectorId} not found.`, "connector_not_found");
|
|
64
|
+
const auth = oauthManifestAuth(adapter.manifest.auth);
|
|
65
|
+
if (!auth) throw new IntegrationError(`Connector ${request.connectorId} does not support OAuth2 authorization (auth kind: ${adapter.manifest.auth.kind}).`, "auth_not_supported");
|
|
66
|
+
if ((auth.grantType ?? "authorization_code") !== "authorization_code") throw new IntegrationError(`Connector ${request.connectorId} uses client_credentials and requires a machine connection path.`, "auth_not_supported");
|
|
67
|
+
const tokenClientAuthMethod = resolveTokenClientAuthMethod(auth.tokenClientAuthMethod, request.connectorId);
|
|
68
|
+
const pkce = resolveOAuthPkceMode(auth.pkce, request.connectorId);
|
|
69
|
+
if (!request.code) throw new IntegrationError(`Authorization code missing on completeAuth for ${request.connectorId}.`, "config_missing");
|
|
70
|
+
if (!options.resolveOAuthClient) throw new IntegrationError(`OAuth client resolver missing on adapter provider; cannot complete auth for ${request.connectorId}.`, "config_missing");
|
|
71
|
+
const client = await options.resolveOAuthClient({ connectorId: request.connectorId });
|
|
72
|
+
if (!client || !client.clientId || tokenClientAuthMethod !== "none" && !client.clientSecret) throw new IntegrationError(`OAuth client credentials unavailable for ${request.connectorId}.`, "config_missing");
|
|
73
|
+
if (!fetchImpl) throw new IntegrationError("No fetch implementation available; inject fetchImpl into createConnectorAdapterProvider.", "config_missing");
|
|
74
|
+
const body = new URLSearchParams({
|
|
75
|
+
grant_type: "authorization_code",
|
|
76
|
+
code: request.code,
|
|
77
|
+
redirect_uri: request.redirectUri
|
|
78
|
+
});
|
|
79
|
+
const codeVerifier = pkce === "unsupported" ? void 0 : requirePkceVerifier(request.codeVerifier, request.connectorId);
|
|
80
|
+
if (codeVerifier) body.set("code_verifier", codeVerifier);
|
|
81
|
+
const headers = createOAuthTokenRequestHeaders(auth.tokenRequestHeaders);
|
|
82
|
+
if (tokenClientAuthMethod === "none") body.set(resolveOAuthParameterName(auth.tokenClientIdParam, "client_id", request.connectorId), client.clientId);
|
|
83
|
+
else if (tokenClientAuthMethod === "client_secret_post") {
|
|
84
|
+
body.set(resolveOAuthParameterName(auth.tokenClientIdParam, "client_id", request.connectorId), client.clientId);
|
|
85
|
+
body.set(resolveOAuthParameterName(auth.tokenClientSecretParam, "client_secret", request.connectorId), client.clientSecret);
|
|
86
|
+
} else headers.authorization = createOAuthBasicAuthorizationHeader(client.clientId, client.clientSecret, base64Encode);
|
|
87
|
+
const redactionValues = [
|
|
88
|
+
request.code,
|
|
89
|
+
...codeVerifier ? [codeVerifier] : [],
|
|
90
|
+
...oauthClientCredentialRedactionValues(client.clientId, client.clientSecret, headers.authorization)
|
|
91
|
+
];
|
|
92
|
+
const tokenUrl = resolveOAuthUrlTemplate(auth.tokenUrl, request.metadata, request.connectorId, auth.urlTemplateMetadata);
|
|
93
|
+
let res;
|
|
94
|
+
try {
|
|
95
|
+
res = await fetchImpl(tokenUrl, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers,
|
|
98
|
+
body
|
|
99
|
+
});
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
const detail = redactOAuthSensitiveText(cause?.message ?? "unknown", redactionValues);
|
|
102
|
+
throw new IntegrationError(`OAuth token exchange transport error for ${request.connectorId}: ${detail}`, "provider_failure");
|
|
103
|
+
}
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
const text = await res.text().catch(() => "");
|
|
106
|
+
const statusText = redactOAuthSensitiveText(res.statusText, redactionValues);
|
|
107
|
+
const redactedText = redactOAuthSensitiveText(text, redactionValues);
|
|
108
|
+
const detail = `${res.status} ${statusText} — ${redactedText.slice(0, 200)}`;
|
|
109
|
+
throw new IntegrationError(`OAuth token exchange failed for ${request.connectorId}: ${detail}`, "provider_failure");
|
|
110
|
+
}
|
|
111
|
+
let json;
|
|
112
|
+
try {
|
|
113
|
+
json = await res.json();
|
|
114
|
+
} catch {
|
|
115
|
+
throw new IntegrationError(`OAuth token exchange returned non-JSON body for ${request.connectorId}.`, "provider_failure");
|
|
116
|
+
}
|
|
117
|
+
if (!json.access_token) throw new IntegrationError(`OAuth token exchange returned no access_token for ${request.connectorId}.`, "provider_failure");
|
|
118
|
+
const grantedScopes = typeof json.scope === "string" && json.scope.length > 0 ? json.scope.split(/[\s,]+/).filter(Boolean) : [];
|
|
119
|
+
const issued = now();
|
|
120
|
+
const issuedIso = issued.toISOString();
|
|
121
|
+
const expiresAt = typeof json.expires_in === "number" && json.expires_in > 0 ? new Date(issued.getTime() + json.expires_in * 1e3).toISOString() : void 0;
|
|
122
|
+
const capturedMetadata = captureTokenMetadata(auth.tokenMetadata, json, request.connectorId);
|
|
123
|
+
return {
|
|
124
|
+
id: randomConnectionId(),
|
|
125
|
+
owner: request.owner,
|
|
126
|
+
providerId,
|
|
127
|
+
connectorId: request.connectorId,
|
|
128
|
+
status: "active",
|
|
129
|
+
grantedScopes,
|
|
130
|
+
createdAt: issuedIso,
|
|
131
|
+
updatedAt: issuedIso,
|
|
132
|
+
expiresAt,
|
|
133
|
+
metadata: {
|
|
134
|
+
...request.metadata,
|
|
135
|
+
...capturedMetadata
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
async invokeAction(connection, request) {
|
|
140
|
+
const adapter = adapters.get(connection.connectorId);
|
|
141
|
+
if (!adapter) throw new IntegrationError(`Connector adapter ${connection.connectorId} not found.`, "connector_not_found");
|
|
142
|
+
const capability = adapter.manifest.capabilities.find((candidate) => candidate.name === request.action);
|
|
143
|
+
if (!capability) throw new IntegrationError(`Capability ${request.action} is not defined by ${connection.connectorId}.`, "action_not_found");
|
|
144
|
+
const source = await options.resolveDataSource(connection);
|
|
145
|
+
let rotationPersistence = Promise.resolve();
|
|
146
|
+
const persistRotation = (credentials) => {
|
|
147
|
+
if (!options.onCredentialsRotated) return Promise.resolve();
|
|
148
|
+
rotationPersistence = rotationPersistence.then(() => options.onCredentialsRotated({
|
|
149
|
+
connection,
|
|
150
|
+
credentials
|
|
151
|
+
}));
|
|
152
|
+
rotationPersistence.catch(() => {});
|
|
153
|
+
return rotationPersistence;
|
|
154
|
+
};
|
|
155
|
+
const invocation = {
|
|
156
|
+
source,
|
|
157
|
+
capabilityName: request.action,
|
|
158
|
+
args: toRecord(request.input),
|
|
159
|
+
idempotencyKey: request.idempotencyKey ?? `idem_${connection.id}_${request.action}_${now().getTime()}`,
|
|
160
|
+
expectedEtag: typeof request.metadata?.expectedEtag === "string" ? request.metadata.expectedEtag : void 0,
|
|
161
|
+
callSessionId: typeof request.metadata?.callSessionId === "string" ? request.metadata.callSessionId : void 0,
|
|
162
|
+
onCredentialsRotated: options.onCredentialsRotated ? persistRotation : void 0
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
if (capability.class === "read") {
|
|
166
|
+
if (!adapter.executeRead) throw new IntegrationError(`Connector ${connection.connectorId} does not implement reads.`, "action_not_found");
|
|
167
|
+
return readResultToAction(request, await adapter.executeRead(invocation));
|
|
168
|
+
}
|
|
169
|
+
if (capability.class === "mutation") {
|
|
170
|
+
if (!adapter.executeMutation) throw new IntegrationError(`Connector ${connection.connectorId} does not implement mutations.`, "action_not_found");
|
|
171
|
+
return mutationResultToAction(request, await adapter.executeMutation(invocation));
|
|
172
|
+
}
|
|
173
|
+
throw new IntegrationError(`Capability ${request.action} is not invokable as an action.`, "action_not_found");
|
|
174
|
+
} finally {
|
|
175
|
+
await rotationPersistence;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function adapterManifestsToConnectors(adapters, providerId = "first-party") {
|
|
181
|
+
return adapters.map((adapter) => manifestToConnector(providerId, adapter));
|
|
182
|
+
}
|
|
183
|
+
function createConnectorAdapterCatalogSource(options) {
|
|
184
|
+
const sourceId = options.id ?? "first-party";
|
|
185
|
+
return {
|
|
186
|
+
id: sourceId,
|
|
187
|
+
precedence: options.precedence,
|
|
188
|
+
connectors: adapterManifestsToConnectors(options.adapters, options.providerId ?? sourceId)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function manifestToConnector(providerId, adapter) {
|
|
192
|
+
const manifest = adapter.manifest;
|
|
193
|
+
const primaryAuth = primaryManifestAuth(manifest.auth);
|
|
194
|
+
const oauthAuth = oauthManifestAuth(manifest.auth);
|
|
195
|
+
return {
|
|
196
|
+
id: manifest.kind,
|
|
197
|
+
providerId,
|
|
198
|
+
title: manifest.displayName,
|
|
199
|
+
category: mapCategory(manifest.category),
|
|
200
|
+
auth: mapAuth(primaryAuth.kind),
|
|
201
|
+
scopes: primaryAuth.kind === "oauth2" ? primaryAuth.scopes : [],
|
|
202
|
+
actions: manifest.capabilities.filter((capability) => capability.class === "read" || capability.class === "mutation").map((capability) => ({
|
|
203
|
+
id: capability.name,
|
|
204
|
+
title: titleFromName(capability.name),
|
|
205
|
+
risk: capability.class === "read" ? "read" : capability.externalEffect ? "destructive" : "write",
|
|
206
|
+
requiredScopes: capability.requiredScopes ?? [],
|
|
207
|
+
dataClass: inferDataClass(manifest.category),
|
|
208
|
+
...capability.consistencyModel ? { consistencyModel: capability.consistencyModel } : {},
|
|
209
|
+
description: capability.description,
|
|
210
|
+
approvalRequired: capability.class === "mutation",
|
|
211
|
+
inputSchema: capability.parameters
|
|
212
|
+
})),
|
|
213
|
+
metadata: {
|
|
214
|
+
source: "first-party-adapter",
|
|
215
|
+
supportTier: "firstPartyExecutable",
|
|
216
|
+
executable: true,
|
|
217
|
+
oauthPkce: oauthAuth ? resolveOAuthPkceMode(oauthAuth.pkce, manifest.kind) : void 0,
|
|
218
|
+
authOptions: manifest.auth.kind === "one_of" ? manifest.auth.options.map((auth) => auth.kind) : [manifest.auth.kind],
|
|
219
|
+
preferredAuth: manifest.auth.kind === "one_of" ? manifest.auth.preferred : manifest.auth.kind
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function readResultToAction(request, result) {
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
action: request.action,
|
|
227
|
+
output: result.data,
|
|
228
|
+
metadata: {
|
|
229
|
+
etag: result.etag,
|
|
230
|
+
fetchedAt: result.fetchedAt
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function mutationResultToAction(request, result) {
|
|
235
|
+
if (result.status === "committed") return {
|
|
236
|
+
ok: true,
|
|
237
|
+
action: request.action,
|
|
238
|
+
output: result.data,
|
|
239
|
+
metadata: {
|
|
240
|
+
etagAfter: result.etagAfter,
|
|
241
|
+
committedAt: result.committedAt,
|
|
242
|
+
idempotentReplay: result.idempotentReplay
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
if (result.status === "conflict") return {
|
|
246
|
+
ok: false,
|
|
247
|
+
action: request.action,
|
|
248
|
+
output: {
|
|
249
|
+
conflict: true,
|
|
250
|
+
message: result.message,
|
|
251
|
+
alternatives: result.alternatives,
|
|
252
|
+
currentState: result.currentState
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
action: request.action,
|
|
258
|
+
output: {
|
|
259
|
+
rateLimited: true,
|
|
260
|
+
retryAfterMs: result.retryAfterMs,
|
|
261
|
+
message: result.message
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function mapAuth(kind) {
|
|
266
|
+
if (kind === "one_of") return "custom";
|
|
267
|
+
if (kind === "oauth2") return "oauth2";
|
|
268
|
+
if (kind === "api-key") return "api_key";
|
|
269
|
+
if (kind === "none") return "none";
|
|
270
|
+
return "custom";
|
|
271
|
+
}
|
|
272
|
+
function primaryManifestAuth(auth) {
|
|
273
|
+
if (auth.kind !== "one_of") return auth;
|
|
274
|
+
return auth.options.find((option) => option.kind === auth.preferred) ?? auth.options[0];
|
|
275
|
+
}
|
|
276
|
+
function oauthManifestAuth(auth) {
|
|
277
|
+
if (auth.kind === "oauth2") return auth;
|
|
278
|
+
if (auth.kind !== "one_of") return void 0;
|
|
279
|
+
return auth.options.find((option) => option.kind === "oauth2");
|
|
280
|
+
}
|
|
281
|
+
/** Resolve the manifest value at the last possible point before an OAuth
|
|
282
|
+
* request. The runtime check matters because adapters can enter through JS or
|
|
283
|
+
* an `any` cast without first passing the static manifest validator. */
|
|
284
|
+
function resolveTokenClientAuthMethod(value, connectorId) {
|
|
285
|
+
if (value === void 0 || value === "client_secret_post") return "client_secret_post";
|
|
286
|
+
if (value === "none" || value === "client_secret_basic") return value;
|
|
287
|
+
throw new IntegrationError(`Connector ${connectorId} has an unsupported OAuth token client authentication method.`, "config_missing");
|
|
288
|
+
}
|
|
289
|
+
function resolveOAuthPkceMode(value, connectorId) {
|
|
290
|
+
if (value === void 0 || value === "required") return "required";
|
|
291
|
+
if (value === "supported" || value === "unsupported") return value;
|
|
292
|
+
throw new IntegrationError(`Connector ${connectorId} has an invalid OAuth PKCE posture.`, "config_missing");
|
|
293
|
+
}
|
|
294
|
+
function requirePkceChallenge(value, connectorId) {
|
|
295
|
+
if (!value || !/^[A-Za-z0-9_-]{43}$/.test(value)) throw new IntegrationError(`OAuth PKCE S256 challenge missing or invalid for ${connectorId}.`, "config_missing");
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
function requirePkceVerifier(value, connectorId) {
|
|
299
|
+
if (!value || !/^[A-Za-z0-9._~-]{43,128}$/.test(value)) throw new IntegrationError(`OAuth PKCE verifier missing or invalid for ${connectorId}.`, "config_missing");
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
function resolveOAuthParameterName(value, fallback, connectorId) {
|
|
303
|
+
const name = value ?? fallback;
|
|
304
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(name)) throw new IntegrationError(`Connector ${connectorId} declares an invalid OAuth parameter name.`, "config_missing");
|
|
305
|
+
return name;
|
|
306
|
+
}
|
|
307
|
+
/** Encodes UTF-8 credentials without importing Node's Buffer, so the adapter
|
|
308
|
+
* provider remains usable in browser and edge runtimes. */
|
|
309
|
+
function base64Encode(value) {
|
|
310
|
+
const btoa = globalThis.btoa;
|
|
311
|
+
if (typeof btoa !== "function") throw new IntegrationError("OAuth client_secret_basic requires a base64 encoder in this runtime.", "config_missing");
|
|
312
|
+
const bytes = new TextEncoder().encode(value);
|
|
313
|
+
let binary = "";
|
|
314
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
315
|
+
return btoa(binary);
|
|
316
|
+
}
|
|
317
|
+
/** Resolve provider-owned tenant labels embedded in OAuth URLs.
|
|
318
|
+
*
|
|
319
|
+
* Shopify and Gorgias put the merchant name in the hostname. Only one DNS
|
|
320
|
+
* label is accepted for each placeholder, so connection metadata cannot turn
|
|
321
|
+
* a provider URL into an arbitrary credential destination.
|
|
322
|
+
*/
|
|
323
|
+
function resolveOAuthUrlTemplate(template, metadata, connectorId, metadataSpecs) {
|
|
324
|
+
const placeholders = [...template.matchAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g)];
|
|
325
|
+
let resolved = template;
|
|
326
|
+
for (const match of placeholders) {
|
|
327
|
+
const key = match[1];
|
|
328
|
+
const raw = metadata?.[key];
|
|
329
|
+
const metadataSpec = metadataSpecs?.[key];
|
|
330
|
+
if (metadataSpec?.kind === "base-url") {
|
|
331
|
+
if (typeof raw !== "string") throw new IntegrationError(`OAuth URL for ${connectorId} requires metadata.${key} as a provider base URL.`, "config_missing");
|
|
332
|
+
resolved = resolved.replaceAll(match[0], resolveOAuthMetadataBaseUrl(raw, metadataSpec, connectorId, key));
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (metadataSpec?.kind === "path-segment") {
|
|
336
|
+
if (!isOAuthPathSegmentPlaceholder(template, match.index ?? -1, match[0])) throw new IntegrationError(`OAuth URL for ${connectorId} declares metadata.${key} outside a complete path segment.`, "config_missing");
|
|
337
|
+
if (typeof raw !== "string" || !isOAuthPathSegment(raw.trim())) throw new IntegrationError(`OAuth URL for ${connectorId} requires metadata.${key} as a valid path segment.`, "config_missing");
|
|
338
|
+
resolved = resolved.replaceAll(match[0], encodeURIComponent(raw.trim()));
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (typeof raw !== "string" || !isDnsLabel(raw.trim())) throw new IntegrationError(`OAuth URL for ${connectorId} requires metadata.${key} as a valid tenant label.`, "config_missing");
|
|
342
|
+
resolved = resolved.replaceAll(match[0], raw.trim().toLowerCase());
|
|
343
|
+
}
|
|
344
|
+
let url;
|
|
345
|
+
try {
|
|
346
|
+
url = new URL(resolved);
|
|
347
|
+
} catch {
|
|
348
|
+
throw new IntegrationError(`OAuth URL for ${connectorId} is invalid.`, "config_missing");
|
|
349
|
+
}
|
|
350
|
+
if (url.protocol !== "https:" || url.username || url.password) throw new IntegrationError(`OAuth URL for ${connectorId} must be HTTPS without embedded credentials.`, "config_missing");
|
|
351
|
+
return url.toString();
|
|
352
|
+
}
|
|
353
|
+
function isOAuthPathSegment(value) {
|
|
354
|
+
return value !== "." && value !== ".." && /^[A-Za-z0-9._~-]{1,128}$/.test(value);
|
|
355
|
+
}
|
|
356
|
+
function isOAuthPathSegmentPlaceholder(template, index, placeholder) {
|
|
357
|
+
if (index < 0) return false;
|
|
358
|
+
const schemeIndex = template.indexOf("://");
|
|
359
|
+
const rootEnd = schemeIndex >= 0 ? schemeIndex + 3 : template.startsWith("{") ? template.indexOf("}") + 1 : 0;
|
|
360
|
+
const pathStart = template.indexOf("/", rootEnd);
|
|
361
|
+
const end = index + placeholder.length;
|
|
362
|
+
const queryStart = template.search(/[?#]/);
|
|
363
|
+
return pathStart >= 0 && index >= pathStart && (queryStart < 0 || index < queryStart) && template[index - 1] === "/" && (end === template.length || template[end] === "/" || template[end] === "?" || template[end] === "#");
|
|
364
|
+
}
|
|
365
|
+
function resolveOAuthMetadataBaseUrl(value, spec, connectorId, metadataKey) {
|
|
366
|
+
let url;
|
|
367
|
+
try {
|
|
368
|
+
url = new URL(value.trim());
|
|
369
|
+
} catch {
|
|
370
|
+
throw invalidOAuthMetadataBaseUrl(connectorId, metadataKey);
|
|
371
|
+
}
|
|
372
|
+
if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw invalidOAuthMetadataBaseUrl(connectorId, metadataKey);
|
|
373
|
+
const exactAllowed = spec.allowedBaseUrls?.some((candidate) => sameOrigin(candidate, url)) ?? false;
|
|
374
|
+
const suffixAllowed = spec.allowedBaseUrlSuffixes?.some((suffix) => !url.port && suffix.startsWith(".") && url.hostname.toLowerCase().endsWith(suffix.toLowerCase())) ?? false;
|
|
375
|
+
if (Boolean(spec.allowedBaseUrls?.length || spec.allowedBaseUrlSuffixes?.length) && !exactAllowed && !suffixAllowed) throw invalidOAuthMetadataBaseUrl(connectorId, metadataKey);
|
|
376
|
+
if (spec.requirePublicHttps && !isPublicDnsHostname(url.hostname)) throw invalidOAuthMetadataBaseUrl(connectorId, metadataKey);
|
|
377
|
+
return url.origin;
|
|
378
|
+
}
|
|
379
|
+
function invalidOAuthMetadataBaseUrl(connectorId, metadataKey) {
|
|
380
|
+
return new IntegrationError(`OAuth URL for ${connectorId} requires metadata.${metadataKey} as an allowed HTTPS provider root.`, "config_missing");
|
|
381
|
+
}
|
|
382
|
+
function sameOrigin(candidate, actual) {
|
|
383
|
+
try {
|
|
384
|
+
const allowed = new URL(candidate);
|
|
385
|
+
return allowed.protocol === "https:" && !allowed.username && !allowed.password && allowed.pathname === "/" && !allowed.search && !allowed.hash && allowed.origin === actual.origin;
|
|
386
|
+
} catch {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function isPublicDnsHostname(hostname) {
|
|
391
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
392
|
+
if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal") || normalized.endsWith(".home.arpa")) return false;
|
|
393
|
+
if (/^[0-9.]+$/.test(normalized) || normalized.includes(":")) return false;
|
|
394
|
+
return normalized.includes(".") && normalized.split(".").every(isDnsLabel);
|
|
395
|
+
}
|
|
396
|
+
function isDnsLabel(value) {
|
|
397
|
+
return value.length > 0 && value.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(value);
|
|
398
|
+
}
|
|
399
|
+
/** Project the declared `auth.tokenMetadata` mappings out of an OAuth
|
|
400
|
+
* token-response body into connection metadata. Each entry maps a
|
|
401
|
+
* connection-metadata key → the token-response field to read (string
|
|
402
|
+
* shorthand) or `{ field, required }`. Required fields that are absent/empty
|
|
403
|
+
* throw `provider_failure` so the break surfaces at connect; non-required
|
|
404
|
+
* fields are captured if present and otherwise skipped. */
|
|
405
|
+
function captureTokenMetadata(tokenMetadata, json, connectorId) {
|
|
406
|
+
if (!tokenMetadata) return {};
|
|
407
|
+
const captured = {};
|
|
408
|
+
for (const [metaKey, source] of Object.entries(tokenMetadata)) {
|
|
409
|
+
const field = typeof source === "string" ? source : source.field;
|
|
410
|
+
const required = typeof source === "string" ? false : source.required === true;
|
|
411
|
+
const raw = json[field];
|
|
412
|
+
const value = typeof raw === "string" ? raw.trim() : raw;
|
|
413
|
+
if (value === void 0 || value === null || value === "") {
|
|
414
|
+
if (required) throw new IntegrationError(`OAuth token exchange for ${connectorId} did not return required field "${field}" for metadata.${metaKey}.`, "provider_failure");
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
captured[metaKey] = String(value);
|
|
418
|
+
}
|
|
419
|
+
return captured;
|
|
420
|
+
}
|
|
421
|
+
function mapCategory(category) {
|
|
422
|
+
if (category === "comms") return "chat";
|
|
423
|
+
if (category === "spreadsheet") return "database";
|
|
424
|
+
if (category === "doc") return "docs";
|
|
425
|
+
if (category === "commerce") return "workflow";
|
|
426
|
+
if (category === "sales-intelligence") return "crm";
|
|
427
|
+
if (category === "market-intelligence" || category === "hr") return "other";
|
|
428
|
+
return category === "other" ? "other" : category;
|
|
429
|
+
}
|
|
430
|
+
function inferDataClass(category) {
|
|
431
|
+
if (category === "commerce") return "sensitive";
|
|
432
|
+
if (category === "webhook") return "internal";
|
|
433
|
+
return "private";
|
|
434
|
+
}
|
|
435
|
+
function titleFromName(name) {
|
|
436
|
+
return name.split(/[._-]/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
437
|
+
}
|
|
438
|
+
function toRecord(input) {
|
|
439
|
+
if (input && typeof input === "object" && !Array.isArray(input)) return input;
|
|
440
|
+
return {};
|
|
441
|
+
}
|
|
442
|
+
function randomState() {
|
|
443
|
+
const bytes = /* @__PURE__ */ new Uint8Array(24);
|
|
444
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
445
|
+
return base64UrlEncode$1(bytes);
|
|
446
|
+
}
|
|
447
|
+
function randomConnectionId() {
|
|
448
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return `conn_${globalThis.crypto.randomUUID().replace(/-/g, "")}`;
|
|
449
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
450
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
451
|
+
return `conn_${base64UrlEncode$1(bytes)}`;
|
|
452
|
+
}
|
|
453
|
+
function base64UrlEncode$1(bytes) {
|
|
454
|
+
let bin = "";
|
|
455
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
456
|
+
return (typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/credentials.ts
|
|
460
|
+
var CredentialRefreshRevokedError = class extends Error {
|
|
461
|
+
constructor() {
|
|
462
|
+
super("Connection was revoked during credential refresh.");
|
|
463
|
+
this.name = "CredentialRefreshRevokedError";
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const credentialRefreshes = /* @__PURE__ */ new WeakMap();
|
|
467
|
+
const credentialPersistences = /* @__PURE__ */ new WeakMap();
|
|
468
|
+
const credentialRevocations = /* @__PURE__ */ new WeakMap();
|
|
469
|
+
var InMemoryIntegrationSecretStore = class {
|
|
470
|
+
secrets = /* @__PURE__ */ new Map();
|
|
471
|
+
get(ref) {
|
|
472
|
+
return this.secrets.get(secretKey(ref));
|
|
473
|
+
}
|
|
474
|
+
put(ref, credentials) {
|
|
475
|
+
this.secrets.set(secretKey(ref), credentials);
|
|
476
|
+
}
|
|
477
|
+
delete(ref) {
|
|
478
|
+
this.secrets.delete(secretKey(ref));
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
/** Test/dev double for {@link IntegrationOAuthStateStore}. Production hubs
|
|
482
|
+
* inject a durable implementation; this one keeps records in a Map and
|
|
483
|
+
* enforces the single-use + expiry contract. */
|
|
484
|
+
var InMemoryIntegrationOAuthStateStore = class {
|
|
485
|
+
states = /* @__PURE__ */ new Map();
|
|
486
|
+
put(state) {
|
|
487
|
+
this.states.set(state.state, state);
|
|
488
|
+
}
|
|
489
|
+
consume(state) {
|
|
490
|
+
const record = this.states.get(state);
|
|
491
|
+
this.states.delete(state);
|
|
492
|
+
if (!record) return {
|
|
493
|
+
ok: false,
|
|
494
|
+
reason: "unknown"
|
|
495
|
+
};
|
|
496
|
+
if (record.expiresAt <= Date.now()) return {
|
|
497
|
+
ok: false,
|
|
498
|
+
reason: "expired"
|
|
499
|
+
};
|
|
500
|
+
return {
|
|
501
|
+
ok: true,
|
|
502
|
+
state: record
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
sweep(now) {
|
|
506
|
+
for (const [key, record] of this.states) if (record.expiresAt <= now) this.states.delete(key);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
function createConnectionCredentialResolver(options) {
|
|
510
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
511
|
+
return async function resolveDataSource(connection) {
|
|
512
|
+
const credentials = await resolveConnectionCredentials(connection, {
|
|
513
|
+
secrets: options.secrets,
|
|
514
|
+
connections: options.connections,
|
|
515
|
+
adapters: options.adapters,
|
|
516
|
+
now,
|
|
517
|
+
markConnectionError: options.markConnectionError
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
id: connection.id,
|
|
521
|
+
projectId: String(connection.metadata?.projectId ?? connection.owner.id),
|
|
522
|
+
publishedAgentId: typeof connection.metadata?.publishedAgentId === "string" ? connection.metadata.publishedAgentId : null,
|
|
523
|
+
kind: connection.connectorId,
|
|
524
|
+
label: connection.account?.displayName ?? connection.account?.email ?? connection.connectorId,
|
|
525
|
+
consistencyModel: typeof connection.metadata?.consistencyModel === "string" ? connection.metadata.consistencyModel : "authoritative",
|
|
526
|
+
scopes: connection.grantedScopes,
|
|
527
|
+
metadata: connection.metadata ?? {},
|
|
528
|
+
credentials,
|
|
529
|
+
status: connection.status === "active" ? "active" : connection.status === "revoked" ? "revoked" : "error"
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
async function resolveConnectionCredentials(input, options) {
|
|
534
|
+
if (input.status !== "active") throw new Error(`Connection ${input.id} is ${input.status}.`);
|
|
535
|
+
if (!input.secretRef) return { kind: "none" };
|
|
536
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
537
|
+
const current = await options.secrets.get(input.secretRef);
|
|
538
|
+
if (!current) throw new Error(`Secret ${input.secretRef.provider}/${input.secretRef.id} not found.`);
|
|
539
|
+
if (!isExpiredOauth(current, now)) return current;
|
|
540
|
+
const adapter = options.adapters?.find((candidate) => candidate.manifest.kind === input.connectorId);
|
|
541
|
+
if (!adapter?.refreshToken) return current;
|
|
542
|
+
let refresh;
|
|
543
|
+
try {
|
|
544
|
+
await assertCredentialRefreshActive(input, options, now);
|
|
545
|
+
refresh = acquireCredentialRefresh({
|
|
546
|
+
connectorId: input.connectorId,
|
|
547
|
+
secretRef: input.secretRef,
|
|
548
|
+
secrets: options.secrets,
|
|
549
|
+
now,
|
|
550
|
+
refreshToken: adapter.refreshToken.bind(adapter)
|
|
551
|
+
});
|
|
552
|
+
const refreshed = await refresh.promise;
|
|
553
|
+
await assertCredentialRefreshActive(input, options, now, refresh);
|
|
554
|
+
if (options.connections) {
|
|
555
|
+
await options.connections.put({
|
|
556
|
+
...input,
|
|
557
|
+
status: "active",
|
|
558
|
+
updatedAt: now().toISOString(),
|
|
559
|
+
expiresAt: refreshed.kind === "oauth2" && refreshed.expiresAt ? new Date(refreshed.expiresAt).toISOString() : input.expiresAt
|
|
560
|
+
});
|
|
561
|
+
await assertCredentialRefreshActive(input, options, now, refresh);
|
|
562
|
+
}
|
|
563
|
+
return refreshed;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const err = error instanceof Error ? error : /* @__PURE__ */ new Error("Credential refresh failed.");
|
|
566
|
+
if (err instanceof CredentialRefreshRevokedError) throw err;
|
|
567
|
+
await options.markConnectionError?.(input, err);
|
|
568
|
+
if (options.connections) await options.connections.put({
|
|
569
|
+
...input,
|
|
570
|
+
status: "expired",
|
|
571
|
+
updatedAt: now().toISOString()
|
|
572
|
+
});
|
|
573
|
+
throw err;
|
|
574
|
+
} finally {
|
|
575
|
+
if (refresh) releaseCredentialRefresh(options.secrets, input.connectorId, input.secretRef, refresh);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function acquireCredentialRefresh(input) {
|
|
579
|
+
let refreshes = credentialRefreshes.get(input.secrets);
|
|
580
|
+
if (!refreshes) {
|
|
581
|
+
refreshes = /* @__PURE__ */ new Map();
|
|
582
|
+
credentialRefreshes.set(input.secrets, refreshes);
|
|
583
|
+
}
|
|
584
|
+
const key = credentialRefreshKey(input.connectorId, input.secretRef);
|
|
585
|
+
if (credentialRevocations.get(input.secrets)?.has(key)) throw new CredentialRefreshRevokedError();
|
|
586
|
+
const existing = refreshes.get(key);
|
|
587
|
+
if (existing) {
|
|
588
|
+
existing.waiters += 1;
|
|
589
|
+
return existing;
|
|
590
|
+
}
|
|
591
|
+
let flight;
|
|
592
|
+
const promise = Promise.resolve().then(async () => {
|
|
593
|
+
const latest = await input.secrets.get(input.secretRef);
|
|
594
|
+
if (!latest) throw new Error(`Secret ${input.secretRef.provider}/${input.secretRef.id} not found.`);
|
|
595
|
+
if (!isExpiredOauth(latest, input.now)) return latest;
|
|
596
|
+
const refreshed = await input.refreshToken(latest);
|
|
597
|
+
if (flight.cancelled) throw new CredentialRefreshRevokedError();
|
|
598
|
+
await input.secrets.put(input.secretRef, refreshed);
|
|
599
|
+
if (flight.cancelled) {
|
|
600
|
+
await input.secrets.delete?.(input.secretRef);
|
|
601
|
+
throw new CredentialRefreshRevokedError();
|
|
602
|
+
}
|
|
603
|
+
return refreshed;
|
|
604
|
+
});
|
|
605
|
+
flight = {
|
|
606
|
+
promise,
|
|
607
|
+
cancelled: false,
|
|
608
|
+
settled: false,
|
|
609
|
+
waiters: 1
|
|
610
|
+
};
|
|
611
|
+
refreshes.set(key, flight);
|
|
612
|
+
const settle = () => {
|
|
613
|
+
flight.settled = true;
|
|
614
|
+
clearCredentialRefresh(input.secrets, key, flight);
|
|
615
|
+
};
|
|
616
|
+
promise.then(settle, settle);
|
|
617
|
+
return flight;
|
|
618
|
+
}
|
|
619
|
+
function releaseCredentialRefresh(secrets, connectorId, secretRef, flight) {
|
|
620
|
+
flight.waiters -= 1;
|
|
621
|
+
clearCredentialRefresh(secrets, credentialRefreshKey(connectorId, secretRef), flight);
|
|
622
|
+
}
|
|
623
|
+
function clearCredentialRefresh(secrets, key, flight) {
|
|
624
|
+
if (!flight.settled || flight.waiters !== 0) return;
|
|
625
|
+
const refreshes = credentialRefreshes.get(secrets);
|
|
626
|
+
if (refreshes?.get(key) !== flight) return;
|
|
627
|
+
refreshes.delete(key);
|
|
628
|
+
if (refreshes.size === 0) credentialRefreshes.delete(secrets);
|
|
629
|
+
}
|
|
630
|
+
async function assertCredentialRefreshActive(input, options, now, operation) {
|
|
631
|
+
const key = input.secretRef ? credentialRefreshKey(input.connectorId, input.secretRef) : void 0;
|
|
632
|
+
let revoked = operation?.cancelled === true || key !== void 0 && credentialRevocations.get(options.secrets)?.has(key) === true;
|
|
633
|
+
let stored;
|
|
634
|
+
if (options.connections) {
|
|
635
|
+
stored = await options.connections.get(input.id);
|
|
636
|
+
revoked ||= stored?.status === "revoked";
|
|
637
|
+
}
|
|
638
|
+
if (!revoked && input.secretRef) revoked = await options.secrets.get(input.secretRef) === void 0;
|
|
639
|
+
if (!revoked) return;
|
|
640
|
+
if (operation) operation.cancelled = true;
|
|
641
|
+
if (input.secretRef) await options.secrets.delete?.(input.secretRef);
|
|
642
|
+
if (options.connections && stored?.status !== "revoked") await options.connections.put({
|
|
643
|
+
...stored ?? input,
|
|
644
|
+
status: "revoked",
|
|
645
|
+
updatedAt: now().toISOString()
|
|
646
|
+
});
|
|
647
|
+
throw new CredentialRefreshRevokedError();
|
|
648
|
+
}
|
|
649
|
+
function createCredentialBackedAdapterProvider(options) {
|
|
650
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
651
|
+
return createConnectorAdapterProvider({
|
|
652
|
+
...options,
|
|
653
|
+
resolveDataSource: createConnectionCredentialResolver(options),
|
|
654
|
+
onCredentialsRotated: async ({ connection, credentials }) => {
|
|
655
|
+
const persistence = connection.secretRef ? acquireCredentialPersistence(options.secrets, connection.connectorId, connection.secretRef) : void 0;
|
|
656
|
+
try {
|
|
657
|
+
await assertCredentialRefreshActive(connection, options, now, persistence);
|
|
658
|
+
if (connection.secretRef) await options.secrets.put(connection.secretRef, credentials);
|
|
659
|
+
await assertCredentialRefreshActive(connection, options, now, persistence);
|
|
660
|
+
if (options.connections) {
|
|
661
|
+
await options.connections.put({
|
|
662
|
+
...connection,
|
|
663
|
+
status: "active",
|
|
664
|
+
updatedAt: now().toISOString(),
|
|
665
|
+
expiresAt: credentials.kind === "oauth2" && credentials.expiresAt ? new Date(credentials.expiresAt).toISOString() : connection.expiresAt
|
|
666
|
+
});
|
|
667
|
+
await assertCredentialRefreshActive(connection, options, now, persistence);
|
|
668
|
+
}
|
|
669
|
+
await options.onCredentialsRotated?.({
|
|
670
|
+
connection,
|
|
671
|
+
secretRef: connection.secretRef,
|
|
672
|
+
credentials
|
|
673
|
+
});
|
|
674
|
+
await assertCredentialRefreshActive(connection, options, now, persistence);
|
|
675
|
+
} finally {
|
|
676
|
+
if (persistence && connection.secretRef) releaseCredentialPersistence(options.secrets, connection.connectorId, connection.secretRef, persistence);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function acquireCredentialPersistence(secrets, connectorId, secretRef) {
|
|
682
|
+
let persistences = credentialPersistences.get(secrets);
|
|
683
|
+
if (!persistences) {
|
|
684
|
+
persistences = /* @__PURE__ */ new Map();
|
|
685
|
+
credentialPersistences.set(secrets, persistences);
|
|
686
|
+
}
|
|
687
|
+
const key = credentialRefreshKey(connectorId, secretRef);
|
|
688
|
+
let leases = persistences.get(key);
|
|
689
|
+
if (!leases) {
|
|
690
|
+
leases = /* @__PURE__ */ new Set();
|
|
691
|
+
persistences.set(key, leases);
|
|
692
|
+
}
|
|
693
|
+
const lease = { cancelled: credentialRevocations.get(secrets)?.has(key) === true };
|
|
694
|
+
leases.add(lease);
|
|
695
|
+
return lease;
|
|
696
|
+
}
|
|
697
|
+
function releaseCredentialPersistence(secrets, connectorId, secretRef, lease) {
|
|
698
|
+
const persistences = credentialPersistences.get(secrets);
|
|
699
|
+
if (!persistences) return;
|
|
700
|
+
const key = credentialRefreshKey(connectorId, secretRef);
|
|
701
|
+
const leases = persistences.get(key);
|
|
702
|
+
leases?.delete(lease);
|
|
703
|
+
if (leases?.size === 0) persistences.delete(key);
|
|
704
|
+
if (persistences.size === 0) credentialPersistences.delete(secrets);
|
|
705
|
+
}
|
|
706
|
+
async function revokeConnection(input) {
|
|
707
|
+
const finishRevocation = input.connection.secretRef && input.secrets ? beginCredentialRevocation(input.secrets, input.connection.connectorId, input.connection.secretRef) : void 0;
|
|
708
|
+
try {
|
|
709
|
+
if (input.connection.secretRef) await input.secrets?.delete?.(input.connection.secretRef);
|
|
710
|
+
const revoked = {
|
|
711
|
+
...input.connection,
|
|
712
|
+
status: "revoked",
|
|
713
|
+
updatedAt: (input.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
714
|
+
};
|
|
715
|
+
await input.connections?.put(revoked);
|
|
716
|
+
return revoked;
|
|
717
|
+
} finally {
|
|
718
|
+
finishRevocation?.();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function beginCredentialRevocation(secrets, connectorId, secretRef) {
|
|
722
|
+
const key = credentialRefreshKey(connectorId, secretRef);
|
|
723
|
+
let revocations = credentialRevocations.get(secrets);
|
|
724
|
+
if (!revocations) {
|
|
725
|
+
revocations = /* @__PURE__ */ new Map();
|
|
726
|
+
credentialRevocations.set(secrets, revocations);
|
|
727
|
+
}
|
|
728
|
+
revocations.set(key, (revocations.get(key) ?? 0) + 1);
|
|
729
|
+
const flight = credentialRefreshes.get(secrets)?.get(key);
|
|
730
|
+
if (flight) flight.cancelled = true;
|
|
731
|
+
for (const persistence of credentialPersistences.get(secrets)?.get(key) ?? []) persistence.cancelled = true;
|
|
732
|
+
return () => {
|
|
733
|
+
const remaining = (revocations?.get(key) ?? 1) - 1;
|
|
734
|
+
if (remaining > 0) revocations?.set(key, remaining);
|
|
735
|
+
else revocations?.delete(key);
|
|
736
|
+
if (revocations?.size === 0) credentialRevocations.delete(secrets);
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function isExpiredOauth(credentials, now) {
|
|
740
|
+
return credentials.kind === "oauth2" && typeof credentials.expiresAt === "number" && credentials.expiresAt <= now().getTime() && Boolean(credentials.refreshToken);
|
|
741
|
+
}
|
|
742
|
+
function secretKey(ref) {
|
|
743
|
+
return `${ref.provider}:${ref.id}`;
|
|
744
|
+
}
|
|
745
|
+
function credentialRefreshKey(connectorId, ref) {
|
|
746
|
+
return JSON.stringify([
|
|
747
|
+
connectorId,
|
|
748
|
+
ref.provider,
|
|
749
|
+
ref.id
|
|
750
|
+
]);
|
|
751
|
+
}
|
|
752
|
+
//#endregion
|
|
753
|
+
//#region src/hub.ts
|
|
754
|
+
var IntegrationHub = class {
|
|
755
|
+
providers = /* @__PURE__ */ new Map();
|
|
756
|
+
store;
|
|
757
|
+
capabilitySecret;
|
|
758
|
+
guard;
|
|
759
|
+
policy;
|
|
760
|
+
/** Host-injected (or in-memory default) secret store. The hub re-persists
|
|
761
|
+
* rotated credentials here when a connection carries a secretRef. */
|
|
762
|
+
secretStore;
|
|
763
|
+
oauthStateStore;
|
|
764
|
+
oauthStateTtlMs;
|
|
765
|
+
credentialsRotated;
|
|
766
|
+
now;
|
|
767
|
+
constructor(options) {
|
|
768
|
+
if (!options.capabilitySecret) throw new IntegrationError("capabilitySecret is required.", "capability_invalid");
|
|
769
|
+
for (const provider of options.providers) this.providers.set(provider.id, provider);
|
|
770
|
+
this.store = options.store;
|
|
771
|
+
this.capabilitySecret = options.capabilitySecret;
|
|
772
|
+
this.guard = options.guard;
|
|
773
|
+
this.policy = options.policy;
|
|
774
|
+
this.secretStore = options.secretStore ?? new InMemoryIntegrationSecretStore();
|
|
775
|
+
this.oauthStateStore = options.oauthStateStore ?? new InMemoryIntegrationOAuthStateStore();
|
|
776
|
+
this.oauthStateTtlMs = options.oauthStateTtlMs ?? 6e5;
|
|
777
|
+
this.credentialsRotated = options.credentialsRotated;
|
|
778
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
779
|
+
}
|
|
780
|
+
async listConnectors() {
|
|
781
|
+
return (await Promise.all([...this.providers.values()].map((provider) => provider.listConnectors()))).flat();
|
|
782
|
+
}
|
|
783
|
+
async listRegistry(options = {}) {
|
|
784
|
+
const sources = await Promise.all([...this.providers.values()].map(async (provider) => ({
|
|
785
|
+
id: provider.id,
|
|
786
|
+
connectors: await provider.listConnectors()
|
|
787
|
+
})));
|
|
788
|
+
return composeIntegrationRegistry(sources, options);
|
|
789
|
+
}
|
|
790
|
+
async startAuth(providerId, request) {
|
|
791
|
+
const provider = this.requireProvider(providerId);
|
|
792
|
+
if (!provider.startAuth) throw new IntegrationError(`Provider ${providerId} does not support auth start.`, "auth_not_supported");
|
|
793
|
+
const pkcePair = connectorOAuthPkceMode(await this.requireConnector(provider, request.connectorId)) === "unsupported" ? void 0 : createPkcePair();
|
|
794
|
+
const result = await provider.startAuth({
|
|
795
|
+
...request,
|
|
796
|
+
codeChallenge: pkcePair?.challenge
|
|
797
|
+
});
|
|
798
|
+
const record = {
|
|
799
|
+
state: result.state,
|
|
800
|
+
providerId,
|
|
801
|
+
connectorId: request.connectorId,
|
|
802
|
+
owner: request.owner,
|
|
803
|
+
requestedScopes: request.requestedScopes,
|
|
804
|
+
redirectUri: request.redirectUri,
|
|
805
|
+
codeVerifier: pkcePair?.verifier,
|
|
806
|
+
expiresAt: this.now().getTime() + this.oauthStateTtlMs,
|
|
807
|
+
metadata: request.metadata
|
|
808
|
+
};
|
|
809
|
+
await this.oauthStateStore.put(record);
|
|
810
|
+
return result;
|
|
811
|
+
}
|
|
812
|
+
async completeAuth(providerId, request) {
|
|
813
|
+
const provider = this.requireProvider(providerId);
|
|
814
|
+
if (!provider.completeAuth) throw new IntegrationError(`Provider ${providerId} does not support auth completion.`, "auth_not_supported");
|
|
815
|
+
const outcome = await this.oauthStateStore.consume(request.state);
|
|
816
|
+
if (!outcome.ok) throw new IntegrationError(`Integration OAuth state ${outcome.reason}: possible CSRF, replay, or stale flow.`, "capability_invalid");
|
|
817
|
+
if (outcome.state.providerId !== providerId || outcome.state.connectorId !== request.connectorId) throw new IntegrationError("Integration OAuth state does not match completion request.", "capability_invalid");
|
|
818
|
+
if (outcome.state.redirectUri !== request.redirectUri) throw new IntegrationError("Integration OAuth redirect URI does not match the start request.", "capability_invalid");
|
|
819
|
+
const pinnedMetadata = request.metadata || outcome.state.metadata ? {
|
|
820
|
+
...request.metadata,
|
|
821
|
+
...outcome.state.metadata
|
|
822
|
+
} : void 0;
|
|
823
|
+
const connection = await provider.completeAuth({
|
|
824
|
+
...request,
|
|
825
|
+
connectorId: outcome.state.connectorId,
|
|
826
|
+
owner: outcome.state.owner,
|
|
827
|
+
state: outcome.state.state,
|
|
828
|
+
redirectUri: outcome.state.redirectUri,
|
|
829
|
+
codeVerifier: outcome.state.codeVerifier,
|
|
830
|
+
metadata: pinnedMetadata
|
|
831
|
+
});
|
|
832
|
+
await this.store.put(connection);
|
|
833
|
+
return connection;
|
|
834
|
+
}
|
|
835
|
+
async upsertConnection(connection) {
|
|
836
|
+
await this.store.put(connection);
|
|
837
|
+
return connection;
|
|
838
|
+
}
|
|
839
|
+
async listConnections(owner) {
|
|
840
|
+
return this.store.listByOwner(owner);
|
|
841
|
+
}
|
|
842
|
+
async getConnection(connectionId) {
|
|
843
|
+
return this.store.get(connectionId);
|
|
844
|
+
}
|
|
845
|
+
async issueCapability(request) {
|
|
846
|
+
const connection = await this.requireConnection(request.connectionId);
|
|
847
|
+
this.assertConnectionActive(connection);
|
|
848
|
+
assertScopes(connection, request.scopes);
|
|
849
|
+
const now = this.now();
|
|
850
|
+
const capability = {
|
|
851
|
+
id: `cap_${randomUUID()}`,
|
|
852
|
+
subject: request.subject,
|
|
853
|
+
connectionId: request.connectionId,
|
|
854
|
+
scopes: unique(request.scopes),
|
|
855
|
+
allowedActions: unique(request.allowedActions),
|
|
856
|
+
issuedAt: now.toISOString(),
|
|
857
|
+
expiresAt: new Date(now.getTime() + request.ttlMs).toISOString(),
|
|
858
|
+
metadata: request.metadata
|
|
859
|
+
};
|
|
860
|
+
return {
|
|
861
|
+
capability,
|
|
862
|
+
token: signCapability(capability, this.capabilitySecret)
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
verifyCapability(token) {
|
|
866
|
+
const capability = verifyCapabilityToken(token, this.capabilitySecret);
|
|
867
|
+
if (Date.parse(capability.expiresAt) <= this.now().getTime()) throw new IntegrationError("Integration capability expired.", "capability_expired");
|
|
868
|
+
return capability;
|
|
869
|
+
}
|
|
870
|
+
async invokeWithCapability(token, request) {
|
|
871
|
+
const capability = this.verifyCapability(token);
|
|
872
|
+
if (!capability.allowedActions.includes(request.action)) throw new IntegrationError(`Capability does not allow action ${request.action}.`, "action_denied");
|
|
873
|
+
const connection = await this.requireConnection(capability.connectionId);
|
|
874
|
+
this.assertConnectionActive(connection);
|
|
875
|
+
const provider = this.requireProvider(connection.providerId);
|
|
876
|
+
const connector = await this.requireConnector(provider, connection.connectorId);
|
|
877
|
+
const action = connector.actions.find((candidate) => candidate.id === request.action);
|
|
878
|
+
if (!action) throw new IntegrationError(`Action ${request.action} is not defined by connector ${connector.id}.`, "action_not_found");
|
|
879
|
+
assertScopes(connection, action.requiredScopes);
|
|
880
|
+
assertScopes({
|
|
881
|
+
...connection,
|
|
882
|
+
grantedScopes: capability.scopes
|
|
883
|
+
}, action.requiredScopes);
|
|
884
|
+
const fullRequest = {
|
|
885
|
+
...request,
|
|
886
|
+
connectionId: connection.id
|
|
887
|
+
};
|
|
888
|
+
const proceed = async () => {
|
|
889
|
+
if (!this.policy) return provider.invokeAction(connection, fullRequest);
|
|
890
|
+
const decision = await this.policy.decide({
|
|
891
|
+
connection,
|
|
892
|
+
request: fullRequest,
|
|
893
|
+
action,
|
|
894
|
+
subject: capability.subject
|
|
895
|
+
});
|
|
896
|
+
if (decision.decision === "deny") throw new IntegrationError(decision.reason, "policy_denied");
|
|
897
|
+
if (decision.decision === "require_approval") return {
|
|
898
|
+
ok: false,
|
|
899
|
+
action: request.action,
|
|
900
|
+
output: {
|
|
901
|
+
approvalRequired: true,
|
|
902
|
+
approval: decision.approval
|
|
903
|
+
},
|
|
904
|
+
metadata: {
|
|
905
|
+
policyDecision: decision.decision,
|
|
906
|
+
reason: decision.reason,
|
|
907
|
+
...decision.metadata
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
return provider.invokeAction(connection, fullRequest);
|
|
911
|
+
};
|
|
912
|
+
if (this.guard) return this.guard.invokeAction({
|
|
913
|
+
connection,
|
|
914
|
+
request: fullRequest,
|
|
915
|
+
action
|
|
916
|
+
}, proceed);
|
|
917
|
+
return proceed();
|
|
918
|
+
}
|
|
919
|
+
async subscribeTrigger(connectionId, trigger, targetUrl) {
|
|
920
|
+
const connection = await this.requireConnection(connectionId);
|
|
921
|
+
this.assertConnectionActive(connection);
|
|
922
|
+
const provider = this.requireProvider(connection.providerId);
|
|
923
|
+
const connector = await this.requireConnector(provider, connection.connectorId);
|
|
924
|
+
const spec = connector.triggers?.find((candidate) => candidate.id === trigger);
|
|
925
|
+
if (!spec) throw new IntegrationError(`Trigger ${trigger} is not defined by connector ${connector.id}.`, "trigger_not_found");
|
|
926
|
+
assertScopes(connection, spec.requiredScopes);
|
|
927
|
+
if (!provider.subscribeTrigger) throw new IntegrationError(`Provider ${provider.id} does not support triggers.`, "auth_not_supported");
|
|
928
|
+
return provider.subscribeTrigger(connection, trigger, targetUrl);
|
|
929
|
+
}
|
|
930
|
+
requireProvider(providerId) {
|
|
931
|
+
const provider = this.providers.get(providerId);
|
|
932
|
+
if (!provider) throw new IntegrationError(`Provider ${providerId} not found.`, "provider_not_found");
|
|
933
|
+
return provider;
|
|
934
|
+
}
|
|
935
|
+
async requireConnector(provider, connectorId) {
|
|
936
|
+
const connector = (await provider.listConnectors()).find((candidate) => candidate.id === connectorId);
|
|
937
|
+
if (!connector) throw new IntegrationError(`Connector ${connectorId} not found.`, "connector_not_found");
|
|
938
|
+
return connector;
|
|
939
|
+
}
|
|
940
|
+
async requireConnection(connectionId) {
|
|
941
|
+
const connection = await this.store.get(connectionId);
|
|
942
|
+
if (!connection) throw new IntegrationError(`Connection ${connectionId} not found.`, "connection_not_found");
|
|
943
|
+
return connection;
|
|
944
|
+
}
|
|
945
|
+
assertConnectionActive(connection) {
|
|
946
|
+
if (connection.status !== "active") throw new IntegrationError(`Connection ${connection.id} is ${connection.status}.`, "connection_not_active");
|
|
947
|
+
if (connection.expiresAt && Date.parse(connection.expiresAt) <= this.now().getTime()) throw new IntegrationError(`Connection ${connection.id} is expired.`, "connection_not_active");
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
function signCapability(capability, secret) {
|
|
951
|
+
const payload = base64UrlEncode(JSON.stringify(capability));
|
|
952
|
+
return `${payload}.${hmac(payload, secret)}`;
|
|
953
|
+
}
|
|
954
|
+
function verifyCapabilityToken(token, secret) {
|
|
955
|
+
const [payload, signature] = token.split(".");
|
|
956
|
+
if (!payload || !signature) throw new IntegrationError("Malformed integration capability.", "capability_invalid");
|
|
957
|
+
if (!constantTimeEqual(signature, hmac(payload, secret))) throw new IntegrationError("Invalid integration capability signature.", "capability_invalid");
|
|
958
|
+
let parsed;
|
|
959
|
+
try {
|
|
960
|
+
parsed = JSON.parse(base64UrlDecode(payload));
|
|
961
|
+
} catch {
|
|
962
|
+
throw new IntegrationError("Invalid integration capability payload.", "capability_invalid");
|
|
963
|
+
}
|
|
964
|
+
if (!parsed.id || !parsed.connectionId || !Array.isArray(parsed.scopes) || !Array.isArray(parsed.allowedActions)) throw new IntegrationError("Invalid integration capability payload.", "capability_invalid");
|
|
965
|
+
return parsed;
|
|
966
|
+
}
|
|
967
|
+
function assertScopes(connection, requiredScopes) {
|
|
968
|
+
const missing = requiredScopes.filter((scope) => !connection.grantedScopes.includes(scope));
|
|
969
|
+
if (missing.length > 0) throw new IntegrationError(`Missing integration scopes: ${missing.join(", ")}`, "scope_denied");
|
|
970
|
+
}
|
|
971
|
+
function connectorOAuthPkceMode(connector) {
|
|
972
|
+
const value = connector.metadata?.oauthPkce;
|
|
973
|
+
if (value === void 0) return "required";
|
|
974
|
+
if (value === "required" || value === "supported" || value === "unsupported") return value;
|
|
975
|
+
throw new IntegrationError(`Connector ${connector.id} declares an invalid OAuth PKCE posture.`, "config_missing");
|
|
976
|
+
}
|
|
977
|
+
function createPkcePair() {
|
|
978
|
+
const verifier = randomBytes(48).toString("base64url");
|
|
979
|
+
return {
|
|
980
|
+
verifier,
|
|
981
|
+
challenge: createHash("sha256").update(verifier).digest("base64url")
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function hmac(payload, secret) {
|
|
985
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
986
|
+
}
|
|
987
|
+
function constantTimeEqual(a, b) {
|
|
988
|
+
const left = Buffer.from(a);
|
|
989
|
+
const right = Buffer.from(b);
|
|
990
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
991
|
+
}
|
|
992
|
+
function base64UrlEncode(value) {
|
|
993
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
994
|
+
}
|
|
995
|
+
function base64UrlDecode(value) {
|
|
996
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
997
|
+
}
|
|
998
|
+
function unique(values) {
|
|
999
|
+
return [...new Set(values)];
|
|
1000
|
+
}
|
|
1001
|
+
//#endregion
|
|
1002
|
+
export { InMemoryIntegrationSecretStore as a, resolveConnectionCredentials as c, createConnectorAdapterCatalogSource as d, createConnectorAdapterProvider as f, InMemoryIntegrationOAuthStateStore as i, revokeConnection as l, IntegrationError as m, signCapability as n, createConnectionCredentialResolver as o, manifestToConnector as p, verifyCapabilityToken as r, createCredentialBackedAdapterProvider as s, IntegrationHub as t, adapterManifestsToConnectors as u };
|
|
1003
|
+
|
|
1004
|
+
//# sourceMappingURL=hub-D13zPQGr.js.map
|