@jskit-ai/connectors-core 0.2.3 → 0.2.4
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 +6 -1
- package/docs/setup-command.md +25 -25
- package/locales/en.json +0 -1
- package/package.json +4 -3
- package/src/server/ConnectorsFeature.js +65 -18
- package/src/server/connectionCredentials.js +129 -0
- package/src/server/connectionService.js +104 -319
- package/src/server/credentialProtection.js +31 -20
- package/src/server/environmentReferences.js +10 -1
- package/src/server/errors.js +27 -7
- package/src/server/fileConnectionStore.js +10 -0
- package/src/server/index.js +1 -1
- package/src/server/knexConnectionStore.js +13 -2
- package/src/server/providerRequests.js +291 -0
- package/src/server/types.js +150 -0
- package/src/shared/configuration.js +23 -12
- package/test/connectionService.test.js +10 -8
- package/test/serviceAccount.test.js +4 -2
- package/test/setupCommand.test.js +9 -8
- /package/docs/{online-setup.md → application-setup.md} +0 -0
|
@@ -116,8 +116,8 @@ const MESSAGE_KEY_SOURCE = Symbol.for("@jskit-ai/connectors-core.messageKeySourc
|
|
|
116
116
|
|
|
117
117
|
// Provider definitions carry message keys, which connectors-web translates. Code that
|
|
118
118
|
// shows a definition's texts itself gets the same definitions with every key (and every
|
|
119
|
-
// { key, params } a function returns) rendered to the catalog's English
|
|
120
|
-
//
|
|
119
|
+
// { key, params } a function returns) rendered to the catalog's English text. Each
|
|
120
|
+
// English object keeps its keyed original under MESSAGE_KEY_SOURCE.
|
|
121
121
|
function createEnglishProviderTexts(catalog) {
|
|
122
122
|
const rendered = new WeakMap();
|
|
123
123
|
const englishMessage = (key, params) => {
|
|
@@ -168,25 +168,31 @@ function providerGrantTypes(provider) {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
function getProviderSettingsSchema(provider, settings = {}) {
|
|
171
|
+
// readable-check: allow method-probe - data: a provider definition gives its settings schema as a schema or as a function of the settings
|
|
171
172
|
return typeof provider.settingsSchema === "function" ? provider.settingsSchema(settings) : provider.settingsSchema;
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
function getProviderAuthenticationMethods(provider, settings = {}) {
|
|
175
|
-
|
|
176
|
+
// readable-check: allow method-probe - optional operation: authenticationMethodsForSettings is an optional hook of the provider definition
|
|
177
|
+
return typeof provider.authenticationMethodsForSettings === "function" ? provider.authenticationMethodsForSettings(settings) : provider.authenticationMethods;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
function getProviderAccountModes(provider, settings = {}) {
|
|
179
|
-
|
|
181
|
+
// readable-check: allow method-probe - optional operation: accountModesForSettings is an optional hook of the provider definition
|
|
182
|
+
return typeof provider.accountModesForSettings === "function" ? provider.accountModesForSettings(settings) : provider.accountModes;
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
function getProviderScopes(provider, settings = {}, grantType = DEFAULT_GRANT_TYPE, authenticationMethod) {
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
// readable-check: allow method-probe - optional operation: scopesForGrantType is an optional hook of the provider definition
|
|
187
|
+
const scopes = typeof provider.scopesForGrantType === "function" ? provider.scopesForGrantType(grantType, settings)
|
|
188
|
+
// readable-check: allow method-probe - optional operation: scopesForSettings is an optional hook of the provider definition
|
|
189
|
+
: typeof provider.scopesForSettings === "function" ? provider.scopesForSettings(settings) : provider.scopes;
|
|
185
190
|
return authenticationMethod ? scopes.filter(scope => !scope.authenticationMethods || scope.authenticationMethods.includes(authenticationMethod)) : scopes;
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
function getProviderClientAuthenticationMethods(provider, grantType = DEFAULT_GRANT_TYPE, settings = {}) {
|
|
189
194
|
const methods = provider.oauthClientAuthenticationMethods;
|
|
195
|
+
// readable-check: allow method-probe - data: a provider definition gives its client authentication methods as a list or as a function of the grant type
|
|
190
196
|
return typeof methods === "function" ? methods(grantType, settings) : methods || [DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD];
|
|
191
197
|
}
|
|
192
198
|
|
|
@@ -194,12 +200,15 @@ function getProviderClientAuthenticationMethods(provider, grantType = DEFAULT_GR
|
|
|
194
200
|
function getProviderSetup(provider, settings = {}, authenticationMethod) {
|
|
195
201
|
const setup = provider.setup || {};
|
|
196
202
|
return {
|
|
203
|
+
// readable-check: allow optional-call - data: a provider definition's setup may give its URL for the settings
|
|
197
204
|
url: setup.urlForSettings?.(settings, authenticationMethod) || setup.urlByAuthentication?.[authenticationMethod] || setup.url,
|
|
205
|
+
// readable-check: allow optional-call - data: a provider definition's setup may give its steps for the settings
|
|
198
206
|
steps: setup.stepsForSettings?.(settings, authenticationMethod) || setup.stepsByAuthentication?.[authenticationMethod] || setup.steps
|
|
199
207
|
};
|
|
200
208
|
}
|
|
201
209
|
|
|
202
210
|
function isProviderConfigurationOnly(provider, settings = {}) {
|
|
211
|
+
// readable-check: allow optional-call - data: a provider definition may say per settings whether it is configuration only
|
|
203
212
|
return Boolean(provider.configurationOnly || provider.configurationOnlyForSettings?.(settings));
|
|
204
213
|
}
|
|
205
214
|
|
|
@@ -296,15 +305,16 @@ function validateIntegrationConfiguration(input, { providers = [], allowUnknownP
|
|
|
296
305
|
Object.assign(fieldErrorCodes, codes);
|
|
297
306
|
}
|
|
298
307
|
}
|
|
308
|
+
const settings = integration.settings || {};
|
|
299
309
|
for (const field of provider.settingsFields || []) {
|
|
300
|
-
if (field.authenticationMethods && !field.authenticationMethods.includes(auth.method) && Object.hasOwn(
|
|
310
|
+
if (field.authenticationMethods && !field.authenticationMethods.includes(auth.method) && Object.hasOwn(settings, field.name)) {
|
|
301
311
|
fieldErrors[`${path}.settings.${field.name}`] = FIELD_ERRORS.otherCredentialModeSetting;
|
|
302
312
|
}
|
|
303
313
|
}
|
|
304
|
-
if (!getProviderAccountModes(provider,
|
|
314
|
+
if (!getProviderAccountModes(provider, settings).includes(integration.accountMode)) {
|
|
305
315
|
fieldErrors[`${path}.accountMode`] = FIELD_ERRORS.accountModeUnsupported;
|
|
306
316
|
}
|
|
307
|
-
if (!getProviderAuthenticationMethods(provider,
|
|
317
|
+
if (!getProviderAuthenticationMethods(provider, settings).includes(auth.method)) {
|
|
308
318
|
fieldErrors[`${path}.authentication.method`] = FIELD_ERRORS.credentialModeUnsupported;
|
|
309
319
|
}
|
|
310
320
|
if (auth.method === "oauth2" && registration?.source === "own") {
|
|
@@ -312,16 +322,17 @@ function validateIntegrationConfiguration(input, { providers = [], allowUnknownP
|
|
|
312
322
|
if (!providerGrantTypes(provider).includes(grantType)) {
|
|
313
323
|
fieldErrors[`${registrationPath}.grantType`] = FIELD_ERRORS.oauthFlowUnsupported;
|
|
314
324
|
}
|
|
315
|
-
const methods = getProviderClientAuthenticationMethods(provider, grantType,
|
|
325
|
+
const methods = getProviderClientAuthenticationMethods(provider, grantType, settings);
|
|
316
326
|
if (!methods.includes(registrationClientAuthentication(registration))) {
|
|
317
327
|
fieldErrors[`${registrationPath}.tokenEndpointAuthMethod`] = FIELD_ERRORS.clientAuthenticationUnsupported;
|
|
318
328
|
}
|
|
319
|
-
|
|
329
|
+
// readable-check: allow method-probe - optional operation: validateClientId is an optional hook of the provider definition
|
|
330
|
+
if (registration.clientId && typeof provider.validateClientId === "function") {
|
|
320
331
|
const result = provider.validateClientId(registration.clientId);
|
|
321
332
|
if (result !== true) fieldErrors[`${registrationPath}.clientId`] = result || FIELD_ERRORS.clientId;
|
|
322
333
|
}
|
|
323
334
|
}
|
|
324
|
-
const scopes = getProviderScopes(provider,
|
|
335
|
+
const scopes = getProviderScopes(provider, settings, grantType, auth.method);
|
|
325
336
|
const allowedScopes = new Set(scopes.map((scope) => scope.value));
|
|
326
337
|
if (scopes.some((scope) => scope.required && !integration.scopes.includes(scope.value))) {
|
|
327
338
|
fieldErrors[`${path}.scopes`] = FIELD_ERRORS.requiredScopes;
|
|
@@ -5,7 +5,9 @@ import assert from "node:assert/strict";
|
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import Fastify from "fastify";
|
|
7
7
|
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
8
|
-
import {
|
|
8
|
+
import { ActionProvider } from "@jskit-ai/kernel/server/actions";
|
|
9
|
+
import { EventProvider } from "@jskit-ai/kernel/server/runtime";
|
|
10
|
+
import { silentLogger } from "@jskit-ai/kernel/server/runtime/logger";
|
|
9
11
|
import { googleCalendarProvider } from "../../connector-google-calendar/src/server/provider.js";
|
|
10
12
|
import { createConnectionService, createConnectorsFeature, createEnvironmentReferenceResolver } from "../src/server/index.js";
|
|
11
13
|
import { integrationsSchema, parseIntegrationConfiguration, validateIntegrationConfiguration } from "../src/shared/configuration.js";
|
|
@@ -111,8 +113,8 @@ test("the ordinary credential-free Feature action verifies without resolving ref
|
|
|
111
113
|
return Response.json({ meta: [], data: [{ ok: 1, user: "default" }], rows: 1 });
|
|
112
114
|
}
|
|
113
115
|
};
|
|
114
|
-
const runtime = createCapabilityRuntime({ providers: [
|
|
115
|
-
|
|
116
|
+
const runtime = createCapabilityRuntime({ inputs: { "runtime.logger": silentLogger }, providers: [
|
|
117
|
+
EventProvider, ActionProvider, createConnectorsFeature(options),
|
|
116
118
|
defineProvider({ id: "test.credential-free-observer", requires: { catalogue: "runtime.actions" }, setup({ catalogue }) { actions = catalogue; } })
|
|
117
119
|
] });
|
|
118
120
|
await runtime.start();
|
|
@@ -395,7 +397,7 @@ test("cancelling a service token exchange preserves the previous grant without r
|
|
|
395
397
|
test("the client credentials Feature action uses the ordinary application policy and runtime", async () => {
|
|
396
398
|
const { options } = serviceAccountSetup();
|
|
397
399
|
let actions;
|
|
398
|
-
const runtime = createCapabilityRuntime({ providers: [
|
|
400
|
+
const runtime = createCapabilityRuntime({ inputs: { "runtime.logger": silentLogger }, providers: [EventProvider, ActionProvider, createConnectorsFeature(options),
|
|
399
401
|
defineProvider({ id: "test.service-observer", requires: { catalogue: "runtime.actions" }, setup({ catalogue }) { actions = catalogue; } })] });
|
|
400
402
|
await runtime.start();
|
|
401
403
|
try {
|
|
@@ -828,8 +830,8 @@ test("configuration rejects gateway registrations before constructing a runtime"
|
|
|
828
830
|
test("the Feature exposes ordinary JSKIT actions with application authorization", async () => {
|
|
829
831
|
const { options } = setup();
|
|
830
832
|
let actions;
|
|
831
|
-
const runtime = createCapabilityRuntime({ providers: [
|
|
832
|
-
|
|
833
|
+
const runtime = createCapabilityRuntime({ inputs: { "runtime.logger": silentLogger }, providers: [
|
|
834
|
+
EventProvider, ActionProvider, createConnectorsFeature(options),
|
|
833
835
|
defineProvider({
|
|
834
836
|
id: "test.observer", requires: { catalogue: "runtime.actions" },
|
|
835
837
|
setup({ catalogue }) { actions = catalogue; }
|
|
@@ -997,8 +999,8 @@ test("JSON-body credentials replace supplied values, preserve input and reach th
|
|
|
997
999
|
}
|
|
998
1000
|
};
|
|
999
1001
|
let actions;
|
|
1000
|
-
const runtime = createCapabilityRuntime({ providers: [
|
|
1001
|
-
|
|
1002
|
+
const runtime = createCapabilityRuntime({ inputs: { "runtime.logger": silentLogger }, providers: [
|
|
1003
|
+
EventProvider, ActionProvider, createConnectorsFeature(options),
|
|
1002
1004
|
defineProvider({ id: "test.body-observer", requires: { catalogue: "runtime.actions" }, setup({ catalogue }) { actions = catalogue; } })
|
|
1003
1005
|
] });
|
|
1004
1006
|
await runtime.start();
|
|
@@ -5,7 +5,9 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createSchema } from "json-rest-schema";
|
|
7
7
|
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
8
|
-
import {
|
|
8
|
+
import { ActionProvider } from "@jskit-ai/kernel/server/actions";
|
|
9
|
+
import { EventProvider } from "@jskit-ai/kernel/server/runtime";
|
|
10
|
+
import { silentLogger } from "@jskit-ai/kernel/server/runtime/logger";
|
|
9
11
|
import { createConnectionService, createConnectorsFeature } from "../src/server/index.js";
|
|
10
12
|
import { ConnectorError } from "../src/server/errors.js";
|
|
11
13
|
import { createFileConnectionStore, createCredentialProtection } from "../src/server/fileStorage.js";
|
|
@@ -182,7 +184,7 @@ test("service grants honor cancellation and deadlines without replay or partial
|
|
|
182
184
|
|
|
183
185
|
test("the service-account Feature action uses the same application policy and verified runtime", async (t) => {
|
|
184
186
|
const f = await fixture(t); let actions;
|
|
185
|
-
const runtime = createCapabilityRuntime({ providers: [
|
|
187
|
+
const runtime = createCapabilityRuntime({ inputs: { "runtime.logger": silentLogger }, providers: [EventProvider, ActionProvider, createConnectorsFeature(f.options),
|
|
186
188
|
defineProvider({ id: "test.service-observer", requires: { catalogue: "runtime.actions" }, setup({ catalogue }) { actions = catalogue; } })] });
|
|
187
189
|
await runtime.start();
|
|
188
190
|
try {
|
|
@@ -16,9 +16,10 @@ const guide = await readFile(new URL("../docs/setup-command.md", import.meta.url
|
|
|
16
16
|
const source = guide.match(/```js\n([\s\S]*?)\n```/u)?.[1];
|
|
17
17
|
assert.ok(source, "The setup guide must contain its dispatch example.");
|
|
18
18
|
const dispatchSetup = new Function(`${source}\nreturn dispatchSetup;`)();
|
|
19
|
+
const protocol = "example-host.integration-setup.command.v1";
|
|
19
20
|
const scope = "https://www.googleapis.com/auth/calendar.calendarlist.readonly";
|
|
20
21
|
const callback = "http://127.0.0.1:8080/integrations/google/callback";
|
|
21
|
-
const context = { applicationId: "
|
|
22
|
+
const context = { applicationId: "bookings-dev", subjectId: "business-account" };
|
|
22
23
|
|
|
23
24
|
async function fixture(t) {
|
|
24
25
|
const directory = await mkdtemp(path.join(tmpdir(), "setup-composition-"));
|
|
@@ -72,9 +73,9 @@ async function fixture(t) {
|
|
|
72
73
|
}
|
|
73
74
|
});
|
|
74
75
|
const dispatch = async (operation, integrationId, extra = {}, identity = context) => {
|
|
75
|
-
const result = await dispatchSetup({ protocol
|
|
76
|
+
const result = await dispatchSetup({ protocol,
|
|
76
77
|
requestId: "fixture-request", operation, integrationId, ...extra },
|
|
77
|
-
{ configuration, connections: restart(), context: identity });
|
|
78
|
+
{ protocol, configuration, connections: restart(), context: identity });
|
|
78
79
|
assert.equal(result.requestId, "fixture-request");
|
|
79
80
|
assert.equal(JSON.stringify(result).includes("private-"), false);
|
|
80
81
|
return result;
|
|
@@ -123,14 +124,14 @@ try {
|
|
|
123
124
|
return Response.json({ object: "list", data: [], has_more: false });
|
|
124
125
|
}
|
|
125
126
|
});
|
|
126
|
-
const result = await dispatchSetup(JSON.parse(input), { configuration, connections, context });
|
|
127
|
+
const result = await dispatchSetup(JSON.parse(input), { protocol: ${JSON.stringify(protocol)}, configuration, connections, context });
|
|
127
128
|
process.stdout.write(JSON.stringify(result) + "\\n");
|
|
128
129
|
} catch {
|
|
129
130
|
process.stderr.write("Integration setup failed.\\n");
|
|
130
131
|
process.exitCode = 1;
|
|
131
132
|
}
|
|
132
133
|
`);
|
|
133
|
-
const request = { protocol
|
|
134
|
+
const request = { protocol, requestId: "cli-request", integrationId: "mail" };
|
|
134
135
|
for (const [operation, key, applicationId, expected, allowCheck] of [
|
|
135
136
|
["status", "MISSING", "first-app", "unconfigured", false],
|
|
136
137
|
["connect", "MISSING", "first-app", "unconfigured", false],
|
|
@@ -220,7 +221,7 @@ test("moving application state preserves grants and refresh while invalidating c
|
|
|
220
221
|
const moved = `${directory}-relocated`;
|
|
221
222
|
t.after(() => rm(moved, { recursive: true, force: true }));
|
|
222
223
|
await rename(directory, moved);
|
|
223
|
-
env.CALLBACK = "https://
|
|
224
|
+
env.CALLBACK = "https://bookings.example/integrations/google/callback";
|
|
224
225
|
const relocated = restart(moved);
|
|
225
226
|
const requestCount = requests.length;
|
|
226
227
|
assert.equal(await relocated.resumeAuthorization(input), null);
|
|
@@ -235,7 +236,7 @@ test("moving application state preserves grants and refresh while invalidating c
|
|
|
235
236
|
"oauth2.googleapis.com", "www.googleapis.com"
|
|
236
237
|
]);
|
|
237
238
|
assert.equal((await restart(moved).status(input)).status, "connected");
|
|
238
|
-
const otherEnvironment = { ...context, applicationId: "
|
|
239
|
+
const otherEnvironment = { ...context, applicationId: "bookings-production" };
|
|
239
240
|
assert.equal((await relocated.status({ ...input, context: otherEnvironment })).status, "disconnected");
|
|
240
241
|
await relocated.disconnect({ ...input, context: otherEnvironment });
|
|
241
242
|
assert.equal((await relocated.status(input)).status, "connected");
|
|
@@ -259,7 +260,7 @@ test("documented OAuth setup resumes across restarts, preserves a grant on cance
|
|
|
259
260
|
const callbackUrl = `${callback}?code=fixture-code&state=${pending.attemptId}`;
|
|
260
261
|
await restart().completeAuthorization({ context, integrationId: "calendar", callbackUrl });
|
|
261
262
|
assert.equal((await dispatch("status", "calendar")).status, "connected");
|
|
262
|
-
env.CALLBACK = "https://
|
|
263
|
+
env.CALLBACK = "https://bookings.example/integrations/google/callback";
|
|
263
264
|
assert.equal((await dispatch("status", "calendar")).callbackUrl, env.CALLBACK);
|
|
264
265
|
env.CALLBACK = callback;
|
|
265
266
|
const replacement = await dispatch("connect", "calendar");
|
|
File without changes
|