Return HTTP admission denials through the existing OAuth connection-request failure callback and preserve console versus runtime error response formats, including retry hints.
Add seven regression cases for token-exchange and credential-validation denials, callbacks with and without return URIs, request state, blocked egress, unsaved credentials, callback replay, and route-specific envelopes.
Validation: 222 targeted tests, fix-check, GitHub CI, and package build/installation/startup/Bun verification passed.
## 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]>
This PR adds Cheaper Inference as a provider in
`src/providers/cheaperinference`.
It uses the same pattern as #336 (OrcaRouter).
Cheaper Inference is an OpenAI-compatible LLM gateway.
One API key gives access to models from several labs.
Usage: connect an API key from https://cheaperinference.com/signup.
Then call `cheaperinference.create_chat_completion` with a model such as
`gpt-5.4-mini`.
<details><summary>Details</summary>
Files:
- `src/providers/cheaperinference/definition.ts`
- `src/providers/cheaperinference/actions.ts`
- `src/providers/cheaperinference/executors.ts`
Actions:
- `create_chat_completion`: `POST /v1/chat/completions`.
- `create_message`: `POST /v1/messages` (Anthropic format).
- `list_models`: `GET /v1/models`.
The API has no embeddings endpoint, so this PR does not add
`create_embeddings`.
The credential validator calls `GET /v1/models`.
The proxy uses `defineProviderProxy` with a fixed base URL
`https://api.cheaperinference.com/v1`.
Tests:
- `npm run generate:catalog`: OK.
- `npm run lint` and `npm run format`: clean.
- `npm run typecheck` (src, scripts, examples): clean.
- `vitest run` on the provider source guards, provider loader, catalog
store, catalog index, and catalog assets tests: 13 files, 66 passed.
- I did not run the full `npm test`.
Live check: `gpt-5.4-mini` returned a reply through
`create_chat_completion` and `create_message`.
`list_models` and the credential validator also returned the model list.
A bad key returns `authorization_failed`.
</details>
---------
Co-authored-by: Kevin Cui <[email protected]>
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.
I work on Indexed (https://indexed.vc), a company data API. This adds an
`indexed` provider with API-key auth so the runtime can look up and
search company records.
What it does (all read-only):
- `lookup_company_by_domain`: find the company that owns a website
domain.
- `lookup_companies_by_domains`: up to 100 domains in one call, with a
hit or miss result per domain.
- `search_companies`: text and filter search (industry, country, size,
operating status, funding range) with page or cursor pagination.
- `get_company`: fetch a profile by slug at slim, standard or full
depth.
Credits: domain misses are free, a batch hit costs 1 credit, and
`get_company` costs 1, 2 or 3 credits by depth. Searches are free within
a daily quota. The action descriptions say this, and say which options a
free key cannot use. A 402 from the API is surfaced as
`insufficient_credit`, a 429 as `rate_limited`, and a plan-limit 403 as
invalid input rather than a reconnect prompt.
Auth: the key goes in the `X-API-Key` header. Every plan, including the
free one, can create a key at https://indexed.vc/developers/keys without
a card. Credential validation calls `GET /usage`, which does not spend
credits.
Left out on purpose, to keep this a small runnable subset: investors,
enrich, reveal, webhooks, scheduled exports and technology-stack
filters.
Checks run: `npm run generate:catalog`, `npm run fix-check`, `npm run
build` and `npm test` all pass. One test file covers the header,
list-filter joining and error mapping. I also ran each action once
against the live API with a free-plan key: validation, single and batch
domain lookup (hits and a miss), search, and `get_company` returned the
expected shapes, and the free-key refusals for sorting and filter-only
listing came back as clear errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Retika <[email protected]>
Co-authored-by: Kevin Cui <[email protected]>
Runtime token authentication prepares the same SQLite SELECT and
last-use UPDATE on every successful request. Reuse those two statements
per `SqliteRuntimeTokenStore` instance while continuing to read current
database rows on every call.
Preparation is lazy: custom migration sources can omit `runtime_tokens`,
so eagerly preparing in the constructor would break existing database
initialization behavior. SQL, parameter binding, row decoding, error
propagation, and public interfaces are unchanged. No token records or
authorization decisions are cached.
### Validation
- `npm run fix-check` passed (lint, formatting, and src/scripts/examples
typechecks). Unrelated formatter changes were excluded from the commit.
- SQLite store and runtime token service suites: **36 tests passed**,
including alternating token bindings, all policy fields, revocation,
independent last-use updates, cross-connection changes, reopening,
custom migrations, and best-effort last-use failure handling.
- `npm test` on Windows: 3,132 passed, 9 failed, 4 skipped. Six failures
were source-scan/catalog timeouts under the default parallelism.
- `npm test -- --maxWorkers=2 --testTimeout=30000`: **3,138 passed, 3
failed, 4 skipped**. Remaining failures are in unchanged provider source
guards: two clone-baseline assertions (Windows backslash paths do not
match the committed slash-separated baseline) and one auth-status
error-code assertion. No provider sources or guard tests are changed by
this PR. The upstream Linux workflow remains the CI check for this
change.
### Benchmark
Run `node scripts/benchmark-sqlite-token-statements.ts` after `npm ci`.
It compares the pre-change store method implementations with the actual
updated store, including row decoding and async method calls. Both use
the same migrated schema, 100 tokens, precomputed inputs, warmup, and
seven alternating measurement rounds. Database creation and seeding are
outside the timed loops; there is no timing assertion in CI.
Windows, Node **24.14.1**, SQLite **3.51.2**, `synchronous=2` (FULL):
| Storage | Lookup/update pairs per round | Baseline median | Reused
median | Baseline / reused per pair | Speedup |
| --- | ---: | ---: | ---: | ---: | ---: |
| In-memory | 30,000 | 1,205.28 ms | 247.22 ms | 40.18 / 8.24 us | 4.88x
|
| File, WAL | 1,000 | 652.60 ms | 591.26 ms | 652.60 / 591.26 us | 1.10x
|
The file-backed case reduces elapsed time by about **9.4%** on this
machine. These are isolated store-operation measurements, not end-to-end
HTTP throughput claims; filesystem synchronization and upstream API
latency affect real workloads.
Co-authored-by: machaoxin0407 <[email protected]>
Co-authored-by: l1shen <[email protected]>