@rebasepro/cli 0.25.0-canary.g70dad01 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -216,7 +216,7 @@ async function resetPassword(rawArgs) {
216
216
  if (!tsxBin) exitDependenciesNotInstalled(projectRoot);
217
217
  let prepared;
218
218
  try {
219
- const { ensureDockerDatabase } = await import("./db-4NXrKl5W.js");
219
+ const { ensureDockerDatabase } = await import("./db-CRNpA5mH.js");
220
220
  await ensureDockerDatabase(projectRoot, rawArgs);
221
221
  const { prepareDatabaseEnv } = await import("./prepare-DdnU70z8.js");
222
222
  prepared = await prepareDatabaseEnv(projectRoot, { onProgress: (message) => console.log(chalk.gray(` ${message}`)) });
@@ -225,7 +225,7 @@ async function resetPassword(rawArgs) {
225
225
  console.error(err instanceof Error ? err.message : String(err));
226
226
  process.exit(1);
227
227
  }
228
- const { databaseUrlOf } = await import("./db-4NXrKl5W.js");
228
+ const { databaseUrlOf } = await import("./db-CRNpA5mH.js");
229
229
  const url = databaseUrlOf(prepared, readEnvFile(projectRoot));
230
230
  if (url) {
231
231
  const failure = await databaseOpenFailure(url, prepared.database.source);
@@ -300,7 +300,7 @@ async function databaseOpenFailure(url, source) {
300
300
  await client.query("SELECT 1");
301
301
  return null;
302
302
  } catch (error) {
303
- const { connectionErrorText, connectionFailureBanner, connectionRefusedFixes } = await import("./connection-banner-CCIyvjpW.js").then((n) => n.r);
303
+ const { connectionErrorText, connectionFailureBanner, connectionRefusedFixes } = await import("./connection-banner-DbLJzWf4.js").then((n) => n.r);
304
304
  const consequence = "The password was not reset.";
305
305
  return connectionFailureBanner(url, error, {
306
306
  consequence,
@@ -471,4 +471,4 @@ ${chalk.green.bold("Examples")}
471
471
  //#endregion
472
472
  export { AUTH_SUBCOMMANDS, authCommand };
473
473
 
474
- //# sourceMappingURL=auth-DKi6-KQG.js.map
474
+ //# sourceMappingURL=auth-Vl2hFKXP.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-DKi6-KQG.js","names":[],"sources":["../src/commands/auth.ts"],"sourcesContent":["/**\n * CLI command: rebase auth <action>\n *\n * Subcommands:\n * reset-password — Reset a user's password\n */\nimport chalk from \"chalk\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport { spawn } from \"child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport {\n requireProjectRoot,\n requireBackendDir,\n findEnvFile,\n readEnvFile,\n resolveTsx,\n exitDependenciesNotInstalled\n} from \"../utils/project\";\nimport { parseEnvBoolean } from \"@rebasepro/types\";\nimport { unknownCommand } from \"../utils/unknown-command\";\nimport { redactConnectionPasswords } from \"../utils/redact\";\nimport type { PreparedDatabase } from \"../dev-db/prepare\";\nimport type { DevDatabaseSource } from \"../dev-db/resolve\";\n\n/** The backend was reached and refused: not a reason to fall back to the database. */\nclass ApiAnswered extends Error {}\n\n/** Everything the switch below dispatches, for the did-you-mean. */\nexport const AUTH_SUBCOMMANDS = [\"reset-password\"] as const;\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { resolveBackendAddress } from \"./api-keys\";\n\n/** A user as the admin API returns it, reduced to what this command needs. */\nexport interface ResolvedUser {\n id: string;\n email: string;\n}\n\n/**\n * Pick the user with exactly this email out of a search response.\n *\n * `/api/admin/users?search=` is an `ILIKE '%…%'` over email **or display\n * name**, ordered by role count descending. This used to take row `[0]` and\n * reset it, then print the email it had been *given* as confirmation — so two\n * ordinary situations ended in a successful-looking reset of somebody else's\n * account:\n *\n * - a substring collision: `bob@example.com` also matches\n * `robert.bob@example.com`;\n * - a display name, which is user-controlled and accepted up to 255\n * characters with no constraint on its content, containing an address\n * belonging to someone else.\n *\n * The ordering makes it worse rather than better — `array_length(roles) DESC\n * NULLS LAST` puts the most privileged match first, so the account most likely\n * to be reset by mistake is an admin's.\n *\n * Returns `undefined` when nothing matched exactly, which the caller reports\n * rather than falling through to a guess. The direct-database fallback below\n * has always matched with `eq(usersTable.email, email)`; this is the same\n * definition, so the command no longer resets different accounts depending on\n * whether the backend happened to be running.\n */\nexport function selectUserForEmail(payload: unknown, email: string): ResolvedUser | undefined {\n const wanted = email.trim().toLowerCase();\n if (!wanted) return undefined;\n\n const rows: unknown[] = Array.isArray(payload)\n ? payload\n : (payload && typeof payload === \"object\" && Array.isArray((payload as { users?: unknown }).users)\n ? (payload as { users: unknown[] }).users\n : []);\n\n for (const row of rows) {\n if (!row || typeof row !== \"object\") continue;\n const record = row as Record<string, unknown>;\n const rowEmail = typeof record.email === \"string\" ? record.email.trim().toLowerCase() : undefined;\n if (!rowEmail || rowEmail !== wanted) continue;\n\n const id = typeof record.id === \"string\"\n ? record.id\n : (typeof record.uid === \"string\" ? record.uid : undefined);\n if (!id) continue;\n\n return { id, email: record.email as string };\n }\n\n return undefined;\n}\n\nexport async function authCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void> {\n // `--help` is answered here and never reaches a handler. `cli.ts` only\n // rewrites the subcommand to `\"--help\"` when no subcommand was named, so\n // `rebase auth reset-password --help` used to *run the reset*: `--help`\n // became the email, the backend was contacted, `.tmp-reset-password.ts` was\n // written into the user's `backend/`, and a database UPDATE ran for a user\n // named `--help`. A flag whose whole job is to print text cannot be allowed\n // to reach code that writes.\n if (!subcommand || subcommand === \"--help\" || wantsHelp(rawArgs)) {\n printAuthHelp();\n return;\n }\n\n switch (subcommand) {\n case \"reset-password\":\n await resetPassword(rawArgs);\n break;\n default:\n unknownCommand(subcommand, AUTH_SUBCOMMANDS, \"auth\");\n }\n}\n\n/**\n * The flags `rebase auth reset-password` takes.\n *\n * `-p` was advertised in this command's own help and never declared here, so\n * `arg` — running permissively — pushed it into the positionals and the value\n * *after* it shifted out of reach: anyone following the help set the account's\n * password to the two-character string `-p`. Declared now, and `auth.test.ts`\n * asserts that the help and this spec list the same aliases.\n */\nexport const RESET_PASSWORD_FLAGS = {\n \"--email\": String,\n \"--password\": String,\n \"-e\": \"--email\",\n \"-p\": \"--password\"\n} as const;\n\n/**\n * Which account, and which password, this invocation names.\n *\n * Both may still be absent — the caller reports a missing email — but neither\n * can be a flag. `parseCommandArgs` parses the whole line strictly, so an\n * undeclared flag is an error rather than a positional. That is what stops\n * `rebase auth reset-password bob@example.com --debug` from setting Bob's\n * password to `--debug`, which is the flag the CLI itself prints after every\n * failure as the thing to re-run with.\n *\n * Exported so its tests can drive the real parser rather than a copy of it.\n */\nexport function resolveResetPasswordArgs(rawArgs: string[]): {\n email?: string;\n password?: string;\n} {\n const { flags, positionals } = parseCommandArgs({\n spec: RESET_PASSWORD_FLAGS,\n rawArgs,\n commandWords: 2,\n command: \"auth reset-password\",\n maxPositionals: 2\n });\n\n // Both spellings are supported: `<email> [password]` and `--email/--password`.\n return {\n email: flags[\"--email\"] || positionals[0],\n password: flags[\"--password\"] || positionals[1]\n };\n}\n\n/**\n * A password for a reset that was not given one.\n *\n * This used to be a constant, and `--help` printed it as the default. Reset is\n * the documented way back into an account nobody can sign in to — an admin,\n * usually — so the recovery path set every such account to a fixed string that\n * ships inside a public repository and a published npm package, and left it\n * there until somebody remembered to change it.\n *\n * base64url of 18 random bytes: 24 characters, ~144 bits, no shell-quoting\n * hazard, and nothing that reads like a placeholder somebody might keep.\n */\nexport function generatePassword(): string {\n return randomBytes(18).toString(\"base64url\");\n}\n\n/**\n * What a refusing backend said, from its `{ error: { message, code } }` body.\n *\n * The body used to be printed as it came, so a weak password read\n * `{\"error\":{\"message\":\"…\",\"code\":\"…\"}}`. A body that is not the envelope is\n * still shown, since it is the only account of the refusal there is.\n */\nasync function refusalReason(response: Response): Promise<string> {\n const text = await response.text().catch(() => \"\");\n try {\n const parsed: unknown = JSON.parse(text);\n const error = parsed && typeof parsed === \"object\" && \"error\" in parsed ? parsed.error : undefined;\n if (error && typeof error === \"object\" && \"message\" in error && typeof error.message === \"string\") {\n const code = \"code\" in error && typeof error.code === \"string\" ? ` (${error.code})` : \"\";\n return `${error.message}${code}`;\n }\n } catch {\n // Not JSON: the text is the answer.\n }\n return text.trim() || `${response.status} ${response.statusText}`.trim();\n}\n\nasync function resetPassword(rawArgs: string[]): Promise<void> {\n const { email, password: providedPassword } = resolveResetPasswordArgs(rawArgs);\n // Generated once, so both reset paths set and report the same thing.\n const wasGenerated = !providedPassword;\n const newPassword = providedPassword || generatePassword();\n\n if (!email) {\n console.error(chalk.red(\"✗ Email is required.\"));\n console.log(\"\");\n console.log(chalk.gray(\" Usage: rebase auth reset-password <email> [new-password]\"));\n console.log(chalk.gray(\" rebase auth reset-password --email user@example.com --password NewPass123!\"));\n process.exit(1);\n }\n\n const projectRoot = requireProjectRoot();\n\n // 1. Try API-first reset\n // Was a single-key regex, which did not match `export REBASE_SERVICE_KEY=…`\n // and kept a trailing `# comment` in the value. The command then fell\n // through to the direct-database path as though no key were configured.\n const envFile = findEnvFile(projectRoot);\n const envServiceKey: string | undefined = readEnvFile(projectRoot).REBASE_SERVICE_KEY;\n\n let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;\n\n const statePath = path.join(projectRoot, \".rebase\", \"state.json\");\n if (fs.existsSync(statePath)) {\n try {\n const state = JSON.parse(fs.readFileSync(statePath, \"utf8\")) as Record<string, unknown>;\n if (state && typeof state === \"object\" && typeof state.serviceKey === \"string\" && !serviceKey) {\n serviceKey = state.serviceKey;\n }\n } catch {\n // Ignore\n }\n }\n\n // Where the service key may go: a REBASE_BASE_URL the developer named, or\n // this project's backend while the pid that recorded it is alive — see\n // `resolveBackendAddress`. `.rebase/state.json`'s address on its own, and\n // `.rebase-dev-url`, outlive a killed `rebase dev`, and the port they name\n // may be another server's by now. With neither, the reset goes to the\n // database directly.\n const baseUrl = resolveBackendAddress(readEnvFile(projectRoot), projectRoot).url;\n\n if (baseUrl && serviceKey) {\n console.log(\"Trying API-first reset via running backend...\");\n try {\n const finalPass = newPassword;\n const cleanBaseUrl = baseUrl.replace(/\\/+$/, \"\");\n // Not `limit=1`: the search is fuzzy and ordered by role count, so\n // the exact match is not necessarily first — asking for one row can\n // make it unreachable. Ask for a page, then match exactly.\n const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=50`;\n const searchRes = await fetch(searchUrl, {\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Accept\": \"application/json\"\n }\n });\n if (!searchRes.ok) {\n throw new ApiAnswered(`Failed to list users: ${searchRes.status} ${searchRes.statusText}`);\n }\n const searchData = await searchRes.json() as unknown;\n if (!searchData || typeof searchData !== \"object\") {\n throw new Error(\"Invalid response format from user search API.\");\n }\n\n const matched = selectUserForEmail(searchData, email);\n if (!matched) {\n throw new ApiAnswered(`No user has the email ${email}.`);\n }\n\n const resetUrl = `${cleanBaseUrl}/api/admin/users/${matched.id}/reset-password`;\n const resetRes = await fetch(resetUrl, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\"\n },\n body: JSON.stringify({ password: finalPass })\n });\n\n if (!resetRes.ok) {\n throw new ApiAnswered(`Password reset refused: ${await refusalReason(resetRes)}`);\n }\n\n console.log(\"API reset successful.\");\n console.log(chalk.bold(\" 🔑 Rebase Auth — Reset Password (via API)\"));\n console.log(\"\");\n // The address as stored, not as typed — they differ in case often\n // enough that echoing the input hides which account was touched.\n console.log(` ${chalk.gray(\"Email:\")} ${matched.email}`);\n // Echoed only when we invented it. A password the operator typed\n // is already theirs; printing it again only adds scrollback.\n console.log(` ${chalk.gray(\"Password:\")} ${wasGenerated ? finalPass : \"*\".repeat(finalPass.length)}`);\n console.log(\"\");\n return;\n } catch (err) {\n const errMsg = err instanceof Error ? err.message : String(err);\n // The backend answered and said no — a password too weak, a user\n // it does not have. That answer stands: writing the hash straight\n // into the database instead skipped the strength rule it had just\n // applied, and ended no sessions. Only a backend that could not be\n // reached is a reason to go round it.\n if (err instanceof ApiAnswered) {\n console.error(chalk.red(`✗ ${errMsg}`));\n process.exitCode = 1;\n return;\n }\n console.warn(chalk.yellow(\"API reset failed, falling back to direct database update...\"));\n console.warn(chalk.gray(` Details: ${errMsg}`));\n }\n }\n\n // 2. Direct-DB Fallback\n const backendDir = requireBackendDir(projectRoot);\n const tsxBin = resolveTsx(projectRoot);\n\n if (!tsxBin) {\n exitDependenciesNotInstalled(projectRoot);\n }\n\n // The database the `db` family would use, resolved — and, for a remembered\n // compose container that is stopped, started — the same way. A stock\n // scaffold leaves DATABASE_URL commented out and runs on the managed\n // database, which only exists in a child's environment when something puts\n // it there — without this the script connected with libpq's defaults,\n // localhost:5432 as the OS user. An external DATABASE_URL adds nothing\n // here: the child already reads it.\n let prepared: PreparedDatabase;\n try {\n const { ensureDockerDatabase } = await import(\"./db\");\n await ensureDockerDatabase(projectRoot, rawArgs);\n const { prepareDatabaseEnv } = await import(\"../dev-db/prepare\");\n prepared = await prepareDatabaseEnv(projectRoot, {\n onProgress: (message) => console.log(chalk.gray(` ${message}`))\n });\n } catch (err) {\n console.error(chalk.red(\"✗ Direct database update failed.\"));\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n\n // Opened here first, so a database that does not answer gets the banner\n // every other command prints — not the script's raw error and stack — and\n // no script is written for it.\n const { databaseUrlOf } = await import(\"./db\");\n const url = databaseUrlOf(prepared, readEnvFile(projectRoot));\n if (url) {\n const failure = await databaseOpenFailure(url, prepared.database.source);\n if (failure) {\n for (const line of failure) console.error(line);\n process.exit(1);\n }\n }\n\n try {\n const env: Record<string, string> = { ...process.env as Record<string, string> };\n if (envFile) {\n env.DOTENV_CONFIG_PATH = envFile;\n }\n env.REBASE_RESET_EMAIL = email;\n env.REBASE_RESET_PASSWORD = newPassword;\n env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, \".env\");\n Object.assign(env, prepared.env);\n\n const scriptContent = resetPasswordScript(wasGenerated, parseEnvBoolean(process.env.REBASE_DEBUG) === true);\n\n const tmpScriptPath = path.join(backendDir, \".tmp-reset-password.ts\");\n fs.writeFileSync(tmpScriptPath, scriptContent, \"utf-8\");\n\n console.log(\"\");\n console.log(chalk.bold(\" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)\"));\n console.log(\"\");\n console.log(` ${chalk.gray(\"Email:\")} ${email}`);\n console.log(` ${chalk.gray(\"Database:\")} ${prepared.description}`);\n if (!wasGenerated) {\n console.log(` ${chalk.gray(\"Password:\")} ${\"*\".repeat(newPassword.length)}`);\n }\n console.log(\"\");\n\n const child = spawn(tsxBin, [tmpScriptPath], {\n cwd: backendDir,\n stdio: \"inherit\",\n env\n });\n\n // The script is written into the user's backend directory, so every\n // exit has to remove it. Without an `error` handler a failed spawn\n // raises an unhandled event, the process dies before `close`, and\n // `.tmp-reset-password.ts` is left behind to be committed.\n const cleanup = () => {\n try { fs.unlinkSync(tmpScriptPath); } catch { /* already gone */ }\n };\n\n return new Promise((resolve) => {\n child.on(\"error\", (err) => {\n cleanup();\n console.error(chalk.red(\"✗ Could not run the reset script.\"));\n console.error(chalk.gray(` ${err.message}`));\n process.exit(1);\n });\n child.on(\"close\", (code) => {\n cleanup();\n if (code !== 0) {\n process.exit(code ?? 1);\n }\n resolve();\n });\n });\n } catch (err) {\n console.error(chalk.red(\"✗ Direct database update failed.\"));\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n}\n\n/**\n * Why the database the fallback would reset in cannot be opened, as the lines\n * to print — or null when it opens.\n *\n * A stopped database, refused credentials and a database name the server does\n * not have get the boxed banner the other commands print; anything else is one\n * sentence. Either way the password was not reset and no script was written.\n */\nasync function databaseOpenFailure(url: string, source: DevDatabaseSource): Promise<string[] | null> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: url, connectionTimeoutMillis: 10_000 });\n client.on(\"error\", () => undefined);\n try {\n await client.connect();\n await client.query(\"SELECT 1\");\n return null;\n } catch (error) {\n const { connectionErrorText, connectionFailureBanner, connectionRefusedFixes } =\n await import(\"../dev-db/connection-banner\");\n const consequence = \"The password was not reset.\";\n return connectionFailureBanner(url, error, { consequence, fixes: connectionRefusedFixes(source) }) ?? [\n chalk.red(`✗ Could not open the database: ${redactConnectionPasswords(connectionErrorText(error))}`),\n chalk.gray(` ${consequence}`)\n ];\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n/**\n * The script the direct-database fallback runs under the project's own tsx, so\n * it resolves the project's `@rebasepro/server-postgres` and schema.\n *\n * It ends the account's sessions in the transaction that sets the password,\n * as every door through the API does (`replaceUserPassword`): the refresh\n * tokens and outstanding reset links go, and `tokens_valid_after` voids the\n * access tokens already handed out. An operator resetting a phished account\n * with the backend down is the case this exists for, and a reset that left the\n * attacker's refresh token minting access tokens did not recover anything.\n *\n * It exits 0 only when a row was updated: a missing user and a thrown error are\n * both exit 1. `echoPassword` prints the new password, for a generated one;\n * `showStack` prints a failure's stack rather than its sentence (`--debug`).\n *\n * Exported so its tests can run it.\n */\nexport function resetPasswordScript(echoPassword: boolean, showStack = false): string {\n return `\nimport { createPostgresDatabaseConnection } from \"@rebasepro/server-postgres\";\nimport { hashPassword } from \"@rebasepro/server\";\nimport { getTableConfig } from \"drizzle-orm/pg-core\";\nimport * as dotenv from \"dotenv\";\nimport path from \"path\";\nimport fs from \"fs\";\n\ndotenv.config({ path: process.env.REBASE_ENV_FILE_PATH, quiet: true });\n\n// Lowercased, as every address is stored (\\`normalizeEmail\\`): matched as\n// typed, \"Ops@Acme.test\" found no row.\nconst email = process.env.REBASE_RESET_EMAIL!.trim().toLowerCase();\nconst newPassword = process.env.REBASE_RESET_PASSWORD!;\n\nconst quote = (identifier: string) => '\"' + identifier.replace(/\"/g, '\"\"') + '\"';\n\nasync function resetPassword() {\n const { pool } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);\n const hash = await hashPassword(newPassword);\n\n let usersTable;\n try {\n const schemaPath = path.resolve(\"./src/schema.generated.ts\");\n if (fs.existsSync(schemaPath)) {\n const schema = await import(\"file://\" + schemaPath);\n usersTable = schema.users || schema.tables?.users;\n }\n } catch (e) {\n // ignore and fallback\n }\n\n if (!usersTable) {\n const pgServer = await import(\"@rebasepro/server-postgres\");\n usersTable = pgServer.users;\n }\n\n // The physical names, from the table object: the collection may map its\n // properties to other columns, and the statements below are plain SQL.\n const { name: tableName, schema: tableSchema } = getTableConfig(usersTable);\n const usersSchema = tableSchema || \"public\";\n const users = quote(usersSchema) + \".\" + quote(tableName);\n const idColumn = quote(usersTable.id.name);\n const emailColumn = quote(usersTable.email.name);\n const passwordColumn = quote((usersTable.passwordHash ?? usersTable.password_hash).name);\n // Where \\`ensureAuthTablesExist\\` puts the tables that hang off the users\n // table: beside it, or in \\`rebase\\` when it lives in \\`public\\`.\n const authSchema = usersSchema === \"public\" ? \"rebase\" : usersSchema;\n\n const client = await pool.connect();\n try {\n // A database nothing has booted against — or a managed one \\`db reset\\`\n // emptied — has no users table, and so no account to reset. Said as\n // that, not as pg's \\`relation … does not exist\\`.\n const usersTable = await client.query(\"SELECT to_regclass($1) IS NOT NULL AS present\", [users]);\n if (!usersTable.rows[0]?.present) {\n console.error(\"✗ \" + users + \" does not exist in this database, so it holds no account to reset.\");\n console.error(\" Nothing was changed. \\`rebase dev\\` creates it the first time it boots against this database.\");\n process.exit(1);\n }\n\n await client.query(\"BEGIN\");\n const updated = await client.query(\n \"UPDATE \" + users + \" SET \" + passwordColumn + \" = $1 WHERE \" + emailColumn + \" = $2 \" +\n \"RETURNING \" + idColumn + \" AS id, \" + emailColumn + \" AS email\",\n [hash, email]\n );\n if (updated.rows.length === 0) {\n await client.query(\"ROLLBACK\");\n // Nothing was updated, so nothing was reset. Exiting 0 here reported\n // success for a no-op, which is what a script would have believed.\n console.error(\"✗ User not found: \" + email);\n process.exit(1);\n }\n const uid = updated.rows[0].id;\n\n // A table or column the server has not created yet (it never booted\n // against this database) holds no session to end.\n const watermark = await client.query(\n \"SELECT 1 FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = 'tokens_valid_after'\",\n [usersSchema, tableName]\n );\n if (watermark.rows.length > 0) {\n await client.query(\"UPDATE \" + users + \" SET tokens_valid_after = NOW() WHERE \" + idColumn + \" = $1\", [uid]);\n }\n for (const table of [\"refresh_tokens\", \"password_reset_tokens\"]) {\n const qualified = quote(authSchema) + \".\" + quote(table);\n const present = await client.query(\"SELECT to_regclass($1) IS NOT NULL AS present\", [qualified]);\n if (present.rows[0]?.present) {\n await client.query(\"DELETE FROM \" + qualified + \" WHERE uid = $1\", [uid]);\n }\n }\n await client.query(\"COMMIT\");\n\n console.log(\"✅ Password reset for: \" + updated.rows[0].email);\n console.log(\" Every session of this account has ended.\");\n ${echoPassword ? 'console.log(\" Password: \" + newPassword);' : \"\"}\n process.exit(0);\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n throw error;\n } finally {\n client.release();\n }\n}\n\n// A thrown error is a failed reset, and exits like one — a refused connection\n// that exits 0 reads as a reset to anything checking the exit code. Said as a\n// sentence: the stack is the script's own, and only --debug asks for it.\nconst describe = (error: unknown): string => {\n if (error instanceof AggregateError && !error.message && error.errors.length > 0) return describe(error.errors[0]);\n const message = error instanceof Error ? error.message : String(error);\n const code = error && typeof error === \"object\" && \"code\" in error && typeof error.code === \"string\" ? error.code : \"\";\n return code && !message.includes(code) ? message + \" (\" + code + \")\" : message;\n};\nresetPassword().catch((error) => {\n ${showStack ? \"console.error(error);\" : \"console.error(\\\"✗ The password was not reset: \\\" + describe(error));\"}\n process.exit(1);\n});\n`;\n}\n\nfunction printAuthHelp() {\n console.log(`\n${chalk.bold(\"rebase auth\")} — Authentication management commands\n\n${chalk.green.bold(\"Usage\")}\n rebase auth ${chalk.blue(\"<command>\")} [options]\n\n${chalk.green.bold(\"Commands\")}\n ${chalk.blue.bold(\"reset-password\")} Reset a user's password\n\n${chalk.green.bold(\"reset-password Options\")}\n ${chalk.blue(\"--email, -e\")} User's email address\n ${chalk.blue(\"--password, -p\")} New password (default: one is generated and printed)\n\n${chalk.green.bold(\"Examples\")}\n rebase auth reset-password user@example.com\n rebase auth reset-password --email user@example.com --password MyNewPass!\n`);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth-Vl2hFKXP.js","names":[],"sources":["../src/commands/auth.ts"],"sourcesContent":["/**\n * CLI command: rebase auth <action>\n *\n * Subcommands:\n * reset-password — Reset a user's password\n */\nimport chalk from \"chalk\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport { spawn } from \"child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport {\n requireProjectRoot,\n requireBackendDir,\n findEnvFile,\n readEnvFile,\n resolveTsx,\n exitDependenciesNotInstalled\n} from \"../utils/project\";\nimport { parseEnvBoolean } from \"@rebasepro/types\";\nimport { unknownCommand } from \"../utils/unknown-command\";\nimport { redactConnectionPasswords } from \"../utils/redact\";\nimport type { PreparedDatabase } from \"../dev-db/prepare\";\nimport type { DevDatabaseSource } from \"../dev-db/resolve\";\n\n/** The backend was reached and refused: not a reason to fall back to the database. */\nclass ApiAnswered extends Error {}\n\n/** Everything the switch below dispatches, for the did-you-mean. */\nexport const AUTH_SUBCOMMANDS = [\"reset-password\"] as const;\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { resolveBackendAddress } from \"./api-keys\";\n\n/** A user as the admin API returns it, reduced to what this command needs. */\nexport interface ResolvedUser {\n id: string;\n email: string;\n}\n\n/**\n * Pick the user with exactly this email out of a search response.\n *\n * `/api/admin/users?search=` is an `ILIKE '%…%'` over email **or display\n * name**, ordered by role count descending. This used to take row `[0]` and\n * reset it, then print the email it had been *given* as confirmation — so two\n * ordinary situations ended in a successful-looking reset of somebody else's\n * account:\n *\n * - a substring collision: `bob@example.com` also matches\n * `robert.bob@example.com`;\n * - a display name, which is user-controlled and accepted up to 255\n * characters with no constraint on its content, containing an address\n * belonging to someone else.\n *\n * The ordering makes it worse rather than better — `array_length(roles) DESC\n * NULLS LAST` puts the most privileged match first, so the account most likely\n * to be reset by mistake is an admin's.\n *\n * Returns `undefined` when nothing matched exactly, which the caller reports\n * rather than falling through to a guess. The direct-database fallback below\n * has always matched with `eq(usersTable.email, email)`; this is the same\n * definition, so the command no longer resets different accounts depending on\n * whether the backend happened to be running.\n */\nexport function selectUserForEmail(payload: unknown, email: string): ResolvedUser | undefined {\n const wanted = email.trim().toLowerCase();\n if (!wanted) return undefined;\n\n const rows: unknown[] = Array.isArray(payload)\n ? payload\n : (payload && typeof payload === \"object\" && Array.isArray((payload as { users?: unknown }).users)\n ? (payload as { users: unknown[] }).users\n : []);\n\n for (const row of rows) {\n if (!row || typeof row !== \"object\") continue;\n const record = row as Record<string, unknown>;\n const rowEmail = typeof record.email === \"string\" ? record.email.trim().toLowerCase() : undefined;\n if (!rowEmail || rowEmail !== wanted) continue;\n\n const id = typeof record.id === \"string\"\n ? record.id\n : (typeof record.uid === \"string\" ? record.uid : undefined);\n if (!id) continue;\n\n return { id, email: record.email as string };\n }\n\n return undefined;\n}\n\nexport async function authCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void> {\n // `--help` is answered here and never reaches a handler. `cli.ts` only\n // rewrites the subcommand to `\"--help\"` when no subcommand was named, so\n // `rebase auth reset-password --help` used to *run the reset*: `--help`\n // became the email, the backend was contacted, `.tmp-reset-password.ts` was\n // written into the user's `backend/`, and a database UPDATE ran for a user\n // named `--help`. A flag whose whole job is to print text cannot be allowed\n // to reach code that writes.\n if (!subcommand || subcommand === \"--help\" || wantsHelp(rawArgs)) {\n printAuthHelp();\n return;\n }\n\n switch (subcommand) {\n case \"reset-password\":\n await resetPassword(rawArgs);\n break;\n default:\n unknownCommand(subcommand, AUTH_SUBCOMMANDS, \"auth\");\n }\n}\n\n/**\n * The flags `rebase auth reset-password` takes.\n *\n * `-p` was advertised in this command's own help and never declared here, so\n * `arg` — running permissively — pushed it into the positionals and the value\n * *after* it shifted out of reach: anyone following the help set the account's\n * password to the two-character string `-p`. Declared now, and `auth.test.ts`\n * asserts that the help and this spec list the same aliases.\n */\nexport const RESET_PASSWORD_FLAGS = {\n \"--email\": String,\n \"--password\": String,\n \"-e\": \"--email\",\n \"-p\": \"--password\"\n} as const;\n\n/**\n * Which account, and which password, this invocation names.\n *\n * Both may still be absent — the caller reports a missing email — but neither\n * can be a flag. `parseCommandArgs` parses the whole line strictly, so an\n * undeclared flag is an error rather than a positional. That is what stops\n * `rebase auth reset-password bob@example.com --debug` from setting Bob's\n * password to `--debug`, which is the flag the CLI itself prints after every\n * failure as the thing to re-run with.\n *\n * Exported so its tests can drive the real parser rather than a copy of it.\n */\nexport function resolveResetPasswordArgs(rawArgs: string[]): {\n email?: string;\n password?: string;\n} {\n const { flags, positionals } = parseCommandArgs({\n spec: RESET_PASSWORD_FLAGS,\n rawArgs,\n commandWords: 2,\n command: \"auth reset-password\",\n maxPositionals: 2\n });\n\n // Both spellings are supported: `<email> [password]` and `--email/--password`.\n return {\n email: flags[\"--email\"] || positionals[0],\n password: flags[\"--password\"] || positionals[1]\n };\n}\n\n/**\n * A password for a reset that was not given one.\n *\n * This used to be a constant, and `--help` printed it as the default. Reset is\n * the documented way back into an account nobody can sign in to — an admin,\n * usually — so the recovery path set every such account to a fixed string that\n * ships inside a public repository and a published npm package, and left it\n * there until somebody remembered to change it.\n *\n * base64url of 18 random bytes: 24 characters, ~144 bits, no shell-quoting\n * hazard, and nothing that reads like a placeholder somebody might keep.\n */\nexport function generatePassword(): string {\n return randomBytes(18).toString(\"base64url\");\n}\n\n/**\n * What a refusing backend said, from its `{ error: { message, code } }` body.\n *\n * The body used to be printed as it came, so a weak password read\n * `{\"error\":{\"message\":\"…\",\"code\":\"…\"}}`. A body that is not the envelope is\n * still shown, since it is the only account of the refusal there is.\n */\nasync function refusalReason(response: Response): Promise<string> {\n const text = await response.text().catch(() => \"\");\n try {\n const parsed: unknown = JSON.parse(text);\n const error = parsed && typeof parsed === \"object\" && \"error\" in parsed ? parsed.error : undefined;\n if (error && typeof error === \"object\" && \"message\" in error && typeof error.message === \"string\") {\n const code = \"code\" in error && typeof error.code === \"string\" ? ` (${error.code})` : \"\";\n return `${error.message}${code}`;\n }\n } catch {\n // Not JSON: the text is the answer.\n }\n return text.trim() || `${response.status} ${response.statusText}`.trim();\n}\n\nasync function resetPassword(rawArgs: string[]): Promise<void> {\n const { email, password: providedPassword } = resolveResetPasswordArgs(rawArgs);\n // Generated once, so both reset paths set and report the same thing.\n const wasGenerated = !providedPassword;\n const newPassword = providedPassword || generatePassword();\n\n if (!email) {\n console.error(chalk.red(\"✗ Email is required.\"));\n console.log(\"\");\n console.log(chalk.gray(\" Usage: rebase auth reset-password <email> [new-password]\"));\n console.log(chalk.gray(\" rebase auth reset-password --email user@example.com --password NewPass123!\"));\n process.exit(1);\n }\n\n const projectRoot = requireProjectRoot();\n\n // 1. Try API-first reset\n // Was a single-key regex, which did not match `export REBASE_SERVICE_KEY=…`\n // and kept a trailing `# comment` in the value. The command then fell\n // through to the direct-database path as though no key were configured.\n const envFile = findEnvFile(projectRoot);\n const envServiceKey: string | undefined = readEnvFile(projectRoot).REBASE_SERVICE_KEY;\n\n let serviceKey = process.env.REBASE_SERVICE_KEY || envServiceKey;\n\n const statePath = path.join(projectRoot, \".rebase\", \"state.json\");\n if (fs.existsSync(statePath)) {\n try {\n const state = JSON.parse(fs.readFileSync(statePath, \"utf8\")) as Record<string, unknown>;\n if (state && typeof state === \"object\" && typeof state.serviceKey === \"string\" && !serviceKey) {\n serviceKey = state.serviceKey;\n }\n } catch {\n // Ignore\n }\n }\n\n // Where the service key may go: a REBASE_BASE_URL the developer named, or\n // this project's backend while the pid that recorded it is alive — see\n // `resolveBackendAddress`. `.rebase/state.json`'s address on its own, and\n // `.rebase-dev-url`, outlive a killed `rebase dev`, and the port they name\n // may be another server's by now. With neither, the reset goes to the\n // database directly.\n const baseUrl = resolveBackendAddress(readEnvFile(projectRoot), projectRoot).url;\n\n if (baseUrl && serviceKey) {\n console.log(\"Trying API-first reset via running backend...\");\n try {\n const finalPass = newPassword;\n const cleanBaseUrl = baseUrl.replace(/\\/+$/, \"\");\n // Not `limit=1`: the search is fuzzy and ordered by role count, so\n // the exact match is not necessarily first — asking for one row can\n // make it unreachable. Ask for a page, then match exactly.\n const searchUrl = `${cleanBaseUrl}/api/admin/users?search=${encodeURIComponent(email)}&limit=50`;\n const searchRes = await fetch(searchUrl, {\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Accept\": \"application/json\"\n }\n });\n if (!searchRes.ok) {\n throw new ApiAnswered(`Failed to list users: ${searchRes.status} ${searchRes.statusText}`);\n }\n const searchData = await searchRes.json() as unknown;\n if (!searchData || typeof searchData !== \"object\") {\n throw new Error(\"Invalid response format from user search API.\");\n }\n\n const matched = selectUserForEmail(searchData, email);\n if (!matched) {\n throw new ApiAnswered(`No user has the email ${email}.`);\n }\n\n const resetUrl = `${cleanBaseUrl}/api/admin/users/${matched.id}/reset-password`;\n const resetRes = await fetch(resetUrl, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${serviceKey}`,\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\"\n },\n body: JSON.stringify({ password: finalPass })\n });\n\n if (!resetRes.ok) {\n throw new ApiAnswered(`Password reset refused: ${await refusalReason(resetRes)}`);\n }\n\n console.log(\"API reset successful.\");\n console.log(chalk.bold(\" 🔑 Rebase Auth — Reset Password (via API)\"));\n console.log(\"\");\n // The address as stored, not as typed — they differ in case often\n // enough that echoing the input hides which account was touched.\n console.log(` ${chalk.gray(\"Email:\")} ${matched.email}`);\n // Echoed only when we invented it. A password the operator typed\n // is already theirs; printing it again only adds scrollback.\n console.log(` ${chalk.gray(\"Password:\")} ${wasGenerated ? finalPass : \"*\".repeat(finalPass.length)}`);\n console.log(\"\");\n return;\n } catch (err) {\n const errMsg = err instanceof Error ? err.message : String(err);\n // The backend answered and said no — a password too weak, a user\n // it does not have. That answer stands: writing the hash straight\n // into the database instead skipped the strength rule it had just\n // applied, and ended no sessions. Only a backend that could not be\n // reached is a reason to go round it.\n if (err instanceof ApiAnswered) {\n console.error(chalk.red(`✗ ${errMsg}`));\n process.exitCode = 1;\n return;\n }\n console.warn(chalk.yellow(\"API reset failed, falling back to direct database update...\"));\n console.warn(chalk.gray(` Details: ${errMsg}`));\n }\n }\n\n // 2. Direct-DB Fallback\n const backendDir = requireBackendDir(projectRoot);\n const tsxBin = resolveTsx(projectRoot);\n\n if (!tsxBin) {\n exitDependenciesNotInstalled(projectRoot);\n }\n\n // The database the `db` family would use, resolved — and, for a remembered\n // compose container that is stopped, started — the same way. A stock\n // scaffold leaves DATABASE_URL commented out and runs on the managed\n // database, which only exists in a child's environment when something puts\n // it there — without this the script connected with libpq's defaults,\n // localhost:5432 as the OS user. An external DATABASE_URL adds nothing\n // here: the child already reads it.\n let prepared: PreparedDatabase;\n try {\n const { ensureDockerDatabase } = await import(\"./db\");\n await ensureDockerDatabase(projectRoot, rawArgs);\n const { prepareDatabaseEnv } = await import(\"../dev-db/prepare\");\n prepared = await prepareDatabaseEnv(projectRoot, {\n onProgress: (message) => console.log(chalk.gray(` ${message}`))\n });\n } catch (err) {\n console.error(chalk.red(\"✗ Direct database update failed.\"));\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n\n // Opened here first, so a database that does not answer gets the banner\n // every other command prints — not the script's raw error and stack — and\n // no script is written for it.\n const { databaseUrlOf } = await import(\"./db\");\n const url = databaseUrlOf(prepared, readEnvFile(projectRoot));\n if (url) {\n const failure = await databaseOpenFailure(url, prepared.database.source);\n if (failure) {\n for (const line of failure) console.error(line);\n process.exit(1);\n }\n }\n\n try {\n const env: Record<string, string> = { ...process.env as Record<string, string> };\n if (envFile) {\n env.DOTENV_CONFIG_PATH = envFile;\n }\n env.REBASE_RESET_EMAIL = email;\n env.REBASE_RESET_PASSWORD = newPassword;\n env.REBASE_ENV_FILE_PATH = envFile || path.join(projectRoot, \".env\");\n Object.assign(env, prepared.env);\n\n const scriptContent = resetPasswordScript(wasGenerated, parseEnvBoolean(process.env.REBASE_DEBUG) === true);\n\n const tmpScriptPath = path.join(backendDir, \".tmp-reset-password.ts\");\n fs.writeFileSync(tmpScriptPath, scriptContent, \"utf-8\");\n\n console.log(\"\");\n console.log(chalk.bold(\" 🔑 Rebase Auth — Reset Password (Direct DB Fallback)\"));\n console.log(\"\");\n console.log(` ${chalk.gray(\"Email:\")} ${email}`);\n console.log(` ${chalk.gray(\"Database:\")} ${prepared.description}`);\n if (!wasGenerated) {\n console.log(` ${chalk.gray(\"Password:\")} ${\"*\".repeat(newPassword.length)}`);\n }\n console.log(\"\");\n\n const child = spawn(tsxBin, [tmpScriptPath], {\n cwd: backendDir,\n stdio: \"inherit\",\n env\n });\n\n // The script is written into the user's backend directory, so every\n // exit has to remove it. Without an `error` handler a failed spawn\n // raises an unhandled event, the process dies before `close`, and\n // `.tmp-reset-password.ts` is left behind to be committed.\n const cleanup = () => {\n try { fs.unlinkSync(tmpScriptPath); } catch { /* already gone */ }\n };\n\n return new Promise((resolve) => {\n child.on(\"error\", (err) => {\n cleanup();\n console.error(chalk.red(\"✗ Could not run the reset script.\"));\n console.error(chalk.gray(` ${err.message}`));\n process.exit(1);\n });\n child.on(\"close\", (code) => {\n cleanup();\n if (code !== 0) {\n process.exit(code ?? 1);\n }\n resolve();\n });\n });\n } catch (err) {\n console.error(chalk.red(\"✗ Direct database update failed.\"));\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n}\n\n/**\n * Why the database the fallback would reset in cannot be opened, as the lines\n * to print — or null when it opens.\n *\n * A stopped database, refused credentials and a database name the server does\n * not have get the boxed banner the other commands print; anything else is one\n * sentence. Either way the password was not reset and no script was written.\n */\nasync function databaseOpenFailure(url: string, source: DevDatabaseSource): Promise<string[] | null> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: url, connectionTimeoutMillis: 10_000 });\n client.on(\"error\", () => undefined);\n try {\n await client.connect();\n await client.query(\"SELECT 1\");\n return null;\n } catch (error) {\n const { connectionErrorText, connectionFailureBanner, connectionRefusedFixes } =\n await import(\"../dev-db/connection-banner\");\n const consequence = \"The password was not reset.\";\n return connectionFailureBanner(url, error, { consequence, fixes: connectionRefusedFixes(source) }) ?? [\n chalk.red(`✗ Could not open the database: ${redactConnectionPasswords(connectionErrorText(error))}`),\n chalk.gray(` ${consequence}`)\n ];\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n/**\n * The script the direct-database fallback runs under the project's own tsx, so\n * it resolves the project's `@rebasepro/server-postgres` and schema.\n *\n * It ends the account's sessions in the transaction that sets the password,\n * as every door through the API does (`replaceUserPassword`): the refresh\n * tokens and outstanding reset links go, and `tokens_valid_after` voids the\n * access tokens already handed out. An operator resetting a phished account\n * with the backend down is the case this exists for, and a reset that left the\n * attacker's refresh token minting access tokens did not recover anything.\n *\n * It exits 0 only when a row was updated: a missing user and a thrown error are\n * both exit 1. `echoPassword` prints the new password, for a generated one;\n * `showStack` prints a failure's stack rather than its sentence (`--debug`).\n *\n * Exported so its tests can run it.\n */\nexport function resetPasswordScript(echoPassword: boolean, showStack = false): string {\n return `\nimport { createPostgresDatabaseConnection } from \"@rebasepro/server-postgres\";\nimport { hashPassword } from \"@rebasepro/server\";\nimport { getTableConfig } from \"drizzle-orm/pg-core\";\nimport * as dotenv from \"dotenv\";\nimport path from \"path\";\nimport fs from \"fs\";\n\ndotenv.config({ path: process.env.REBASE_ENV_FILE_PATH, quiet: true });\n\n// Lowercased, as every address is stored (\\`normalizeEmail\\`): matched as\n// typed, \"Ops@Acme.test\" found no row.\nconst email = process.env.REBASE_RESET_EMAIL!.trim().toLowerCase();\nconst newPassword = process.env.REBASE_RESET_PASSWORD!;\n\nconst quote = (identifier: string) => '\"' + identifier.replace(/\"/g, '\"\"') + '\"';\n\nasync function resetPassword() {\n const { pool } = createPostgresDatabaseConnection(process.env.DATABASE_URL!);\n const hash = await hashPassword(newPassword);\n\n let usersTable;\n try {\n const schemaPath = path.resolve(\"./src/schema.generated.ts\");\n if (fs.existsSync(schemaPath)) {\n const schema = await import(\"file://\" + schemaPath);\n usersTable = schema.users || schema.tables?.users;\n }\n } catch (e) {\n // ignore and fallback\n }\n\n if (!usersTable) {\n const pgServer = await import(\"@rebasepro/server-postgres\");\n usersTable = pgServer.users;\n }\n\n // The physical names, from the table object: the collection may map its\n // properties to other columns, and the statements below are plain SQL.\n const { name: tableName, schema: tableSchema } = getTableConfig(usersTable);\n const usersSchema = tableSchema || \"public\";\n const users = quote(usersSchema) + \".\" + quote(tableName);\n const idColumn = quote(usersTable.id.name);\n const emailColumn = quote(usersTable.email.name);\n const passwordColumn = quote((usersTable.passwordHash ?? usersTable.password_hash).name);\n // Where \\`ensureAuthTablesExist\\` puts the tables that hang off the users\n // table: beside it, or in \\`rebase\\` when it lives in \\`public\\`.\n const authSchema = usersSchema === \"public\" ? \"rebase\" : usersSchema;\n\n const client = await pool.connect();\n try {\n // A database nothing has booted against — or a managed one \\`db reset\\`\n // emptied — has no users table, and so no account to reset. Said as\n // that, not as pg's \\`relation … does not exist\\`.\n const usersTable = await client.query(\"SELECT to_regclass($1) IS NOT NULL AS present\", [users]);\n if (!usersTable.rows[0]?.present) {\n console.error(\"✗ \" + users + \" does not exist in this database, so it holds no account to reset.\");\n console.error(\" Nothing was changed. \\`rebase dev\\` creates it the first time it boots against this database.\");\n process.exit(1);\n }\n\n await client.query(\"BEGIN\");\n const updated = await client.query(\n \"UPDATE \" + users + \" SET \" + passwordColumn + \" = $1 WHERE \" + emailColumn + \" = $2 \" +\n \"RETURNING \" + idColumn + \" AS id, \" + emailColumn + \" AS email\",\n [hash, email]\n );\n if (updated.rows.length === 0) {\n await client.query(\"ROLLBACK\");\n // Nothing was updated, so nothing was reset. Exiting 0 here reported\n // success for a no-op, which is what a script would have believed.\n console.error(\"✗ User not found: \" + email);\n process.exit(1);\n }\n const uid = updated.rows[0].id;\n\n // A table or column the server has not created yet (it never booted\n // against this database) holds no session to end.\n const watermark = await client.query(\n \"SELECT 1 FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = 'tokens_valid_after'\",\n [usersSchema, tableName]\n );\n if (watermark.rows.length > 0) {\n await client.query(\"UPDATE \" + users + \" SET tokens_valid_after = NOW() WHERE \" + idColumn + \" = $1\", [uid]);\n }\n for (const table of [\"refresh_tokens\", \"password_reset_tokens\"]) {\n const qualified = quote(authSchema) + \".\" + quote(table);\n const present = await client.query(\"SELECT to_regclass($1) IS NOT NULL AS present\", [qualified]);\n if (present.rows[0]?.present) {\n await client.query(\"DELETE FROM \" + qualified + \" WHERE uid = $1\", [uid]);\n }\n }\n await client.query(\"COMMIT\");\n\n console.log(\"✅ Password reset for: \" + updated.rows[0].email);\n console.log(\" Every session of this account has ended.\");\n ${echoPassword ? 'console.log(\" Password: \" + newPassword);' : \"\"}\n process.exit(0);\n } catch (error) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n throw error;\n } finally {\n client.release();\n }\n}\n\n// A thrown error is a failed reset, and exits like one — a refused connection\n// that exits 0 reads as a reset to anything checking the exit code. Said as a\n// sentence: the stack is the script's own, and only --debug asks for it.\nconst describe = (error: unknown): string => {\n if (error instanceof AggregateError && !error.message && error.errors.length > 0) return describe(error.errors[0]);\n const message = error instanceof Error ? error.message : String(error);\n const code = error && typeof error === \"object\" && \"code\" in error && typeof error.code === \"string\" ? error.code : \"\";\n return code && !message.includes(code) ? message + \" (\" + code + \")\" : message;\n};\nresetPassword().catch((error) => {\n ${showStack ? \"console.error(error);\" : \"console.error(\\\"✗ The password was not reset: \\\" + describe(error));\"}\n process.exit(1);\n});\n`;\n}\n\nfunction printAuthHelp() {\n console.log(`\n${chalk.bold(\"rebase auth\")} — Authentication management commands\n\n${chalk.green.bold(\"Usage\")}\n rebase auth ${chalk.blue(\"<command>\")} [options]\n\n${chalk.green.bold(\"Commands\")}\n ${chalk.blue.bold(\"reset-password\")} Reset a user's password\n\n${chalk.green.bold(\"reset-password Options\")}\n ${chalk.blue(\"--email, -e\")} User's email address\n ${chalk.blue(\"--password, -p\")} New password (default: one is generated and printed)\n\n${chalk.green.bold(\"Examples\")}\n rebase auth reset-password user@example.com\n rebase auth reset-password --email user@example.com --password MyNewPass!\n`);\n}\n"],"mappings":"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"}
@@ -60,8 +60,9 @@ var UNREACHABLE = /* @__PURE__ */ new Set([
60
60
  ]);
61
61
  /**
62
62
  * What to print when opening `url` failed with `error`: the banner, in the
63
- * case it is — the server not answering, credentials refused, or a database
64
- * of that name not there — or `null` when it is none of those three.
63
+ * case it is — the server not answering, credentials refused, a database
64
+ * closed to new connections, or a database of that name not there — or `null`
65
+ * when it is none of those four.
65
66
  *
66
67
  * `consequence` is the line saying what did not happen ("The password was not
67
68
  * reset."); `fixes` is what to do about a server that does not answer, which
@@ -85,6 +86,18 @@ function connectionFailureBanner(url, error, options) {
85
86
  } catch {}
86
87
  return banner(`Authentication failed for user "${user}" at ${host}`, said, ["PostgreSQL rejected the credentials in the connection string.", options.consequence]);
87
88
  }
89
+ const closed = /database "(.+)" is not currently accepting connections/.exec(text)?.[1];
90
+ if (closed) return banner(`Database "${closed}" at ${host} is closed to new connections`, said, [
91
+ "It is set to refuse every new connection (ALLOW_CONNECTIONS false).",
92
+ "`rebase db branch create --force` closes its source for the copy, and one",
93
+ "stopped before it finished (a closed terminal, a kill) leaves it closed.",
94
+ options.consequence,
95
+ "",
96
+ "Nothing in it is lost. Open it again with:",
97
+ " rebase db branch list (every rebase db branch command opens it first)",
98
+ "or run this, connected to the server's postgres database:",
99
+ ` ALTER DATABASE "${closed.replace(/"/g, "\"\"")}" ALLOW_CONNECTIONS true;`
100
+ ]);
88
101
  if (code === "3D000") {
89
102
  let database = "(default)";
90
103
  try {
@@ -112,4 +125,4 @@ function connectionRefusedFixes(source) {
112
125
  //#endregion
113
126
  export { hostOf as i, connectionErrorText as n, connection_banner_exports as r, banner as t };
114
127
 
115
- //# sourceMappingURL=connection-banner-CCIyvjpW.js.map
128
+ //# sourceMappingURL=connection-banner-DbLJzWf4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-banner-DbLJzWf4.js","names":[],"sources":["../src/dev-db/connection-banner.ts"],"sourcesContent":["/**\n * The boxed banner the database commands print when a connection fails.\n *\n * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n * ❌ Cannot connect to PostgreSQL at 127.0.0.1:5508\n * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n *\n * The driver said: connect ECONNREFUSED 127.0.0.1:5508\n * …\n *\n * The driver prints it for the commands it runs (`cli-errors.ts` in\n * `@rebasepro/server-postgres`, which its entry does not export); this is the\n * same box for the commands the CLI answers in its own process — `db pull`\n * and `auth reset-password`'s database fallback — as plain lines for the\n * caller to print. Never with the connection string in it: it carries the\n * password.\n */\nimport type { DevDatabaseSource } from \"./resolve\";\n\nconst RULE = \"━\".repeat(60);\n\n/** The banner, as lines: a title, what the driver or tool said, then the body. */\nexport function banner(title: string, said: string | null, body: readonly string[]): string[] {\n return [\"\", RULE, ` ❌ ${title}`, RULE, \"\", ...(said ? [` ${said}`, \"\"] : []), ...body.map(line => ` ${line}`), \"\", RULE];\n}\n\n/** `host:port` of a connection string, for a message: never the string, which carries the password. */\nexport function hostOf(connectionString: string): string {\n try {\n const url = new URL(connectionString);\n\n return `${url.hostname || \"localhost\"}:${url.port || \"5432\"}`;\n } catch {\n return \"(unparseable connection string)\";\n }\n}\n\n/**\n * A connection error's own sentence. A refused connection on a dual-stack\n * host is an `AggregateError` with an empty message, and its sentence —\n * `connect ECONNREFUSED 127.0.0.1:5432` — is in the attempts it holds.\n */\nexport function connectionErrorText(error: unknown): string {\n if (error instanceof AggregateError && error.errors.length > 0 && !error.message) {\n return connectionErrorText(error.errors[0]);\n }\n\n return error instanceof Error && error.message ? error.message : String(error);\n}\n\n/** The `code` a `pg` or `net` error carries, looking inside an `AggregateError`'s attempts. */\nfunction codeOf(error: unknown): string | undefined {\n if (error instanceof AggregateError && error.errors.length > 0) {\n for (const inner of error.errors) {\n const code = codeOf(inner);\n if (code) return code;\n }\n }\n if (error && typeof error === \"object\" && \"code\" in error && typeof error.code === \"string\") return error.code;\n\n return undefined;\n}\n\n/** A server that is not there, or not reachable from here. */\nconst UNREACHABLE = new Set([\"ECONNREFUSED\", \"ENOTFOUND\", \"EAI_AGAIN\", \"ETIMEDOUT\", \"EHOSTUNREACH\", \"ENETUNREACH\", \"ECONNRESET\"]);\n\n/**\n * What to print when opening `url` failed with `error`: the banner, in the\n * case it is — the server not answering, credentials refused, a database\n * closed to new connections, or a database of that name not there — or `null`\n * when it is none of those four.\n *\n * `consequence` is the line saying what did not happen (\"The password was not\n * reset.\"); `fixes` is what to do about a server that does not answer, which\n * depends on what named the database.\n */\nexport function connectionFailureBanner(\n url: string,\n error: unknown,\n options: { consequence: string; fixes: readonly string[] }\n): string[] | null {\n const code = codeOf(error);\n const text = connectionErrorText(error);\n const said = `The driver said: ${text}${code && !text.includes(code) ? ` (${code})` : \"\"}`;\n const host = hostOf(url);\n\n if ((code && UNREACHABLE.has(code)) || /timeout expired|Connection terminated due to connection timeout/i.test(text)) {\n return banner(`Cannot connect to PostgreSQL at ${host}`, said, [\n \"The database server is not running or is not accepting connections.\",\n options.consequence,\n \"\",\n ...options.fixes.map(fix => ` • ${fix}`)\n ]);\n }\n if (code === \"28P01\" || code === \"28000\") {\n let user = \"unknown\";\n try {\n user = decodeURIComponent(new URL(url).username) || \"unknown\";\n } catch { /* named as unknown */ }\n return banner(`Authentication failed for user \"${user}\" at ${host}`, said, [\n \"PostgreSQL rejected the credentials in the connection string.\",\n options.consequence\n ]);\n }\n // 55000 covers more than this, so the sentence is matched — as the driver's banner does.\n const closed = /database \"(.+)\" is not currently accepting connections/.exec(text)?.[1];\n if (closed) {\n return banner(`Database \"${closed}\" at ${host} is closed to new connections`, said, [\n \"It is set to refuse every new connection (ALLOW_CONNECTIONS false).\",\n \"`rebase db branch create --force` closes its source for the copy, and one\",\n \"stopped before it finished (a closed terminal, a kill) leaves it closed.\",\n options.consequence,\n \"\",\n \"Nothing in it is lost. Open it again with:\",\n \" rebase db branch list (every rebase db branch command opens it first)\",\n \"or run this, connected to the server's postgres database:\",\n ` ALTER DATABASE \"${closed.replace(/\"/g, '\"\"')}\" ALLOW_CONNECTIONS true;`\n ]);\n }\n if (code === \"3D000\") {\n let database = \"(default)\";\n try {\n database = decodeURIComponent(new URL(url).pathname.replace(/^\\//, \"\")) || database;\n } catch { /* named as the default */ }\n return banner(`Database \"${database}\" does not exist at ${host}`, said, [\n \"The server answered, and has no database of that name.\",\n options.consequence\n ]);\n }\n\n return null;\n}\n\n/**\n * What to do about a database that does not answer, for what named it — the\n * list the driver's banner gives for the same `REBASE_DATABASE_SOURCE`.\n */\nexport function connectionRefusedFixes(source: DevDatabaseSource): string[] {\n switch (source) {\n case \"flag\": return [\"Check the URL passed with --database-url, and that its server is running\"];\n case \"environment\": return [\"Check DATABASE_URL in this shell's environment, and that its server is running\"];\n case \"env-file\": return [\"Check DATABASE_URL in .env, and that its server is running\"];\n case \"branch\": return [\n \"The branch's database is on the server DATABASE_URL names: start that server\",\n \"Or leave the branch: rebase db branch switch --off\"\n ];\n case \"docker\":\n case \"last-dev\": return [\"docker compose up -d db (the project's Postgres in Docker)\"];\n case \"managed\": return [\"Restart the managed development database: rebase db stop, then run the command again\"];\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { n as parseCommandArgs, r as wantsHelp } from "./args-BX0YD14a.js";
2
2
  import { r as refuseMissingCollectionsPath, t as COLLECTIONS_FLAG_HELP } from "./path-flags-BFgG9V6y.js";
3
3
  import { DEV_DATABASE_KIND_ENV, managedNotices, prepareDatabaseEnv } from "./prepare-DdnU70z8.js";
4
- import { absolutizeLocalPathArgs, ensureDockerDatabase, n as argsFromCommand, resolveDriverCli, spawnDriverCli, t as reportSpawnFailure } from "./db-4NXrKl5W.js";
4
+ import { absolutizeLocalPathArgs, ensureDockerDatabase, n as argsFromCommand, resolveDriverCli, spawnDriverCli, t as reportSpawnFailure } from "./db-CRNpA5mH.js";
5
5
  import chalk from "chalk";
6
6
  import path from "path";
7
7
  //#region src/commands/console.ts
@@ -100,4 +100,4 @@ async function consoleCommand(rawArgs) {
100
100
  //#endregion
101
101
  export { consoleCommand };
102
102
 
103
- //# sourceMappingURL=console-BfPGw5m8.js.map
103
+ //# sourceMappingURL=console-CEGm0XNc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"console-BfPGw5m8.js","names":[],"sources":["../src/commands/console.ts"],"sourcesContent":["/**\n * CLI command: rebase console\n *\n * A Node REPL with the project open — `rebase` (what `openRebase` from\n * `@rebasepro/server-postgres` returns: `dataAsAdmin`, `sql`, `withAuth`) and\n * `collections` in scope — or, with `--eval`, one piece of code run and its\n * result printed as JSON.\n *\n * Resolved like `rebase db push`: the same project and `.env`, the same\n * database (the managed one started if that is what the project uses), the\n * same `--collections` loader — the driver's, spawned in `backend/`. Opening\n * the database changes nothing in it.\n */\nimport chalk from \"chalk\";\nimport path from \"path\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { argsFromCommand } from \"../utils/command-words\";\nimport { COLLECTIONS_FLAG_HELP, refuseMissingCollectionsPath } from \"../utils/path-flags\";\nimport { reportSpawnFailure } from \"../utils/spawn-error\";\nimport { DEV_DATABASE_KIND_ENV, managedNotices, prepareDatabaseEnv } from \"../dev-db/prepare\";\nimport { absolutizeLocalPathArgs, ensureDockerDatabase, resolveDriverCli, spawnDriverCli } from \"./db\";\n\nexport const CONSOLE_FLAGS = {\n \"--eval\": String,\n \"-e\": \"--eval\",\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--database-url\": String,\n \"--docker\": Boolean\n} as const;\n\n/** What each long flag above does, for the Options block. */\nconst CONSOLE_FLAG_HELP: Record<string, string> = {\n \"--eval\": \"Run this code instead of the REPL, print its awaited result as JSON, and exit 1 if it throws\",\n \"--collections\": COLLECTIONS_FLAG_HELP,\n \"--database-url\": \"Open this database instead of the project's\",\n \"--docker\": \"Use the database from docker compose, starting it if needed\"\n};\n\n/** Where the REPL keeps its history: the project's own `.rebase/`, which the scaffold ignores. */\nexport function consoleHistoryPath(projectRoot: string): string {\n return path.join(projectRoot, \".rebase\", \"console_history\");\n}\n\nfunction flagLabel(flag: string): string {\n const aliases = Object.entries(CONSOLE_FLAGS)\n .filter(([, target]) => target === flag)\n .map(([alias]) => alias);\n return [flag, ...aliases].join(\", \");\n}\n\nexport function printConsoleHelp(): void {\n console.log(`\n${chalk.bold(\"rebase console\")} — Explore the project's data from a REPL\n\n${chalk.green.bold(\"Usage\")}\n rebase console [options]\n\n${chalk.green.bold(\"Options\")}\n${Object.keys(CONSOLE_FLAG_HELP)\n .map(flag => ` ${chalk.blue(flagLabel(flag).padEnd(22))} ${CONSOLE_FLAG_HELP[flag]}`)\n .join(\"\\n\")}\n\nIn scope:\n rebase dataAsAdmin, sql(query, { params }), withAuth(user) → { data }, close()\n collections the project's collections\n\nTop-level await works. Opening the database changes nothing in it: no table,\nrole or trigger is created. withAuth runs as the user under row-level security.\n\n${chalk.green.bold(\"Examples\")}\n rebase console\n rebase console -e \"rebase.dataAsAdmin.posts.find({ limit: 5 })\"\n rebase console -e \"(await rebase.withAuth({ uid: 'u1', roles: ['user'] })).data.posts.find()\"\n rebase console -e \"rebase.sql('select count(*) from posts')\"\n`);\n}\n\nexport async function consoleCommand(rawArgs: string[]): Promise<void> {\n if (wantsHelp(rawArgs)) {\n printConsoleHelp();\n return;\n }\n\n // Strictly, before the line is relayed: the driver would otherwise be the\n // first to see a mistyped flag, after a database had been started for it.\n const { flags } = parseCommandArgs({\n spec: CONSOLE_FLAGS,\n rawArgs,\n commandWords: 1,\n command: \"console\",\n maxPositionals: 0\n });\n\n const resolved = await resolveDriverCli();\n const { projectRoot, backendDir } = resolved;\n refuseMissingCollectionsPath(rawArgs, process.cwd(), backendDir, \"console\");\n await ensureDockerDatabase(projectRoot, rawArgs);\n\n // With --eval, stdout is the result and nothing else, so it can be piped\n // into `jq`; piped into the REPL, it is the REPL's answers. Either way what\n // the database resolution has to say goes to stderr.\n const say = flags[\"--eval\"] === undefined && process.stdin.isTTY\n ? (line: string) => console.log(chalk.gray(` ${line}`))\n : (line: string) => console.error(chalk.gray(` ${line}`));\n const prepared = await prepareDatabaseEnv(projectRoot, {\n flagUrl: flags[\"--database-url\"] ?? null,\n flagDocker: flags[\"--docker\"] === true,\n onProgress: say\n });\n Object.assign(resolved.env, prepared.env);\n // The database this run resolved, flags included, so the driver words a\n // failure to open it for that one — a managed database that has no free\n // connection is not \"is your Postgres running?\".\n resolved.env[DEV_DATABASE_KIND_ENV] = prepared.database.kind;\n for (const line of managedNotices(prepared)) say(line);\n\n const childArgs = [\n ...absolutizeLocalPathArgs(argsFromCommand(rawArgs, \"console\"), process.cwd()),\n `--history=${consoleHistoryPath(projectRoot)}`\n ];\n\n try {\n await spawnDriverCli(resolved, childArgs);\n } catch (error) {\n // A child that exited non-zero has already said why, on the terminal it\n // shares — an `--eval` that threw, a database that refused.\n reportSpawnFailure(error);\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,gBAAgB;CACzB,UAAU;CACV,MAAM;CACN,iBAAiB;CACjB,MAAM;CACN,kBAAkB;CAClB,YAAY;AAChB;;AAGA,IAAM,oBAA4C;CAC9C,UAAU;CACV,iBAAiB;CACjB,kBAAkB;CAClB,YAAY;AAChB;;AAGA,SAAgB,mBAAmB,aAA6B;CAC5D,OAAO,KAAK,KAAK,aAAa,WAAW,iBAAiB;AAC9D;AAEA,SAAS,UAAU,MAAsB;CAIrC,OAAO,CAAC,MAAM,GAHE,OAAO,QAAQ,aAAa,CAAC,CACxC,QAAQ,GAAG,YAAY,WAAW,IAAI,CAAC,CACvC,KAAK,CAAC,WAAW,KACL,CAAO,CAAC,CAAC,KAAK,IAAI;AACvC;AAEA,SAAgB,mBAAyB;CACrC,QAAQ,IAAI;EACd,MAAM,KAAK,gBAAgB,EAAE;;EAE7B,MAAM,MAAM,KAAK,OAAO,EAAE;;;EAG1B,MAAM,MAAM,KAAK,SAAS,EAAE;EAC5B,OAAO,KAAK,iBAAiB,CAAC,CACvB,KAAI,SAAQ,KAAK,MAAM,KAAK,UAAU,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,kBAAkB,OAAO,CAAC,CACrF,KAAK,IAAI,EAAE;;;;;;;;;EASlB,MAAM,MAAM,KAAK,UAAU,EAAE;;;;;CAK9B;AACD;AAEA,eAAsB,eAAe,SAAkC;CACnE,IAAI,UAAU,OAAO,GAAG;EACpB,iBAAiB;EACjB;CACJ;CAIA,MAAM,EAAE,UAAU,iBAAiB;EAC/B,MAAM;EACN;EACA,cAAc;EACd,SAAS;EACT,gBAAgB;CACpB,CAAC;CAED,MAAM,WAAW,MAAM,iBAAiB;CACxC,MAAM,EAAE,aAAa,eAAe;CACpC,6BAA6B,SAAS,QAAQ,IAAI,GAAG,YAAY,SAAS;CAC1E,MAAM,qBAAqB,aAAa,OAAO;CAK/C,MAAM,MAAM,MAAM,cAAc,KAAA,KAAa,QAAQ,MAAM,SACpD,SAAiB,QAAQ,IAAI,MAAM,KAAK,KAAK,MAAM,CAAC,KACpD,SAAiB,QAAQ,MAAM,MAAM,KAAK,KAAK,MAAM,CAAC;CAC7D,MAAM,WAAW,MAAM,mBAAmB,aAAa;EACnD,SAAS,MAAM,qBAAqB;EACpC,YAAY,MAAM,gBAAgB;EAClC,YAAY;CAChB,CAAC;CACD,OAAO,OAAO,SAAS,KAAK,SAAS,GAAG;CAIxC,SAAS,IAAI,yBAAyB,SAAS,SAAS;CACxD,KAAK,MAAM,QAAQ,eAAe,QAAQ,GAAG,IAAI,IAAI;CAErD,MAAM,YAAY,CACd,GAAG,wBAAwB,gBAAgB,SAAS,SAAS,GAAG,QAAQ,IAAI,CAAC,GAC7E,aAAa,mBAAmB,WAAW,GAC/C;CAEA,IAAI;EACA,MAAM,eAAe,UAAU,SAAS;CAC5C,SAAS,OAAO;EAGZ,mBAAmB,KAAK;EACxB,QAAQ,KAAK,CAAC;CAClB;AACJ"}
1
+ {"version":3,"file":"console-CEGm0XNc.js","names":[],"sources":["../src/commands/console.ts"],"sourcesContent":["/**\n * CLI command: rebase console\n *\n * A Node REPL with the project open — `rebase` (what `openRebase` from\n * `@rebasepro/server-postgres` returns: `dataAsAdmin`, `sql`, `withAuth`) and\n * `collections` in scope — or, with `--eval`, one piece of code run and its\n * result printed as JSON.\n *\n * Resolved like `rebase db push`: the same project and `.env`, the same\n * database (the managed one started if that is what the project uses), the\n * same `--collections` loader — the driver's, spawned in `backend/`. Opening\n * the database changes nothing in it.\n */\nimport chalk from \"chalk\";\nimport path from \"path\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { argsFromCommand } from \"../utils/command-words\";\nimport { COLLECTIONS_FLAG_HELP, refuseMissingCollectionsPath } from \"../utils/path-flags\";\nimport { reportSpawnFailure } from \"../utils/spawn-error\";\nimport { DEV_DATABASE_KIND_ENV, managedNotices, prepareDatabaseEnv } from \"../dev-db/prepare\";\nimport { absolutizeLocalPathArgs, ensureDockerDatabase, resolveDriverCli, spawnDriverCli } from \"./db\";\n\nexport const CONSOLE_FLAGS = {\n \"--eval\": String,\n \"-e\": \"--eval\",\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--database-url\": String,\n \"--docker\": Boolean\n} as const;\n\n/** What each long flag above does, for the Options block. */\nconst CONSOLE_FLAG_HELP: Record<string, string> = {\n \"--eval\": \"Run this code instead of the REPL, print its awaited result as JSON, and exit 1 if it throws\",\n \"--collections\": COLLECTIONS_FLAG_HELP,\n \"--database-url\": \"Open this database instead of the project's\",\n \"--docker\": \"Use the database from docker compose, starting it if needed\"\n};\n\n/** Where the REPL keeps its history: the project's own `.rebase/`, which the scaffold ignores. */\nexport function consoleHistoryPath(projectRoot: string): string {\n return path.join(projectRoot, \".rebase\", \"console_history\");\n}\n\nfunction flagLabel(flag: string): string {\n const aliases = Object.entries(CONSOLE_FLAGS)\n .filter(([, target]) => target === flag)\n .map(([alias]) => alias);\n return [flag, ...aliases].join(\", \");\n}\n\nexport function printConsoleHelp(): void {\n console.log(`\n${chalk.bold(\"rebase console\")} — Explore the project's data from a REPL\n\n${chalk.green.bold(\"Usage\")}\n rebase console [options]\n\n${chalk.green.bold(\"Options\")}\n${Object.keys(CONSOLE_FLAG_HELP)\n .map(flag => ` ${chalk.blue(flagLabel(flag).padEnd(22))} ${CONSOLE_FLAG_HELP[flag]}`)\n .join(\"\\n\")}\n\nIn scope:\n rebase dataAsAdmin, sql(query, { params }), withAuth(user) → { data }, close()\n collections the project's collections\n\nTop-level await works. Opening the database changes nothing in it: no table,\nrole or trigger is created. withAuth runs as the user under row-level security.\n\n${chalk.green.bold(\"Examples\")}\n rebase console\n rebase console -e \"rebase.dataAsAdmin.posts.find({ limit: 5 })\"\n rebase console -e \"(await rebase.withAuth({ uid: 'u1', roles: ['user'] })).data.posts.find()\"\n rebase console -e \"rebase.sql('select count(*) from posts')\"\n`);\n}\n\nexport async function consoleCommand(rawArgs: string[]): Promise<void> {\n if (wantsHelp(rawArgs)) {\n printConsoleHelp();\n return;\n }\n\n // Strictly, before the line is relayed: the driver would otherwise be the\n // first to see a mistyped flag, after a database had been started for it.\n const { flags } = parseCommandArgs({\n spec: CONSOLE_FLAGS,\n rawArgs,\n commandWords: 1,\n command: \"console\",\n maxPositionals: 0\n });\n\n const resolved = await resolveDriverCli();\n const { projectRoot, backendDir } = resolved;\n refuseMissingCollectionsPath(rawArgs, process.cwd(), backendDir, \"console\");\n await ensureDockerDatabase(projectRoot, rawArgs);\n\n // With --eval, stdout is the result and nothing else, so it can be piped\n // into `jq`; piped into the REPL, it is the REPL's answers. Either way what\n // the database resolution has to say goes to stderr.\n const say = flags[\"--eval\"] === undefined && process.stdin.isTTY\n ? (line: string) => console.log(chalk.gray(` ${line}`))\n : (line: string) => console.error(chalk.gray(` ${line}`));\n const prepared = await prepareDatabaseEnv(projectRoot, {\n flagUrl: flags[\"--database-url\"] ?? null,\n flagDocker: flags[\"--docker\"] === true,\n onProgress: say\n });\n Object.assign(resolved.env, prepared.env);\n // The database this run resolved, flags included, so the driver words a\n // failure to open it for that one — a managed database that has no free\n // connection is not \"is your Postgres running?\".\n resolved.env[DEV_DATABASE_KIND_ENV] = prepared.database.kind;\n for (const line of managedNotices(prepared)) say(line);\n\n const childArgs = [\n ...absolutizeLocalPathArgs(argsFromCommand(rawArgs, \"console\"), process.cwd()),\n `--history=${consoleHistoryPath(projectRoot)}`\n ];\n\n try {\n await spawnDriverCli(resolved, childArgs);\n } catch (error) {\n // A child that exited non-zero has already said why, on the terminal it\n // shares — an `--eval` that threw, a database that refused.\n reportSpawnFailure(error);\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,gBAAgB;CACzB,UAAU;CACV,MAAM;CACN,iBAAiB;CACjB,MAAM;CACN,kBAAkB;CAClB,YAAY;AAChB;;AAGA,IAAM,oBAA4C;CAC9C,UAAU;CACV,iBAAiB;CACjB,kBAAkB;CAClB,YAAY;AAChB;;AAGA,SAAgB,mBAAmB,aAA6B;CAC5D,OAAO,KAAK,KAAK,aAAa,WAAW,iBAAiB;AAC9D;AAEA,SAAS,UAAU,MAAsB;CAIrC,OAAO,CAAC,MAAM,GAHE,OAAO,QAAQ,aAAa,CAAC,CACxC,QAAQ,GAAG,YAAY,WAAW,IAAI,CAAC,CACvC,KAAK,CAAC,WAAW,KACL,CAAO,CAAC,CAAC,KAAK,IAAI;AACvC;AAEA,SAAgB,mBAAyB;CACrC,QAAQ,IAAI;EACd,MAAM,KAAK,gBAAgB,EAAE;;EAE7B,MAAM,MAAM,KAAK,OAAO,EAAE;;;EAG1B,MAAM,MAAM,KAAK,SAAS,EAAE;EAC5B,OAAO,KAAK,iBAAiB,CAAC,CACvB,KAAI,SAAQ,KAAK,MAAM,KAAK,UAAU,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,kBAAkB,OAAO,CAAC,CACrF,KAAK,IAAI,EAAE;;;;;;;;;EASlB,MAAM,MAAM,KAAK,UAAU,EAAE;;;;;CAK9B;AACD;AAEA,eAAsB,eAAe,SAAkC;CACnE,IAAI,UAAU,OAAO,GAAG;EACpB,iBAAiB;EACjB;CACJ;CAIA,MAAM,EAAE,UAAU,iBAAiB;EAC/B,MAAM;EACN;EACA,cAAc;EACd,SAAS;EACT,gBAAgB;CACpB,CAAC;CAED,MAAM,WAAW,MAAM,iBAAiB;CACxC,MAAM,EAAE,aAAa,eAAe;CACpC,6BAA6B,SAAS,QAAQ,IAAI,GAAG,YAAY,SAAS;CAC1E,MAAM,qBAAqB,aAAa,OAAO;CAK/C,MAAM,MAAM,MAAM,cAAc,KAAA,KAAa,QAAQ,MAAM,SACpD,SAAiB,QAAQ,IAAI,MAAM,KAAK,KAAK,MAAM,CAAC,KACpD,SAAiB,QAAQ,MAAM,MAAM,KAAK,KAAK,MAAM,CAAC;CAC7D,MAAM,WAAW,MAAM,mBAAmB,aAAa;EACnD,SAAS,MAAM,qBAAqB;EACpC,YAAY,MAAM,gBAAgB;EAClC,YAAY;CAChB,CAAC;CACD,OAAO,OAAO,SAAS,KAAK,SAAS,GAAG;CAIxC,SAAS,IAAI,yBAAyB,SAAS,SAAS;CACxD,KAAK,MAAM,QAAQ,eAAe,QAAQ,GAAG,IAAI,IAAI;CAErD,MAAM,YAAY,CACd,GAAG,wBAAwB,gBAAgB,SAAS,SAAS,GAAG,QAAQ,IAAI,CAAC,GAC7E,aAAa,mBAAmB,WAAW,GAC/C;CAEA,IAAI;EACA,MAAM,eAAe,UAAU,SAAS;CAC5C,SAAS,OAAO;EAGZ,mBAAmB,KAAK;EACxB,QAAQ,KAAK,CAAC;CAClB;AACJ"}
@@ -810,7 +810,7 @@ async function manageLocalDatabase(subcommand, projectRoot, rawArgs) {
810
810
  * and the wrong one here means overwriting production with a laptop.
811
811
  */
812
812
  async function pullIntoLocal(projectRoot, rawArgs) {
813
- const { describeRestoreFailure, describeTarget, dumpSource, findPgTool, restoreCopy } = await import("./pull-CVKJTXWQ.js");
813
+ const { describeRestoreFailure, describeTarget, dumpSource, findPgTool, restoreCopy } = await import("./pull-C3PXduzl.js");
814
814
  const { prepareDatabaseEnv } = await import("./prepare-DdnU70z8.js");
815
815
  const { readEnvFile } = await import("./project-Wp2zoyeN.js").then((n) => n.u);
816
816
  const flags = parsePullFlags(rawArgs);
@@ -936,7 +936,7 @@ async function pullIntoLocal(projectRoot, rawArgs) {
936
936
  */
937
937
  async function restoreAppRole(target) {
938
938
  const { Client } = await import("pg");
939
- const { provisionableSchemas } = await import("./pull-CVKJTXWQ.js");
939
+ const { provisionableSchemas } = await import("./pull-C3PXduzl.js");
940
940
  const client = new Client({ connectionString: target });
941
941
  client.on("error", () => void 0);
942
942
  try {
@@ -971,7 +971,7 @@ async function restoreAppRole(target) {
971
971
  */
972
972
  async function anonymizeTarget(target, source, kind) {
973
973
  const { Client } = await import("pg");
974
- const { anonymizeCopy } = await import("./pull-CVKJTXWQ.js");
974
+ const { anonymizeCopy } = await import("./pull-C3PXduzl.js");
975
975
  const deleteIt = kind === "managed" ? `Delete it now: ${chalk.cyan("rebase db reset --yes")}` : kind === "docker" ? `Delete it now: ${chalk.cyan("docker compose down -v")}` : "Delete it now: drop the restored tables, or the database, before anything else reads it.";
976
976
  const holdsRealData = (reason) => [
977
977
  chalk.red(`✗ The copy could not be anonymized: ${reason}`),
@@ -1263,6 +1263,9 @@ async function switchBranch(projectRoot, rawArgs) {
1263
1263
  process.exit(1);
1264
1264
  }
1265
1265
  const database = branchDatabaseName(name);
1266
+ const { reopenClosedDatabases, reopenedDatabasesNotice } = await import("@rebasepro/server-postgres");
1267
+ const healed = await reopenClosedDatabases(base, [readActiveBranch(projectRoot)?.database]);
1268
+ for (const line of reopenedDatabasesNotice(healed)) console.error(chalk.yellow(` ${line}`));
1266
1269
  const { Client } = await import("pg");
1267
1270
  const client = new Client({ connectionString: base });
1268
1271
  try {
@@ -1489,4 +1492,4 @@ function dbExamples(kind) {
1489
1492
  //#endregion
1490
1493
  export { DB_SUBCOMMANDS, absolutizeLocalPathArgs, databaseUrlOf, dbCommand, ensureDockerDatabase, argsFromCommand as n, resolveDriverCli, runDriverDbCommand, runDriverSchemaCommand, spawnDriverCli, reportSpawnFailure as t };
1491
1494
 
1492
- //# sourceMappingURL=db-4NXrKl5W.js.map
1495
+ //# sourceMappingURL=db-CRNpA5mH.js.map