@jskit-ai/connectors-core 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/docs/setup-command.md +25 -25
- package/locales/en.json +0 -1
- package/package.json +4 -3
- package/src/server/ConnectorsFeature.js +65 -18
- package/src/server/connectionCredentials.js +129 -0
- package/src/server/connectionService.js +104 -319
- package/src/server/credentialProtection.js +31 -20
- package/src/server/environmentReferences.js +10 -1
- package/src/server/errors.js +27 -7
- package/src/server/fileConnectionStore.js +10 -0
- package/src/server/index.js +1 -1
- package/src/server/knexConnectionStore.js +13 -2
- package/src/server/providerRequests.js +291 -0
- package/src/server/types.js +150 -0
- package/src/shared/configuration.js +23 -12
- package/test/connectionService.test.js +10 -8
- package/test/serviceAccount.test.js +4 -2
- package/test/setupCommand.test.js +9 -8
- /package/docs/{online-setup.md → application-setup.md} +0 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ configuration validation.
|
|
|
15
15
|
|
|
16
16
|
See [application-owned OAuth callbacks](docs/oauth-callbacks.md) for callback
|
|
17
17
|
routes, environment bindings and changes of domain or hosting.
|
|
18
|
-
The [application setup guide](docs/
|
|
18
|
+
The [application setup guide](docs/application-setup.md) explains credential and
|
|
19
19
|
runtime ownership for hosted editors and direct CLI use.
|
|
20
20
|
|
|
21
21
|
## Pre-release migration to application-owned connections
|
|
@@ -136,6 +136,11 @@ retain placeholders while an administrator finishes setup.
|
|
|
136
136
|
Never trust owner IDs supplied by a browser. Include deployment/environment
|
|
137
137
|
identity in `applicationId` when several environments share storage.
|
|
138
138
|
|
|
139
|
+
`@jskit-ai/connectors-core/server/types` names these contracts for code that
|
|
140
|
+
composes the service: `ConnectorAuthorize`, `ResolveReference`,
|
|
141
|
+
`ConnectionStore`, `ConnectorProvider`, `ConnectionServiceOptions` and the
|
|
142
|
+
`ConnectorsCore` service itself.
|
|
143
|
+
|
|
139
144
|
For `invoke`, `input` is a separate copy of the caller's operation input. It is
|
|
140
145
|
omitted for connection-management methods. Check it when permission depends on
|
|
141
146
|
the requested tool, document or action. The service snapshots input before
|
package/docs/setup-command.md
CHANGED
|
@@ -4,35 +4,34 @@ An application can expose its connection setup to a CLI or an editor using the
|
|
|
4
4
|
same `createConnectionService()` instance as its backend. JSKIT owns connection
|
|
5
5
|
logic and optional stores; the application owns the executable, operator
|
|
6
6
|
identity, environment, callback route and deployment. Neither the backend nor
|
|
7
|
-
this library requires
|
|
7
|
+
this library requires a particular host to run.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- Command with `nodejs` in `.`: `node` `scripts/integrations.js`
|
|
15
|
-
```
|
|
9
|
+
The host that runs the command (an editor, a CLI wrapper or an operations tool)
|
|
10
|
+
says how the application declares it, for example as `node
|
|
11
|
+
scripts/integrations.js`, and owns its request protocol: the protocol id that
|
|
12
|
+
requests carry and responses echo, the input and output size bounds and the
|
|
13
|
+
time limit.
|
|
16
14
|
|
|
17
15
|
The script loads the application's existing configuration and server composition.
|
|
18
16
|
Resolve administrator references from its existing Env using
|
|
19
|
-
`createEnvironmentReferenceResolver()`. Use an application-owned store whose
|
|
17
|
+
`createEnvironmentReferenceResolver(process.env)`. Use an application-owned store whose
|
|
20
18
|
identity includes the environment. Keep runtime data outside source and release
|
|
21
19
|
snapshots. The script's operator context must be established by the application;
|
|
22
20
|
never accept applicationId, subjectId or administrator privileges from stdin.
|
|
23
21
|
The normal service `authorize` callback still enforces connect/status/disconnect
|
|
24
|
-
permissions.
|
|
22
|
+
permissions. A host user's identity is not an application user's identity.
|
|
25
23
|
|
|
26
24
|
## Dispatch example
|
|
27
25
|
|
|
28
26
|
The following function belongs in the application's script. It accepts the
|
|
29
|
-
already-created service, validated configuration
|
|
30
|
-
|
|
27
|
+
already-created service, validated configuration, trusted operator context and
|
|
28
|
+
the protocol id of the host that runs the command. It is deliberately a
|
|
29
|
+
composition example, not a new JSKIT command runner.
|
|
31
30
|
|
|
32
31
|
```js
|
|
33
|
-
async function dispatchSetup(request, { connections, configuration, context }) {
|
|
34
|
-
const {
|
|
35
|
-
if (protocol !==
|
|
32
|
+
async function dispatchSetup(request, { protocol, connections, configuration, context }) {
|
|
33
|
+
const { requestId, operation, integrationId } = request;
|
|
34
|
+
if (request.protocol !== protocol ||
|
|
36
35
|
typeof requestId !== "string" || !requestId ||
|
|
37
36
|
!["status", "connect", "cancel", "disconnect"].includes(operation) ||
|
|
38
37
|
!Object.hasOwn(configuration.integrations, integrationId)) {
|
|
@@ -94,11 +93,12 @@ async function dispatchSetup(request, { connections, configuration, context }) {
|
|
|
94
93
|
}
|
|
95
94
|
```
|
|
96
95
|
|
|
97
|
-
Read exactly one JSON request from stdin
|
|
98
|
-
returned JSON object plus a newline to stdout. On failure exit nonzero;
|
|
99
|
-
print credentials, provider responses or raw exceptions. Close the
|
|
100
|
-
database pool/resources in `finally`. The
|
|
101
|
-
return pending immediately instead of waiting for browser
|
|
96
|
+
Read exactly one JSON request from stdin within the host's input bound. Write
|
|
97
|
+
only the returned JSON object plus a newline to stdout. On failure exit nonzero;
|
|
98
|
+
do not print credentials, provider responses or raw exceptions. Close the
|
|
99
|
+
application's database pool/resources in `finally`. The host bounds the
|
|
100
|
+
command's time; return pending immediately instead of waiting for browser
|
|
101
|
+
consent.
|
|
102
102
|
|
|
103
103
|
`verificationInput` supplies provider-specific check inputs such as a resource
|
|
104
104
|
ID. It is not a credential entry channel. Keys and client secrets belong in Env.
|
|
@@ -107,11 +107,11 @@ A configuration save does not verify credentials or produce a connected state.
|
|
|
107
107
|
Status validates required local bindings without calling the provider. Before a
|
|
108
108
|
first connection, missing credentials or an invalid callback produce
|
|
109
109
|
`unconfigured` with a safe `configurationError` code; the dispatcher maps that
|
|
110
|
-
code to the
|
|
110
|
+
code to the host's setup guidance. A previous grant remains available for
|
|
111
111
|
disconnect and reports `reconnect-required` when its bindings are incomplete.
|
|
112
112
|
For a valid authorization-code registration, status and pending authorization
|
|
113
113
|
report `callbackUrl` resolved from the application's Env. This is the URL to
|
|
114
|
-
register with the provider, not the
|
|
114
|
+
register with the provider, not one the host suggests. Invalid or incomplete
|
|
115
115
|
bindings do not expose an unvalidated callback or private Env values.
|
|
116
116
|
|
|
117
117
|
Providers may expose `accountLabel` from their verified account response. Gmail
|
|
@@ -127,7 +127,7 @@ the grant; cancelled replacement consent retains the previous grant's label.
|
|
|
127
127
|
|
|
128
128
|
### Individual application accounts
|
|
129
129
|
|
|
130
|
-
For `accountMode: "per-user"`, the
|
|
130
|
+
For `accountMode: "per-user"`, the host configures the OAuth client but does
|
|
131
131
|
not run this shared-operator dispatcher. The application implements its own
|
|
132
132
|
connection screen using its existing UI and authenticated routes:
|
|
133
133
|
|
|
@@ -141,7 +141,7 @@ connection screen using its existing UI and authenticated routes:
|
|
|
141
141
|
|
|
142
142
|
Supply `{ context, integrationId }` to each operation. Derive `context` from
|
|
143
143
|
trusted application authentication and membership, never a form field, callback
|
|
144
|
-
query or
|
|
144
|
+
query or host identity. The service's `authorize` function must enforce the
|
|
145
145
|
application and user boundary. Keep registration credentials in backend Env;
|
|
146
146
|
each user's grant lives separately in the existing connection store. The browser
|
|
147
147
|
receives only safe status/consent metadata. OAuth callback state does not replace
|
|
@@ -162,7 +162,7 @@ The callback Env value must match both this real route and the provider's
|
|
|
162
162
|
registered redirect URI. For a hosted project use its assigned application URL
|
|
163
163
|
as the starting origin, then append the implemented callback path. Custom domains
|
|
164
164
|
or explicit overrides require updating that Env value and provider registration;
|
|
165
|
-
changing
|
|
165
|
+
changing the host's hostname cannot change application identity.
|
|
166
166
|
|
|
167
167
|
Status can recover a still-valid pending attempt after the process restarts.
|
|
168
168
|
Cancellation consumes its state and preserves an earlier working connection.
|
package/locales/en.json
CHANGED
|
@@ -55,7 +55,6 @@
|
|
|
55
55
|
"providerClientIdNotConfigured": "The provider client ID is not configured.",
|
|
56
56
|
"providerDeniedOperation": "The provider denied this operation.",
|
|
57
57
|
"providerDoesNotDeclareAssistant": "This provider does not declare this assistant action.",
|
|
58
|
-
"providerDoesNotImplementService": "This provider does not implement service-account authorization.",
|
|
59
58
|
"providerInvalidCredentialPath": "This provider has an invalid credential path.",
|
|
60
59
|
"providerNonPkceFlowRequires": "This provider's non-PKCE flow requires a confidential client.",
|
|
61
60
|
"providerOperationInvalidDestination": "The provider operation has an invalid destination.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/connectors-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Portable integration configuration and server-side account connection runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"./server": "./src/server/index.js",
|
|
12
12
|
"./server/storage": "./src/server/storage.js",
|
|
13
13
|
"./server/file-storage": "./src/server/fileStorage.js",
|
|
14
|
+
"./server/types": "./src/server/types.js",
|
|
14
15
|
"./locales/en.json": "./locales/en.json"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
}
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
58
|
-
"@jskit-ai/kernel": "0.2.
|
|
58
|
+
"@jskit-ai/http-runtime": "0.2.4",
|
|
59
|
+
"@jskit-ai/kernel": "0.2.4"
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -2,6 +2,70 @@ import { defineFeature } from "@jskit-ai/kernel/server/features";
|
|
|
2
2
|
import { createSchema } from "json-rest-schema";
|
|
3
3
|
import { createConnectionService } from "./connectionService.js";
|
|
4
4
|
|
|
5
|
+
const integrationId = Object.freeze({ type: "string", required: true, minLength: 1, maxLength: 200 });
|
|
6
|
+
const verificationInput = Object.freeze({ type: "object", additionalProperties: true });
|
|
7
|
+
// Every connectors action serves API and internal callers on app surfaces.
|
|
8
|
+
const connectorsActionReach = Object.freeze({
|
|
9
|
+
channels: Object.freeze(["api", "internal"]),
|
|
10
|
+
surfaces: Object.freeze(["app"])
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/** @param {{ connections: import("./types.js").ConnectorsCore }} dependencies */
|
|
14
|
+
function createConnectorsActions({ connections }) {
|
|
15
|
+
return Object.freeze([
|
|
16
|
+
Object.freeze({
|
|
17
|
+
id: "connectors.status",
|
|
18
|
+
kind: "query",
|
|
19
|
+
...connectorsActionReach,
|
|
20
|
+
input: { schema: createSchema({ integrationId }) },
|
|
21
|
+
execute: (input, context) => connections.status({ ...input, context })
|
|
22
|
+
}),
|
|
23
|
+
Object.freeze({
|
|
24
|
+
id: "connectors.connect",
|
|
25
|
+
kind: "command",
|
|
26
|
+
...connectorsActionReach,
|
|
27
|
+
input: { schema: createSchema({ integrationId, verificationInput }) },
|
|
28
|
+
execute: (input, context) => connections.beginAuthorization({ ...input, context })
|
|
29
|
+
}),
|
|
30
|
+
Object.freeze({
|
|
31
|
+
id: "connectors.verifyClientCredentials",
|
|
32
|
+
kind: "command",
|
|
33
|
+
...connectorsActionReach,
|
|
34
|
+
input: { schema: createSchema({ integrationId, verificationInput }) },
|
|
35
|
+
execute: (input, context) => connections.connectClientCredentials({ ...input, context })
|
|
36
|
+
}),
|
|
37
|
+
Object.freeze({
|
|
38
|
+
id: "connectors.verifyServiceAccount",
|
|
39
|
+
kind: "command",
|
|
40
|
+
...connectorsActionReach,
|
|
41
|
+
input: { schema: createSchema({ integrationId, verificationInput }) },
|
|
42
|
+
execute: (input, context) => connections.connectServiceAccount({ ...input, context })
|
|
43
|
+
}),
|
|
44
|
+
Object.freeze({
|
|
45
|
+
id: "connectors.verifyApiKey",
|
|
46
|
+
kind: "command",
|
|
47
|
+
...connectorsActionReach,
|
|
48
|
+
input: { schema: createSchema({ integrationId, verificationInput }) },
|
|
49
|
+
execute: (input, context) => connections.connectApiKey({ ...input, context })
|
|
50
|
+
}),
|
|
51
|
+
Object.freeze({
|
|
52
|
+
id: "connectors.verifyWithoutCredentials",
|
|
53
|
+
kind: "command",
|
|
54
|
+
...connectorsActionReach,
|
|
55
|
+
input: { schema: createSchema({ integrationId, verificationInput }) },
|
|
56
|
+
execute: (input, context) => connections.connectWithoutCredentials({ ...input, context })
|
|
57
|
+
}),
|
|
58
|
+
Object.freeze({
|
|
59
|
+
id: "connectors.disconnect",
|
|
60
|
+
kind: "command",
|
|
61
|
+
...connectorsActionReach,
|
|
62
|
+
input: { schema: createSchema({ integrationId }) },
|
|
63
|
+
execute: (input, context) => connections.disconnect({ ...input, context })
|
|
64
|
+
})
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {import("./types.js").ConnectionServiceOptions} options */
|
|
5
69
|
function createConnectorsFeature(options) {
|
|
6
70
|
return defineFeature({
|
|
7
71
|
id: "connectors.core",
|
|
@@ -11,24 +75,7 @@ function createConnectorsFeature(options) {
|
|
|
11
75
|
return { connections: createConnectionService(options) };
|
|
12
76
|
},
|
|
13
77
|
actions({ connections }) {
|
|
14
|
-
|
|
15
|
-
return [
|
|
16
|
-
["status", "query", { integrationId }, (input, context) => connections.status({ ...input, context })],
|
|
17
|
-
["connect", "command", { integrationId, verificationInput: { type: "object", additionalProperties: true } }, (input, context) => connections.beginAuthorization({ ...input, context })],
|
|
18
|
-
["verifyClientCredentials", "command", { integrationId, verificationInput: { type: "object", additionalProperties: true } }, (input, context) => connections.connectClientCredentials({ ...input, context })],
|
|
19
|
-
["verifyServiceAccount", "command", { integrationId, verificationInput: { type: "object", additionalProperties: true } }, (input, context) => connections.connectServiceAccount({ ...input, context })],
|
|
20
|
-
["verifyApiKey", "command", { integrationId, verificationInput: { type: "object", additionalProperties: true } }, (input, context) => connections.connectApiKey({ ...input, context })],
|
|
21
|
-
["verifyWithoutCredentials", "command", { integrationId, verificationInput: { type: "object", additionalProperties: true } }, (input, context) => connections.connectWithoutCredentials({ ...input, context })],
|
|
22
|
-
["disconnect", "command", { integrationId }, (input, context) => connections.disconnect({ ...input, context })]
|
|
23
|
-
].map(([name, kind, fields, execute]) => ({
|
|
24
|
-
id: `connectors.${name}`,
|
|
25
|
-
kind,
|
|
26
|
-
channels: ["api", "internal"],
|
|
27
|
-
surfaces: ["app"],
|
|
28
|
-
idempotency: "none",
|
|
29
|
-
input: { schema: createSchema(fields) },
|
|
30
|
-
execute
|
|
31
|
-
}));
|
|
78
|
+
return createConnectorsActions({ connections });
|
|
32
79
|
}
|
|
33
80
|
});
|
|
34
81
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as oauth from "oauth4webapi";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { registrationClientAuthentication, registrationGrantType } from "../shared/configuration.js";
|
|
4
|
+
import { ConnectorError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
function validateCallbackUrl(value) {
|
|
7
|
+
let url;
|
|
8
|
+
try { url = new URL(value); } catch { /* reported below */ }
|
|
9
|
+
const localHttp = url?.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
10
|
+
if (!url || (!localHttp && url.protocol !== "https:") || url.username || url.password || url.hash || url.search) {
|
|
11
|
+
throw new ConnectorError("connector_callback_invalid", "Configure an HTTPS callback URL, or a local development callback.", { messageCode: "connectors.connection.configureHttpsCallbackUrlLocal" });
|
|
12
|
+
}
|
|
13
|
+
return url.href;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The credentials an integration's bindings resolve to: its OAuth
|
|
18
|
+
* registration, API key, service-account credential or Env refresh token.
|
|
19
|
+
* @param {object} dependencies
|
|
20
|
+
* @param {Record<string, any>} dependencies.config The validated integration configuration.
|
|
21
|
+
* @param {import("./types.js").ResolveReference} dependencies.resolveReference
|
|
22
|
+
*/
|
|
23
|
+
function createConnectionCredentials({ config, resolveReference }) {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Record<string, any>} integration
|
|
26
|
+
* @param {import("./providerRequests.js").ConnectionProvider} provider
|
|
27
|
+
*/
|
|
28
|
+
async function registrationFor(integration, provider) {
|
|
29
|
+
const registration = config.registrations[integration.authentication.registrationRef];
|
|
30
|
+
if (registration?.source !== "own" || integration.authentication.method !== "oauth2" || !provider.oauth) {
|
|
31
|
+
throw new ConnectorError("connector_mode_unavailable", "This runtime does not yet support the selected credential mode.", { messageCode: "connectors.connection.runtimeDoesNotYetSupport" });
|
|
32
|
+
}
|
|
33
|
+
if (registration.clientId.trim() === "MISSING") {
|
|
34
|
+
throw new ConnectorError("connector_binding_missing", "The provider client ID is not configured.", { messageCode: "connectors.connection.providerClientIdNotConfigured" });
|
|
35
|
+
}
|
|
36
|
+
const grantType = registrationGrantType(registration);
|
|
37
|
+
const tokenEndpointAuthMethod = registrationClientAuthentication(registration);
|
|
38
|
+
if (provider.oauthPkce === false && tokenEndpointAuthMethod === "none") {
|
|
39
|
+
throw new ConnectorError("connector_mode_unavailable", "This provider's non-PKCE flow requires a confidential client.", { messageCode: "connectors.connection.providerNonPkceFlowRequires" });
|
|
40
|
+
}
|
|
41
|
+
let clientSecret;
|
|
42
|
+
let callback;
|
|
43
|
+
try {
|
|
44
|
+
if (tokenEndpointAuthMethod !== "none") {
|
|
45
|
+
clientSecret = await resolveReference(registration.clientSecretRef);
|
|
46
|
+
if (typeof clientSecret !== "string" || !clientSecret.trim() || clientSecret.trim() === "MISSING") throw new Error("Invalid client secret.");
|
|
47
|
+
}
|
|
48
|
+
if (grantType === "authorization_code") callback = await resolveReference(registration.callbackUrlRef);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new ConnectorError("connector_binding_missing", "A required registration binding is missing.", { messageCode: "connectors.connection.requiredRegistrationBindingMissing" });
|
|
51
|
+
}
|
|
52
|
+
const callbackUrl = grantType === "authorization_code" ? validateCallbackUrl(callback) : undefined;
|
|
53
|
+
// readable-check: allow method-probe - optional operation: validateCallbackUrl is an optional hook of the provider definition
|
|
54
|
+
if (callbackUrl && typeof provider.validateCallbackUrl === "function" && provider.validateCallbackUrl(callbackUrl) !== true) {
|
|
55
|
+
throw new ConnectorError("connector_callback_invalid", "The callback URL does not meet this provider's requirements.", { messageCode: "connectors.connection.callbackUrlDoesNotMeet" });
|
|
56
|
+
}
|
|
57
|
+
let clientAuth = tokenEndpointAuthMethod === "none" ? oauth.None()
|
|
58
|
+
: tokenEndpointAuthMethod === "client_secret_basic" ? oauth.ClientSecretBasic(clientSecret) : oauth.ClientSecretPost(clientSecret);
|
|
59
|
+
if (tokenEndpointAuthMethod === "client_secret_basic" && provider.oauthBasicEncoding === "raw") {
|
|
60
|
+
clientAuth = (_as, client, _body, headers) => {
|
|
61
|
+
headers.set("authorization", `Basic ${Buffer.from(`${client.client_id}:${clientSecret}`).toString("base64")}`);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
// readable-check: allow method-probe - data: a provider definition gives oauth as metadata or as a function of the settings
|
|
66
|
+
oauth: typeof provider.oauth === "function" ? provider.oauth(integration.settings) : provider.oauth,
|
|
67
|
+
// readable-check: allow method-probe - data: a provider definition gives oauthResource as a value or as a function of the settings
|
|
68
|
+
resource: typeof provider.oauthResource === "function" ? provider.oauthResource(integration.settings) : provider.oauthResource,
|
|
69
|
+
client: { client_id: registration.clientId },
|
|
70
|
+
clientAuth: provider.oauthClientIdParameter ? (as, client, body, headers) => {
|
|
71
|
+
clientAuth(as, client, body, headers);
|
|
72
|
+
body.set(provider.oauthClientIdParameter, client.client_id);
|
|
73
|
+
body.delete("client_id");
|
|
74
|
+
} : clientAuth,
|
|
75
|
+
tokenEndpointAuthMethod,
|
|
76
|
+
grantType,
|
|
77
|
+
callbackUrl,
|
|
78
|
+
registrationRef: integration.authentication.registrationRef
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {Record<string, any>} integration
|
|
84
|
+
* @param {import("./providerRequests.js").ConnectionProvider} provider
|
|
85
|
+
*/
|
|
86
|
+
async function apiKeyFor(integration, provider) {
|
|
87
|
+
if (!integration.authentication.secretRef && provider.apiKeySecretOptional) return "";
|
|
88
|
+
try {
|
|
89
|
+
const value = await resolveReference(integration.authentication.secretRef);
|
|
90
|
+
if (typeof value !== "string" || (!provider.apiKeySecretOptional && !value.trim()) || value.trim() === "MISSING" || /[\r\n]/u.test(value)) throw new Error("Invalid key.");
|
|
91
|
+
return value;
|
|
92
|
+
} catch {
|
|
93
|
+
throw new ConnectorError("connector_binding_missing", "The API key binding is missing or invalid.", { messageCode: "connectors.connection.apiKeyBindingMissingInvalid" });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function serviceAccountCredentialFor(integration) {
|
|
98
|
+
try {
|
|
99
|
+
const credential = await resolveReference(integration.authentication.secretRef);
|
|
100
|
+
if (typeof credential !== "string" || !credential.trim() || credential.trim() === "MISSING" || credential.length > 65_536 || credential.includes("\0")) throw new Error("Invalid credential.");
|
|
101
|
+
return { credential, fingerprint: createHash("sha256").update(credential).digest("hex") };
|
|
102
|
+
} catch {
|
|
103
|
+
throw new ConnectorError("connector_binding_missing", "The service-account credential binding is missing or invalid.", { messageCode: "connectors.connection.serviceAccountCredentialBindingMissing" });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// An Env refresh token seeds the shared connection; the app then refreshes
|
|
108
|
+
// and stores rotated tokens itself, and reseeds when the value or the OAuth
|
|
109
|
+
// client changes.
|
|
110
|
+
async function envRefreshTokenFor(integration) {
|
|
111
|
+
try {
|
|
112
|
+
const value = await resolveReference(integration.authentication.refreshTokenRef);
|
|
113
|
+
const refreshToken = typeof value === "string" ? value.trim() : "";
|
|
114
|
+
if (!refreshToken || refreshToken === "MISSING" || /\s/u.test(refreshToken)) throw new Error("Invalid token.");
|
|
115
|
+
return { refreshToken, fingerprint: createHash("sha256").update(refreshToken).digest("hex") };
|
|
116
|
+
} catch {
|
|
117
|
+
throw new ConnectorError("connector_binding_missing", "The refresh token binding is missing or invalid.", { messageCode: "connectors.connection.refreshTokenBindingMissingInvalid" });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
registrationFor,
|
|
123
|
+
apiKeyFor,
|
|
124
|
+
serviceAccountCredentialFor,
|
|
125
|
+
envRefreshTokenFor
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { createConnectionCredentials };
|