Skip to content

Add query metrics, batch retry recovery, input sanitization, and HTTP retry with backoff - #368

Merged
Cjay-Cyber-2 merged 79 commits into
ASTROIDX556:mainfrom
Depo-dev:feat/query-metrics-batch-recovery-validation-retry
Oct 2, 2026
Merged

Cjay-Cyber-2 merged 79 commits into
ASTROIDX556:mainfrom
Depo-dev:feat/query-metrics-batch-recovery-validation-retry

Conversation

@Depo-dev

Copy link
Copy Markdown
Contributor

Summary

Note: this repo (NestJS + Prisma + Stellar/Horizon integration) doesn't have the exact file paths named in the original issues (src/repositories/, src/streams/transactions.ts, src/services/batchProcessor.ts, src/controllers/userController.ts) - those don't exist in the codebase. Each issue's underlying concern was mapped to its closest real equivalent.

Test plan

  • npx tsc --noEmit - clean
  • npx eslint on all changed files - clean
  • npx prettier --write on all changed files
  • npx vitest run - full suite, 1103/1103 passing (includes 19 new tests)

Closes #326
Closes #322
Closes #324
Closes #330

…vements

Implements four major feature requests for enhanced API observability,
security, and transaction simulation capabilities:

ASTROIDX556#247 - Prometheus Metrics Interceptor
- Create MetricsInterceptor for HTTP request metrics collection
- Add request duration histograms, active request gauges, and request counters
- Categorize metrics by route, method, and status code
- Exclude /metrics endpoint from self-instrumentation
- Add comprehensive unit tests (9 tests)

ASTROIDX556#246 - Stellar Transaction Simulation Service
- Create StellarSimulationService for transaction simulation
- Integrate with Soroban RPC for XDR validation and simulation
- Include risk assessment and fee estimation
- Add circuit breaker protection for RPC failures
- Add XDR validation helper method
- Add comprehensive unit tests with mocked Stellar RPC (24 tests)

ASTROIDX556#245 - Cryptographic API Key Hashing Upgrade
- Upgrade from SHA-256 to Argon2id for enhanced security
- Implement memory-hard algorithm resistant to GPU/ASIC attacks
- Add timing-attack resistant comparison via Argon2 verification
- Maintain SHA-256 fallback for backward compatibility
- Update ApiKeyService with dual-algorithm verification
- Add comprehensive unit tests (32 crypto tests, 15 API key tests)

ASTROIDX556#244 - Webhook Retry and Dead Letter Queue
- Verify existing implementation meets all requirements
- Confirm exponential backoff with jitter (2000ms base, 20% jitter)
- Confirm 5 max attempts and non-transient error detection
- Verify dead-letter handler via DeadLetterService
- All existing tests passing

Closes ASTROIDX556#247, ASTROIDX556#246, ASTROIDX556#245, ASTROIDX556#244

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@drips-wave

drips-wave Bot commented Sep 28, 2026

Copy link
Copy Markdown

@Depo-dev Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

… logging

Add runWorkerJob, a wrapper every background worker now routes its handler
through. It times the job via WorkerMetricsService when available, logs a
structured completion record, and classifies failures before rethrowing:
transient failures log a job.retrying warning, while a failure on the final
attempt or an UnrecoverableError logs a job.dead-lettered error with the
scrubbed payload and stack. The original error is always rethrown untouched
so BullMQ retry semantics are preserved, and logging can never mask it.

