Skip to content

Commit 50f8534

Browse files
Brooooooklynclaude
andauthored
feat: local database access via void db --remote (#170)
* feat(api): add project_database_credentials table and helpers Adds the project_database_credentials table (one row per project, PK on project_id, ON DELETE CASCADE) to store encrypted database connection strings. Includes the drizzle migration (0019), an updated snapshot, and three db.ts helpers: upsertProjectDatabaseUrl, getProjectDatabaseUrl, and deleteProjectDatabaseUrl. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(api): align project_database_credentials helpers with peer pattern - `upsertProjectDatabaseUrl`: add `.returning()` and return `Promise<ProjectDatabaseCredentials>` instead of `Promise<void>`, matching `upsertProjectSecret`. - `getProjectDatabaseUrl`: narrow return type from an inline object literal to `Promise<ProjectDatabaseCredentials | null>`, matching `getProjectSecret`. - Import `ProjectDatabaseCredentials` from `./types.ts`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(api): encrypt+persist database URL at set-url time, expose GET endpoint - POST /:id/database and PUT /:id/database-url now encrypt the connection string with AES-GCM (using encryptProjectSecret) and upsert it into project_database_credentials before calling Cloudflare Hyperdrive, so the URL is never unrecoverable if Hyperdrive succeeds but DB write fails. - New GET /:id/database-url decrypts and returns { url } to the project owner; returns 404 with database_url_not_stored error when no row exists. - Integration tests cover: round-trip via POST and PUT, last-write-wins upsert, ownership 403, and structured 404. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(api): return 403 (not 404) on database-url ownership mismatch Split the combined `!project || wrong-owner` guard in GET /:id/database-url into two separate checks — 404 when the project does not exist, 403 when it exists but belongs to a different user. Matches the pattern used by all other ownership-gated endpoints in projects.ts (e.g. /resources, /logs). Update the corresponding test assertion from 404 to 403. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: bump framework (getDatabaseUrl + db --remote args) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: bump framework (db execute --remote) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: bump framework (db execute --remote test coverage) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: bump framework (db execute --remote ordering fix) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: bump framework (db studio --remote + docs) Points framework submodule at de247c3 which adds --remote support to `void db studio` (PG only), updates help.ts and db-args.ts descriptions, adds full docs in reference/cli.md, and includes 9 new unit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: bump framework (test type fixes) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: bump framework (db --remote polish) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(api): persist database URL only after Cloudflare accepts it CF is the source of truth for production routing; the stored credential is the URL CF accepted and is now serving. Previously both POST /:id/database and PUT /:id/database-url wrote to project_database_credentials before calling Cloudflare, so a CF rejection would leave a stale row that GET /:id/database-url (used by the CLI for remote access) would serve — diverging the CLI from what production Hyperdrive actually routes. The fix calls CF first and only persists on success. If the upsert fails after CF succeeds (rare), a 500 is returned instructing the user to re-run `void db set-url`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: bump framework (remote-db safety hardening) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(api): update orphaned Hyperdrive before persisting URL in isAlreadyExistsError paths In both POST /:id/database and PUT /:id/database-url, the isAlreadyExistsError recovery branch retrieved an existing Hyperdrive ID (via getHyperdriveByName) but never pushed the submitted connectionString to CF before storing it locally. This could cause GET /:id/database-url to return a URL that diverged from what CF was actually routing. Fix: call updateHyperdrive with the found ID and submitted connectionString before falling through to the persist block, ensuring CF and the stored credential stay in sync. Also add updateHyperdrive to the POST handler's dynamic import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style(api): apply vp fmt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: bump framework to main (db --remote merged via #213) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 002e147 commit 50f8534

9 files changed

Lines changed: 1845 additions & 3 deletions

File tree

‎framework‎

Submodule framework updated 63 files
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE TABLE "project_database_credentials" (
2+
"project_id" text PRIMARY KEY NOT NULL,
3+
"ciphertext" text NOT NULL,
4+
"iv" text NOT NULL,
5+
"key_version" text NOT NULL,
6+
"created_at" timestamp DEFAULT now() NOT NULL,
7+
"updated_at" timestamp DEFAULT now() NOT NULL
8+
);
9+
--> statement-breakpoint
10+
ALTER TABLE "project_database_credentials" ADD CONSTRAINT "project_database_credentials_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;

0 commit comments

Comments
 (0)