Hashing
Introduction
Hash stores passwords as one-way digests. You hash on registration or password change, then verify the plain text on login. Import it from @bunyad/auth. By default Bunyad uses bcrypt through Bun's password API. Argon2id is available when you ask for it.
import { Hash } from "@bunyad/auth";
const hashed = await Hash.make("secret");
const ok = await Hash.check("secret", hashed);make and check are asynchronous. Always await them.
Bcrypt's work factor is adjustable. A higher cost makes hashing slower for everyone — including an attacker trying every candidate password. The default cost of 10 is appropriate for most apps.
Configuration
There is no separate hashing config file. The default algorithm is bcrypt. The default cost is 10. Change the process-wide bcrypt cost with setRounds:
import { Hash } from "@bunyad/auth";
Hash.setRounds(12);
Hash.cost(); // 12
Hash.rounds(); // 12 — alias of cost()setRounds returns Hash, so you can chain it. verifyConfiguration checks that a cost sits in bcrypt's valid range (4–31):
Hash.verifyConfiguration({ cost: 12 }); // true
Hash.verifyConfiguration({ cost: 2 }); // falsePass cost (and optionally algorithm) on individual make / needsRehash calls when you need a one-off override without changing the process default.
Hashing passwords
Call make with the plain-text password:
import type { Request } from "@bunyad/http";
import { redirect } from "@bunyad/http";
import { Hash } from "@bunyad/auth";
export default class PasswordController {
async update(request: Request) {
const user = request.user;
if (!user) {
return redirect("/login");
}
const newPassword = String(request.input("newPassword") ?? "");
await user.fill({ password: await Hash.make(newPassword) }).save();
return redirect("/profile");
}
}Adjusting the bcrypt cost
Pass cost in the options object. If you omit it, make uses the value from setRounds (default 10):
const hashed = await Hash.make("password", {
cost: 12,
});Using argon2id
Set algorithm to "argon2id" when you want Argon2id instead of bcrypt:
const hashed = await Hash.make("password", {
algorithm: "argon2id",
});Bun chooses Argon2id parameters. There are no memory, time, or threads options on Hash.make.
The `bcrypt` helper
bcrypt from @bunyad/auth is a thin wrapper around Hash.make with the bcrypt algorithm. An optional second argument sets the cost for that call:
import { bcrypt } from "@bunyad/auth";
const hashed = await bcrypt("secret");
const expensive = await bcrypt("secret", 12);Framework apps that call installGlobals() also expose bcrypt on globalThis. Prefer the import when the call site should stay explicit.
Verifying a password
check compares plain text to a stored hash. It returns true when they match:
if (await Hash.check("plain-text", hashedPassword)) {
// The passwords match
}Verification accepts bcrypt and Argon2 hashes that Bun can verify. Algorithm selection for check comes from the hash string itself, not from a config driver.
Determining if a password needs to be rehashed
needsRehash returns true when the stored hash does not match the algorithm and options you expect. Apps often run this after a successful login and rewrite the column when the work factor has changed:
if (await Hash.check(plain, hashed) && Hash.needsRehash(hashed)) {
user.password = await Hash.make(plain);
await user.save();
}For bcrypt (the default), needsRehash is true when the value does not look like a bcrypt hash, or when the embedded cost differs from options.cost / the current setRounds value:
Hash.needsRehash(hashed); // false if cost matches default
Hash.needsRehash(hashed, { cost: 4 }); // true when the hash used a different costFor algorithm: "argon2id", the hash must start with $argon2id$ or it needs rehashing.
Inspecting hashes
isHashed reports whether a string looks like a bcrypt or Argon2 digest:
Hash.isHashed(hashed); // true for $2… or $argon2…
Hash.isHashed("plain"); // falseinfo returns algorithm metadata shaped for inspection:
const meta = Hash.info(hashed);
// bcrypt: { algo: 1, algoName: "bcrypt", options: { cost: 10 } }
// argon2id: { algo: 2, algoName: "argon2id", options: {} }
// unknown: { algo: 0, algoName: "unknown", options: {} }isUsingCorrectAlgorithm checks that the hash matches the algorithm you intend (bcrypt or argon2id). isUsingValidOptions combines that check with needsRehash — it is true only when the algorithm matches and the hash does not need rehashing:
Hash.isUsingCorrectAlgorithm(hashed); // bcrypt prefix by default
Hash.isUsingCorrectAlgorithm(hashed, { algorithm: "argon2id" });
Hash.isUsingValidOptions(hashed, { cost: 10 });Method reference
| Method | Role |
|---|---|
make(value, options?) |
Hash a plain string (Promise<string>) |
check(value, hashed) |
Verify plain text against a hash (Promise<boolean>) |
needsRehash(hashed, options?) |
Whether cost / algorithm no longer match |
isHashed(value) |
Whether the string looks like a password hash |
info(hashed) |
Algorithm name and options for a hash |
setRounds(n) |
Set the default bcrypt cost; returns Hash |
cost() / rounds() |
Current default bcrypt cost |
verifyConfiguration(options?) |
Whether cost is in 4–31 |
isUsingCorrectAlgorithm(hashed, options?) |
Prefix matches the chosen algorithm |
isUsingValidOptions(hashed, options?) |
Correct algorithm and no rehash needed |
bcrypt(value, cost?) |
Helper that hashes with bcrypt |
HashOptions accepts algorithm?: "bcrypt" | "argon2id" and cost?: number. HashInfo is the return type of info.