@jskit-ai/connectors-core 0.1.65 → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,183 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 - 2026-10-29
4
+
5
+ - Translation ready: every user-visible text is a message key with its English in `locales/en.json`, so an app translates it by supplying translation files only. With no translations, texts and behaviour are unchanged. See the guide "Translating a JSKIT app".
6
+ - `ConnectorError` carries `messageCode`/`messageParams`; `MESSAGE_KEY_SOURCE` links an English provider definition to its keyed original.
7
+ - `createEnglishProviderTexts(catalog)` renders a keyed provider definition to its English.
8
+
9
+ ### New message keys (80)
10
+
11
+ All keys in `locales/en.json` (exported as `./locales/en.json`) are new in this version.
12
+
13
+ <details><summary>Key list</summary>
14
+
15
+ - `jskit.connectors.configuration.accountModeUnsupported`
16
+ - `jskit.connectors.configuration.apiKeyRegistration`
17
+ - `jskit.connectors.configuration.assistantActionUndeclared`
18
+ - `jskit.connectors.configuration.clientAuthenticationUnsupported`
19
+ - `jskit.connectors.configuration.clientId`
20
+ - `jskit.connectors.configuration.confidentialClientRequired`
21
+ - `jskit.connectors.configuration.credentialModeUnsupported`
22
+ - `jskit.connectors.configuration.duplicateScope`
23
+ - `jskit.connectors.configuration.invalid`
24
+ - `jskit.connectors.configuration.invalidJson`
25
+ - `jskit.connectors.configuration.noCredentials`
26
+ - `jskit.connectors.configuration.oauthFlowUnsupported`
27
+ - `jskit.connectors.configuration.otherCredentialModeField`
28
+ - `jskit.connectors.configuration.otherCredentialModeSetting`
29
+ - `jskit.connectors.configuration.providerSettings`
30
+ - `jskit.connectors.configuration.providerUnregistered`
31
+ - `jskit.connectors.configuration.refreshTokenClientCredentials`
32
+ - `jskit.connectors.configuration.refreshTokenOAuthOnly`
33
+ - `jskit.connectors.configuration.refreshTokenSharedOnly`
34
+ - `jskit.connectors.configuration.registrationSecret`
35
+ - `jskit.connectors.configuration.registrationUndefined`
36
+ - `jskit.connectors.configuration.requiredScopes`
37
+ - `jskit.connectors.configuration.schemaVersionUnsupported`
38
+ - `jskit.connectors.configuration.scopeRequired`
39
+ - `jskit.connectors.configuration.scopeUnsupported`
40
+ - `jskit.connectors.configuration.serviceAccountPerUser`
41
+ - `jskit.connectors.configuration.serviceAccountRegistration`
42
+ - `jskit.connectors.configuration.stableName`
43
+ - `jskit.connectors.configuration.valueRequired`
44
+ - `jskit.connectors.connection.ambiguousPermissionGrant`
45
+ - `jskit.connectors.connection.apiKeyBindingMissingInvalid`
46
+ - `jskit.connectors.connection.assistantActionDisabled`
47
+ - `jskit.connectors.connection.authorizationAttemptInvalidExpired`
48
+ - `jskit.connectors.connection.authorizationCallbackInvalid`
49
+ - `jskit.connectors.connection.callbackDoesNotMatchRegistration`
50
+ - `jskit.connectors.connection.callbackUrlDoesNotMeet`
51
+ - `jskit.connectors.connection.checkOperationInputValues`
52
+ - `jskit.connectors.connection.configureHttpsCallbackUrlLocal`
53
+ - `jskit.connectors.connection.connectAccountAgain`
54
+ - `jskit.connectors.connection.connectAgainPermissionRequiredOperation`
55
+ - `jskit.connectors.connection.connectAgainPermissionRequiredVerification`
56
+ - `jskit.connectors.connection.connectionNotAllowed`
57
+ - `jskit.connectors.connection.envRefreshTokenRejected`
58
+ - `jskit.connectors.connection.envSeeded`
59
+ - `jskit.connectors.connection.integrationNotConfigured`
60
+ - `jskit.connectors.connection.operationCancelled`
61
+ - `jskit.connectors.connection.operationRequiresDifferentMethod`
62
+ - `jskit.connectors.connection.operationRequiresPermissionAbsentApplication`
63
+ - `jskit.connectors.connection.providerClientIdNotConfigured`
64
+ - `jskit.connectors.connection.providerDeniedOperation`
65
+ - `jskit.connectors.connection.providerDoesNotDeclareAssistant`
66
+ - `jskit.connectors.connection.providerDoesNotImplementService`
67
+ - `jskit.connectors.connection.providerInvalidCredentialPath`
68
+ - `jskit.connectors.connection.providerNonPkceFlowRequires`
69
+ - `jskit.connectors.connection.providerOperationInvalidDestination`
70
+ - `jskit.connectors.connection.providerOperationNotAvailable`
71
+ - `jskit.connectors.connection.providerRequestFailedTryAgain`
72
+ - `jskit.connectors.connection.providerRequestLimitReached`
73
+ - `jskit.connectors.connection.providerRequiresJsonRequestBody`
74
+ - `jskit.connectors.connection.providerReturnedInvalidAccountLabel`
75
+ - `jskit.connectors.connection.providerReturnedInvalidPermissionGrant`
76
+ - `jskit.connectors.connection.providerReturnedInvalidServiceAccount`
77
+ - `jskit.connectors.connection.providerReturnedUnexpectedResponse`
78
+ - `jskit.connectors.connection.refreshTokenBindingMissingInvalid`
79
+ - `jskit.connectors.connection.requiredEnvironmentBindingMissing`
80
+ - `jskit.connectors.connection.requiredRegistrationBindingMissing`
81
+ - `jskit.connectors.connection.runtimeDoesNotYetSupport`
82
+ - `jskit.connectors.connection.selectClientCredentialsConnectService`
83
+ - `jskit.connectors.connection.selectMatchingCredentialModeVerify`
84
+ - `jskit.connectors.connection.selectServiceAccountCredentialsVerify`
85
+ - `jskit.connectors.connection.serviceAccountCredentialBindingMissing`
86
+ - `jskit.connectors.connection.serviceAccountsConnectWithoutBrowser`
87
+ - `jskit.connectors.connection.serviceAccountsDoNotUse`
88
+ - `jskit.connectors.connection.verificationScopeAbsent`
89
+ - `jskit.errors.connector_approval_required`
90
+ - `jskit.errors.connector_consent_denied`
91
+ - `jskit.errors.connector_credentials_unavailable`
92
+ - `jskit.errors.connector_provider_timeout`
93
+ - `jskit.errors.connector_storage_invalid`
94
+ - `jskit.errors.integration_configuration_invalid`
95
+
96
+ </details>
97
+
98
+ ### Codes (80)
99
+
100
+ Error, field-error and message codes this package sends, with their English. Undotted codes kept their existing values and render from `jskit.errors.<code>`.
101
+
102
+ | Code | English |
103
+ | --- | --- |
104
+ | `connectors.configuration.accountModeUnsupported` | This provider does not support this account mode. |
105
+ | `connectors.configuration.apiKeyRegistration` | API keys do not use an OAuth registration. |
106
+ | `connectors.configuration.assistantActionUndeclared` | This provider does not declare this assistant action. |
107
+ | `connectors.configuration.clientAuthenticationUnsupported` | This provider does not support this client authentication method. |
108
+ | `connectors.configuration.clientId` | Check this provider's client identifier. |
109
+ | `connectors.configuration.confidentialClientRequired` | Client credentials require a confidential client. |
110
+ | `connectors.configuration.credentialModeUnsupported` | This provider configuration does not support this credential mode. |
111
+ | `connectors.configuration.duplicateScope` | Select each permission only once. |
112
+ | `connectors.configuration.invalid` | Invalid configuration. |
113
+ | `connectors.configuration.invalidJson` | Enter valid JSON. |
114
+ | `connectors.configuration.noCredentials` | This mode does not use credentials. |
115
+ | `connectors.configuration.oauthFlowUnsupported` | This provider does not support this OAuth flow. |
116
+ | `connectors.configuration.otherCredentialModeField` | This field belongs to another credential mode. |
117
+ | `connectors.configuration.otherCredentialModeSetting` | This setting belongs to another credential mode. |
118
+ | `connectors.configuration.providerSettings` | Check this provider's settings. |
119
+ | `connectors.configuration.providerUnregistered` | Install and register this provider before using it. |
120
+ | `connectors.configuration.refreshTokenClientCredentials` | Client credentials do not use a refresh token. |
121
+ | `connectors.configuration.refreshTokenOAuthOnly` | Only an OAuth connection can start from a refresh token. |
122
+ | `connectors.configuration.refreshTokenSharedOnly` | Only a shared connection can start from a refresh token in Env. |
123
+ | `connectors.configuration.registrationSecret` | Use the registration's secret reference. |
124
+ | `connectors.configuration.registrationUndefined` | Select a registration defined in this file. |
125
+ | `connectors.configuration.requiredScopes` | Include this provider's required permissions. |
126
+ | `connectors.configuration.schemaVersionUnsupported` | Use numeric schemaVersion 1; other versions require an explicit migration. |
127
+ | `connectors.configuration.scopeRequired` | Select at least one permission. |
128
+ | `connectors.configuration.scopeUnsupported` | A selected permission is not supported by this provider configuration. |
129
+ | `connectors.configuration.serviceAccountPerUser` | A service account cannot connect as each app user. |
130
+ | `connectors.configuration.serviceAccountRegistration` | Service-account credentials do not use a browser OAuth registration. |
131
+ | `connectors.configuration.stableName` | Use a stable lowercase name containing letters, digits and hyphens. |
132
+ | `connectors.configuration.valueRequired` | This value is required. |
133
+ | `connectors.connection.ambiguousPermissionGrant` | The provider did not return an unambiguous permission grant. |
134
+ | `connectors.connection.apiKeyBindingMissingInvalid` | The API key binding is missing or invalid. |
135
+ | `connectors.connection.assistantActionDisabled` | This assistant action is disabled. |
136
+ | `connectors.connection.authorizationAttemptInvalidExpired` | The authorization attempt is invalid or has expired. |
137
+ | `connectors.connection.authorizationCallbackInvalid` | The authorization callback is invalid. |
138
+ | `connectors.connection.callbackDoesNotMatchRegistration` | The callback does not match this registration. |
139
+ | `connectors.connection.callbackUrlDoesNotMeet` | The callback URL does not meet this provider's requirements. |
140
+ | `connectors.connection.checkOperationInputValues` | Check the operation's input values. |
141
+ | `connectors.connection.configureHttpsCallbackUrlLocal` | Configure an HTTPS callback URL, or a local development callback. |
142
+ | `connectors.connection.connectAccountAgain` | Connect this account again. |
143
+ | `connectors.connection.connectAgainPermissionRequiredOperation` | Connect again with the permission required for this operation. |
144
+ | `connectors.connection.connectAgainPermissionRequiredVerification` | Connect again with the permission required for verification. |
145
+ | `connectors.connection.connectionNotAllowed` | You cannot use this connection. |
146
+ | `connectors.connection.envRefreshTokenRejected` | The refresh token in Env no longer works; replace that Env value. |
147
+ | `connectors.connection.envSeeded` | This connection comes from an Env value; unlink or replace that value instead. |
148
+ | `connectors.connection.integrationNotConfigured` | This integration is not configured. |
149
+ | `connectors.connection.operationCancelled` | The operation was cancelled. |
150
+ | `connectors.connection.operationRequiresDifferentMethod` | This operation requires a different connection method. |
151
+ | `connectors.connection.operationRequiresPermissionAbsentApplication` | This operation requires a permission absent from the application's configuration. |
152
+ | `connectors.connection.providerClientIdNotConfigured` | The provider client ID is not configured. |
153
+ | `connectors.connection.providerDeniedOperation` | The provider denied this operation. |
154
+ | `connectors.connection.providerDoesNotDeclareAssistant` | This provider does not declare this assistant action. |
155
+ | `connectors.connection.providerDoesNotImplementService` | This provider does not implement service-account authorization. |
156
+ | `connectors.connection.providerInvalidCredentialPath` | This provider has an invalid credential path. |
157
+ | `connectors.connection.providerNonPkceFlowRequires` | This provider's non-PKCE flow requires a confidential client. |
158
+ | `connectors.connection.providerOperationInvalidDestination` | The provider operation has an invalid destination. |
159
+ | `connectors.connection.providerOperationNotAvailable` | This provider operation is not available. |
160
+ | `connectors.connection.providerRequestFailedTryAgain` | The provider request failed. Try again. |
161
+ | `connectors.connection.providerRequestLimitReached` | The provider's request limit was reached. |
162
+ | `connectors.connection.providerRequiresJsonRequestBody` | This provider requires a JSON request body. |
163
+ | `connectors.connection.providerReturnedInvalidAccountLabel` | The provider returned an invalid account label. |
164
+ | `connectors.connection.providerReturnedInvalidPermissionGrant` | The provider returned an invalid permission grant. |
165
+ | `connectors.connection.providerReturnedInvalidServiceAccount` | The provider returned an invalid service-account token. |
166
+ | `connectors.connection.providerReturnedUnexpectedResponse` | The provider returned an unexpected response. |
167
+ | `connectors.connection.refreshTokenBindingMissingInvalid` | The refresh token binding is missing or invalid. |
168
+ | `connectors.connection.requiredEnvironmentBindingMissing` | A required environment binding is missing. |
169
+ | `connectors.connection.requiredRegistrationBindingMissing` | A required registration binding is missing. |
170
+ | `connectors.connection.runtimeDoesNotYetSupport` | This runtime does not yet support the selected credential mode. |
171
+ | `connectors.connection.selectClientCredentialsConnectService` | Select client credentials to connect a service account. |
172
+ | `connectors.connection.selectMatchingCredentialModeVerify` | Select the matching credential mode to verify this connection. |
173
+ | `connectors.connection.selectServiceAccountCredentialsVerify` | Select service-account credentials to verify this connection. |
174
+ | `connectors.connection.serviceAccountCredentialBindingMissing` | The service-account credential binding is missing or invalid. |
175
+ | `connectors.connection.serviceAccountsConnectWithoutBrowser` | Service accounts connect without browser consent. |
176
+ | `connectors.connection.serviceAccountsDoNotUse` | Service accounts do not use an authorization callback. |
177
+ | `connectors.connection.verificationScopeAbsent` | Connection verification requires a permission absent from the application's configuration. |
178
+ | `connector_approval_required` | Approve this assistant action before continuing. |
179
+ | `connector_consent_denied` | Account access was declined. |
180
+ | `connector_credentials_unavailable` | Stored credentials could not be opened. |
181
+ | `connector_provider_timeout` | The provider request timed out. Try again. |
182
+ | `connector_storage_invalid` | Connection state could not be read safely. |
183
+ | `integration_configuration_invalid` | Integration configuration is invalid. |
@@ -0,0 +1,92 @@
1
+ {
2
+ "jskit": {
3
+ "connectors": {
4
+ "configuration": {
5
+ "accountModeUnsupported": "This provider does not support this account mode.",
6
+ "apiKeyRegistration": "API keys do not use an OAuth registration.",
7
+ "assistantActionUndeclared": "This provider does not declare this assistant action.",
8
+ "clientAuthenticationUnsupported": "This provider does not support this client authentication method.",
9
+ "clientId": "Check this provider's client identifier.",
10
+ "confidentialClientRequired": "Client credentials require a confidential client.",
11
+ "credentialModeUnsupported": "This provider configuration does not support this credential mode.",
12
+ "duplicateScope": "Select each permission only once.",
13
+ "invalid": "Invalid configuration.",
14
+ "invalidJson": "Enter valid JSON.",
15
+ "noCredentials": "This mode does not use credentials.",
16
+ "oauthFlowUnsupported": "This provider does not support this OAuth flow.",
17
+ "otherCredentialModeField": "This field belongs to another credential mode.",
18
+ "otherCredentialModeSetting": "This setting belongs to another credential mode.",
19
+ "providerSettings": "Check this provider's settings.",
20
+ "providerUnregistered": "Install and register this provider before using it.",
21
+ "refreshTokenClientCredentials": "Client credentials do not use a refresh token.",
22
+ "refreshTokenOAuthOnly": "Only an OAuth connection can start from a refresh token.",
23
+ "refreshTokenSharedOnly": "Only a shared connection can start from a refresh token in Env.",
24
+ "registrationSecret": "Use the registration's secret reference.",
25
+ "registrationUndefined": "Select a registration defined in this file.",
26
+ "requiredScopes": "Include this provider's required permissions.",
27
+ "schemaVersionUnsupported": "Use numeric schemaVersion 1; other versions require an explicit migration.",
28
+ "scopeRequired": "Select at least one permission.",
29
+ "scopeUnsupported": "A selected permission is not supported by this provider configuration.",
30
+ "serviceAccountPerUser": "A service account cannot connect as each app user.",
31
+ "serviceAccountRegistration": "Service-account credentials do not use a browser OAuth registration.",
32
+ "stableName": "Use a stable lowercase name containing letters, digits and hyphens.",
33
+ "valueRequired": "This value is required."
34
+ },
35
+ "connection": {
36
+ "ambiguousPermissionGrant": "The provider did not return an unambiguous permission grant.",
37
+ "apiKeyBindingMissingInvalid": "The API key binding is missing or invalid.",
38
+ "assistantActionDisabled": "This assistant action is disabled.",
39
+ "authorizationAttemptInvalidExpired": "The authorization attempt is invalid or has expired.",
40
+ "authorizationCallbackInvalid": "The authorization callback is invalid.",
41
+ "callbackDoesNotMatchRegistration": "The callback does not match this registration.",
42
+ "callbackUrlDoesNotMeet": "The callback URL does not meet this provider's requirements.",
43
+ "checkOperationInputValues": "Check the operation's input values.",
44
+ "configureHttpsCallbackUrlLocal": "Configure an HTTPS callback URL, or a local development callback.",
45
+ "connectAccountAgain": "Connect this account again.",
46
+ "connectAgainPermissionRequiredOperation": "Connect again with the permission required for this operation.",
47
+ "connectAgainPermissionRequiredVerification": "Connect again with the permission required for verification.",
48
+ "connectionNotAllowed": "You cannot use this connection.",
49
+ "envRefreshTokenRejected": "The refresh token in Env no longer works; replace that Env value.",
50
+ "envSeeded": "This connection comes from an Env value; unlink or replace that value instead.",
51
+ "integrationNotConfigured": "This integration is not configured.",
52
+ "operationCancelled": "The operation was cancelled.",
53
+ "operationRequiresDifferentMethod": "This operation requires a different connection method.",
54
+ "operationRequiresPermissionAbsentApplication": "This operation requires a permission absent from the application's configuration.",
55
+ "providerClientIdNotConfigured": "The provider client ID is not configured.",
56
+ "providerDeniedOperation": "The provider denied this operation.",
57
+ "providerDoesNotDeclareAssistant": "This provider does not declare this assistant action.",
58
+ "providerDoesNotImplementService": "This provider does not implement service-account authorization.",
59
+ "providerInvalidCredentialPath": "This provider has an invalid credential path.",
60
+ "providerNonPkceFlowRequires": "This provider's non-PKCE flow requires a confidential client.",
61
+ "providerOperationInvalidDestination": "The provider operation has an invalid destination.",
62
+ "providerOperationNotAvailable": "This provider operation is not available.",
63
+ "providerRequestFailedTryAgain": "The provider request failed. Try again.",
64
+ "providerRequestLimitReached": "The provider's request limit was reached.",
65
+ "providerRequiresJsonRequestBody": "This provider requires a JSON request body.",
66
+ "providerReturnedInvalidAccountLabel": "The provider returned an invalid account label.",
67
+ "providerReturnedInvalidPermissionGrant": "The provider returned an invalid permission grant.",
68
+ "providerReturnedInvalidServiceAccount": "The provider returned an invalid service-account token.",
69
+ "providerReturnedUnexpectedResponse": "The provider returned an unexpected response.",
70
+ "refreshTokenBindingMissingInvalid": "The refresh token binding is missing or invalid.",
71
+ "requiredEnvironmentBindingMissing": "A required environment binding is missing.",
72
+ "requiredRegistrationBindingMissing": "A required registration binding is missing.",
73
+ "runtimeDoesNotYetSupport": "This runtime does not yet support the selected credential mode.",
74
+ "selectClientCredentialsConnectService": "Select client credentials to connect a service account.",
75
+ "selectMatchingCredentialModeVerify": "Select the matching credential mode to verify this connection.",
76
+ "selectServiceAccountCredentialsVerify": "Select service-account credentials to verify this connection.",
77
+ "serviceAccountCredentialBindingMissing": "The service-account credential binding is missing or invalid.",
78
+ "serviceAccountsConnectWithoutBrowser": "Service accounts connect without browser consent.",
79
+ "serviceAccountsDoNotUse": "Service accounts do not use an authorization callback.",
80
+ "verificationScopeAbsent": "Connection verification requires a permission absent from the application's configuration."
81
+ }
82
+ },
83
+ "errors": {
84
+ "connector_approval_required": "Approve this assistant action before continuing.",
85
+ "connector_consent_denied": "Account access was declined.",
86
+ "connector_credentials_unavailable": "Stored credentials could not be opened.",
87
+ "connector_provider_timeout": "The provider request timed out. Try again.",
88
+ "connector_storage_invalid": "Connection state could not be read safely.",
89
+ "integration_configuration_invalid": "Integration configuration is invalid."
90
+ }
91
+ }
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/connectors-core",
3
- "version": "0.1.65",
3
+ "version": "0.2.1",
4
4
  "description": "Portable integration configuration and server-side account connection runtime.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -10,7 +10,8 @@
10
10
  "./shared/configuration": "./src/shared/configuration.js",
11
11
  "./server": "./src/server/index.js",
12
12
  "./server/storage": "./src/server/storage.js",
13
- "./server/file-storage": "./src/server/fileStorage.js"
13
+ "./server/file-storage": "./src/server/fileStorage.js",
14
+ "./locales/en.json": "./locales/en.json"
14
15
  },
