Primo commit - Prompt 02 — WebSocket endpoints /agent e /client + parsing robusto
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
coverage/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Runtime env
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# OS/editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
255
CODEX_INIT.txt
Normal file
255
CODEX_INIT.txt
Normal file
@@ -0,0 +1,255 @@
|
||||
1. Contesto del progetto (READ FIRST)
|
||||
|
||||
Stiamo costruendo un sistema di controllo remoto composto da:
|
||||
|
||||
Windows Client (Electron)
|
||||
⇅ WebSocket (control-plane)
|
||||
VPS Signaling Server (Node.js / TS)
|
||||
⇅ WebSocket
|
||||
Android Tablet Agent (Kotlin)
|
||||
|
||||
|
||||
⚠️ IMPORTANTE
|
||||
|
||||
Il VPS NON trasporta video/audio/input.
|
||||
|
||||
Il VPS serve SOLO per:
|
||||
|
||||
autenticazione
|
||||
|
||||
signaling
|
||||
|
||||
presence (online/offline)
|
||||
|
||||
autorizzazione connessioni
|
||||
|
||||
In fasi successive, il traffico media passerà via WebRTC P2P/TURN (NON in Fase 1).
|
||||
|
||||
2. Stack tecnologico (decisioni definitive)
|
||||
Linguaggi
|
||||
|
||||
TypeScript ovunque possibile
|
||||
|
||||
server: Node.js + TypeScript
|
||||
|
||||
windows: Electron + TypeScript
|
||||
|
||||
shared: TypeScript
|
||||
|
||||
Android: Kotlin (fuori scope TS)
|
||||
|
||||
Scelte chiave (NON cambiare senza chiedere)
|
||||
|
||||
Client Windows: Electron
|
||||
|
||||
Protocollo: WebSocket + JSON tipizzato
|
||||
|
||||
Condivisione tipi: pacchetto shared/protocol
|
||||
|
||||
Stato: in-memory per Fase 1 (DB arriverà dopo)
|
||||
|
||||
Auth Fase 1: seed user + pairingKey (MVP)
|
||||
|
||||
3. Modalità operativa tablet (definitiva)
|
||||
|
||||
Tablet Android:
|
||||
|
||||
sempre in ascolto
|
||||
|
||||
ForegroundService sempre attivo
|
||||
|
||||
MediaProjection verrà avviata in Fase 2
|
||||
|
||||
Connessioni in ingresso:
|
||||
|
||||
SEMPRE con accettazione manuale
|
||||
|
||||
notifica Android con “Accetta / Rifiuta”
|
||||
|
||||
⚠️ Non implementare:
|
||||
|
||||
auto-accept
|
||||
|
||||
connessioni silenziose
|
||||
|
||||
bypass dei permessi Android
|
||||
|
||||
4. Fase attuale del progetto
|
||||
✅ Fase 1 — Control-plane ONLY
|
||||
|
||||
In questa fase:
|
||||
|
||||
❌ NIENTE WebRTC
|
||||
|
||||
❌ NIENTE MediaProjection
|
||||
|
||||
❌ NIENTE streaming
|
||||
|
||||
❌ NIENTE input remoto
|
||||
|
||||
✔️ SOLO:
|
||||
|
||||
WebSocket server
|
||||
|
||||
login client
|
||||
|
||||
register agent
|
||||
|
||||
device list
|
||||
|
||||
connect_request
|
||||
|
||||
accept / deny / expired
|
||||
|
||||
Se una PR introduce WebRTC o video → È SBAGLIATA
|
||||
|
||||
5. Protocollo di comunicazione
|
||||
|
||||
Tutti i messaggi:
|
||||
|
||||
{
|
||||
"v": 1,
|
||||
"type": "...",
|
||||
"requestId": "...",
|
||||
"...": "..."
|
||||
}
|
||||
|
||||
|
||||
I tipi sono definiti in:
|
||||
|
||||
/shared/protocol
|
||||
|
||||
|
||||
⚠️ Regole:
|
||||
|
||||
Non inventare campi fuori dai tipi condivisi
|
||||
|
||||
Non usare stringhe “magiche”
|
||||
|
||||
Se serve un nuovo messaggio → aggiornare shared/protocol
|
||||
|
||||
6. Regole di sviluppo per Codex
|
||||
Come lavorare
|
||||
|
||||
Ogni task = UNA PR
|
||||
|
||||
PR piccole, focalizzate
|
||||
|
||||
Niente “mega commit”
|
||||
|
||||
Ogni PR DEVE includere:
|
||||
|
||||
Codice funzionante
|
||||
|
||||
Logging minimo (no spam)
|
||||
|
||||
Error handling (no crash)
|
||||
|
||||
Checklist “come testare manualmente”
|
||||
|
||||
NON fare:
|
||||
|
||||
refactor non richiesti
|
||||
|
||||
miglioramenti “creativi”
|
||||
|
||||
cambiare stack
|
||||
|
||||
anticipare fasi future
|
||||
|
||||
7. Stato & persistenza (Fase 1)
|
||||
|
||||
Tutto in memoria
|
||||
|
||||
Restart server = stato perso (OK per MVP)
|
||||
|
||||
Nessun database obbligatorio ora
|
||||
|
||||
Strutture tipiche:
|
||||
|
||||
devicesById
|
||||
|
||||
sessionsById
|
||||
|
||||
clientSessionsByWs
|
||||
|
||||
8. Sicurezza (livello attuale)
|
||||
|
||||
Fase 1 = MVP controllato
|
||||
|
||||
Seed user + password in env
|
||||
|
||||
PairingKey in env
|
||||
|
||||
TLS solo in VPS (non locale)
|
||||
|
||||
⚠️ Non implementare:
|
||||
|
||||
hashing avanzato
|
||||
|
||||
JWT
|
||||
|
||||
refresh token
|
||||
|
||||
OAuth
|
||||
|
||||
Arriveranno in Fase 3.
|
||||
|
||||
9. Error handling (obbligatorio)
|
||||
|
||||
Su errore:
|
||||
|
||||
{
|
||||
"v": 1,
|
||||
"type": "error",
|
||||
"requestId": "...",
|
||||
"code": "UNAUTHORIZED | BAD_REQUEST | NOT_FOUND | CONFLICT | DEVICE_OFFLINE",
|
||||
"message": "human readable"
|
||||
}
|
||||
|
||||
|
||||
Mai crashare il server
|
||||
|
||||
Mai lasciare una promise non gestita
|
||||
|
||||
10. Naming e stile
|
||||
|
||||
camelCase per JSON
|
||||
|
||||
kebab-case per file
|
||||
|
||||
TypeScript strict
|
||||
|
||||
No any salvo motivazione
|
||||
|
||||
11. Test mindset
|
||||
|
||||
Ogni feature deve essere:
|
||||
|
||||
testabile via CLI o WS
|
||||
|
||||
osservabile via log
|
||||
|
||||
verificabile senza UI (se possibile)
|
||||
|
||||
Se non è testabile → probabilmente è sbagliata.
|
||||
|
||||
12. Frase guida del progetto
|
||||
|
||||
“Il VPS coordina, non guarda.
|
||||
Il tablet decide, non subisce.
|
||||
Il client controlla, non buca.”
|
||||
|
||||
13. In caso di dubbio
|
||||
|
||||
Se una decisione:
|
||||
|
||||
tocca sicurezza
|
||||
|
||||
cambia protocollo
|
||||
|
||||
anticipa WebRTC
|
||||
|
||||
rompe compatibilità
|
||||
|
||||
👉 FERMATI e chiedi prima.
|
||||
18
README.md
Normal file
18
README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Assistenza Monorepo
|
||||
|
||||
## Setup
|
||||
|
||||
- Install dependencies: `pnpm i`
|
||||
- Build all packages: `pnpm -r build`
|
||||
|
||||
## Packages
|
||||
|
||||
- `server`: Node.js + TypeScript signaling server (placeholder)
|
||||
- `windows`: Electron + TypeScript client (placeholder)
|
||||
- `shared/protocol`: Shared WebSocket protocol types and helpers
|
||||
- `android`: Placeholder folder (Kotlin later)
|
||||
|
||||
## Notes
|
||||
|
||||
- Protocol types live in `shared/protocol`
|
||||
- Phase 1 is control-plane only (no WebRTC)
|
||||
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "assistenza-monorepo",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.0",
|
||||
"workspaces": [
|
||||
"server",
|
||||
"windows",
|
||||
"shared/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -r build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.15.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5"
|
||||
}
|
||||
}
|
||||
774
pnpm-lock.yaml
generated
Normal file
774
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,774 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.6.1
|
||||
devDependencies:
|
||||
tsx:
|
||||
specifier: ^4.15.6
|
||||
version: 4.21.0
|
||||
|
||||
server:
|
||||
dependencies:
|
||||
'@assistenza/protocol':
|
||||
specifier: workspace:*
|
||||
version: link:../shared/protocol
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.6.1
|
||||
fastify:
|
||||
specifier: ^4.28.1
|
||||
version: 4.29.1
|
||||
pino:
|
||||
specifier: ^9.3.2
|
||||
version: 9.14.0
|
||||
ws:
|
||||
specifier: ^8.17.1
|
||||
version: 8.19.0
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.12.12
|
||||
version: 20.19.27
|
||||
tsx:
|
||||
specifier: ^4.15.6
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.4.5
|
||||
version: 5.9.3
|
||||
|
||||
shared/protocol:
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.4.5
|
||||
version: 5.9.3
|
||||
|
||||
windows:
|
||||
dependencies:
|
||||
'@assistenza/protocol':
|
||||
specifier: workspace:*
|
||||
version: link:../shared/protocol
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.4.5
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@fastify/ajv-compiler@3.6.0':
|
||||
resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==}
|
||||
|
||||
'@fastify/error@3.4.1':
|
||||
resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==}
|
||||
|
||||
'@fastify/fast-json-stringify-compiler@4.3.0':
|
||||
resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==}
|
||||
|
||||
'@fastify/merge-json-schemas@0.1.1':
|
||||
resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==}
|
||||
|
||||
'@fastify/websocket@8.3.1':
|
||||
resolution: {integrity: sha512-hsQYHHJme/kvP3ZS4v/WMUznPBVeeQHHwAoMy1LiN6m/HuPfbdXq1MBJ4Nt8qX1YI+eVbog4MnOsU7MTozkwYA==}
|
||||
|
||||
'@pinojs/redact@0.4.0':
|
||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||
|
||||
'@types/node@20.19.27':
|
||||
resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==}
|
||||
|
||||
abstract-logging@2.0.1:
|
||||
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
ajv: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
ajv:
|
||||
optional: true
|
||||
|
||||
ajv-formats@3.0.1:
|
||||
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
||||
peerDependencies:
|
||||
ajv: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
ajv:
|
||||
optional: true
|
||||
|
||||
ajv@8.17.1:
|
||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||
|
||||
atomic-sleep@1.0.0:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
avvio@8.4.0:
|
||||
resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==}
|
||||
|
||||
cookie@0.7.2:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
dotenv@16.6.1:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
esbuild@0.27.2:
|
||||
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
fast-content-type-parse@1.1.0:
|
||||
resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==}
|
||||
|
||||
fast-decode-uri-component@1.0.1:
|
||||
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-json-stringify@5.16.1:
|
||||
resolution: {integrity: sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==}
|
||||
|
||||
fast-querystring@1.1.2:
|
||||
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
|
||||
|
||||
fast-uri@2.4.0:
|
||||
resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==}
|
||||
|
||||
fast-uri@3.1.0:
|
||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||
|
||||
fastify-plugin@4.5.1:
|
||||
resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
|
||||
|
||||
fastify@4.29.1:
|
||||
resolution: {integrity: sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==}
|
||||
|
||||
fastq@1.20.1:
|
||||
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
||||
|
||||
find-my-way@8.2.2:
|
||||
resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
get-tsconfig@4.13.0:
|
||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
json-schema-ref-resolver@1.0.1:
|
||||
resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
light-my-request@5.14.0:
|
||||
resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==}
|
||||
|
||||
on-exit-leak-free@2.1.2:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
pino-abstract-transport@2.0.0:
|
||||
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
|
||||
|
||||
pino-std-serializers@7.0.0:
|
||||
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
|
||||
|
||||
pino@9.14.0:
|
||||
resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==}
|
||||
hasBin: true
|
||||
|
||||
process-warning@3.0.0:
|
||||
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
|
||||
|
||||
process-warning@5.0.0:
|
||||
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
real-require@0.2.0:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
ret@0.4.3:
|
||||
resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
reusify@1.1.0:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
safe-regex2@3.1.0:
|
||||
resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==}
|
||||
|
||||
safe-stable-stringify@2.5.0:
|
||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
secure-json-parse@2.7.0:
|
||||
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
set-cookie-parser@2.7.2:
|
||||
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
||||
|
||||
sonic-boom@4.2.0:
|
||||
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
|
||||
|
||||
split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
||||
thread-stream@3.1.0:
|
||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||
|
||||
toad-cache@3.7.0:
|
||||
resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
tsx@4.21.0:
|
||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
ws@8.19.0:
|
||||
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@fastify/ajv-compiler@3.6.0':
|
||||
dependencies:
|
||||
ajv: 8.17.1
|
||||
ajv-formats: 2.1.1(ajv@8.17.1)
|
||||
fast-uri: 2.4.0
|
||||
|
||||
'@fastify/error@3.4.1': {}
|
||||
|
||||
'@fastify/fast-json-stringify-compiler@4.3.0':
|
||||
dependencies:
|
||||
fast-json-stringify: 5.16.1
|
||||
|
||||
'@fastify/merge-json-schemas@0.1.1':
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
|
||||
'@fastify/websocket@8.3.1':
|
||||
dependencies:
|
||||
fastify-plugin: 4.5.1
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@pinojs/redact@0.4.0': {}
|
||||
|
||||
'@types/node@20.19.27':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
abstract-logging@2.0.1: {}
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
|
||||
ajv-formats@3.0.1(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
|
||||
ajv@8.17.1:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.0
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
avvio@8.4.0:
|
||||
dependencies:
|
||||
'@fastify/error': 3.4.1
|
||||
fastq: 1.20.1
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
dotenv@16.6.1: {}
|
||||
|
||||
esbuild@0.27.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.2
|
||||
'@esbuild/android-arm': 0.27.2
|
||||
'@esbuild/android-arm64': 0.27.2
|
||||
'@esbuild/android-x64': 0.27.2
|
||||
'@esbuild/darwin-arm64': 0.27.2
|
||||
'@esbuild/darwin-x64': 0.27.2
|
||||
'@esbuild/freebsd-arm64': 0.27.2
|
||||
'@esbuild/freebsd-x64': 0.27.2
|
||||
'@esbuild/linux-arm': 0.27.2
|
||||
'@esbuild/linux-arm64': 0.27.2
|
||||
'@esbuild/linux-ia32': 0.27.2
|
||||
'@esbuild/linux-loong64': 0.27.2
|
||||
'@esbuild/linux-mips64el': 0.27.2
|
||||
'@esbuild/linux-ppc64': 0.27.2
|
||||
'@esbuild/linux-riscv64': 0.27.2
|
||||
'@esbuild/linux-s390x': 0.27.2
|
||||
'@esbuild/linux-x64': 0.27.2
|
||||
'@esbuild/netbsd-arm64': 0.27.2
|
||||
'@esbuild/netbsd-x64': 0.27.2
|
||||
'@esbuild/openbsd-arm64': 0.27.2
|
||||
'@esbuild/openbsd-x64': 0.27.2
|
||||
'@esbuild/openharmony-arm64': 0.27.2
|
||||
'@esbuild/sunos-x64': 0.27.2
|
||||
'@esbuild/win32-arm64': 0.27.2
|
||||
'@esbuild/win32-ia32': 0.27.2
|
||||
'@esbuild/win32-x64': 0.27.2
|
||||
|
||||
fast-content-type-parse@1.1.0: {}
|
||||
|
||||
fast-decode-uri-component@1.0.1: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-json-stringify@5.16.1:
|
||||
dependencies:
|
||||
'@fastify/merge-json-schemas': 0.1.1
|
||||
ajv: 8.17.1
|
||||
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 2.4.0
|
||||
json-schema-ref-resolver: 1.0.1
|
||||
rfdc: 1.4.1
|
||||
|
||||
fast-querystring@1.1.2:
|
||||
dependencies:
|
||||
fast-decode-uri-component: 1.0.1
|
||||
|
||||
fast-uri@2.4.0: {}
|
||||
|
||||
fast-uri@3.1.0: {}
|
||||
|
||||
fastify-plugin@4.5.1: {}
|
||||
|
||||
fastify@4.29.1:
|
||||
dependencies:
|
||||
'@fastify/ajv-compiler': 3.6.0
|
||||
'@fastify/error': 3.4.1
|
||||
'@fastify/fast-json-stringify-compiler': 4.3.0
|
||||
abstract-logging: 2.0.1
|
||||
avvio: 8.4.0
|
||||
fast-content-type-parse: 1.1.0
|
||||
fast-json-stringify: 5.16.1
|
||||
find-my-way: 8.2.2
|
||||
light-my-request: 5.14.0
|
||||
pino: 9.14.0
|
||||
process-warning: 3.0.0
|
||||
proxy-addr: 2.0.7
|
||||
rfdc: 1.4.1
|
||||
secure-json-parse: 2.7.0
|
||||
semver: 7.7.3
|
||||
toad-cache: 3.7.0
|
||||
|
||||
fastq@1.20.1:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
|
||||
find-my-way@8.2.2:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-querystring: 1.1.2
|
||||
safe-regex2: 3.1.0
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
get-tsconfig@4.13.0:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
json-schema-ref-resolver@1.0.1:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
light-my-request@5.14.0:
|
||||
dependencies:
|
||||
cookie: 0.7.2
|
||||
process-warning: 3.0.0
|
||||
set-cookie-parser: 2.7.2
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
pino-abstract-transport@2.0.0:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
pino-std-serializers@7.0.0: {}
|
||||
|
||||
pino@9.14.0:
|
||||
dependencies:
|
||||
'@pinojs/redact': 0.4.0
|
||||
atomic-sleep: 1.0.0
|
||||
on-exit-leak-free: 2.1.2
|
||||
pino-abstract-transport: 2.0.0
|
||||
pino-std-serializers: 7.0.0
|
||||
process-warning: 5.0.0
|
||||
quick-format-unescaped: 4.0.4
|
||||
real-require: 0.2.0
|
||||
safe-stable-stringify: 2.5.0
|
||||
sonic-boom: 4.2.0
|
||||
thread-stream: 3.1.0
|
||||
|
||||
process-warning@3.0.0: {}
|
||||
|
||||
process-warning@5.0.0: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
ret@0.4.3: {}
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
safe-regex2@3.1.0:
|
||||
dependencies:
|
||||
ret: 0.4.3
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
secure-json-parse@2.7.0: {}
|
||||
|
||||
semver@7.7.3: {}
|
||||
|
||||
set-cookie-parser@2.7.2: {}
|
||||
|
||||
sonic-boom@4.2.0:
|
||||
dependencies:
|
||||
atomic-sleep: 1.0.0
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
thread-stream@3.1.0:
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
|
||||
toad-cache@3.7.0: {}
|
||||
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
get-tsconfig: 4.13.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
ws@8.19.0: {}
|
||||
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
- "server"
|
||||
- "windows"
|
||||
- "shared/*"
|
||||
107
roadmap.txt
Normal file
107
roadmap.txt
Normal file
@@ -0,0 +1,107 @@
|
||||
Fase 1 — Signaling minimal (MVP control-plane)
|
||||
|
||||
Obiettivo: vedere agent online e fare “connect_request/accept”.
|
||||
|
||||
Server
|
||||
|
||||
WebSocket endpoints: /agent e /client
|
||||
|
||||
Registry device in memoria + persist (DB)
|
||||
|
||||
Auth semplice:
|
||||
|
||||
client login (username/pwd hash)
|
||||
|
||||
agent pairing con pairCode (inizialmente manuale)
|
||||
|
||||
Android
|
||||
|
||||
Connessione WS + heartbeat
|
||||
|
||||
DeviceId stabile
|
||||
|
||||
Ricezione connect_request → mostra notifica → accept/deny
|
||||
|
||||
Windows
|
||||
|
||||
Login + lista device + “richiedi connessione”
|
||||
|
||||
Deliverable: flusso completo “richiesta/accetta” senza WebRTC ancora.
|
||||
|
||||
Fase 2 — WebRTC video solo (prima connessione reale)
|
||||
|
||||
Obiettivo: stream schermo tablet → viewer su Windows.
|
||||
|
||||
Android
|
||||
|
||||
MediaProjection → encoder → WebRTC VideoTrack
|
||||
|
||||
ICE config: STUN+TURN
|
||||
|
||||
Start/Stop session
|
||||
|
||||
Windows
|
||||
|
||||
WebRTC receiver + render (Electron: <video> e basta)
|
||||
|
||||
Gestione resize + aspect
|
||||
|
||||
Deliverable: vedere lo schermo in tempo reale.
|
||||
|
||||
Fase 3 — Input base (tap, swipe, back/home)
|
||||
|
||||
Obiettivo: controllo reale.
|
||||
|
||||
Windows
|
||||
|
||||
Cattura mouse/keyboard
|
||||
|
||||
Coordinate mapping (considera rotazione e scaling)
|
||||
|
||||
Invio su DataChannel
|
||||
|
||||
Android
|
||||
|
||||
Parser input
|
||||
|
||||
dispatchGesture() per tap/swipe
|
||||
|
||||
performGlobalAction() per back/home/recents
|
||||
|
||||
Rate limit / validation (anti spam)
|
||||
|
||||
Deliverable: posso aprire app, cliccare bottoni, scorrere.
|
||||
|
||||
Fase 4 — Robustezza rete + osservabilità
|
||||
|
||||
Fallback TURN automatico (già in ICE, ma monitora)
|
||||
|
||||
Retry WS, reconnect, session resume
|
||||
|
||||
Metriche:
|
||||
|
||||
% TURN vs P2P
|
||||
|
||||
bitrate, fps, packet loss
|
||||
|
||||
tempi handshake
|
||||
|
||||
Log sessioni (audit)
|
||||
|
||||
Deliverable: sistema che regge ambienti reali.
|
||||
|
||||
Fase 5 — Sicurezza “production grade”
|
||||
|
||||
Pairing serio:
|
||||
|
||||
QR o codice one-time
|
||||
|
||||
device associato a account
|
||||
|
||||
Token short-lived (access) + refresh
|
||||
|
||||
Ruoli (admin/operatore)
|
||||
|
||||
“Allowlist controller” per device (opzionale)
|
||||
|
||||
Deliverable: pronto per distribuzione controllata.
|
||||
23
server/Dockerfile
Normal file
23
server/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
|
||||
COPY shared/protocol/package.json shared/protocol/package.json
|
||||
COPY server/package.json server/package.json
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY shared/protocol shared/protocol
|
||||
COPY server server
|
||||
|
||||
RUN pnpm -r build
|
||||
|
||||
WORKDIR /app/server
|
||||
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
25
server/package.json
Normal file
25
server/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@assistenza/server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@assistenza/protocol": "workspace:*",
|
||||
"dotenv": "^16.4.5",
|
||||
"fastify": "^4.28.1",
|
||||
"pino": "^9.3.2",
|
||||
"ws": "^8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
"tsx": "^4.15.6",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
135
server/src/index.ts
Normal file
135
server/src/index.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import "dotenv/config";
|
||||
import Fastify from "fastify";
|
||||
import pino from "pino";
|
||||
import { WebSocketServer } from "ws";
|
||||
import type { RawData, WebSocket } from "ws";
|
||||
import type { IncomingMessage } from "http";
|
||||
import { ProtocolVersion, isMessage, nowTs } from "@assistenza/protocol";
|
||||
|
||||
export function serverBanner(): string {
|
||||
return `server v${ProtocolVersion} ${nowTs()}`;
|
||||
}
|
||||
|
||||
const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });
|
||||
const app = Fastify({ logger });
|
||||
|
||||
app.get("/health", async (_request, reply) => {
|
||||
reply.code(200).type("text/plain").send("ok");
|
||||
});
|
||||
|
||||
type WsKind = "agent" | "client";
|
||||
|
||||
function getRequestId(value: unknown): string | undefined {
|
||||
if (typeof value !== "object" || value === null) return undefined;
|
||||
const record = value as Record<string, unknown>;
|
||||
return typeof record.requestId === "string" ? record.requestId : undefined;
|
||||
}
|
||||
|
||||
function getPath(request: IncomingMessage): string {
|
||||
if (!request.url) return "unknown";
|
||||
try {
|
||||
return new URL(request.url, "http://localhost").pathname;
|
||||
} catch {
|
||||
return request.url;
|
||||
}
|
||||
}
|
||||
|
||||
function getIp(request: IncomingMessage): string {
|
||||
return request.socket.remoteAddress ?? "unknown";
|
||||
}
|
||||
|
||||
function sendBadRequest(ws: WebSocket, requestId: string | undefined, message: string): void {
|
||||
const payload: {
|
||||
v: typeof ProtocolVersion;
|
||||
type: "error";
|
||||
code: "BAD_REQUEST";
|
||||
message: string;
|
||||
requestId?: string;
|
||||
} = {
|
||||
v: ProtocolVersion,
|
||||
type: "error",
|
||||
code: "BAD_REQUEST",
|
||||
message,
|
||||
...(requestId ? { requestId } : {}),
|
||||
};
|
||||
ws.send(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function rawToString(data: RawData): string {
|
||||
if (typeof data === "string") return data;
|
||||
if (data instanceof Buffer) return data.toString("utf8");
|
||||
if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
|
||||
return Buffer.from(data).toString("utf8");
|
||||
}
|
||||
|
||||
function handleConnection(kind: WsKind, ws: WebSocket, request: IncomingMessage): void {
|
||||
const path = getPath(request);
|
||||
const ip = getIp(request);
|
||||
app.log.info({ ip, path, kind }, "ws connect");
|
||||
|
||||
ws.on("message", (data) => {
|
||||
const raw = rawToString(data);
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
sendBadRequest(ws, undefined, "Invalid JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = getRequestId(parsed);
|
||||
if (!isMessage(parsed)) {
|
||||
sendBadRequest(ws, requestId, "Invalid message shape");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", (code, reason) => {
|
||||
app.log.info(
|
||||
{ ip, path, kind, code, reason: reason.toString() },
|
||||
"ws disconnect"
|
||||
);
|
||||
});
|
||||
|
||||
ws.on("error", (err) => {
|
||||
app.log.warn({ ip, path, kind, err }, "ws error");
|
||||
});
|
||||
}
|
||||
|
||||
const agentWs = new WebSocketServer({ noServer: true });
|
||||
const clientWs = new WebSocketServer({ noServer: true });
|
||||
|
||||
agentWs.on("connection", (ws, request) => handleConnection("agent", ws, request));
|
||||
clientWs.on("connection", (ws, request) => handleConnection("client", ws, request));
|
||||
|
||||
app.server.on("upgrade", (request, socket, head) => {
|
||||
const path = getPath(request);
|
||||
if (path === "/agent") {
|
||||
agentWs.handleUpgrade(request, socket, head, (ws) => {
|
||||
agentWs.emit("connection", ws, request);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === "/client") {
|
||||
clientWs.handleUpgrade(request, socket, head, (ws) => {
|
||||
clientWs.emit("connection", ws, request);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
async function start() {
|
||||
const port = Number(process.env.PORT ?? "3000");
|
||||
const host = process.env.HOST ?? "0.0.0.0";
|
||||
|
||||
await app.listen({ port, host });
|
||||
app.log.info({ port, host }, serverBanner());
|
||||
}
|
||||
|
||||
start().catch((err) => {
|
||||
app.log.error(err, "failed to start server");
|
||||
process.exit(1);
|
||||
});
|
||||
8
server/tsconfig.json
Normal file
8
server/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
14
shared/protocol/package.json
Normal file
14
shared/protocol/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@assistenza/protocol",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
210
shared/protocol/src/index.ts
Normal file
210
shared/protocol/src/index.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
Shared protocol v1 types and helpers.
|
||||
Keep validation minimal and dependency-free.
|
||||
*/
|
||||
|
||||
export const ProtocolVersion = 1 as const;
|
||||
export type ProtocolVersion = typeof ProtocolVersion;
|
||||
|
||||
export type RequestId = string;
|
||||
|
||||
export type MessageBase = {
|
||||
v: ProtocolVersion;
|
||||
type: string;
|
||||
requestId: RequestId;
|
||||
};
|
||||
|
||||
// Agent -> Server
|
||||
export type AgentRegister = MessageBase & {
|
||||
type: "agent_register";
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
pairingKey: string;
|
||||
};
|
||||
|
||||
export type AgentRegistered = MessageBase & {
|
||||
type: "agent_registered";
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export type AgentHeartbeat = MessageBase & {
|
||||
type: "agent_heartbeat";
|
||||
deviceId: string;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
// Server -> Agent
|
||||
export type ServerConnectRequest = MessageBase & {
|
||||
type: "server_connect_request";
|
||||
deviceId: string;
|
||||
sessionId: string;
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
export type AgentConnectResponse = MessageBase & {
|
||||
type: "agent_connect_response";
|
||||
deviceId: string;
|
||||
sessionId: string;
|
||||
accepted: boolean;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
// Client -> Server
|
||||
export type ClientLogin = MessageBase & {
|
||||
type: "client_login";
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type ClientLoginResult = MessageBase & {
|
||||
type: "client_login_result";
|
||||
ok: boolean;
|
||||
clientId?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type ClientListDevices = MessageBase & {
|
||||
type: "client_list_devices";
|
||||
};
|
||||
|
||||
export type ClientDeviceList = MessageBase & {
|
||||
type: "client_device_list";
|
||||
devices: Array<{
|
||||
deviceId: string;
|
||||
deviceName: string;
|
||||
online: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ClientConnectRequest = MessageBase & {
|
||||
type: "client_connect_request";
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export type ClientConnectStatus = MessageBase & {
|
||||
type: "client_connect_status";
|
||||
deviceId: string;
|
||||
sessionId: string;
|
||||
status: "pending" | "accepted" | "denied" | "expired";
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type ClientConnectResult = MessageBase & {
|
||||
type: "client_connect_result";
|
||||
deviceId: string;
|
||||
sessionId: string;
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
// Error
|
||||
export type ErrorMessage = MessageBase & {
|
||||
type: "error";
|
||||
code: "UNAUTHORIZED" | "BAD_REQUEST" | "NOT_FOUND" | "CONFLICT" | "DEVICE_OFFLINE";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type AnyMessage =
|
||||
| AgentRegister
|
||||
| AgentRegistered
|
||||
| AgentHeartbeat
|
||||
| ServerConnectRequest
|
||||
| AgentConnectResponse
|
||||
| ClientLogin
|
||||
| ClientLoginResult
|
||||
| ClientListDevices
|
||||
| ClientDeviceList
|
||||
| ClientConnectRequest
|
||||
| ClientConnectStatus
|
||||
| ClientConnectResult
|
||||
| ErrorMessage;
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function hasString(obj: Record<string, unknown>, key: string): boolean {
|
||||
return typeof obj[key] === "string" && obj[key] !== "";
|
||||
}
|
||||
|
||||
function hasNumber(obj: Record<string, unknown>, key: string): boolean {
|
||||
return typeof obj[key] === "number" && Number.isFinite(obj[key]);
|
||||
}
|
||||
|
||||
export function isMessage(obj: unknown): obj is AnyMessage {
|
||||
if (!isObject(obj)) return false;
|
||||
if (obj.v !== ProtocolVersion) return false;
|
||||
if (!hasString(obj, "type")) return false;
|
||||
if (!hasString(obj, "requestId")) return false;
|
||||
|
||||
switch (obj.type) {
|
||||
case "agent_register":
|
||||
return hasString(obj, "deviceId") && hasString(obj, "deviceName") && hasString(obj, "pairingKey");
|
||||
case "agent_registered":
|
||||
return hasString(obj, "deviceId");
|
||||
case "agent_heartbeat":
|
||||
return hasString(obj, "deviceId") && hasNumber(obj, "ts");
|
||||
case "server_connect_request":
|
||||
return hasString(obj, "deviceId") && hasString(obj, "sessionId") && hasString(obj, "clientId");
|
||||
case "agent_connect_response":
|
||||
return (
|
||||
hasString(obj, "deviceId") &&
|
||||
hasString(obj, "sessionId") &&
|
||||
typeof obj.accepted === "boolean"
|
||||
);
|
||||
case "client_login":
|
||||
return hasString(obj, "username") && hasString(obj, "password");
|
||||
case "client_login_result":
|
||||
return typeof obj.ok === "boolean";
|
||||
case "client_list_devices":
|
||||
return true;
|
||||
case "client_device_list":
|
||||
return Array.isArray(obj.devices);
|
||||
case "client_connect_request":
|
||||
return hasString(obj, "deviceId");
|
||||
case "client_connect_status":
|
||||
return (
|
||||
hasString(obj, "deviceId") &&
|
||||
hasString(obj, "sessionId") &&
|
||||
hasString(obj, "status")
|
||||
);
|
||||
case "client_connect_result":
|
||||
return (
|
||||
hasString(obj, "deviceId") &&
|
||||
hasString(obj, "sessionId") &&
|
||||
typeof obj.ok === "boolean"
|
||||
);
|
||||
case "error":
|
||||
return hasString(obj, "code") && hasString(obj, "message");
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function nowTs(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
// Minimal UUID v4 (RFC4122) generator without dependencies.
|
||||
export function makeRequestId(): string {
|
||||
const bytes = new Uint8Array(16);
|
||||
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
||||
crypto.getRandomValues(bytes);
|
||||
} else {
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
bytes[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
}
|
||||
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
|
||||
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
||||
return (
|
||||
hex.slice(0, 4).join("") + "-" +
|
||||
hex.slice(4, 6).join("") + "-" +
|
||||
hex.slice(6, 8).join("") + "-" +
|
||||
hex.slice(8, 10).join("") + "-" +
|
||||
hex.slice(10, 16).join("")
|
||||
);
|
||||
}
|
||||
8
shared/protocol/tsconfig.json
Normal file
8
shared/protocol/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
321
tasklist_fase1.txt
Normal file
321
tasklist_fase1.txt
Normal file
@@ -0,0 +1,321 @@
|
||||
Prompt 00 — Monorepo + shared protocol (TypeScript ovunque)
|
||||
Crea una PR: “Monorepo bootstrap + shared protocol v1”.
|
||||
|
||||
Obiettivo:
|
||||
- Impostare un monorepo con TypeScript ovunque e un pacchetto condiviso `shared/protocol` con tipi e helper per il protocollo WebSocket v1.
|
||||
|
||||
Struttura richiesta:
|
||||
- /server (Node.js + TS)
|
||||
- /windows (Electron + TS)
|
||||
- /shared/protocol (TS, esporta tipi e validator base)
|
||||
- /android NON in TS (verrà dopo), ma lascia spazio /android
|
||||
|
||||
Requisiti tecnici:
|
||||
- Usa pnpm workspaces (preferibile) oppure yarn workspaces.
|
||||
- In `shared/protocol` definisci:
|
||||
- `ProtocolVersion = 1`
|
||||
- tipi per tutti i messaggi Fase 1:
|
||||
- agent_register, agent_registered, agent_heartbeat
|
||||
- server_connect_request
|
||||
- agent_connect_response
|
||||
- client_login, client_login_result
|
||||
- client_list_devices, client_device_list
|
||||
- client_connect_request
|
||||
- client_connect_status, client_connect_result
|
||||
- error
|
||||
- un `type AnyMessage = ...` union
|
||||
- helper `isMessage(obj): obj is AnyMessage` con validazione minimale (no librerie pesanti; ok zod se vuoi ma tienilo semplice)
|
||||
- helper `makeRequestId()` (uuid v4) e `nowTs()`
|
||||
|
||||
Output:
|
||||
- Workspace funzionante con `pnpm i` e build TS.
|
||||
- README root con comandi base.
|
||||
|
||||
Acceptance:
|
||||
- `pnpm -r build` passa.
|
||||
- Import da `shared/protocol` funziona da server e windows (anche se windows non è ancora implementato).
|
||||
- Niente codice Android in questa PR.
|
||||
|
||||
Prompt 01 — Server skeleton + healthcheck + Docker
|
||||
Crea una PR: “Server signaling TS skeleton + /health + Docker”.
|
||||
|
||||
Contesto:
|
||||
- Monorepo esiste con `shared/protocol`.
|
||||
|
||||
Obiettivo:
|
||||
- Creare `server` Node.js TypeScript che espone:
|
||||
- HTTP GET `/health` => 200 “ok”
|
||||
- WebSocket upgrade pronto (endpoints verranno dopo)
|
||||
|
||||
Requisiti:
|
||||
- Framework HTTP: Fastify o Express (scegli uno e mantienilo semplice).
|
||||
- Logger: pino (consigliato) o console strutturata.
|
||||
- Config `.env` (usa dotenv).
|
||||
- Dockerfile per server.
|
||||
- Script: `pnpm --filter server dev` e `build` e `start`.
|
||||
|
||||
Acceptance:
|
||||
- Avvio locale: `pnpm --filter server dev`
|
||||
- `curl localhost:<port>/health` => 200
|
||||
- `docker build` e `docker run` funzionano.
|
||||
- Nessun endpoint WS richiesto ancora (solo scaffolding).
|
||||
|
||||
Prompt 02 — WebSocket endpoints /agent e /client + parsing robusto
|
||||
Crea una PR: “WS endpoints /agent e /client + parsing robusto”.
|
||||
|
||||
Obiettivo:
|
||||
- Implementare WebSocket su server con due path:
|
||||
- ws://.../agent
|
||||
- ws://.../client
|
||||
|
||||
Requisiti:
|
||||
- Usa libreria `ws` (preferita) integrata con il server HTTP scelto.
|
||||
- Per ogni connessione, identifica “kind” (agent/client) in base al path.
|
||||
- Parsing JSON robusto: se arriva JSON invalido o messaggio non conforme, rispondi con:
|
||||
{ v:1, type:"error", requestId: <se presente>, code:"BAD_REQUEST", message:"..." }
|
||||
- Non deve crashare.
|
||||
|
||||
Acceptance:
|
||||
- Connessione WS a /agent e /client possibile.
|
||||
- Invio messaggio non JSON => ricevo error e connessione resta (o chiude con motivo, ok entrambe purché non crashi).
|
||||
- Logga connect/disconnect con ip e path.
|
||||
|
||||
Prompt 03 — Auth Fase 1 (client login + agent pairingKey)
|
||||
Crea una PR: “Auth F1: client_login + agent_register pairingKey”.
|
||||
|
||||
Obiettivo:
|
||||
- Implementare autenticazione minimale:
|
||||
- Client: `client_login` con username/password contro seed in env
|
||||
- Agent: `agent_register` con pairingKey contro seed in env, associato allo stesso userId del seed
|
||||
|
||||
Env richieste (server):
|
||||
- SEED_USERNAME
|
||||
- SEED_PASSWORD (plaintext per ora, MVP)
|
||||
- SEED_USER_ID (es. "user-1")
|
||||
- SEED_PAIRING_KEY (string)
|
||||
|
||||
Comportamento:
|
||||
- client_login ok => memorizza sessione in-memory (map ws -> userId).
|
||||
- richieste client senza login => error UNAUTHORIZED.
|
||||
- agent_register con pairingKey errata => error UNAUTHORIZED.
|
||||
|
||||
Usa i tipi di `shared/protocol`.
|
||||
|
||||
Acceptance:
|
||||
- client senza login che fa list => UNAUTHORIZED
|
||||
- login corretto => ok:true
|
||||
- agent_register con pairingKey corretto => agent_registered ok:true
|
||||
- agent_register con pairingKey errata => UNAUTHORIZED
|
||||
|
||||
Prompt 04 — Presence registry + heartbeat + lastSeen
|
||||
Crea una PR: “Presence registry: devices online/offline + heartbeat”.
|
||||
|
||||
Obiettivo:
|
||||
- Mantenere registry in memoria:
|
||||
devicesById: deviceId -> { deviceId, deviceName, userId, online, lastSeenIso, wsRef? }
|
||||
|
||||
Regole:
|
||||
- Su agent_register: crea/aggiorna entry, set online=true, lastSeen=now, salva wsRef.
|
||||
- Su agent_heartbeat: aggiorna lastSeen=now (se deviceId noto), altrimenti error BAD_REQUEST.
|
||||
- Su WS disconnect agent: online=false, lastSeen=now, wsRef=null.
|
||||
|
||||
Extra:
|
||||
- Aggiungi un cleanup interval che marca offline se non riceve heartbeat entro 90s (anche se la socket resta “mezzo morta”).
|
||||
|
||||
Acceptance:
|
||||
- Simulando heartbeat si aggiorna lastSeen.
|
||||
- Chiudendo connessione agent => online=false.
|
||||
- Se heartbeat manca >90s => online=false.
|
||||
|
||||
Prompt 05 — client_list_devices + filtro per userId
|
||||
Crea una PR: “client_list_devices + device list filtrata”.
|
||||
|
||||
Obiettivo:
|
||||
- Implementare:
|
||||
- client_list_devices => server risponde client_device_list
|
||||
|
||||
Dettagli:
|
||||
- Deve essere richiesto solo da client autenticato.
|
||||
- Ritorna SOLO device associati al userId del client.
|
||||
- Ogni device include: deviceId, deviceName, online, lastSeenIso.
|
||||
|
||||
Acceptance:
|
||||
- Un client loggato vede la lista.
|
||||
- Un client non loggato riceve UNAUTHORIZED.
|
||||
- Se ci sono più device (simulati) appartenenti a userId diversi, ne vede solo i suoi.
|
||||
|
||||
Prompt 06 — Connect request routing + session state + timeout expired
|
||||
Crea una PR: “Connect request routing + session state + timeout”.
|
||||
|
||||
Obiettivo:
|
||||
- Implementare client_connect_request:
|
||||
- verifica auth client
|
||||
- verifica ownership (device.userId == client.userId)
|
||||
- se device offline => error DEVICE_OFFLINE
|
||||
- se device online => crea sessionId e status=pending
|
||||
- inoltra a agent `server_connect_request` con {sessionId, fromUser: username}
|
||||
- al client risponde `client_connect_status` status=pending
|
||||
|
||||
Session store in memory:
|
||||
- sessionsById: sessionId -> { sessionId, userId, deviceId, status, createdAt, clientWsRef }
|
||||
|
||||
Timeout:
|
||||
- dopo 60s se ancora pending => status=expired e invia al client `client_connect_result` status=expired
|
||||
|
||||
Acceptance:
|
||||
- Richiesta verso device offline => DEVICE_OFFLINE
|
||||
- Richiesta verso device online => agent riceve server_connect_request e client riceve pending
|
||||
- Se agent non risponde entro 60s => client riceve expired
|
||||
|
||||
Prompt 07 — Agent accept/deny -> client result
|
||||
Crea una PR: “agent_connect_response accept/deny -> client_connect_result”.
|
||||
|
||||
Obiettivo:
|
||||
- Implementare agent_connect_response:
|
||||
- valida sessionId esistente
|
||||
- valida che session.deviceId == msg.deviceId
|
||||
- valida che session.status == pending
|
||||
- se decision accept => status=accepted
|
||||
- se decision deny => status=denied
|
||||
- invia a client `client_connect_result` con status accepted/denied
|
||||
|
||||
Edge cases:
|
||||
- sessionId inesistente => error NOT_FOUND
|
||||
- session non pending => error CONFLICT
|
||||
- deviceId mismatch => error FORBIDDEN
|
||||
|
||||
Acceptance:
|
||||
- Agent accept => client riceve accepted
|
||||
- Agent deny => client riceve denied
|
||||
- Risposte duplicate => CONFLICT
|
||||
|
||||
Prompt 08 — docker-compose dev (server + nginx reverse proxy WS)
|
||||
Crea una PR: “Dev compose: nginx reverse proxy -> server (WS upgrade ok)”.
|
||||
|
||||
Obiettivo:
|
||||
- Aggiungere `infra/docker-compose.yml` con:
|
||||
- server
|
||||
- nginx reverse proxy davanti
|
||||
|
||||
Requisiti:
|
||||
- Nginx deve supportare WebSocket upgrade per /agent e /client.
|
||||
- In dev, niente TLS (http + ws).
|
||||
- Documenta in README come testare:
|
||||
- ws://localhost/agent
|
||||
- ws://localhost/client
|
||||
(oppure con porte diverse, ma deve essere chiaro)
|
||||
|
||||
Acceptance:
|
||||
- Connessione WS tramite nginx funziona (upgrade ok).
|
||||
- /health raggiungibile via nginx.
|
||||
|
||||
Prompt Windows (Electron) — Fase 1
|
||||
Prompt W1 — Electron skeleton + pagine login/devices
|
||||
Crea una PR: “Electron TS skeleton: login page + devices page”.
|
||||
|
||||
Obiettivo:
|
||||
- Creare app Electron in /windows con TypeScript.
|
||||
- UI minimale con due schermate:
|
||||
- Login (username/password, serverUrl)
|
||||
- Devices (placeholder lista)
|
||||
|
||||
Requisiti:
|
||||
- Puoi usare React+Vite oppure Electron Forge. Scegli stack semplice e moderno.
|
||||
- Config serverUrl persistente (localStorage ok).
|
||||
|
||||
Acceptance:
|
||||
- `pnpm --filter windows dev` avvia l’app.
|
||||
- Navigazione login -> devices (anche finta).
|
||||
|
||||
Prompt W2 — WS client + client_login
|
||||
Crea una PR: “Windows: WS client + client_login”.
|
||||
|
||||
Obiettivo:
|
||||
- Connettere Electron a ws://<serverUrl>/client
|
||||
- Implementare invio client_login e gestione client_login_result usando `shared/protocol`.
|
||||
|
||||
Requisiti:
|
||||
- Gestisci reconnect base (retry ogni 2s fino a successo) o almeno errore UI.
|
||||
- Dopo login ok => vai a schermata Devices.
|
||||
|
||||
Acceptance:
|
||||
- Login corretto => ok e vai a Devices.
|
||||
- Login errato => mostra errore.
|
||||
|
||||
Prompt W3 — Lista device (client_list_devices + refresh)
|
||||
Crea una PR: “Windows: device list + refresh/poll”.
|
||||
|
||||
Obiettivo:
|
||||
- Implementare:
|
||||
- client_list_devices all’apertura della schermata
|
||||
- poll ogni 5s (o pulsante refresh) per aggiornare
|
||||
- Render tabella: deviceName, deviceId, online, lastSeenIso.
|
||||
|
||||
Acceptance:
|
||||
- Vedo device comparire e cambiare stato online/offline.
|
||||
|
||||
Prompt W4 — Connect request UI + stato pending/accepted/denied/expired
|
||||
Crea una PR: “Windows: connect request + status UI”.
|
||||
|
||||
Obiettivo:
|
||||
- Aggiungere un bottone “Connect” per ogni device.
|
||||
- Click => invia client_connect_request.
|
||||
- Mostra stato:
|
||||
- pending (spinner o testo)
|
||||
- accepted / denied / expired
|
||||
|
||||
Requisiti:
|
||||
- Gestisci più richieste (almeno una alla volta; se già pending, disabilita altri connect).
|
||||
|
||||
Acceptance:
|
||||
- Pending appare subito.
|
||||
- Alla risposta dell’agent si aggiorna la UI.
|
||||
|
||||
Prompt Android (Kotlin) — Fase 1
|
||||
Prompt A1 — Foreground service + WS + deviceId persistente
|
||||
Crea una PR Android: “Agent: ForegroundService + WS client + deviceId persistente”.
|
||||
|
||||
Obiettivo:
|
||||
- App Android Kotlin con:
|
||||
- ForegroundService “Remote Agent”
|
||||
- WebSocket OkHttp
|
||||
- deviceId UUID persistente in SharedPreferences
|
||||
- schermata settings: serverUrl, pairingKey, deviceName (persistenti)
|
||||
|
||||
Comportamento:
|
||||
- Avvio app -> start service -> connect ws://serverUrl/agent
|
||||
- Log utile.
|
||||
|
||||
Acceptance:
|
||||
- Service parte e resta in foreground con notifica.
|
||||
- deviceId resta uguale tra riavvii app.
|
||||
|
||||
Prompt A2 — agent_register + heartbeat + reconnect
|
||||
Crea una PR Android: “Agent: agent_register + heartbeat + reconnect”.
|
||||
|
||||
Obiettivo:
|
||||
- Dopo WS open => invia agent_register {deviceId, deviceName, pairingKey}
|
||||
- Heartbeat ogni 30s: agent_heartbeat
|
||||
- Reconnect automatico se WS cade (retry con backoff semplice).
|
||||
|
||||
Acceptance:
|
||||
- Server mostra device online.
|
||||
- Stacco/riattacco rete => torna online senza riaprire app.
|
||||
|
||||
Prompt A3 — Notifica connect_request con azioni + agent_connect_response
|
||||
Crea una PR Android: “Agent: notifica connect request + accept/deny”.
|
||||
|
||||
Obiettivo:
|
||||
- Ricevere server_connect_request dal WS
|
||||
- Mostrare notifica con azioni:
|
||||
- ACCETTA
|
||||
- RIFIUTA
|
||||
- Click azione => invia agent_connect_response con {sessionId, deviceId, decision}
|
||||
|
||||
Requisiti:
|
||||
- Deve funzionare anche con app in background.
|
||||
- Usa PendingIntent + BroadcastReceiver per gestire azioni.
|
||||
|
||||
Acceptance:
|
||||
- Notifica appare quando il client richiede connessione.
|
||||
- Accept/deny inviano risposta e il client riceve l’esito.
|
||||
14
tsconfig.base.json
Normal file
14
tsconfig.base.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
17
windows/package.json
Normal file
17
windows/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@assistenza/windows",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@assistenza/protocol": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
5
windows/src/index.ts
Normal file
5
windows/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ProtocolVersion } from "@assistenza/protocol";
|
||||
|
||||
export function windowsBanner(): string {
|
||||
return `windows v${ProtocolVersion}`;
|
||||
}
|
||||
8
windows/tsconfig.json
Normal file
8
windows/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user