@jskit-ai/connectors-core 0.1.65 → 0.2.0
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 +183 -0
- package/locales/en.json +92 -0
- package/package.json +5 -4
- package/src/server/connectionService.js +51 -48
- package/src/server/environmentReferences.js +1 -1
- package/src/server/errors.js +13 -6
- package/src/shared/configuration.js +136 -35
- package/test/i18nCatalog.test.js +16 -0
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. |
|
package/locales/en.json
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
57
|
-
"@jskit-ai/kernel": "0.
|
|
57
|
+
"@jskit-ai/http-runtime": "0.2.0",
|
|
58
|
+
"@jskit-ai/kernel": "0.2.0"
|
|
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
|
};
|
package/src/server/errors.js
CHANGED
|
@@ -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 =
|
|
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:
|
|
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
|
-
|
|
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}`] =
|
|
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}`] =
|
|
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}`] =
|
|
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`] =
|
|
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`] =
|
|
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`] =
|
|
254
|
+
fieldErrors[`${path}.authentication.registrationRef`] = FIELD_ERRORS.registrationUndefined;
|
|
156
255
|
}
|
|
157
|
-
if (auth.secretRef) fieldErrors[`${path}.authentication.secretRef`] =
|
|
256
|
+
if (auth.secretRef) fieldErrors[`${path}.authentication.secretRef`] = FIELD_ERRORS.registrationSecret;
|
|
158
257
|
if (auth.refreshTokenRef && integration.accountMode !== "shared") {
|
|
159
|
-
fieldErrors[`${path}.authentication.refreshTokenRef`] =
|
|
258
|
+
fieldErrors[`${path}.authentication.refreshTokenRef`] = FIELD_ERRORS.refreshTokenSharedOnly;
|
|
160
259
|
} else if (auth.refreshTokenRef && grantType !== "authorization_code") {
|
|
161
|
-
fieldErrors[`${path}.authentication.refreshTokenRef`] =
|
|
260
|
+
fieldErrors[`${path}.authentication.refreshTokenRef`] = FIELD_ERRORS.refreshTokenClientCredentials;
|
|
162
261
|
}
|
|
163
262
|
if (grantType === "client_credentials" && integration.accountMode === "per-user") {
|
|
164
|
-
fieldErrors[`${path}.accountMode`] =
|
|
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`] =
|
|
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`] =
|
|
172
|
-
if (integration.accountMode === "per-user") fieldErrors[`${path}.accountMode`] =
|
|
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}`] =
|
|
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`] =
|
|
278
|
+
fieldErrors[`${path}.scopes`] = FIELD_ERRORS.duplicateScope;
|
|
180
279
|
}
|
|
181
280
|
if (providers.length) {
|
|
182
281
|
if (!provider) {
|
|
183
|
-
if (!allowUnknownProviders) fieldErrors[`${path}.provider`] =
|
|
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}`] =
|
|
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
|
-
|
|
196
|
-
|
|
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}`] =
|
|
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`] =
|
|
305
|
+
fieldErrors[`${path}.accountMode`] = FIELD_ERRORS.accountModeUnsupported;
|
|
207
306
|
}
|
|
208
307
|
if (!getProviderAuthenticationMethods(provider, integration.settings).includes(auth.method)) {
|
|
209
|
-
fieldErrors[`${path}.authentication.method`] =
|
|
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`] =
|
|
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`] =
|
|
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 ||
|
|
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`] =
|
|
327
|
+
fieldErrors[`${path}.scopes`] = FIELD_ERRORS.requiredScopes;
|
|
229
328
|
}
|
|
230
329
|
if (integration.scopes.some((scope) => !allowedScopes.has(scope))) {
|
|
231
|
-
fieldErrors[`${path}.scopes`] =
|
|
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`] =
|
|
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:
|
|
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
|
+
});
|