@jskit-ai/connectors-core 0.2.3 → 0.2.5

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.
@@ -92,17 +92,23 @@ const FIELD_ERRORS = Object.freeze({
92
92
  clientId: { code: "connectors.configuration.clientId", message: "Check this provider's client identifier." },
93
93
  requiredScopes: { code: "connectors.configuration.requiredScopes", message: "Include this provider's required permissions." },
94
94
  scopeUnsupported: { code: "connectors.configuration.scopeUnsupported", message: "A selected permission is not supported by this provider configuration." },
95
- scopeRequired: { code: "connectors.configuration.scopeRequired", message: "Select at least one permission." }
95
+ scopeRequired: { code: "connectors.configuration.scopeRequired", message: "Select at least one permission." },
96
+ secretReference: { code: "connectors.configuration.secretReference", message: "Use a reference such as env:VARIABLE_NAME." }
96
97
  });
97
98
 
98
- // A schema validation error's field errors with the codes the validator gave them.
99
+ // A schema validation error's field errors with the codes the validator gave them. A
100
+ // credential reference that fails secretReference's pattern says what a reference looks like.
99
101
  function schemaFieldErrors(error, fallback, prefix = "") {
100
102
  const messages = {};
101
103
  const codes = {};
102
104
  for (const [field, message] of Object.entries(error.fieldErrors || fallback)) {
103
- messages[`${prefix}${field}`] = message;
104
105
  const coded = error.fieldErrorCodes?.[field];
105
- if (coded) codes[`${prefix}${field}`] = coded;
106
+ if (coded?.code === "kernel.validation.field.pattern" && coded.params.pattern === secretReference.pattern) {
107
+ messages[`${prefix}${field}`] = FIELD_ERRORS.secretReference;
108
+ } else {
109
+ messages[`${prefix}${field}`] = message;
110
+ if (coded) codes[`${prefix}${field}`] = coded;
111
+ }
106
112
  }
107
113
  return { messages, codes };
108
114
  }
