hazo_secure
v1.0.1
Published
Security & compliance primitives — SSRF-safe fetch, rate limiting, field-level encryption, GDPR orchestration. Four subpath modules, pick the ones you need.
Readme
hazo_secure
Security & compliance primitives for Node.js / Next.js apps. Four independent subpath modules — import only the ones you need.
| Subpath | Purpose |
|---|---|
| hazo_secure/fetch | SSRF-safe safeFetch() — domain allowlist, private-CIDR blocker, redirect controls |
| hazo_secure/ratelimit | Sliding-window + token-bucket limiter with pluggable store (in-memory default, hazo_connect adapter) |
| hazo_secure/crypto | AES-256-GCM field-level encryption with key versioning and AAD support |
| hazo_secure/gdpr | Exporter / anonymiser registry + lifecycle orchestrator. Uses hazo_jobs for async export, hazo_files for ZIP delivery |
| hazo_secure/csrf | Double-submit CSRF protection for Next.js routes — token generation, cookie header, constant-time verification |
Install
npm install hazo_securehazo_logs is a required peer dep. hazo_connect, hazo_jobs, and hazo_files are optional peers — hazo_connect is needed for ConnectRateLimitStore; hazo_jobs and hazo_files are only needed for hazo_secure/gdpr.
Quick start
SSRF-safe outbound fetch
import { safeFetch } from "hazo_secure/fetch";
const res = await safeFetch("https://nominatim.openstreetmap.org/search", {
policy: {
allowedHosts: ["nominatim.openstreetmap.org"],
blockPrivateIps: true,
maxRedirects: 3,
timeoutMs: 5000,
},
});Rate limit a Next.js route
import { checkRateLimit } from "hazo_secure/ratelimit";
export async function POST(req: Request) {
const decision = await checkRateLimit(req, { windowMs: 60_000, max: 30 });
if (!decision.allowed) return new Response("Too Many Requests", { status: 429 });
// ...
}Encrypt a PII field
import { encryptField, decryptField } from "hazo_secure/crypto";
const stored = await encryptField(plaintext, { keys, aad: `user:${userId}` });
const recovered = await decryptField(stored, { keys });Register GDPR exporters
import { createGdprRegistry } from "hazo_secure/gdpr";
const gdpr = createGdprRegistry();
gdpr.registerExporter({
domain: "persons",
async *export({ userId }) {
for (const row of await db.personsByUser(userId)) {
yield { filename: `persons/${row.id}.json`, content: JSON.stringify(row, null, 2) };
}
},
});New in 1.0.0
hazo_secure/ratelimit — ConnectRateLimitStore (token bucket)
import { ConnectRateLimitStore } from "hazo_secure/ratelimit";
const store = new ConnectRateLimitStore({ getHazoConnect: () => db });
const limiter = createRateLimiter({ store, windowMs: 60_000, max: 30 });Stores token-bucket state in hazo_rl_buckets (see migrations/001_hazo_rl_buckets.sql). Run the migration before use. Requires hazo_connect peer.
hazo_secure/fetch — undici dispatcher + validateConnectIp
import { safeFetch, createSecureDispatcher, validateConnectIp } from "hazo_secure/fetch";
// Use standalone to close the DNS-rebinding gap at socket creation time
const dispatcher = createSecureDispatcher();
const res = await safeFetch(url, { policy, deps: { fetchImpl: (u, i) => fetch(u, { ...i, dispatcher }) } });
// Or validate resolved IPs yourself
validateConnectIp("169.254.169.254"); // throws — blocked CIDRNode.js only — undici is a Node dep. Edge runtime gets pre-flight checks only.
hazo_secure/crypto — HttpKeyProvider
import { HttpKeyProvider } from "hazo_secure/crypto";
const provider = new HttpKeyProvider({
endpoint: "https://kms.internal/keys",
headers: { Authorization: `Bearer ${token}` },
ttlMs: 30_000, // in-process cache TTL (default: 30 s)
});
provider.clearCache(); // force next fetch to re-fetch from endpointCloud-agnostic — works with any HTTPS key endpoint that returns { keyId, key: "<base64>" }. Caches keys in-process to avoid latency on every encrypt/decrypt call.
hazo_secure/gdpr — registry enhancements
import { createGdprRegistry, GdprRegistryError } from "hazo_secure/gdpr";
const gdpr = createGdprRegistry({ auditIntent: logAuditEntry });
// Validate registry completeness
const issues = await gdpr.validateRegistry({ requiredDomains: ["persons", "payments"] });
// Export user data (streams records from all registered exporters)
for await (const file of gdpr.exportUser({ userId })) {
console.log(file.filename, file.content);
}
// Dry-run export without side effects
const files = await gdpr.dryRunExport({ userId });
// Anonymise user across all registered domains
await gdpr.anonymiseUser({ userId });GdprRegistryError is a typed error class — catch and instanceof-check it.
Runtime Compatibility
| Subpath | Edge runtime | Node.js (server) | Notes |
|---|---|---|---|
| hazo_secure | ✅ | ✅ | Types only, no runtime weight |
| hazo_secure/csrf | ✅ | ✅ | Web Crypto API only |
| hazo_secure/ratelimit | ✅ (MemoryRateLimitStore) | ✅ | ConnectRateLimitStore is Node-only (hazo_connect) |
| hazo_secure/fetch | ⚠️ pre-flight only | ✅ full protection | Edge: host/IP checks only; Node: + undici connect-time validation |
| hazo_secure/crypto | ❌ | ✅ | Uses node:crypto |
| hazo_secure/gdpr | ❌ | ✅ | Async iterables + optional hazo_connect |
Node.js ≥ 18 required for all Node-only subpaths.
Design
Each subpath is independent — no cross-imports between /fetch, /ratelimit, /crypto, /gdpr. The gdpr module consumes the others when a project wires them together, but the package itself doesn't entangle them.
See CLAUDE.md and AGENTS.md for architecture details.
License
MIT
