Skip to content
Pilotoo
v0.1

Server integration

Connections are part of PilotooModule. You pass providers, set a few environment variables, and the kit adds the routes, storage, encryption, refresh and audit.

import { githubProvider, PilotooModule } from '@pilotoo/server';
PilotooModule.forRoot({
connections: {
providers: [githubProvider(), myProvider],
tools: true,
},
});
Option Default Meaning
providers required The ProviderDefinitions users may connect. See Providers.
tools false Registers connections_search and connections_execute, opt-in. They answer from a remote source when CONNECTIONS_REMOTE_* is set.
publicApi false Serves POST /api/tools/search, POST /api/tools/list, POST /api/tools/execute and POST /mcp. See Tools API and MCP.
storage PostgreSQL A ConnectionStorage of your own. Required on a host without a database.
http global fetch Every outbound call: token, revocation, account and tool calls.

Without the connections option the catalog is empty and every route answers as for an unknown provider.

Variable Default Meaning
CONNECTIONS_ENCRYPTION_KEYS none Comma-separated version:base64 32-byte keys, current first. Without it nothing can be connected.
CONNECTIONS_CALLBACK_URL none Public URL of GET /connections/oauth/callback, registered in each OAuth app. Without it OAuth providers cannot connect.
CONNECTIONS_RETURN_ORIGINS none Origins a returnUrl may point at, and that the callback page posts its result to.
CONNECTIONS_ROLES none Roles that may connect accounts and use the tools. Unset means nobody.
CONNECTIONS_ADMIN_ROLES none Roles that administer providers, connections, audit, metrics, health and rekey. Unset means nobody.
CONNECTIONS_STATE_TTL_SECONDS 600 How long an authorization may take (at least 60).
CONNECTIONS_REFRESH_SKEW_SECONDS 60 Tokens this close to expiry are refreshed before use.
CONNECTIONS_HTTP_TIMEOUT_MS 10000 Token, revocation and account calls. At most 60000.
CONNECTIONS_TOOL_TIMEOUT_MS 30000 One tool call.
CONNECTIONS_TOOL_RESULT_MAX_BYTES 32768 Longer results reach the model truncated.
CONNECTIONS_MAX_PENDING_STATES 20 Pending authorizations per user.
CONNECTIONS_AUDIT_RETENTION_DAYS 30 Audit rows older than this are pruned.
CONNECTIONS_MAINTENANCE_MINUTES 60 How often expired states and old audit rows are pruned. 0 switches it off.
CONNECTIONS_REMOTE_URL, CONNECTIONS_REMOTE_TOKEN none Take the tools from a connections service. See Remote sources.
CONNECTIONS_REMOTE_SOURCES none JSON array of several services. Wins over the two above.

A malformed value stops the boot instead of falling back to a default. Callback and return URLs must be https, except on localhost and 127.0.0.1.

Generate a key:

Terminal window
node -e "console.log('v1:' + require('crypto').randomBytes(32).toString('base64'))"
Port Provided by default Replace to
ConnectionStorage PostgresConnectionStorage, which creates its tables on first use Keep connections elsewhere.
ConnectionHttp global fetch Add a proxy, tracing or a test double.
CredentialCipher AesGcmCredentialCipher from CONNECTIONS_ENCRYPTION_KEYS Not replaceable through module options.
Identity with tenant, subject, roles Your identity provider See Authentication.

ConnectionStorage covers connections, pending OAuth states, tenant provider configs and the audit log. Its contract requires every read and write to be scoped to a tenant, consumeState to be atomic, and withConnectionLock to exclude other holders in any process sharing the storage. The default stores these in PostgreSQL, in tables created on first use (see Storage).

User routes need a role in CONNECTIONS_ROLES.

Route Purpose
GET /connections Providers the tenant enabled, the caller’s connections to each, the organisation’s connections they may use, and canConnect.
POST /connections/:provider/authorize Body { returnUrl? }. Returns { authorizationUrl, expiresAt }.
GET /connections/oauth/callback Anonymous: the one-time state binds the tenant and user. Redirects to returnUrl or renders a result page.
PUT /connections/:provider/api-key Body { apiKey }. 204.
PUT /connections/:provider/fields Body { values }, every value of a fields credential, checked against the provider’s fields. 204.
POST /connections/:id/default Makes one of the caller’s connections the default of its provider. 204.
DELETE /connections/:id Disconnects one of the caller’s own connections; the provider’s other accounts stay. An organisation connection or someone else’s answers not_found. 204.

Admin routes need a role in CONNECTIONS_ADMIN_ROLES and act on the caller’s own tenant.

