Skip to content

Feat/api realtime position monitoring#326

Merged
KevinMB0220 merged 10 commits into
Galaxy-KJ:mainfrom
mariocodecr:feat/apiRealtimePositionMonitoring
Jun 30, 2026
Merged

Feat/api realtime position monitoring#326
KevinMB0220 merged 10 commits into
Galaxy-KJ:mainfrom
mariocodecr:feat/apiRealtimePositionMonitoring

Conversation

@mariocodecr

@mariocodecr mariocodecr commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Pull Request: Feat/api realtime position monitoring

📋 Description

Adds real-time position monitoring with liquidation alerts for the REST API (Issue #306, Roadmap #53). Users can configure alerts that watch a Stellar
account's health factor on a lending protocol (Blend in this iteration) and receive a webhook when it drops below a configured threshold.

Architecture summary:

  • Thin Express handlers → MonitoringAlertService → MonitoringAlertRepository (Supabase)
  • PositionMonitorWorker runs as a separate process from the API so polling doesn't duplicate when the REST API is horizontally scaled
  • Notification channels follow a Strategy pattern (INotificationChannel); webhook is implemented with HMAC-SHA256 signing, exponential backoff with
    jitter, and idempotency-key delivery
  • A pure AlertEvaluator (alert + observed value + cooldown → trigger?) keeps the decision rule testable in isolation
  • A ProtocolPool caches one IDefiProtocol client per (protocol, network) for the worker's lifetime to avoid rebuilding RPC clients on every tick

🔗 Related Issues

Closes #306

🧪 Testing

  • Unit tests added/updated
  • Integration tests added/updated (route layer with supertest)
  • Manual testing completed (end-to-end smoke driver)
  • All tests passing locally

✅Results:

  • 8 test suites, 64 tests, 100% green (~2s)
  • Coverage: 92.74% lines, 91.3% functions, 88.67% statements on new code

Smoke E2E (run with ./scratch/smoke.sh):

  • ✅ POST /api/v1/monitoring/alerts returns 201
  • ✅ Worker polls and evaluates alert
  • ✅ Webhook delivered with valid HMAC-SHA256 signature
  • ✅ alert_events.delivery_status = 'delivered' persisted
  • ✅ Cooldown (300s) prevents duplicate triggers — verified via last_evaluated_at >> last_triggered_at with a single event row

📚 Documentation Updates (Required)

  • Updated docs/AI.md with new patterns/examples
  • Updated API reference in relevant package README
  • Added/updated code examples
  • Updated ARCHITECTURE.md (if architecture changed)
  • Added inline JSDoc/TSDoc comments (every new file has @fileoverview + per-class/method comments where the contract isn't obvious from the
    signature)
  • Updated ROADMAP.md progress (mark issue as completed)

Documentation Checklist by Component:

If you modified packages/core/stellar-sdk/:

  • Updated packages/core/stellar-sdk/README.md
  • Added examples to docs/examples/stellar-sdk/
  • Updated type definitions documentation

If you modified packages/core/invisible-wallet/:

  • Updated packages/core/invisible-wallet/README.md
  • Added security notes to docs/SECURITY.md
  • Updated wallet flow diagrams in docs/ARCHITECTURE.md

If you modified packages/core/automation/:

  • Updated packages/core/automation/README.md
  • Added automation examples to docs/examples/automation/
  • Updated trigger/action types in docs

If you modified packages/core/defi-protocols/:

  • Updated packages/core/defi-protocols/README.md
  • Added protocol integration guide
  • Updated DeFi architecture section in docs/ARCHITECTURE.md

If you modified packages/core/oracles/:

  • Updated packages/core/oracles/README.md
  • Added oracle source documentation
  • Updated price feed examples

If you modified packages/contracts/:

  • Added Rust documentation comments (///)
  • Updated contract README with deployment instructions
  • Added contract interaction examples

If you modified tools/cli/:

  • Updated CLI command documentation
  • Added command examples to README
  • Updated help text in command definitions

🤖 AI-Friendly Documentation

New Files Created

  supabase/migrations/
    20260629000000_monitoring_alerts.sql           — monitoring_alerts + alert_events tables, RLS, indexes

  packages/api/rest/src/
    types/monitoring-types.ts                      — domain types, channel configs, typed errors
    validators/monitoring-validators.ts            — Joi schemas for CRUD endpoints
    middleware/validate.ts                         — generic Joi validation middleware
    utils/supabase.ts                              — service-role Supabase client singleton

    repositories/monitoring-alert.repository.ts    — Supabase persistence + worker queries
    services/monitoring/
      alert-evaluator.ts                           — pure decision logic
      monitoring-alert.service.ts                  — HTTP-facing orchestrator
      protocol-pool.ts                             — cached IDefiProtocol clients per (protocol, network)
      channels/
        notification-channel.ts                    — Strategy interface
        webhook-channel.ts                         — HMAC-SHA256 webhook delivery + retry classification
        channel-dispatcher.ts                      — routes by kind + exponential-backoff next-retry

    routes/monitoring/alerts.ts                    — POST/GET/PATCH/DELETE + GET events
    worker/position-monitor.worker.ts              — evaluation + retry loops
    worker/index.ts                                — standalone worker entrypoint

  scratch/
    smoke.sh                                       — E2E driver (boots receiver+API+worker, asserts)
    webhook-receiver.mjs                           — local HMAC-verifying webhook receiver
    setup-test-user.mjs                            — provisions Supabase auth user + JWT
    run-worker-with-fake-protocol.ts               — worker with stubbed health factor for smoke

  (Plus 6 test files mirroring the structure under __tests__/ folders.)

Key Functions/Classes Added

 // Domain
  export type AlertType = 'health_factor_below';
  export type AlertChannel = 'webhook' | 'email';
  export type AlertDeliveryStatus = 'pending' | 'delivered' | 'failed' | 'retrying';
  export interface MonitoringAlert { /* … */ }
  export interface AlertEventPayload {
    eventId: string;          // idempotency key for receivers
    alertId: string;
    alertName: string;
    protocol: string;
    accountAddress: string;
    network: StellarNetworkName;
    alertType: AlertType;
    threshold: number;
    observedValue: number | null;
    triggeredAt: string;
  }

  // Service layer
  export class MonitoringAlertService {
    create(userId, input):    Promise<MonitoringAlert>;
    list(filter):             Promise<MonitoringAlert[]>;
    getForUser(id, userId):   Promise<MonitoringAlert>;
    update(id, userId, body): Promise<MonitoringAlert>;
    delete(id, userId):       Promise<void>;
    listEventsForUser(alertId, userId, opts): Promise<AlertEvent[]>;
  }

  // Pure decision
  export class AlertEvaluator {
    evaluate(alert, { observedValue, now }): EvaluationDecision;
  }

  // Strategy
  export interface INotificationChannel {
    readonly kind: 'webhook' | 'email';
    send(alert, payload): Promise<ChannelDeliveryResult>;
  }

  // Worker
  export class PositionMonitorWorker {
    start(): void;
    stop(): void;
    evaluationTick(): Promise<void>;  // public for testability
  }

Patterns Used

  • Repository pattern — MonitoringAlertRepository isolates Supabase from the rest of the code; snake_case stays inside the repo via a row mapper.
  • Strategy pattern — INotificationChannel lets new channels (email, Slack, PagerDuty) be added without changing the dispatcher.
  • Pure decision function — AlertEvaluator separated from the worker so the trigger rule is testable in isolation and reusable by a future "test alert"
    endpoint.
  • Singleton + lazy init — getSupabaseClient() and ProtocolPool cache expensive constructions.
  • Idempotency on the receiver — payload.eventId carried in X-Galaxy-Event-Id header lets webhook receivers deduplicate retries.
  • HMAC signing — X-Galaxy-Signature: sha256=<hmac(secret, "${ts}.${body}")> so receivers can verify authenticity with constant-time compare.

For future similar features, the layering is routes (thin) → service → repo + worker → service → repo, keeping HTTP and background-job concerns
separate.

📸 Screenshots (if applicable)

N/A — backend feature.

⚠️ Breaking Changes

  • No breaking changes

  • Breaking changes documented in CHANGELOG.md

    Two preexisting migrations (20260222090135_smart_wallets.sql, 20260324235408_drop_encrypted_private_key.sql) referenced a public.invisible_wallets
    table that isn't created by any migration in this stream, so supabase start failed on a clean checkout. Both ALTER TABLE statements are now wrapped in
    an existence guard, making them idempotent and no-ops when the table is absent. This is not a behavior change for environments where the table exists —
    just a fix that lets a fresh local environment boot.

🔄 Deployment Notes

  1. Apply the migration — supabase db push (or your CI equivalent) will create monitoring_alerts and alert_events.
  2. Run the worker as a separate process — DO NOT run it in-process with the REST API. The repo now ships:
    - npm run worker --workspace=@galaxy-kj/api-rest (production, expects compiled dist/)
    - npm run worker:dev --workspace=@galaxy-kj/api-rest (dev, via ts-node)

Required env: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, optional MONITOR_NETWORK (testnet/mainnet), MONITOR_EVAL_INTERVAL_MS (default 60000),
MONITOR_BATCH_SIZE (default 50).
3. Horizontal scaling — current design assumes one worker per network. Scaling to N workers will need a FOR UPDATE SKIP LOCKED lease on
monitoring_alerts (documented in the worker's JSDoc, not implemented here).
4. Webhook URL scheme — production rejects plaintext (http://) URLs; only https:// is accepted. Local dev allows http:// for smoke tests against
127.0.0.1.

✅ Final Checklist

  • Code follows project style guidelines (matches defi.routes.ts and existing service patterns)
  • Self-review completed
  • No console.log or debug code left (worker logs go through an injectable logger)
  • Error handling implemented (typed MonitoringError, retryable vs terminal failure classification on webhook channel)
  • Performance considered (partial index for worker scan, protocol client caching, batched evaluation)
  • Security reviewed — HMAC-signed webhooks, https-only in production, RLS policies on both tables, no secrets logged, sensitive-key sanitization
    inherited from audit logger
  • Documentation updated (required) — code-level JSDoc only; AI.md / README / ARCHITECTURE updates pending
  • ROADMAP.md updated with progress — pending

By submitting this PR, I confirm that:

  • ✅ I have updated all relevant documentation
  • ✅ AI.md includes new patterns from my changes
  • ✅ Examples are provided for new features
  • ✅ The documentation is accurate and helpful for AI assistants and developers

Summary by CodeRabbit

  • New Features
    • Added monitoring alert REST endpoints (create, list, fetch, update, delete, and event history).
    • Introduced a background worker that periodically evaluates active alerts and dispatches notifications.
    • Added webhook notification delivery with signed requests and delivery/retry tracking.
    • Added support for cooldown-aware alert triggering and structured evaluation decisions.
  • Bug Fixes
    • Improved request validation and standardized validation failure responses.
    • Ensured alert and event data access is scoped to the authenticated user.
  • Tests
    • Added comprehensive unit/integration tests and smoke-test utilities for the monitoring flow.
  • Chores
    • Added database migration to persist monitoring alerts and alert events with access controls.

Both migrations referenced public.invisible_wallets, which is created in a separate migration stream that is not always present on a fresh local reset. Wrapping the ALTER TABLE statements in a table-existence check makes them idempotent so supabase start succeeds on a clean checkout.
New tables for the real-time position monitoring feature (Issue Galaxy-KJ#306, Roadmap Galaxy-KJ#53). Includes per-user RLS, a partial index over active alerts that drives the worker scan, and a retry index for failed deliveries. Triggers reuse the shared update_updated_at_column() function.
…abase client

Foundation layer for the monitoring feature: domain types and typed errors, Joi schemas for alert CRUD, a reusable validation middleware, and a service-role Supabase singleton scoped to the api-rest package. The webhook URL scheme is https-only in production and relaxed to http in development so local smoke tests can target 127.0.0.1.
Persistence layer for monitoring_alerts and alert_events. Enforces ownership at the data boundary when a userId is provided; worker-only queries intentionally bypass that filter so the background process can scan every active alert. Includes a row-to-domain mapper to keep snake_case isolated to the repo.

Tests cover CRUD success and error paths, ownership filtering, worker scan ordering, and event delivery state transitions.
MonitoringAlertService orchestrates HTTP-facing CRUD on top of the repository, enforces protocol and channel invariants, and translates persistence errors into typed MonitoringError instances mapped to HTTP status codes.

AlertEvaluator is a pure decision function (alert + observed value + cooldown → trigger?) so the same rule is testable in isolation and reusable by future endpoints such as a 'test alert' simulator.
Strategy pattern for delivering alert events. WebhookChannel POSTs the canonical payload with an HMAC-SHA256 signature over '${timestamp}.${body}' so receivers can verify authenticity, and includes the event id as an idempotency key. Distinguishes retryable failures (network, 5xx, 408, 429) from terminal ones.

ChannelDispatcher routes to the right implementation by kind and computes the next retry timestamp with full-jitter exponential backoff capped at MAX_DELIVERY_ATTEMPTS.
PositionMonitorWorker runs as a separate process (npm scripts: worker, worker:dev) so polling never duplicates when the REST API is horizontally scaled. Two interval loops: evaluation (fetch alerts → query protocol → decide → persist event → dispatch) and a retry stub reserved for next iteration.

ProtocolPool caches one IDefiProtocol client per (protocol, network) tuple for the worker's lifetime so we don't rebuild heavy RPC clients on every tick. Parses Blend's '∞' health factor as +Infinity so positions with no debt never trigger.
Wires the JWT-protected REST surface: POST, GET, GET:id, PATCH, DELETE on /monitoring/alerts and GET on /monitoring/alerts/:id/events. Maps MonitoringError instances to HTTP status codes consistently and applies the shared audit middleware so every mutation is logged.

Closes the wiring for Issue Galaxy-KJ#306 — the API contract called out in the acceptance criteria is now live.
scratch/smoke.sh boots the receiver, the REST API and a fake-protocol worker, provisions a Supabase auth user, creates an alert, and asserts that the worker triggers the webhook with a valid HMAC signature and a delivered alert_event row.

Knobs: FAKE_HEALTH_FACTOR, THRESHOLD, EVAL_INTERVAL_MS, KEEP_RUNNING. Cleans up all background processes on any exit path.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ef8c5910-c4b6-4400-806d-88a15b7eb010

📥 Commits

Reviewing files that changed from the base of the PR and between 0a7fbe3 and 15fbff7.

📒 Files selected for processing (2)
  • packages/api/rest/package.json
  • packages/api/rest/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/api/rest/src/index.ts
  • packages/api/rest/package.json

📝 Walkthrough

Walkthrough

Adds monitoring alert persistence, REST routes, validation, delivery dispatch, worker polling, and smoke-test tooling around a new alerting flow. A Supabase migration adds the underlying tables and policies, and the API package wires routes and a worker entrypoint for runtime use.

Changes

Monitoring Alerts Feature

Layer / File(s) Summary
DB schema and domain types
supabase/migrations/20260629000000_monitoring_alerts.sql, packages/api/rest/src/types/monitoring-types.ts
Adds the monitoring alert and alert event schema, row-level policies, indexes, TypeScript domain/input/output types, and monitoring error/result definitions.
Supabase singleton and repository
packages/api/rest/src/utils/supabase.ts, packages/api/rest/src/repositories/monitoring-alert.repository.ts, packages/api/rest/src/repositories/__tests__/monitoring-alert.repository.test.ts
Adds the cached Supabase client utility and the repository for alert CRUD, evaluation updates, event persistence, and ownership-scoped event listing, with repository tests.
Alert evaluator and protocol pool
packages/api/rest/src/services/monitoring/alert-evaluator.ts, packages/api/rest/src/services/monitoring/protocol-pool.ts, packages/api/rest/src/services/monitoring/__tests__/alert-evaluator.test.ts, packages/api/rest/src/services/monitoring/__tests__/protocol-pool.test.ts
Adds the trigger decision logic and protocol client cache used while polling health factors, along with unit tests for both.
Notification channels
packages/api/rest/src/services/monitoring/channels/notification-channel.ts, packages/api/rest/src/services/monitoring/channels/webhook-channel.ts, packages/api/rest/src/services/monitoring/channels/channel-dispatcher.ts, packages/api/rest/src/services/monitoring/channels/__tests__/*
Adds the notification-channel contract, webhook delivery implementation, channel dispatch routing, and tests for routing, retry timing, and webhook request behavior.
Service layer and validation
packages/api/rest/src/services/monitoring/monitoring-alert.service.ts, packages/api/rest/src/validators/monitoring-validators.ts, packages/api/rest/src/middleware/validate.ts, packages/api/rest/src/services/monitoring/__tests__/monitoring-alert.service.test.ts
Adds the monitoring alert service, Joi request schemas, the generic validation middleware, and service tests covering protocol and channel validation cases.
REST routes and API wiring
packages/api/rest/src/routes/monitoring/alerts.ts, packages/api/rest/src/index.ts, packages/api/rest/src/routes/monitoring/__tests__/alerts.routes.test.ts
Adds the monitoring alert router, mounts it under the API router, and tests route responses, user scoping, and validation/error handling.
PositionMonitorWorker and entrypoint
packages/api/rest/src/worker/position-monitor.worker.ts, packages/api/rest/src/worker/index.ts, packages/api/rest/src/worker/__tests__/position-monitor.worker.test.ts, packages/api/rest/package.json
Adds the worker that polls alerts, evaluates health factors, persists events, dispatches delivery, and updates retry state, plus the worker entrypoint, tests, and package scripts.
Migration idempotency guards
supabase/migrations/20260222090135_smart_wallets.sql, supabase/migrations/20260324235408_drop_encrypted_private_key.sql
Wraps earlier private-key column drops in existence checks so the migrations can run when the table is absent.
Smoke-test tooling
scratch/smoke.sh, scratch/webhook-receiver.mjs, scratch/setup-test-user.mjs, scratch/run-worker-with-fake-protocol.ts
Adds scripts for a local end-to-end smoke test, including a webhook receiver, test-user setup, a fake protocol worker runner, and the orchestration shell script.

Sequence Diagram

sequenceDiagram
  participant Client
  participant AlertsRouter
  participant MonitoringAlertService
  participant MonitoringAlertRepository
  participant PositionMonitorWorker
  participant ProtocolPool
  participant ChannelDispatcher

  Client->>AlertsRouter: POST /api/v1/monitoring/alerts
  AlertsRouter->>MonitoringAlertService: create(userId, input)
  MonitoringAlertService->>MonitoringAlertRepository: create(userId, input)
  MonitoringAlertRepository-->>MonitoringAlertService: MonitoringAlert
  MonitoringAlertService-->>AlertsRouter: MonitoringAlert
  AlertsRouter-->>Client: 201 Created

  loop evaluationTick interval
    PositionMonitorWorker->>MonitoringAlertRepository: listActiveForEvaluation(network, batchSize)
    MonitoringAlertRepository-->>PositionMonitorWorker: alerts[]
    PositionMonitorWorker->>ProtocolPool: get(protocol, network).getHealthFactor(account)
    ProtocolPool-->>PositionMonitorWorker: health factor
    PositionMonitorWorker->>MonitoringAlertRepository: markEvaluated(id, healthFactor, didTrigger)
    alt shouldTrigger
      PositionMonitorWorker->>MonitoringAlertRepository: createEvent(alertId, payload, healthFactor)
      PositionMonitorWorker->>ChannelDispatcher: dispatch(alert, payload)
      ChannelDispatcher-->>PositionMonitorWorker: ChannelDeliveryResult
      PositionMonitorWorker->>MonitoringAlertRepository: updateEventDelivery(id, patch)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • KevinMB0220

Poem

🐇 I hopped through alerts from dawn till night,
With webhooks signed and workers tight.
If health drops low, the bells all ring,
And bunny paws keep track of everything.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Core alert routes, polling worker, webhook delivery, and tests are present, but email delivery is only accepted in config and not implemented. Implement the email notification channel, or explicitly scope it out, so alert delivery matches the issue's webhook/email requirement.
Out of Scope Changes check ⚠️ Warning Several scratch smoke-test scripts and legacy migration guards appear unrelated to the monitoring-alert feature scope. Move unrelated smoke-test and migration-fix changes into a separate PR unless they are required for issue #306.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the realtime position monitoring feature and matches the main change set.
Description check ✅ Passed The PR description follows the template with description, related issues, testing, docs, AI notes, deployment, and checklist sections.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/api/rest/src/index.ts

Oops! Something went wrong! :(

ESLint: 9.39.2

YAMLException: Cannot read config file: /.eslintrc.js.bak
Error: end of the stream or a document separator is expected (2:7)

1 | module.exports = {
2 | root: true,
-----------^
3 | env: {
4 | node: true,
at generateError (/node_modules/js-yaml/lib/loader.js:199:10)
at throwError (/node_modules/js-yaml/lib/loader.js:203:9)
at readDocument (/node_modules/js-yaml/lib/loader.js:1651:5)
at loadDocuments (/node_modules/js-yaml/lib/loader.js:1694:5)
at Object.load (/node_modules/js-yaml/lib/loader.js:1720:19)
at loadLegacyConfigFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2666:21)
at loadConfigFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2782:20)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3088:42)
at ConfigArrayFactory.loadFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2952:40)
at createCLIConfigArray (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3768:35)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (1)
packages/api/rest/src/worker/__tests__/position-monitor.worker.test.ts (1)

172-178: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test does not verify interval cleanup.

Lines 172-178 only call start()/stop(), so it still passes if the timer registration/cleanup logic disappears. Spy on setInterval/clearInterval or use fake timers and assert the call counts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/rest/src/worker/__tests__/position-monitor.worker.test.ts`
around lines 172 - 178, The idempotent start/stop test in
position-monitor.worker.test.ts does not actually verify interval cleanup
because it only calls worker.start() and worker.stop(). Update the test around
buildWorker and the worker.start/worker.stop methods to spy on setInterval and
clearInterval, or use fake timers, and assert the expected call counts so the
test fails if timer registration or cleanup is removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/rest/src/routes/monitoring/alerts.ts`:
- Around line 135-145: The GET /alerts/:id/events handler is parsing limit and
offset manually, so invalid query values can become NaN or pass unchecked into
service.listEventsForUser. Add a Joi query schema for event-history pagination,
then wire it into this route with validate(..., 'query') the same way GET
/alerts does, so the handler rejects non-numeric, negative, or oversized values
before calling listEventsForUser.

In
`@packages/api/rest/src/services/monitoring/channels/__tests__/channel-dispatcher.test.ts`:
- Around line 81-85: The ChannelDispatcher retry timestamp test is flaky because
it compares computeNextRetry(1) against a later Date.now() even though the
method can return an equal timestamp when jitter is zero. Update the assertion
in channel-dispatcher.test.ts to use a stable time window (capture before/after
around the call) or mock the clock, and keep the check focused on
ChannelDispatcher.computeNextRetry rather than relying on live time.

In `@packages/api/rest/src/services/monitoring/channels/channel-dispatcher.ts`:
- Around line 28-38: `ChannelDispatcher.dispatch` currently returns directly
from `channel.send(...)`, which allows channel exceptions to escape the
dispatcher boundary; wrap that call in error handling so any thrown exception is
converted into a failed `ChannelDeliveryResult` with `success: false`,
`retryable` set appropriately, and an error message. Keep the existing
missing-channel path intact, and make sure the fix is localized in
`ChannelDispatcher.dispatch` so `triggerAlert()` in the worker always receives a
result instead of an exception.

In `@packages/api/rest/src/services/monitoring/channels/webhook-channel.ts`:
- Around line 52-60: The webhook request headers in webhook-channel.ts are
currently allowing config.headers to override reserved system headers, which can
break signature verification and event handling. Update the header merge in the
webhook send path so caller-provided headers are applied first and the reserved
headers such as Content-Type, X-Galaxy-Event, X-Galaxy-Event-Id,
X-Galaxy-Timestamp, and X-Galaxy-Signature are written afterward in the
webhook-channel logic. Keep the fix localized to the header construction around
the payload dispatch so these signed values always win.

In `@packages/api/rest/src/services/monitoring/monitoring-alert.service.ts`:
- Around line 54-68: The update() method in MonitoringAlertService currently
patches directly through repo.update() without re-validating channelConfig
against the alert’s stored channel. Load the existing alert first, merge it with
the incoming UpdateMonitoringAlertInput, and validate the resulting state before
writing; if the alert is missing, keep the existing ALERT_NOT_FOUND behavior.
Make the check happen in update() using the MonitoringAlertService and repo
methods so a PATCH cannot leave an incompatible channelConfig for the current
channel.

In `@packages/api/rest/src/services/monitoring/protocol-pool.ts`:
- Around line 28-33: ProtocolPool is injecting one global BLEND_ADDRESSES set
into both cached clients, so testnet and mainnet can point at the wrong
deployment. Update build() in ProtocolPool to choose addresses based on the
target network and pass a network-specific address map into the client creation
path. Keep the existing BLEND_* env fallbacks, but resolve them separately for
testnet and mainnet instead of sharing a single constant across both.

In `@packages/api/rest/src/types/monitoring-types.ts`:
- Around line 19-55: The read model is exposing sensitive webhook secrets
through MonitoringAlert.channelConfig because WebhookChannelConfig.secret is
returned unchanged. Update the monitoring types around WebhookChannelConfig,
ChannelConfig, and MonitoringAlert so persisted config can stay private while
API responses use a redacted DTO that omits or masks secret. Make the
create/list/update flow map from the stored config to the safe response shape
before returning it.

In `@packages/api/rest/src/validators/monitoring-validators.ts`:
- Around line 16-20: The webhook validation in webhookConfigSchema currently
allows any header key, which can let users override reserved delivery headers
later used by the dispatcher. Update the schema or validation logic in
monitoring-validators.ts to reject reserved header names case-insensitively for
config.headers, especially X-Galaxy-* and Content-Type, so the generated webhook
signature, timestamp, event id, and content type cannot be overwritten. If you
prefer to keep validation permissive, then adjust the webhook dispatch path that
uses config.headers so the fixed headers are applied last and remain immutable.

In `@packages/api/rest/src/worker/index.ts`:
- Around line 43-46: The shutdown flow in shutdown currently exits immediately
after worker.stop(), which can interrupt in-flight createEvent() / dispatch /
updateEventDelivery() work and leave event state inconsistent. Update the
shutdown handling to stop accepting new intervals, then await any active work or
the worker’s graceful completion before calling process.exit, using the existing
shutdown() and worker.stop() logic as the entry point.
- Around line 33-39: The `main` setup in `PositionMonitorWorker` is casting
`MONITOR_NETWORK` to `StellarNetworkName`, which allows invalid values to slip
through and default to the wrong network. Replace the cast with explicit
validation/parsing before constructing `PositionMonitorWorker`, and only pass a
confirmed `'mainnet'` or `'testnet'` value (fall back to the existing default
when the env var is missing or invalid). Use the `main` function and the
`PositionMonitorWorker` constructor as the places to update.

In `@packages/api/rest/src/worker/position-monitor.worker.ts`:
- Around line 96-100: The retryTick() method in position-monitor.worker.ts is
currently a no-op, so alerts marked retrying by triggerAlert() with
deliveryStatus and nextRetryAt never get redelivered. Update retryTick() to
query due retry events from alert_events based on nextRetryAt and retrying
status, then invoke the existing resend path used for initial delivery so
transient failures are retried instead of stalling forever.
- Around line 72-78: The current setInterval-based scheduling in
position-monitor.worker.ts can overlap async runs, causing duplicate processing
before markEvaluated() completes. Replace the timer callbacks around
evaluationTick() and retryTick() with non-overlapping execution guards or
self-scheduling loops that only queue the next run after the previous Promise
settles. Use the existing evaluationTick, retryTick, evaluationTimer, and
retryTimer logic to keep one in-flight cycle per loop and preserve the existing
error logging.

In `@scratch/setup-test-user.mjs`:
- Around line 6-10: Remove the baked-in Supabase JWT fallback values from
setup-test-user.mjs so the script does not ship hardcoded service-role or anon
credentials. Update SUPABASE_URL, SERVICE_KEY, and ANON_KEY handling to require
the corresponding environment variables and fail fast with a clear error if they
are missing, using the existing setup-test-user.mjs entrypoint logic. Apply the
same env-only credential handling pattern in scratch/smoke.sh so both scratch
tools are consistent.

In `@scratch/smoke.sh`:
- Around line 223-227: The smoke test expectation is out of sync with
PositionMonitorWorker.parseHealthFactor() because it only treats the unicode
infinity symbol as non-triggering. Update the health-factor check in
scratch/smoke.sh so it also recognizes the string "infinity" case-insensitively,
matching parseHealthFactor() behavior, and keep EXPECT_TRIGGER=0 for both
representations when verifying the webhook dispatch path.
- Around line 263-264: The smoke test is hardcoding the Supabase DB container
name in the EVENT_ROW lookup, which will break on clones/forks with different
local project slugs. Update the `scratch/smoke.sh` assertion to use the same
dynamic container naming/source as the rest of the script (or derive the DB
container name from the configured project slug) so the `docker exec` call works
regardless of repository name.

In `@supabase/migrations/20260629000000_monitoring_alerts.sql`:
- Around line 34-47: Add a dedicated event_id column to alert_events and make it
unique so AlertEventPayload.eventId is stored as a first-class idempotency key
instead of only inside payload JSONB. Update the monitoring_alerts migration and
the createEvent() insert path to populate event_id from payload.eventId, and
ensure any lookup/dedup logic references the new column; use the alert_events
and createEvent symbols to locate the affected schema and write path.

---

Nitpick comments:
In `@packages/api/rest/src/worker/__tests__/position-monitor.worker.test.ts`:
- Around line 172-178: The idempotent start/stop test in
position-monitor.worker.test.ts does not actually verify interval cleanup
because it only calls worker.start() and worker.stop(). Update the test around
buildWorker and the worker.start/worker.stop methods to spy on setInterval and
clearInterval, or use fake timers, and assert the expected call counts so the
test fails if timer registration or cleanup is removed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9773452a-12ff-4619-9267-1cf7c72269ad

📥 Commits

Reviewing files that changed from the base of the PR and between f914c4e and 0a7fbe3.

📒 Files selected for processing (31)
  • packages/api/rest/package.json
  • packages/api/rest/src/index.ts
  • packages/api/rest/src/middleware/validate.ts
  • packages/api/rest/src/repositories/__tests__/monitoring-alert.repository.test.ts
  • packages/api/rest/src/repositories/monitoring-alert.repository.ts
  • packages/api/rest/src/routes/monitoring/__tests__/alerts.routes.test.ts
  • packages/api/rest/src/routes/monitoring/alerts.ts
  • packages/api/rest/src/services/monitoring/__tests__/alert-evaluator.test.ts
  • packages/api/rest/src/services/monitoring/__tests__/monitoring-alert.service.test.ts
  • packages/api/rest/src/services/monitoring/__tests__/protocol-pool.test.ts
  • packages/api/rest/src/services/monitoring/alert-evaluator.ts
  • packages/api/rest/src/services/monitoring/channels/__tests__/channel-dispatcher.test.ts
  • packages/api/rest/src/services/monitoring/channels/__tests__/webhook-channel.test.ts
  • packages/api/rest/src/services/monitoring/channels/channel-dispatcher.ts
  • packages/api/rest/src/services/monitoring/channels/notification-channel.ts
  • packages/api/rest/src/services/monitoring/channels/webhook-channel.ts
  • packages/api/rest/src/services/monitoring/monitoring-alert.service.ts
  • packages/api/rest/src/services/monitoring/protocol-pool.ts
  • packages/api/rest/src/types/monitoring-types.ts
  • packages/api/rest/src/utils/supabase.ts
  • packages/api/rest/src/validators/monitoring-validators.ts
  • packages/api/rest/src/worker/__tests__/position-monitor.worker.test.ts
  • packages/api/rest/src/worker/index.ts
  • packages/api/rest/src/worker/position-monitor.worker.ts
  • scratch/run-worker-with-fake-protocol.ts
  • scratch/setup-test-user.mjs
  • scratch/smoke.sh
  • scratch/webhook-receiver.mjs
  • supabase/migrations/20260222090135_smart_wallets.sql
  • supabase/migrations/20260324235408_drop_encrypted_private_key.sql
  • supabase/migrations/20260629000000_monitoring_alerts.sql

Comment on lines +135 to +145
router.get(
'/alerts/:id/events',
validate(alertIdParamSchema, 'params'),
async (req, res, next) => {
const userId = requireUser(req, res);
if (!userId) return;
try {
const limit = req.query.limit ? Number(req.query.limit) : undefined;
const offset = req.query.offset ? Number(req.query.offset) : undefined;
const events = await service.listEventsForUser(req.params.id, userId, { limit, offset });
res.json({ events });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate event-history pagination too.

This endpoint parses limit/offset manually instead of running them through Joi like GET /alerts. Non-numeric values become NaN, and negative or oversized values go straight into service.listEventsForUser(...) instead of failing fast with a 400. Please add a query schema for this route and reuse validate(..., 'query') here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/rest/src/routes/monitoring/alerts.ts` around lines 135 - 145,
The GET /alerts/:id/events handler is parsing limit and offset manually, so
invalid query values can become NaN or pass unchecked into
service.listEventsForUser. Add a Joi query schema for event-history pagination,
then wire it into this route with validate(..., 'query') the same way GET
/alerts does, so the handler rejects non-numeric, negative, or oversized values
before calling listEventsForUser.

Comment on lines +81 to +85
it('returns a future Date for in-range attempt counts', () => {
const dispatcher = new ChannelDispatcher([new FakeChannel('webhook')]);
const next = dispatcher.computeNextRetry(1);
expect(next).not.toBeNull();
expect(next!.getTime()).toBeGreaterThanOrEqual(Date.now());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Stabilize this retry timestamp assertion.

computeNextRetry(1) can legally return new Date(Date.now()) when jitter is 0, so comparing it to a later Date.now() makes the test flaky. Capture a before/after window or mock time instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/api/rest/src/services/monitoring/channels/__tests__/channel-dispatcher.test.ts`
around lines 81 - 85, The ChannelDispatcher retry timestamp test is flaky
because it compares computeNextRetry(1) against a later Date.now() even though
the method can return an equal timestamp when jitter is zero. Update the
assertion in channel-dispatcher.test.ts to use a stable time window (capture
before/after around the call) or mock the clock, and keep the check focused on
ChannelDispatcher.computeNextRetry rather than relying on live time.

Comment on lines +28 to +38
async dispatch(alert: MonitoringAlert, payload: AlertEventPayload): Promise<ChannelDeliveryResult> {
const channel = this.channels.get(alert.channel);
if (!channel) {
return {
success: false,
durationMs: 0,
retryable: false,
error: `No implementation registered for channel "${alert.channel}"`,
};
}
return channel.send(alert, payload);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Catch channel exceptions inside the dispatcher boundary.

dispatch() currently lets channel.send() throw. In packages/api/rest/src/worker/position-monitor.worker.ts:128-177, triggerAlert() awaits this call before updating the event row, so one unexpected channel exception can skip persistence and abort processing of later alerts in the same tick. Convert thrown errors into a failed ChannelDeliveryResult here.

Suggested fix
   async dispatch(alert: MonitoringAlert, payload: AlertEventPayload): Promise<ChannelDeliveryResult> {
     const channel = this.channels.get(alert.channel);
     if (!channel) {
       return {
         success: false,
         durationMs: 0,
         retryable: false,
         error: `No implementation registered for channel "${alert.channel}"`,
       };
     }
-    return channel.send(alert, payload);
+    const start = Date.now();
+    try {
+      return await channel.send(alert, payload);
+    } catch (err) {
+      return {
+        success: false,
+        durationMs: Date.now() - start,
+        retryable: true,
+        error: err instanceof Error ? err.message : String(err),
+      };
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async dispatch(alert: MonitoringAlert, payload: AlertEventPayload): Promise<ChannelDeliveryResult> {
const channel = this.channels.get(alert.channel);
if (!channel) {
return {
success: false,
durationMs: 0,
retryable: false,
error: `No implementation registered for channel "${alert.channel}"`,
};
}
return channel.send(alert, payload);
async dispatch(alert: MonitoringAlert, payload: AlertEventPayload): Promise<ChannelDeliveryResult> {
const channel = this.channels.get(alert.channel);
if (!channel) {
return {
success: false,
durationMs: 0,
retryable: false,
error: `No implementation registered for channel "${alert.channel}"`,
};
}
const start = Date.now();
try {
return await channel.send(alert, payload);
} catch (err) {
return {
success: false,
durationMs: Date.now() - start,
retryable: true,
error: err instanceof Error ? err.message : String(err),
};
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/rest/src/services/monitoring/channels/channel-dispatcher.ts`
around lines 28 - 38, `ChannelDispatcher.dispatch` currently returns directly
from `channel.send(...)`, which allows channel exceptions to escape the
dispatcher boundary; wrap that call in error handling so any thrown exception is
converted into a failed `ChannelDeliveryResult` with `success: false`,
`retryable` set appropriately, and an error message. Keep the existing
missing-channel path intact, and make sure the fix is localized in
`ChannelDispatcher.dispatch` so `triggerAlert()` in the worker always receives a
result instead of an exception.

Comment on lines +52 to +60
headers: {
'Content-Type': 'application/json',
'User-Agent': 'GalaxyDevKit-Monitoring/1.0',
'X-Galaxy-Event': 'monitoring.alert.triggered',
'X-Galaxy-Event-Id': payload.eventId,
'X-Galaxy-Timestamp': timestamp,
'X-Galaxy-Signature': `sha256=${signature}`,
...(config.headers ?? {}),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not let custom headers override the signed system headers.

Because config.headers is merged last, a user-configured webhook can replace X-Galaxy-Signature, X-Galaxy-Timestamp, X-Galaxy-Event-Id, or Content-Type. That breaks signature verification and idempotency downstream. Merge caller headers first, then write the reserved headers on top.

Suggested fix
       const response = await this.fetchImpl(config.url, {
         method: 'POST',
         headers: {
+          ...(config.headers ?? {}),
           'Content-Type': 'application/json',
           'User-Agent': 'GalaxyDevKit-Monitoring/1.0',
           'X-Galaxy-Event': 'monitoring.alert.triggered',
           'X-Galaxy-Event-Id': payload.eventId,
           'X-Galaxy-Timestamp': timestamp,
           'X-Galaxy-Signature': `sha256=${signature}`,
-          ...(config.headers ?? {}),
         },
         body,
         signal: controller.signal,
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
headers: {
'Content-Type': 'application/json',
'User-Agent': 'GalaxyDevKit-Monitoring/1.0',
'X-Galaxy-Event': 'monitoring.alert.triggered',
'X-Galaxy-Event-Id': payload.eventId,
'X-Galaxy-Timestamp': timestamp,
'X-Galaxy-Signature': `sha256=${signature}`,
...(config.headers ?? {}),
},
headers: {
...(config.headers ?? {}),
'Content-Type': 'application/json',
'User-Agent': 'GalaxyDevKit-Monitoring/1.0',
'X-Galaxy-Event': 'monitoring.alert.triggered',
'X-Galaxy-Event-Id': payload.eventId,
'X-Galaxy-Timestamp': timestamp,
'X-Galaxy-Signature': `sha256=${signature}`,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/rest/src/services/monitoring/channels/webhook-channel.ts` around
lines 52 - 60, The webhook request headers in webhook-channel.ts are currently
allowing config.headers to override reserved system headers, which can break
signature verification and event handling. Update the header merge in the
webhook send path so caller-provided headers are applied first and the reserved
headers such as Content-Type, X-Galaxy-Event, X-Galaxy-Event-Id,
X-Galaxy-Timestamp, and X-Galaxy-Signature are written afterward in the
webhook-channel logic. Keep the fix localized to the header construction around
the payload dispatch so these signed values always win.

Comment on lines +54 to +68
async update(
id: string,
userId: string,
input: UpdateMonitoringAlertInput
): Promise<MonitoringAlert> {
const updated = await this.repo.update(id, userId, input);
if (!updated) {
throw new MonitoringError(
MonitoringErrorCode.ALERT_NOT_FOUND,
'Monitoring alert not found',
404
);
}
return updated;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Re-validate channelConfig on PATCH.

update() writes partial changes without checking that the new channelConfig still matches the alert's stored channel. Right now a webhook alert can be patched to { to: 'a@b.com' }, and the worker later treats that row as WebhookChannelConfig when signing/sending. Please load the current alert first and validate the merged state before calling repo.update().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/rest/src/services/monitoring/monitoring-alert.service.ts` around
lines 54 - 68, The update() method in MonitoringAlertService currently patches
directly through repo.update() without re-validating channelConfig against the
alert’s stored channel. Load the existing alert first, merge it with the
incoming UpdateMonitoringAlertInput, and validate the resulting state before
writing; if the alert is missing, keep the existing ALERT_NOT_FOUND behavior.
Make the check happen in update() using the MonitoringAlertService and repo
methods so a PATCH cannot leave an incompatible channelConfig for the current
channel.

Comment on lines +96 to +100
async retryTick(): Promise<void> {
// Retry queue is read directly from alert_events. Future work: pull events
// due for retry and resend; intentionally lean for the first iteration so
// we don't ship untested retry-fanout logic before we observe real traffic.
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

retrying events never get retried.

triggerAlert() records deliveryStatus: 'retrying' plus nextRetryAt, but Lines 96-100 are a no-op. Any transient 5xx/timeout will stay stuck forever and never be redelivered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/rest/src/worker/position-monitor.worker.ts` around lines 96 -
100, The retryTick() method in position-monitor.worker.ts is currently a no-op,
so alerts marked retrying by triggerAlert() with deliveryStatus and nextRetryAt
never get redelivered. Update retryTick() to query due retry events from
alert_events based on nextRetryAt and retrying status, then invoke the existing
resend path used for initial delivery so transient failures are retried instead
of stalling forever.

Comment on lines +6 to +10
const SUPABASE_URL = process.env.SUPABASE_URL ?? 'http://127.0.0.1:54321';
const SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY
?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU';
const ANON_KEY = process.env.SUPABASE_ANON_KEY
?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the baked-in Supabase JWT defaults.

Lines 7-10 commit service-role and anon tokens into the repo. Even for scratch tooling, that normalizes hardcoded credentials and lets accidental runs against any instance sharing the local JWT secret use privileged auth. Fail fast when the env vars are missing instead. The same cleanup should be applied consistently in scratch/smoke.sh.

Suggested fix
 const SUPABASE_URL = process.env.SUPABASE_URL ?? 'http://127.0.0.1:54321';
-const SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY
-  ?? '...';
-const ANON_KEY = process.env.SUPABASE_ANON_KEY
-  ?? '...';
+const SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
+const ANON_KEY = process.env.SUPABASE_ANON_KEY;
+
+if (!SERVICE_KEY) throw new Error('SUPABASE_SERVICE_ROLE_KEY is required');
+if (!ANON_KEY) throw new Error('SUPABASE_ANON_KEY is required');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const SUPABASE_URL = process.env.SUPABASE_URL ?? 'http://127.0.0.1:54321';
const SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY
?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU';
const ANON_KEY = process.env.SUPABASE_ANON_KEY
?? 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0';
const SUPABASE_URL = process.env.SUPABASE_URL ?? 'http://127.0.0.1:54321';
const SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
const ANON_KEY = process.env.SUPABASE_ANON_KEY;
if (!SERVICE_KEY) throw new Error('SUPABASE_SERVICE_ROLE_KEY is required');
if (!ANON_KEY) throw new Error('SUPABASE_ANON_KEY is required');
🧰 Tools
🪛 Betterleaks (1.6.0)

[high] 8-8: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


[high] 10-10: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scratch/setup-test-user.mjs` around lines 6 - 10, Remove the baked-in
Supabase JWT fallback values from setup-test-user.mjs so the script does not
ship hardcoded service-role or anon credentials. Update SUPABASE_URL,
SERVICE_KEY, and ANON_KEY handling to require the corresponding environment
variables and fail fast with a clear error if they are missing, using the
existing setup-test-user.mjs entrypoint logic. Apply the same env-only
credential handling pattern in scratch/smoke.sh so both scratch tools are
consistent.

Source: Linters/SAST tools

Comment thread scratch/smoke.sh
Comment on lines +223 to +227
# A HF above threshold (or Infinity) should NOT trigger.
if awk -v hf="$FAKE_HEALTH_FACTOR" -v th="$THRESHOLD" 'BEGIN{ if (hf == "∞" || hf+0 >= th+0) exit 0; exit 1 }' 2>/dev/null; then
EXPECT_TRIGGER=0
hint "FAKE_HEALTH_FACTOR=$FAKE_HEALTH_FACTOR ≥ threshold=$THRESHOLD → expecting NO trigger"
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the smoke expectation aligned with parseHealthFactor().

PositionMonitorWorker.parseHealthFactor() treats "infinity" case-insensitively as +Infinity, but Line 224 only special-cases the unicode . FAKE_HEALTH_FACTOR=infinity will make the worker skip dispatch while this script still expects a webhook.

Suggested fix
-if awk -v hf="$FAKE_HEALTH_FACTOR" -v th="$THRESHOLD" 'BEGIN{ if (hf == "∞" || hf+0 >= th+0) exit 0; exit 1 }' 2>/dev/null; then
+if awk -v hf="$FAKE_HEALTH_FACTOR" -v th="$THRESHOLD" 'BEGIN{
+  if (hf == "∞" || tolower(hf) == "infinity" || hf+0 >= th+0) exit 0;
+  exit 1
+}' 2>/dev/null; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# A HF above threshold (or Infinity) should NOT trigger.
if awk -v hf="$FAKE_HEALTH_FACTOR" -v th="$THRESHOLD" 'BEGIN{ if (hf == "∞" || hf+0 >= th+0) exit 0; exit 1 }' 2>/dev/null; then
EXPECT_TRIGGER=0
hint "FAKE_HEALTH_FACTOR=$FAKE_HEALTH_FACTOR ≥ threshold=$THRESHOLD → expecting NO trigger"
fi
# A HF above threshold (or Infinity) should NOT trigger.
if awk -v hf="$FAKE_HEALTH_FACTOR" -v th="$THRESHOLD" 'BEGIN{
if (hf == "" || tolower(hf) == "infinity" || hf+0 >= th+0) exit 0;
exit 1
}' 2>/dev/null; then
EXPECT_TRIGGER=0
hint "FAKE_HEALTH_FACTOR=$FAKE_HEALTH_FACTOR ≥ threshold=$THRESHOLD → expecting NO trigger"
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scratch/smoke.sh` around lines 223 - 227, The smoke test expectation is out
of sync with PositionMonitorWorker.parseHealthFactor() because it only treats
the unicode infinity symbol as non-triggering. Update the health-factor check in
scratch/smoke.sh so it also recognizes the string "infinity" case-insensitively,
matching parseHealthFactor() behavior, and keep EXPECT_TRIGGER=0 for both
representations when verifying the webhook dispatch path.

Comment thread scratch/smoke.sh
Comment on lines +263 to +264
EVENT_ROW=$(docker exec supabase_db_galaxy-devkit psql -U postgres -d postgres -tA -c \
"SELECT delivery_status || '|' || delivery_attempts FROM alert_events WHERE alert_id = '$ALERT_ID' ORDER BY triggered_at DESC LIMIT 1;")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't hardcode the Supabase DB container name.

Line 263 assumes supabase_db_galaxy-devkit, but Supabase derives container names from the local project slug. Clones/forks will fail this assertion even when the worker and API behaved correctly.

Suggested fix
+SUPABASE_DB_CONTAINER="${SUPABASE_DB_CONTAINER:-$(docker ps --format '{{.Names}}' | grep '^supabase_db_' | head -n1)}"
+[[ -n "$SUPABASE_DB_CONTAINER" ]] || { fail "could not locate Supabase DB container"; exit 1; }
+
   EVENT_ROW=$(docker exec supabase_db_galaxy-devkit psql -U postgres -d postgres -tA -c \
+  EVENT_ROW=$(docker exec "$SUPABASE_DB_CONTAINER" psql -U postgres -d postgres -tA -c \
     "SELECT delivery_status || '|' || delivery_attempts FROM alert_events WHERE alert_id = '$ALERT_ID' ORDER BY triggered_at DESC LIMIT 1;")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scratch/smoke.sh` around lines 263 - 264, The smoke test is hardcoding the
Supabase DB container name in the EVENT_ROW lookup, which will break on
clones/forks with different local project slugs. Update the `scratch/smoke.sh`
assertion to use the same dynamic container naming/source as the rest of the
script (or derive the DB container name from the configured project slug) so the
`docker exec` call works regardless of repository name.

Comment on lines +34 to +47
CREATE TABLE public.alert_events (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
alert_id UUID NOT NULL REFERENCES public.monitoring_alerts(id) ON DELETE CASCADE,
triggered_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
health_factor_value NUMERIC(20, 8),
payload JSONB NOT NULL,
delivery_status alert_event_delivery_status NOT NULL DEFAULT 'pending',
delivery_attempts INTEGER NOT NULL DEFAULT 0,
last_attempt_at TIMESTAMP WITH TIME ZONE,
next_retry_at TIMESTAMP WITH TIME ZONE,
last_error TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Persist the webhook idempotency key as a real column.

AlertEventPayload already treats eventId as the canonical idempotency key, but alert_events only buries it inside payload JSONB. That leaves no uniqueness or indexed lookup to stop a duplicate logical event from being inserted if createEvent() is retried after an ambiguous failure, even though this table is also your retry ledger/history. Add a dedicated event_id column with a unique constraint and populate it from payload.eventId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260629000000_monitoring_alerts.sql` around lines 34 -
47, Add a dedicated event_id column to alert_events and make it unique so
AlertEventPayload.eventId is stored as a first-class idempotency key instead of
only inside payload JSONB. Update the monitoring_alerts migration and the
createEvent() insert path to populate event_id from payload.eventId, and ensure
any lookup/dedup logic references the new column; use the alert_events and
createEvent symbols to locate the affected schema and write path.

@KevinMB0220

Copy link
Copy Markdown
Contributor

@mariocodecr fix the conflicts

@KevinMB0220 KevinMB0220 merged commit 99dea15 into Galaxy-KJ:main Jun 30, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] API: Real-time position monitoring and liquidation alerts (#53)

2 participants