## Summary Add OOMOL cloud OAuth as a provider authorization source. Users can save an encrypted project API key, select a cloud provider configuration, authorize named connections, and run their actions and proxy requests through SaaS. Existing connections retain their source when the provider default changes, and third-party credentials remain in SaaS. - Implement project discovery, OAuth request synchronization, remote execution, and persistent cleanup of disconnected cloud accounts. Add SQLite, PostgreSQL, and D1 migrations and storage support, with instance reset and maintenance tooling. - Add Console project and authorization settings, localized errors, disconnect notifications, and styled OAuth completion pages that close after successful authorization. Align local development callback origins and proxy routes. - Improve safe request diagnostics and retry transient project health-check failures once. Execution requests are not automatically replayed. - Group storage implementations by database backend, share guarded HTTP requests with Marketplace, update Zod to 4.6.5, and load provider icons from the public catalog. - Document setup, maintenance, and the manual acceptance checklist. ## Validation - `npm run fix-check` passed. - `npm run build:web` passed. - Latest focused regression run: 346 tests passed across 33 files covering Console, SaaS project/client/routes, OAuth completion, and guarded HTTP requests. - Earlier implementation validation included the broader runtime/storage suites and a real PostgreSQL integration run. Full deployed action/proxy, cleanup recovery, and D1 maintenance acceptance remain documented manual checks; they are not claimed as completed by the unit tests. ## Deployment notes Apply migrations `0014_saas_project` and `0015_saas_cleanup_runtime`, configure encryption and an explicit `OOMOL_CONNECT_ORIGIN`, and enable the relevant cleanup scheduling. The remote SaaS deployment must support the project OAuth and remote execution APIs described in `docs/saas-oauth.md`.
8.8 KiB
Programmatic connections
The /v1/connections and /v1/connection-requests endpoints provide personal connection
management through the same HTTP paths and envelopes as OOMOL Hosted Connector. They support
OAuth authorization, polling one authorization attempt, connection details, and synchronous
API-key or custom-credential creation and replacement.
SaaS OAuth sources
For a service configured with a SaaS source, the connection endpoints below authorize remotely without a local OAuth client. Reconnecting an existing connection retains its source. Read the four setup fields inside oauthClient: SaaS sets customClientAvailable to false, uses its callback URL and requires no local client fields. Send no OAuth overrides in SaaS mode.
An explicit valid administrator Bearer GET synchronizes a known SaaS request; cookie-only GET does not. Poll at least two seconds apart and honor Retry-After. Browser clients use the protected same-origin POST described in SaaS OAuth. Never replay link creation after an unknown result. Completed connections use the same execution selectors and policies, with provider credentials and execution remaining on SaaS.
Authentication
Use the local administrator bearer token (OOMOL_CONNECT_ADMIN_TOKEN) to manage connections.
Execution-only runtime tokens and runtime JWTs do not grant management access. A fresh instance
with neither administrator nor runtime authentication configured accepts local management
requests without a token. Once runtime authentication is configured, configure an administrator
token before using these endpoints.
The open runtime has one administrator identity; its bearer token and authenticated console session act as that same identity. It does not require Hosted Team headers. Hosted deployments apply their own user and Team management permissions.
GET /v1/apps remains execution discovery and applies execution access policies.
GET /v1/connections returns the administrator's stored connections, including connections
hidden from execution discovery. It excludes virtual no-auth and Marketplace entries.
Discover setup requirements
GET /v1/providers/:service/setup describes what a provider needs before it can be connected:
the credential fields of each supported credential type, the OAuth client inputs, the scopes the
connector requests, the provider's registration steps, the callback URL to register, and which
OAuth client inputs are still missing. It never returns saved values, so a host can build its own
connection form from it and submit through the endpoints below.
Start and track OAuth
For local OAuth, configure your provider's OAuth client through the console or
/api/oauth/configs/:service first. Register /oauth/callback on this runtime as the callback URL.
For SaaS OAuth, configure the cloud project and select the provider configuration instead.
The provider's OAuth callback is hosted by SaaS; after authorization, SaaS returns to a
Connect-generated /oauth/saas/complete URL. The final returnUri stays in Connect and is
used after synchronization, rather than being passed to SaaS.
curl -sS -X POST http://localhost:3000/v1/connections/github/connect \
-H "Authorization: Bearer $OOMOL_CONNECT_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{}'
The success envelope contains:
{
"success": true,
"message": "OK",
"data": {
"authorizationUrl": "https://github.com/login/oauth/authorize?...",
"stateHandle": "oauth-state-id",
"connectionRequestId": "request-id",
"status": "initiated",
"expiresAt": "2026-09-07T10:10:00.000Z"
},
"meta": {}
}
Open authorizationUrl in a browser and save connectionRequestId. The state handle belongs
to the OAuth callback and is not the result-query identifier.
curl -sS http://localhost:3000/v1/connection-requests/request-id \
-H "Authorization: Bearer $OOMOL_CONNECT_ADMIN_TOKEN"
The result's data has connectionRequestId, service, status, appId, errorCode,
errorMessage, expiresAt, createdAt, and updatedAt. Creation and update times are Unix
milliseconds; expiresAt is an ISO timestamp. appId and the error fields are nullable.
| Status | Meaning |
|---|---|
initiated |
Authorization has not reached a terminal result. Poll again, for example after two seconds. |
connected |
This attempt saved its connection. Use its exact appId and stop polling. |
failed |
Authorization failed or was denied. Inspect the safe error fields and stop polling. |
expired |
The ten-minute authorization window elapsed without a terminal result. |
A callback must begin before expiresAt. A callback already processing may finish after that
time and change an expired view to a connected or failed result. Success and failure remain
queryable until expiresAt plus 24 hours. Missing or expired-retention results return HTTP 404
with connection_request_not_found. Results use Cache-Control: private, no-store and never
include credentials or raw provider errors.
Starting another request for the same provider supersedes earlier requests that have not
started processing, returning failed with request_superseded. A callback already processing
continues. Repeated callbacks do not overwrite terminal results.
OAuth inputs can include:
returnUri: optionalhttp:,https:, oroomol:URL. The callback addsstatusandservice, and safecodeandmessagefields on failure.authorizationOptionIds(local OAuth only): provider-declared option IDs. GitHub and Slack expose selectable provider-native scopes in their OAuth definitions. Required options are always included. Omission preserves the configured scopes; unknown options or options on unsupported providers returninvalid_input.requiresdescribes selection dependencies for clients; the server preserves the explicitly selected options and required options.extraandsecretExtra(local OAuth only): provider-declared OAuth configuration fields, merged for this authorization attempt without changing the saved client configuration.
Inspect and reconnect
GET /v1/connections
GET /v1/connections?status=active
GET /v1/connections/by-id/:appId
POST /v1/connections/by-id/:appId/connect
A successful OAuth request records the outcome of that attempt. Query connection details for
its current state. An expired OAuth credential with no refresh token reports reauth_required.
Reconnection takes the same OAuth body and returns a new request ID. Success keeps the original
appId. If the original connection is deleted or its credentials change during authorization,
the stale callback fails instead of recreating it or overwriting the replacement.
New connections receive distinct local aliases. Use the returned alias as the connection
selector when executing actions. Existing local default-connection selection remains available.
API keys and custom credentials
These operations validate and save credentials synchronously. They return the connection in the success envelope and do not require polling.
POST /v1/connections/:service/connect/api-key
POST /v1/connections/by-id/:appId/connect/api-key
Body: { "apiKey": "...", "extra": { "field": "value" }, "comment": "Optional note" }.
extra and comment are optional.
POST /v1/connections/:service/connect/custom-credential
POST /v1/connections/by-id/:appId/connect/custom-credential
Body: { "values": { "field": "value" }, "comment": "Optional note" }.
comment is optional; use null to clear an existing note. Replacement retains the connection
ID, requires the existing credential type, and does not discard existing credentials when
validation fails or a concurrent update wins.
The OpenAPI document at /openapi.json describes the request and response envelopes. The local
console's /api/connections and /api/oauth/authorizations endpoints continue to work.
Persistence
Migration 0013_connection_requests.sql stores authorization attempts on SQLite, PostgreSQL,
and Cloudflare D1. SQLite applies it through the existing startup migrations. PostgreSQL and D1
use their existing deployment migration commands; migrate before starting the new runtime.
The pending OAuth snapshot uses the configured secret codec and participates in secret rotation.
A connection write and its successful request result commit together. D1 uses transactional
batch() for these writes;
SQLite and PostgreSQL use their native transactions. Expired retained records are cleaned up
when new requests are created.