npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@happy-technologies/audit

v0.2.0

Published

Tamper-proof audit hash-chain kernel shared across Happy Technologies products: keyed HMAC-SHA256 entry hashing, chain verification, and an optional Postgres single-writer helper. Domain-agnostic; products own their audit_log schema and migrations; app ow

Downloads

370

Readme

@happy-technologies/audit

Tamper-proof audit hash-chain kernel shared across Happy Technologies products. Each audit entry is HMAC-SHA256-hashed, keyed with an app-held secret, together with the previous entry's hash for the same tenant, forming a per-tenant hash chain: any content edit, insert, delete, or reorder breaks the chain and is detectable. Because the hash is keyed, a party who can only write to the database (no app secret) cannot forge a chain that re-verifies: this is tamper-proof, not merely tamper-evident.

0.2.0 is a breaking change from 0.1.x. The unkeyed, plain-SHA-256 computeEntryHash/verifyAuditChain top-level exports are removed. Every consumer must call createAuditChain(secret) to get a keyed kernel. There is no data migration path baked into this package: because HMAC replaces plain SHA-256, chains computed under 0.1.x will not verify under 0.2.0. (No production audit data existed at the time of this change.)

Install

{
  "dependencies": {
    "@happy-technologies/audit": "^0.2.0"
  }
}

Usage

Root export: pure kernel

createAuditChain, stableStringify, and the types have no dependency on Postgres or any database driver, so any TypeScript runtime can use them.

import { createAuditChain, type ChainRow } from "@happy-technologies/audit";

// secret comes from the app's own env/KMS -- see "Key management" below.
const chain = createAuditChain(process.env.AUDIT_HMAC_SECRET!);

const entryHash = chain.computeEntryHash(
  {
    id: "entry-1",
    action: "document.created",
    actorId: "user-1",
    tenantId: "tenant-1",
    resourceType: "document",
    resourceId: "doc-1",
    metadata: { source: "upload" },
    ipAddress: "127.0.0.1",
    userAgent: "Mozilla/5.0",
  },
  null, // prevHash, or the previous row's entryHash
);

const rows: ChainRow[] = []; // loaded from storage, ordered (timestamp ASC, id ASC)
const result = chain.verifyAuditChain(rows);
if (!result.ok) {
  console.error(`chain broken at ${result.brokenAt} (${result.reason})`);
}

createAuditChain returns a ChainKernel bound to one secret:

interface ChainKernel {
  computeEntryHash(fields: AuditChainFields, prevHash: string | null): string;
  verifyAuditChain(rows: readonly ChainRow[]): ChainVerification;
}

./pg subpath: optional Postgres write helper

For consumers using pg directly, insertAuditEntry provides the enforced single-writer path: it acquires a per-tenant pg_advisory_xact_lock, reads the tenant's last entry_hash, computes the new (keyed) hash via the ChainKernel you pass in, and inserts prev_hash + entry_hash in one transaction.

import { createAuditChain } from "@happy-technologies/audit";
import { insertAuditEntry } from "@happy-technologies/audit/pg";

const chain = createAuditChain(process.env.AUDIT_HMAC_SECRET!); // create once, reuse

const client = await pool.connect();
try {
  await insertAuditEntry(
    client,
    {
      id: "entry-1",
      timestamp: new Date(),
      action: "document.created",
      actorId: "user-1",
      tenantId: "tenant-1",
      resourceType: "document",
      resourceId: "doc-1",
      metadata: { source: "upload" },
      ipAddress: "127.0.0.1",
      userAgent: "Mozilla/5.0",
    },
    chain,
  );
} finally {
  client.release();
}

pg is an optional peerDependency: only install it if you import from @happy-technologies/audit/pg. The root export never imports pg.

Key management (consumer responsibility)

The app supplies the HMAC secret; this package never does.

  • The secret is created and held by the CONSUMER application, sourced from an environment variable or a KMS-backed secret store at process start. It is passed to createAuditChain(secret) once (typically at startup) and the returned ChainKernel reused across calls.
  • The secret MUST NOT be hardcoded, defaulted, or committed anywhere in this package's source, and MUST NOT be stored in the audit database itself (storing the verification key next to the data it protects would let a DB-write attacker forge new entries, defeating the entire point of the keyed chain).
  • createAuditChain throws if given an empty/undefined secret; it never falls back to an unkeyed or default mode.
  • Losing the key does not lose data. If the secret is lost or rotated without a deliberate re-chaining migration, existing chain rows remain intact and readable, but verifyAuditChain can no longer confirm their integrity under the new/absent key. Treat secret loss as an availability incident for verification, not a data-loss incident.
  • Rotating the secret starts a new chain under the new key; consumers that need continuous verifiability across a rotation must design that migration themselves (out of scope for this package).

Canonical field order contract

ChainKernel#computeEntryHash computes the HMAC-SHA256 digest, keyed with the kernel's secret, of a canonical JSON serialization of the tuple:

[prevHash ?? "", id, action, actorId, tenantId, resourceType, resourceId,
 metadata ?? {}, ipAddress ?? null, userAgent ?? null]
  • The row timestamp is deliberately excluded from the hash. Ordering integrity comes from prevHash linkage, not from a hashed timestamp value; timestamp-value integrity is a documented non-goal.
  • metadata is serialized with stableStringify, which sorts object keys recursively so key order (e.g. after a JSONB round-trip) never changes the hash.

This field order and serialization is a versioned contract. Changing it breaks every chain computed under the old contract; adding a field must be a deliberate, versioned change (e.g. a new computeEntryHashV2), not a silent edit to computeEntryHash.

Non-goals

  • This package does not define an audit_log table schema, the set of audited actions, or additive column migrations. Each consuming app owns its own schema and migration (e.g. happy-knowledge's scripts/040_audit_hash_chain.sql).
  • This package does not absorb SDK-owned or purpose-specific chains (e.g. happyhive's governance_provenance table) and does not assert a global cross-tenant chain; chains are per-tenant.
  • This package does not manage the HMAC secret's lifecycle (generation, storage, rotation) -- that is entirely the consuming application's responsibility. See "Key management" above.
  • This package does not implement external anchoring (e.g. periodically publishing chain-head hashes to an external, independently-controlled ledger or timestamping service) to defend against an attacker who compromises both the database AND the app secret simultaneously. That would further strengthen the tamper-proofing guarantee beyond "attacker lacks the secret" to "attacker lacks the secret and cannot also alter an external record," but it is optional future hardening, not implemented here.
  • TypeScript-only, using Node's crypto.createHmac. No Rust implementation exists or is planned; there is no polyglot twin to keep in sync.