diff --git a/.changeset/print-seeded-admin-banner.md b/.changeset/print-seeded-admin-banner.md new file mode 100644 index 000000000..6a048a64d --- /dev/null +++ b/.changeset/print-seeded-admin-banner.md @@ -0,0 +1,21 @@ +--- +"@objectstack/plugin-auth": patch +"@objectstack/cli": patch +--- + +fix(dev): surface the seeded dev-admin credentials in the `serve` startup banner. + +When the runtime seeds the dev admin on an empty DB, the confirmation was +emitted via `ctx.logger` during `runtime.start()` โ€” inside serve's boot-quiet +window โ€” so it was swallowed and never reached the console. plugin-auth now +records the seed result on the `auth` service and `serve` prints it in the +ready banner (after stdout is restored), e.g.: + +``` + ๐Ÿ”‘ Dev admin: admin@objectos.ai / admin123 + seeded on empty DB ยท dev only โ€” do not use in production +``` + +Shown only when an admin was actually seeded this boot (empty DB) โ€” never on a +DB that already had a user, so stale credentials are never displayed. Visible +in both `serve --dev` and `os dev` (the child's stdout is inherited). diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 2a9bfca53..686e6ea39 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -1754,6 +1754,17 @@ export default class Serve extends Command { } } + // Surface the dev admin seeded this boot (if any) in the banner. The + // seed runs in-process during runtime.start() under serve's boot-quiet + // window, so plugin-auth records the result on the `auth` service and + // we print it here, after stdout is restored. Visible in both + // `serve --dev` and `os dev` (the child's stdout is inherited). + let seededAdmin: { email: string; password: string } | undefined; + try { + const authSvc: any = kernel.getService?.('auth'); + if (authSvc?.devSeedResult?.email) seededAdmin = authSvc.devSeedResult; + } catch { /* auth service not present โ€” nothing to show */ } + // โ”€โ”€ Clean startup summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ printServerReady({ port, @@ -1766,6 +1777,7 @@ export default class Serve extends Command { driverLabel: resolvedDriverLabel, databaseUrl: redactDbUrl(resolvedDatabaseUrl), multiTenant: String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false', + seededAdmin, }); // โ”€โ”€ Publish the actually-bound port โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/packages/cli/src/utils/format.ts b/packages/cli/src/utils/format.ts index d4852dbea..038dab5a0 100644 --- a/packages/cli/src/utils/format.ts +++ b/packages/cli/src/utils/format.ts @@ -189,6 +189,12 @@ export interface ServerReadyOptions { databaseUrl?: string; /** Whether the SecurityPlugin was wired in multi-tenant mode (default true). */ multiTenant?: boolean; + /** + * Credentials of the dev admin seeded on an empty DB this boot (dev only). + * When present, the banner surfaces them so backend debugging never has to + * guess the login. Absent when nothing was seeded. + */ + seededAdmin?: { email: string; password: string }; } export function printServerReady(opts: ServerReadyOptions) { @@ -200,6 +206,14 @@ export function printServerReady(opts: ServerReadyOptions) { if (opts.uiEnabled && opts.consolePath) { console.log(chalk.cyan(' โžœ') + chalk.bold(' Console: ') + chalk.cyan(base + opts.consolePath + '/')); } + if (opts.seededAdmin) { + console.log(''); + console.log( + chalk.green(' ๐Ÿ”‘') + chalk.bold(' Dev admin: ') + + chalk.bold.green(`${opts.seededAdmin.email} / ${opts.seededAdmin.password}`), + ); + console.log(chalk.dim(' seeded on empty DB ยท dev only โ€” do not use in production')); + } console.log(''); console.log(chalk.dim(` Config: ${opts.configFile}`)); console.log(chalk.dim(` Mode: ${opts.isDev ? 'development' : 'production'}`)); diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index a9cf4c202..358c4448c 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -235,6 +235,15 @@ export class AuthManager { private auth: Auth | null = null; private config: AuthManagerOptions; + /** + * Result of the dev-only admin seed (set by `AuthPlugin.maybeSeedDevAdmin` + * when it provisions the well-known admin on an empty DB). The `serve` + * command reads this after boot to surface the credentials in the startup + * banner. Undefined when no seed ran (production, opt-out, or a DB that + * already had a user). + */ + public devSeedResult?: { email: string; password: string }; + constructor(config: AuthManagerOptions) { this.config = config; diff --git a/packages/plugins/plugin-auth/src/auth-plugin.ts b/packages/plugins/plugin-auth/src/auth-plugin.ts index 5ba28d659..83c8a5128 100644 --- a/packages/plugins/plugin-auth/src/auth-plugin.ts +++ b/packages/plugins/plugin-auth/src/auth-plugin.ts @@ -389,6 +389,12 @@ export class AuthPlugin implements Plugin { // is otherwise disabled. await api.signUpEmail({ body: { email, password, name } }); ctx.logger.info(`๐Ÿ”‘ Dev admin seeded: ${email} / ${password}`); + // Surface the credentials in the `serve` startup banner. The + // ctx.logger line above is swallowed by serve's boot-quiet window + // (the seed runs during runtime.start(), before stdout is restored), + // so the CLI reads this off the `auth` service and prints it after + // the banner instead. + this.authManager.devSeedResult = { email, password }; } catch (err: any) { // Best-effort. The common benign case is a race where a real sign-up // landed first (unique-email violation) โ€” treat as "already seeded".