@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 ADDED
@@ -0,0 +1,551 @@
1
+ # Account connections
2
+
3
+ This package owns portable integration configuration, OAuth authorization and
4
+ refresh, connection state, and optional encrypted persistence. Application code owns
5
+ identity, permission decisions, HTTP routes, environment bindings and database
6
+ operation scripts. The same exports work in a server or an app-owned CLI.
7
+
8
+ The implementation supports **own OAuth registrations** (user consent and
9
+ provider-declared client credentials), provider-declared **service-account credentials**, **API-key connections**
10
+ and provider-declared **no-credential connections**.
11
+ Google Calendar supplies OAuth operations; Resend and Firecrawl supply API-key
12
+ operations through the connectors catalogue. Registrations use `source: "own"`.
13
+ Managed gateway registrations and their assignment fields are rejected during
14
+ configuration validation.
15
+
16
+ See [application-owned OAuth callbacks](docs/oauth-callbacks.md) for callback
17
+ routes, environment bindings and changes of domain or hosting.
18
+ The [application setup guide](docs/online-setup.md) explains credential and
19
+ runtime ownership for hosted editors and direct CLI use.
20
+
21
+ ## Pre-release migration to application-owned connections
22
+
23
+ This is V0. The runtime accepts one current configuration format; it has no
24
+ legacy registration reader, gateway adapter or automatic grant conversion.
25
+ `schemaVersion: 1` identifies the current schema, not every historical draft
26
+ that used that number.
27
+
28
+ 1. Back up the application's source and private runtime state before changing
29
+ its configuration. Review each named integration's provider, account mode,
30
+ permissions and settings against the current provider definition.
31
+ 2. Replace managed registrations with the application's own provider
32
+ registration. Remove `serviceUrlRef`, `serviceCredentialRef` and
33
+ `assignmentRef`; changing only `source` to `own` cannot supply a real client.
34
+ Set the actual client ID, required secret reference and callback reference.
35
+ API-key integrations instead reference the application's own key.
36
+ 3. Supply required secrets through the application's existing Env facilities.
37
+ `MISSING` is an incomplete placeholder, not a working credential. Keep
38
+ development and production bindings distinct. Register the exact app-owned
39
+ callback URL with the provider before attempting OAuth consent.
40
+ 4. Wire the current connection service into the application's authenticated
41
+ routes and optional setup command. Use stable application/principal IDs and
42
+ private persistent storage outside source or release directories. Retain the
43
+ existing encryption key for records that the application already owns.
44
+ 5. Reconnect when changing provider registration or moving away from a service
45
+ that held the grants. Do not copy another service's tokens or infer that
46
+ matching email addresses transfer consent or merge application users.
47
+ 6. Validate with `validateIntegrationConfiguration(configuration, { providers })`,
48
+ then verify connection, restart, cancellation and disconnect in the selected
49
+ environment. A source-file migration alone does not establish a connection.
50
+
51
+ Retire obsolete service credentials deliberately after the replacement works.
52
+ Local disconnect removes the selected app connection; provider-side revocation
53
+ and cleanup of old services are separate operator actions.
54
+
55
+ ## Configuration
56
+
57
+ Import `parseIntegrationConfiguration`, `validateIntegrationConfiguration` and
58
+ `integrationsSchema` from `@jskit-ai/connectors-core/shared/configuration`.
59
+ `integrationsSchema.getFieldDefinitions()` exposes the field definitions for
60
+ form inspection. Validate with `validateIntegrationConfiguration`, which also
61
+ checks references, credential combinations and provider scopes. Custom reference
62
+ validators do not currently provide transport JSON Schema export hooks.
63
+ References use a binding namespace such as `env:NAME` or `vault:path`;
64
+ literal HTTP/HTTPS URLs are rejected in reference fields.
65
+ Pass registered provider definitions in `{ providers }` for provider validation.
66
+ Scope entries may declare `required: true`; both CLI and UI validation reject
67
+ configurations missing any required permission. LinkedIn uses this for OpenID
68
+ and profile. Runtime operations still check the permissions actually granted.
69
+ Provider `settingsSchema` validates settings and supplies defaults in the returned
70
+ configuration. It can be a Schema instance or a synchronous function of the current
71
+ settings returning one. `getProviderSettingsSchema(provider, settings)` resolves
72
+ either form for CLI and UI consumers. Redshift uses this to require a workgroup
73
+ or cluster identifier according to deployment type and reject mixed fields.
74
+ Unknown providers fail by default; configuration editors can pass
75
+ `allowUnknownProviders: true` to preserve custom slots while validating known ones.
76
+ Errors expose `code`, `statusCode: 422` and dotted `fieldErrors` for shared forms.
77
+
78
+ `integrations.json` is application source. It contains named integration slots,
79
+ requested scopes, account modes and registration references. A registration
80
+ contains its public client ID and references to secrets and the **full callback
81
+ URL**, including its path. It never contains client secrets, authorization
82
+ codes, access tokens or refresh tokens. Unknown ordinary fields are rejected;
83
+ application-owned data belongs under `extensions` or provider `settings`.
84
+ Version changes require an explicit migration. Editing source does not transfer
85
+ an existing grant or request additional consent automatically.
86
+
87
+ Own registrations can specify `grantType: "client_credentials"`; omitted grant
88
+ types use `authorization_code`. Service accounts require a confidential client
89
+ and secret reference, forbid `callbackUrlRef`, and cannot use `per-user` ownership.
90
+ The provider must declare this flow in `oauthGrantTypes`. Its
91
+ `oauthClientAuthenticationMethods` can be an array or a synchronous function of
92
+ the grant type. `getProviderClientAuthenticationMethods(provider, grantType)`
93
+ resolves that contract for validation and forms. `getProviderScopes(provider,
94
+ settings, grantType)` similarly resolves `scopesForGrantType`, the existing
95
+ settings-dependent scopes, or the static scope list. Databricks uses both to
96
+ keep service credentials and user consent correctly configured.
97
+
98
+ Provider-specific service-account credentials use
99
+ `authentication: { method: "service-account", secretRef: "env:SERVICE_ACCOUNT" }`.
100
+ The reference resolves to a server-only credential string, including multiline
101
+ JSON when required by the provider. This mode requires selected permissions and
102
+ shared or assistant ownership. It does not use a registration, client ID,
103
+ callback URL or app-user identity. The provider validates its credential format;
104
+ the portable file retains only the reference.
105
+
106
+ ## Runtime
107
+
108
+ Import `createConnectionService`, `createConnectorsFeature` and
109
+ `createEnvironmentReferenceResolver` from `@jskit-ai/connectors-core/server`.
110
+
111
+ ```js
112
+ const connections = createConnectionService({
113
+ configuration,
114
+ providers: [googleCalendarProvider],
115
+ store,
116
+ resolveReference: createEnvironmentReferenceResolver(process.env),
117
+ authorize: applicationConnectionPolicy
118
+ });
119
+ ```
120
+
121
+ The required `authorize(context, { integrationId, operation, accountMode, input })`
122
+ returns `{ applicationId, subjectId }` from trusted application identity after
123
+ checking access. Return no owner to deny access. For a personal connection use
124
+ the authenticated app user's stable ID. For a shared account use a stable shared
125
+ subject only after checking membership and the requested operation. For
126
+ assistant access check the assistant's delegated permissions separately.
127
+ The environment resolver treats absent, blank and literal `MISSING` values as
128
+ incomplete bindings. Runtime credential checks also reject `MISSING` returned by
129
+ custom resolvers, and OAuth rejects a placeholder client ID before starting
130
+ consent. These failures report `connector_binding_missing`; they do not create a
131
+ verified connection or send the placeholder to the provider. Configuration may
132
+ retain placeholders while an administrator finishes setup.
133
+
134
+ Never trust owner IDs supplied by a browser. Include deployment/environment
135
+ identity in `applicationId` when several environments share storage.
136
+
137
+ For `invoke`, `input` is a separate copy of the caller's operation input. It is
138
+ omitted for connection-management methods. Check it when permission depends on
139
+ the requested tool, document or action. The service snapshots input before
140
+ awaiting policy, so neither caller nor policy mutation changes what executes.
141
+ For MCP `tools.call`, approve `input.name` and `input.arguments` explicitly;
142
+ membership alone must not grant arbitrary assistant tool execution.
143
+
144
+ Compose assistant-facing routes/tools with `executionMode: "assistant"`. This
145
+ is a trusted server/CLI composition choice, never an option from a request body.
146
+ The default `application` mode retains ordinary app authorization; `accountMode`
147
+ selects connection ownership and does not select the caller's execution mode.
148
+ In assistant mode, `assistantPolicy` on each integration supplies `enabled`,
149
+ `defaultPermission` (`ask`, `always`, `never`), and per-action `actions` overrides.
150
+ Omitted policy defaults to asking. Providers declare available override names in
151
+ `assistantActions`; the shared schema rejects unknown names and invalid values.
152
+ Keep this policy outside credential settings so edits do not require reconnection.
153
+
154
+ The existing `authorize` callback receives `assistantPermission: { action, decision }`
155
+ alongside the operation and copied input. For `ask`, the host must verify an
156
+ approval for that exact request and return `approved: true` with the authorized
157
+ owner. A missing approval raises `connector_approval_required` before provider
158
+ access. The host owns decision UI, persistence, expiry and replay prevention.
159
+ `always` still checks access; `never` or disabled access denies the action.
160
+ Workspace/organization rules remain in the host's authorization boundary and
161
+ cannot be overridden by the file. Do not trust a browser's `approved` flag.
162
+ Status, cancellation and local disconnect remain available through ordinary
163
+ access checks even when assistant actions are disabled. Pending OAuth completion
164
+ is still checked as a connect action; do not reuse an approval indiscriminately.
165
+
166
+ `authorizeAssistantAction({ context, integrationId, action, input })` checks a
167
+ declared host lifecycle action in assistant mode. It executes no provider operation
168
+ and creates no activation state. The host validates and performs its own operation
169
+ after authorization; use `invoke` for actual provider operations. This helper's
170
+ input is copied before authorization just like `invoke` input.
171
+
172
+ All methods take `{ context, integrationId }`:
173
+
174
+ | Method | Additional input | Result |
175
+ |---|---|---|
176
+ | `connectClientCredentials` | optional `verificationInput`, `signal` | Verified service account metadata; no browser consent |
177
+ | `connectServiceAccount` | optional `verificationInput`, `signal` | Provider-specific service credentials exchanged for a verified short-lived token |
178
+ | `beginAuthorization` | optional `verificationInput`, `signal` | Authorization URL, expiry and resolved callback URL |
179
+ | `completeAuthorization` | `callbackUrl`, optional `signal` | Verified connection metadata |
180
+ | `cancelAuthorization` | `state` | Cancelled attempt; existing connection preserved |
181
+ | `connectApiKey` | optional `verificationInput`, `signal` | API key verified through a provider read operation |
182
+ | `status` | — | Safe metadata or disconnected |
183
+ | `invoke` | named `operation`, `input`, optional `signal` | Provider operation result |
184
+ | `disconnect` | — | Local connection and pending attempts removed |
185
+
186
+ HTTP callback handlers must recover the same authenticated owner context as the
187
+ connect request. The runtime checks state, PKCE by default, exact callback destination,
188
+ registration identity, expiry and actual granted scopes. It verifies a provider
189
+ operation before reporting connected. Resource-specific providers (such as
190
+ Sheets) require a document ID in `verificationInput` when starting consent.
191
+ That input is validated before consent and retained in the owning attempt. Safe metadata excludes credentials.
192
+ Disconnect is local; it does not revoke the provider's entire shared grant.
193
+
194
+ A provider whose documented confidential flow excludes PKCE can set
195
+ `oauthPkce: false` (Workday). This cannot be used with a public client, and
196
+ pending attempts cannot switch between PKCE and non-PKCE flows. A provider with
197
+ no selectable scopes can use an empty scope list; authorization omits `scope`
198
+ in that case. Providers that offer permissions still require a selection.
199
+
200
+ Own registrations default to `tokenEndpointAuthMethod: "client_secret_post"`.
201
+ A provider can declare `"client_secret_basic"` in
202
+ `oauthClientAuthenticationMethods`; its own registration still requires a
203
+ secret reference. The OAuth library applies HTTP Basic client authentication
204
+ for both code exchange and refresh, without putting the secret in the body.
205
+ A provider may explicitly support `"none"` through
206
+ `oauthClientAuthenticationMethods`. Such registrations require Client ID and
207
+ callback references but forbid `clientSecretRef`; token grants use the OAuth
208
+ library's `None()` method and never resolve a client secret. Provider-owned
209
+ `validateClientId` checks specialized identifiers, such as Canva metadata URLs.
210
+ Authentication-type changes invalidate pending attempts and connected grants.
211
+ `clientIdLabel` and `clientIdHint` provide matching shared form copy. Providers
212
+ without an explicit methods list retain confidential-client authentication.
213
+
214
+ A provider can set `oauthClientIdParameter` when its documented wire protocol
215
+ uses a different field name. TikTok sets `client_key`; the portable file and
216
+ OAuth SDK client metadata still use `clientId` and `client_id`, respectively.
217
+ The runtime substitutes the wire field on authorization and token requests.
218
+ Provider `validateCallbackUrl(url)` can impose additional restrictions after the
219
+ shared URL checks; TikTok requires HTTPS and fewer than 512 characters.
220
+ These are provider-owned protocol rules, not arbitrary configuration overrides.
221
+
222
+ Providers that document granted scopes on the authorization callback can set
223
+ `scopesInAuthorizationResponse: true`. Their callback must contain exactly one
224
+ `scope` value. When the token response omits scopes, the service retains only
225
+ requested permissions present in that callback. Explicit token-response scopes
226
+ take precedence, and a permission required by the verification operation must
227
+ still be granted. Refresh preserves this reduced grant when scopes are omitted.
228
+
229
+ A provider with comma-separated OAuth permissions can set `scopeSeparator: ","`.
230
+ The default remains a space. Providers whose verification response describes
231
+ the grant can implement `grantedScopesFromVerification(result, { clientId })`.
232
+ It must return an array of scope strings and may reject a mismatched client.
233
+ The service retains only scopes present in the request, token/callback grant
234
+ and this verified array. Verification must still have its required permission;
235
+ otherwise the attempt fails without replacing an existing connection.
236
+ Initial OAuth grants are limited to configured permissions, even if a token
237
+ response contains additional scopes. For every OAuth provider, refresh retains
238
+ only scopes already granted and still
239
+ requested by configuration. It may reduce a grant but cannot expand it without
240
+ new consent, including providers without verification metadata.
241
+
242
+ API-key providers declare `apiKey.headers(key, settings)`, `apiKey.queryParameter`,
243
+ `apiKey.bodyParameter` or `apiKey.pathPrefix(key)` in their trusted runtime
244
+ definition. The service resolves the secret reference at request time. Query credentials are encoded and replace existing values only
245
+ after the operation destination passes its HTTPS/origin check. Body credentials
246
+ similarly replace their named JSON field in a new body object without mutating
247
+ operation input. They require an object body and reject GET/HEAD requests.
248
+ Path prefixes are supplied by trusted provider code after destination validation;
249
+ providers validate or encode the key as a path segment. The service changes only
250
+ the pathname and rejects ambiguous prefixes and dot segments. Neither operation
251
+ input nor configuration supplies a credential URL. Redirects are rejected.
252
+ Connection records contain the reference, not the key or authenticated URL,
253
+ and provider errors are reduced to safe connector errors. Application HTTP
254
+ instrumentation must redact authenticated URLs, credential headers and bodies.
255
+ Some read operations need input: PostHog flag verification requires an explicit
256
+ `distinct_id` in `verificationInput`. `connectApiKey` and the JSKIT
257
+ `verifyApiKey` action pass that input through the operation's ordinary validation.
258
+ It is not retained in connection records, and failed verification never creates
259
+ a connected record.
260
+ API keys are resolved on the server for each call. Connection storage retains
261
+ the reference, a private fingerprint of the last successfully used key and its
262
+ verified state. Key rotation does not copy a raw key into configuration or
263
+ connection records. Status reports `reconnect-required` when the Env key differs
264
+ from the verified key, without making a provider request. Successful explicit
265
+ verification or an authorized provider operation binds the replacement key. A
266
+ failed check leaves the last successful binding intact; a changed reference
267
+ requires verification again. The fingerprint is omitted from public status.
268
+
269
+ For pre-release installations with previously saved API-key connections, run
270
+ connection verification again. Records without a verified-key fingerprint report
271
+ `reconnect-required`; no table migration or compatibility adapter is required.
272
+ Do not populate the fingerprint from Env without performing verification.
273
+
274
+ A provider may set `apiKeySecretOptional: true` when an empty credential is a
275
+ documented mode, as with a ClickHouse database user's empty password. Omitting
276
+ `secretRef` then passes an empty string to its header function without resolving
277
+ a binding. An explicit reference still must resolve to a valid string; a missing
278
+ binding never falls back to no credentials. Providers without this flag retain
279
+ the required nonempty-secret contract.
280
+
281
+ `authenticationMethods: ["api-key", "none"]` declares a separate no-credential
282
+ mode. Its configuration is `{ method: "none" }` without either secret or
283
+ registration references. `connectWithoutCredentials({ context, integrationId,
284
+ verificationInput?, signal? })` performs the provider's check operation before
285
+ saving a grant, with no credential header or binding resolution. Authorization,
286
+ ownership, destination validation and file persistence still apply. Mode changes
287
+ invalidate old connections. This mode never substitutes for an application's
288
+ login or bypasses provider-side database permissions.
289
+ Provider `settingsFields[].authenticationMethods` restricts fields to particular
290
+ credential modes; the shared parser rejects values left in an incompatible mode.
291
+
292
+ Provider operations receive `(input, settings)` when constructing a request.
293
+ OAuth providers may supply `oauth(settings)` to select regional authorization
294
+ metadata. `oauthResource` may be a string or function of those settings; when
295
+ present, it is sent as `resource` in consent, code exchange and refresh. The
296
+ provider owns these fixed destinations, and existing settings comparisons
297
+ invalidate attempts/grants after a region change.
298
+ Providers that require the original callback when refreshing, such as Wave,
299
+ set `refreshRequiresRedirectUri: true`. The runtime saves that callback with
300
+ the grant and supplies `redirect_uri` during refresh. Changing the callback
301
+ binding then requires reconnection; configuration cannot redirect an existing
302
+ grant's refresh request to a new callback.
303
+ `oauthHeaders({ clientId })` can add a provider-required application header;
304
+ the runtime supplies the saved registration ID and retains ownership of the
305
+ Bearer header. `normalizeTokenResponse(response, { settings, grantType })` adapts
306
+ code-exchange and refresh replies before OAuth validation, for example Twitch's
307
+ scope arrays or Slack's nested user token. `grantType` is `authorization_code`
308
+ or `refresh_token`. `authorizationScopeParameter(settings)` selects the consent
309
+ permission parameter when a provider uses another name, such as Slack's
310
+ `user_scope`; the default is `scope`.
311
+ These are trusted server-provider hooks, not configuration or browser inputs.
312
+ Normalization must preserve validation and redact provider error bodies.
313
+ Shared definitions can expose `scopesForSettings(settings)` to select applicable
314
+ permissions from their catalogue. The parser rejects inapplicable selections;
315
+ the shared form uses the same function and removes incompatible selections when
316
+ settings change. Slack uses this for its user and bot permission sets.
317
+ Trusted operation request builders can return `headers` for protocol fields
318
+ such as Xero's tenant ID. The connection service adds its credential headers;
319
+ operation inputs must validate identifiers before constructing these headers.
320
+ These headers are provider code, never a free-form client configuration field.
321
+ Providers can supply `apiOrigins(settings)` for destinations derived from validated
322
+ application settings, such as Algolia's application ID. The runtime still checks
323
+ the exact origin before attaching credentials; it does not allow caller-supplied
324
+ URLs. Provider settings can reuse the exported `secretReference` field contract
325
+ from `connectors-core/shared/configuration` for additional credential references.
326
+ Verified connections and pending OAuth attempts retain the validated settings.
327
+ Changing settings, including region or sandbox/live environment, requires a new
328
+ verification and invalidates completion of consent begun with the old settings.
329
+
330
+ A trusted provider may supply `exchange(url, requestOptions, { fetchImpl, request, resolveReference, settings, apiKey })`
331
+ for a protocol such as MCP or AWS Signature Version 4. Operation validation, scopes, ownership and the
332
+ HTTPS/origin check run before this transport receives credential headers.
333
+ It receives the request body and bounded signal, and must enforce those
334
+ credentials' destination on every protocol request, reject redirects and
335
+ preserve cancellation. Ordinary providers retain the shared HTTP client.
336
+ This is a server-code hook, never an executable configuration field.
337
+ A provider may set `requestTimeoutMs` for its documented API timing needs;
338
+ ordinary requests and OAuth grants default to 15 seconds. Canva MCP uses
339
+ 60 seconds for design operations. These are trusted provider constants.
340
+ `request` is the existing parsed-JSON HTTP client. `resolveReference` is the
341
+ same authorized application's binding resolver, allowing a provider such as
342
+ Inngest to resolve its separate Event Key without duplicating runtime ownership.
343
+ The provider must select credentials for each destination; Inngest never sends
344
+ its Signing Key to the Event API.
345
+
346
+ `createConnectorsFeature(options)` supplies the ordinary `connectors.core`
347
+ capability and `connectors.status`, `connectors.connect` and
348
+ `connectors.verifyClientCredentials`, `connectors.verifyServiceAccount`, `connectors.verifyApiKey`, `connectors.verifyWithoutCredentials` and
349
+ `connectors.disconnect` actions. Compose it with the application's action
350
+ runtime. Product-specific operations call the service from their own named
351
+ actions. The library does not expose an arbitrary authenticated URL proxy.
352
+
353
+ Client credentials use `oauth4webapi`'s client credentials exchange and the same
354
+ verification, authorization and storage lock as user connections. Expiry renews
355
+ with the resolved application secret; no refresh token is stored for this flow.
356
+ Renewal requests only the existing grant. Changing a service connection's
357
+ configured scopes or grant type requires explicit verification again, and a
358
+ changed grant type also invalidates browser attempts. A failed subsequent API
359
+ request still commits renewed tokens. `invalid_client` and `invalid_grant`
360
+ require reconnection. Trusted providers can set `tokenRefreshLeewayMs`; the
361
+ default is 30 seconds and Databricks uses 40 seconds. This is runtime metadata,
362
+ not an editable source setting.
363
+
364
+ For `service-account` mode, a trusted server provider implements
365
+ `serviceAccountGrant({ credential, settings, scopes, fetchImpl, signal, now })`.
366
+ `now` is the current timestamp in milliseconds. The provider owns credential
367
+ parsing and the provider's grant protocol, pins its token destination, rejects
368
+ redirects and honors the bounded signal. It returns `access_token`,
369
+ `token_type: "Bearer"`, a numeric `expires_in` between zero (exclusive) and
370
+ 86,400 seconds, and optionally the granted `scope` string. Refresh tokens are
371
+ rejected. This is provider code, never executable configuration.
372
+
373
+ A provider's `checkOperation` may be an operation name or a function receiving
374
+ an authentication method and returning its verification operation name. An
375
+ operation can declare `authenticationMethods` to reject other credential methods
376
+ before transport. This supports providers whose REST API and MCP service have
377
+ different credentials and verification calls, without mixing their grants.
378
+
379
+ The connection service verifies the grant through `checkOperation`, enforces
380
+ both configured and granted permissions, and stores tokens through the existing
381
+ encrypted store. Each invocation resolves the credential reference; expiry or
382
+ a changed credential fingerprint obtains a replacement token under the same
383
+ connection lock. The credential itself is never saved. Renewal is committed
384
+ even if the following operation fails, and failed renewal never falls back to
385
+ an old token. Changing settings, the reference or requested scopes requires
386
+ verification again. This mode does not provide managed provisioning or browser
387
+ consent. The Firebase adapter supplies its provider-specific JWT bearer exchange;
388
+ the core and provider contracts are verified with controlled service-account fixtures.
389
+
390
+ ## File storage
391
+
392
+ Import `createFileConnectionStore` and `createCredentialProtection` from
393
+ `@jskit-ai/connectors-core/server/file-storage`. This entry point does not import
394
+ or require database-runtime. Use `createFileConnectionStore({ directory,
395
+ protection })` with an absolute private directory outside application source.
396
+ Each connection uses an encrypted JSON record containing its grant and pending
397
+ authorization attempts. The same protection contract is described below.
398
+
399
+ The store locks each identity across processes, commits by replacing the whole
400
+ file, and preserves the previous record when a callback or write fails. It
401
+ prunes expired attempts when that connection is accessed. Different owners
402
+ cannot open copied records. Malformed files and symlinks fail without rewriting
403
+ them. Files default to mode 0600 and new directories to 0700; hosts can supply
404
+ `fileMode` and `directoryMode` to match an existing filesystem identity contract.
405
+ It does not change permissions recursively or fix an incorrectly provisioned
406
+ runtime directory. Back up both the private JSON state and its separate key.
407
+
408
+ Locks use `proper-lockfile` with a consistent 60-second stale interval and
409
+ heartbeat. All writers must use this store and must not manually remove live
410
+ locks. A process killed during a provider exchange can still need reconnection;
411
+ local file replacement cannot make a remote token rotation transactional.
412
+
413
+ ## Storage and migrations
414
+
415
+ Import `createKnexConnectionStore` and `createCredentialProtection` from
416
+ `@jskit-ai/connectors-core/server/storage`. Use the application's existing Knex
417
+ client, with its selected JSKIT MySQL or PostgreSQL driver.
418
+
419
+ ```js
420
+ const protection = createCredentialProtection({
421
+ keys: { current: decoded32ByteSecretKey },
422
+ activeKeyId: "current"
423
+ });
424
+ const store = createKnexConnectionStore({ knex, protection });
425
+ ```
426
+
427
+ Keys come from server secret storage, separately from the database and source.
428
+ The protection wrapper uses the `jose` library's authenticated JWE encryption;
429
+ each encrypted record is bound to its owning connection. Keep previous named
430
+ keys while existing rows still use them. New writes use `activeKeyId`. Deleting
431
+ an old key before those rows are rewritten makes them unreadable. Applications
432
+ with an existing vault can instead provide compatible `seal(value, binding)`
433
+ and `open(ciphertext, binding)` methods.
434
+
435
+ Install `@jskit-ai/database-runtime` explicitly when using the Knex store. It is
436
+ an optional peer: editing portable configuration does not install or activate a
437
+ database provider. Applications using another store do not need this peer.
438
+
439
+ The package declares its authoritative migration directory in `package.json`.
440
+ Use the normal app-owned `knexfile.js` and migration scripts described by the
441
+ selected database package; do not copy these migrations or run them on startup
442
+ implicitly. Tables contain encrypted credentials/attempts and opaque identity
443
+ keys, with no dependency on a particular user table. Run
444
+ `store.pruneExpiredAttempts()` from the application's existing maintenance task.
445
+
446
+ A custom store implements `withConnection({ owner, integrationId }, work)`.
447
+ It must serialize the whole callback across processes for that identity and
448
+ commit on successful return. The callback receives `connection`, `save(value)`,
449
+ `remove()`, `putAttempt(attempt)`, `latestAttempt({ after })` and
450
+ `consumeAttempt(state)`. `latestAttempt` returns the current owner/slot's
451
+ latest-expiring attempt strictly after the supplied timestamp, or null, without
452
+ consuming it. Consumption must
453
+ affect only the current owner and slot. Removal also invalidates pending
454
+ attempts. Failures returned by the runtime are thrown after commit so failed
455
+ API requests do not roll back rotated refresh tokens or restore consumed codes.
456
+
457
+ ## Current limits and proof
458
+
459
+ SQL locking spans bounded provider requests to serialize refresh, completion
460
+ and disconnect. Different connections use different locks. Database connection
461
+ pool sizing and process-crash recovery during provider token rotation need
462
+ deployment testing. A crash between provider rotation and a database commit
463
+ can still require reconnecting; there is no distributed atomic transaction
464
+ with Google.
465
+
466
+ Focused tests cover consent denial, replay, ownership, refresh, cancellation,
467
+ safe errors, configuration and Feature composition. The SQL test covers real
468
+ MariaDB persistence, independent pools, rollback, encryption and migrations.
469
+ File tests cover restart, interrupted callbacks, rejected writes, record binding,
470
+ symlinks, consent expiry and serialization across independent Node processes.
471
+ PostgreSQL and live Google consent are not yet verified. Gmail supplies a
472
+ verified mailbox display label; general account-selection screens and
473
+ provider-wide revocation remain separate work. Managed registrations are rejected.
474
+
475
+ The three new packages also install from local npm tarballs into a clean
476
+ standalone application. Its CLI validation, package migration discovery,
477
+ migration status, disconnected status and repeat migration run were exercised
478
+ against a disposable MariaDB database. These checks do not publish packages.
479
+
480
+ For application wiring see the Google Calendar package's `calendar-cli` pattern.
481
+
482
+ For direct API-key or credential-free connections, operation scopes constrain
483
+ the application configuration; they are not provider consent grants. Both
484
+ verification and invocation enforce those configured limits. `grantedScopes`
485
+ remains empty. OAuth connections additionally enforce the actual stored grant.
486
+ Trusted exchange implementations receive validated `settings` and the resolved
487
+ `apiKey` only for API-key mode, allowing an SDK to sign requests without placing
488
+ raw credentials into transport headers first. These inputs are server-only.
489
+
490
+
491
+ ## Resuming application setup
492
+
493
+ `resumeAuthorization({ context, integrationId })` checks application connect
494
+ permission and returns the pending `{ authorizationUrl, expiresAt, callbackUrl }`, or null.
495
+ It uses the existing store and does not call the provider. Changing registration,
496
+ callback, scopes or settings invalidates the resumable result. Cancellation and
497
+ completion still consume the exact OAuth state once. A previous connected grant
498
+ remains independent of a new pending attempt.
499
+
500
+ For valid authorization-code bindings, `status()` also reports the resolved
501
+ `callbackUrl`. Applications can display this validated, public URL independently
502
+ of any editor suggestion; credentials and other Env values remain private.
503
+
504
+ Use [the application setup command guide](docs/setup-command.md) when connecting
505
+ an editor or CLI setup screen to this runtime. The app owns that executable and
506
+ its authenticated operator context; this library does not import an editor.
507
+
508
+
509
+ Status checks resolve required local bindings without a provider request. Before
510
+ a first connection, missing credentials or invalid callbacks return `unconfigured`
511
+ with `configurationError` set to `connector_binding_missing` or
512
+ `connector_callback_invalid`. For an existing matching connection they report
513
+ `reconnect-required` without deleting or changing the stored grant. Restoring the
514
+ binding can restore its previous status; status alone does not verify a new key.
515
+ Explicit disconnect remains available without working provider credentials.
516
+
517
+
518
+ Provider definitions may select `tokenRequestEncoding: "json"` when the token
519
+ endpoint requires JSON instead of OAuth's ordinary form encoding (Notion).
520
+ Only token grant requests use this encoding; API calls keep their existing
521
+ operation format. Authentication headers, redirect policy and abort signal
522
+ remain supplied by the OAuth library. `oauthBasicEncoding: "raw"` selects
523
+ base64 of the literal client ID/secret pair for providers documenting that
524
+ Basic convention; the default remains oauth4webapi's OAuth encoding. These
525
+ are provider implementation choices, not user-editable configuration fields.
526
+
527
+ Provider runtime implementations can declare `runtimeForSettings(settings)`
528
+ for materially different protocols under one catalogue entry (Notion REST/MCP).
529
+ The connection service resolves this trusted implementation after configuration
530
+ validation and before operations; settings never supply executable code.
531
+ `accountModesForSettings(settings)` constrains ownership choices in both
532
+ validation and forms. Client authentication metadata functions also receive
533
+ settings as their second argument. Existing providers keep their static behavior.
534
+
535
+ `dataFromTokenResponse(response, previousData)` lets a provider validate and
536
+ retain required account-specific routing from a processed token response
537
+ (Pipedrive's company API domain). Its return value is stored as private
538
+ `providerData` with the grant, including refreshes; it is omitted from public
539
+ connection status. Operation `request(input, settings, providerData)` and
540
+ `apiOrigins(settings, providerData)` receive this data. The provider must
541
+ validate destinations and account continuity before returning metadata. The
542
+ core still enforces HTTPS, allowed origins and rejection of URL credentials.
543
+ This metadata does not alter shared configuration or accept user-supplied
544
+ executable code; the file store seals it with the rest of the connection.
545
+
546
+ Scope metadata may include `authenticationMethods` when a permission belongs only
547
+ to particular credential modes. `getProviderScopes(provider, settings, grantType,
548
+ authenticationMethod)` filters these choices; configuration validation uses the
549
+ selected method. Forms remove incompatible scopes on method changes and select
550
+ recommended method-specific scopes when entering that mode. Granola uses this
551
+ to keep OAuth identity scopes out of API-key configuration.
@@ -0,0 +1,65 @@
1
+ # Application-owned OAuth callbacks
2
+
3
+ Each application supplies its own provider registration and callback route.
4
+ The editor's hosting domain does not supply a callback or a shared registration.
5
+ The application must keep working when exported or when the editor stops.
6
+
7
+ ## Configuration and ownership
8
+
9
+ Set registration `source` to `own`. Put the public client ID in `clientId`,
10
+ the client secret reference in `clientSecretRef`, and the full callback URL
11
+ reference in `callbackUrlRef`. For example, `env:GOOGLE_CALLBACK_URL` resolves
12
+ to an application-controlled URL such as
13
+ `https://example.com/integrations/google/callback`. That example is not a
14
+ provisioned endpoint: application code must implement it before connection.
15
+
16
+ Register the resolved callback with the provider using its required redirect
17
+ rules. Development and production use their own environment bindings and runtime
18
+ state. Never use an editor dashboard URL as an application callback. Providers
19
+ may require separate registrations or allow several explicit redirect URLs;
20
+ follow the provider guide rather than assuming wildcard support.
21
+
22
+ ## Application flow
23
+
24
+ 1. Authenticate the initiating user or authorized setup operator in the app.
25
+ 2. Call `beginAuthorization` with the app-derived owner and integration ID.
26
+ 3. Open the returned authorization URL. Keep the pending attempt in the app's
27
+ durable connection store; closing an editor panel must not delete it.
28
+ 4. The app callback passes the complete callback URL to `completeAuthorization`
29
+ under the same authenticated owner. The runtime validates the pending attempt,
30
+ exchanges the code, verifies access and stores the grant.
31
+ 5. Render safe connection status. Tokens and provider callback parameters do
32
+ not belong in editor messages or source configuration.
33
+
34
+ Application code owns HTTP routes and identity recovery on the callback. A
35
+ browser message is not proof of a successful connection. A CLI can use these
36
+ same library operations with the application's callback and storage.
37
+ `cancelAuthorization` cancels a pending attempt while preserving an existing
38
+ connection. `disconnect` removes local access and invalidates pending consent;
39
+ it does not promise provider-wide revocation.
40
+
41
+ The application authorization callback must distinguish shared-account setup
42
+ from individual app-user connection. Connecting an administrator's mailbox does
43
+ not grant every user access to their own mailbox, nor grant other projects access.
44
+ Application login remains separate from permission to read provider data.
45
+
46
+ ## Changing domains or moving the application
47
+
48
+ Before changing domains, configure the new callback at the provider and in the
49
+ application environment. A changed callback invalidates pending authorization;
50
+ start a fresh attempt instead of reusing one begun with the old callback.
51
+ Existing grants can remain usable when the provider does not bind refresh to
52
+ the old redirect URI. Providers whose refresh operation requires that URI report
53
+ reconnection when it changes. Changing the registration's client identity also
54
+ requires reconnection. Users may need to sign into the app again on the new
55
+ domain, independently of whether their provider grant remains usable.
56
+
57
+ Moving source alone does not move grants. Supply the app's environment, durable
58
+ connection storage and encryption keys at the new installation. Preserve its
59
+ application and subject identities. No editor service is needed to refresh tokens.
60
+
61
+ ## Verification limits
62
+
63
+ Protocol fixtures cover callback binding, ownership, denial, replay, cancellation
64
+ and registration changes. They do not prove provider approval, a deployed route,
65
+ a real browser consent flow or a complete generated application.