Route Purpose
GET /admin/connections/providers Every host-defined provider with the tenant’s configuration.
PUT /admin/connections/providers/:provider Enable, set client, scopes, tool allow-list (allowedTools), and tool-name patterns (includeTools, excludeTools; * matches any run of characters, [a-z0-9_*-], at most 64 characters, 500 per list). A tool is offered only when allowed, matching an include pattern (an empty or null list matches all) and matching no exclude pattern. An absent field keeps the stored value.
DELETE /admin/connections/providers/:provider Remove the configuration. 204.
GET /admin/connections and GET /admin/connections/:id List (filters scope, owner, provider, status, limit, cursor) or read connections.
POST /admin/connections/shared/:provider/authorize and PUT /admin/connections/shared/:provider/api-key or .../fields Connect an organisation account, with the same flow as a user’s.
PUT /admin/connections/:id/sharing Body { roles, tools }; null means any role, or whatever the provider configuration allows. Organisation connections only.
POST /admin/connections/:id/default Makes an organisation connection the tenant’s default for its provider.
POST /admin/connections/:id/revoke Revoke and delete. 204.
POST /admin/connections/:id/refresh Refresh now; returns the connection.
GET /admin/connections/audit Filter by owner, provider, kind, connectionId, outcome, since.
GET /admin/connections/metrics?windowHours= Counts, refresh and tool-call error rates and latency percentiles. Default 24, at most 720.
GET /admin/connections/health Vault state, key usage, pending authorizations and problems.
POST /admin/connections/rekey Body { limit? } (default 200). Re-seals records under the current key.
GET, POST /admin/connections/gateways and PUT, DELETE /admin/connections/gateways/:provider, POST .../refresh, POST .../worker-token A tenant’s custom gateways. See Custom gateway.

Errors carry a machine-readable code beside message:

code HTTP
unavailable 501
forbidden, provider_disabled 403
provider_unknown, not_found, not_connected 404
needs_reauth, conflict 409
invalid_request, invalid_arguments, state_invalid 400
provider_rejected, tool_failed 502
provider_unavailable, vault_error 503
timeout 504
  1. authorize checks the role, the provider, CONNECTIONS_CALLBACK_URL and the returnUrl origin, caps pending states per user, and stores a state record keyed by the SHA-256 of the random state. The PKCE verifier is stored sealed.
  2. The browser goes to the provider, then back to the callback with state and code.
  3. The callback consumes the state once (a replay finds nothing), exchanges the code, reads the account through account, seals the credential, and upserts the connection. Reconnecting replaces the row of the same user and provider.
  4. The browser is redirected to returnUrl with connection, provider and error query parameters, or a small result page is shown. That page posts { type: 'pilotoo:connection', ... } to window.opener only for origins listed in CONNECTIONS_RETURN_ORIGINS.

Failure values in error include access_denied, state_invalid, unavailable, provider_disabled, provider_unavailable, provider_rejected and server_error.

Guarantee How
Secrets are never returned Routes return hasClientSecret and connection metadata, never a token, key or client secret. Tool errors are scrubbed of the caller’s tokens.
Encrypted at rest Each credential and client secret is sealed with AES-256-GCM under a per-record key wrapped by the current master key, and bound to its tenant and record, so a copied ciphertext does not open elsewhere.
Key rotation Put the new key first in CONNECTIONS_ENCRYPTION_KEYS, call POST /admin/connections/rekey until remaining is 0, then drop the old key. health reports problems when a record uses a key the server no longer holds.
Single refresh A token near expiry is refreshed under a per-connection lock (a 120 second lease in storage), so concurrent calls on any instance refresh once.
Failed refresh A provider that refuses the refresh token marks the connection needs_reauth; a transient failure keeps the old token until it expires and records refresh_failed.
Revocation Disconnect, by the user or an admin, calls the provider’s revokeUrl best-effort and deletes the row.
Deadlines Outbound calls time out; a tool that ignores its signal is still cut off at the deadline.
Audit Appended for connected, reconnected, refreshed, refresh_failed, needs_reauth, disconnected, revoked_by_admin, tool_call, rekeyed, config_changed, connect_failed, default_changed and sharing_changed. Details are redacted and bounded.
Callback page Served with a nonce-only content security policy, no-store, and nothing from the request reflected.
  • An admin revoking a connection records revoked_by_admin; the owner disconnecting records disconnected.
  • A user cannot disconnect another user’s connection; the answer is not_found.
  • Connecting an account that is already connected replaces its credential and keeps its id; another account of the same provider adds a connection. Two instances racing to connect the same account end with one row.
  • A busy connection lock answers unavailable (“try again”) rather than waiting forever.