From 3ab9bf840cebedf49b02c7b246cddb3ea23fe8d0 Mon Sep 17 00:00:00 2001 From: varutasu <104105839+varutasu@users.noreply.github.com> Date: Tue, 26 May 2026 22:08:45 -0500 Subject: [PATCH] fix(scripts): convert reset-db.js to ESM + require ADMIN_INITIAL_PASSWORD (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold of two queued follow-ups from pick-a-name architect audit (convert-reset-db-to-esm + purge-weak-creds-from-helpers). Three bugs in one file; all three fixed atomically by mirroring the proven post- drop-public-setup setup-neon-db.js shape (commit b63b509). Bugs fixed: 1. CJS-in-ESM (lines 10, 12, 142): require('dotenv'), require('@neon...'), inline require('bcryptjs'). package.json has "type": "module" since bump-next-js, so npm run reset-db threw ReferenceError on Node 22.x. Same bug pattern that hit setup-neon-db.js pre-drop-public-setup B2. 2. Hardcoded weak admin password (line 143: bcrypt.hash('admin123', 12)). Same anti-pattern drop-public-setup B1 removed from setup-neon-db.js. 3. Password echoed to stdout (line 156: console.log('Admin Password: admin123')). Security anti-pattern; setup-neon-db.js post-DPS does NOT echo passwords. Fix shape (verbatim mirror of setup-neon-db.js): - ESM top-level imports (dotenv, neon, bcrypt) - Fail-loud ADMIN_INITIAL_PASSWORD env-var check at function top with helpful error message pointing to README "First-time admin setup" - bcrypt.hash(adminPassword, 12) instead of literal - ON CONFLICT (email) DO NOTHING on INSERT (defensive against double-run, matches setup-neon-db.js line 149) - No password echo in success block; admin email logged for confirmation - Updated docstring to flag DESTRUCTIVE + reference required env Convoy file: .convoys/fix-reset-db-script.md (P2 hygiene, parent-owned, no architect — this is a proven-pattern fold with no new decisions to ratify). Verification: - node --check scripts/reset-db.js: exit 0 - npm run lint: 128 problems (baseline preserved, no regression) - npm run test:run: 21/21 pass - Grep: 0 require( | 0 admin123 | 0 'Admin Password' in scripts/reset-db.js - Grep: 3 ADMIN_INITIAL_PASSWORD references (docstring, const, error msg) NOT live-tested (script is destructive — drops all tables). Operator can optionally run npm run reset-db against a non-prod Neon branch post-merge to verify end-to-end. Surfaces follow-up: lint-against-cjs-in-esm-scripts (P3 polish — add ESLint rule to prevent any future require() in scripts/** under "type": "module"). Surfaced for future convoy queue. Co-authored-by: Cursor --- .convoys/fix-reset-db-script.md | 124 ++++++++++++++++++++++++++++++++ scripts/reset-db.js | 55 +++++++++----- 2 files changed, 161 insertions(+), 18 deletions(-) create mode 100644 .convoys/fix-reset-db-script.md diff --git a/.convoys/fix-reset-db-script.md b/.convoys/fix-reset-db-script.md new file mode 100644 index 0000000..f36f768 --- /dev/null +++ b/.convoys/fix-reset-db-script.md @@ -0,0 +1,124 @@ +# fix-reset-db-script (P2 hygiene — fold of two queued follow-ups) + +**Status:** in-progress +**Priority:** P2 hygiene (not a security blocker; `npm run reset-db` is dev-only +and currently broken on Node 22, so blast radius is low — but the bug +pattern is the same as the P0-grade weak-creds shape that +`drop-public-setup` already fixed once) +**Convoy owner:** parent (no architect — fold of two well-scoped +follow-ups; single-file fix following an established proven pattern) +**Opened:** 2026-05-25 + +## Problem (3 bugs in 1 file) + +`scripts/reset-db.js` carries three known bugs surfaced by the +`pick-a-name` architect audit (2026-05-24) and ratified for fix in +this session: + +1. **CJS-in-ESM environment** (lines 10, 12, 142): `require()` calls in + an ESM file (`package.json "type": "module"` since `bump-next-js`). + `npm run reset-db` throws `ReferenceError: require is not defined` + on Node 22.x. **Same bug pattern that hit `setup-neon-db.js` + pre-`drop-public-setup` Brief 2.** Fix is the same fix. +2. **Hardcoded weak admin password** (line 143: `bcrypt.hash('admin123', 12)`): + identical anti-pattern to the one `drop-public-setup` Brief 1 + removed from `setup-neon-db.js`. Should require `ADMIN_INITIAL_PASSWORD` + env var, fail loud if unset. +3. **Password echoed to stdout** (line 156: + `console.log(' Admin Password: admin123')`): explicit security + anti-pattern. `setup-neon-db.js` post-`drop-public-setup` does NOT + echo the password — same convention applies here. + +## Fix (mirror `setup-neon-db.js` exactly) + +The fix shape is fully derived from the post-`drop-public-setup` +`scripts/setup-neon-db.js` file shipped at `b63b509`. Verbatim mirror: + +- Replace CJS `require('dotenv').config()` with ESM + `import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' })` +- Replace CJS `const { neon } = require('@neondatabase/serverless')` + with ESM `import { neon } from '@neondatabase/serverless'` +- Replace inline CJS `const bcrypt = require('bcryptjs')` (line 142) + with top-level ESM `import bcrypt from 'bcryptjs'` +- Add the same fail-loud `ADMIN_INITIAL_PASSWORD` env-var check at the + top of the `async function resetDatabase()` body, with the same + helpful error message that points to README.md +- Replace `bcrypt.hash('admin123', 12)` with `bcrypt.hash(adminPassword, 12)` +- Update the admin email to the post-`pick-a-name` canonical + (`admin@deckhearth.com` — already correct in the file at line 147, + by the B2 sweep) +- Replace the `Admin Password: admin123` log line with `Admin user + ready (email: admin@deckhearth.com)` (matching `setup-neon-db.js` + line 157) +- Add `ON CONFLICT (email) DO NOTHING` to the INSERT (matching + `setup-neon-db.js` line 149 — defensive against double-run) + +## Scope + +- **In scope:** `scripts/reset-db.js` only. +- **Out of scope:** any other `scripts/*.js` files (none have the same + bugs — `setup-neon-db.js` already fixed, migration script already + ESM, the rest don't ship admin creds). + +## Why no architect + +This is a **proven-pattern fold** — both `convert-reset-db-to-esm` and +`purge-weak-creds-from-helpers` were architect-recommended in +`pick-a-name` for "may fold if more such bugs accumulate in helper +scripts." All 3 bugs are in 1 file; the fix shape is verbatim-mirror of +the post-`drop-public-setup` `setup-neon-db.js`. No new decisions; no +new precedents; no surface for an architect to add value. Parent +applies the fix, runs the bounded checks, opens the PR. If anything +surprising surfaces (a 4th bug, a different bcrypt API, etc.), the +parent stops and dispatches an architect mid-execution. + +## Acceptance criteria + +- `node --check scripts/reset-db.js` exit 0 +- `npm run lint` exit 1 with 128 problems (baseline preserved; no + regression) +- `npm run test:run` 21/21 pass (no test surface touched; verification + only) +- Grep: 0 occurrences of `require(` in `scripts/reset-db.js` +- Grep: 0 occurrences of `admin123` in `scripts/reset-db.js` +- Grep: 0 occurrences of `Admin Password` in `scripts/reset-db.js` +- Grep: `ADMIN_INITIAL_PASSWORD` referenced (1 hit) + +## Operator action required pre-merge + +- **None pre-merge** (no schema change, no env-var addition). +- **Optional post-merge:** if the operator wants to verify the fix + works end-to-end, they can run `npm run reset-db` against a + **non-prod** Neon branch (the script drops all tables — DO NOT run + against prod). The script will refuse to run if `ADMIN_INITIAL_PASSWORD` + is not set in `.env.local`, with the same helpful error message + `setup-neon-db.js` already uses. + +## Known constraints + +- The script is **destructive** (drops all tables). We do NOT live-test + it in this convoy. Boot-the-brief is syntax + lint + vitest only. + Live verification is the operator's optional post-merge action. +- This convoy does NOT add ESLint `no-restricted-syntax` rule against + CJS `require()` in `scripts/**`. That would be a separate + `lint-against-cjs-in-esm-scripts` convoy. Surfaced here so future + helper scripts don't re-introduce the bug. + +## Out of scope (queued follow-ups) + +- `lint-against-cjs-in-esm-scripts` (NEW, P3 polish): add ESLint rule + to prevent any future `require()` in `scripts/**` once `package.json` + has `"type": "module"`. +- `add-neon-return-shape-rule` (P3 polish, surfaced 2026-05-25 by + PR #24): codify the `neon()` vs `@vercel/postgres` return-shape + difference as a rule. **May fold into `single-sql-client`** which + would eliminate the dual-client problem entirely. + +## Owns + +Parent (single-file proven-pattern fix; no architect or implementer +subagent required). + +## As-shipped + +(To be appended post-merge.) diff --git a/scripts/reset-db.js b/scripts/reset-db.js index 79338bd..c3961ab 100644 --- a/scripts/reset-db.js +++ b/scripts/reset-db.js @@ -2,24 +2,47 @@ /** * Reset Database Script - * - * This script drops and recreates all tables in your Neon database. + * + * This script drops and recreates all tables in your Neon database, then + * seeds an admin user. DESTRUCTIVE — never run against production. + * + * Required env (in .env.local): + * POSTGRES_URL — Neon connection string + * ADMIN_INITIAL_PASSWORD — strong password for the seeded admin user + * (generate with `openssl rand -base64 24`) + * + * Mirrors the post-`drop-public-setup` shape of `setup-neon-db.js` + * (commit b63b509) — same ESM imports, same fail-loud env-var check, + * same no-password-echo convention. Convoy: `fix-reset-db-script` + * (2026-05-25). */ -// Load environment variables from .env.local -require('dotenv').config({ path: '.env.local' }); +import dotenv from 'dotenv'; +dotenv.config({ path: '.env.local' }); -const { neon } = require('@neondatabase/serverless'); +import { neon } from '@neondatabase/serverless'; +import bcrypt from 'bcryptjs'; async function resetDatabase() { + const adminPassword = process.env.ADMIN_INITIAL_PASSWORD; + if (!adminPassword || !adminPassword.trim()) { + console.error( + '❌ ADMIN_INITIAL_PASSWORD environment variable is not set.\n' + + '\n' + + ' Set it in .env.local for local dev, or as a CI secret if you run reset from CI.\n' + + ' Generate a strong password with: openssl rand -base64 24\n' + + ' See README.md → "First-time admin setup" for the full flow.\n' + ); + process.exit(1); + } + const sql = neon(process.env.POSTGRES_URL); try { console.log('✅ Connecting to Neon database...'); - // Drop all tables in correct order (due to foreign key constraints) console.log('🗑️ Dropping existing tables...'); - + await sql`DROP TABLE IF EXISTS deck_cards CASCADE`; await sql`DROP TABLE IF EXISTS decks CASCADE`; await sql`DROP TABLE IF EXISTS collection_cards CASCADE`; @@ -27,10 +50,9 @@ async function resetDatabase() { await sql`DROP TABLE IF EXISTS user_cards CASCADE`; await sql`DROP TABLE IF EXISTS cards CASCADE`; await sql`DROP TABLE IF EXISTS users CASCADE`; - + console.log('✅ Dropped all tables'); - // Create tables await sql` CREATE TABLE users ( id SERIAL PRIMARY KEY, @@ -138,13 +160,12 @@ async function resetDatabase() { `; console.log('✅ Created deck_cards table'); - // Create admin user - const bcrypt = require('bcryptjs'); - const hashedPassword = await bcrypt.hash('admin123', 12); - + const hashedPassword = await bcrypt.hash(adminPassword, 12); + await sql` - INSERT INTO users (email, password, role) + INSERT INTO users (email, password, role) VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'}) + ON CONFLICT (email) DO NOTHING `; console.log('✅ Created admin user'); @@ -152,13 +173,11 @@ async function resetDatabase() { console.log(''); console.log('📋 Database Details:'); console.log(' Database: Neon PostgreSQL'); - console.log(' Admin User: admin@deckhearth.com'); - console.log(' Admin Password: admin123'); - + console.log(' Admin user ready (email: admin@deckhearth.com)'); } catch (error) { console.error('❌ Database reset failed:', error.message); process.exit(1); } } -resetDatabase(); \ No newline at end of file +resetDatabase();