Server integration
Introduction
Section titled “Introduction”Connections are part of PilotooModule. You pass providers, set a few environment variables, and the kit adds the routes, storage, encryption, refresh and audit.
Enabling connections
Section titled “Enabling connections”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.
Configuring the environment
Section titled “Configuring the environment”| 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:
node -e "console.log('v1:' + require('crypto').randomBytes(32).toString('base64'))"Replacing storage, HTTP and the cipher
Section titled “Replacing storage, HTTP and the cipher”| 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).
HTTP routes
Section titled “HTTP routes”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 |
The connect flow
Section titled “The connect flow”authorizechecks the role, the provider,CONNECTIONS_CALLBACK_URLand thereturnUrlorigin, caps pending states per user, and stores a state record keyed by the SHA-256 of the randomstate. The PKCE verifier is stored sealed.- The browser goes to the provider, then back to the callback with
stateandcode. - 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. - The browser is redirected to
returnUrlwithconnection,provideranderrorquery parameters, or a small result page is shown. That page posts{ type: 'pilotoo:connection', ... }towindow.openeronly for origins listed inCONNECTIONS_RETURN_ORIGINS.
Failure values in error include access_denied, state_invalid, unavailable, provider_disabled, provider_unavailable, provider_rejected and server_error.
Security
Section titled “Security”| 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. |
Edge cases
Section titled “Edge cases”- An admin revoking a connection records
revoked_by_admin; the owner disconnecting recordsdisconnected. - 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.
Next steps
Section titled “Next steps”- Using the library: mount these routes in your own NestJS app.
- Providers: what the host defines and the tenant enables.
- Settings UI: the screens that call these routes.