#!/usr/bin/env node // Idempotent production seed for the auth bootstrap. // // What it does (each step is independently idempotent and safe to re-run): // 1. Inserts a set of initial invite codes into the `invite_codes` table. // Sources, in priority order: // a. --invite-codes CLI flag // b. $INITIAL_INVITE_CODES (preferred for deploy) // c. $INVITE_CODES (reuses the runtime env-derived list) // Existing rows are left untouched (ON CONFLICT DO NOTHING). // // 2. Optionally creates an initial admin user when ALL of: // $INITIAL_ADMIN_EMAIL, $INITIAL_ADMIN_USERNAME, $INITIAL_ADMIN_PASSWORD // are set. If a user with the same email OR username already exists, // the step is skipped (no overwrite). Password must be >= 8 chars. // // Usage: // DATABASE_URL=... \ // INITIAL_INVITE_CODES="INVITE-A,INVITE-B" \ // INITIAL_ADMIN_EMAIL=admin@example.com \ // INITIAL_ADMIN_USERNAME=admin \ // INITIAL_ADMIN_PASSWORD='strong-password' \ // node artifacts/api-server/scripts/seed-prod.mjs // // Exit codes: // 0 - success (including "nothing to do") // 1 - misconfiguration / hard failure import pg from "pg"; import bcrypt from "bcrypt"; import { ulid } from "ulid"; function parseArgs(argv) { const out = {}; for (let i = 2; i < argv.length; i++) { const a = argv[i]; if (a === "--invite-codes") out.inviteCodes = argv[++i]; else if (a.startsWith("--invite-codes=")) out.inviteCodes = a.slice("--invite-codes=".length); else if (a === "--help" || a === "-h") out.help = true; } return out; } function parseList(raw) { return String(raw || "") .split(",") .map((s) => s.trim()) .filter(Boolean); } function defaultPeriodEnd(start = new Date()) { return new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000); } function defaultSettingsBlob() { return { appearance: { theme: "system", density: "comfortable", font_scale: 1.0, code_theme: "github-dark", }, language: "zh-CN", default_model_id: "mdl_claude-sonnet-4-6", send_on_enter: true, show_token_count: true, spellcheck: true, voice: { enabled: false, voice_id: null, speed: 1.0 }, data_controls: { chat_history_enabled: true, improve_model_with_my_data: false, auto_archive_after_days: 0, }, beta_features: { streaming: true, code_interpreter: false }, notifications: { job_completed: true, weekly_digest: false }, long_term_memory: { enabled: false, auto_extract: true, max_facts: 100, max_tokens_per_turn: 2000, }, }; } async function main() { const args = parseArgs(process.argv); if (args.help) { process.stdout.write( "Usage: node artifacts/api-server/scripts/seed-prod.mjs [--invite-codes A,B,C]\n", ); return; } const url = process.env.DATABASE_URL; if (!url) { console.error("seed-prod: DATABASE_URL is required"); process.exit(1); } const codes = parseList( args.inviteCodes || process.env.INITIAL_INVITE_CODES || process.env.INVITE_CODES, ); const adminEmail = String(process.env.INITIAL_ADMIN_EMAIL || "").trim().toLowerCase(); const adminUsername = String(process.env.INITIAL_ADMIN_USERNAME || "").trim().toLowerCase(); const adminPassword = String(process.env.INITIAL_ADMIN_PASSWORD || ""); const wantsAdmin = Boolean(adminEmail || adminUsername || adminPassword); if (!codes.length && !wantsAdmin) { console.warn( "seed-prod: no INITIAL_INVITE_CODES / INVITE_CODES and no INITIAL_ADMIN_* set; nothing to do.", ); return; } const pool = new pg.Pool({ connectionString: url }); const client = await pool.connect(); try { // 1) invite codes if (codes.length) { let inserted = 0; let skipped = 0; for (const code of codes) { const r = await client.query( `INSERT INTO invite_codes (code) VALUES ($1) ON CONFLICT (code) DO NOTHING RETURNING code`, [code], ); if (r.rowCount === 1) inserted++; else skipped++; } console.log( `seed-prod: invite_codes — inserted=${inserted} skipped=${skipped} total=${codes.length}`, ); } else { console.log("seed-prod: no invite codes provided; skipping invite seed"); } // 2) optional admin if (wantsAdmin) { if (!adminEmail || !adminUsername || !adminPassword) { console.error( "seed-prod: INITIAL_ADMIN_EMAIL, INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD must all be set together.", ); process.exit(1); } if (adminPassword.length < 8) { console.error("seed-prod: INITIAL_ADMIN_PASSWORD must be at least 8 characters."); process.exit(1); } const existing = await client.query( `SELECT id, email, username FROM users WHERE email = $1 OR username = $2 LIMIT 1`, [adminEmail, adminUsername], ); if (existing.rowCount && existing.rowCount > 0) { const row = existing.rows[0]; console.log( `seed-prod: admin user already exists (id=${row.id} email=${row.email} username=${row.username}); not overwriting`, ); } else { const id = `usr_${ulid()}`; const now = new Date(); const passwordHash = await bcrypt.hash(adminPassword, 12); await client.query("BEGIN"); try { await client.query( `INSERT INTO users (id, email, username, password_hash, display_name, role, plan, tokens_limit, tokens_used, period_start, period_end, created_at) VALUES ($1,$2,$3,$4,$5,'admin','free',5000000,0,$6,$7,$6)`, [ id, adminEmail, adminUsername, passwordHash, adminUsername, now, defaultPeriodEnd(now), ], ); await client.query( `INSERT INTO user_settings (user_id, data) VALUES ($1, $2::jsonb) ON CONFLICT (user_id) DO NOTHING`, [id, JSON.stringify(defaultSettingsBlob())], ); await client.query( `INSERT INTO custom_instructions (user_id, about_me, response_style, enabled) VALUES ($1, '', '', true) ON CONFLICT (user_id) DO NOTHING`, [id], ); await client.query("COMMIT"); console.log( `seed-prod: created admin user id=${id} email=${adminEmail} username=${adminUsername}`, ); } catch (err) { await client.query("ROLLBACK"); throw err; } } } else { console.log("seed-prod: no INITIAL_ADMIN_* provided; skipping admin user"); } } finally { client.release(); await pool.end(); } } main().catch((err) => { console.error("seed-prod: failed:", err?.stack || err?.message || err); process.exit(1); });