Skip to main content

CLI reference

WIP

duck-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' })