refactor(extension): 모듈 분리 + Hub 통합 테스트 #task-395

- extension.ts 3,446→1,289줄 (-63%)
- step-probe.ts (1,435줄): setupMonitor, processResponseFile, tryApprovalStrategies
- observer-script.ts (687줄): DOM observer script
- ws-client.ts (390줄): WSBridgeClient
- step-utils.ts (114줄): step 파싱 유틸
- auth.py (115줄): JWT + registration code
- hub.py (581줄): WSHub + per-client queue
- Hub WS 연동 테스트 통과 (auth, chat, register)
- VSIX v0.4.0 빌드
This commit is contained in:
Variet Worker
2026-03-17 06:41:42 +09:00
parent a372bd8b2d
commit 5f795b9a91
19 changed files with 5426 additions and 5538 deletions

View File

@@ -1,10 +1,12 @@
"""Gateway HTTP API — receives data from remote Collectors and routes to Discord bot.
"""Gateway HTTP API + WebSocket Hub — receives data from Collectors and Extensions.
Runs alongside the Discord bot in the server Docker container.
Collectors (local PCs) push pending approvals, chat snapshots, and registrations
to this API, and poll for responses.
Supports both:
- REST API: for legacy Collectors (HTTP polling)
- WebSocket: for direct Extension connections (real-time)
Endpoints:
GET /ws — WebSocket endpoint (Extension direct connection)
POST /api/pending — Collector pushes a new approval request
GET /api/pending — List all pending requests (for diagnostics)
POST /api/response/{rid} — Collector polls for response (or Gateway pushes)
@@ -14,6 +16,7 @@ Endpoints:
POST /api/command — Gateway pushes command to specific collector
GET /api/commands/{project} — Collector polls for commands
GET /health — Health check
GET /hub/status — WebSocket Hub diagnostics
"""
import asyncio
@@ -34,13 +37,15 @@ COMMAND_TTL = 1800 # 30 min — stale commands auto-deleted
class GatewayAPI:
"""HTTP API server for Collector ↔ Gateway communication."""
"""HTTP API + WebSocket Hub server."""
def __init__(self, bot, host: str = "0.0.0.0", port: int = 8585, api_key: str = ""):
def __init__(self, bot, host: str = "0.0.0.0", port: int = 8585,
api_key: str = "", hub=None):
self.bot = bot
self.host = host
self.port = port
self.api_key = api_key
self.hub = hub # WSHub instance (None = WS disabled)
self.app = web.Application(
middlewares=[self._auth_middleware],
client_max_size=1024 * 1024, # Security: 1MB max request body
@@ -52,6 +57,10 @@ class GatewayAPI:
self._rate_limits: dict[str, list[float]] = defaultdict(list) # IP → [timestamps]
def _setup_routes(self):
# WebSocket endpoint (no auth middleware — Hub handles its own auth)
self.app.router.add_get("/ws", self._ws_handler)
self.app.router.add_get("/hub/status", self._hub_status)
# Legacy REST endpoints (Collector compatibility)
self.app.router.add_get("/health", self._health)
self.app.router.add_post("/api/pending", self._post_pending)
self.app.router.add_get("/api/pending", self._list_pending)
@@ -61,13 +70,29 @@ class GatewayAPI:
self.app.router.add_get("/api/commands/{project}", self._get_commands)
self.app.router.add_post("/api/event", self._post_event)
# ─── WebSocket Handler ───
async def _ws_handler(self, request: web.Request) -> web.WebSocketResponse:
"""WebSocket endpoint for direct Extension connections."""
if not self.hub:
return web.json_response(
{"error": "WebSocket Hub not enabled"}, status=503
)
return await self.hub.handle_ws(request)
async def _hub_status(self, request: web.Request) -> web.Response:
"""WebSocket Hub diagnostics."""
if not self.hub:
return web.json_response({"hub": "disabled"})
return web.json_response(self.hub.get_status())
# ─── Auth Middleware ───
@web.middleware
async def _auth_middleware(self, request: web.Request, handler):
"""Reject requests without valid API key on /api/* routes."""
# Health endpoint is public
if request.path == "/health":
# WebSocket and public endpoints skip API key auth
if request.path in ("/health", "/ws", "/hub/status"):
return await handler(request)
# All /api/* routes require auth + rate limit
@@ -109,11 +134,15 @@ class GatewayAPI:
# ─── Health ───
async def _health(self, request: web.Request) -> web.Response:
return web.json_response({
status = {
"status": "ok",
"bot_ready": self.bot.is_ready() if self.bot else False,
"timestamp": time.time(),
})
"hub_enabled": self.hub is not None,
}
if self.hub:
status["hub_connections"] = len(self.hub.connections)
return web.json_response(status)
# ─── Pending Approvals (Collector → Gateway → Discord) ───