feat(integration): adopt verb to bind an existing Nango connection#141
Conversation
`relayfile integration adopt PROVIDER --connection-id ID` mirrors the
disconnect command's flag shape and confirmation prompt, then POSTs to the
new Cloud route POST .../integrations/{provider}/adopt. The SDK gains
`adoptIntegration(provider, connectionId, options)` next to
`disconnectIntegration`. On success the CLI saves the new connection
locally and clears any prior disconnect marker; on 4xx the local state is
left untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
cmd/relayfile-cli/main_test.go (2)
2264-2266: ⚡ Quick winDon’t ignore credential setup errors in these tests.
Line 2264, Line 2308, and Line 2373 swallow
loadCloudCredentials/saveCloudCredentialsfailures; this can mask setup breakage and make failures misleading.Proposed explicit error checks
- creds, _ := loadCloudCredentials() + creds, err := loadCloudCredentials() + if err != nil { + t.Fatalf("loadCloudCredentials failed: %v", err) + } creds.APIURL = server.URL - _ = saveCloudCredentials(creds) + if err := saveCloudCredentials(creds); err != nil { + t.Fatalf("saveCloudCredentials(update) failed: %v", err) + }Also applies to: 2308-2310, 2373-2375
🤖 Prompt for 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. In `@cmd/relayfile-cli/main_test.go` around lines 2264 - 2266, The test currently ignores errors from loadCloudCredentials/saveCloudCredentials which can hide setup failures; update each call site (where loadCloudCredentials() and saveCloudCredentials(creds) are invoked in main_test.go — the occurrences around the blocks currently at the three locations) to check the returned error and fail the test on error (e.g., use t.Fatalf or t.Helper()+t.Fatalf or require.NoError) so any credential load/save failure aborts the test and surfaces the real cause.
2145-2150: ⚡ Quick winStrengthen bootstrap endpoint assertions to avoid false-positive adopt tests.
Line 2145 and Line 2148 currently accept refresh/join requests without validating method/auth/body. That lets refresh-flow regressions pass undetected (e.g., stale token accepted on join).
Proposed tightening in the test helper
switch { case r.URL.Path == "/api/v1/auth/token/refresh": seen["refresh"]++ + if r.Method != http.MethodPost { + t.Fatalf("expected refresh POST, got %s", r.Method) + } + if got := r.Header.Get("Authorization"); got != "Bearer cld_old" { + t.Fatalf("unexpected refresh Authorization: %q", got) + } + var body cloudTokenRefreshRequest + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode refresh body failed: %v", err) + } + if body.RefreshToken != "refresh_123" { + t.Fatalf("unexpected refresh token: %q", body.RefreshToken) + } _, _ = w.Write([]byte(`{"apiUrl":"` + server.URL + `","accessToken":"cld_new","refreshToken":"refresh_123","accessTokenExpiresAt":"2030-05-01T00:00:00Z"}`)) case r.URL.Path == "/api/v1/workspaces/ws_123/join": seen["join"]++ + if r.Method != http.MethodPost { + t.Fatalf("expected join POST, got %s", r.Method) + } + if got := r.Header.Get("Authorization"); got != "Bearer cld_new" { + t.Fatalf("unexpected join Authorization: %q", got) + } _, _ = w.Write([]byte(`{"workspaceId":"ws_123","token":"rf_join","relayfileUrl":"` + server.URL + `"}`))🤖 Prompt for 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. In `@cmd/relayfile-cli/main_test.go` around lines 2145 - 2150, The test HTTP handler currently accepts any request to "/api/v1/auth/token/refresh" and "/api/v1/workspaces/ws_123/join" without validating method, headers or body which allows false-positive adopt tests; update the handler for those cases (the switch arms matching those paths) to assert the expected HTTP method (e.g., POST), validate the Authorization header or request body contains the expected refresh token/credentials for "/api/v1/auth/token/refresh" and expected join token/body for "/api/v1/workspaces/ws_123/join", increment seen["refresh"] or seen["join"] only after validation, and return a 400/401 when validation fails so regressions (stale tokens/method mismatches) fail the test.
🤖 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 `@cmd/relayfile-cli/main.go`:
- Around line 1808-1810: The os.Remove call that clears the disconnect marker
for record.LocalDir is currently ignoring all errors; change this to capture the
returned error from os.Remove(filepath.Join(record.LocalDir, ".relay",
"disconnected", provider+".json")) and only ignore it when the error is
os.IsNotExist(err); for any other error, surface it (e.g., return it or log it
via the existing logger) so unexpected filesystem failures aren’t silently
swallowed—update the code around record.LocalDir and provider to handle and
propagate/log non-ENOENT errors.
- Around line 1729-1810: The provider string from normalizeProviderID(fs.Arg(0))
must be validated/sanitized before any local filesystem operations
(saveIntegrationConnection, filepath.Join/ os.Remove) to prevent path traversal;
add a strict check right after provider is set (e.g., ensure no path separators
or "..", and match a safe regexp like allowed chars [a-z0-9_-] with a non-empty
value) and return an error if it fails, then proceed to use provider for
saving/removing files only when it passes validation.
---
Nitpick comments:
In `@cmd/relayfile-cli/main_test.go`:
- Around line 2264-2266: The test currently ignores errors from
loadCloudCredentials/saveCloudCredentials which can hide setup failures; update
each call site (where loadCloudCredentials() and saveCloudCredentials(creds) are
invoked in main_test.go — the occurrences around the blocks currently at the
three locations) to check the returned error and fail the test on error (e.g.,
use t.Fatalf or t.Helper()+t.Fatalf or require.NoError) so any credential
load/save failure aborts the test and surfaces the real cause.
- Around line 2145-2150: The test HTTP handler currently accepts any request to
"/api/v1/auth/token/refresh" and "/api/v1/workspaces/ws_123/join" without
validating method, headers or body which allows false-positive adopt tests;
update the handler for those cases (the switch arms matching those paths) to
assert the expected HTTP method (e.g., POST), validate the Authorization header
or request body contains the expected refresh token/credentials for
"/api/v1/auth/token/refresh" and expected join token/body for
"/api/v1/workspaces/ws_123/join", increment seen["refresh"] or seen["join"] only
after validation, and return a 400/401 when validation fails so regressions
(stale tokens/method mismatches) fail the test.
🪄 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 Plus
Run ID: 35600739-b5fc-4a80-a9ab-3d5f23210c1c
📒 Files selected for processing (4)
cmd/relayfile-cli/main.gocmd/relayfile-cli/main_test.gopackages/sdk/typescript/src/setup.test.tspackages/sdk/typescript/src/setup.ts
Summary
relayfile integration adopt PROVIDER --connection-id ID [--workspace NAME] [--provider-config-key KEY] [--yes]. Mirrors the disconnect command's flag parsing and confirmation prompt. Posts to Cloud's newPOST .../integrations/{provider}/adoptroute and on success persists the new connection in the local mirror and clears any prior disconnect marker so the status probe stops reporting the workspace as disconnected.adoptIntegration(provider, connectionId, options)to the TypeScript SDK next todisconnectIntegration. Returns{ connectionId, replacedConnectionId? }so callers can surface whether a stale prior row was migrated.apiClient.doto read Cloud's{ error }field (alongside{ message }) so 4xx bodies surface the operator-facing message instead of a raw JSON blob.integration adopt.Cloud dependency
The Cloud route is AgentWorkforce/cloud#537 (
agent-relay/integration-adopt-endpoint, draft pending merge of #536). This PR can be reviewed in parallel but should land after the Cloud endpoint is deployed.Test plan
go test ./cmd/relayfile-cli/— full Go CLI suite green, 5 new adopt-specific tests (fresh-insert success, replacement reporting, 409 refusal preserves local state, --connection-id required, disconnect-marker cleared on adopt)npm test --workspace packages/sdk/typescript— 138 tests green, 3 new SDK tests (request shape, replacedConnectionId presence/absence, 409 surfaces as CloudApiError)tsc --noEmitinpackages/sdk/typescript— cleango build ./cmd/relayfile-cli/— cleanrelayfile integration adopt github --connection-id <real conn>against a staging workspace