19 Commits
Author SHA1 Message Date
860057e8a3 feat(oauth): support optional token revocation on disconnect (#634)
## Summary

`DELETE /api/connections/:service` deletes the stored credential and
nothing else: the app stays in the account's authorized list at the
provider, the refresh token stays valid there, and the next sign-in at
Google can be a silent re-issue with no consent shown. This PR adds the
one concept the runtime lacked — a provider-declared revocation endpoint
— and lets a disconnect use it on request: with `revoke: true` in the
JSON body, an OAuth connection of a provider that declares a
`revocationUrl` has its token posted there (RFC 7009) once the delete
has gone through, and the answer says what became of the grant. Without
`revoke: true` the delete is exactly what it was, so no existing
deployment changes behaviour.

## Changes

- `src/core/types.ts`: `OAuth2AuthDefinition.revocationUrl?: string`,
beside `refreshTokenUrl`.
- `src/oauth/oauth-token.ts`: `requestTokenRevocation` — an RFC 7009
§2.1 form POST of `token` and `token_type_hint`, authenticated the way
the token endpoint is (`client_secret_basic` as an `Authorization:
Basic` header, `client_secret_post` in the body, `none` with `client_id`
alone), its own 5 s deadline (a disconnect is something the person waits
for), `oauth_token_revocation_failed` on a non-2xx with the provider's
error code in the message.
- `src/oauth/oauth-credential-refresh-service.ts`:
`IOAuthCredentialRefresher.revoke?(service, credential)` and its
implementation: the refresh token when the credential holds one, else
the access token; the client config resolved as a refresh resolves it;
`done` on success, `unsupported` when the provider declares no endpoint.
- `src/connection-service.ts`: `disconnect(service, connectionName, {
revoke })`. The credential is read before the delete and revoked after
it, so a delete the store refuses (a row bound to active Trigger
subscriptions) ends nothing at the provider. The answer gains `revoked`:
`done`, `failed` (the provider refused or could not be reached; the
credential is deleted all the same, and the host log carries the error
code), `unsupported` (no `revocationUrl`, no OAuth token held, or a SaaS
connection) or `skipped` (the body did not ask — the default).
- `src/server/connect-server.ts`: the DELETE handler reads `revoke` as
an optional boolean.
- The six Google providers (`gmail`, `googlecalendar`, `googledocs`,
`googledrive`, `googlesheets`, `googleslides`) declare
`https://oauth2.googleapis.com/revoke` — inert data until a caller asks.
- `src/server/api/openapi.ts`, `docs/runtime-api.md`: the body field,
the `revoked` enum and the prose.

## Testing

- `npm run fix-check`: exit 0.
- `npm test` on Node 22.23.1: 277 files passed, 1 skipped; 3464 tests
passed, 20 skipped (the Postgres integration cases that need
`TEST_POSTGRES_URL`).
- New tests: `src/oauth/oauth-token.test.ts` (`describe("OAuth token
revocation")`: the RFC 7009 form POST authenticated like the token
endpoint; basic client authentication as a header and never in the body;
only the client id for a public client; a refusal reported by status and
error code, a non-JSON refusal by status alone; no HTTP response; the
deadline), `src/oauth/oauth-credential-refresh-service.test.ts`
(`describe("OAuthCredentialRefreshService revoke")`: `unsupported`
without a request when the definition names no endpoint; a templated
endpoint resolved through the client configuration the credential was
minted under; one it cannot fill in refused),
`src/connection-service.test.ts` (`describe("ConnectionService
disconnect revocation")`: the refresh token posted once then the row
deleted; a refused token and an unreachable endpoint still delete and
answer `failed`; the access token when no refresh token is held; no
secret for a public client; the default keeps the grant and makes no
request; `unsupported` for a provider without an endpoint, a non-OAuth
connection and a connection that is not held; a delete the store refuses
revokes nothing), `src/server/connect-server.test.ts` (the DELETE route
answers `skipped` without `revoke: true`, and `unsupported` with it for
a provider that declares no endpoint),
`src/providers/google-revocation.test.ts` (the six declarations share
the token endpoint's origin).

## Notes

- Compatibility: additive and off by default. A DELETE without `revoke:
true` deletes the row and leaves the grant standing, as before; the only
wire additions are the optional body field and `revoked` on the answer.
- Google revokes the whole authorization of a user and client on any one
of its tokens
(https://developers.google.com/identity/protocols/oauth2/native-app,
"Token revocation"), so a host that keeps several connections of one
account under one grant should revoke once and forget the rest — hence
per request rather than per deployment.
- Microsoft is not touched: the identity platform has no endpoint an app
can call to revoke a delegated grant or a refresh token, and whether a
sign-in after a disconnect should re-ask every permission
(`prompt=consent`) is a host's choice to make on the authorize URL, not
a default for every deployment of these providers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: RoryCai92 <[email protected]>
Co-authored-by: hyrious <[email protected]>
2026-10-02 12:58:16 +08:00
l1shen cdd4b5970f feat(triggers): add native provider trigger runtime (#630)
Self-hosted Open Flow needs OpenConnector to execute provider Triggers
and own their remote subscriptions. This adds a native Trigger runtime
for 21 Triggers across 18 providers, including polling, webhook
delivery, configuration options, and Feishu resource subscriptions.

## Changes

- Register provider Trigger definitions and lazy executors, expose
permission guidance and the five supported operations (`options`,
`read`, `reconcile`, `receive`, `resource`), and export the catalog
consumed by Open Flow.
- Add independent deployment, runtime, and token Trigger grants.
Existing persistent tokens default to an empty Trigger grant; stateful
operations require a persistent token. Trigger-only grants authorize
registered provider calls through the existing authenticated,
SSRF-guarded transport.
- Resolve `x-oo-connector-app-id` as a stable connection ID for
Triggers, Actions, and public proxy. Conflicting selectors fail, and
unknown IDs cannot fall back to another account. Runtime connection
metadata exposes verified provider account identity.
- Persist subscription ownership, callback nonces, provider state, and
cleanup status using the existing secret codec and SQL leases across
SQLite, PostgreSQL, and D1. Enforce immutable active bindings, validate
callbacks, and share Feishu resource references by provider account
across connection aliases and tokens.
- Protect connections while remote resources still need cleanup, allow
verified same-account reauthorization to recover cleanup credentials and
rebuild released resource bindings with the current connection revision,
and provide administrator subscription inspection, cancellation, and
explicit abandonment.
- Run bounded cleanup after grant or token revocation in Node and
Workers. Open Flow owns public ingress, scheduling, business
checkpoints, deduplication, and active subscription reconciliation.
- Bound Poll event pages to 100 events, preserve provider continuation
checkpoints, and align native Google Drive requests with the registered
proxy. Gmail resumes within a history page before advancing its history
cursor; Google Sheets reads from the next unread row with an inclusive
window that respects the event budget.
- Document the API, permissions, lifecycle, and headless packaging
contracts. Preserve Trigger grants through Web Console token edits.
- Fix the PGlite socket fixture's premature `ReadyForQuery` responses
after extended-query errors, with regression coverage for connection
recovery after both Parse and Execute failures.

## Deployment notes

Apply migrations `0016_trigger_policy` and `0017_trigger_subscriptions`
before starting a PostgreSQL runtime. Grant `allowedTriggers` explicitly
to persistent tokens that manage subscriptions. Native local connections
are supported; SaaS and Marketplace Trigger requests return
`trigger_source_not_supported`.

## Validation

- `npm run fix-check`: passed lint, formatting, and type checks.
- `npm test`: 3435 passed, 20 skipped.
- PostgreSQL suite: 10 consecutive runs passed, 38 tests per run. Both
new protocol regression cases fail against the uncorrected fixture.
- Resource HTTP regressions cover cross-alias/token release and
abandonment, account isolation, and same-account credential rotation.
- Native provider regressions use the registered proxies and drain
250-event backlogs under default, small, and large page settings. All 24
provider contract tests also pass against the compiled modules installed
from the actual tarball. New cases cover empty and populated Sheets
seeds, Gmail history records containing more messages than the event
budget, filtering, deleted messages, transient failures, and JSON
checkpoint persistence. All 11 new cases fail against the preceding
implementation.
- Generated catalog: all 21 Trigger contracts match the Open Flow
consumer catalog.
- Runtime package build and isolated tarball consumption: passed,
including native GitHub subscription creation and cleanup against a fake
upstream.
- Compiled host smoke checks: passed with all providers, GitHub only,
and an empty provider selection.
- Web build and Workers deployment dry run: passed.
- Cross-repository HTTP integration: GitHub and Feishu publish,
callback-to-run delivery, provider account identity, and cleanup after
token revocation passed against fake upstreams.

Real third-party account acceptance remains unverified. Workers
validation covers build and D1 behavior; it does not include a
Cloudflare deployment.
2026-09-30 18:14:00 +08:00
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
Kevin Cui 1455f05083 fix(action-guides): use the public origin and MCP tool examples (#503)
Signed-off-by: Kevin Cui <[email protected]>
2026-09-06 17:01:49 +08:00
Kevin Cui eecd1705ee fix(proxy): give defineProviderProxy a default request deadline (#463)
Stacked on #462 (`fix/e2-proxy-signal-credential-memo`), part of the
E-tier audit stack. Retargets to `main` once the branch below lands.
#462 is a prerequisite: the deadline composes with the signal it
threads.

A `defineProviderProxy` proxy had no request deadline at all, so a slow
or stuck upstream held the request open until the platform's own limit
fired (undici's 300s on Node, the Workers wall-clock on Cloudflare), ten
times the 30s budget the same provider's actions get. This gives the
shared proxy definition the same default 30s deadline, covering 653 of
the 773 proxies.

Requests to `/v1/proxy/<service>` now give up after 30s and return a
`provider_error` that says the request timed out. The budget covers the
response body too, so a proxied download that takes longer than 30s now
fails and should be fetched from the provider's direct URL instead. The
sharpest thing to watch is a provider that declares a longer budget for
the same upstream on its action path, `googledrive` at 300s most of all.
A `timeoutMs` opt-out for the proxies that legitimately need longer is
the natural follow-up.

---------

Signed-off-by: Kevin Cui <[email protected]>
2026-08-31 12:36:15 -04:00
Frankie-Xu 6871f2049e feat(runtime): align unknown_action errors with OpenAPI and runtime docs (#373) 2026-08-20 11:56:00 +08:00
b028ecd38e feat: allow scoping runtime tokens to specific connections (#363)
Closes #357

## Summary
- Add optional `allowedConnections` on stored runtime tokens (`oct_…`)
so a token can be limited to exact bare `connectionName` values. Omitted
or `[]` keeps today's unrestricted behavior (same empty-allowlist
meaning as `allowedActions`, not `allowedProxies`).
- Enforce the grant **before** credential lookup on HTTP Actions, MCP
`execute_action`, and provider proxy. Denied names return `403
connection_not_allowed` so existence is not leaked; an
allowed-but-missing name can still return `connection_not_found`.
Unnamed requests resolve to `default` first, so a restricted token must
list `default` to use that path.
- Filter runtime discovery that currently returns connection identity
(`GET /v1/apps*`, MCP `list_connections` / `list_apps` /
`get_action_guide`). Admin `/api/connections` and `agent.md` stay
unfiltered. Bootstrap `OOMOL_CONNECT_RUNTIME_TOKEN`, JWTs, and admin
elevation remain unrestricted (they never receive a `RuntimeGrant`).
- Persist the grant in SQLite/D1
(`0011_runtime_token_connection_scope.sql`), expose it in OpenAPI and
the Access console, and document the contract.

## Test plan
- [x] `npm run fix-check`
- [x] `npm test` (1062 passing)
- [ ] Create two connections on one provider (`default` and `work`)
- [ ] `POST /api/runtime-tokens` with `allowedConnections: ["work"]`
- [ ] Unnamed `/v1/actions/:id` and `/v1/proxy/:service` return `403
connection_not_allowed`
- [ ] Same calls with `x-oo-connector-alias: work` succeed
- [ ] `GET /v1/apps` and MCP `list_connections` show only `work`
- [ ] `GET /api/connections` still lists both names
- [ ] Token with `allowedConnections: ["work", "ghost"]` requesting
`ghost` returns `404 connection_not_found`
- [ ] Admin, bootstrap runtime token, JWT, and `allowedConnections: []`
still use `default`


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: 昆昆 <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: l1shen <[email protected]>
2026-08-19 18:38:53 +08:00
l1shen c40c2bf4a4 feat(storage): support S3 transit files (#345) 2026-08-17 15:05:07 +08:00
l1shen 2251e1220a feat(runtime): add proxy permissions to tokens (#192) 2026-07-25 16:14:47 +08:00
LJAYiandl1shen 5e4bdad27f feat(mcp): support named provider connections (#142)
## Summary

- add `list_connections` for safe discovery of configured provider
accounts
- allow `get_action_guide` and `execute_action` to select an optional
named connection
- include safe connection identity in execution metadata and record the
effective connection in run logs

## Behavior

Omitting `connectionName` keeps the existing `default` behavior. An
explicitly selected connection must exist; the runtime returns
`connection_not_found` instead of falling back to another account.
Stored credentials and runtime metadata are never exposed.

## Validation

- `npm run fix-check`
- `npm test` (62 test files, 527 tests)

---------

Co-authored-by: l1shen <[email protected]>
2026-07-20 11:49:06 +08:00
l1shen 5677453dac feat(runtime): improve action run auditing (#141) 2026-07-17 17:56:48 +08:00
l1shen 1fc404feeb feat(auth): add JWT runtime authentication (#130)
#107
2026-07-16 16:07:07 +08:00
Kevin Cui 3d0db9c178 fix(proxy): stop action policy from denying provider proxies (#128)
restrictsActions was an OR over array length, so configuring any action
policy denied every provider proxy unless OOMOL_CONNECT_ALLOWED_PROXIES
was also set. Blacklisting a single Action was enough:

BLOCKED_ACTIONS=github.delete_repository -> every /v1/proxy/:service
403s

The 403 then blamed "local action policy", a policy the operator did not
believe they had configured for proxies at all. Nothing about locking
down one Action implies anything about provider proxies, and the
length-based OR made even a one-entry denylist trip it.

Action policy and proxy policy are now independent. Proxy is decided by
OOMOL_CONNECT_ALLOWED_PROXIES / OOMOL_CONNECT_BLOCKED_PROXIES and
nothing else, which is what those variables already claimed to do.
restrictsActions is deleted outright rather than switched off, so the
coupling has nothing left to read. The environment variable surface is
unchanged.

Behavior change for existing deployments: a deployment that sets any
action policy and no proxy policy had every provider proxy denied and
now has them allowed. /v1/proxy/:service reaches provider endpoints
beyond the curated catalog, so operators who were relying on this side
effect must now set the proxy variables explicitly. SECURITY.md
recommended the action variables to reduce attack surface without
mentioning that proxies need their own, and now says so.
2026-07-16 11:17:54 +08:00
l1shen c50596dfed feat(runtime): add idempotent action execution (#120)
#119
2026-07-15 17:31:01 +08:00
hyrious 9e4afe0285 feat(proxy): add policy env (#49) 2026-07-07 17:28:15 +08:00
l1shen a438ad5513 feat: impl proxy (#45) 2026-07-07 16:25:22 +08:00
l1shen 951ab3c5a2 feat(tests): add MCP server tests for action execution (#33) 2026-07-06 13:39:37 +08:00
shaun 515057d539 Address README review comments 2026-07-03 14:01:15 +08:00
shaun 6e8caa9dff Refresh OpenConnector README 2026-07-03 13:47:13 +08:00