fix(link): poll for real database password after project becomes active#122
Conversation
After `create --auth better-auth` (or any first-time link against a newly provisioned cloud project), the platform flips `status=active` before `/api/metadata/database-password` has the real password populated. During that window the endpoint returns `********` itself, our splice into the masked URL was a silent no-op, and `.env.local` ended up with `postgres:********@<host>/insforge?sslmode=require` — an unusable URL that BA's pg pool can't authenticate with. Detect the masked-password response and poll briefly (9 retries × 2s = ~20s) before giving up. Most cloud projects finalize within a few seconds, so the common case sees no added latency. If the endpoint never returns a real password we fall back to null, the manifest default (localhost) is written, and the user can re-run `link` later — at which point 0.1.73's masked-pattern refresh kicks in. Adds 8 tests covering the new `isMaskedDatabasePassword` helper and keeps the existing `spliceDatabasePassword` coverage intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
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 ignored due to path filters (1)
📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
WalkthroughThis PR adds masked-password detection and polling to handle the provisioning window where the platform's password endpoint may return a run-of-asterisks placeholder. It introduces ChangesMasked Database Password Handling
Package Version
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/lib/api/oss.test.ts (1)
32-49: ⚡ Quick winAdd behavior tests for polling/fallback path in
getDatabaseConnectionString()Current additions validate the helper, but not the end-to-end polling decision logic. Please add tests for: masked-then-real password, always-masked timeout fallback, and non-retryable endpoint-missing fallback.
🤖 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 `@src/lib/api/oss.test.ts` around lines 32 - 49, Add end-to-end tests for getDatabaseConnectionString that exercise its polling and fallback logic: (1) "masked-then-real" — mock the password fetch endpoint used by getDatabaseConnectionString to return a masked value (use isMaskedDatabasePassword to detect) for the first N calls and then a real password, use Jest fake timers/advanceTimersByTime to simulate polling and assert the returned connection string uses the real password; (2) "always-masked timeout fallback" — mock the endpoint to always return a masked value, advance timers past the retry timeout and assert getDatabaseConnectionString falls back to the configured fallback (env/secret) connection string; (3) "non-retryable endpoint-missing fallback" — mock the endpoint to return a non-retryable error (e.g., 404 or explicit endpoint-missing response) on first call and assert getDatabaseConnectionString immediately uses the fallback. Locate tests near existing isMaskedDatabasePassword tests in src/lib/api/oss.test.ts and stub the same HTTP/fetch helper used by getDatabaseConnectionString so tests are deterministic.
🤖 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 `@src/lib/api/oss.ts`:
- Around line 98-104: The current loop repeatedly retries on any null from
fetchDatabasePasswordOnce(), causing unnecessary delay for deterministic
failures; update fetchDatabasePasswordOnce() to distinguish retryable vs
non-retryable outcomes (e.g., return { password, retryable } or throw a
PasswordFetchError with a retryable flag) and then change the polling logic that
uses password, POLL_ATTEMPTS and POLL_DELAY_MS to stop retrying immediately when
the result indicates non-retryable failure (break/throw), while only looping
when retryable is true and attempt < POLL_ATTEMPTS.
---
Nitpick comments:
In `@src/lib/api/oss.test.ts`:
- Around line 32-49: Add end-to-end tests for getDatabaseConnectionString that
exercise its polling and fallback logic: (1) "masked-then-real" — mock the
password fetch endpoint used by getDatabaseConnectionString to return a masked
value (use isMaskedDatabasePassword to detect) for the first N calls and then a
real password, use Jest fake timers/advanceTimersByTime to simulate polling and
assert the returned connection string uses the real password; (2) "always-masked
timeout fallback" — mock the endpoint to always return a masked value, advance
timers past the retry timeout and assert getDatabaseConnectionString falls back
to the configured fallback (env/secret) connection string; (3) "non-retryable
endpoint-missing fallback" — mock the endpoint to return a non-retryable error
(e.g., 404 or explicit endpoint-missing response) on first call and assert
getDatabaseConnectionString immediately uses the fallback. Locate tests near
existing isMaskedDatabasePassword tests in src/lib/api/oss.test.ts and stub the
same HTTP/fetch helper used by getDatabaseConnectionString so tests are
deterministic.
🪄 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: 5c531084-453c-4158-95e4-45d3b47a5dd0
📒 Files selected for processing (2)
src/lib/api/oss.test.tssrc/lib/api/oss.ts
| let password = await fetchDatabasePasswordOnce(); | ||
| const POLL_ATTEMPTS = 9; | ||
| const POLL_DELAY_MS = 2_000; | ||
| for (let attempt = 0; password === null && attempt < POLL_ATTEMPTS; attempt++) { | ||
| await new Promise((r) => setTimeout(r, POLL_DELAY_MS)); | ||
| password = await fetchDatabasePasswordOnce(); | ||
| } |
There was a problem hiding this comment.
Avoid retrying non-retryable password fetch failures
Line 101 retries on every null, but fetchDatabasePasswordOnce() returns null for both transient “not ready” and deterministic failures (like endpoint missing). That can add ~18s avoidable latency on older/self-hosted backends.
Proposed direction
-async function fetchDatabasePasswordOnce(): Promise<string | null> {
+type DatabasePasswordFetchResult = {
+ password: string | null;
+ retryable: boolean;
+};
+
+async function fetchDatabasePasswordOnce(): Promise<DatabasePasswordFetchResult> {
try {
- const res = await ossFetch('/api/metadata/database-password');
+ const res = await ossFetch('/api/metadata/database-password');
const body = await res.json() as { databasePassword?: string };
const pw = body.databasePassword;
- if (typeof pw !== 'string' || !pw || isMaskedDatabasePassword(pw)) return null;
- return pw;
+ if (typeof pw !== 'string' || !pw || isMaskedDatabasePassword(pw)) {
+ return { password: null, retryable: true };
+ }
+ return { password: pw, retryable: false };
} catch {
- return null;
+ // Distinguish non-retryable cases (e.g. route not available) if possible.
+ return { password: null, retryable: false };
}
}
@@
- let password = await fetchDatabasePasswordOnce();
+ let { password, retryable } = await fetchDatabasePasswordOnce();
@@
- for (let attempt = 0; password === null && attempt < POLL_ATTEMPTS; attempt++) {
+ for (let attempt = 0; password === null && retryable && attempt < POLL_ATTEMPTS; attempt++) {
await new Promise((r) => setTimeout(r, POLL_DELAY_MS));
- password = await fetchDatabasePasswordOnce();
+ ({ password, retryable } = await fetchDatabasePasswordOnce());
}🤖 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 `@src/lib/api/oss.ts` around lines 98 - 104, The current loop repeatedly
retries on any null from fetchDatabasePasswordOnce(), causing unnecessary delay
for deterministic failures; update fetchDatabasePasswordOnce() to distinguish
retryable vs non-retryable outcomes (e.g., return { password, retryable } or
throw a PasswordFetchError with a retryable flag) and then change the polling
logic that uses password, POLL_ATTEMPTS and POLL_DELAY_MS to stop retrying
immediately when the result indicates non-retryable failure (break/throw), while
only looping when retryable is true and attempt < POLL_ATTEMPTS.
There was a problem hiding this comment.
No issues found across 2 files
Tip: cubic could auto-approve low-risk PRs like this, if it thinks it's safe to merge. Learn more
Ship the masked-password poll fix above (no first-run masked DATABASE_URL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom
User reported that on first link after `create --auth better-auth`, `.env.local` was written with a masked password:
```
DATABASE_URL=postgresql://postgres:********@4e7yvep2.us-east.database.insforge.app:5432/insforge?sslmode=require
```
This URL is unusable — BA's pg pool can't authenticate with it. Re-running `link` does fix it (0.1.73's masked-pattern refresh kicks in), but the first-run experience is broken.
Root cause
The platform flips a project to `status=active` as soon as the container is up, before `/api/metadata/database-password` is populated. During that window the endpoint itself returns ``. Our `spliceDatabasePassword` silently spliced `` into the masked URL — same masked URL out.
Fix
Most cloud projects finish provisioning within a few seconds, so the common case adds no latency. If the endpoint never returns a real password, we fall back to `null` and the user can re-run `link` — at which point 0.1.73's refresh logic handles the masked URL.
Test plan
🤖 Generated with Claude Code
Summary by cubic
Fixes first-time link writing a masked Postgres URL by polling for the real database password after a project becomes active. Prevents unusable
postgresql://...:********@...in.env.localand avoids auth failures.Bug Fixes
********(any run of*) from/api/metadata/database-passwordas not ready.nullinstead of writing a masked URL.isMaskedDatabasePasswordand a one-shot fetch helper; updatedgetDatabaseConnectionString; tests added.Dependencies
@insforge/clito0.1.76.Written for commit 7c68220. Summary will update on new commits.
Summary by CodeRabbit
New Features
Tests