Files
open-connector/docs/programmatic-connections.md
l1shen b969ec0b4e feat: add cloud project OAuth and remote execution (#627)
## 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`.
2026-09-30 13:25:49 +08:00

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: optional http:, https:, or oomol: URL. The callback adds status and service, and safe code and message fields 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 return invalid_input. requires describes selection dependencies for clients; the server preserves the explicitly selected options and required options.
  • extra and secretExtra (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.