15
16
  "dependencies": {
16
17
  "jose": "^6.1.3",
@@ -53,7 +54,7 @@
53
54
  }
54
55
  },
55
56
  "peerDependencies": {
56
- "@jskit-ai/http-runtime": "0.1.246",
57
- "@jskit-ai/kernel": "0.1.248"
57
+ "@jskit-ai/http-runtime": "0.2.1",
58
+ "@jskit-ai/kernel": "0.2.1"
58
59
  }
59
60
  }
@@ -19,12 +19,14 @@ function publicConnection(connection) {
19
19
  }
20
20
 
21
21
  const ENV_SEEDED_MESSAGE = "This connection comes from an Env value; unlink or replace that value instead.";
22
+ const ENV_SEEDED_CODE = { messageCode: "connectors.connection.envSeeded" };
22
23
  const VERIFICATION_SCOPE_MESSAGE = "Connection verification requires a permission absent from the application's configuration.";
24
+ const VERIFICATION_SCOPE_CODE = { messageCode: "connectors.connection.verificationScopeAbsent" };
23
25
 
24
26
  // An operation without scopes always runs; otherwise one of its scopes must be present.
25
- function requireOperationScope(operation, scopes, message) {
27
+ function requireOperationScope(operation, scopes, message, { messageCode }) {
26
28
  if (operation.scopes.length && !operation.scopes.some((scope) => scopes.includes(scope))) {
27
- throw new ConnectorError("connector_scope_missing", message, { statusCode: 403 });
29
+ throw new ConnectorError("connector_scope_missing", message, { statusCode: 403, messageCode });
28
30
  }
29
31
  }
30
32
 
@@ -32,8 +34,9 @@ function requireOperationScope(operation, scopes, message) {
32
34
  function operationRequest(operation, input, settings, providerData) {
33
35
  try { return operation.request(input, settings || {}, providerData); } catch (error) {
34
36
  if (!error.fieldErrors) throw error;
35
- const failure = new ConnectorError("connector_input_invalid", "Check the operation's input values.", { statusCode: 422 });
37
+ const failure = new ConnectorError("connector_input_invalid", "Check the operation's input values.", { statusCode: 422, messageCode: "connectors.connection.checkOperationInputValues" });
36
38
  failure.fieldErrors = error.fieldErrors;
39
+ if (error.fieldErrorCodes) failure.fieldErrorCodes = error.fieldErrorCodes;
37
40
  throw failure;
38
41
  }
39
42
  }
@@ -43,7 +46,7 @@ function validateCallbackUrl(value) {
43
46
  try { url = new URL(value); } catch { /* reported below */ }
44
47
  const localHttp = url?.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
45
48
  if (!url || (!localHttp && url.protocol !== "https:") || url.username || url.password || url.hash || url.search) {
46
- throw new ConnectorError("connector_callback_invalid", "Configure an HTTPS callback URL, or a local development callback.");
49
+ throw new ConnectorError("connector_callback_invalid", "Configure an HTTPS callback URL, or a local development callback.", { messageCode: "connectors.connection.configureHttpsCallbackUrlLocal" });
47
50
  }
48
51
  return url.href;
49
52
  }
@@ -94,7 +97,7 @@ function createConnectionService({
94
97
  async function access(context, integrationId, operation, input, checkAssistantPermission = true) {
95
98
  const integration = config.integrations[integrationId];
96
99
  if (!Object.hasOwn(config.integrations, integrationId)) {
97
- throw new ConnectorError("connector_not_found", "This integration is not configured.", { statusCode: 404 });
100
+ throw new ConnectorError("connector_not_found", "This integration is not configured.", { statusCode: 404, messageCode: "connectors.connection.integrationNotConfigured" });
98
101
  }
99
102
  const baseDefinition = definitions.get(integration.provider);
100
103
  const definition = baseDefinition.runtimeForSettings
@@ -108,7 +111,7 @@ function createConnectionService({
108
111
  const policy = integration.assistantPolicy;
109
112
  const decision = policy?.enabled === false ? "never" : policy?.actions?.[action] || policy?.defaultPermission || "ask";
110
113
  if (decision === "never") {
111
- throw new ConnectorError("connector_access_denied", "This assistant action is disabled.", { statusCode: 403 });
114
+ throw new ConnectorError("connector_access_denied", "This assistant action is disabled.", { statusCode: 403, messageCode: "connectors.connection.assistantActionDisabled" });
112
115
  }
113
116
  assistantPermission = { action, decision };
114
117
  }
@@ -117,14 +120,14 @@ function createConnectionService({
117
120
  ...(input === undefined ? {} : { input: structuredClone(input) }) });
118
121
  if (!owner || typeof owner.applicationId !== "string" || !owner.applicationId ||
119
122
  typeof owner.subjectId !== "string" || !owner.subjectId) {
120
- throw new ConnectorError("connector_access_denied", "You cannot use this connection.", { statusCode: 403 });
123
+ throw new ConnectorError("connector_access_denied", "You cannot use this connection.", { statusCode: 403, messageCode: "connectors.connection.connectionNotAllowed" });
121
124
  }
122
125
  if (assistantPermission?.decision === "ask" && owner.approved !== true) {
123
126
  throw new ConnectorError("connector_approval_required", "Approve this assistant action before continuing.", { statusCode: 409 });
124
127
  }
125
128
  const methods = provider.operations?.[operation]?.authenticationMethods;
126
129
  if (methods && !methods.includes(integration.authentication.method)) {
127
- throw new ConnectorError("connector_mode_unavailable", "This operation requires a different connection method.");
130
+ throw new ConnectorError("connector_mode_unavailable", "This operation requires a different connection method.", { messageCode: "connectors.connection.operationRequiresDifferentMethod" });
128
131
  }
129
132
  return { owner: { applicationId: owner.applicationId, subjectId: owner.subjectId }, integration, provider };
130
133
  }
@@ -132,7 +135,7 @@ function createConnectionService({
132
135
  async function authorizeAssistantAction({ context, integrationId, action, input = {} }) {
133
136
  const provider = definitions.get(config.integrations[integrationId]?.provider);
134
137
  if (executionMode !== "assistant" || !provider?.assistantActions?.some((item) => item.value === action)) {
135
- throw new ConnectorError("connector_operation_unknown", "This provider does not declare this assistant action.");
138
+ throw new ConnectorError("connector_operation_unknown", "This provider does not declare this assistant action.", { messageCode: "connectors.connection.providerDoesNotDeclareAssistant" });
136
139
  }
137
140
  await access(context, integrationId, action, structuredClone(input));
138
141
  // The host performs its own lifecycle operation after authorization; no provider action is executed here.
@@ -141,15 +144,15 @@ function createConnectionService({
141
144
  async function registrationFor(integration, provider) {
142
145
  const registration = config.registrations[integration.authentication.registrationRef];
143
146
  if (registration?.source !== "own" || integration.authentication.method !== "oauth2" || !provider.oauth) {
144
- throw new ConnectorError("connector_mode_unavailable", "This runtime does not yet support the selected credential mode.");
147
+ throw new ConnectorError("connector_mode_unavailable", "This runtime does not yet support the selected credential mode.", { messageCode: "connectors.connection.runtimeDoesNotYetSupport" });
145
148
  }
146
149
  if (registration.clientId.trim() === "MISSING") {
147
- throw new ConnectorError("connector_binding_missing", "The provider client ID is not configured.");
150
+ throw new ConnectorError("connector_binding_missing", "The provider client ID is not configured.", { messageCode: "connectors.connection.providerClientIdNotConfigured" });
148
151
  }
149
152
  const grantType = registrationGrantType(registration);
150
153
  const tokenEndpointAuthMethod = registrationClientAuthentication(registration);
151
154
  if (provider.oauthPkce === false && tokenEndpointAuthMethod === "none") {
152
- throw new ConnectorError("connector_mode_unavailable", "This provider's non-PKCE flow requires a confidential client.");
155
+ throw new ConnectorError("connector_mode_unavailable", "This provider's non-PKCE flow requires a confidential client.", { messageCode: "connectors.connection.providerNonPkceFlowRequires" });
153
156
  }
154
157
  let clientSecret;
155
158
  let callback;
@@ -160,11 +163,11 @@ function createConnectionService({
160
163
  }
161
164
  if (grantType === "authorization_code") callback = await resolveReference(registration.callbackUrlRef);
162
165
  } catch {
163
- throw new ConnectorError("connector_binding_missing", "A required registration binding is missing.");
166
+ throw new ConnectorError("connector_binding_missing", "A required registration binding is missing.", { messageCode: "connectors.connection.requiredRegistrationBindingMissing" });
164
167
  }
165
168
  const callbackUrl = grantType === "authorization_code" ? validateCallbackUrl(callback) : undefined;
166
169
  if (callbackUrl && provider.validateCallbackUrl && provider.validateCallbackUrl(callbackUrl) !== true) {
167
- throw new ConnectorError("connector_callback_invalid", "The callback URL does not meet this provider's requirements.");
170
+ throw new ConnectorError("connector_callback_invalid", "The callback URL does not meet this provider's requirements.", { messageCode: "connectors.connection.callbackUrlDoesNotMeet" });
168
171
  }
169
172
  let clientAuth = tokenEndpointAuthMethod === "none" ? oauth.None()
170
173
  : tokenEndpointAuthMethod === "client_secret_basic" ? oauth.ClientSecretBasic(clientSecret) : oauth.ClientSecretPost(clientSecret);
@@ -196,7 +199,7 @@ function createConnectionService({
196
199
  if (typeof value !== "string" || (!provider.apiKeySecretOptional && !value.trim()) || value.trim() === "MISSING" || /[\r\n]/u.test(value)) throw new Error("Invalid key.");
197
200
  return value;
198
201
  } catch {
199
- throw new ConnectorError("connector_binding_missing", "The API key binding is missing or invalid.");
202
+ throw new ConnectorError("connector_binding_missing", "The API key binding is missing or invalid.", { messageCode: "connectors.connection.apiKeyBindingMissingInvalid" });
200
203
  }
201
204
  }
202
205
 
@@ -206,7 +209,7 @@ function createConnectionService({
206
209
  if (typeof credential !== "string" || !credential.trim() || credential.trim() === "MISSING" || credential.length > 65_536 || credential.includes("\0")) throw new Error("Invalid credential.");
207
210
  return { credential, fingerprint: createHash("sha256").update(credential).digest("hex") };
208
211
  } catch {
209
- throw new ConnectorError("connector_binding_missing", "The service-account credential binding is missing or invalid.");
212
+ throw new ConnectorError("connector_binding_missing", "The service-account credential binding is missing or invalid.", { messageCode: "connectors.connection.serviceAccountCredentialBindingMissing" });
210
213
  }
211
214
  }
212
215
 
@@ -220,7 +223,7 @@ function createConnectionService({
220
223
  if (!refreshToken || refreshToken === "MISSING" || /\s/u.test(refreshToken)) throw new Error("Invalid token.");
221
224
  return { refreshToken, fingerprint: createHash("sha256").update(refreshToken).digest("hex") };
222
225
  } catch {
223
- throw new ConnectorError("connector_binding_missing", "The refresh token binding is missing or invalid.");
226
+ throw new ConnectorError("connector_binding_missing", "The refresh token binding is missing or invalid.", { messageCode: "connectors.connection.refreshTokenBindingMissingInvalid" });
224
227
  }
225
228
  }
226
229
 
@@ -244,7 +247,7 @@ function createConnectionService({
244
247
 
245
248
  async function serviceAccountGrant(integration, provider, binding, signal) {
246
249
  if (typeof provider.serviceAccountGrant !== "function") {
247
- throw new ConnectorError("connector_mode_unavailable", "This provider does not implement service-account authorization.");
250
+ throw new ConnectorError("connector_mode_unavailable", "This provider does not implement service-account authorization.", { messageCode: "connectors.connection.providerDoesNotImplementService" });
248
251
  }
249
252
  const options = requestOptions(signal, provider.requestTimeoutMs);
250
253
  options.signal.throwIfAborted();
@@ -263,7 +266,7 @@ function createConnectionService({
263
266
  response.token_type?.toLowerCase?.() !== "bearer" ||
264
267
  !Number.isFinite(response.expires_in) || response.expires_in <= 0 || response.expires_in > 86_400 ||
265
268
  response.refresh_token !== undefined || (response.scope !== undefined && typeof response.scope !== "string")) {
266
- throw new ConnectorError("connector_response_invalid", "The provider returned an invalid service-account token.", { statusCode: 502 });
269
+ throw new ConnectorError("connector_response_invalid", "The provider returned an invalid service-account token.", { statusCode: 502, messageCode: "connectors.connection.providerReturnedInvalidServiceAccount" });
267
270
  }
268
271
  const grant = tokensFrom(response, null, integration.scopes, provider.scopeSeparator, provider);
269
272
  grant.grantedScopes = grant.grantedScopes.filter((scope) => integration.scopes.includes(scope));
@@ -274,10 +277,10 @@ function createConnectionService({
274
277
  verificationInput = structuredClone(verificationInput);
275
278
  const { owner, integration, provider } = await access(context, integrationId, "connect");
276
279
  if (integration.authentication.method !== "service-account" || typeof provider.serviceAccountGrant !== "function") {
277
- throw new ConnectorError("connector_mode_unavailable", "Select service-account credentials to verify this connection.");
280
+ throw new ConnectorError("connector_mode_unavailable", "Select service-account credentials to verify this connection.", { messageCode: "connectors.connection.selectServiceAccountCredentialsVerify" });
278
281
  }
279
282
  operationRequest(provider.operations[provider.checkOperation], verificationInput, integration.settings);
280
- requireOperationScope(provider.operations[provider.checkOperation], integration.scopes, VERIFICATION_SCOPE_MESSAGE);
283
+ requireOperationScope(provider.operations[provider.checkOperation], integration.scopes, VERIFICATION_SCOPE_MESSAGE, VERIFICATION_SCOPE_CODE);
281
284
  return store.withConnection({ owner, integrationId }, async ({ save }) => {
282
285
  try {
283
286
  const binding = await serviceAccountCredentialFor(integration);
@@ -309,22 +312,22 @@ function createConnectionService({
309
312
  async function request(provider, operationId, input, connection, signal) {
310
313
  const operation = provider.operations[operationId];
311
314
  if (!operation || !Object.hasOwn(provider.operations, operationId)) {
312
- throw new ConnectorError("connector_operation_unknown", "This provider operation is not available.");
315
+ throw new ConnectorError("connector_operation_unknown", "This provider operation is not available.", { messageCode: "connectors.connection.providerOperationNotAvailable" });
313
316
  }
314
317
  if (["oauth2", "service-account"].includes(connection.method || "oauth2")) {
315
- requireOperationScope(operation, connection.grantedScopes, "Connect again with the permission required for this operation.");
318
+ requireOperationScope(operation, connection.grantedScopes, "Connect again with the permission required for this operation.", { messageCode: "connectors.connection.connectAgainPermissionRequiredOperation" });
316
319
  }
317
320
  const descriptor = operationRequest(operation, input, connection.settings, connection.providerData);
318
321
  const url = new URL(descriptor.url);
319
322
  const origins = typeof provider.apiOrigins === "function" ? provider.apiOrigins(connection.settings || {}, connection.providerData) : provider.apiOrigins;
320
323
  if (url.protocol !== "https:" || url.username || url.password || !origins.includes(url.origin)) {
321
- throw new ConnectorError("connector_destination_invalid", "The provider operation has an invalid destination.");
324
+ throw new ConnectorError("connector_destination_invalid", "The provider operation has an invalid destination.", { messageCode: "connectors.connection.providerOperationInvalidDestination" });
322
325
  }
323
326
  if (connection.method === "api-key" && provider.apiKey.pathPrefix) {
324
327
  const prefix = provider.apiKey.pathPrefix(connection.apiKey);
325
328
  if (typeof prefix !== "string" || !prefix.startsWith("/") || prefix.startsWith("//") ||
326
329
  /[?#\\]/.test(prefix) || prefix.split("/").some((part) => /^(?:\.|%2e){1,2}$/i.test(part))) {
327
- throw new ConnectorError("connector_request_invalid", "This provider has an invalid credential path.");
330
+ throw new ConnectorError("connector_request_invalid", "This provider has an invalid credential path.", { messageCode: "connectors.connection.providerInvalidCredentialPath" });
328
331
  }
329
332
  // A pathname assignment cannot replace the already checked origin.
330
333
  url.pathname = prefix + url.pathname;
@@ -335,7 +338,7 @@ function createConnectionService({
335
338
  let body = descriptor.body;
336
339
  if (connection.method === "api-key" && provider.apiKey.bodyParameter) {
337
340
  if (!body || typeof body !== "object" || Array.isArray(body) || ["GET", "HEAD"].includes(descriptor.method)) {
338
- throw new ConnectorError("connector_request_invalid", "This provider requires a JSON request body.");
341
+ throw new ConnectorError("connector_request_invalid", "This provider requires a JSON request body.", { messageCode: "connectors.connection.providerRequiresJsonRequestBody" });
339
342
  }
340
343
  body = { ...body, [provider.apiKey.bodyParameter]: connection.apiKey };
341
344
  }
@@ -363,7 +366,7 @@ function createConnectionService({
363
366
  }
364
367
  // The validator also receives the operation's own request, so it can check the provider answered what was asked.
365
368
  if (typeof operation.validateResult === "function" && !operation.validateResult(result, descriptor)) {
366
- throw new ConnectorError("connector_response_invalid", "The provider returned an unexpected response.", { statusCode: 502 });
369
+ throw new ConnectorError("connector_response_invalid", "The provider returned an unexpected response.", { statusCode: 502, messageCode: "connectors.connection.providerReturnedUnexpectedResponse" });
367
370
  }
368
371
  return result;
369
372
  }
@@ -387,17 +390,17 @@ function createConnectionService({
387
390
  if (provider.accountLabelFromVerification) {
388
391
  const label = provider.accountLabelFromVerification(verification);
389
392
  if (typeof label !== "string" || !label.trim() || label.length > 256 || /[\p{Cc}\p{Cf}]/u.test(label)) {
390
- throw new ConnectorError("connector_response_invalid", "The provider returned an invalid account label.", { statusCode: 502 });
393
+ throw new ConnectorError("connector_response_invalid", "The provider returned an invalid account label.", { statusCode: 502, messageCode: "connectors.connection.providerReturnedInvalidAccountLabel" });
391
394
  }
392
395
  connection.accountLabel = label;
393
396
  }
394
397
  if (!provider.grantedScopesFromVerification) return;
395
398
  const scopes = provider.grantedScopesFromVerification(verification, { clientId: connection.clientId });
396
399
  if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string" || !scope)) {
397
- throw new ConnectorError("connector_response_invalid", "The provider returned an invalid permission grant.", { statusCode: 502 });
400
+ throw new ConnectorError("connector_response_invalid", "The provider returned an invalid permission grant.", { statusCode: 502, messageCode: "connectors.connection.providerReturnedInvalidPermissionGrant" });
398
401
  }
399
402
  connection.grantedScopes = connection.grantedScopes.filter((scope) => requestedScopes.includes(scope) && scopes.includes(scope));
400
- requireOperationScope(provider.operations[provider.checkOperation], connection.grantedScopes, "Connect again with the permission required for verification.");
403
+ requireOperationScope(provider.operations[provider.checkOperation], connection.grantedScopes, "Connect again with the permission required for verification.", { messageCode: "connectors.connection.connectAgainPermissionRequiredVerification" });
401
404
  }
402
405
 
403
406
  async function clientCredentialsGrant(registration, provider, settings, scopes, signal) {
@@ -420,10 +423,10 @@ function createConnectionService({
420
423
  const { owner, integration, provider } = await access(context, integrationId, "connect");
421
424
  const registration = await registrationFor(integration, provider);
422
425
  if (registration.grantType !== "client_credentials") {
423
- throw new ConnectorError("connector_mode_unavailable", "Select client credentials to connect a service account.");
426
+ throw new ConnectorError("connector_mode_unavailable", "Select client credentials to connect a service account.", { messageCode: "connectors.connection.selectClientCredentialsConnectService" });
424
427
  }
425
428
  operationRequest(provider.operations[provider.checkOperation], verificationInput, integration.settings);
426
- requireOperationScope(provider.operations[provider.checkOperation], integration.scopes, VERIFICATION_SCOPE_MESSAGE);
429
+ requireOperationScope(provider.operations[provider.checkOperation], integration.scopes, VERIFICATION_SCOPE_MESSAGE, VERIFICATION_SCOPE_CODE);
427
430
  return store.withConnection({ owner, integrationId }, async ({ save }) => {
428
431
  try {
429
432
  const connection = {
@@ -443,13 +446,13 @@ function createConnectionService({
443
446
  async function beginAuthorization({ context, integrationId, verificationInput = {}, signal }) {
444
447
  const { owner, integration, provider } = await access(context, integrationId, "connect");
445
448
  if (integration.authentication.refreshTokenRef) {
446
- throw new ConnectorError("connector_env_managed", ENV_SEEDED_MESSAGE, { statusCode: 409 });
449
+ throw new ConnectorError("connector_env_managed", ENV_SEEDED_MESSAGE, { statusCode: 409, ...ENV_SEEDED_CODE });
447
450
  }
448
451
  // Resource-specific providers validate the first document ID before opening consent.
449
452
  operationRequest(provider.operations[provider.checkOperation], verificationInput, integration.settings);
450
453
  const registration = await registrationFor(integration, provider);
451
454
  if (registration.grantType !== "authorization_code") {
452
- throw new ConnectorError("connector_mode_unavailable", "Service accounts connect without browser consent.");
455
+ throw new ConnectorError("connector_mode_unavailable", "Service accounts connect without browser consent.", { messageCode: "connectors.connection.serviceAccountsConnectWithoutBrowser" });
453
456
  }
454
457
  signal?.throwIfAborted();
455
458
  const state = oauth.generateRandomState();
@@ -502,18 +505,18 @@ function createConnectionService({
502
505
  const { owner, integration, provider } = await access(context, integrationId, "connect");
503
506
  const registration = await registrationFor(integration, provider);
504
507
  if (registration.grantType !== "authorization_code") {
505
- throw new ConnectorError("connector_mode_unavailable", "Service accounts do not use an authorization callback.");
508
+ throw new ConnectorError("connector_mode_unavailable", "Service accounts do not use an authorization callback.", { messageCode: "connectors.connection.serviceAccountsDoNotUse" });
506
509
  }
507
510
  let url;
508
511
  try { url = new URL(callbackUrl); } catch {
509
- throw new ConnectorError("connector_callback_invalid", "The authorization callback is invalid.");
512
+ throw new ConnectorError("connector_callback_invalid", "The authorization callback is invalid.", { messageCode: "connectors.connection.authorizationCallbackInvalid" });
510
513
  }
511
514
  if (`${url.origin}${url.pathname}` !== registration.callbackUrl || url.hash || url.username || url.password) {
512
- throw new ConnectorError("connector_callback_invalid", "The callback does not match this registration.");
515
+ throw new ConnectorError("connector_callback_invalid", "The callback does not match this registration.", { messageCode: "connectors.connection.callbackDoesNotMatchRegistration" });
513
516
  }
514
517
  const state = url.searchParams.get("state");
515
518
  if (!state || url.searchParams.getAll("state").length !== 1) {
516
- throw new ConnectorError("connector_attempt_invalid", "The authorization attempt is invalid or has expired.");
519
+ throw new ConnectorError("connector_attempt_invalid", "The authorization attempt is invalid or has expired.", { messageCode: "connectors.connection.authorizationAttemptInvalidExpired" });
517
520
  }
518
521
  const outcome = await store.withConnection({ owner, integrationId }, async ({ consumeAttempt, save }) => {
519
522
  const attempt = await consumeAttempt(state);
@@ -525,14 +528,14 @@ function createConnectionService({
525
528
  registrationClientAuthentication(attempt) !== registration.tokenEndpointAuthMethod ||
526
529
  attempt.callbackUrl !== registration.callbackUrl || JSON.stringify(attempt.scopes) !== JSON.stringify(integration.scopes) ||
527
530
  !isDeepStrictEqual(attempt.settings || {}, integration.settings || {})) {
528
- throw new ConnectorError("connector_attempt_invalid", "The authorization attempt is invalid or has expired.");
531
+ throw new ConnectorError("connector_attempt_invalid", "The authorization attempt is invalid or has expired.", { messageCode: "connectors.connection.authorizationAttemptInvalidExpired" });
529
532
  }
530
533
  const parameters = oauth.validateAuthResponse(registration.oauth, registration.client, url, state);
531
534
  let fallbackScopes = attempt.scopes;
532
535
  if (provider.scopesInAuthorizationResponse) {
533
536
  const values = parameters.getAll("scope");
534
537
  if (values.length !== 1) {
535
- throw new ConnectorError("connector_response_invalid", "The provider did not return an unambiguous permission grant.", { statusCode: 502 });
538
+ throw new ConnectorError("connector_response_invalid", "The provider did not return an unambiguous permission grant.", { statusCode: 502, messageCode: "connectors.connection.ambiguousPermissionGrant" });
536
539
  }
537
540
  const granted = new Set(values[0].split(provider.scopeSeparator || " ").map((scope) => scope.trim()).filter(Boolean));
538
541
  fallbackScopes = attempt.scopes.filter((scope) => granted.has(scope));
@@ -570,9 +573,9 @@ function createConnectionService({
570
573
  if (integration.authentication.method !== method || (method === "api-key" &&
571
574
  (typeof provider.apiKey?.headers !== "function" && !provider.apiKey?.queryParameter &&
572
575
  !provider.apiKey?.bodyParameter && typeof provider.apiKey?.pathPrefix !== "function"))) {
573
- throw new ConnectorError("connector_mode_unavailable", "Select the matching credential mode to verify this connection.");
576
+ throw new ConnectorError("connector_mode_unavailable", "Select the matching credential mode to verify this connection.", { messageCode: "connectors.connection.selectMatchingCredentialModeVerify" });
574
577
  }
575
- requireOperationScope(provider.operations[provider.checkOperation], integration.scopes, VERIFICATION_SCOPE_MESSAGE);
578
+ requireOperationScope(provider.operations[provider.checkOperation], integration.scopes, VERIFICATION_SCOPE_MESSAGE, VERIFICATION_SCOPE_CODE);
576
579
  return store.withConnection({ owner, integrationId }, async ({ save }) => {
577
580
  const connection = {
578
581
  integrationId, provider: provider.id, method,
@@ -597,9 +600,9 @@ function createConnectionService({
597
600
  input = structuredClone(input);
598
601
  const { owner, integration, provider } = await access(context, integrationId, operation, input);
599
602
  if (!Object.hasOwn(provider.operations, operation)) {
600
- throw new ConnectorError("connector_operation_unknown", "This provider operation is not available.");
603
+ throw new ConnectorError("connector_operation_unknown", "This provider operation is not available.", { messageCode: "connectors.connection.providerOperationNotAvailable" });
601
604
  }
602
- requireOperationScope(provider.operations[operation], integration.scopes, "This operation requires a permission absent from the application's configuration.");
605
+ requireOperationScope(provider.operations[operation], integration.scopes, "This operation requires a permission absent from the application's configuration.", { messageCode: "connectors.connection.operationRequiresPermissionAbsentApplication" });
603
606
  const usesApiKey = integration.authentication.method === "api-key";
604
607
  const usesServiceAccount = integration.authentication.method === "service-account";
605
608
  const usesOAuth = integration.authentication.method === "oauth2";
@@ -613,7 +616,7 @@ function createConnectionService({
613
616
  if (!connection || connection.status !== "connected" ||
614
617
  !connectionMatches(connection, integration, provider.id, usesOAuth ? { ...registration, clientId: registration.client.client_id } : null) ||
615
618
  (usesOAuth && !callbackMatches(connection, provider, registration))) {
616
- throw new ConnectorError("connector_reconnect_required", "Connect this account again.", { statusCode: 401 });
619
+ throw new ConnectorError("connector_reconnect_required", "Connect this account again.", { statusCode: 401, messageCode: "connectors.connection.connectAccountAgain" });
617
620
  }
618
621
  try {
619
622
  if (usesServiceAccount) {
@@ -629,7 +632,7 @@ function createConnectionService({
629
632
  grant = await clientCredentialsGrant(registration, provider, integration.settings || {}, connection.grantedScopes, signal);
630
633
  } else {
631
634
  if (!connection.tokens.refreshToken) {
632
- throw new ConnectorError("connector_reconnect_required", "Connect this account again.", { statusCode: 401 });
635
+ throw new ConnectorError("connector_reconnect_required", "Connect this account again.", { statusCode: 401, messageCode: "connectors.connection.connectAccountAgain" });
633
636
  }
634
637
  const response = await oauth.refreshTokenGrantRequest(
635
638
  registration.oauth, registration.client, registration.clientAuth,
@@ -658,7 +661,7 @@ function createConnectionService({
658
661
  let failure = providerError(error);
659
662
  if (failure.code === "connector_reconnect_required") {
660
663
  await save({ ...connection, status: "reconnect-required" });
661
- if (seed) failure = new ConnectorError(failure.code, "The refresh token in Env no longer works; replace that Env value.", { statusCode: 401 });
664
+ if (seed) failure = new ConnectorError(failure.code, "The refresh token in Env no longer works; replace that Env value.", { statusCode: 401, messageCode: "connectors.connection.envRefreshTokenRejected" });
662
665
  }
663
666
  // Commit rotated tokens and reconnect state even when the API request fails.
664
667
  return { failure };
@@ -713,7 +716,7 @@ function createConnectionService({
713
716
  async function disconnect({ context, integrationId }) {
714
717
  const { owner, integration } = await access(context, integrationId, "disconnect");
715
718
  if (integration.authentication.refreshTokenRef) {
716
- throw new ConnectorError("connector_env_managed", ENV_SEEDED_MESSAGE, { statusCode: 409 });
719
+ throw new ConnectorError("connector_env_managed", ENV_SEEDED_MESSAGE, { statusCode: 409, ...ENV_SEEDED_CODE });
717
720
  }
718
721
  await store.withConnection({ owner, integrationId }, async ({ remove }) => remove());
719
722
  return { status: "disconnected" };
@@ -4,7 +4,7 @@ function createEnvironmentReferenceResolver(env = process.env) {
4
4
  return async (reference) => {
5
5
  const match = /^env:([A-Z_][A-Z0-9_]*)$/u.exec(reference);
6
6
  if (!match || typeof env[match[1]] !== "string" || (!env[match[1]].trim() || env[match[1]].trim() === "MISSING")) {
7
- throw new ConnectorError("connector_binding_missing", "A required environment binding is missing.");
7
+ throw new ConnectorError("connector_binding_missing", "A required environment binding is missing.", { messageCode: "connectors.connection.requiredEnvironmentBindingMissing" });
8
8
  }
9
9
  return env[match[1]];
10
10
  };
@@ -1,27 +1,34 @@
1
+ // code is the stable value callers branch on, and many sentences share one code,
2
+ // so each sentence also has a messageCode (with messageParams) that renders it from
3
+ // the "jskit.<messageCode>" catalog entry; message stays that English text.
1
4
  class ConnectorError extends Error {
2
- constructor(code, message, { statusCode = 400, retryAfterSeconds } = {}) {
5
+ constructor(code, message, { statusCode = 400, retryAfterSeconds, messageCode, messageParams } = {}) {
3
6
  super(message);
4
7
  this.name = "ConnectorError";
5
8
  this.code = code;
6
9
  this.statusCode = statusCode;
7
10
  if (retryAfterSeconds !== undefined) this.retryAfterSeconds = retryAfterSeconds;
11
+ if (messageCode) {
12
+ this.messageCode = messageCode;
13
+ this.messageParams = { ...messageParams };
14
+ }
8
15
  }
9
16
  }
10
17
 
11
18
  function providerError(error) {
12
19
  if (error instanceof ConnectorError) return error;
13
20
  const status = Number(error?.status || error?.statusCode || 0);
14
- if (error?.name === "AbortError") return new ConnectorError("connector_cancelled", "The operation was cancelled.");
21
+ if (error?.name === "AbortError") return new ConnectorError("connector_cancelled", "The operation was cancelled.", { messageCode: "connectors.connection.operationCancelled" });
15
22
  if (error?.name === "TimeoutError") return new ConnectorError("connector_provider_timeout", "The provider request timed out. Try again.", { statusCode: 504 });
16
23
  if (["invalid_grant", "invalid_client"].includes(error?.error) || status === 401) {
17
- return new ConnectorError("connector_reconnect_required", "Connect this account again.", { statusCode: 401 });
24
+ return new ConnectorError("connector_reconnect_required", "Connect this account again.", { statusCode: 401, messageCode: "connectors.connection.connectAccountAgain" });
18
25
  }
19
26
  if (["access_denied", "user_cancelled_login", "user_cancelled_authorize"].includes(error?.error)) {
20
27
  return new ConnectorError("connector_consent_denied", "Account access was declined.");
21
28
  }
22
- if (status === 403) return new ConnectorError("connector_permission_denied", "The provider denied this operation.", { statusCode: 403 });
23
- if (status === 429) return new ConnectorError("connector_rate_limited", "The provider's request limit was reached.", { statusCode: 429 });
24
- return new ConnectorError("connector_provider_failed", "The provider request failed. Try again.", { statusCode: 502 });
29
+ if (status === 403) return new ConnectorError("connector_permission_denied", "The provider denied this operation.", { statusCode: 403, messageCode: "connectors.connection.providerDeniedOperation" });
30
+ if (status === 429) return new ConnectorError("connector_rate_limited", "The provider's request limit was reached.", { statusCode: 429, messageCode: "connectors.connection.providerRequestLimitReached" });
31
+ return new ConnectorError("connector_provider_failed", "The provider request failed. Try again.", { statusCode: 502, messageCode: "connectors.connection.providerRequestFailedTryAgain" });
25
32
  }
26
33
 
27
34
  export { ConnectorError, providerError };
@@ -1,5 +1,6 @@
1
1
  import { createSchema } from "json-rest-schema";
2
2
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { catalogMessage, formatMessage } from "@jskit-ai/kernel/shared/i18n";
3
4
 
4
5
  const name = { type: "string", required: true, minLength: 1, maxLength: 200 };
5
6
  const secretReference = {
@@ -44,19 +45,115 @@ const integrationsSchema = createSchema({
44
45
  extensions: { type: "object", additionalProperties: true, required: false }
45
46
  });
46
47
 
48
+ // A field error is English text or { code, params, message }, as for the kernel's
49
+ // createValidationError: fieldErrors keeps the English, fieldErrorCodes the codes.
47
50
  class IntegrationConfigurationError extends Error {
48
- constructor(fieldErrors) {
51
+ constructor(fieldErrors, fieldErrorCodes = {}) {
49
52
  super("Integration configuration is invalid.");
50
53
  this.name = "IntegrationConfigurationError";
51
54
  this.code = "integration_configuration_invalid";
52
55
  this.statusCode = 422;
53
- this.fieldErrors = fieldErrors;
56
+ this.fieldErrors = {};
57
+ this.fieldErrorCodes = { ...fieldErrorCodes };
58
+ for (const [field, entry] of Object.entries(fieldErrors)) {
59
+ this.fieldErrors[field] = typeof entry === "string" ? entry : entry.message;
60
+ if (entry?.code) this.fieldErrorCodes[field] = { code: entry.code, params: { ...entry.params } };
61
+ }
62
+ }
63
+ }
64
+
65
+ // Each configuration field error's English, rendered from "jskit.<code>".
66
+ const FIELD_ERRORS = Object.freeze({
67
+ schemaVersion: { code: "connectors.configuration.schemaVersionUnsupported", message: "Use numeric schemaVersion 1; other versions require an explicit migration." },
68
+ invalid: { code: "connectors.configuration.invalid", message: "Invalid configuration." },
69
+ invalidJson: { code: "connectors.configuration.invalidJson", message: "Enter valid JSON." },
70
+ required: { code: "connectors.configuration.valueRequired", message: "This value is required." },
71
+ stableName: { code: "connectors.configuration.stableName", message: "Use a stable lowercase name containing letters, digits and hyphens." },
72
+ otherCredentialMode: { code: "connectors.configuration.otherCredentialModeField", message: "This field belongs to another credential mode." },
73
+ confidentialClient: { code: "connectors.configuration.confidentialClientRequired", message: "Client credentials require a confidential client." },
74
+ refreshTokenOAuthOnly: { code: "connectors.configuration.refreshTokenOAuthOnly", message: "Only an OAuth connection can start from a refresh token." },
75
+ registrationUndefined: { code: "connectors.configuration.registrationUndefined", message: "Select a registration defined in this file." },
76
+ registrationSecret: { code: "connectors.configuration.registrationSecret", message: "Use the registration's secret reference." },
77
+ refreshTokenSharedOnly: { code: "connectors.configuration.refreshTokenSharedOnly", message: "Only a shared connection can start from a refresh token in Env." },
78
+ refreshTokenClientCredentials: { code: "connectors.configuration.refreshTokenClientCredentials", message: "Client credentials do not use a refresh token." },
79
+ serviceAccountPerUser: { code: "connectors.configuration.serviceAccountPerUser", message: "A service account cannot connect as each app user." },
80
+ apiKeyRegistration: { code: "connectors.configuration.apiKeyRegistration", message: "API keys do not use an OAuth registration." },
81
+ serviceAccountRegistration: { code: "connectors.configuration.serviceAccountRegistration", message: "Service-account credentials do not use a browser OAuth registration." },
82
+ noCredentials: { code: "connectors.configuration.noCredentials", message: "This mode does not use credentials." },
83
+ duplicateScope: { code: "connectors.configuration.duplicateScope", message: "Select each permission only once." },
84
+ providerUnregistered: { code: "connectors.configuration.providerUnregistered", message: "Install and register this provider before using it." },
85
+ assistantActionUndeclared: { code: "connectors.configuration.assistantActionUndeclared", message: "This provider does not declare this assistant action." },
86
+ providerSettings: { code: "connectors.configuration.providerSettings", message: "Check this provider's settings." },
87
+ otherCredentialModeSetting: { code: "connectors.configuration.otherCredentialModeSetting", message: "This setting belongs to another credential mode." },
88
+ accountModeUnsupported: { code: "connectors.configuration.accountModeUnsupported", message: "This provider does not support this account mode." },
89
+ credentialModeUnsupported: { code: "connectors.configuration.credentialModeUnsupported", message: "This provider configuration does not support this credential mode." },
90
+ oauthFlowUnsupported: { code: "connectors.configuration.oauthFlowUnsupported", message: "This provider does not support this OAuth flow." },
91
+ clientAuthenticationUnsupported: { code: "connectors.configuration.clientAuthenticationUnsupported", message: "This provider does not support this client authentication method." },
92
+ clientId: { code: "connectors.configuration.clientId", message: "Check this provider's client identifier." },
93
+ requiredScopes: { code: "connectors.configuration.requiredScopes", message: "Include this provider's required permissions." },
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." }
96
+ });
97
+
98
+ // A schema validation error's field errors with the codes the validator gave them.
99
+ function schemaFieldErrors(error, fallback, prefix = "") {
100
+ const messages = {};
101
+ const codes = {};
102
+ for (const [field, message] of Object.entries(error.fieldErrors || fallback)) {
103
+ messages[`${prefix}${field}`] = message;
104
+ const coded = error.fieldErrorCodes?.[field];
105
+ if (coded) codes[`${prefix}${field}`] = coded;
54
106
  }
107
+ return { messages, codes };
55
108
  }
56
109
 
57
110
  // Registrations, attempts and stored connections omit these when they use the defaults.
58
111
  const DEFAULT_GRANT_TYPE = "authorization_code";
59
112
  const DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD = "client_secret_post";
113
+ // A provider definition whose texts were rendered to English keeps its message-key
114
+ // original under this symbol, so a translating UI can render the keys instead.
115
+ const MESSAGE_KEY_SOURCE = Symbol.for("@jskit-ai/connectors-core.messageKeySource");
116
+
117
+ // Provider definitions carry message keys, which connectors-web translates. Code that
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, exactly as
120
+ // before keys. Each English object keeps its keyed original under MESSAGE_KEY_SOURCE.
121
+ function createEnglishProviderTexts(catalog) {
122
+ const rendered = new WeakMap();
123
+ const englishMessage = (key, params) => {
124
+ const message = typeof key === "string" ? catalogMessage(catalog, key) : undefined;
125
+ return message === undefined ? undefined : formatMessage(message, params);
126
+ };
127
+ return function englishTexts(value) {
128
+ if (typeof value === "string") {
129
+ return englishMessage(value) ?? value;
130
+ }
131
+ if (!value || (typeof value !== "object" && typeof value !== "function")) {
132
+ return value;
133
+ }
134
+ if (rendered.has(value)) {
135
+ return rendered.get(value);
136
+ }
137
+ let english = value;
138
+ if (typeof value === "function") {
139
+ english = function (...args) { return englishTexts(value.apply(this, args)); };
140
+ } else if (Array.isArray(value)) {
141
+ english = value.map((item) => englishTexts(item));
142
+ } else if (Object.getPrototypeOf(value) === Object.prototype) {
143
+ const message = englishMessage(value.key, value.params);
144
+ if (message !== undefined) {
145
+ return message;
146
+ }
147
+ english = Object.fromEntries(Object.entries(value).map(([name, inner]) => [name, englishTexts(inner)]));
148
+ Object.defineProperty(english, MESSAGE_KEY_SOURCE, { value });
149
+ }
150
+ if (english !== value && Object.isFrozen(value)) {
151
+ Object.freeze(english);
152
+ }
153
+ rendered.set(value, english);
154
+ return english;
155
+ };
156
+ }
60
157
 
61
158
  function registrationGrantType(record) {
62
159
  return record?.grantType || DEFAULT_GRANT_TYPE;
@@ -108,23 +205,25 @@ function isProviderConfigurationOnly(provider, settings = {}) {
108
205
 
109
206
  function validateIntegrationConfiguration(input, { providers = [], allowUnknownProviders = false } = {}) {
110
207
  if (input?.schemaVersion !== 1) {
111
- throw new IntegrationConfigurationError({ schemaVersion: "Use numeric schemaVersion 1; other versions require an explicit migration." });
208
+ throw new IntegrationConfigurationError({ schemaVersion: FIELD_ERRORS.schemaVersion });
112
209
  }
113
210
  let config;
114
211
  try {
115
212
  config = validateSchemaPayload({ schema: integrationsSchema, mode: "replace" }, input);
116
213
  } catch (error) {
117
- throw new IntegrationConfigurationError(error.fieldErrors || { configuration: "Invalid configuration." });
214
+ const { messages, codes } = schemaFieldErrors(error, { configuration: FIELD_ERRORS.invalid });
215
+ throw new IntegrationConfigurationError(messages, codes);
118
216
  }
119
217
  const fieldErrors = {};
218
+ const fieldErrorCodes = {};
120
219
  const definitions = new Map(providers.map((provider) => [provider.id, provider]));
121
220
  const requireValue = (record, key, path) => {
122
- if (!record[key]) fieldErrors[`${path}.${key}`] = "This value is required.";
221
+ if (!record[key]) fieldErrors[`${path}.${key}`] = FIELD_ERRORS.required;
123
222
  };
124
223
  for (const field of ["integrations", "registrations"]) {
125
224
  for (const id of Object.keys(config[field])) {
126
225
  if (!/^[a-z][a-z0-9-]*$/u.test(id) || ["constructor", "prototype"].includes(id)) {
127
- fieldErrors[`${field}.${id}`] = "Use a stable lowercase name containing letters, digits and hyphens.";
226
+ fieldErrors[`${field}.${id}`] = FIELD_ERRORS.stableName;
128
227
  }
129
228
  }
130
229
  }
@@ -135,10 +234,10 @@ function validateIntegrationConfiguration(input, { providers = [], allowUnknownP
135
234
  for (const key of required) requireValue(registration, key, path);
136
235
  const forbidden = [...(clientCredentials ? ["callbackUrlRef"] : []), ...(registration.tokenEndpointAuthMethod === "none" ? ["clientSecretRef"] : [])];
137
236
  for (const key of forbidden) {
138
- if (Object.hasOwn(registration, key)) fieldErrors[`${path}.${key}`] = "This field belongs to another credential mode.";
237
+ if (Object.hasOwn(registration, key)) fieldErrors[`${path}.${key}`] = FIELD_ERRORS.otherCredentialMode;
139
238
  }
140
239
  if (clientCredentials && registration.tokenEndpointAuthMethod === "none") {
141
- fieldErrors[`${path}.tokenEndpointAuthMethod`] = "Client credentials require a confidential client.";
240
+ fieldErrors[`${path}.tokenEndpointAuthMethod`] = FIELD_ERRORS.confidentialClient;
142
241
  }
143
242
  }
144
243
  for (const [id, integration] of Object.entries(config.integrations)) {
@@ -148,94 +247,94 @@ function validateIntegrationConfiguration(input, { providers = [], allowUnknownP
148
247
  const registration = config.registrations[auth.registrationRef];
149
248
  const grantType = registrationGrantType(registration);
150
249
  if (auth.refreshTokenRef && auth.method !== "oauth2") {
151
- fieldErrors[`${path}.authentication.refreshTokenRef`] = "Only an OAuth connection can start from a refresh token.";
250
+ fieldErrors[`${path}.authentication.refreshTokenRef`] = FIELD_ERRORS.refreshTokenOAuthOnly;
152
251
  }
153
252
  if (auth.method === "oauth2") {
154
253
  if (!auth.registrationRef || !Object.hasOwn(config.registrations, auth.registrationRef)) {
155
- fieldErrors[`${path}.authentication.registrationRef`] = "Select a registration defined in this file.";
254
+ fieldErrors[`${path}.authentication.registrationRef`] = FIELD_ERRORS.registrationUndefined;
156
255
  }
157
- if (auth.secretRef) fieldErrors[`${path}.authentication.secretRef`] = "Use the registration's secret reference.";
256
+ if (auth.secretRef) fieldErrors[`${path}.authentication.secretRef`] = FIELD_ERRORS.registrationSecret;
158
257
  if (auth.refreshTokenRef && integration.accountMode !== "shared") {
159
- fieldErrors[`${path}.authentication.refreshTokenRef`] = "Only a shared connection can start from a refresh token in Env.";
258
+ fieldErrors[`${path}.authentication.refreshTokenRef`] = FIELD_ERRORS.refreshTokenSharedOnly;
160
259
  } else if (auth.refreshTokenRef && grantType !== "authorization_code") {
161
- fieldErrors[`${path}.authentication.refreshTokenRef`] = "Client credentials do not use a refresh token.";
260
+ fieldErrors[`${path}.authentication.refreshTokenRef`] = FIELD_ERRORS.refreshTokenClientCredentials;
162
261
  }
163
262
  if (grantType === "client_credentials" && integration.accountMode === "per-user") {
164
- fieldErrors[`${path}.accountMode`] = "A service account cannot connect as each app user.";
263
+ fieldErrors[`${path}.accountMode`] = FIELD_ERRORS.serviceAccountPerUser;
165
264
  }
166
265
  } else if (auth.method === "api-key") {
167
266
  if (!provider?.apiKeySecretOptional) requireValue(auth, "secretRef", `${path}.authentication`);
168
- if (auth.registrationRef) fieldErrors[`${path}.authentication.registrationRef`] = "API keys do not use an OAuth registration.";
267
+ if (auth.registrationRef) fieldErrors[`${path}.authentication.registrationRef`] = FIELD_ERRORS.apiKeyRegistration;
169
268
  } else if (auth.method === "service-account") {
170
269
  requireValue(auth, "secretRef", `${path}.authentication`);
171
- if (auth.registrationRef) fieldErrors[`${path}.authentication.registrationRef`] = "Service-account credentials do not use a browser OAuth registration.";
172
- if (integration.accountMode === "per-user") fieldErrors[`${path}.accountMode`] = "A service account cannot connect as each app user.";
270
+ if (auth.registrationRef) fieldErrors[`${path}.authentication.registrationRef`] = FIELD_ERRORS.serviceAccountRegistration;
271
+ if (integration.accountMode === "per-user") fieldErrors[`${path}.accountMode`] = FIELD_ERRORS.serviceAccountPerUser;
173
272
  } else {
174
273
  for (const field of ["secretRef", "registrationRef"]) {
175
- if (Object.hasOwn(auth, field)) fieldErrors[`${path}.authentication.${field}`] = "This mode does not use credentials.";
274
+ if (Object.hasOwn(auth, field)) fieldErrors[`${path}.authentication.${field}`] = FIELD_ERRORS.noCredentials;
176
275
  }
177
276
  }
178
277
  if (new Set(integration.scopes).size !== integration.scopes.length) {
179
- fieldErrors[`${path}.scopes`] = "Select each permission only once.";
278
+ fieldErrors[`${path}.scopes`] = FIELD_ERRORS.duplicateScope;
180
279
  }
181
280
  if (providers.length) {
182
281
  if (!provider) {
183
- if (!allowUnknownProviders) fieldErrors[`${path}.provider`] = "Install and register this provider before using it.";
282
+ if (!allowUnknownProviders) fieldErrors[`${path}.provider`] = FIELD_ERRORS.providerUnregistered;
184
283
  continue;
185
284
  }
186
285
  for (const action of Object.keys(integration.assistantPolicy?.actions || {})) {
187
286
  if (!provider.assistantActions?.some((item) => item.value === action)) {
188
- fieldErrors[`${path}.assistantPolicy.actions.${action}`] = "This provider does not declare this assistant action.";
287
+ fieldErrors[`${path}.assistantPolicy.actions.${action}`] = FIELD_ERRORS.assistantActionUndeclared;
189
288
  }
190
289
  }
191
290
  if (provider.settingsSchema) {
192
291
  try {
193
292
  integration.settings = validateSchemaPayload({ schema: getProviderSettingsSchema(provider, integration.settings), mode: "replace" }, integration.settings || {});
194
293
  } catch (error) {
195
- for (const [field, message] of Object.entries(error.fieldErrors || { settings: "Check this provider's settings." })) {
196
- fieldErrors[`${path}.settings.${field}`] = message;
197
- }
294
+ const { messages, codes } = schemaFieldErrors(error, { settings: FIELD_ERRORS.providerSettings }, `${path}.settings.`);
295
+ Object.assign(fieldErrors, messages);
296
+ Object.assign(fieldErrorCodes, codes);
198
297
  }
199
298
  }
200
299
  for (const field of provider.settingsFields || []) {
201
300
  if (field.authenticationMethods && !field.authenticationMethods.includes(auth.method) && Object.hasOwn(integration.settings || {}, field.name)) {
202
- fieldErrors[`${path}.settings.${field.name}`] = "This setting belongs to another credential mode.";
301
+ fieldErrors[`${path}.settings.${field.name}`] = FIELD_ERRORS.otherCredentialModeSetting;
203
302
  }
204
303
  }
205
304
  if (!getProviderAccountModes(provider, integration.settings || {}).includes(integration.accountMode)) {
206
- fieldErrors[`${path}.accountMode`] = "This provider does not support this account mode.";
305
+ fieldErrors[`${path}.accountMode`] = FIELD_ERRORS.accountModeUnsupported;
207
306
  }
208
307
  if (!getProviderAuthenticationMethods(provider, integration.settings).includes(auth.method)) {
209
- fieldErrors[`${path}.authentication.method`] = "This provider configuration does not support this credential mode.";
308
+ fieldErrors[`${path}.authentication.method`] = FIELD_ERRORS.credentialModeUnsupported;
210
309
  }
211
310
  if (auth.method === "oauth2" && registration?.source === "own") {
212
311
  const registrationPath = `registrations.${auth.registrationRef}`;
213
312
  if (!providerGrantTypes(provider).includes(grantType)) {
214
- fieldErrors[`${registrationPath}.grantType`] = "This provider does not support this OAuth flow.";
313
+ fieldErrors[`${registrationPath}.grantType`] = FIELD_ERRORS.oauthFlowUnsupported;
215
314
  }
216
315
  const methods = getProviderClientAuthenticationMethods(provider, grantType, integration.settings || {});
217
316
  if (!methods.includes(registrationClientAuthentication(registration))) {
218
- fieldErrors[`${registrationPath}.tokenEndpointAuthMethod`] = "This provider does not support this client authentication method.";
317
+ fieldErrors[`${registrationPath}.tokenEndpointAuthMethod`] = FIELD_ERRORS.clientAuthenticationUnsupported;
219
318
  }
220
319
  if (registration.clientId && provider.validateClientId) {
221
320
  const result = provider.validateClientId(registration.clientId);
222
- if (result !== true) fieldErrors[`${registrationPath}.clientId`] = result || "Check this provider's client identifier.";
321
+ if (result !== true) fieldErrors[`${registrationPath}.clientId`] = result || FIELD_ERRORS.clientId;
223
322
  }
224
323
  }
225
324
  const scopes = getProviderScopes(provider, integration.settings || {}, grantType, auth.method);
226
325
  const allowedScopes = new Set(scopes.map((scope) => scope.value));
227
326
  if (scopes.some((scope) => scope.required && !integration.scopes.includes(scope.value))) {
228
- fieldErrors[`${path}.scopes`] = "Include this provider's required permissions.";
327
+ fieldErrors[`${path}.scopes`] = FIELD_ERRORS.requiredScopes;
229
328
  }
230
329
  if (integration.scopes.some((scope) => !allowedScopes.has(scope))) {
231
- fieldErrors[`${path}.scopes`] = "A selected permission is not supported by this provider configuration.";
330
+ fieldErrors[`${path}.scopes`] = FIELD_ERRORS.scopeUnsupported;
232
331
  }
233
332
  if (["oauth2", "service-account"].includes(auth.method) && scopes.length && !integration.scopes.length) {
234
- fieldErrors[`${path}.scopes`] = "Select at least one permission.";
333
+ fieldErrors[`${path}.scopes`] = FIELD_ERRORS.scopeRequired;
235
334
  }
236
335
  }
237
336
  }
238
- if (Object.keys(fieldErrors).length) throw new IntegrationConfigurationError(fieldErrors);
337
+ if (Object.keys(fieldErrors).length) throw new IntegrationConfigurationError(fieldErrors, fieldErrorCodes);
239
338
  return config;
240
339
  }
241
340
 
@@ -244,7 +343,7 @@ function parseIntegrationConfiguration(text, options) {
244
343
  try {
245
344
  value = JSON.parse(text);
246
345
  } catch {
247
- throw new IntegrationConfigurationError({ configuration: "Enter valid JSON." });
346
+ throw new IntegrationConfigurationError({ configuration: FIELD_ERRORS.invalidJson });
248
347
  }
249
348
  return validateIntegrationConfiguration(value, options);
250
349
  }
@@ -252,6 +351,8 @@ function parseIntegrationConfiguration(text, options) {
252
351
  export {
253
352
  DEFAULT_GRANT_TYPE,
254
353
  DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD,
354
+ MESSAGE_KEY_SOURCE,
355
+ createEnglishProviderTexts,
255
356
  registrationGrantType,
256
357
  registrationClientAuthentication,
257
358
  providerGrantTypes,
@@ -0,0 +1,16 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import test from "node:test";
4
+ import { fileURLToPath } from "node:url";
5
+ import { evaluateI18nCatalogContract } from "../../../tooling/test-support/i18nCatalogContract.mjs";
6
+
7
+ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
+ const REPO_ROOT = path.resolve(TEST_DIRECTORY, "..", "..", "..");
9
+
10
+ test("connectors-core English catalog matches its source", () => {
11
+ const { problems } = evaluateI18nCatalogContract({
12
+ repoRoot: REPO_ROOT,
13
+ packageDir: path.join(REPO_ROOT, "packages", "connectors-core")
14
+ });
15
+ assert.deepEqual(problems, []);
16
+ });