Skip to content

Encryption and Hashing

Authentication, integrity checks, and secure comparisons all need cryptographic primitives. Slingshot exposes a small, audited set: argon2id password hashing through the runtime, HMAC-SHA256 signing for arbitrary payloads, SHA-256 hashing, and timing-safe equality.

PrimitiveUse it for
runtime.password.hashHashing user passwords (argon2id by default in Bun)
runtime.password.verifyVerifying a plaintext password against a stored hash
hmacSignSigning webhook payloads, cookies, cursors
hmacVerifyVerifying inbound HMAC signatures
sha256Non-keyed hashing — fingerprints, dedup keys
timingSafeEqualComparing secrets without leaking timing

Password hashing happens through the runtime so it stays portable across Bun, Node, and edge hosts. The auth plugin uses these methods internally for credential authentication — you only call them yourself when you’re rolling custom credential handling:

// @skip-typecheck
import { bunRuntime } from '@lastshotlabs/slingshot-runtime-bun';
const runtime = bunRuntime();
const hash = await runtime.password.hash('correct horse battery staple');
const ok = await runtime.password.verify('correct horse battery staple', hash);
// ok === true

The Bun runtime uses Bun.password.hash (argon2id by default with sane parameters). The Node runtime uses argon2 via the same interface. Stored hashes are self-describing — they include the algorithm and parameters, so a future parameter upgrade verifies old hashes correctly.

For signing arbitrary payloads — webhook bodies, cursor tokens, presigned URLs, or anything you’ll later verify — use hmacSign and hmacVerify:

// @skip-typecheck
import { hmacSign, hmacVerify } from '@lastshotlabs/slingshot';
const secret = process.env.WEBHOOK_SECRET!;
const payload = JSON.stringify({ event: 'order.shipped', orderId: 'ord_123' });
const sig = hmacSign(payload, secret);
// Send `payload` and `sig` to the recipient.
const valid = hmacVerify(payload, sig, secret);
// valid === true

Both functions accept secret: string | string[]. Pass an array when you’re rotating keys — the active key is at index 0, and rotated keys live after. New signatures use the active key; verification accepts any key in the list. This makes key rotation a two-step deploy: add the new key at index 0, wait for outstanding signatures to age out, then drop the old key.

For non-keyed hashes (fingerprints, deduplication keys, content hashes):

// @skip-typecheck
import { sha256 } from '@lastshotlabs/slingshot';
const fingerprint = sha256(`${userAgent}|${ipAddress}|${acceptLanguage}`);

When comparing any value that came from the network — signatures, tokens, secrets — use timingSafeEqual, never ===:

// @skip-typecheck
import { timingSafeEqual } from '@lastshotlabs/slingshot';
if (timingSafeEqual(providedToken, expectedToken)) {
// safe to proceed
}