Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/print-seeded-admin-banner.md
Original file line number Diff line number Diff line change
@@ -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).
12 changes: 12 additions & 0 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 ────────────────────────────
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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'}`));
Expand Down
9 changes: 9 additions & 0 deletions packages/plugins/plugin-auth/src/auth-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,15 @@ export class AuthManager {
private auth: Auth<any> | 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;

Expand Down
6 changes: 6 additions & 0 deletions packages/plugins/plugin-auth/src/auth-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down