Docs: README · Architecture · Wire contract · SQL schema · TypeORM entities · Express · NestJS · Nuxt · Next.js · Release notes 1.0.0 · CHANGELOG · POC
RabbitMQ-backed credential propagation layer on top of @naskot/node-hmac-auth-core.
Two modes:
- Receive-only (no
managementadapter): consumes events from the mesh, applies them locally viahmacAuth.clients.*, publishes the return ACK. - Full mode (8 callbacks in
management): in addition to receive-only, the peer can drive credentials viaensure / rotate / revoke / sync. The lib publishes one event per target, signed with that target'spropagationSecret.
RabbitMQ is never visible at usage time. The application calls high-level helpers; the lib drives AMQP internally.
npm install @naskot/node-hmac-auth-core-propagation @naskot/node-hmac-auth-core amqplib redis@naskot/node-hmac-auth-core is a peer dep. amqplib is a runtime dep. A node-redis
v4+ client (or any client matching the same camelCase shape) is required at runtime
because the lib uses Redis for the monotonic cursor and the ack store.
import { createClient } from "redis";
import type { RedisLikeClient } from "@naskot/node-hmac-auth-core";
import { initializeHmacHttpAuth, initializeHmacMessageAuth } from "@naskot/node-hmac-auth-core";
import type { PropagationRedisClient } from "@naskot/node-hmac-auth-core-propagation";
import { createPropagator } from "@naskot/node-hmac-auth-core-propagation";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
// node-redis's `createClient` returns a deeply-overloaded type (`RedisClientType<...>`
// with hundreds of method overloads). Our two interfaces are intentionally minimal
// subsets of that surface, so TypeScript's structural check on the overloads fails
// without an explicit cast. We do it ONCE here instead of at every call site.
const redisClient = redis as unknown as RedisLikeClient & PropagationRedisClient;
// HTTP track: verifies signed HTTP requests, signs outbound calls.
// Namespace "hmac:http" → keys land under `hmac:http:clients`, `hmac:http:credentials-backup:*`, ...
// `:` is the canonical Redis separator; GUIs (RedisInsight, redis-commander, ...)
// render keys as a clean sub-tree `hmac > http > ...`.
const hmacHttpAuth = initializeHmacHttpAuth({
redis: redisClient,
namespace: "hmac:http",
secretToken: process.env.HMAC_SECRET_TOKEN,
});
// Message track: signs/verifies non-HTTP payloads (AMQP business messages,
// Kafka, etc.). Lives on a sibling sub-tree `hmac > message > ...` so HTTP
// and message credentials never share a key.
const hmacMessageAuth = initializeHmacMessageAuth({
redis: redisClient,
namespace: "hmac:message",
secretToken: process.env.HMAC_SECRET_TOKEN,
});
const propagator = await createPropagator({
amqpHost: process.env.AMQP_HOST!,
amqpPort: Number(process.env.AMQP_PORT ?? 5672),
amqpUser: process.env.AMQP_USER!,
amqpPassword: process.env.AMQP_PASSWORD!,
amqpProtocol: "amqp",
amqpQueue: process.env.AMQP_QUEUE!,
propagationSecret: process.env.PROPAGATION_SECRET!,
redis: redisClient,
// Pass either or both. The lib routes each propagation to the matching
// store based on `track: "http" | "message"` on the pending row.
hmacHttpAuth,
hmacMessageAuth,
management: {
upsertCredentialWithTargets: async (input) => {
/* INSERT/UPSERT your 4-table schema, status='pending' on the pivot */
},
rotateCredentialSecret: async (input) => {
/* UPDATE the plain on Table 1 + reset Table 3 to status='pending' */
},
fetchPendingPropagations: async () => {
/* JOIN Table 1 + Table 3 + Table 2, return PendingPropagation[] */
return [];
},
fetchSourcePropagationSecret: async (senderAmqpQueue) => {
/* SELECT propagation_secret FROM Table 2 WHERE target_amqp_queue = ? */
return null;
},
markTargetSent: async () => {
/* UPDATE Table 3 SET status='sent', sent_at=?, attempt_count=? */
},
markTargetSuccess: async () => {
/* UPDATE Table 3 SET status='success', applied_at=? */
},
markTargetError: async () => {
/* UPDATE Table 3 SET status='error', failed_at=?, reason=? */
},
markCredentialFullyPropagated: async () => {
/* UPDATE Table 1 SET secret_plain = NULL */
},
},
});
await propagator.ensure({
clientId: "client_partner_a",
secret: "plain-text-secret",
targets: ["x-ged-extract-mistral", "docker-nestjs-template"],
});
// From your cron:
await propagator.sync();Full per-framework guides:
Omit management from createPropagator(...). The peer consumes events, applies
them locally, publishes the return ACK with an empty propagationSecret (the source
warn-drops it but the local apply has already happened). ensure/rotate/revoke/sync
throw HmacPropagationError("MANAGEMENT_NOT_CONFIGURED").
| Helper | Mode | What |
|---|---|---|
propagator.close() |
always | Graceful shutdown of the AMQP connection and the consume loop |
propagator.ensure() |
full | Add or update a credential, write its targets in status='pending', apply locally immediately (no target needed) |
propagator.rotate() |
full | Rotate the plain for an existing clientId, apply the new hash locally immediately |
propagator.revoke() |
full | Delete propagated: NULL the plain on Table 1, repropagate credential.delete, drop locally immediately |
propagator.sync() |
full | Read pending rows, apply locally, publish one event per target with target secret |
The lib does not impose a schema; it only requires the callbacks to honor the input and output contracts. A reference 4-table MariaDB schema and ready-to-copy TypeORM entities live in docs/sql-schema.md and docs/entities-nestjs.md.
The wire is frozen at v1. The apply and ack event shapes are documented in docs/wire-contract.md so peer implementations in other languages can interoperate without code-sharing.
End-to-end demonstration in poc/:
- 1 NestJS authority (
management-nest) with MariaDB schemamgmt - 1 NestJS receive-only consumer (
consumer-nest) - 1 Nuxt v4 + Tailwind v4 full-mode peer (
consumer-nuxt) with MariaDB schemanuxt - 1 Next.js v15 + Tailwind v4 full-mode peer (
consumer-next) with MariaDB schemanext - 1 RabbitMQ, 1 MariaDB, 4 Redis (one per peer)
cd poc && docker compose up --buildNuxt and Next expose a UI that lists the state of the 4 Redis instances and lets
the operator drive ensure from a form.
Same notation as package.json. Caret ranges only: the table never claims an
upper bound that does not exist yet; bumping a major on either side replaces the
row.
@naskot/node-hmac-auth-core (peer dep) |
@naskot/node-hmac-auth-core-propagation |
Notes |
|---|---|---|
^1.0.0 |
^1.0.0 |
Wire contract pinned v1. Latest released: core 1.0.0. |
@naskot/node-hmac-auth-core is the auth primitives (sign, verify, manage
credentials). This package is the orchestration layer that distributes those
credentials across a mesh of peers over RabbitMQ. Together they reproduce the
behavior of the deprecated @naskot/node-hmac-auth 1.x but with two clean
boundaries:
- The core knows nothing about the mesh and can be used standalone.
- The propagation layer takes the core as a peer dep and adds RabbitMQ.
The decision to split was driven by single-responsibility: an HMAC verifier should not also be a credential-distribution agent.
MIT, see LICENSE.