@meetreeve/module-connects
v1.0.0
Published
Reeve.Connects module (DEV-10005, D1): the CONFIG surface for an org's connector credentials - inventory (what this org holds, when it was added, its last health check), revoke, and the ConnectorPicker mount for connecting. Talks to /api/connects/v1. Vers
Readme
@meetreeve/module-connects
The Reeve.Connects module (DEV-10005, D1). Two halves:
././client— a framework-agnostic, server-safeConnectsClient. No React, no Next.js, so a tenant's"use server"actions file can construct it and stay the only place the host key is read../config— the CONFIG surface: the org's connector-credential inventory (what it holds, when it was added, its last health check), revoke, and a slot forConnectorPickerto do the connecting.
Both talk to the /api/connects/v1 router with the tenant's host key (the
connects capability) — retargeted from the legacy /api/org-connectors/*
router on Matt's call (DEV-10005, 2026-09-11), which has no host-key branch
and is sunsetting under DEV-547.E.
| Method | Route | Returns |
| --- | --- | --- |
| listConnections({ provider?, full? }) | GET /api/connects/v1/connections[?provider=&status=full] | { connections } |
| storeCredentials(provider, credentials) | POST /api/connects/v1/connect/{provider}/credentials | { id, provider, connected, account_name, account_id } |
| testCredentials(provider, credentials) | POST /api/connects/v1/connect/{provider}/test | { valid, account_name, error } — validates pasted credentials, never stores |
| disconnect(connectionId) | DELETE /api/connects/v1/connections/{connId} | 204 |
X-Reeve-Host-App is sent only when hostApp is configured: a default value
answers 403 host_app_mismatch for every tenant that doesn't set one. Each
request is bounded by timeoutMs (default 30s, 0 disables it) and aborts
with a ConnectsError of status 0 rather than leaving a server action pending.
Credential fields must be strings; the client rejects anything else before any
round trip, naming the key but never the value.
Versioned in lockstep with the Python reeve-module-connects package — both
always ship the same MAJOR.MINOR.PATCH, the contract module-channels already
holds with its own sibling.
The picker is injected, not imported
./config renders a picker slot. The host passes <ConnectorPicker client={pickerClient} entries={entries} />
from @meetreeve/ui; this package never imports it.
Two reasons, both learned the hard way:
- One paste path, not two. DEV-7857's standing decision (Matt,
2026-08-05) is that credential paste lives in
ConnectorPicker. Building a second one here would be two paste paths with different validation where only one carries the setup guides — exactly what produced DEV-7726 (two divergentMETA_SCOPESlists, neither a superset of the other) and DEV-7751 (a third copy in adbot). If the picker can't do what a surface needs, extend the picker. - One version of it. The host resolves
@meetreeve/ui; this package neither imports nor peers it, so a slot means exactly one picker version resolves — the host's. DEV-7868 is the standing example of what version drift costs.
Injected data access
Every component takes a ConnectsConfigClient rather than importing a host's
fetch client, which is what lets reeve-frontend (productFetch + tenant-pin
auth) and a spawned tenant (a different scheme entirely) mount the identical
components.
<ConnectsSettingsClient
client={connectsClient}
permissions={{ canManage: userRole === "admin" || userRole === "owner" }}
picker={<ConnectorPicker client={pickerClient} entries={entries} />}
/>Authorization is the substrate's, not this package's
permissions.canManage is supplied by the host and is UI affordance only —
it exists so the surface doesn't offer an action that will certainly 403.
| Finding | How it lands here |
| --- | --- |
| DEV-8519 — surfaces gated on bare membership; any member reached every connector including disconnect | The real gate is server-side. Without canManage no disconnect control renders, and a read-only notice explains why. |
| DEV-8789 — the legacy gate read role only, so a per-connector grant couldn't satisfy it | The v1 delete gate authorizes the connection's owner, a live per-connector connectors.activate grant, or an org admin/owner — so a host that knows a viewer holds that grant may set canManage for them. A 403 is still surfaced as readable text rather than an unhandled rejection. |
| DEV-6187 — /api/composio/execute took a caller-supplied connected_account_id with no ownership check | Neither ConnectsConfigClient nor ConnectsClient takes an org or account identifier. The acting org is resolved server-side from the credential, and a connection id is looked up within that org (another org's id answers 404). |
There is no per-row "Test": the v1 /test route validates pasted
credentials, which is the picker's job. Each row shows the stored health probe
instead.
Provider names and connection ids go through pathSegment(): an unvalidated value there
changes which endpoint the host key is presented to (/ walks out of the
surface, ? re-shapes the URL) with no error anywhere.
Scripts
npm test # vitest
npm run build # tsup
npm run check:esm