@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.
@@ -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 };