Files
pn-new-crm/src/lib/portal/passwords.ts
Matt Ciaccio 8699f81879
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped
chore(style): codebase em-dash sweep + minor layout polish
Replaces every em-dash and en-dash with regular ASCII hyphens
across comments, JSX strings, and dev-facing logs. Mostly cosmetic
but stops the inconsistent mix that crept in over the last few
months (some files used em-dashes in comments, others didn't,
some used both).

Bundles two small dashboard-layout tweaks that touch a couple of
already-modified files:
- (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6
  pb-6 so page content sits closer to the topbar.
- Sidebar now receives the ports list it needs for the footer
  port switcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:57:01 +02:00

61 lines
1.9 KiB
TypeScript

import { randomBytes, scrypt as scryptCb, timingSafeEqual, createHash } from 'node:crypto';
import { promisify } from 'node:util';
const scrypt = promisify(scryptCb) as (
password: string,
salt: Buffer,
keyLen: number,
) => Promise<Buffer>;
const KEY_LENGTH = 64;
const SALT_LENGTH = 16;
/**
* Hash a password with a fresh random salt. Stored format is
* `salt:keyHex` (both as hex strings) so verification can re-derive without
* a separate salt column.
*/
export async function hashPassword(password: string): Promise<string> {
const salt = randomBytes(SALT_LENGTH);
const key = await scrypt(password, salt, KEY_LENGTH);
return `${salt.toString('hex')}:${key.toString('hex')}`;
}
/**
* Constant-time check of a candidate password against a stored
* `salt:keyHex` hash. Returns false on any malformed input rather than
* throwing - callers should treat false uniformly.
*/
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
const parts = stored.split(':');
if (parts.length !== 2) return false;
const [saltHex, keyHex] = parts;
if (!saltHex || !keyHex) return false;
let salt: Buffer;
let expected: Buffer;
try {
salt = Buffer.from(saltHex, 'hex');
expected = Buffer.from(keyHex, 'hex');
} catch {
return false;
}
if (expected.length !== KEY_LENGTH) return false;
const candidate = await scrypt(password, salt, KEY_LENGTH);
return timingSafeEqual(candidate, expected);
}
/**
* Mint a fresh raw token (returned to the caller) and its SHA-256 hash
* (stored in the DB). The raw token is meant to be embedded in a one-shot
* URL; only the hash persists.
*/
export function mintToken(byteLength = 32): { raw: string; hash: string } {
const raw = randomBytes(byteLength).toString('base64url');
const hash = createHash('sha256').update(raw).digest('hex');
return { raw, hash };
}
export function hashToken(raw: string): string {
return createHash('sha256').update(raw).digest('hex');
}