@jskit-ai/connectors-core 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +551 -0
- package/docs/oauth-callbacks.md +65 -0
- package/docs/online-setup.md +56 -0
- package/docs/setup-command.md +178 -0
- package/migrations/connectors_core_initial.cjs +17 -0
- package/package.json +65 -0
- package/src/server/ConnectorsFeature.js +36 -0
- package/src/server/connectionService.js +664 -0
- package/src/server/credentialProtection.js +34 -0
- package/src/server/environmentReferences.js +13 -0
- package/src/server/errors.js +27 -0
- package/src/server/fileConnectionStore.js +86 -0
- package/src/server/fileStorage.js +2 -0
- package/src/server/index.js +4 -0
- package/src/server/knexConnectionStore.js +65 -0
- package/src/server/storage.js +2 -0
- package/src/shared/configuration.js +225 -0
- package/test/connectionService.test.js +1231 -0
- package/test/fileConnectionStore.test.js +165 -0
- package/test/knexConnectionStore.test.js +178 -0
- package/test/serviceAccount.test.js +192 -0
- package/test/setupCommand.test.js +364 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Application setup in a hosted editor or CLI
|
|
2
|
+
|
|
3
|
+
Each project owns its configuration and connections independently. Hosted and
|
|
4
|
+
installed editors can edit the same configuration; neither supplies a default
|
|
5
|
+
shared provider registration, a token gateway or operator-funded API capacity.
|
|
6
|
+
JSKIT supplies JavaScript libraries. Other application frameworks implement their
|
|
7
|
+
own runtime and use their native tools.
|
|
8
|
+
|
|
9
|
+
## Inputs by connection type
|
|
10
|
+
|
|
11
|
+
| Type | Application developer or administrator supplies | Application runtime owns |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| OAuth user consent | Provider client ID, secret reference where required, callback reference, permissions | Callback route, attempts, account grants and refresh |
|
|
14
|
+
| OAuth client credentials | Confidential client ID, secret reference and service permissions | Token acquisition, verification and renewal; no browser callback |
|
|
15
|
+
| API key or service-account credential | Private environment binding or app-supported secret reference; resource settings | Credential resolution, provider check and authorized requests |
|
|
16
|
+
| Credential-free provider | Endpoint and supported non-secret settings | Requests and honest availability/error reporting |
|
|
17
|
+
| Browser resource | Public configuration and provider origin restrictions | Browser integration; private secrets remain server-side |
|
|
18
|
+
| Webhooks | App receiving endpoint, subscription settings and signing secret | Signature verification, event handling and subscription lifecycle |
|
|
19
|
+
|
|
20
|
+
Provider-specific fields and supported modes come from the provider definition.
|
|
21
|
+
A supported API-key mode does not imply OAuth support. Browser origins, webhook
|
|
22
|
+
URLs, sending-domain DNS and OAuth callbacks are different inputs. Each guide
|
|
23
|
+
must identify which apply and what changes when hosting or domains change.
|
|
24
|
+
|
|
25
|
+
## Source, environment and runtime state
|
|
26
|
+
|
|
27
|
+
- `integrations.json` contains non-secret settings and references. CLI users
|
|
28
|
+
author it directly; an editor uses the same validation.
|
|
29
|
+
- Existing project Env facilities supply administrator keys and client secrets.
|
|
30
|
+
Saving a reference does not prove its binding exists or has provider access.
|
|
31
|
+
- Individual users' provider grants live in application-owned private runtime
|
|
32
|
+
storage. They are not shared environment variables.
|
|
33
|
+
- A shared connection serves app-authorized users of one application. Independent
|
|
34
|
+
projects do not share live connections merely because they share an editor workspace.
|
|
35
|
+
- Applications choose their storage. The file-store option does not require a
|
|
36
|
+
database; selecting SQL explicitly makes its migrations the application's job.
|
|
37
|
+
|
|
38
|
+
See [application-owned callbacks](oauth-callbacks.md) for OAuth setup and moves.
|
|
39
|
+
|
|
40
|
+
## Editor integration boundary
|
|
41
|
+
|
|
42
|
+
Configuration editing must work before an app runtime has been implemented.
|
|
43
|
+
Connection management requires an explicit application-owned setup operation
|
|
44
|
+
invoked through the editor's existing execution facilities. JSKIT's connection
|
|
45
|
+
service supplies reusable runtime methods; it does not implement that editor
|
|
46
|
+
transport or infer another framework's command.
|
|
47
|
+
|
|
48
|
+
The editor should display missing configuration or missing app setup honestly.
|
|
49
|
+
Only successful provider verification establishes a connected account. Status,
|
|
50
|
+
reconnect, cancellation and disconnect must use the same application owner and
|
|
51
|
+
environment as the original operation. Production management must use deployed
|
|
52
|
+
application code and state, not an arbitrary editing session.
|
|
53
|
+
|
|
54
|
+
The complete editor setup journey remains work in progress. Focused protocol
|
|
55
|
+
fixtures are evidence for library behavior, not live provider consent or a
|
|
56
|
+
complete generated application's functionality.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Application-owned setup command
|
|
2
|
+
|
|
3
|
+
An application can expose its connection setup to a CLI or an editor using the
|
|
4
|
+
same `createConnectionService()` instance as its backend. JSKIT owns connection
|
|
5
|
+
logic and optional stores; the application owns the executable, operator
|
|
6
|
+
identity, environment, callback route and deployment. Neither the backend nor
|
|
7
|
+
this library requires Vibe64 to run.
|
|
8
|
+
|
|
9
|
+
For Vibe64, the application declares its own command in its Stack:
|
|
10
|
+
|
|
11
|
+
```markdown
|
|
12
|
+
## Integration setup
|
|
13
|
+
|
|
14
|
+
- Command with `nodejs` in `.`: `node` `scripts/integrations.js`
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The script loads the application's existing configuration and server composition.
|
|
18
|
+
Resolve administrator references from its existing Env using
|
|
19
|
+
`createEnvironmentReferenceResolver()`. Use an application-owned store whose
|
|
20
|
+
identity includes the environment. Keep runtime data outside source and release
|
|
21
|
+
snapshots. The script's operator context must be established by the application;
|
|
22
|
+
never accept applicationId, subjectId or administrator privileges from stdin.
|
|
23
|
+
The normal service `authorize` callback still enforces connect/status/disconnect
|
|
24
|
+
permissions. An editor user's identity is not an application user's identity.
|
|
25
|
+
|
|
26
|
+
## Dispatch example
|
|
27
|
+
|
|
28
|
+
The following function belongs in the application's script. It accepts the
|
|
29
|
+
already-created service, validated configuration and trusted operator context.
|
|
30
|
+
It is deliberately a composition example, not a new JSKIT command runner.
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
async function dispatchSetup(request, { connections, configuration, context }) {
|
|
34
|
+
const { protocol, requestId, operation, integrationId } = request;
|
|
35
|
+
if (protocol !== "vibe64.integration-setup.command.v1" ||
|
|
36
|
+
typeof requestId !== "string" || !requestId ||
|
|
37
|
+
!["status", "connect", "cancel", "disconnect"].includes(operation) ||
|
|
38
|
+
!Object.hasOwn(configuration.integrations, integrationId)) {
|
|
39
|
+
throw new Error("Invalid integration setup request.");
|
|
40
|
+
}
|
|
41
|
+
const integration = configuration.integrations[integrationId];
|
|
42
|
+
if (integration.accountMode === "per-user") {
|
|
43
|
+
throw new Error("Individual users connect inside the application.");
|
|
44
|
+
}
|
|
45
|
+
const input = { context, integrationId };
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
if (operation === "disconnect") result = await connections.disconnect(input);
|
|
49
|
+
else if (operation === "cancel") {
|
|
50
|
+
if (typeof request.attemptId !== "string" || !request.attemptId) {
|
|
51
|
+
throw new Error("Choose the pending attempt to cancel.");
|
|
52
|
+
}
|
|
53
|
+
result = await connections.cancelAuthorization({ ...input, state: request.attemptId });
|
|
54
|
+
} else if (operation === "status") {
|
|
55
|
+
result = await connections.resumeAuthorization(input) || await connections.status(input);
|
|
56
|
+
} else {
|
|
57
|
+
const verification = { ...input, verificationInput: request.verificationInput || {} };
|
|
58
|
+
const method = integration.authentication.method;
|
|
59
|
+
if (method === "api-key") result = await connections.connectApiKey(verification);
|
|
60
|
+
else if (method === "none") result = await connections.connectWithoutCredentials(verification);
|
|
61
|
+
else if (method === "service-account") result = await connections.connectServiceAccount(verification);
|
|
62
|
+
else if (configuration.registrations[integration.authentication.registrationRef].grantType === "client_credentials") {
|
|
63
|
+
result = await connections.connectClientCredentials(verification);
|
|
64
|
+
} else result = await connections.beginAuthorization(verification);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const setupIssue = {
|
|
68
|
+
connector_binding_missing: "credentials-missing",
|
|
69
|
+
connector_callback_invalid: "callback-invalid"
|
|
70
|
+
}[error.code];
|
|
71
|
+
if (["status", "connect"].includes(operation) && setupIssue) {
|
|
72
|
+
const previous = await connections.status(input);
|
|
73
|
+
if (["connected", "reconnect-required"].includes(previous.status)) {
|
|
74
|
+
return { protocol, requestId, status: "reconnect-required" };
|
|
75
|
+
}
|
|
76
|
+
return { protocol, requestId, status: "unconfigured", setupIssue };
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (result.authorizationUrl) {
|
|
81
|
+
return { protocol, requestId, status: "pending", authorizationUrl: result.authorizationUrl,
|
|
82
|
+
attemptId: new URL(result.authorizationUrl).searchParams.get("state"),
|
|
83
|
+
expiresAt: new Date(result.expiresAt).toISOString(), callbackUrl: result.callbackUrl };
|
|
84
|
+
}
|
|
85
|
+
return { protocol, requestId, status: result.status,
|
|
86
|
+
...(result.callbackUrl ? { callbackUrl: result.callbackUrl } : {}),
|
|
87
|
+
...(result.accountLabel ? { accountLabel: result.accountLabel } : {}),
|
|
88
|
+
...(result.configurationError ? { setupIssue: {
|
|
89
|
+
connector_binding_missing: "credentials-missing",
|
|
90
|
+
connector_callback_invalid: "callback-invalid"
|
|
91
|
+
}[result.configurationError] } : {}),
|
|
92
|
+
...(result.grantedScopes ? { grantedScopes: result.grantedScopes } : {}),
|
|
93
|
+
...(result.verifiedAt !== undefined ? { verifiedAt: new Date(result.verifiedAt).toISOString() } : {}) };
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Read exactly one JSON request from stdin with a 32 KiB bound. Write only the
|
|
98
|
+
returned JSON object plus a newline to stdout. On failure exit nonzero; do not
|
|
99
|
+
print credentials, provider responses or raw exceptions. Close the application's
|
|
100
|
+
database pool/resources in `finally`. The editor bounds the command at 30 seconds;
|
|
101
|
+
return pending immediately instead of waiting for browser consent.
|
|
102
|
+
|
|
103
|
+
`verificationInput` supplies provider-specific check inputs such as a resource
|
|
104
|
+
ID. It is not a credential entry channel. Keys and client secrets belong in Env.
|
|
105
|
+
A configuration save does not verify credentials or produce a connected state.
|
|
106
|
+
|
|
107
|
+
Status validates required local bindings without calling the provider. Before a
|
|
108
|
+
first connection, missing credentials or an invalid callback produce
|
|
109
|
+
`unconfigured` with a safe `configurationError` code; the dispatcher maps that
|
|
110
|
+
code to the editor's setup guidance. A previous grant remains available for
|
|
111
|
+
disconnect and reports `reconnect-required` when its bindings are incomplete.
|
|
112
|
+
For a valid authorization-code registration, status and pending authorization
|
|
113
|
+
report `callbackUrl` resolved from the application's Env. This is the URL to
|
|
114
|
+
register with the provider, not the editor's suggestion. Invalid or incomplete
|
|
115
|
+
bindings do not expose an unvalidated callback or private Env values.
|
|
116
|
+
|
|
117
|
+
Providers may expose `accountLabel` from their verified account response. Gmail
|
|
118
|
+
uses the mailbox address returned by its existing profile check; no additional
|
|
119
|
+
permission or request is needed. The runtime persists this display label with
|
|
120
|
+
the grant and status returns it after restart. Labels must be nonblank strings
|
|
121
|
+
of at most 256 characters without control or formatting characters. They are
|
|
122
|
+
display metadata, never an authentication identity or authorization input.
|
|
123
|
+
Providers without a reliable account label omit it. Disconnect removes it with
|
|
124
|
+
the grant; cancelled replacement consent retains the previous grant's label.
|
|
125
|
+
|
|
126
|
+
## Callback and recovery
|
|
127
|
+
|
|
128
|
+
### Individual application accounts
|
|
129
|
+
|
|
130
|
+
For `accountMode: "per-user"`, the editor configures the OAuth client but does
|
|
131
|
+
not run this shared-operator dispatcher. The application implements its own
|
|
132
|
+
connection screen using its existing UI and authenticated routes:
|
|
133
|
+
|
|
134
|
+
| App action | Existing runtime operation | Ownership input |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| Show connection | `status()` and, when needed, `resumeAuthorization()` | Current authenticated app user |
|
|
137
|
+
| Connect or reconnect | `beginAuthorization()` | Current app user after the app's mutation/CSRF checks |
|
|
138
|
+
| Receive provider callback | `completeAuthorization()` | Authenticated initiating user, recovered by the app's session system |
|
|
139
|
+
| Cancel pending consent | `cancelAuthorization()` followed by `status()` | Current user and the exact pending state |
|
|
140
|
+
| Disconnect | `disconnect()` | Current user after the app's mutation/CSRF checks |
|
|
141
|
+
|
|
142
|
+
Supply `{ context, integrationId }` to each operation. Derive `context` from
|
|
143
|
+
trusted application authentication and membership, never a form field, callback
|
|
144
|
+
query or editor identity. The service's `authorize` function must enforce the
|
|
145
|
+
application and user boundary. Keep registration credentials in backend Env;
|
|
146
|
+
each user's grant lives separately in the existing connection store. The browser
|
|
147
|
+
receives only safe status/consent metadata. OAuth callback state does not replace
|
|
148
|
+
the application's own authentication. This account-linking flow does not create
|
|
149
|
+
a Google login feature.
|
|
150
|
+
|
|
151
|
+
### Shared callback requirements
|
|
152
|
+
|
|
153
|
+
Implement the HTTP callback in the application using its own routing and
|
|
154
|
+
session/authentication system. Bind the browser's authorization flow to the
|
|
155
|
+
application identity that initiated it. Call `completeAuthorization()` with that
|
|
156
|
+
trusted context, integration ID and the received absolute callback URL. Do not
|
|
157
|
+
trust a query-string user ID. Its saved state/PKCE checks and one-time consumption
|
|
158
|
+
remain mandatory. Redirect back to an application-owned result page after
|
|
159
|
+
completion; never put tokens in that redirect.
|
|
160
|
+
|
|
161
|
+
The callback Env value must match both this real route and the provider's
|
|
162
|
+
registered redirect URI. For a hosted project use its assigned application URL
|
|
163
|
+
as the starting origin, then append the implemented callback path. Custom domains
|
|
164
|
+
or explicit overrides require updating that Env value and provider registration;
|
|
165
|
+
changing an editor hostname cannot change application identity.
|
|
166
|
+
|
|
167
|
+
Status can recover a still-valid pending attempt after the process restarts.
|
|
168
|
+
Cancellation consumes its state and preserves an earlier working connection.
|
|
169
|
+
After cancel, check status again. Disconnect removes this app connection and its
|
|
170
|
+
pending attempts. It makes no provider revocation request and does not delete
|
|
171
|
+
configuration or Env credentials. Revoke consent or keys separately through the
|
|
172
|
+
provider's account settings; that may affect other apps sharing the registration
|
|
173
|
+
or key. Do not present local removal as provider-wide revocation.
|
|
174
|
+
No raw PKCE verifier, access token or refresh token belongs in setup output.
|
|
175
|
+
|
|
176
|
+
This example documents the command boundary. It does not install an application's
|
|
177
|
+
callback, create users, configure its store, register an OAuth client, or prove
|
|
178
|
+
that its deployed environment has been wired correctly.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
exports.up = async function up(knex) {
|
|
2
|
+
await knex.schema.createTable("connector_connections", (table) => {
|
|
3
|
+
table.string("connection_key", 64).primary();
|
|
4
|
+
table.text("payload", "mediumtext").nullable();
|
|
5
|
+
});
|
|
6
|
+
await knex.schema.createTable("connector_authorization_attempts", (table) => {
|
|
7
|
+
table.string("attempt_key", 64).primary();
|
|
8
|
+
table.string("connection_key", 64).notNullable().references("connection_key").inTable("connector_connections").onDelete("CASCADE");
|
|
9
|
+
table.bigInteger("expires_at").notNullable().index();
|
|
10
|
+
table.text("payload", "mediumtext").notNullable();
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
exports.down = async function down(knex) {
|
|
15
|
+
await knex.schema.dropTable("connector_authorization_attempts");
|
|
16
|
+
await knex.schema.dropTable("connector_connections");
|
|
17
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/connectors-core",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Portable integration configuration and server-side account connection runtime.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test --test-concurrency=1"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./shared/configuration": "./src/shared/configuration.js",
|
|
11
|
+
"./server": "./src/server/index.js",
|
|
12
|
+
"./server/storage": "./src/server/storage.js",
|
|
13
|
+
"./server/file-storage": "./src/server/fileStorage.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"jose": "^6.1.3",
|
|
17
|
+
"json-rest-schema": "^1.0.17",
|
|
18
|
+
"oauth4webapi": "3.8.8",
|
|
19
|
+
"proper-lockfile": "4.1.2"
|
|
20
|
+
},
|
|
21
|
+
"jskit": {
|
|
22
|
+
"kind": "runtime",
|
|
23
|
+
"migrations": {
|
|
24
|
+
"directories": [
|
|
25
|
+
"migrations"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"metadata": {
|
|
29
|
+
"jskit": {
|
|
30
|
+
"tableOwnership": {
|
|
31
|
+
"tables": [
|
|
32
|
+
{
|
|
33
|
+
"tableName": "connector_connections"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"tableName": "connector_authorization_attempts"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"capabilities": {
|
|
43
|
+
"provides": [],
|
|
44
|
+
"requires": []
|
|
45
|
+
},
|
|
46
|
+
"runtime": {
|
|
47
|
+
"server": {
|
|
48
|
+
"providers": []
|
|
49
|
+
},
|
|
50
|
+
"client": {
|
|
51
|
+
"providers": []
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@jskit-ai/database-runtime": "0.1.184",
|
|
57
|
+
"@jskit-ai/http-runtime": "0.1.182",
|
|
58
|
+
"@jskit-ai/kernel": "0.1.184"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"@jskit-ai/database-runtime": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineFeature } from "@jskit-ai/kernel/server/features";
|
|
2
|
+
import { createSchema } from "json-rest-schema";
|
|
3
|
+
import { createConnectionService } from "./connectionService.js";
|
|
4
|
+
|
|
5
|
+
function createConnectorsFeature(options) {
|
|
6
|
+
return defineFeature({
|
|
7
|
+
id: "connectors.core",
|
|
8
|
+
domain: "connectors",
|
|
9
|
+
provides: { connections: "connectors.core" },
|
|
10
|
+
setup() {
|
|
11
|
+
return { connections: createConnectionService(options) };
|
|
12
|
+
},
|
|
13
|
+
actions({ connections }) {
|
|
14
|
+
const integrationId = { type: "string", required: true, minLength: 1, maxLength: 200 };
|
|
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
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { createConnectorsFeature };
|