10 Commits

Author SHA1 Message Date
dextmorgn
b317e08e57 feat(api): authenticate SSE via header, drop query-param auth and dead route 2026-06-03 11:28:41 +02:00
dextmorgn
9db1e675d6 test(api): SSE auth-gate and endpoint-removal tests (red) 2026-06-03 11:22:58 +02:00
dextmorgn
4a75d6a924 refactor(app): stream graph status via connectSSE (token out of URL) 2026-06-03 11:08:11 +02:00
dextmorgn
d7f21e9384 refactor(app): stream logs via connectSSE (token out of URL) 2026-06-03 10:56:32 +02:00
dextmorgn
6e5f26aaf0 feat(app): add connectSSE helper with header auth and backoff 2026-06-03 09:28:49 +02:00
dextmorgn
078ac77eee build(app): add fetch-event-source + vitest test runner 2026-06-02 22:45:36 +02:00
dextmorgn
ed8bfc5248 Merge pull request #149 from Ghraven/fix/timezone-aware-service-timestamps
Use timezone-aware UTC timestamps in core services
2026-06-02 21:11:45 +02:00
dextmorgn
438bbe18c4 Merge pull request #150 from Ghraven/fix-social-enricher-output-encoding
fix: read social enricher outputs as utf-8
2026-06-02 21:10:12 +02:00
Ghraven
4e14df6c61 fix: read social enricher outputs as utf-8 2026-06-01 00:32:48 +08:00
Ghraven
b707961e68 fix: use timezone-aware service timestamps 2026-05-31 02:35:11 +08:00
17 changed files with 642 additions and 193 deletions

View File

@@ -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

View File

@@ -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",
},
)

View 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()

View 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"

View File

@@ -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"
}
}

View 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')
})
})

View 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()
}

View File

@@ -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(

View File

@@ -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])
}

View 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',
},
})

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View 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)

View File

@@ -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(

View File

@@ -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
View File

@@ -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"