feat(api): add ProjectConnector for end-user account connections - #4
Conversation
Add a SaaS-platform capability alongside the personal `Connector`: a platform built on OOMOL can now connect a third-party account on behalf of each of its own end-users and run actions for them — the composio / pipedream "managed auth" model. This ships as a SEPARATE `ProjectConnector` client, constructed with a project API key (`oo_proj_…`) over the same Bearer transport. Keeping it a distinct class — disjoint methods and types from `Connector`, with no shared `execute` / namespace / `proxy` / `catalog` / `apps` / `using` surface — means a project key only unlocks project-scoped operations and the two mental models never bleed into each other. The surface: `connect.oauth` (pending `ConnectionRequest` + `authorizationUrl`) plus the synchronous `connect.apiKey` / `connect.customCredential` (ready `ConnectedAccount`); `getConnectionRequest` and a polling `waitForConnection`; `execute` / `executeRaw` that reuse the existing action registry for precise per-action types; and `forUser(externalUserId)` to bind one end-user. The SDK speaks `externalUserId` and `connectionName` everywhere; the wire spellings (`userId`, `alias`) live only in request serialization. Supporting changes: add the `provider_config_not_found`, `connection_request_not_found`, `connected_account_not_found`, and client-only `client_wait_timeout` error codes; share `abortErrorFrom` and a new `assertHeadersSafe` helper from `http`; and harden `ensureSelfLink` in the type-acceptance harness so a stale self-copy can no longer shadow the live build. Signed-off-by: Kevin Cui <bh@bugs.cc>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
Summary by CodeRabbit
WalkthroughAdds Sequence Diagram(s)sequenceDiagram
participant App
participant ProjectConnector
participant ProjectUser
participant fetch
App->>ProjectConnector: forUser(EXTERNAL_USER_ID)
ProjectConnector-->>App: ProjectUser
App->>ProjectUser: connect.oauth(...)
ProjectUser->>fetch: POST connection request
fetch-->>ProjectUser: authorizationUrl + connection request
App->>ProjectUser: waitForConnection(connectionRequestId)
loop until terminal status or maxWaitMs
ProjectUser->>fetch: GET connection request status
fetch-->>ProjectUser: status
end
App->>ProjectUser: execute(...)
ProjectUser->>fetch: POST action execution
fetch-->>ProjectUser: execution result
Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/test-types.ts`:
- Line 13: The symlink handling in test-types.ts is too permissive because it
only checks whether node_modules/@oomol-lab/connector is a symlink, not whether
it points to the current checkout. Update the setup logic around the symlink
check and creation to verify the symlink target matches the expected live repo
location before reusing it, and if it points elsewhere, replace it so the
harness always uses the correct package source. Use the existing setup path
logic in the script’s symlinkSync/lstatSync flow to keep the live repo from
being shadowed by a stale checkout.
In `@src/project.ts`:
- Around line 367-387: The wait loop in waitForConnection is checking maxWaitMs
only after getConnectionRequest already starts a poll, so an over-budget poll
can still run past the wall-clock cap. Move the deadline check ahead of
getConnectionRequest in the loop, and pass a remaining-time budget into the
request path so the poll itself is clamped or aborted when maxWaitMs is reached;
keep using the existing symbols waitForConnection, getConnectionRequest, and
deps.sleep.
- Around line 300-303: The provider selector serialization in providerKeys
currently prefers providerConfigId and silently drops service when both are
present. Update providerKeys in src/project.ts to enforce the “exactly one”
contract by rejecting selectors that include both fields, and only serialize
when exactly one of providerConfigId or service is set. Keep the check close to
providerKeys so callers get a clear failure instead of sending the wrong
selector.
In `@test/project.test.ts`:
- Around line 7-25: The mocked gateway payload helper in requestPayload (and the
related test fixtures it feeds) is using externalUserId instead of the wire
userId field, so these tests bypass the ConnectionRequest/ConnectedAccount
mapping. Update the mocked responses to use userId at the wire boundary and keep
the domain translation verified through the existing mapper-focused test paths,
so regressions in the userId -> externalUserId conversion are caught.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8ecef99b-75da-4510-a930-4dc89a078fb3
📒 Files selected for processing (13)
README.mdexamples/README.mdexamples/project.tsfixtures/consumer/partial-b/index.test-d.tsfixtures/consumer/with-b/index.test-d.tsscripts/test-types.tssrc/connector.tssrc/errors.tssrc/http.tssrc/index.tssrc/project.tstest/helpers.tstest/project.test.ts
Address CodeRabbit review on the ProjectConnector PR. `providerKeys` rejected a both-present (or neither-present) provider selector by silently dropping `service` and connecting against the wrong provider config — which also masked the conflict from the gateway's exactly-one check. It now throws `client_invalid_request` locally, the same precheck the client already does for a missing apiKey or a CRLF header. `waitForConnection` only checked the `maxWaitMs` budget after a poll, so the final poll could run its own per-request timeout and retries past the documented wall-clock cap. It now checks the deadline BEFORE each poll and clamps the poll's timeout to the remaining budget, threading the client's default timeout through a new `defaultTimeoutMs` seam. `ensureSelfLink` returned on any symlink; a link pointing at a different checkout would still shadow the live build. It now also requires the symlink target to resolve to the repo root. Tests: pin no-poll-past-cap, and the existing sleep-clamp test still asserts the clamped final sleep. Signed-off-by: Kevin Cui <bh@bugs.cc>
Adds
ProjectConnector, a new client for building a SaaS platform on OOMOL: connect a third-party account on behalf of each of your own end-users and run actions for them — the composio / pipedream "managed auth" model. The personalConnectoris unchanged; this is purely additive.The key design decision is to ship this as a separate client rather than a namespace on
Connector. You construct it with a project API key (oo_proj_…) over the sameAuthorization: Bearertransport, and it exposes only project-scoped operations — disjoint methods and types, with no sharedexecute/ service namespaces /proxy/catalog/apps/using. A project key unlocks only the project surface and a personal key only the personal one, so the two mental models can't bleed into each other (the type fixtures assert this disjointness).Surface:
connect.oauth(externalUserId, …)returns a pendingConnectionRequestwith anauthorizationUrlto send the user to;connect.apiKey/connect.customCredentialare synchronous and return a readyConnectedAccount.getConnectionRequest(id)andwaitForConnection(requestOrId, …)poll an OAuth link to a terminal status (returning onconnected/failed/ naturalexpired; throwing the non-retryableclient_wait_timeoutonly when the overallmaxWaitMselapses).execute/executeRaw(externalUserId, actionId, input, …), reusing the existing@oomol-lab/connector-typesaction registry for precise per-action types.forUser(externalUserId)binds one end-user and drops the repeated id.The SDK surface speaks
externalUserIdandconnectionNameeverywhere; the gateway's wire spellings (userId,alias) are mapped only inside request/response serialization.Supporting changes: four new
ConnectorErrorcodes (provider_config_not_found,connection_request_not_found,connected_account_not_found, and the client-only, non-retryableclient_wait_timeout);abortErrorFromplus a new sharedassertHeadersSafeare lifted intohttpand used by both clients; andensureSelfLinkin the type-acceptance harness now replaces a stale self-installed copy that could otherwise shadow the live build during type checks.Docs and a runnable
examples/project.tslifecycle are included; tests cover the wire shapes, polling / timeout / abort, selector precedence, and client disjointness.