@tangle-network/agent-integrations 0.58.0 → 0.59.1

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.
Files changed (56) hide show
  1. package/README.md +31 -0
  2. package/dist/{adapters-ediOgE75.js → adapters-DQ0JJ-aG.js} +6 -283
  3. package/dist/{adapters-ediOgE75.js.map → adapters-DQ0JJ-aG.js.map} +1 -1
  4. package/dist/catalog-CFvKzwVj.js +201 -0
  5. package/dist/catalog-CFvKzwVj.js.map +1 -0
  6. package/dist/catalog.js +1 -1
  7. package/dist/connect/index.js +1 -1
  8. package/dist/connectors/adapters/index.js +1 -1
  9. package/dist/connectors/index.d.ts +2 -1
  10. package/dist/connectors/index.js +2 -1
  11. package/dist/connectors/oauth.d.ts +2 -0
  12. package/dist/connectors/oauth.js +282 -0
  13. package/dist/connectors/oauth.js.map +1 -0
  14. package/dist/{consumer-BC6pCUzI.d.ts → consumer-zHtoJixe.d.ts} +5 -37
  15. package/dist/{consumer-BC6pCUzI.d.ts.map → consumer-zHtoJixe.d.ts.map} +1 -1
  16. package/dist/consumer.d.ts +1 -1
  17. package/dist/hub-0Od2lgun.d.ts +38 -0
  18. package/dist/hub-0Od2lgun.d.ts.map +1 -0
  19. package/dist/hub-D13zPQGr.js +1004 -0
  20. package/dist/hub-D13zPQGr.js.map +1 -0
  21. package/dist/hub.d.ts +2 -0
  22. package/dist/hub.js +2 -0
  23. package/dist/index-BQXENE9p.d.ts +130 -0
  24. package/dist/index-BQXENE9p.d.ts.map +1 -0
  25. package/dist/index.d.ts +5 -3
  26. package/dist/index.js +15 -11
  27. package/dist/internal/bundled-manifest-runtime.js +1 -1
  28. package/dist/mcp.d.ts +1 -1
  29. package/dist/mcp.js +2 -1
  30. package/dist/mcp.js.map +1 -1
  31. package/dist/middleware/index.js +1 -1
  32. package/dist/oauth-C5pa5j3O.d.ts +136 -0
  33. package/dist/oauth-C5pa5j3O.d.ts.map +1 -0
  34. package/dist/registry-core-BZo69C37.js +186 -0
  35. package/dist/registry-core-BZo69C37.js.map +1 -0
  36. package/dist/registry.js +2 -2
  37. package/dist/runtime.d.ts +1 -1
  38. package/dist/runtime.js +1 -1
  39. package/dist/sandbox.d.ts +2 -0
  40. package/dist/sandbox.js +114 -0
  41. package/dist/sandbox.js.map +1 -0
  42. package/dist/{src-Bd4KMI-J.js → src-wcW1OCA7.js} +84922 -86026
  43. package/dist/src-wcW1OCA7.js.map +1 -0
  44. package/dist/stripe/index.js +1 -1
  45. package/dist/tangle-search/index.d.ts +37 -1
  46. package/dist/tangle-search/index.d.ts.map +1 -1
  47. package/dist/tangle-search/index.js +81 -1
  48. package/dist/tangle-search/index.js.map +1 -1
  49. package/dist/webhooks/index.js +1 -1
  50. package/docs/host-search-and-phone.md +28 -1
  51. package/package.json +17 -2
  52. package/dist/catalog-CBd0zbdD.js +0 -383
  53. package/dist/catalog-CBd0zbdD.js.map +0 -1
  54. package/dist/index-j2QziJA5.d.ts +0 -263
  55. package/dist/index-j2QziJA5.d.ts.map +0 -1
  56. 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