@jskit-ai/connectors-core 0.1.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.
- package/README.md +551 -0
- package/docs/oauth-callbacks.md +65 -0
- package/docs/online-setup.md +56 -0
- package/docs/setup-command.md +178 -0
- package/migrations/connectors_core_initial.cjs +17 -0
- package/package.json +65 -0
- package/src/server/ConnectorsFeature.js +36 -0
- package/src/server/connectionService.js +664 -0
- package/src/server/credentialProtection.js +34 -0
- package/src/server/environmentReferences.js +13 -0
- package/src/server/errors.js +27 -0
- package/src/server/fileConnectionStore.js +86 -0
- package/src/server/fileStorage.js +2 -0
- package/src/server/index.js +4 -0
- package/src/server/knexConnectionStore.js +65 -0
- package/src/server/storage.js +2 -0
- package/src/shared/configuration.js +225 -0
- package/test/connectionService.test.js +1231 -0
- package/test/fileConnectionStore.test.js +165 -0
- package/test/knexConnectionStore.test.js +178 -0
- package/test/serviceAccount.test.js +192 -0
- package/test/setupCommand.test.js +364 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import * as oauth from "oauth4webapi";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { createHttpClient } from "@jskit-ai/http-runtime/client";
|
|
5
|
+
import { validateIntegrationConfiguration } from "../shared/configuration.js";
|
|
6
|
+
import { ConnectorError, providerError } from "./errors.js";
|
|
7
|
+
|
|
8
|
+
function publicConnection(connection) {
|
|
9
|
+
if (!connection) return { status: "disconnected" };
|
|
10
|
+
return {
|
|
11
|
+
status: connection.status,
|
|
12
|
+
provider: connection.provider,
|
|
13
|
+
integrationId: connection.integrationId,
|
|
14
|
+
grantedScopes: [...connection.grantedScopes],
|
|
15
|
+
verifiedAt: connection.verifiedAt,
|
|
16
|
+
...(connection.accountLabel ? { accountLabel: connection.accountLabel } : {})
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateCallbackUrl(value) {
|
|
21
|
+
let url;
|
|
22
|
+
try { url = new URL(value); } catch { /* reported below */ }
|
|
23
|
+
const localHttp = url?.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
24
|
+
if (!url || (!localHttp && url.protocol !== "https:") || url.username || url.password || url.hash || url.search) {
|
|
25
|
+
throw new ConnectorError("connector_callback_invalid", "Configure an HTTPS callback URL, or a local development callback.");
|
|
26
|
+
}
|
|
27
|
+
return url.href;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createConnectionService({
|
|
31
|
+
configuration,
|
|
32
|
+
providers,
|
|
33
|
+
resolveReference,
|
|
34
|
+
store,
|
|
35
|
+
authorize,
|
|
36
|
+
executionMode = "application",
|
|
37
|
+
fetchImpl = globalThis.fetch,
|
|
38
|
+
now = Date.now
|
|
39
|
+
}) {
|
|
40
|
+
if (!["application", "assistant"].includes(executionMode)) throw new TypeError("Select application or assistant execution.");
|
|
41
|
+
const config = validateIntegrationConfiguration(configuration, { providers });
|
|
42
|
+
const definitions = new Map(providers.map((provider) => [provider.id, provider]));
|
|
43
|
+
if (!providers.length || definitions.size !== providers.length) throw new TypeError("Register unique connector providers.");
|
|
44
|
+
if (typeof authorize !== "function" || typeof resolveReference !== "function") {
|
|
45
|
+
throw new TypeError("Connections require application authorization and reference resolution.");
|
|
46
|
+
}
|
|
47
|
+
if (typeof store?.withConnection !== "function") throw new TypeError("Connection storage requires withConnection().");
|
|
48
|
+
const http = createHttpClient({ fetchImpl, csrf: { enabled: false }, credentials: "omit" });
|
|
49
|
+
|
|
50
|
+
async function access(context, integrationId, operation, input, checkAssistantPermission = true) {
|
|
51
|
+
const integration = config.integrations[integrationId];
|
|
52
|
+
if (!Object.hasOwn(config.integrations, integrationId)) {
|
|
53
|
+
throw new ConnectorError("connector_not_found", "This integration is not configured.", { statusCode: 404 });
|
|
54
|
+
}
|
|
55
|
+
const baseDefinition = definitions.get(integration.provider);
|
|
56
|
+
const definition = baseDefinition.runtimeForSettings
|
|
57
|
+
? { ...baseDefinition, ...baseDefinition.runtimeForSettings(integration.settings || {}) } : baseDefinition;
|
|
58
|
+
const provider = typeof definition.checkOperation === "function"
|
|
59
|
+
? { ...definition, checkOperation: definition.checkOperation(integration.authentication.method) }
|
|
60
|
+
: definition;
|
|
61
|
+
let assistantPermission;
|
|
62
|
+
if (executionMode === "assistant" && checkAssistantPermission && !["status", "disconnect"].includes(operation)) {
|
|
63
|
+
const action = provider.operations?.[operation]?.assistantAction || operation;
|
|
64
|
+
const policy = integration.assistantPolicy;
|
|
65
|
+
const decision = policy?.enabled === false ? "never" : policy?.actions?.[action] || policy?.defaultPermission || "ask";
|
|
66
|
+
if (decision === "never") {
|
|
67
|
+
throw new ConnectorError("connector_access_denied", "This assistant action is disabled.", { statusCode: 403 });
|
|
68
|
+
}
|
|
69
|
+
assistantPermission = { action, decision };
|
|
70
|
+
}
|
|
71
|
+
const owner = await authorize(context, { integrationId, operation, accountMode: integration.accountMode,
|
|
72
|
+
...(assistantPermission ? { assistantPermission: { ...assistantPermission } } : {}),
|
|
73
|
+
...(input === undefined ? {} : { input: structuredClone(input) }) });
|
|
74
|
+
if (!owner || typeof owner.applicationId !== "string" || !owner.applicationId ||
|
|
75
|
+
typeof owner.subjectId !== "string" || !owner.subjectId) {
|
|
76
|
+
throw new ConnectorError("connector_access_denied", "You cannot use this connection.", { statusCode: 403 });
|
|
77
|
+
}
|
|
78
|
+
if (assistantPermission?.decision === "ask" && owner.approved !== true) {
|
|
79
|
+
throw new ConnectorError("connector_approval_required", "Approve this assistant action before continuing.", { statusCode: 409 });
|
|
80
|
+
}
|
|
81
|
+
const methods = provider.operations?.[operation]?.authenticationMethods;
|
|
82
|
+
if (methods && !methods.includes(integration.authentication.method)) {
|
|
83
|
+
throw new ConnectorError("connector_mode_unavailable", "This operation requires a different connection method.");
|
|
84
|
+
}
|
|
85
|
+
return { owner: { applicationId: owner.applicationId, subjectId: owner.subjectId }, integration, provider };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function authorizeAssistantAction({ context, integrationId, action, input = {} }) {
|
|
89
|
+
const provider = definitions.get(config.integrations[integrationId]?.provider);
|
|
90
|
+
if (executionMode !== "assistant" || !provider?.assistantActions?.some((item) => item.value === action)) {
|
|
91
|
+
throw new ConnectorError("connector_operation_unknown", "This provider does not declare this assistant action.");
|
|
92
|
+
}
|
|
93
|
+
await access(context, integrationId, action, structuredClone(input));
|
|
94
|
+
// The host performs its own lifecycle operation after authorization; no provider action is executed here.
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function registrationFor(integration, provider) {
|
|
98
|
+
const registration = config.registrations[integration.authentication.registrationRef];
|
|
99
|
+
if (registration?.source !== "own" || integration.authentication.method !== "oauth2" || !provider.oauth) {
|
|
100
|
+
throw new ConnectorError("connector_mode_unavailable", "This runtime does not yet support the selected credential mode.");
|
|
101
|
+
}
|
|
102
|
+
if (registration.clientId.trim() === "MISSING") {
|
|
103
|
+
throw new ConnectorError("connector_binding_missing", "The provider client ID is not configured.");
|
|
104
|
+
}
|
|
105
|
+
const grantType = registration.grantType || "authorization_code";
|
|
106
|
+
const tokenEndpointAuthMethod = registration.tokenEndpointAuthMethod || "client_secret_post";
|
|
107
|
+
if (provider.oauthPkce === false && tokenEndpointAuthMethod === "none") {
|
|
108
|
+
throw new ConnectorError("connector_mode_unavailable", "This provider's non-PKCE flow requires a confidential client.");
|
|
109
|
+
}
|
|
110
|
+
let clientSecret;
|
|
111
|
+
let callback;
|
|
112
|
+
try {
|
|
113
|
+
if (tokenEndpointAuthMethod !== "none") {
|
|
114
|
+
clientSecret = await resolveReference(registration.clientSecretRef);
|
|
115
|
+
if (typeof clientSecret !== "string" || !clientSecret.trim() || clientSecret.trim() === "MISSING") throw new Error("Invalid client secret.");
|
|
116
|
+
}
|
|
117
|
+
if (grantType === "authorization_code") callback = await resolveReference(registration.callbackUrlRef);
|
|
118
|
+
} catch {
|
|
119
|
+
throw new ConnectorError("connector_binding_missing", "A required registration binding is missing.");
|
|
120
|
+
}
|
|
121
|
+
const callbackUrl = grantType === "authorization_code" ? validateCallbackUrl(callback) : undefined;
|
|
122
|
+
if (callbackUrl && provider.validateCallbackUrl && provider.validateCallbackUrl(callbackUrl) !== true) {
|
|
123
|
+
throw new ConnectorError("connector_callback_invalid", "The callback URL does not meet this provider's requirements.");
|
|
124
|
+
}
|
|
125
|
+
let clientAuth = tokenEndpointAuthMethod === "none" ? oauth.None()
|
|
126
|
+
: tokenEndpointAuthMethod === "client_secret_basic" ? oauth.ClientSecretBasic(clientSecret) : oauth.ClientSecretPost(clientSecret);
|
|
127
|
+
if (tokenEndpointAuthMethod === "client_secret_basic" && provider.oauthBasicEncoding === "raw") {
|
|
128
|
+
clientAuth = (_as, client, _body, headers) => {
|
|
129
|
+
headers.set("authorization", `Basic ${Buffer.from(`${client.client_id}:${clientSecret}`).toString("base64")}`);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
oauth: typeof provider.oauth === "function" ? provider.oauth(integration.settings || {}) : provider.oauth,
|
|
134
|
+
resource: typeof provider.oauthResource === "function" ? provider.oauthResource(integration.settings || {}) : provider.oauthResource,
|
|
135
|
+
client: { client_id: registration.clientId },
|
|
136
|
+
clientAuth: provider.oauthClientIdParameter ? (as, client, body, headers) => {
|
|
137
|
+
clientAuth(as, client, body, headers);
|
|
138
|
+
body.set(provider.oauthClientIdParameter, client.client_id);
|
|
139
|
+
body.delete("client_id");
|
|
140
|
+
} : clientAuth,
|
|
141
|
+
tokenEndpointAuthMethod,
|
|
142
|
+
grantType,
|
|
143
|
+
callbackUrl,
|
|
144
|
+
registrationRef: integration.authentication.registrationRef
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function apiKeyFor(integration, provider) {
|
|
149
|
+
if (!integration.authentication.secretRef && provider.apiKeySecretOptional) return "";
|
|
150
|
+
try {
|
|
151
|
+
const value = await resolveReference(integration.authentication.secretRef);
|
|
152
|
+
if (typeof value !== "string" || (!provider.apiKeySecretOptional && !value.trim()) || value.trim() === "MISSING" || /[\r\n]/u.test(value)) throw new Error("Invalid key.");
|
|
153
|
+
return value;
|
|
154
|
+
} catch {
|
|
155
|
+
throw new ConnectorError("connector_binding_missing", "The API key binding is missing or invalid.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function serviceAccountCredentialFor(integration) {
|
|
160
|
+
try {
|
|
161
|
+
const credential = await resolveReference(integration.authentication.secretRef);
|
|
162
|
+
if (typeof credential !== "string" || !credential.trim() || credential.trim() === "MISSING" || credential.length > 65_536 || credential.includes("\0")) throw new Error("Invalid credential.");
|
|
163
|
+
return { credential, fingerprint: createHash("sha256").update(credential).digest("hex") };
|
|
164
|
+
} catch {
|
|
165
|
+
throw new ConnectorError("connector_binding_missing", "The service-account credential binding is missing or invalid.");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function serviceAccountGrant(integration, provider, binding, signal) {
|
|
170
|
+
if (typeof provider.serviceAccountGrant !== "function") {
|
|
171
|
+
throw new ConnectorError("connector_mode_unavailable", "This provider does not implement service-account authorization.");
|
|
172
|
+
}
|
|
173
|
+
const options = requestOptions(signal, provider.requestTimeoutMs);
|
|
174
|
+
options.signal.throwIfAborted();
|
|
175
|
+
let response;
|
|
176
|
+
try {
|
|
177
|
+
response = await provider.serviceAccountGrant({
|
|
178
|
+
credential: binding.credential, settings: structuredClone(integration.settings || {}),
|
|
179
|
+
scopes: [...integration.scopes], fetchImpl, signal: options.signal, now: now()
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
options.signal.throwIfAborted();
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
options.signal.throwIfAborted();
|
|
186
|
+
if (typeof response?.access_token !== "string" || !response.access_token || /\s/u.test(response.access_token) ||
|
|
187
|
+
response.token_type?.toLowerCase?.() !== "bearer" ||
|
|
188
|
+
!Number.isFinite(response.expires_in) || response.expires_in <= 0 || response.expires_in > 86_400 ||
|
|
189
|
+
response.refresh_token !== undefined || (response.scope !== undefined && typeof response.scope !== "string")) {
|
|
190
|
+
throw new ConnectorError("connector_response_invalid", "The provider returned an invalid service-account token.", { statusCode: 502 });
|
|
191
|
+
}
|
|
192
|
+
const grant = tokensFrom(response, null, integration.scopes, provider.scopeSeparator, provider);
|
|
193
|
+
grant.grantedScopes = grant.grantedScopes.filter((scope) => integration.scopes.includes(scope));
|
|
194
|
+
return { ...grant, credentialFingerprint: binding.fingerprint };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function connectServiceAccount({ context, integrationId, verificationInput = {}, signal }) {
|
|
198
|
+
verificationInput = structuredClone(verificationInput);
|
|
199
|
+
const { owner, integration, provider } = await access(context, integrationId, "connect");
|
|
200
|
+
if (integration.authentication.method !== "service-account" || typeof provider.serviceAccountGrant !== "function") {
|
|
201
|
+
throw new ConnectorError("connector_mode_unavailable", "Select service-account credentials to verify this connection.");
|
|
202
|
+
}
|
|
203
|
+
provider.operations[provider.checkOperation].request(verificationInput, integration.settings || {});
|
|
204
|
+
const required = provider.operations[provider.checkOperation].scopes;
|
|
205
|
+
if (required.length && !required.some((scope) => integration.scopes.includes(scope))) {
|
|
206
|
+
throw new ConnectorError("connector_scope_missing", "Connection verification requires a permission absent from the application's configuration.", { statusCode: 403 });
|
|
207
|
+
}
|
|
208
|
+
return store.withConnection({ owner, integrationId }, async ({ save }) => {
|
|
209
|
+
try {
|
|
210
|
+
const binding = await serviceAccountCredentialFor(integration);
|
|
211
|
+
const connection = {
|
|
212
|
+
integrationId, provider: provider.id, method: "service-account", secretRef: integration.authentication.secretRef,
|
|
213
|
+
requestedScopes: [...integration.scopes], settings: structuredClone(integration.settings || {}),
|
|
214
|
+
status: "connected", verifiedAt: now(), ...await serviceAccountGrant(integration, provider, binding, signal)
|
|
215
|
+
};
|
|
216
|
+
await request(provider, provider.checkOperation, verificationInput, connection, signal);
|
|
217
|
+
await save(connection);
|
|
218
|
+
return publicConnection(connection);
|
|
219
|
+
} catch (error) { throw providerError(error); }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function requestOptions(signal, timeoutMs = 15_000, tokenRequestEncoding) {
|
|
224
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
225
|
+
return {
|
|
226
|
+
[oauth.customFetch]: tokenRequestEncoding === "json" ? (address, options) => {
|
|
227
|
+
const headers = new Headers(options.headers);
|
|
228
|
+
headers.set("Content-Type", "application/json");
|
|
229
|
+
return fetchImpl(address, { ...options, headers,
|
|
230
|
+
body: JSON.stringify(Object.fromEntries(new URLSearchParams(options.body))) });
|
|
231
|
+
} : fetchImpl,
|
|
232
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function request(provider, operationId, input, connection, signal) {
|
|
237
|
+
const operation = provider.operations[operationId];
|
|
238
|
+
if (!operation || !Object.hasOwn(provider.operations, operationId)) {
|
|
239
|
+
throw new ConnectorError("connector_operation_unknown", "This provider operation is not available.");
|
|
240
|
+
}
|
|
241
|
+
if (["oauth2", "service-account"].includes(connection.method || "oauth2") && operation.scopes.length && !operation.scopes.some((scope) => connection.grantedScopes.includes(scope))) {
|
|
242
|
+
throw new ConnectorError("connector_scope_missing", "Connect again with the permission required for this operation.", { statusCode: 403 });
|
|
243
|
+
}
|
|
244
|
+
let request;
|
|
245
|
+
try { request = operation.request(input, connection.settings || {}, connection.providerData); } catch (error) {
|
|
246
|
+
if (!error.fieldErrors) throw error;
|
|
247
|
+
const failure = new ConnectorError("connector_input_invalid", "Check the operation's input values.", { statusCode: 422 });
|
|
248
|
+
failure.fieldErrors = error.fieldErrors;
|
|
249
|
+
throw failure;
|
|
250
|
+
}
|
|
251
|
+
const url = new URL(request.url);
|
|
252
|
+
const origins = typeof provider.apiOrigins === "function" ? provider.apiOrigins(connection.settings || {}, connection.providerData) : provider.apiOrigins;
|
|
253
|
+
if (url.protocol !== "https:" || url.username || url.password || !origins.includes(url.origin)) {
|
|
254
|
+
throw new ConnectorError("connector_destination_invalid", "The provider operation has an invalid destination.");
|
|
255
|
+
}
|
|
256
|
+
if (connection.method === "api-key" && provider.apiKey.pathPrefix) {
|
|
257
|
+
const prefix = provider.apiKey.pathPrefix(connection.apiKey);
|
|
258
|
+
if (typeof prefix !== "string" || !prefix.startsWith("/") || prefix.startsWith("//") ||
|
|
259
|
+
/[?#\\]/.test(prefix) || prefix.split("/").some((part) => /^(?:\.|%2e){1,2}$/i.test(part))) {
|
|
260
|
+
throw new ConnectorError("connector_request_invalid", "This provider has an invalid credential path.");
|
|
261
|
+
}
|
|
262
|
+
// A pathname assignment cannot replace the already checked origin.
|
|
263
|
+
url.pathname = prefix + url.pathname;
|
|
264
|
+
}
|
|
265
|
+
if (connection.method === "api-key" && provider.apiKey.queryParameter) {
|
|
266
|
+
url.searchParams.set(provider.apiKey.queryParameter, connection.apiKey);
|
|
267
|
+
}
|
|
268
|
+
let body = request.body;
|
|
269
|
+
if (connection.method === "api-key" && provider.apiKey.bodyParameter) {
|
|
270
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || ["GET", "HEAD"].includes(request.method)) {
|
|
271
|
+
throw new ConnectorError("connector_request_invalid", "This provider requires a JSON request body.");
|
|
272
|
+
}
|
|
273
|
+
body = { ...body, [provider.apiKey.bodyParameter]: connection.apiKey };
|
|
274
|
+
}
|
|
275
|
+
const options = {
|
|
276
|
+
method: request.method,
|
|
277
|
+
body,
|
|
278
|
+
headers: { ...request.headers, Accept: "application/json", ...(connection.method === "api-key"
|
|
279
|
+
? provider.apiKey.headers?.(connection.apiKey, connection.settings || {})
|
|
280
|
+
: connection.method === "none" ? {} : {
|
|
281
|
+
...provider.oauthHeaders?.({ clientId: connection.clientId }), Authorization: `Bearer ${connection.tokens.accessToken}`
|
|
282
|
+
}) },
|
|
283
|
+
redirect: "error",
|
|
284
|
+
signal: requestOptions(signal, provider.requestTimeoutMs).signal
|
|
285
|
+
};
|
|
286
|
+
let result;
|
|
287
|
+
try {
|
|
288
|
+
result = provider.exchange
|
|
289
|
+
? await provider.exchange(url.href, options, { fetchImpl, request: http.request, resolveReference,
|
|
290
|
+
settings: connection.settings || {}, apiKey: connection.method === "api-key" ? connection.apiKey : undefined })
|
|
291
|
+
: await http.request(url.href, options);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// Transports can wrap cancellation in a network or protocol error.
|
|
294
|
+
options.signal.throwIfAborted();
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
if (typeof operation.validateResult === "function" && !operation.validateResult(result)) {
|
|
298
|
+
throw new ConnectorError("connector_response_invalid", "The provider returned an unexpected response.", { statusCode: 502 });
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function tokensFrom(response, previous, requestedScopes, scopeSeparator = " ", provider) {
|
|
304
|
+
return {
|
|
305
|
+
...(provider?.dataFromTokenResponse ? { providerData: provider.dataFromTokenResponse(response, previous?.providerData) } : {}),
|
|
306
|
+
tokens: {
|
|
307
|
+
accessToken: response.access_token,
|
|
308
|
+
refreshToken: response.refresh_token || previous?.tokens.refreshToken || null,
|
|
309
|
+
expiresAt: response.expires_in === undefined ? null : now() + response.expires_in * 1000
|
|
310
|
+
},
|
|
311
|
+
grantedScopes: response.scope === undefined
|
|
312
|
+
? [...(previous?.grantedScopes || requestedScopes)]
|
|
313
|
+
: response.scope.split(scopeSeparator).map((scope) => scope.trim()).filter(Boolean)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function verifyOAuthConnection(provider, connection, verificationInput, requestedScopes, signal) {
|
|
318
|
+
const verification = await request(provider, provider.checkOperation, verificationInput, connection, signal);
|
|
319
|
+
if (provider.accountLabelFromVerification) {
|
|
320
|
+
const label = provider.accountLabelFromVerification(verification);
|
|
321
|
+
if (typeof label !== "string" || !label.trim() || label.length > 256 || /[\p{Cc}\p{Cf}]/u.test(label)) {
|
|
322
|
+
throw new ConnectorError("connector_response_invalid", "The provider returned an invalid account label.", { statusCode: 502 });
|
|
323
|
+
}
|
|
324
|
+
connection.accountLabel = label;
|
|
325
|
+
}
|
|
326
|
+
if (!provider.grantedScopesFromVerification) return;
|
|
327
|
+
const scopes = provider.grantedScopesFromVerification(verification, { clientId: connection.clientId });
|
|
328
|
+
if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string" || !scope)) {
|
|
329
|
+
throw new ConnectorError("connector_response_invalid", "The provider returned an invalid permission grant.", { statusCode: 502 });
|
|
330
|
+
}
|
|
331
|
+
connection.grantedScopes = connection.grantedScopes.filter((scope) => requestedScopes.includes(scope) && scopes.includes(scope));
|
|
332
|
+
const required = provider.operations[provider.checkOperation].scopes;
|
|
333
|
+
if (required.length && !required.some((scope) => connection.grantedScopes.includes(scope))) {
|
|
334
|
+
throw new ConnectorError("connector_scope_missing", "Connect again with the permission required for verification.", { statusCode: 403 });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function clientCredentialsGrant(registration, provider, settings, scopes, signal) {
|
|
339
|
+
const response = await oauth.clientCredentialsGrantRequest(
|
|
340
|
+
registration.oauth, registration.client, registration.clientAuth,
|
|
341
|
+
{ scope: scopes.join(provider.scopeSeparator || " "), ...(registration.resource ? { resource: registration.resource } : {}) },
|
|
342
|
+
requestOptions(signal)
|
|
343
|
+
);
|
|
344
|
+
const result = await oauth.processClientCredentialsResponse(registration.oauth, registration.client,
|
|
345
|
+
provider.normalizeTokenResponse ? await provider.normalizeTokenResponse(response, { settings, grantType: "client_credentials" }) : response);
|
|
346
|
+
const grant = tokensFrom(result, null, scopes, provider.scopeSeparator, provider);
|
|
347
|
+
// Client credentials renew with the application's secret, never a user refresh token.
|
|
348
|
+
grant.tokens.refreshToken = null;
|
|
349
|
+
grant.grantedScopes = grant.grantedScopes.filter((scope) => scopes.includes(scope));
|
|
350
|
+
return grant;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function connectClientCredentials({ context, integrationId, verificationInput = {}, signal }) {
|
|
354
|
+
verificationInput = structuredClone(verificationInput);
|
|
355
|
+
const { owner, integration, provider } = await access(context, integrationId, "connect");
|
|
356
|
+
const registration = await registrationFor(integration, provider);
|
|
357
|
+
if (registration.grantType !== "client_credentials") {
|
|
358
|
+
throw new ConnectorError("connector_mode_unavailable", "Select client credentials to connect a service account.");
|
|
359
|
+
}
|
|
360
|
+
provider.operations[provider.checkOperation].request(verificationInput, integration.settings || {});
|
|
361
|
+
const required = provider.operations[provider.checkOperation].scopes;
|
|
362
|
+
if (required.length && !required.some((scope) => integration.scopes.includes(scope))) {
|
|
363
|
+
throw new ConnectorError("connector_scope_missing", "Connection verification requires a permission absent from the application's configuration.", { statusCode: 403 });
|
|
364
|
+
}
|
|
365
|
+
return store.withConnection({ owner, integrationId }, async ({ save }) => {
|
|
366
|
+
try {
|
|
367
|
+
const connection = {
|
|
368
|
+
integrationId, provider: provider.id, registrationRef: registration.registrationRef,
|
|
369
|
+
clientId: registration.client.client_id, tokenEndpointAuthMethod: registration.tokenEndpointAuthMethod,
|
|
370
|
+
grantType: registration.grantType, requestedScopes: [...integration.scopes],
|
|
371
|
+
settings: structuredClone(integration.settings || {}), status: "connected", verifiedAt: now(),
|
|
372
|
+
...await clientCredentialsGrant(registration, provider, integration.settings || {}, integration.scopes, signal)
|
|
373
|
+
};
|
|
374
|
+
await verifyOAuthConnection(provider, connection, verificationInput, integration.scopes, signal);
|
|
375
|
+
await save(connection);
|
|
376
|
+
return publicConnection(connection);
|
|
377
|
+
} catch (error) { throw providerError(error); }
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function beginAuthorization({ context, integrationId, verificationInput = {}, signal }) {
|
|
382
|
+
const { owner, integration, provider } = await access(context, integrationId, "connect");
|
|
383
|
+
// Resource-specific providers validate the first document ID before opening consent.
|
|
384
|
+
provider.operations[provider.checkOperation].request(verificationInput, integration.settings || {});
|
|
385
|
+
const registration = await registrationFor(integration, provider);
|
|
386
|
+
if (registration.grantType !== "authorization_code") {
|
|
387
|
+
throw new ConnectorError("connector_mode_unavailable", "Service accounts connect without browser consent.");
|
|
388
|
+
}
|
|
389
|
+
signal?.throwIfAborted();
|
|
390
|
+
const state = oauth.generateRandomState();
|
|
391
|
+
const codeVerifier = provider.oauthPkce === false ? null : oauth.generateRandomCodeVerifier();
|
|
392
|
+
const url = new URL(registration.oauth.authorization_endpoint);
|
|
393
|
+
for (const [key, value] of Object.entries(provider.authorizationParameters || {})) url.searchParams.set(key, value);
|
|
394
|
+
if (registration.resource) url.searchParams.set("resource", registration.resource);
|
|
395
|
+
url.searchParams.set(provider.oauthClientIdParameter || "client_id", registration.client.client_id);
|
|
396
|
+
url.searchParams.set("redirect_uri", registration.callbackUrl);
|
|
397
|
+
url.searchParams.set("response_type", "code");
|
|
398
|
+
const scopeParameter = provider.authorizationScopeParameter?.(integration.settings || {}) || "scope";
|
|
399
|
+
if (integration.scopes.length) url.searchParams.set(scopeParameter, integration.scopes.join(provider.scopeSeparator || " "));
|
|
400
|
+
url.searchParams.set("state", state);
|
|
401
|
+
if (codeVerifier !== null) {
|
|
402
|
+
url.searchParams.set("code_challenge", await oauth.calculatePKCECodeChallenge(codeVerifier));
|
|
403
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
404
|
+
}
|
|
405
|
+
const expiresAt = now() + 10 * 60 * 1000;
|
|
406
|
+
await store.withConnection({ owner, integrationId }, async ({ putAttempt }) => putAttempt({
|
|
407
|
+
state, owner, integrationId, provider: provider.id,
|
|
408
|
+
clientId: registration.client.client_id, tokenEndpointAuthMethod: registration.tokenEndpointAuthMethod,
|
|
409
|
+
registrationRef: registration.registrationRef,
|
|
410
|
+
grantType: registration.grantType,
|
|
411
|
+
callbackUrl: registration.callbackUrl,
|
|
412
|
+
scopes: [...integration.scopes], settings: structuredClone(integration.settings || {}),
|
|
413
|
+
verificationInput: structuredClone(verificationInput), authorizationUrl: url.href, codeVerifier, expiresAt
|
|
414
|
+
}));
|
|
415
|
+
return { authorizationUrl: url.href, expiresAt, callbackUrl: registration.callbackUrl };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function resumeAuthorization({ context, integrationId }) {
|
|
419
|
+
const { owner, integration, provider } = await access(context, integrationId, "connect");
|
|
420
|
+
if (integration.authentication.method !== "oauth2") return null;
|
|
421
|
+
const registration = await registrationFor(integration, provider);
|
|
422
|
+
if (registration.grantType !== "authorization_code") return null;
|
|
423
|
+
return store.withConnection({ owner, integrationId }, async ({ latestAttempt }) => {
|
|
424
|
+
const attempt = await latestAttempt({ after: now() });
|
|
425
|
+
if (!attempt || attempt.provider !== provider.id ||
|
|
426
|
+
attempt.clientId !== registration.client.client_id ||
|
|
427
|
+
attempt.registrationRef !== registration.registrationRef ||
|
|
428
|
+
attempt.callbackUrl !== registration.callbackUrl ||
|
|
429
|
+
attempt.tokenEndpointAuthMethod !== registration.tokenEndpointAuthMethod ||
|
|
430
|
+
!isDeepStrictEqual(attempt.scopes, integration.scopes) ||
|
|
431
|
+
!isDeepStrictEqual(attempt.settings, integration.settings || {})) return null;
|
|
432
|
+
return { authorizationUrl: attempt.authorizationUrl, expiresAt: attempt.expiresAt, callbackUrl: registration.callbackUrl };
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function completeAuthorization({ context, integrationId, callbackUrl, signal }) {
|
|
437
|
+
const { owner, integration, provider } = await access(context, integrationId, "connect");
|
|
438
|
+
const registration = await registrationFor(integration, provider);
|
|
439
|
+
if (registration.grantType !== "authorization_code") {
|
|
440
|
+
throw new ConnectorError("connector_mode_unavailable", "Service accounts do not use an authorization callback.");
|
|
441
|
+
}
|
|
442
|
+
let url;
|
|
443
|
+
try { url = new URL(callbackUrl); } catch {
|
|
444
|
+
throw new ConnectorError("connector_callback_invalid", "The authorization callback is invalid.");
|
|
445
|
+
}
|
|
446
|
+
if (`${url.origin}${url.pathname}` !== registration.callbackUrl || url.hash || url.username || url.password) {
|
|
447
|
+
throw new ConnectorError("connector_callback_invalid", "The callback does not match this registration.");
|
|
448
|
+
}
|
|
449
|
+
const state = url.searchParams.get("state");
|
|
450
|
+
if (!state || url.searchParams.getAll("state").length !== 1) {
|
|
451
|
+
throw new ConnectorError("connector_attempt_invalid", "The authorization attempt is invalid or has expired.");
|
|
452
|
+
}
|
|
453
|
+
const outcome = await store.withConnection({ owner, integrationId }, async ({ consumeAttempt, save }) => {
|
|
454
|
+
const attempt = await consumeAttempt(state);
|
|
455
|
+
try {
|
|
456
|
+
if (!attempt || attempt.expiresAt <= now() || attempt.provider !== provider.id ||
|
|
457
|
+
(attempt.codeVerifier === null) !== (provider.oauthPkce === false) ||
|
|
458
|
+
attempt.clientId !== registration.client.client_id || attempt.registrationRef !== registration.registrationRef ||
|
|
459
|
+
(attempt.grantType || "authorization_code") !== registration.grantType ||
|
|
460
|
+
(attempt.tokenEndpointAuthMethod || "client_secret_post") !== registration.tokenEndpointAuthMethod ||
|
|
461
|
+
attempt.callbackUrl !== registration.callbackUrl || JSON.stringify(attempt.scopes) !== JSON.stringify(integration.scopes) ||
|
|
462
|
+
!isDeepStrictEqual(attempt.settings || {}, integration.settings || {})) {
|
|
463
|
+
throw new ConnectorError("connector_attempt_invalid", "The authorization attempt is invalid or has expired.");
|
|
464
|
+
}
|
|
465
|
+
const parameters = oauth.validateAuthResponse(registration.oauth, registration.client, url, state);
|
|
466
|
+
let fallbackScopes = attempt.scopes;
|
|
467
|
+
if (provider.scopesInAuthorizationResponse) {
|
|
468
|
+
const values = parameters.getAll("scope");
|
|
469
|
+
if (values.length !== 1) {
|
|
470
|
+
throw new ConnectorError("connector_response_invalid", "The provider did not return an unambiguous permission grant.", { statusCode: 502 });
|
|
471
|
+
}
|
|
472
|
+
const granted = new Set(values[0].split(provider.scopeSeparator || " ").map((scope) => scope.trim()).filter(Boolean));
|
|
473
|
+
fallbackScopes = attempt.scopes.filter((scope) => granted.has(scope));
|
|
474
|
+
}
|
|
475
|
+
const response = await oauth.authorizationCodeGrantRequest(
|
|
476
|
+
registration.oauth, registration.client, registration.clientAuth,
|
|
477
|
+
parameters, registration.callbackUrl, attempt.codeVerifier === null ? oauth.nopkce : attempt.codeVerifier, { ...requestOptions(signal, undefined, provider.tokenRequestEncoding),
|
|
478
|
+
...(registration.resource ? { additionalParameters: { resource: registration.resource } } : {}) }
|
|
479
|
+
);
|
|
480
|
+
const result = await oauth.processAuthorizationCodeResponse(registration.oauth, registration.client,
|
|
481
|
+
provider.normalizeTokenResponse ? await provider.normalizeTokenResponse(response, { settings: integration.settings || {}, grantType: "authorization_code" }) : response);
|
|
482
|
+
const connection = {
|
|
483
|
+
integrationId, provider: provider.id, registrationRef: registration.registrationRef,
|
|
484
|
+
grantType: registration.grantType,
|
|
485
|
+
clientId: registration.client.client_id, tokenEndpointAuthMethod: registration.tokenEndpointAuthMethod, status: "connected", verifiedAt: now(),
|
|
486
|
+
settings: structuredClone(integration.settings || {}),
|
|
487
|
+
...(provider.refreshRequiresRedirectUri ? { callbackUrl: registration.callbackUrl } : {}),
|
|
488
|
+
...tokensFrom(result, null, fallbackScopes, provider.scopeSeparator, provider)
|
|
489
|
+
};
|
|
490
|
+
connection.grantedScopes = connection.grantedScopes.filter((scope) => integration.scopes.includes(scope));
|
|
491
|
+
await verifyOAuthConnection(provider, connection, attempt.verificationInput || {}, attempt.scopes, signal);
|
|
492
|
+
await save(connection);
|
|
493
|
+
return { result: publicConnection(connection) };
|
|
494
|
+
} catch (error) {
|
|
495
|
+
// Commit one-time consumption even when consent or the provider request fails.
|
|
496
|
+
return { failure: providerError(error) };
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
if (outcome.failure) throw outcome.failure;
|
|
500
|
+
return outcome.result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function verifyDirectConnection({ context, integrationId, verificationInput = {}, signal }, method) {
|
|
504
|
+
const { owner, integration, provider } = await access(context, integrationId, "connect");
|
|
505
|
+
if (integration.authentication.method !== method || (method === "api-key" &&
|
|
506
|
+
(typeof provider.apiKey?.headers !== "function" && !provider.apiKey?.queryParameter &&
|
|
507
|
+
!provider.apiKey?.bodyParameter && typeof provider.apiKey?.pathPrefix !== "function"))) {
|
|
508
|
+
throw new ConnectorError("connector_mode_unavailable", "Select the matching credential mode to verify this connection.");
|
|
509
|
+
}
|
|
510
|
+
const requiredScopes = provider.operations[provider.checkOperation].scopes;
|
|
511
|
+
if (requiredScopes.length && !requiredScopes.some((scope) => integration.scopes.includes(scope))) {
|
|
512
|
+
throw new ConnectorError("connector_scope_missing", "Connection verification requires a permission absent from the application's configuration.", { statusCode: 403 });
|
|
513
|
+
}
|
|
514
|
+
return store.withConnection({ owner, integrationId }, async ({ save }) => {
|
|
515
|
+
const connection = {
|
|
516
|
+
integrationId, provider: provider.id, method,
|
|
517
|
+
...(method === "api-key" && integration.authentication.secretRef ? { secretRef: integration.authentication.secretRef } : {}),
|
|
518
|
+
grantedScopes: [], status: "connected", verifiedAt: now(), settings: structuredClone(integration.settings || {})
|
|
519
|
+
};
|
|
520
|
+
try {
|
|
521
|
+
const credentials = method === "api-key" ? { ...connection, apiKey: await apiKeyFor(integration, provider) } : connection;
|
|
522
|
+
await request(provider, provider.checkOperation, verificationInput, credentials, signal);
|
|
523
|
+
if (method === "api-key") connection.credentialFingerprint = createHash("sha256").update(credentials.apiKey).digest("hex");
|
|
524
|
+
await save(connection);
|
|
525
|
+
return publicConnection(connection);
|
|
526
|
+
} catch (error) { throw providerError(error); }
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const connectApiKey = (input) => verifyDirectConnection(input, "api-key");
|
|
531
|
+
const connectWithoutCredentials = (input) => verifyDirectConnection(input, "none");
|
|
532
|
+
|
|
533
|
+
async function invoke({ context, integrationId, operation, input = {}, signal }) {
|
|
534
|
+
// Authorize and execute the same input even if the caller changes its object while awaiting policy.
|
|
535
|
+
input = structuredClone(input);
|
|
536
|
+
const { owner, integration, provider } = await access(context, integrationId, operation, input);
|
|
537
|
+
if (!Object.hasOwn(provider.operations, operation)) {
|
|
538
|
+
throw new ConnectorError("connector_operation_unknown", "This provider operation is not available.");
|
|
539
|
+
}
|
|
540
|
+
if (provider.operations[operation].scopes.length && !provider.operations[operation].scopes.some((scope) => integration.scopes.includes(scope))) {
|
|
541
|
+
throw new ConnectorError("connector_scope_missing", "This operation requires a permission absent from the application's configuration.", { statusCode: 403 });
|
|
542
|
+
}
|
|
543
|
+
const usesApiKey = integration.authentication.method === "api-key";
|
|
544
|
+
const usesServiceAccount = integration.authentication.method === "service-account";
|
|
545
|
+
const usesOAuth = integration.authentication.method === "oauth2";
|
|
546
|
+
const registration = usesOAuth ? await registrationFor(integration, provider) : null;
|
|
547
|
+
const outcome = await store.withConnection({ owner, integrationId }, async ({ connection, save }) => {
|
|
548
|
+
if (!connection || connection.status !== "connected" || connection.provider !== provider.id ||
|
|
549
|
+
(connection.method || "oauth2") !== integration.authentication.method ||
|
|
550
|
+
!isDeepStrictEqual(connection.settings || {}, integration.settings || {}) ||
|
|
551
|
+
((usesApiKey || usesServiceAccount) && connection.secretRef !== integration.authentication.secretRef) ||
|
|
552
|
+
(usesServiceAccount && !isDeepStrictEqual(connection.requestedScopes, integration.scopes)) ||
|
|
553
|
+
(usesOAuth && (connection.registrationRef !== registration.registrationRef || connection.clientId !== registration.client.client_id ||
|
|
554
|
+
(connection.grantType || "authorization_code") !== registration.grantType ||
|
|
555
|
+
(registration.grantType === "client_credentials" && !isDeepStrictEqual(connection.requestedScopes, integration.scopes)) ||
|
|
556
|
+
(registration.grantType === "authorization_code" && provider.refreshRequiresRedirectUri && connection.callbackUrl !== registration.callbackUrl) ||
|
|
557
|
+
(connection.tokenEndpointAuthMethod || "client_secret_post") !== registration.tokenEndpointAuthMethod))) {
|
|
558
|
+
throw new ConnectorError("connector_reconnect_required", "Connect this account again.", { statusCode: 401 });
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
if (usesServiceAccount) {
|
|
562
|
+
const binding = await serviceAccountCredentialFor(integration);
|
|
563
|
+
if (binding.fingerprint !== connection.credentialFingerprint || connection.tokens.expiresAt <= now() + (provider.tokenRefreshLeewayMs ?? 30_000)) {
|
|
564
|
+
connection = { ...connection, ...await serviceAccountGrant(integration, provider, binding, signal) };
|
|
565
|
+
await save(connection);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (usesOAuth && connection.tokens.expiresAt !== null && connection.tokens.expiresAt <= now() + (provider.tokenRefreshLeewayMs ?? 30_000)) {
|
|
569
|
+
let grant;
|
|
570
|
+
if (registration.grantType === "client_credentials") {
|
|
571
|
+
grant = await clientCredentialsGrant(registration, provider, integration.settings || {}, connection.grantedScopes, signal);
|
|
572
|
+
} else {
|
|
573
|
+
if (!connection.tokens.refreshToken) {
|
|
574
|
+
throw new ConnectorError("connector_reconnect_required", "Connect this account again.", { statusCode: 401 });
|
|
575
|
+
}
|
|
576
|
+
const response = await oauth.refreshTokenGrantRequest(
|
|
577
|
+
registration.oauth, registration.client, registration.clientAuth,
|
|
578
|
+
connection.tokens.refreshToken, { ...requestOptions(signal, undefined, provider.tokenRequestEncoding),
|
|
579
|
+
additionalParameters: {
|
|
580
|
+
...(registration.resource ? { resource: registration.resource } : {}),
|
|
581
|
+
...(provider.refreshRequiresRedirectUri ? { redirect_uri: connection.callbackUrl } : {})
|
|
582
|
+
} }
|
|
583
|
+
);
|
|
584
|
+
const refreshed = await oauth.processRefreshTokenResponse(registration.oauth, registration.client,
|
|
585
|
+
provider.normalizeTokenResponse ? await provider.normalizeTokenResponse(response, { settings: integration.settings || {}, grantType: "refresh_token" }) : response);
|
|
586
|
+
grant = tokensFrom(refreshed, connection, integration.scopes, provider.scopeSeparator, provider);
|
|
587
|
+
}
|
|
588
|
+
grant.grantedScopes = grant.grantedScopes.filter((scope) =>
|
|
589
|
+
connection.grantedScopes.includes(scope) && integration.scopes.includes(scope));
|
|
590
|
+
connection = { ...connection, ...grant };
|
|
591
|
+
await save(connection);
|
|
592
|
+
}
|
|
593
|
+
const credentials = usesApiKey ? { ...connection, apiKey: await apiKeyFor(integration, provider) } : connection;
|
|
594
|
+
const result = await request(provider, operation, input, credentials, signal);
|
|
595
|
+
await save({ ...connection, verifiedAt: now(),
|
|
596
|
+
...(usesApiKey ? { credentialFingerprint: createHash("sha256").update(credentials.apiKey).digest("hex") } : {})
|
|
597
|
+
});
|
|
598
|
+
return { result };
|
|
599
|
+
} catch (error) {
|
|
600
|
+
const failure = providerError(error);
|
|
601
|
+
if (failure.code === "connector_reconnect_required") await save({ ...connection, status: "reconnect-required" });
|
|
602
|
+
// Commit rotated tokens and reconnect state even when the API request fails.
|
|
603
|
+
return { failure };
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
if (outcome.failure) throw outcome.failure;
|
|
607
|
+
return outcome.result;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function status({ context, integrationId }) {
|
|
611
|
+
const { owner, integration, provider } = await access(context, integrationId, "status");
|
|
612
|
+
const registration = config.registrations[integration.authentication.registrationRef];
|
|
613
|
+
return store.withConnection({ owner, integrationId }, async ({ connection }) => {
|
|
614
|
+
if (connection && (connection.provider !== integration.provider ||
|
|
615
|
+
(connection.method || "oauth2") !== integration.authentication.method ||
|
|
616
|
+
!isDeepStrictEqual(connection.settings || {}, integration.settings || {}) ||
|
|
617
|
+
(["api-key", "service-account"].includes(integration.authentication.method) && connection.secretRef !== integration.authentication.secretRef) ||
|
|
618
|
+
(integration.authentication.method === "service-account" && !isDeepStrictEqual(connection.requestedScopes, integration.scopes)) ||
|
|
619
|
+
(integration.authentication.method === "oauth2" && (connection.registrationRef !== integration.authentication.registrationRef || connection.clientId !== registration?.clientId ||
|
|
620
|
+
(connection.grantType || "authorization_code") !== (registration?.grantType || "authorization_code") ||
|
|
621
|
+
(registration?.grantType === "client_credentials" && !isDeepStrictEqual(connection.requestedScopes, integration.scopes)) ||
|
|
622
|
+
(connection.tokenEndpointAuthMethod || "client_secret_post") !== (registration?.tokenEndpointAuthMethod || "client_secret_post"))))) {
|
|
623
|
+
return publicConnection({ ...connection, status: "reconnect-required" });
|
|
624
|
+
}
|
|
625
|
+
let callbackUrl;
|
|
626
|
+
try {
|
|
627
|
+
if (integration.authentication.method === "oauth2") {
|
|
628
|
+
const resolved = await registrationFor(integration, provider);
|
|
629
|
+
callbackUrl = resolved.callbackUrl;
|
|
630
|
+
if (connection && provider.refreshRequiresRedirectUri && resolved.grantType === "authorization_code" &&
|
|
631
|
+
connection.callbackUrl !== resolved.callbackUrl) {
|
|
632
|
+
return publicConnection({ ...connection, status: "reconnect-required" });
|
|
633
|
+
}
|
|
634
|
+
} else if (integration.authentication.method === "api-key") {
|
|
635
|
+
const key = await apiKeyFor(integration, provider);
|
|
636
|
+
if (connection && connection.credentialFingerprint !== createHash("sha256").update(key).digest("hex")) {
|
|
637
|
+
return publicConnection({ ...connection, status: "reconnect-required" });
|
|
638
|
+
}
|
|
639
|
+
} else if (integration.authentication.method === "service-account") await serviceAccountCredentialFor(integration);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (!["connector_binding_missing", "connector_callback_invalid"].includes(error.code)) throw error;
|
|
642
|
+
if (!connection) return { status: "unconfigured", configurationError: error.code };
|
|
643
|
+
return publicConnection({ ...connection, status: "reconnect-required" });
|
|
644
|
+
}
|
|
645
|
+
return { ...publicConnection(connection), ...(callbackUrl ? { callbackUrl } : {}) };
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function disconnect({ context, integrationId }) {
|
|
650
|
+
const { owner } = await access(context, integrationId, "disconnect");
|
|
651
|
+
await store.withConnection({ owner, integrationId }, async ({ remove }) => remove());
|
|
652
|
+
return { status: "disconnected" };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function cancelAuthorization({ context, integrationId, state }) {
|
|
656
|
+
const { owner } = await access(context, integrationId, "connect", undefined, false);
|
|
657
|
+
await store.withConnection({ owner, integrationId }, async ({ consumeAttempt }) => consumeAttempt(state));
|
|
658
|
+
return { status: "cancelled" };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return Object.freeze({ beginAuthorization, resumeAuthorization, completeAuthorization, cancelAuthorization, connectClientCredentials, connectServiceAccount, connectApiKey, connectWithoutCredentials, invoke, status, disconnect, authorizeAssistantAction });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export { createConnectionService };
|