mirror of
https://github.com/reconurge/flowsint.git
synced 2026-07-11 10:24:43 -05:00
Compare commits
10 Commits
v1.2.9
...
fix/sse-au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b317e08e57 | ||
|
|
9db1e675d6 | ||
|
|
4a75d6a924 | ||
|
|
d7f21e9384 | ||
|
|
6e5f26aaf0 | ||
|
|
078ac77eee | ||
|
|
ed8bfc5248 | ||
|
|
438bbe18c4 | ||
|
|
4e14df6c61 | ||
|
|
b707961e68 |
@@ -1,11 +1,10 @@
|
||||
from fastapi import Depends, HTTPException, status, Request
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy.orm import Session
|
||||
from flowsint_core.core.auth import ALGORITHM, AUTH_SECRET
|
||||
from flowsint_core.core.postgre_db import get_db
|
||||
from flowsint_core.core.models import Profile
|
||||
from typing import Optional
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
@@ -29,42 +28,3 @@ def get_current_user(
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
def get_current_user_sse(
|
||||
request: Request, db: Session = Depends(get_db)
|
||||
) -> Profile:
|
||||
"""
|
||||
Alternative authentication for SSE endpoints that accepts token via query parameter.
|
||||
EventSource API doesn't support custom headers, so we need to pass the token in the URL.
|
||||
"""
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
|
||||
# Try to get token from query parameter
|
||||
token: Optional[str] = request.query_params.get("token")
|
||||
|
||||
# Fallback to Authorization header if query param not present
|
||||
if not token:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if auth_header and auth_header.startswith("Bearer "):
|
||||
token = auth_header.replace("Bearer ", "")
|
||||
|
||||
if not token:
|
||||
raise credentials_exception
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, AUTH_SECRET, algorithms=[ALGORITHM])
|
||||
email: str = payload.get("sub")
|
||||
if email is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
user = db.query(Profile).filter(Profile.email == email).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
@@ -14,7 +14,7 @@ from flowsint_core.core.services import (
|
||||
PermissionDeniedError,
|
||||
DatabaseError,
|
||||
)
|
||||
from app.api.deps import get_current_user, get_current_user_sse
|
||||
from app.api.deps import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -42,7 +42,7 @@ async def stream_events(
|
||||
request: Request,
|
||||
sketch_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Profile = Depends(get_current_user_sse),
|
||||
current_user: Profile = Depends(get_current_user),
|
||||
):
|
||||
"""Stream events for a specific sketch in real-time."""
|
||||
service = create_log_service(db)
|
||||
@@ -58,7 +58,7 @@ async def stream_events(
|
||||
channel = sketch_id
|
||||
await event_emitter.subscribe(channel)
|
||||
try:
|
||||
yield 'data: {"event": "connected", "data": "Connected to log stream"}\n\n'
|
||||
yield json.dumps({"event": "connected", "data": "Connected to log stream"})
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
break
|
||||
@@ -115,7 +115,7 @@ async def stream_sketch_status(
|
||||
request: Request,
|
||||
sketch_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Profile = Depends(get_current_user_sse),
|
||||
current_user: Profile = Depends(get_current_user),
|
||||
):
|
||||
"""Stream COMPLETED events for a specific sketch (for graph refresh)."""
|
||||
service = create_log_service(db)
|
||||
@@ -160,49 +160,3 @@ async def stream_sketch_status(
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status/scan/{scan_id}/stream")
|
||||
async def stream_status(
|
||||
request: Request,
|
||||
scan_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Profile = Depends(get_current_user_sse),
|
||||
):
|
||||
"""Stream status updates for a specific scan in real-time."""
|
||||
service = create_log_service(db)
|
||||
try:
|
||||
service.get_scan_with_permission(scan_id, current_user.id)
|
||||
except NotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except PermissionDeniedError:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
async def status_generator():
|
||||
print("[EventEmitter] Start status generator")
|
||||
await event_emitter.subscribe(f"scan_{scan_id}_status")
|
||||
try:
|
||||
yield 'data: {"event": "connected", "data": "Connected to status stream"}\n\n'
|
||||
|
||||
while True:
|
||||
data = await event_emitter.get_message(f"scan_{scan_id}_status")
|
||||
if data is None:
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
print(f"[EventEmitter] Received status data: {data}")
|
||||
yield f"data: {data}\n\n"
|
||||
|
||||
except asyncio.CancelledError:
|
||||
print(f"[EventEmitter] Client disconnected from status stream for scan_id: {scan_id}")
|
||||
finally:
|
||||
await event_emitter.unsubscribe(f"scan_{scan_id}_status")
|
||||
|
||||
return EventSourceResponse(
|
||||
status_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
38
flowsint-api/tests/conftest.py
Normal file
38
flowsint-api/tests/conftest.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Pytest harness for flowsint-api. Provides a TestClient backed by an
|
||||
in-memory SQLite database, overriding the real Postgres get_db dependency."""
|
||||
|
||||
import os
|
||||
|
||||
# Env required at import time by flowsint-core modules. Set before importing app.
|
||||
os.environ.setdefault("AUTH_SECRET", "test-secret-please-ignore")
|
||||
os.environ.setdefault(
|
||||
"MASTER_VAULT_KEY_V1", "base64:qnHTmwYb+uoygIw9MsRMY22vS5YPchY+QOi/E79GAvM="
|
||||
)
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from flowsint_core.core.models import Base
|
||||
from flowsint_core.core.postgre_db import get_db
|
||||
from app.main import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
engine = create_engine(
|
||||
"sqlite:///:memory:", connect_args={"check_same_thread": False}
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(db_session):
|
||||
app.dependency_overrides[get_db] = lambda: db_session
|
||||
yield TestClient(app)
|
||||
app.dependency_overrides.clear()
|
||||
50
flowsint-api/tests/test_events_auth.py
Normal file
50
flowsint-api/tests/test_events_auth.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Auth and shape contract for the SSE event endpoints."""
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
from flowsint_core.core.auth import create_access_token
|
||||
from flowsint_core.core.models import Profile
|
||||
|
||||
|
||||
def test_get_current_user_sse_is_removed():
|
||||
"""The query-param SSE auth helper must no longer exist."""
|
||||
deps = importlib.import_module("app.api.deps")
|
||||
assert not hasattr(deps, "get_current_user_sse")
|
||||
|
||||
|
||||
def test_log_stream_requires_auth_header(client):
|
||||
"""No Authorization header -> 401 (no token in URL accepted)."""
|
||||
res = client.get("/api/events/sketch/abc/stream")
|
||||
assert res.status_code == 401
|
||||
|
||||
|
||||
def test_status_stream_requires_auth_header(client):
|
||||
res = client.get("/api/events/sketch/abc/status/stream")
|
||||
assert res.status_code == 401
|
||||
|
||||
|
||||
def test_log_stream_rejects_token_query_param(client):
|
||||
"""A token in the URL must NOT authenticate the request anymore."""
|
||||
token = create_access_token({"sub": "user@example.com"})
|
||||
res = client.get(f"/api/events/sketch/abc/stream?token={token}")
|
||||
assert res.status_code == 401
|
||||
|
||||
|
||||
def test_dead_scan_stream_endpoint_removed(client):
|
||||
"""The unused scan status stream endpoint must be gone."""
|
||||
res = client.get("/api/events/status/scan/abc/stream")
|
||||
assert res.status_code == 404
|
||||
|
||||
|
||||
def test_get_current_user_accepts_valid_bearer(db_session):
|
||||
"""The dependency the streams now use authenticates via a valid JWT."""
|
||||
from app.api.deps import get_current_user
|
||||
|
||||
db_session.add(Profile(email="user@example.com", hashed_password="x"))
|
||||
db_session.commit()
|
||||
|
||||
token = create_access_token({"sub": "user@example.com"})
|
||||
user = get_current_user(token=token, db=db_session)
|
||||
assert user.email == "user@example.com"
|
||||
@@ -11,6 +11,7 @@
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
@@ -19,6 +20,7 @@
|
||||
"@ai-sdk/react": "^3.0.79",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
@@ -139,6 +141,7 @@
|
||||
"react-dom": "^19.2.0",
|
||||
"standard-version": "^9.5.0",
|
||||
"typescript": "^5.5.2",
|
||||
"vite": "^5.3.1"
|
||||
"vite": "^5.3.1",
|
||||
"vitest": "^2.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
117
flowsint-app/src/api/sse.test.ts
Normal file
117
flowsint-app/src/api/sse.test.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// Capture the options passed to fetchEventSource so we can drive its callbacks.
|
||||
const calls: any[] = []
|
||||
const fetchEventSourceMock = vi.fn((url: string, opts: any) => {
|
||||
calls.push({ url, opts })
|
||||
return new Promise(() => {}) // never resolves; we drive callbacks manually
|
||||
})
|
||||
|
||||
vi.mock('@microsoft/fetch-event-source', () => ({
|
||||
fetchEventSource: (url: string, opts: any) => fetchEventSourceMock(url, opts),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/auth-store', () => ({
|
||||
useAuthStore: { getState: () => ({ token: 'tok123' }) },
|
||||
}))
|
||||
|
||||
import { connectSSE } from './sse'
|
||||
|
||||
const lastOpts = () => calls[calls.length - 1].opts
|
||||
|
||||
const fakeResponse = (status: number, contentType: string) =>
|
||||
({ ok: status >= 200 && status < 300, status, headers: { get: () => contentType } } as any)
|
||||
|
||||
beforeEach(() => {
|
||||
calls.length = 0
|
||||
fetchEventSourceMock.mockClear()
|
||||
})
|
||||
|
||||
describe('connectSSE', () => {
|
||||
it('passes the URL and Authorization header from the store', () => {
|
||||
connectSSE({ url: 'http://x/stream', onMessage: () => {} })
|
||||
expect(calls[0].url).toBe('http://x/stream')
|
||||
expect(lastOpts().headers).toEqual({ Authorization: 'Bearer tok123' })
|
||||
})
|
||||
|
||||
it('forwards parsed envelopes and skips "connected"', () => {
|
||||
const received: any[] = []
|
||||
connectSSE({ url: 'http://x', onMessage: (m) => received.push(m) })
|
||||
const o = lastOpts()
|
||||
o.onmessage({ data: JSON.stringify({ event: 'connected', data: 'hi' }) })
|
||||
o.onmessage({ data: JSON.stringify({ event: 'log', data: '{"msg":"a"}' }) })
|
||||
expect(received).toEqual([{ event: 'log', data: '{"msg":"a"}' }])
|
||||
})
|
||||
|
||||
it('ignores empty and malformed messages without throwing', () => {
|
||||
const received: any[] = []
|
||||
connectSSE({ url: 'http://x', onMessage: (m) => received.push(m) })
|
||||
const o = lastOpts()
|
||||
o.onmessage({ data: '' })
|
||||
o.onmessage({ data: 'not-json' })
|
||||
expect(received).toEqual([])
|
||||
})
|
||||
|
||||
it('aborts the request when the disposer is called', () => {
|
||||
const dispose = connectSSE({ url: 'http://x', onMessage: () => {} })
|
||||
expect(lastOpts().signal.aborted).toBe(false)
|
||||
dispose()
|
||||
expect(lastOpts().signal.aborted).toBe(true)
|
||||
})
|
||||
|
||||
it('throws (no retry) on a non-event-stream open', async () => {
|
||||
connectSSE({ url: 'http://x', onMessage: () => {} })
|
||||
await expect(lastOpts().onopen(fakeResponse(401, 'application/json'))).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('accepts a valid event-stream open', async () => {
|
||||
connectSSE({ url: 'http://x', onMessage: () => {} })
|
||||
await expect(
|
||||
lastOpts().onopen(fakeResponse(200, 'text/event-stream'))
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns increasing backoff for transient errors', () => {
|
||||
connectSSE({ url: 'http://x', onMessage: () => {} })
|
||||
const o = lastOpts()
|
||||
const first = o.onerror(new Error('network'))
|
||||
const second = o.onerror(new Error('network'))
|
||||
expect(first).toBe(2000)
|
||||
expect(second).toBe(4000)
|
||||
})
|
||||
|
||||
it('rethrows fatal errors from onerror to stop retrying', async () => {
|
||||
connectSSE({ url: 'http://x', onMessage: () => {} })
|
||||
const o = lastOpts()
|
||||
let fatal: unknown
|
||||
try {
|
||||
await o.onopen(fakeResponse(403, 'application/json'))
|
||||
} catch (e) {
|
||||
fatal = e
|
||||
}
|
||||
expect(() => o.onerror(fatal)).toThrow()
|
||||
})
|
||||
|
||||
it('ignores valid JSON that is not an envelope', () => {
|
||||
const received: any[] = []
|
||||
connectSSE({ url: 'http://x', onMessage: (m) => received.push(m) })
|
||||
const o = lastOpts()
|
||||
o.onmessage({ data: '42' })
|
||||
o.onmessage({ data: 'null' })
|
||||
o.onmessage({ data: '{"data":"no-event"}' })
|
||||
expect(received).toEqual([])
|
||||
})
|
||||
|
||||
it('calls onError once when the connection promise rejects (non-fatal)', async () => {
|
||||
const errors: unknown[] = []
|
||||
fetchEventSourceMock.mockImplementationOnce((url: string, opts: any) => {
|
||||
calls.push({ url, opts })
|
||||
return Promise.reject(new Error('boom'))
|
||||
})
|
||||
connectSSE({ url: 'http://x', onMessage: () => {}, onError: (e) => errors.push(e) })
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(errors).toHaveLength(1)
|
||||
expect((errors[0] as Error).message).toBe('boom')
|
||||
})
|
||||
})
|
||||
72
flowsint-app/src/api/sse.ts
Normal file
72
flowsint-app/src/api/sse.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
|
||||
/**
|
||||
* Outer SSE envelope from the server. `data` is forwarded exactly as received;
|
||||
* for the log/status streams it is itself a JSON-encoded string that the
|
||||
* consumer parses (e.g. `JSON.parse(envelope.data)`).
|
||||
*/
|
||||
export type SSEEnvelope = { event: string; data: unknown }
|
||||
|
||||
/** Thrown to tell fetch-event-source to stop retrying (e.g. auth failure). */
|
||||
class FatalSSEError extends Error {}
|
||||
|
||||
const MAX_BACKOFF_MS = 30000
|
||||
|
||||
/**
|
||||
* Open an authenticated SSE connection.
|
||||
* Reads the bearer token once at connect time and sends it via the
|
||||
* Authorization header. Returns a disposer that aborts the connection.
|
||||
*/
|
||||
export function connectSSE(opts: {
|
||||
url: string
|
||||
onMessage: (msg: SSEEnvelope) => void
|
||||
onError?: (err: unknown) => void
|
||||
}): () => void {
|
||||
const controller = new AbortController()
|
||||
let retry = 0
|
||||
|
||||
const token = useAuthStore.getState().token
|
||||
const headers: Record<string, string> = token
|
||||
? { Authorization: `Bearer ${token}` }
|
||||
: {}
|
||||
|
||||
fetchEventSource(opts.url, {
|
||||
signal: controller.signal,
|
||||
openWhenHidden: true,
|
||||
headers,
|
||||
onopen: async (res) => {
|
||||
const contentType = res.headers.get('content-type') ?? ''
|
||||
if (!res.ok || !contentType.includes('text/event-stream')) {
|
||||
throw new FatalSSEError(`SSE open failed: ${res.status}`)
|
||||
}
|
||||
retry = 0
|
||||
},
|
||||
onmessage: (ev) => {
|
||||
if (!ev.data) return
|
||||
let envelope: SSEEnvelope
|
||||
try {
|
||||
envelope = JSON.parse(ev.data) as SSEEnvelope
|
||||
} catch (error) {
|
||||
console.error('[connectSSE] malformed envelope:', error, ev.data)
|
||||
return
|
||||
}
|
||||
if (typeof envelope?.event !== 'string') return
|
||||
if (envelope.event === 'connected') return
|
||||
opts.onMessage(envelope)
|
||||
},
|
||||
onerror: (err) => {
|
||||
if (err instanceof FatalSSEError) {
|
||||
opts.onError?.(err)
|
||||
throw err // stop: no retry on auth/protocol failure
|
||||
}
|
||||
// transient failure: retry silently with exponential backoff (onError is not called)
|
||||
retry += 1
|
||||
return Math.min(1000 * 2 ** retry, MAX_BACKOFF_MS)
|
||||
},
|
||||
}).catch((err) => {
|
||||
if (!(err instanceof FatalSSEError)) opts.onError?.(err)
|
||||
})
|
||||
|
||||
return () => controller.abort()
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { logService } from '@/api/log-service'
|
||||
import { queryKeys } from '@/api/query-keys'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { connectSSE } from '@/api/sse'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL
|
||||
|
||||
@@ -30,45 +31,21 @@ export function useEvents(sketch_id: string | undefined) {
|
||||
useEffect(() => {
|
||||
if (!sketch_id || !token) return
|
||||
|
||||
const eventSource = new EventSource(
|
||||
`${API_URL}/api/events/sketch/${sketch_id}/stream?token=${token}`
|
||||
)
|
||||
|
||||
eventSource.onmessage = (e) => {
|
||||
try {
|
||||
// Handle malformed SSE data (connection message has extra "data: " prefix)
|
||||
let dataStr = e.data
|
||||
if (dataStr.startsWith('data: ')) {
|
||||
dataStr = dataStr.substring(6) // Remove "data: " prefix
|
||||
}
|
||||
|
||||
const raw = JSON.parse(dataStr) as any
|
||||
|
||||
// Ignore connection messages
|
||||
if (raw.event === 'connected') {
|
||||
return
|
||||
}
|
||||
|
||||
const dispose = connectSSE({
|
||||
url: `${API_URL}/api/events/sketch/${sketch_id}/stream`,
|
||||
onMessage: (raw) => {
|
||||
// Only process log events
|
||||
if (raw.event !== 'log') {
|
||||
return
|
||||
if (raw.event !== 'log') return
|
||||
try {
|
||||
const event = JSON.parse(raw.data as string) as Event
|
||||
setLiveLogs((prev) => [...prev.slice(-99), event])
|
||||
} catch (error) {
|
||||
console.error('[useSketchEvents] Failed to parse log payload:', error, raw.data)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const event = JSON.parse(raw.data) as Event
|
||||
setLiveLogs((prev) => [...prev.slice(-99), event])
|
||||
} catch (error) {
|
||||
console.error('[useSketchEvents] Failed to parse SSE event:', error, e.data)
|
||||
}
|
||||
}
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('[useSketchEvents] EventSource error:', error)
|
||||
eventSource.close()
|
||||
}
|
||||
|
||||
return () => {
|
||||
eventSource.close()
|
||||
}
|
||||
return dispose
|
||||
}, [sketch_id, token])
|
||||
|
||||
const logs = useMemo(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'
|
||||
import { useGraphControls } from '@/stores/graph-controls-store'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { EventLevel } from '@/types'
|
||||
import { connectSSE } from '@/api/sse'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL
|
||||
|
||||
@@ -25,55 +26,35 @@ export function useGraphRefresh(sketch_id: string | undefined) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!sketch_id || !token) return
|
||||
const eventSource = new EventSource(
|
||||
`${API_URL}/api/events/sketch/${sketch_id}/status/stream?token=${token}`
|
||||
)
|
||||
eventSource.onmessage = (e) => {
|
||||
try {
|
||||
// Handle malformed SSE data (connection message has extra "data: " prefix)
|
||||
let dataStr = e.data
|
||||
if (dataStr.startsWith('data: ')) {
|
||||
dataStr = dataStr.substring(6) // Remove "data: " prefix
|
||||
}
|
||||
const raw = JSON.parse(dataStr) as any
|
||||
// Ignore connection messages
|
||||
if (raw.event === 'connected') {
|
||||
return
|
||||
}
|
||||
|
||||
const dispose = connectSSE({
|
||||
url: `${API_URL}/api/events/sketch/${sketch_id}/status/stream`,
|
||||
onMessage: (raw) => {
|
||||
// Only process status events
|
||||
if (raw.event !== 'status') {
|
||||
return
|
||||
}
|
||||
const event = JSON.parse(raw.data) as any
|
||||
// Only handle COMPLETED events
|
||||
if (event.type === EventLevel.COMPLETED) {
|
||||
const refetch = refetchGraphRef.current
|
||||
const regenerate = regenerateLayoutRef.current
|
||||
const layoutType = currentLayoutTypeRef.current
|
||||
if (raw.event !== 'status') return
|
||||
try {
|
||||
const event = JSON.parse(raw.data as string) as any
|
||||
// Only handle COMPLETED events
|
||||
if (event.type === EventLevel.COMPLETED) {
|
||||
const refetch = refetchGraphRef.current
|
||||
const regenerate = regenerateLayoutRef.current
|
||||
const layoutType = currentLayoutTypeRef.current
|
||||
|
||||
if (typeof refetch !== 'function') {
|
||||
return
|
||||
if (typeof refetch !== 'function') return
|
||||
|
||||
// Refetch graph data, then regenerate layout if one is active
|
||||
refetch(() => {
|
||||
if (layoutType && typeof regenerate === 'function') {
|
||||
regenerate(layoutType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Refetch graph data with callback to regenerate layout after
|
||||
refetch(() => {
|
||||
// After refetch completes, regenerate layout if we have one active
|
||||
if (layoutType && typeof regenerate === 'function') {
|
||||
regenerate(layoutType)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[useGraphRefresh] Failed to parse status payload:', error, raw.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[useGraphRefresh] Failed to parse SSE event:', error, e.data)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
eventSource.onerror = () => {
|
||||
eventSource.close()
|
||||
}
|
||||
|
||||
return () => {
|
||||
eventSource.close()
|
||||
}
|
||||
}, [sketch_id, token]) // Only reconnect when sketch_id or token changes
|
||||
return dispose
|
||||
}, [sketch_id, token])
|
||||
}
|
||||
|
||||
13
flowsint-app/vitest.config.ts
Normal file
13
flowsint-app/vitest.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'node',
|
||||
},
|
||||
})
|
||||
@@ -2,9 +2,9 @@
|
||||
Analysis service for managing analyses within investigations.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -64,8 +64,8 @@ class AnalysisService(BaseService):
|
||||
content=content,
|
||||
owner_id=owner_id,
|
||||
investigation_id=investigation_id,
|
||||
created_at=datetime.utcnow(),
|
||||
last_updated_at=datetime.utcnow(),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
last_updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
self._analysis_repo.add(new_analysis)
|
||||
self._commit()
|
||||
@@ -97,7 +97,7 @@ class AnalysisService(BaseService):
|
||||
self._check_permission(user_id, investigation_id, ["update"])
|
||||
analysis.investigation_id = investigation_id
|
||||
|
||||
analysis.last_updated_at = datetime.utcnow()
|
||||
analysis.last_updated_at = datetime.now(timezone.utc)
|
||||
self._commit()
|
||||
self._refresh(analysis)
|
||||
return analysis
|
||||
|
||||
@@ -4,7 +4,7 @@ Chat service for managing chats and messages with AI integration.
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, AsyncIterator, Dict, List, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
@@ -76,8 +76,8 @@ class ChatService(BaseService):
|
||||
description=description,
|
||||
owner_id=owner_id,
|
||||
investigation_id=investigation_id,
|
||||
created_at=datetime.utcnow(),
|
||||
last_updated_at=datetime.utcnow(),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
last_updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
self._chat_repo.add(new_chat)
|
||||
self._commit()
|
||||
@@ -103,7 +103,7 @@ class ChatService(BaseService):
|
||||
if not chat:
|
||||
raise NotFoundError("Chat not found")
|
||||
|
||||
chat.last_updated_at = datetime.utcnow()
|
||||
chat.last_updated_at = datetime.now(timezone.utc)
|
||||
|
||||
user_message = ChatMessage(
|
||||
id=uuid4(),
|
||||
@@ -111,7 +111,7 @@ class ChatService(BaseService):
|
||||
context=context,
|
||||
chat_id=chat_id,
|
||||
is_bot=False,
|
||||
created_at=datetime.utcnow(),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
self._chat_repo.add_message(user_message)
|
||||
self._commit()
|
||||
@@ -124,7 +124,7 @@ class ChatService(BaseService):
|
||||
content=content,
|
||||
chat_id=chat_id,
|
||||
is_bot=True,
|
||||
created_at=datetime.utcnow(),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
self._chat_repo.add_message(chat_message)
|
||||
self._commit()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Flow service for managing flows and flow computations.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
@@ -82,8 +82,8 @@ class FlowService(BaseService):
|
||||
description=description,
|
||||
category=category,
|
||||
flow_schema=flow_schema,
|
||||
created_at=datetime.utcnow(),
|
||||
last_updated_at=datetime.utcnow(),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
last_updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
self._flow_repo.add(new_flow)
|
||||
self._commit()
|
||||
@@ -101,7 +101,7 @@ class FlowService(BaseService):
|
||||
value.append("Username")
|
||||
setattr(flow, key, value)
|
||||
|
||||
flow.last_updated_at = datetime.utcnow()
|
||||
flow.last_updated_at = datetime.now(timezone.utc)
|
||||
self._commit()
|
||||
self._refresh(flow)
|
||||
return flow
|
||||
|
||||
72
flowsint-core/tests/services/test_timezone_timestamps.py
Normal file
72
flowsint-core/tests/services/test_timezone_timestamps.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Tests for timezone-aware service timestamps."""
|
||||
|
||||
from datetime import timezone
|
||||
from unittest.mock import MagicMock
|
||||
from uuid import uuid4
|
||||
|
||||
from flowsint_core.core.services.analysis_service import AnalysisService
|
||||
from flowsint_core.core.services.chat_service import ChatService
|
||||
from flowsint_core.core.services.flow_service import FlowService
|
||||
|
||||
|
||||
def _assert_utc(value):
|
||||
assert value.tzinfo is not None
|
||||
assert value.utcoffset() == timezone.utc.utcoffset(value)
|
||||
|
||||
|
||||
def test_analysis_service_create_uses_timezone_aware_timestamps():
|
||||
service = AnalysisService(
|
||||
db=MagicMock(),
|
||||
analysis_repo=MagicMock(),
|
||||
investigation_repo=MagicMock(),
|
||||
)
|
||||
service._check_permission = MagicMock()
|
||||
|
||||
analysis = service.create(
|
||||
title="Summary",
|
||||
description=None,
|
||||
content={},
|
||||
investigation_id=uuid4(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
|
||||
_assert_utc(analysis.created_at)
|
||||
_assert_utc(analysis.last_updated_at)
|
||||
|
||||
|
||||
def test_chat_service_create_uses_timezone_aware_timestamps():
|
||||
service = ChatService(
|
||||
db=MagicMock(),
|
||||
chat_repo=MagicMock(),
|
||||
vault_service=MagicMock(),
|
||||
)
|
||||
|
||||
chat = service.create(
|
||||
title="Investigation chat",
|
||||
description=None,
|
||||
investigation_id=uuid4(),
|
||||
owner_id=uuid4(),
|
||||
)
|
||||
|
||||
_assert_utc(chat.created_at)
|
||||
_assert_utc(chat.last_updated_at)
|
||||
|
||||
|
||||
def test_flow_service_create_uses_timezone_aware_timestamps():
|
||||
service = FlowService(
|
||||
db=MagicMock(),
|
||||
flow_repo=MagicMock(),
|
||||
custom_type_repo=MagicMock(),
|
||||
sketch_repo=MagicMock(),
|
||||
investigation_repo=MagicMock(),
|
||||
)
|
||||
|
||||
flow = service.create(
|
||||
name="Resolve domain",
|
||||
description=None,
|
||||
category=["Domain"],
|
||||
flow_schema={"nodes": [], "edges": []},
|
||||
)
|
||||
|
||||
_assert_utc(flow.created_at)
|
||||
_assert_utc(flow.last_updated_at)
|
||||
@@ -53,7 +53,7 @@ class MaigretEnricher(Enricher):
|
||||
return results
|
||||
|
||||
try:
|
||||
with open(output_file, "r") as f:
|
||||
with open(output_file, "r", encoding="utf-8") as f:
|
||||
raw_data = json.load(f)
|
||||
except Exception as e:
|
||||
Logger.error(
|
||||
|
||||
@@ -63,7 +63,7 @@ class SherlockEnricher(Enricher):
|
||||
continue
|
||||
|
||||
found_accounts = {}
|
||||
with open(output_file, "r") as f:
|
||||
with open(output_file, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and line.startswith("http"):
|
||||
|
||||
218
yarn.lock
218
yarn.lock
@@ -956,6 +956,11 @@
|
||||
pbf "^4.0.1"
|
||||
supercluster "^8.0.1"
|
||||
|
||||
"@microsoft/fetch-event-source@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d"
|
||||
integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==
|
||||
|
||||
"@monaco-editor/loader@^1.5.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz"
|
||||
@@ -2580,6 +2585,65 @@
|
||||
"@types/babel__core" "^7.20.5"
|
||||
react-refresh "^0.17.0"
|
||||
|
||||
"@vitest/expect@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.9.tgz#b566ea20d58ea6578d8dc37040d6c1a47ebe5ff8"
|
||||
integrity sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==
|
||||
dependencies:
|
||||
"@vitest/spy" "2.1.9"
|
||||
"@vitest/utils" "2.1.9"
|
||||
chai "^5.1.2"
|
||||
tinyrainbow "^1.2.0"
|
||||
|
||||
"@vitest/mocker@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.9.tgz#36243b27351ca8f4d0bbc4ef91594ffd2dc25ef5"
|
||||
integrity sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==
|
||||
dependencies:
|
||||
"@vitest/spy" "2.1.9"
|
||||
estree-walker "^3.0.3"
|
||||
magic-string "^0.30.12"
|
||||
|
||||
"@vitest/pretty-format@2.1.9", "@vitest/pretty-format@^2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz#434ff2f7611689f9ce70cd7d567eceb883653fdf"
|
||||
integrity sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==
|
||||
dependencies:
|
||||
tinyrainbow "^1.2.0"
|
||||
|
||||
"@vitest/runner@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.9.tgz#cc18148d2d797fd1fd5908d1f1851d01459be2f6"
|
||||
integrity sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==
|
||||
dependencies:
|
||||
"@vitest/utils" "2.1.9"
|
||||
pathe "^1.1.2"
|
||||
|
||||
"@vitest/snapshot@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.9.tgz#24260b93f798afb102e2dcbd7e61c6dfa118df91"
|
||||
integrity sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==
|
||||
dependencies:
|
||||
"@vitest/pretty-format" "2.1.9"
|
||||
magic-string "^0.30.12"
|
||||
pathe "^1.1.2"
|
||||
|
||||
"@vitest/spy@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.9.tgz#cb28538c5039d09818b8bfa8edb4043c94727c60"
|
||||
integrity sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==
|
||||
dependencies:
|
||||
tinyspy "^3.0.2"
|
||||
|
||||
"@vitest/utils@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.9.tgz#4f2486de8a54acf7ecbf2c5c24ad7994a680a6c1"
|
||||
integrity sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==
|
||||
dependencies:
|
||||
"@vitest/pretty-format" "2.1.9"
|
||||
loupe "^3.1.2"
|
||||
tinyrainbow "^1.2.0"
|
||||
|
||||
"@webgpu/types@^0.1.69":
|
||||
version "0.1.69"
|
||||
resolved "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz"
|
||||
@@ -2831,6 +2895,11 @@ arrify@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz"
|
||||
integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
|
||||
|
||||
assertion-error@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
|
||||
integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
|
||||
|
||||
assign-symbols@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz"
|
||||
@@ -2990,6 +3059,11 @@ bytewise@^1.1.0:
|
||||
bytewise-core "^1.2.2"
|
||||
typewise "^1.0.3"
|
||||
|
||||
cac@^6.7.14:
|
||||
version "6.7.14"
|
||||
resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959"
|
||||
integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==
|
||||
|
||||
cachedir@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz"
|
||||
@@ -3057,6 +3131,17 @@ ccount@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz"
|
||||
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
|
||||
|
||||
chai@^5.1.2:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06"
|
||||
integrity sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==
|
||||
dependencies:
|
||||
assertion-error "^2.0.1"
|
||||
check-error "^2.1.1"
|
||||
deep-eql "^5.0.1"
|
||||
loupe "^3.1.0"
|
||||
pathval "^2.0.0"
|
||||
|
||||
chalk@^2.4.1, chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
|
||||
@@ -3119,6 +3204,11 @@ chardet@^0.7.0:
|
||||
resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz"
|
||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||
|
||||
check-error@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.3.tgz#2427361117b70cca8dc89680ead32b157019caf5"
|
||||
integrity sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==
|
||||
|
||||
chokidar@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
|
||||
@@ -3862,7 +3952,7 @@ dateformat@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz"
|
||||
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
||||
|
||||
debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2:
|
||||
debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.7:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz"
|
||||
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
||||
@@ -3904,6 +3994,11 @@ deep-diff@^1.0.2:
|
||||
resolved "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz"
|
||||
integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==
|
||||
|
||||
deep-eql@^5.0.1:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341"
|
||||
integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==
|
||||
|
||||
deep-is@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz"
|
||||
@@ -4208,6 +4303,11 @@ es-iterator-helpers@^1.2.1:
|
||||
iterator.prototype "^1.1.4"
|
||||
safe-array-concat "^1.1.3"
|
||||
|
||||
es-module-lexer@^1.5.4:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
|
||||
integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
|
||||
|
||||
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz"
|
||||
@@ -4441,6 +4541,13 @@ estree-util-is-identifier-name@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz"
|
||||
integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==
|
||||
|
||||
estree-walker@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
|
||||
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.0"
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
|
||||
@@ -4468,6 +4575,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
|
||||
dependencies:
|
||||
homedir-polyfill "^1.0.1"
|
||||
|
||||
expect-type@^1.1.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68"
|
||||
integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==
|
||||
|
||||
extend-shallow@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz"
|
||||
@@ -5984,6 +6096,11 @@ loose-envify@^1.4.0:
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
loupe@^3.1.0, loupe@^3.1.2:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76"
|
||||
integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==
|
||||
|
||||
lowlight@^1.17.0:
|
||||
version "1.20.0"
|
||||
resolved "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz"
|
||||
@@ -6020,7 +6137,7 @@ lucide-react@^0.511.0:
|
||||
resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz"
|
||||
integrity sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==
|
||||
|
||||
magic-string@^0.30.19:
|
||||
magic-string@^0.30.12, magic-string@^0.30.19:
|
||||
version "0.30.21"
|
||||
resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz"
|
||||
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
|
||||
@@ -7026,11 +7143,21 @@ path-type@^3.0.0:
|
||||
dependencies:
|
||||
pify "^3.0.0"
|
||||
|
||||
pathe@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec"
|
||||
integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==
|
||||
|
||||
pathe@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz"
|
||||
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
|
||||
|
||||
pathval@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d"
|
||||
integrity sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==
|
||||
|
||||
pbf@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz"
|
||||
@@ -8041,6 +8168,11 @@ side-channel@^1.1.0:
|
||||
side-channel-map "^1.0.1"
|
||||
side-channel-weakmap "^1.0.2"
|
||||
|
||||
siginfo@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
|
||||
integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
|
||||
|
||||
signal-exit@^3.0.2:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
|
||||
@@ -8150,6 +8282,11 @@ split@^1.0.0:
|
||||
dependencies:
|
||||
through "2"
|
||||
|
||||
stackback@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
|
||||
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
|
||||
|
||||
standard-version@^9.5.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.npmjs.org/standard-version/-/standard-version-9.5.0.tgz"
|
||||
@@ -8175,6 +8312,11 @@ state-local@^1.0.6:
|
||||
resolved "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz"
|
||||
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
|
||||
|
||||
std-env@^3.8.0:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b"
|
||||
integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==
|
||||
|
||||
stop-iteration-iterator@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz"
|
||||
@@ -8430,11 +8572,21 @@ tiny-warning@^1.0.3:
|
||||
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
|
||||
tinybench@^2.9.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
|
||||
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
|
||||
|
||||
tinycolor2@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz"
|
||||
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
|
||||
|
||||
tinyexec@^0.3.1:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2"
|
||||
integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
|
||||
|
||||
tinyexec@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz"
|
||||
@@ -8448,11 +8600,26 @@ tinyglobby@^0.2.15:
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.3"
|
||||
|
||||
tinypool@^1.0.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591"
|
||||
integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==
|
||||
|
||||
tinyqueue@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz"
|
||||
integrity sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==
|
||||
|
||||
tinyrainbow@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5"
|
||||
integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==
|
||||
|
||||
tinyspy@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a"
|
||||
integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==
|
||||
|
||||
tippy.js@^6.3.7:
|
||||
version "6.3.7"
|
||||
resolved "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz"
|
||||
@@ -8838,7 +9005,18 @@ victory-vendor@^36.6.8:
|
||||
d3-time "^3.0.0"
|
||||
d3-timer "^3.0.1"
|
||||
|
||||
vite@^5.3.1:
|
||||
vite-node@2.1.9:
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f"
|
||||
integrity sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==
|
||||
dependencies:
|
||||
cac "^6.7.14"
|
||||
debug "^4.3.7"
|
||||
es-module-lexer "^1.5.4"
|
||||
pathe "^1.1.2"
|
||||
vite "^5.0.0"
|
||||
|
||||
vite@^5.0.0, vite@^5.3.1:
|
||||
version "5.4.21"
|
||||
resolved "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz"
|
||||
integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==
|
||||
@@ -8863,6 +9041,32 @@ vite@^7.1.7:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vitest@^2.1.9:
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.9.tgz#7d01ffd07a553a51c87170b5e80fea3da7fb41e7"
|
||||
integrity sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==
|
||||
dependencies:
|
||||
"@vitest/expect" "2.1.9"
|
||||
"@vitest/mocker" "2.1.9"
|
||||
"@vitest/pretty-format" "^2.1.9"
|
||||
"@vitest/runner" "2.1.9"
|
||||
"@vitest/snapshot" "2.1.9"
|
||||
"@vitest/spy" "2.1.9"
|
||||
"@vitest/utils" "2.1.9"
|
||||
chai "^5.1.2"
|
||||
debug "^4.3.7"
|
||||
expect-type "^1.1.0"
|
||||
magic-string "^0.30.12"
|
||||
pathe "^1.1.2"
|
||||
std-env "^3.8.0"
|
||||
tinybench "^2.9.0"
|
||||
tinyexec "^0.3.1"
|
||||
tinypool "^1.0.1"
|
||||
tinyrainbow "^1.2.0"
|
||||
vite "^5.0.0"
|
||||
vite-node "2.1.9"
|
||||
why-is-node-running "^2.3.0"
|
||||
|
||||
w3c-keyname@^2.2.0:
|
||||
version "2.2.8"
|
||||
resolved "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz"
|
||||
@@ -8947,6 +9151,14 @@ which@^2.0.1:
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
why-is-node-running@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04"
|
||||
integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==
|
||||
dependencies:
|
||||
siginfo "^2.0.0"
|
||||
stackback "0.0.2"
|
||||
|
||||
word-wrap@^1.0.3, word-wrap@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"
|
||||
|
||||
Reference in New Issue
Block a user