Catalog-driven credit enforcement for NestJS, backed by atomic Redis Lua transactions and BullMQ lifecycle transport.
- Complete integration procedure
- Developer reference
- Technical architecture
- Redis keyspace
- Lua state transitions
- Production credit debugging runbook
Catalog selection happens at build time. Each command creates one installable tarball containing exactly one runtime catalog:
npm run build:example # EXAMPLE_API -> *-example.tgz
npm run build:ssi # SSI_API -> *-ssi.tgz
npm run build:kyc # CAVACH_API -> *-kyc.tgz
npm run build # builds and verifies all three profilesInstall the artifact that matches the host service. The selected catalog is immutable at runtime; passing a catalog through module options does not replace the bundled catalog.
- Each SDK artifact bundles exactly one route and price catalog. Controllers do not declare prices.
- A catalog charge may include an optional request-body
whencondition. Charges without one remain unconditional. - Startup fails when the NestJS route table and bundled catalog differ.
- The host provides one
CREDIT_REDIS_CLIENT. The SDK creates and closes its own Redis Stream and BullMQ connections. - SDK defaults cover leases, retention, recovery batches, Stream limits, and queue names.
prodrequests enforce credit.devrequests emit usage events with zero deduction. The value is trusted per-request metadata, not a process-wide environment setting.
Use exported enums for protocol values:
CreditEnvironment.PROD // 'prod'
CreditEnvironment.DEV // 'dev'
CreditServiceType.EXAMPLE_API
CreditServiceType.SSI_API
CreditServiceType.CAVACH_API // catalog and transport identity
CreditAppType.SSI_API
CreditAppType.CAVACH_API // subject.appType wallet dimension
CreditType.API_CREDIT
CreditType.BLOCKCHAIN_TXN_CREDITUppercase PROD and DEV are invalid environment inputs.
npm install @hypersign-protocol/credit-middleware ioredis @nestjs/scheduleThe package includes BullMQ. Do not register a BullMQ provider or Redis Stream client for the SDK.
import { UnauthorizedException } from '@nestjs/common';
import {
CreditAppType,
CreditEnvironment,
CreditModule,
CreditType,
} from '@hypersign-protocol/credit-middleware';
CreditModule.forRootAsync({
imports: [CreditInfrastructureModule],
useFactory: () => ({
requestContextResolver: (unknownRequest: unknown) => {
const request = unknownRequest as AuthenticatedRequest;
const appId = request.service?.appId?.trim();
const environment = request.service?.env?.trim();
if (!appId) {
throw new UnauthorizedException('Trusted service appId is required');
}
if (
environment !== CreditEnvironment.PROD &&
environment !== CreditEnvironment.DEV
) {
throw new UnauthorizedException(
'Trusted service environment must be prod or dev',
);
}
return {
subject: {
tenantId: request.service?.subdomain?.trim() || undefined,
appId,
appType: CreditAppType.CAVACH_API,
creditType: CreditType.API_CREDIT,
},
requestId: request.requestId,
environment,
};
},
}),
});Authentication must populate the request context before the global credit interceptor runs. Do not accept wallet identity or environment from an unauthenticated body, query parameter, or header.
See the integration guide for the Redis provider, Nest module, scheduler, and verification flow.
A wallet is identified by:
tenantId + appType + appId + creditType
All values are trimmed and case-sensitive. serviceType is transport metadata
and is not part of wallet identity.
Each grant creates an immutable plan with its own amount, grant time, expiry,
reference, and critical-balance threshold. Plans are consumed by grantedAt,
then planId.
Full metadata is retained for every unfinished plan and, by default, the newest
100 depleted, expired, or revoked plans per wallet. Older terminal metadata is
removed only after all SDK-tracked reservations referencing that plan finalize.
Persistent ownership and grant-reference records still prevent an old payment
from being applied twice. Configure the full-history limit with
terminalPlanRetentionCount.
await creditService.grant({
subject,
planId: 'plan_01',
amount: 100,
criticalBalance: 40,
grantedAt: Date.now(),
expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1_000,
referenceId: 'payment_01',
});A reservation may span several plans. Allocation is all-or-nothing: if the complete price cannot be funded, no plan is changed. Commit, rollback, and recovery use the allocations stored on the reservation rather than recalculating FIFO order.
Grant retries are idempotent when every immutable field matches. Reusing a
planId or referenceId with different semantics is rejected.
catalog match
-> retain only charges whose optional request-body condition matches
-> resolve trusted subject and environment
-> prod: reserve plan allocations
-> dev: emit CREDIT_OBSERVED with deductedAmount=0
-> execute controller
-> prod: commit, retain deferred work, or roll back
Before the first plan grant, getBalance() returns null and priced prod
requests return HTTP 402. A plan stored only in an external database cannot
fund a request; the SDK must first apply its grant command.
Default queues:
| Purpose | Queue |
|---|---|
| Lifecycle events | credit.lifecycle |
| Trusted commands | credit.commands.CAVACH_API |
Transport envelopes use schemaVersion: 3. BullMQ delivery is at least once;
lifecycle consumers must enforce a durable unique constraint on eventId.
Workers on the same queue compete. Use separate lifecycle queue names when
multiple systems each need every event.
Run recovery every five minutes:
@Cron(CronExpression.EVERY_5_MINUTES, {
name: 'credit-recovery',
waitForCompletion: true,
})
async run(): Promise<void> {
await this.creditRecoveryService.runOnce();
}One pass handles expired reservation leases and expired unused plan credit. Redis transactions make concurrent recovery workers safe.
The repository contains host integration modules and a runnable external grant/lifecycle process:
npm run build:example
npm run start:example:eventsThe examples are not included in the npm package or public import surface. Instructions are in example/README.md.
- Redis 6.2+ with authentication, TLS, AOF, replication, tested backups, and
noeviction. - Stable request, plan, command, and payment identifiers.
- A five-minute recovery schedule.
- Idempotent lifecycle persistence keyed by
eventId. - Monitoring for HTTP 402 rates, rejected commands, BullMQ failures, recovery backlog, and pending Stream entries.
- An
eventStreamMaxLengthlarge enough for the longest expected relay outage.
Redis state uses <keyPrefix>:v2:{<hashTag>}:....