Mid-convoy discovery: brief 1's implementer confirmed scripts/setup-neon-db.js does not actually run on Node 22.x because bump-next-js added "type": "module" to package.json but the seed script still uses CJS require() calls. Throws ReferenceError immediately on `npm run setup-db`. Architect's original "Anything flagged but not acted on" #1 claim that "it runs successfully today under Node 22" was incorrect for Node 22.14.0. Decision D (ratified by user 2026-05-23): expand convoy to include brief 2 rather than queue a separate convert-setup-db-to-esm follow-up. Rationale: brief 1's env-var gate is theatrical security on a script no operator can execute; the CJS→ESM conversion is mechanical (~6 LOC, same file, no functional changes); splitting into two convoys creates a regression window where operators on Node 22.x cannot bootstrap a database. Brief 2 scope: pure module-system conversion in scripts/setup-neon-db.js: - require('dotenv').config(...) → import dotenv + dotenv.config(...) - require('@neondatabase/serverless') → import { neon } - inline require('bcryptjs') hoisted to top-of-file import - no functional changes; same DDL, same env-var gate, same console.logs Verification: smoke must now show npm run setup-db actually executes (no ReferenceError); brief-1 env-var gate must still fire as documented; all 16 vitest tests must still pass. Updated: - .convoys/drop-public-setup.md Decomposition (brief 2 added, depends_on brief 1) - .convoys/drop-public-setup.md slice_dependencies YAML - .convoys/drop-public-setup.md § Decisions (added Decision D) - .convoys/drop-public-setup.md § Anything flagged but not acted on (item #1 marked resolved by brief 2) addresses: P0 #3 from .convoys/ship-readiness.md + Node 22.x compat parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com>
11 KiB
| convoy | brief_number | depends_on | files | ||||
|---|---|---|---|---|---|---|---|
| drop-public-setup | 2 |
|
|
Brief 2: Convert scripts/setup-neon-db.js from CJS to ESM
Goal (1 sentence)
Convert scripts/setup-neon-db.js from CommonJS (require()) to native ES modules (import) so it runs on Node 22.x where package.json has "type": "module"; no functional changes — pure module-system conversion.
Why this brief exists (context)
bump-next-js added "type": "module" to package.json so ESLint v9's flat config (eslint.config.mjs) could be picked up under the default loader semantics. As a side effect, every untyped .js file in the repo is now treated as ESM by Node. scripts/setup-neon-db.js still uses require() and immediately throws on Node 22.x:
ReferenceError: require is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension
and '/.../package.json' contains "type": "module".
Brief 1 added an ADMIN_INITIAL_PASSWORD env-var gate to this script, but the gate is theatrical until the script actually executes. This brief makes the script executable so brief 1's hardening takes effect.
The CJS→ESM conversion was originally flagged in .convoys/drop-public-setup.md § "Anything flagged but not acted on" #1 as a separate follow-up. After mid-convoy discovery (the implementer for brief 1 confirmed the script is non-functional today on Node 22.14.0), the scope was expanded to land both fixes together. See .convoys/drop-public-setup.md § Decisions D for the ratification.
Files in scope (do not edit anything else)
scripts/setup-neon-db.js— modified (module-system conversion only)
Do NOT edit README.md, package.json, package-lock.json, any other scripts/*.js, or any file under lib/, pages/, components/, .github/, or .cursor/. Brief 1's README changes already shipped on the convoy branch; do not touch them.
Conventions to follow
- No functional changes. This brief is a 1:1 module-system conversion. Same logic, same DDL, same
process.exit(1), same env-var gate, sameconsole.loglines. The diff should be 4-6 lines ofrequire→importplus thedotenvinvocation change. - No new dependencies.
bcryptjs,dotenv, and@neondatabase/serverlessare already installed (verified bynpm run setup-dbfailing onrequirerather than on missing modules). - Match brief 1's verbatim env-var-check block. Do not touch the
if (!adminPassword || !adminPassword.trim())block from brief 1 — only therequire()calls around it change. - No top-level
await. ESM supports it, but the existingsetupNeonDatabase()bottom-of-file invocation is intentionally fire-and-forget (the function does its owntry/catch+process.exit(1)). Keep that pattern; do not introduceawait setupNeonDatabase(). - Move
bcryptimport to the top of the file. The CJS version uses inlineconst bcrypt = require('bcryptjs')mid-function. ESM has no equivalent of laterequire()— allimportstatements must be at the top. This is a structural requirement of ESM, not a stylistic preference. - No
__dirnameor__filename. The script does not currently use either, so no ESM equivalents (import.meta.url,fileURLToPath) are needed. - No
.cjsrename. The right answer is real ESM, not bypassing"type": "module"with a.cjsextension. A.cjsrename would also break thesetup-dbnpm script (node scripts/setup-neon-db.js) without a correspondingpackage.jsonedit, which is out of scope here.
Acceptance criteria
scripts/setup-neon-db.js
- Replace the
dotenvinvocation (currently line 11):
// before:
require('dotenv').config({ path: '.env.local' });
// after:
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
- Replace the
neonimport (currently line 13):
// before:
const { neon } = require('@neondatabase/serverless');
// after:
import { neon } from '@neondatabase/serverless';
- Hoist the
bcryptjsimport to the top of the file, alongside the other imports. Remove the inlineconst bcrypt = require('bcryptjs');from insidesetupNeonDatabase()(currently line 142):
// at the top of the file, after the dotenv block:
import bcrypt from 'bcryptjs';
// inside setupNeonDatabase(), delete this line:
const bcrypt = require('bcryptjs');
- Final import-block order at the top of the file (after the shebang + JSDoc header):
#!/usr/bin/env node
/**
* Neon Database Setup Script
* ... (existing JSDoc) ...
*/
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
import { neon } from '@neondatabase/serverless';
import bcrypt from 'bcryptjs';
async function setupNeonDatabase() {
// ... env-var check from brief 1, unchanged ...
// ... rest of function, with the inline require deleted ...
}
setupNeonDatabase();
Note: placing dotenv.config(...) between the import statements is intentional — dotenv must run before any other module reads process.env. ESM hoists import declarations but executes them in source order, so the import dotenv from 'dotenv' declaration hoists to the top, the dotenv.config({...}) call executes after the import resolves, and the subsequent import { neon } / import bcrypt resolve after that. If the linter complains about "imports not grouped together," resolve by moving the dotenv.config({...}) call into a separate side-effect import (import 'dotenv/config' will not work here because we need the custom .env.local path).
-
The env-var check from brief 1 is unchanged. Lines 15-26 (the
const adminPassword = ...block, theif (!adminPassword || !adminPassword.trim())block, and theprocess.exit(1)call) stay byte-identical. This brief only touches the import block at the top of the file and the inlinerequire('bcryptjs')deletion. -
The DDL,
INSERT,ON CONFLICTclause, success summaryconsole.loglines, andtry/catch/process.exit(1)are all unchanged. Pure module-system conversion. -
setupNeonDatabase();at the bottom of the file stays as-is (bare call, noawait).
Verification
- Smoke: fail-loud path. With
ADMIN_INITIAL_PASSWORDunset:
unset ADMIN_INITIAL_PASSWORD
npm run setup-db
echo "exit=$?"
Expected:
-
Exit code:
1 -
Stderr contains the brief-1 actionable error message (names
ADMIN_INITIAL_PASSWORD, points at.env.local, suggestsopenssl rand -base64 24) -
Stdout does NOT contain
✅ Connecting to Neon database...— the env-var check fires beforeneon(...), just like in the brief-1 smoke -
No
ReferenceError: require is not defined— this is the bug brief 2 fixes; its absence is the primary signal -
No
SyntaxError: Cannot use import statement outside a module— would indicate the conversion picked the wrong direction -
Smoke: happy path. With the env var set:
export ADMIN_INITIAL_PASSWORD=brief-2-smoke-temporary-pw
npm run setup-db
echo "exit=$?"
Expected:
-
Exit code:
0 -
Stdout contains
✅ Connecting to Neon database...and✅ Created admin user(or the equivalent for an already-existing admin row —ON CONFLICT DO NOTHINGkeeps the script idempotent) -
Stdout does NOT contain
admin123orbrief-2-smoke-temporary-pw(R3 from brief 1 — credentials never echoed to stdout) -
No
ReferenceError, noSyntaxError, noimport/requirecomplaints -
Lint.
npm run lintexits 0 (or matches the existing baseline — pre-existing lint errors are fine; do not introduce new ones). -
Tests.
npm run test:runis green. All 16 existing vitest tests should still pass — this brief does not touch any file they cover. -
Grep sanity.
rg 'require\(' scripts/setup-neon-db.jsreturns zero hits after the conversion. -
Brief-1 smoke still passes. Re-run the brief-1 end-to-end login smoke against
npm run dev:
curl -sS -X POST http://localhost:3000/api/auth/login \
-H 'content-type: application/json' \
-d '{"email":"admin@tcgvault.com","password":"brief-2-smoke-temporary-pw"}'
Expected: same R1 case-A behavior as brief 1 (if the local admin row predates ADMIN_INITIAL_PASSWORD, the new password 401s and the old admin123 still 200s; if the admin row was newly created, the new password 200s with a JWT). Paste the result into the PR description.
Pre-merge checklist (paste into PR description, alongside brief 1's checklist)
- R1 callout (carried over from brief 1): existing deployed admin rows with the weak
admin123hash are NOT rotated by this PR. Operators rotate manually after merge. - Brief 2 makes
npm run setup-dbexecutable again on Node 22.x. Before this brief, the script threwReferenceError: require is not defined in ES module scopeimmediately on invocation. After this brief, the env-var gate from brief 1 actually fires. - Reviewer ran the brief-2 fail-loud and happy-path smoke locally OR confirmed the PR description includes terminal output proving both paths.
Out of scope (do not do these)
- No
package.jsonchange."type": "module"stays; this brief makes the script align with that setting, not the other way around. - No edit to
scripts/reset-db.js,scripts/create-test-users.js, or any otherscripts/*.js. They have the same CJS-in-ESM bug, but converting all of them widens the diff and bleeds into the no-go-zones rule for historical scripts. Queueconvert-helper-scripts-to-esmas a separate convoy if/when those scripts need to run. - No edit to
README.md. Brief 1 owns the README changes; this brief is code-only. - No
.cjsrename. Real ESM only. - No top-level
awaitintroduction at the bottom of the file. - No new dependencies.
- No edit to
lib/,pages/,components/,.github/, or.cursor/. - No vitest tests. The smoke commands above are the verification; setup-db is not a runtime-auth surface (Decision C1 from brief 1 still applies — operational scripts use manual smoke, not unit tests).
Rationale (≤3 sentences)
bump-next-js's "type": "module" flag silently broke npm run setup-db on Node 22.x because the script still uses CJS require() calls. Brief 1's ADMIN_INITIAL_PASSWORD env-var gate is the right hardening but lands as theatrical security on a script no operator can actually execute — converting to native ESM imports here ensures the gate fires as intended and unblocks first-time DB setup on any Node 22+ environment (including Vercel's default). The diff is 4-6 lines of mechanical conversion with no functional changes, so review risk is minimal and the convoy stays a single coherent PR.