Add scrubForLog/scrubString, which redact sensitive keys (sharing the audit
sanitizer's key list) and secret-shaped substrings such as Stellar seeds,
bearer tokens and URL credentials, and coerce cycles, bigints and errors
into JSON-safe values.

QueueFailureListener now scrubs its log line as well; previously the raw
webhook payload, including its signing secret, was written to the error
log. The dead-letter copy keeps the raw payload so re-drive still works.
@Cjay-Cyber-2

Copy link
Copy Markdown
Contributor

Merge Conflict — Action Needed

This pull request has merge conflicts with the base branch (main).

What to do: update your branch by merging or rebasing against main, resolve any conflicts locally, and push the result.

@Cjay-Cyber-2

Copy link
Copy Markdown
Contributor

Merge Conflict — Action Needed

This pull request has merge conflicts with the base branch (main).

What to do: update your branch by merging or rebasing against main, resolve any conflicts locally, and push the result.

@Depo-dev
Depo-dev force-pushed the feat/query-metrics-batch-recovery-validation-retry branch from 48367ae to 91a4da0 Compare September 29, 2026 10:44
IyanuOluwaJesuloba and others added 2 commits September 29, 2026 18:13
- docs/configuration.md was missing entries for DATABASE_SLOW_QUERY_THRESHOLD_MS,
  DATABASE_CONNECT_RETRY_ATTEMPTS, DATABASE_CONNECT_RETRY_DELAY_MS, and the
  PUBLIC_RATE_LIMIT_* vars, failing the configuration-documentation test.
- retry.util.spec.ts left three rejected promises unhandled between
  `runAllTimersAsync()` and the `expect(...).rejects` assertion that
  attaches the handler; attach a no-op .catch() immediately after creating
  each promise so fake-timer-driven rejections don't fire as unhandled
  rejections mid-test-run.
@Depo-dev
Depo-dev force-pushed the feat/query-metrics-batch-recovery-validation-retry branch from 6dc43bc to d49abf1 Compare September 29, 2026 20:24
samad13 and others added 18 commits September 29, 2026 22:38
Every authenticated request performed a Redis round trip against the
token blacklist to answer "is this session still revoked?". This adds a
short-TTL caching layer in front of the blacklist so repeated
verifications within one window skip the Redis query entirely.

- Add CacheService: a small get/set/delete cache over the shared
  REDIS_CLIENT with TTL-bounded entries, JSON payloads, and SCAN-based
  prefix invalidation. Every operation degrades to a no-op/miss on Redis
  failure so caching can never break the request path.
- Add TokenVerificationCacheService: caches per-session revocation
  answers for TOKEN_CACHE_TTL seconds (default 30, well below the
  15-minute access-token lifetime) and exposes invalidation hooks.
- Wire the cache into JwtStrategy.validate (cache-first, source of truth
  on miss, fail-open unchanged on Redis outages).
- Hook invalidation into every revocation path: TokenBlacklistService
  drops the cached answer after each blacklist write (including on
  Redis-outage fallback), AuthService invalidates on logout and on
  refresh rotation, so revocations are observed immediately instead of
  after the TTL window.

Revocation reliability is preserved because no cached answer outlives
its TTL, and explicit logout/rotation clears the entry at once.

Tests: unit suites for CacheService and TokenVerificationCacheService
(hits, misses, resolver fallback, invalidation hooks), an integration
suite proving repeated authentications trigger a single blacklist
lookup and that logout flips a cached-valid session to 401 immediately,
plus updated JwtStrategy/TokenBlacklistService/api-key integration
suites for the new wiring.

Closes ASTROIDX556#341

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
…ic routes

Public endpoints are the first thing abusive traffic hits, but their
limits were a single fixed IP budget: every public route shared
PUBLIC_RATE_LIMIT_MAX_REQUESTS per window, and all clients behind one
shared address (NAT, office egress, CI runners) exhausted one bucket
together. This makes the public rate limiter configurable per route and
per client identifier, on the existing Redis sliding-window counter.

- Add @PublicRateLimit(max, windowSeconds) decorator: per-route (or
  per-controller) budget overrides resolved by the guard through
  Reflector; the global PUBLIC_RATE_LIMIT_* settings remain the default.
- Add PUBLIC_RATE_LIMIT_CLIENT_IDENTIFIERS (optional, comma-separated,
  currently 'apiKey'): when enabled, a presented x-api-key or
  ApiKey/Bearer ak_... Authorization header is folded into the bucket
  key so distinct key-holding clients behind one IP get their own
  budgets. The IP always participates; keyless callers share the
  plain-IP bucket as before.
- Extract a testable PublicRateLimitGuard.check() returning the full
  decision (allowed, limit, windowSeconds, count, resetAt); canActivate
  keeps its existing 429 + X-RateLimit-Limit/Remaining/Reset +
  Retry-After contract and in-memory fallback on Redis outage.

Tests: unit suites for per-route rule resolution, identifier bucketing
(with/without identifiers configured), header correctness on allowed
and limited requests, and @SkipPublicRateLimit() interaction with rules;
an HTTP-level integration suite simulating bursts that proves the
429-with-headers behaviour at the global limit, per-route overrides
(next to unaffected sibling routes), and per-key budget isolation.
Existing public-rate-limit suites pass unchanged (bucket keys keep the
ip: prefix, so stored counters stay compatible).

Closes ASTROIDX556#342

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
…d docs

Get CI green for the token-verification cache PR by fixing pre-existing
main-branch type errors alongside PR-specific ones: Express specs no longer
use Fastify-only app.inject, Stellar mocks match the real Soroban result
interface, the transaction spec exercises the actual create pipeline,
TokenBlacklistService resolves the global REDIS_CLIENT token explicitly,
and the configuration docs cover every THROTTLE_* env var the docs test
asserts.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
…d docs

Get CI green for the token-verification cache PR by fixing pre-existing
main-branch type errors alongside PR-specific ones: Express specs no longer
use Fastify-only app.inject, Stellar mocks match the real Soroban result
interface, the transaction spec exercises the actual create pipeline,
TokenBlacklistService resolves the global REDIS_CLIENT token explicitly,
and the configuration docs cover every THROTTLE_* env var the docs test
asserts.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
…able

The duplicated helper also ignored its parameter while the config factory
read the raw variable at the call site with none, so the module never
compiled. Collapse to a single parser that takes the raw value.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
…tive operations

Closes ASTROIDX556#255

Adds the @auditlog() decorator (with optional action/entity metadata) and reworks AuditLogInterceptor so only decorated routes are persisted, keeping read-only traffic free of audit writes.

Each record captures the actor (human user id or acting agent), client IP, HTTP method, request path, a SHA-256 fingerprint of the sanitized payload, the sanitized body itself, the final response status and handler duration. Secrets, keys, tokens, signatures and mnemonics are redacted recursively without mutating the original request body, and @SkipAudit() always wins.

Applied to the sensitive policy, budget and API-key endpoints; persistence stays fire-and-forget so a failed audit write never breaks the client request.
…ing policies

Closes ASTROIDX556#254

Introduces SpendingPolicyRepository (src/modules/policies/spending-policy.repository.ts) as the single owner of every Prisma call for policies: create/find/update/soft-delete, the paginated read, the rolling spend aggregation used by velocity checks, the POLICY_EVALUATED audit row, and an interactive withTransaction() helper. Every method funnels through one error-handling wrapper that logs the operation (plus the Prisma error code) and rethrows the original error, so error handling and transaction safety are uniform across the repository.

Adds SpendingPolicyService, which injects the repository and now owns spending-policy validation and enforcement (staleness checks, daily velocity limit, evaluation audit) with no direct Prisma access. PolicyService is reduced to orchestration - domain events plus the pure PolicyEngine - and delegates all persistence to SpendingPolicyService, which removes the direct prisma.transaction/prisma.auditLog calls from the service layer. PolicyRepository is replaced by the new, better-named repository.

Unit tests cover the repository against a mocked Prisma client (including transaction usage, Decimal summing and error propagation) and the service against a mocked repository (validation, pagination, not-found, velocity limits and audit failure swallowing).
…hook deliveries

Closes ASTROIDX556#253

Centralises the webhook queue configuration in src/queues/webhook.queue.ts: 5 attempts, exponential backoff with a 2000ms base, the jittered custom backoff strategy, 24h retention of failed jobs and the dead-letter destination. Both the queue registration and the enqueue path consume the same constants, so the API and the worker can no longer drift apart.

Adds WebhookAuditService, which appends a WEBHOOK_DELIVERY_FAILED record (subscriber, event, attempts, HTTP status, failure reason) to the audit trail for deliveries that will never be retried - either an unrecoverable 4xx or the final attempt after retries are exhausted. Both the webhook processor and the worker call it through a fire-and-forget hook that swallows and logs its own failures, so audit problems can never mask the delivery error, crash the NestJS process or interfere with BullMQ retry/backoff.

Tests cover the retry/backoff/DLQ configuration and jitter envelope, the audit service (including persistence failures) and the processor's terminal-failure behaviour - retry without auditing while attempts remain, exactly one audit entry when retries are exhausted or a 4xx is returned, and error preservation when auditing fails.
…quency agent endpoints

Closes ASTROIDX556#251

Adds AgentThrottlerGuard in src/common/guards/agent-throttler.guard.ts, extending @nestjs/throttler's ThrottlerGuard so counters keep running through the shared RedisThrottlerStorage while the guard adds agent-aware behaviour: the bucket key is derived from the acting agent (x-agent-id, a route/body/query agentId, or an agent-bound API-key principal) instead of the organization or IP, falling back to the organization, then a hashed API key, then the client IP (honouring x-forwarded-for) for unauthenticated public routes.

Introduces a third 'agent' tier (THROTTLE_AGENT_LIMIT, default 300/window) alongside api/auth in config/throttler.config.ts and env.validation.ts, and routes each request to exactly one tier - explicit @ThrottleTierDecorator() metadata wins, otherwise agent-identified traffic uses the agent tier and everything else the api tier. Rejections are standard 429s that also carry plain Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining headers, and the tier limit is advertised on allowed responses too.

Applied selectively to the agent-facing controllers (agents, transactions, wallets) via @UseGuards so the existing global AstroidThrottlerGuard keeps enforcing api/auth untouched. Vitest coverage asserts tier routing, tracker resolution for agents/orgs/API keys/anonymous callers, header emission and 429 enforcement when a single agent's burst exhausts its budget.
main's HEAD was red (27 typecheck errors) from unrelated merged PRs
(ASTROIDX556#357, ASTROIDX556#374, ASTROIDX556#381-ASTROIDX556#388). Fixed to get this branch's CI green:

- throttler.guard.ts / sliding-window-throttler.guard.ts: AuthenticatedUser
  has no `sub` or `tier` field; use `.id` and treat `tier` as an optional
  extension until the type actually carries it.
- agent.controller.ts: imported SlidingWindowThrottlerGuard/SlidingWindowLimit
  (unused) instead of the AstroidThrottlerGuard actually referenced by
  @UseGuards.
- event-names.ts: dropped a duplicate TransactionRiskScoringRequested key.
- risk.service.ts: the TransactionCreated handler built a RiskFactorsInput
  with fields (`destination`, `velocityCount`, `isNewRecipient`) that don't
  exist on the type; mapped to the real shape instead. Removed dead
  lowRisk/createEventBus fixtures left over in risk.service.spec.ts.
- stellar.service.ts (Soroban variant): fixed getTransactionInfo calling a
  nonexistent client method (getTransaction), simulateTransaction being
  called with a bare string instead of {transactionXdr}, and an error
  message interpolating the whole error object instead of `.message`.
  Rewrote stellar.service.spec.ts's mocks/assertions to match the real
  SorobanSimulationResult shape, and fixed two tests reusing a `mockOnce`
  across two separate calls to the service (second call fell through to
  the unmocked default and threw on undefined).
- transaction.service.spec.ts targeted a pre-broadcast Soroban simulation
  step that was never wired into TransactionService.create, against a
  StellarService overload TransactionService doesn't even inject; skipped
  with an explanation rather than fabricating the feature.
- sensitive-rate-limit.integration.spec.ts: replaced Fastify-only
  `app.inject()` (this app runs on platform-express) with a small
  http.request helper; the throttler in the test module was unnamed
  ('default'), so AstroidThrottlerGuard's per-tier name match against the
  route's default 'api' tier always skipped it — named it 'api' to match.
- Removed remaining `any` usages in the touched guard files/specs.
- docs/configuration.md: documented THROTTLE_WEBHOOK_LIMIT,
  THROTTLE_API_BURST, THROTTLE_AUTH_BURST, THROTTLE_WEBHOOK_BURST (missing
  from ASTROIDX556#386), failing the configuration-documentation test.
A merge of upstream main (PR ASTROIDX556#372's overlapping CI fixes) into this
branch left several files with duplicated/glued content instead of
resolved conflicts:

- sliding-window-throttler.guard.spec.ts: duplicate makeContext()
  declaration (one-line + multi-line versions concatenated).
- sliding-window-throttler.guard.ts: `limit` reverted to `const` while
  the tier-adjustment branches below still reassign it.
- sensitive-rate-limit.integration.spec.ts: my raw http.request-based
  version and another (cleaner, fetch()-based) version from main were
  concatenated rather than merged — duplicate imports, an unclosed `it`
  block. Kept the fetch()-based version.
- stellar.service.spec.ts: duplicate `error` key in a SorobanSimulationResult
  object literal.
- transaction.service.spec.ts: my placeholder describe.skip stub was
  prepended to a real, correctly implemented version of the same suite
  that showed up on main independently. Kept the real implementation,
  dropped the stub.
Deb-Auth and others added 15 commits October 2, 2026 18:45
Add PublicRateLimitGuard, a global guard that applies a per-IP sliding
window limit to every unauthenticated route: handlers marked @public()
and any route under /<API_PREFIX>/public/. Requests beyond the limit
are rejected with 429 Too Many Requests and a Retry-After header.

- X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset are set
  on every limited response
- counters live in the shared REDIS_CLIENT via an atomic Lua sliding
  window script, so all replicas enforce one budget per IP; rejected
  requests are not recorded
- if Redis is unavailable the guard falls back to an in-memory window
  per instance instead of failing open
- thresholds are configurable with PUBLIC_RATE_LIMIT_ENABLED,
  PUBLIC_RATE_LIMIT_MAX_REQUESTS, PUBLIC_RATE_LIMIT_WINDOW_SECONDS and
  PUBLIC_RATE_LIMIT_TRUST_PROXY (X-Forwarded-For is ignored by default)
- @SkipPublicRateLimit() exempts routes; applied to the
  network-restricted /metrics scrape endpoint
- add store and guard unit tests plus an HTTP burst integration test
…DX556#378)

Replace the { success, error: { code, message }, requestId } error
envelope with a uniform problem details body served as
application/problem+json:

  { type, title, status, detail, instance, code, requestId, details? }

- type is a stable URN per ErrorCode (urn:astroid:problem:<code>); HTTP
  errors without a dedicated code use about:blank with the reason phrase
- title comes from a new ERROR_TITLE map kept exhaustive by the type
  system; instance is the request path without its query string
- code and requestId are kept as extension members so clients can keep
  switching on the machine-readable code
- ZodValidationException now keeps its VALIDATION_ERROR code and
  field-level details instead of collapsing to BAD_REQUEST
- unhandled exceptions still map to a generic 500 INTERNAL_ERROR
  without leaking internals
- update the shared response types and API documentation
- rewrite the filter spec and add an HTTP integration test covering
  validation, authentication, domain, not-found and server errors
* fix: worker error handling + log scrubbing (resolve PR ASTROIDX556#370 conflicts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xRrTYwEy2y8yow4o8iUQ8

* test: add job-worker spec and queue-failure-listener scrubbing test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012xRrTYwEy2y8yow4o8iUQ8

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ASTROIDX556#338, ASTROIDX556#339) (ASTROIDX556#369)

* perf(analytics): batch dashboard overview queries into one transaction

Closes ASTROIDX556#338

AnalyticsService.overview() issued 7 independent queries (counts,
spend aggregates, status/risk group-bys) via Promise.all, each its own
roundtrip. Batches the 3 counts and 2 aggregates into a single
$transaction([...]) call; the 2 groupBy calls stay outside the batch
since Prisma's groupBy return type doesn't infer correctly inside a
$transaction array. Response shape is unchanged. Existing indexes on
organizationId/status/createdAt already cover these queries.

* test(audit): cover pagination and sorting edge cases for activity log

Closes ASTROIDX556#339

The audit log (this repo's activity log) already supported
page/limit/sort/order/filter query params and returned pagination
metadata (total, totalPages, hasNext, hasPrev), with limit capped at
100. Adds unit test coverage for the previously-untested list()
method: normal pagination, empty results, out-of-bounds pages,
invalid sort field fallback, ascending order, and entity filtering.

* fix(migrations): resolve colliding timestamp between two merged migrations

Migrations 20260928120000_add_agent_contribution_stats_index and
20260928120000_add_notifications_user_created_at_index landed with the
same 14-digit timestamp prefix from two separately merged PRs (ASTROIDX556#374,
ASTROIDX556#357), which scripts/verify-migrations.sh rejects as a conflict. Bumps
the notifications index migration to 20260928120001; both migrations
are independent, additive CREATE INDEX statements with no ordering
dependency between them, so the rename is safe.

* fix(ci): document missing env vars and fix flaky retry.util test

Two pre-existing, unrelated-to-this-PR CI failures fixed while unblocking
this branch:

- docs/configuration.md was missing 7 env vars added by recent merges
  (DATABASE_SLOW_QUERY_THRESHOLD_MS, DATABASE_CONNECT_RETRY_ATTEMPTS,
  DATABASE_CONNECT_RETRY_DELAY_MS, PUBLIC_RATE_LIMIT_*), which
  env.validation.spec.ts asserts against. Documented all 7.
- retry.util.spec.ts had 3 tests that create a rejecting promise, advance
  fake timers with vi.runAllTimersAsync(), then attach the rejection
  assertion afterward — a race that surfaces as an unhandled rejection
  under full-suite load (deterministic once >100 files run together).
  Attaching a no-op .catch() immediately after creating the promise
  prevents the unhandled state without changing what each test asserts.
tecch-wiz pushed a commit to tecch-wiz/astroid-api that referenced this pull request Oct 2, 2026
@Cjay-Cyber-2
Cjay-Cyber-2 merged commit aca42eb into ASTROIDX556:main Oct 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment