Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

hashPassword

Computes a password's scrypt hash in a self-describing format, ready to store in a column or .env.

utility Since 1.2.0 @meekojs/meeko/password Source code
ts
import { hashPassword } from "@meekojs/meeko/password";

hashPassword(password) hashes a password with scrypt (N = 2^17, r = 8, p = 1, a random 16-byte salt and a 32-byte key) and returns it in the self-describing format scrypt$<N>$<r>$<p>$<sal>$<hash>, with the salt and hash in base64url. Store the result as is: in a column, or in .env for an administration account.

Import it from @meekojs/meeko/password, only on the server. It uses node:crypto, so it works in Node, Bun and Deno, not in Cloudflare Workers.

Usage#

ts
// server/api/signup.post.ts
import { ApiError, defineApiRoute } from "@meekojs/meeko/server";
import { hashPassword } from "@meekojs/meeko/password";
import { signupInput, signupResult } from "../../shared/schemas/signup";

export default defineApiRoute({
  origin: "required",
  rateLimit: { limit: 5, window: "10m" },
  body: signupInput,
  response: signupResult,
  status: 201,
  handler: async ({ body, context }) => {
    if (await context.services.db.users.exists(body.email)) throw new ApiError("EMAIL_TAKEN", 409);
    const passwordHash = await hashPassword(body.password);
    const user = await context.services.db.users.insert({ email: body.email, passwordHash });
    return { id: user.id };
  },
});

Type#

ts
function hashPassword(password: string): Promise<string>

Parameters#

Parameter Type Default Description
password string required The plaintext password, from 1 to 4096 bytes in UTF-8.

Return value#

A promise with the hash, such as scrypt$131072$8$1$<sal>$<hash> (84 characters with the current parameters). Each call uses a new salt, so the same password produces different hashes: always compare them with verifyPassword, never with ===.

Throws:

  • TypeError: The password must be a string if it is not a string;
  • RangeError: The password is empty for an empty string;
  • RangeError: The password exceeds 4096 bytes above the limit.

Examples#

The administration password in .env#

For a single account, without a database, the hash goes in an environment variable. meeko auth hash computes it without displaying the password:

sh
npx meeko auth hash                          # prompts for it twice, without displaying it
printf '%s' "$PASSWORD" | npx meeko auth hash  # or reads it from stdin
sh
# .env (unversioned)
ADMIN_PASSWORD_HASH=scrypt$131072$8$1$…

Change the password#

ts
// server/rpc/account/password.ts
import { ApiError, defineRpc } from "@meekojs/meeko/server";
import { hashPassword, verifyPassword } from "@meekojs/meeko/password";
import { changePasswordInput, okOutput } from "../../../shared/schemas/account";

export default defineRpc({
  input: changePasswordInput,
  output: okOutput,
  auth: "required",
  handler: async ({ input, context }) => {
    const user = await context.services.db.users.find(context.session.current!.data.userId);
    if (!user || !(await verifyPassword(input.current, user.passwordHash))) throw new ApiError("INVALID_CREDENTIALS", 401);
    await context.services.db.users.update(user.id, { passwordHash: await hashPassword(input.next) });
    await context.session.rotate();
    return { ok: true };
  },
});

Common mistakes#

Use it in a component#

Incorrectmeeko
<script lang="ts">
import { hashPassword } from "@meekojs/meeko/password";
</script>

meeko check warns with MEEK109: @meekojs/meeko/password sólo funciona en el servidor: el <script> y la plantilla de un componente también se ejecutan en el navegador, donde sus funciones fallan. Mueve ese código a <script server> o a server/; para usar sus tipos, impórtalos con import type. Desde 2.0 será un error.

Correctts
// server/api/signup.post.ts
import { hashPassword } from "@meekojs/meeko/password";

Compare hashes with ===#

Incorrectts
const ok = (await hashPassword(input.password)) === user.passwordHash; // always false

The salt is random: two hashes of the same password never match, and the comparison is not constant-time.

Correctts
const ok = await verifyPassword(input.password, user.passwordHash);

An empty or overly long password#

Incorrectts
await hashPassword("");

RangeError: The password is empty (and The password exceeds 4096 bytes above the limit). Validate the length in the input schema, so the user receives a 400 with its message.

Correctts
const signupInput = v.object({ password: v.pipe(v.string(), v.minLength(12, "Use at least 12 characters."), v.maxLength(1024)) });

Notes#

  • Cost. Each hash allocates 128 MiB of memory and takes on the order of hundreds of milliseconds. Meeko computes at most two concurrently per process; protect endpoints using it with rateLimit.
  • Security. Never log or return the password. The hash is safe to store, but not to publish: treat it as a secret.
  • Runtimes. Not available in Cloudflare Workers, which does not have node:crypto with scrypt.
  • Parameters. If a future version increases the cost, needsRehash tells you which hashes to recompute after a successful login.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/utils/hashpassword.md