CLI reference
WIPduck-auth init, doctor, keys generate, keys rotate, migrate, emit-openapi.
Invocation
Every CLI command is available via bunx, npx, or pnpm dlx:
bunx @gentleduck/auth <subcommand> [args]
If you have @gentleduck/auth installed as a dependency, the binary
duck-auth is on the local PATH:
./node_modules/.bin/duck-auth <subcommand>
Subcommands
init <directory>
Scaffold a starter auth.ts plus a .env.example.
bunx @gentleduck/auth init src/lib
bunx @gentleduck/auth init src/lib --production
Flags:
--production- emit a production-flavoured starter (Redis + JWT + Argon2id + Drizzle bridge stub).--js- emit JavaScript instead of TypeScript.
Output: src/lib/auth.ts + src/lib/.env.example.
doctor
Load auth.ts from the current working directory, instantiate
AuthEngine, and call strict({ env: 'production' }). Surface every
failing gate as a human-readable diagnostic.
bunx @gentleduck/auth doctor
Output:
[v] baseUrl set: https://api.example.com
[v] transport: JwtTransport with 2 verify keys
[v] limiter: AuthRedisLimiter
[x] events: AuthInMemoryEvents - strict() requires cross-process delivery in production
[x] no listener attached to 'lockout' event
2 failed gates. Run `duck-auth doctor --explain` for fix suggestions.
The --explain flag prints the recommended fix for each failed gate.
keys generate <alg>
Mint signing material for JwtTransport. Supports hs256, ec256,
rs256, and ed25519.
bunx @gentleduck/auth keys generate hs256
Output:
kid: kid-2026-05-27-jw5e
secret (base64url): aBcD0_eFgHi...
# .env line:
JWT_HS256_SECRET=aBcD0_eFgHi...
JWT_HS256_KID=kid-2026-05-27-jw5e
For asymmetric algorithms, outputs both the private key (signer) and
the public JWK (for the /.well-known/jwks.json endpoint).
keys rotate <alg>
Mint new material and emit the snippet to add the new key as the
signer while keeping the prior key on verifyKeys for overlap.
bunx @gentleduck/auth keys rotate hs256
Output:
# Update your config:
new JwtTransport({
signKey: { kid: 'kid-2026-06-...', key: process.env.JWT_HS256_NEW! },
verifyKeys: [
{ kid: 'kid-2026-06-...', key: process.env.JWT_HS256_NEW! },
{ kid: 'kid-2026-05-...', key: process.env.JWT_HS256_PREV! }, // keep until oldest exp passes
],
})
# .env updates:
JWT_HS256_NEW=aBcDeFg...
JWT_HS256_PREV=hIjKlMn...
Apply the snippet. After the longest expected exp has passed (the JWT
ttlMs), drop the retired kid.
migrate <dialect>
Emit DDL for the SQL bridge schema. Supports pg, mysql, sqlite.
bunx @gentleduck/auth migrate pg > migrations/0001_auth.sql
psql $DATABASE_URL < migrations/0001_auth.sql
The DDL matches the bundled Drizzle reference implementations in
@gentleduck/auth/adapters/drizzle/{pg,mysql,sqlite}. If you check it
into a Drizzle migration folder, drizzle-kit will manage further
migrations on top.
emit-openapi
Introspect auth.ts from the current working directory and emit an
OpenAPI 3.1 spec for the mounted routes.
bunx @gentleduck/auth emit-openapi --out openapi.yaml
Flags:
--out <path>- write to file instead of stdout.--format yaml|json- choose serialisation (default yaml).--prefix <path>- override the route prefix.
The emitted spec covers /signin, /signout, /session, and one
/providers/:providerId/begin operation per registered provider.
help
bunx @gentleduck/auth help
bunx @gentleduck/auth help keys
Prints a top-level usage message or the per-subcommand help.
Working directory
Every command that introspects auth.ts (doctor, emit-openapi)
expects to run from the project root - the directory containing your
package.json. The CLI uses TypeScript's project references to resolve
the right tsconfig and load the file with --experimental-transform-types.
Programmatic use
Every CLI command is also exported as a function from @gentleduck/auth/cli
so you can wire it into a custom tool:
import { runDoctor, runMigrate } from '@gentleduck/auth/cli'
await runDoctor({ cwd: process.cwd(), explain: true })
await runMigrate({ dialect: 'pg', out: 'migrations/0001_auth.sql' })