@@ -116,8 +122,8 @@ const MESSAGE_KEY_SOURCE = Symbol.for("@jskit-ai/connectors-core.messageKeySourc
116
122
 
117
123
  // Provider definitions carry message keys, which connectors-web translates. Code that
118
124
  // 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, exactly as
120
- // before keys. Each English object keeps its keyed original under MESSAGE_KEY_SOURCE.
125
+ // { key, params } a function returns) rendered to the catalog's English text. Each
126
+ // English object keeps its keyed original under MESSAGE_KEY_SOURCE.
121
127
  function createEnglishProviderTexts(catalog) {
122
128
  const rendered = new WeakMap();
123
129
  const englishMessage = (key, params) => {
@@ -168,25 +174,31 @@ function providerGrantTypes(provider) {
168
174
  }
169
175
 
170
176
  function getProviderSettingsSchema(provider, settings = {}) {
177
+ // readable-check: allow method-probe - data: a provider definition gives its settings schema as a schema or as a function of the settings
171
178
  return typeof provider.settingsSchema === "function" ? provider.settingsSchema(settings) : provider.settingsSchema;
172
179
  }
173
180
 
174
181
  function getProviderAuthenticationMethods(provider, settings = {}) {
175
- return provider.authenticationMethodsForSettings ? provider.authenticationMethodsForSettings(settings) : provider.authenticationMethods;
182
+ // readable-check: allow method-probe - optional operation: authenticationMethodsForSettings is an optional hook of the provider definition
183
+ return typeof provider.authenticationMethodsForSettings === "function" ? provider.authenticationMethodsForSettings(settings) : provider.authenticationMethods;
176
184
  }
177
185
 
178
186
  function getProviderAccountModes(provider, settings = {}) {
179
- return provider.accountModesForSettings ? provider.accountModesForSettings(settings) : provider.accountModes;
187
+ // readable-check: allow method-probe - optional operation: accountModesForSettings is an optional hook of the provider definition
188
+ return typeof provider.accountModesForSettings === "function" ? provider.accountModesForSettings(settings) : provider.accountModes;
180
189
  }
181
190
 
182
191
  function getProviderScopes(provider, settings = {}, grantType = DEFAULT_GRANT_TYPE, authenticationMethod) {
183
- const scopes = provider.scopesForGrantType ? provider.scopesForGrantType(grantType, settings)
184
- : provider.scopesForSettings ? provider.scopesForSettings(settings) : provider.scopes;
192
+ // readable-check: allow method-probe - optional operation: scopesForGrantType is an optional hook of the provider definition
193
+ const scopes = typeof provider.scopesForGrantType === "function" ? provider.scopesForGrantType(grantType, settings)
194
+ // readable-check: allow method-probe - optional operation: scopesForSettings is an optional hook of the provider definition
195
+ : typeof provider.scopesForSettings === "function" ? provider.scopesForSettings(settings) : provider.scopes;
185
196
  return authenticationMethod ? scopes.filter(scope => !scope.authenticationMethods || scope.authenticationMethods.includes(authenticationMethod)) : scopes;
186
197
  }
187
198
 
188
199
  function getProviderClientAuthenticationMethods(provider, grantType = DEFAULT_GRANT_TYPE, settings = {}) {
189
200
  const methods = provider.oauthClientAuthenticationMethods;
201
+ // 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
202
  return typeof methods === "function" ? methods(grantType, settings) : methods || [DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD];
191
203
  }
192
204
 
@@ -194,12 +206,15 @@ function getProviderClientAuthenticationMethods(provider, grantType = DEFAULT_GR
194
206
  function getProviderSetup(provider, settings = {}, authenticationMethod) {
195
207
  const setup = provider.setup || {};
196
208
  return {
209
+ // readable-check: allow optional-call - data: a provider definition's setup may give its URL for the settings
197
210
  url: setup.urlForSettings?.(settings, authenticationMethod) || setup.urlByAuthentication?.[authenticationMethod] || setup.url,
211
+ // readable-check: allow optional-call - data: a provider definition's setup may give its steps for the settings
198
212
  steps: setup.stepsForSettings?.(settings, authenticationMethod) || setup.stepsByAuthentication?.[authenticationMethod] || setup.steps
199
213
  };
200
214
  }
201
215
 
202
216
  function isProviderConfigurationOnly(provider, settings = {}) {
217
+ // readable-check: allow optional-call - data: a provider definition may say per settings whether it is configuration only
203
218
  return Boolean(provider.configurationOnly || provider.configurationOnlyForSettings?.(settings));
204
219
  }
205
220
 
@@ -296,15 +311,16 @@ function validateIntegrationConfiguration(input, { providers = [], allowUnknownP
296
311
  Object.assign(fieldErrorCodes, codes);
297
312
  }
298
313
  }
314
+ const settings = integration.settings || {};
299
315
  for (const field of provider.settingsFields || []) {
300
- if (field.authenticationMethods && !field.authenticationMethods.includes(auth.method) && Object.hasOwn(integration.settings || {}, field.name)) {
316
+ if (field.authenticationMethods && !field.authenticationMethods.includes(auth.method) && Object.hasOwn(settings, field.name)) {
301
317
  fieldErrors[`${path}.settings.${field.name}`] = FIELD_ERRORS.otherCredentialModeSetting;
302
318
  }
303
319
  }
304
- if (!getProviderAccountModes(provider, integration.settings || {}).includes(integration.accountMode)) {
320
+ if (!getProviderAccountModes(provider, settings).includes(integration.accountMode)) {
305
321
  fieldErrors[`${path}.accountMode`] = FIELD_ERRORS.accountModeUnsupported;
306
322
  }
307
- if (!getProviderAuthenticationMethods(provider, integration.settings).includes(auth.method)) {
323
+ if (!getProviderAuthenticationMethods(provider, settings).includes(auth.method)) {
308
324
  fieldErrors[`${path}.authentication.method`] = FIELD_ERRORS.credentialModeUnsupported;
309
325
  }
310
326
  if (auth.method === "oauth2" && registration?.source === "own") {
@@ -312,16 +328,17 @@ function validateIntegrationConfiguration(input, { providers = [], allowUnknownP
312
328
  if (!providerGrantTypes(provider).includes(grantType)) {
313
329
  fieldErrors[`${registrationPath}.grantType`] = FIELD_ERRORS.oauthFlowUnsupported;
314
330
  }
315
- const methods = getProviderClientAuthenticationMethods(provider, grantType, integration.settings || {});
331
+ const methods = getProviderClientAuthenticationMethods(provider, grantType, settings);
316
332
  if (!methods.includes(registrationClientAuthentication(registration))) {
317
333
  fieldErrors[`${registrationPath}.tokenEndpointAuthMethod`] = FIELD_ERRORS.clientAuthenticationUnsupported;
318
334
  }
319
- if (registration.clientId && provider.validateClientId) {
335
+ // readable-check: allow method-probe - optional operation: validateClientId is an optional hook of the provider definition
336
+ if (registration.clientId && typeof provider.validateClientId === "function") {
320
337
  const result = provider.validateClientId(registration.clientId);
321
338
  if (result !== true) fieldErrors[`${registrationPath}.clientId`] = result || FIELD_ERRORS.clientId;
322
339
  }
323
340
  }
324
- const scopes = getProviderScopes(provider, integration.settings || {}, grantType, auth.method);
341
+ const scopes = getProviderScopes(provider, settings, grantType, auth.method);
325
342
  const allowedScopes = new Set(scopes.map((scope) => scope.value));
326
343
  if (scopes.some((scope) => scope.required && !integration.scopes.includes(scope.value))) {
327
344
  fieldErrors[`${path}.scopes`] = FIELD_ERRORS.requiredScopes;
@@ -5,11 +5,15 @@ 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 { createActionProvider } from "@jskit-ai/kernel/server/actions";
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";
12
14
  import { clickhouseProvider } from "../../connectors-catalog/src/server/clickhouse.js";
15
+ import { algoliaDefinition } from "../../connectors-catalog/src/shared/algolia.js";
16
+ import { firebaseCloudMessagingDefinition, firebaseMessagingScope } from "../../connectors-catalog/src/shared/firebase-cloud-messaging.js";
13
17
 
14
18
  const listScope = "https://www.googleapis.com/auth/calendar.calendarlist.readonly";
15
19
  const eventsScope = "https://www.googleapis.com/auth/calendar.events.readonly";
@@ -111,8 +115,8 @@ test("the ordinary credential-free Feature action verifies without resolving ref
111
115
  return Response.json({ meta: [], data: [{ ok: 1, user: "default" }], rows: 1 });
112
116
  }
113
117
  };
114
- const runtime = createCapabilityRuntime({ providers: [
115
- createActionProvider(), createConnectorsFeature(options),
118
+ const runtime = createCapabilityRuntime({ inputs: { "runtime.logger": silentLogger }, providers: [
119
+ EventProvider, ActionProvider, createConnectorsFeature(options),
116
120
  defineProvider({ id: "test.credential-free-observer", requires: { catalogue: "runtime.actions" }, setup({ catalogue }) { actions = catalogue; } })
117
121
  ] });
118
122
  await runtime.start();
@@ -395,7 +399,7 @@ test("cancelling a service token exchange preserves the previous grant without r
395
399
  test("the client credentials Feature action uses the ordinary application policy and runtime", async () => {
396
400
  const { options } = serviceAccountSetup();
397
401
  let actions;
398
- const runtime = createCapabilityRuntime({ providers: [createActionProvider(), createConnectorsFeature(options),
402
+ const runtime = createCapabilityRuntime({ inputs: { "runtime.logger": silentLogger }, providers: [EventProvider, ActionProvider, createConnectorsFeature(options),
399
403
  defineProvider({ id: "test.service-observer", requires: { catalogue: "runtime.actions" }, setup({ catalogue }) { actions = catalogue; } })] });
400
404
  await runtime.start();
401
405
  try {
@@ -487,6 +491,44 @@ test("reference fields reject pasted web URLs while preserving environment and c
487
491
  }
488
492
  });
489
493
 
494
+ function assertReferenceFieldError(input, providers, field) {
495
+ assert.throws(() => validateIntegrationConfiguration(input, { providers }), (error) => {
496
+ assert.equal(error.fieldErrors[field], "Use a reference such as env:VARIABLE_NAME.", field);
497
+ assert.deepEqual(error.fieldErrorCodes[field], { code: "connectors.configuration.secretReference", params: {} }, field);
498
+ return true;
499
+ });
500
+ }
501
+
502
+ test("a secretRef that is not a reference says to use a reference such as env:VARIABLE_NAME", () => {
503
+ for (const value of ["https://x", "plain"]) {
504
+ assertReferenceFieldError(databaseConfiguration({ method: "api-key", secretRef: value }), [clickhouseProvider], "integrations.database.authentication.secretRef");
505
+ }
506
+ });
507
+
508
+ test("a registration clientSecretRef that is not a reference says to use a reference such as env:VARIABLE_NAME", () => {
509
+ for (const value of ["https://x", "plain"]) {
510
+ const input = configuration();
511
+ input.registrations.google.clientSecretRef = value;
512
+ assertReferenceFieldError(input, [googleCalendarProvider], "registrations.google.clientSecretRef");
513
+ }
514
+ });
515
+
516
+ test("a provider setting's reference says the same, while its other pattern failures keep the kernel's sentence", () => {
517
+ const input = { schemaVersion: 1, registrations: {}, integrations: {
518
+ search: { provider: "algolia", accountMode: "shared", scopes: [], authentication: { method: "api-key", secretRef: "env:ALGOLIA_KEY" },
519
+ settings: { applicationId: "APP1", publicApiKeyRef: "raw-search-key" } },
520
+ push: { provider: "firebase-cloud-messaging", accountMode: "shared", scopes: [firebaseMessagingScope], authentication: { method: "service-account", secretRef: "env:FCM" },
521
+ settings: { projectId: "push-project", clientMode: "web", apiKey: "not-a-key" } }
522
+ } };
523
+ const providers = [algoliaDefinition, firebaseCloudMessagingDefinition];
524
+ assertReferenceFieldError(input, providers, "integrations.search.settings.publicApiKeyRef");
525
+ assert.throws(() => validateIntegrationConfiguration(input, { providers }), (error) => {
526
+ assert.equal(error.fieldErrors["integrations.push.settings.apiKey"], "Value does not match the required pattern.");
527
+ assert.equal(error.fieldErrorCodes["integrations.push.settings.apiKey"].code, "kernel.validation.field.pattern");
528
+ return true;
529
+ });
530
+ });
531
+
490
532
  test("the exported configuration contract enforces reference constraints for every credential field", async (t) => {
491
533
  const app = Fastify({ ajv: { plugins: [(ajv) => ajv.addKeyword("x-json-rest-schema")] } });
492
534
  t.after(() => app.close());
@@ -828,8 +870,8 @@ test("configuration rejects gateway registrations before constructing a runtime"
828
870
  test("the Feature exposes ordinary JSKIT actions with application authorization", async () => {
829
871
  const { options } = setup();
830
872
  let actions;
831
- const runtime = createCapabilityRuntime({ providers: [
832
- createActionProvider(), createConnectorsFeature(options),
873
+ const runtime = createCapabilityRuntime({ inputs: { "runtime.logger": silentLogger }, providers: [
874
+ EventProvider, ActionProvider, createConnectorsFeature(options),
833
875
  defineProvider({
834
876
  id: "test.observer", requires: { catalogue: "runtime.actions" },
835
877
  setup({ catalogue }) { actions = catalogue; }
@@ -997,8 +1039,8 @@ test("JSON-body credentials replace supplied values, preserve input and reach th
997
1039
  }
998
1040
  };
999
1041
  let actions;
1000
- const runtime = createCapabilityRuntime({ providers: [
1001
- createActionProvider(), createConnectorsFeature(options),
1042
+ const runtime = createCapabilityRuntime({ inputs: { "runtime.logger": silentLogger }, providers: [
1043
+ EventProvider, ActionProvider, createConnectorsFeature(options),
1002
1044
  defineProvider({ id: "test.body-observer", requires: { catalogue: "runtime.actions" }, setup({ catalogue }) { actions = catalogue; } })
1003
1045
  ] });
1004
1046
  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 { createActionProvider } from "@jskit-ai/kernel/server/actions";
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: [createActionProvider(), createConnectorsFeature(f.options),
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: "dogandgroom-dev", subjectId: "business-account" };
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: "vibe64.integration-setup.command.v1",
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: "vibe64.integration-setup.command.v1", requestId: "cli-request", integrationId: "mail" };
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://dogandgroom.example/integrations/google/callback";
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: "dogandgroom-production" };
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://dogandgroom.example/integrations/google/callback";
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