viveworker 0.7.0-beta.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -4
- package/package.json +7 -3
- package/scripts/lib/remote-pairing/README.md +164 -0
- package/scripts/lib/remote-pairing/_browser-bundle-entry.mjs +86 -0
- package/scripts/lib/remote-pairing/audit.mjs +122 -0
- package/scripts/lib/remote-pairing/bridge-relay-client.mjs +737 -0
- package/scripts/lib/remote-pairing/control.mjs +156 -0
- package/scripts/lib/remote-pairing/envelope.mjs +224 -0
- package/scripts/lib/remote-pairing/http-dispatch.mjs +530 -0
- package/scripts/lib/remote-pairing/keys-core.mjs +110 -0
- package/scripts/lib/remote-pairing/keys.mjs +181 -0
- package/scripts/lib/remote-pairing/noise.mjs +436 -0
- package/scripts/lib/remote-pairing/orchestrator.mjs +356 -0
- package/scripts/lib/remote-pairing/pairings.mjs +446 -0
- package/scripts/lib/remote-pairing/rpc.mjs +381 -0
- package/scripts/moltbook-scout-auto.sh +16 -8
- package/scripts/share-cli.mjs +14 -130
- package/scripts/viveworker-bridge.mjs +1696 -223
- package/scripts/viveworker.mjs +27 -6
- package/web/app.css +727 -9
- package/web/app.js +1810 -232
- package/web/i18n.js +207 -17
- package/web/index.html +115 -1
- package/web/remote-pairing/api-router.js +873 -0
- package/web/remote-pairing/keys.js +237 -0
- package/web/remote-pairing/pairing-state.js +313 -0
- package/web/remote-pairing/rpc-client.js +765 -0
- package/web/remote-pairing/transport.js +804 -0
- package/web/remote-pairing/wake.js +149 -0
- package/web/remote-pairing-test.html +400 -0
- package/web/remote-pairing.bundle.js +3 -0
- package/web/remote-pairing.bundle.js.LEGAL.txt +15 -0
- package/web/remote-pairing.bundle.js.map +7 -0
- package/web/sw.js +190 -20
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://badge.fury.io/js/viveworker)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
`viveworker` is an open mobile control surface for Codex Desktop, Claude Desktop, Claude Code, A2A tasks, File Share, and Moltbook.
|
|
8
|
+
`viveworker` is an open mobile control surface for Codex Desktop, Claude Desktop, Claude Code, Remote connection, A2A tasks, File Share, and Moltbook.
|
|
9
9
|
|
|
10
10
|
When your AI desktop session needs an approval, asks whether to implement a plan, wants you to choose from options, finishes a task, needs to hand off a file, or receives a task from another agent while you are away from your desk, `viveworker` keeps all of that within reach on your phone. Instead of breaking your rhythm, it helps you keep vivecoding going from anywhere in your home or office.
|
|
11
11
|
|
|
@@ -31,6 +31,7 @@ keep your AI session moving, keep context close, and keep your momentum.
|
|
|
31
31
|
|
|
32
32
|
- **AI coding sessions**: approvals, plan checks, questions, completions, and mobile code review for Codex and Claude
|
|
33
33
|
- **Thread Sharing**: pass context, plan-review requests, or full handoffs between Codex and Claude sessions
|
|
34
|
+
- **Remote connection**: reach your Mac from a paired device outside your LAN through an end-to-end encrypted relay
|
|
34
35
|
- **File Share**: host static files on a private URL, with optional password protection and expiry
|
|
35
36
|
- **Moltbook ops**: draft posts, scout replies, and handle incoming responses from the same phone UI
|
|
36
37
|
- **A2A relay**: receive tasks from other agents, approve them on your phone, and execute locally on your Mac
|
|
@@ -50,8 +51,9 @@ one phone, one local control surface, multiple agent workflows.
|
|
|
50
51
|
It gets even more fun with a Mac mini.
|
|
51
52
|
Leave Codex or Claude running on a small always-on machine, and `viveworker` starts to feel like a local coding appliance: your Mac mini keeps building in the background while your device handles approvals, plan checks, questions, and follow-up replies from anywhere in your home or office.
|
|
52
53
|
|
|
53
|
-
`viveworker`
|
|
54
|
-
|
|
54
|
+
`viveworker` starts from a local-first model: the bridge runs on your Mac and is not exposed directly to the Internet.
|
|
55
|
+
When Remote connection is enabled, paired devices can also reach that bridge off-LAN through an end-to-end encrypted relay.
|
|
56
|
+
External agent communication is handled separately through the A2A relay (`a2a.viveworker.com`), which the bridge polls outbound.
|
|
55
57
|
|
|
56
58
|
## Mac mini Ideas
|
|
57
59
|
|
|
@@ -117,6 +119,22 @@ After setup:
|
|
|
117
119
|
- use the pairing URL only for first-time setup or when you intentionally add another device
|
|
118
120
|
- keep using the Home Screen app if you want notifications to work reliably
|
|
119
121
|
|
|
122
|
+
## Remote Connection
|
|
123
|
+
|
|
124
|
+
Remote connection lets a paired device reach your Mac even when it is not on the same Wi-Fi.
|
|
125
|
+
|
|
126
|
+
Enable it from `Settings > Remote connection` in the Home Screen app. The bridge still stays local on your Mac; it does not open an inbound public port. Instead, the Mac and the paired device meet through the relay, and the actual traffic is end-to-end encrypted between the paired device and your bridge.
|
|
127
|
+
|
|
128
|
+
Security details:
|
|
129
|
+
|
|
130
|
+
- only devices already paired on your LAN can use Remote connection
|
|
131
|
+
- the relay token can be rotated manually from the paired device when you are back on the same LAN
|
|
132
|
+
- existing devices refresh their relay token automatically during LAN enrollment after the rotation window
|
|
133
|
+
- connection events and token rotations appear in `Settings > Remote connection > Connection history`
|
|
134
|
+
- if a device is lost, revoke it from `Settings > Remote connection` or `Settings > Devices`
|
|
135
|
+
|
|
136
|
+
If Remote connection is turned off, off-LAN devices cannot reach the Mac again until you return to the same Wi-Fi and turn it back on.
|
|
137
|
+
|
|
120
138
|
## Common Commands
|
|
121
139
|
|
|
122
140
|
Use these commands most often:
|
|
@@ -392,6 +410,7 @@ Today the project already exposes practical integration points through:
|
|
|
392
410
|
|
|
393
411
|
- Codex + Claude Desktop support
|
|
394
412
|
- Claude hooks
|
|
413
|
+
- Remote connection for paired devices
|
|
395
414
|
- A2A relay + Agent Card
|
|
396
415
|
- File Share URLs
|
|
397
416
|
- Thread Sharing across sessions
|
|
@@ -404,6 +423,7 @@ make `viveworker` the default local/mobile surface that other AI tools can plug
|
|
|
404
423
|
If you want to build on `viveworker`, these are the main surfaces to think in:
|
|
405
424
|
|
|
406
425
|
- **Approvals and structured decisions**: approvals, plan checks, multiple-choice questions, and completions all land in the same mobile flow
|
|
426
|
+
- **Remote connection**: keep that mobile flow reachable from paired devices even when they are outside the LAN
|
|
407
427
|
- **Thread Sharing**: move notes, plan reviews, and handoffs between Codex and Claude sessions with phone approval in the loop
|
|
408
428
|
- **File Share**: hand back static artefacts as private URLs instead of chat attachments
|
|
409
429
|
- **A2A relay**: receive or send external agent tasks through a public relay while execution stays local
|
|
@@ -416,6 +436,7 @@ These parts already feel like core product surface, not side experiments:
|
|
|
416
436
|
- Codex mobile approvals, questions, completions, and code review
|
|
417
437
|
- Claude Desktop integration through hooks
|
|
418
438
|
- trusted-LAN pairing, HTTPS, PWA install, and Web Push
|
|
439
|
+
- Remote connection for paired devices, with E2E relay traffic, token rotation, and connection history
|
|
419
440
|
- A2A task intake + approval + local execution
|
|
420
441
|
- File Share for static artefacts
|
|
421
442
|
- Thread Sharing between Codex and Claude sessions
|
|
@@ -474,8 +495,11 @@ If you can map your tool onto one of those paths, you probably do not need a bra
|
|
|
474
495
|
|
|
475
496
|
## Security Model
|
|
476
497
|
|
|
477
|
-
-
|
|
498
|
+
- pair devices only on a trusted LAN
|
|
478
499
|
- do not expose the bridge directly to the Internet
|
|
500
|
+
- Remote connection uses a relay only for rendezvous/transport; paired-device traffic is end-to-end encrypted to your bridge
|
|
501
|
+
- relay tokens can be rotated manually and are refreshed automatically during LAN enrollment after the rotation window
|
|
502
|
+
- remote connection activity is recorded locally in `~/.viveworker/remote-pairing-audit.jsonl`
|
|
479
503
|
- if you lose a paired device, revoke it from `Settings > Devices`
|
|
480
504
|
- use `pair` only when you want to add another trusted device
|
|
481
505
|
- A2A relay authentication: external agents must provide a valid API key (`X-A2A-Key` header), and registration requires GitHub OAuth
|
|
@@ -490,6 +514,7 @@ If you later want a second wake-up notification path, you can add `ntfy` alongsi
|
|
|
490
514
|
## Troubleshooting
|
|
491
515
|
|
|
492
516
|
- If the `.local` URL does not open, use the printed IP-based URL
|
|
517
|
+
- If Remote connection does not work off-LAN, open `Settings > Remote connection` once on the same Wi-Fi as the Mac and confirm the device is registered
|
|
493
518
|
- If pairing has expired, run `npx viveworker pair`
|
|
494
519
|
- If notifications do not appear, make sure you opened the Home Screen app, not just a browser tab
|
|
495
520
|
- If Web Push is enabled, make sure you are opening the HTTPS URL
|
|
@@ -512,3 +537,7 @@ Planned next steps include:
|
|
|
512
537
|
- ✅ ~~Android support~~ (Apr 1, 2026)
|
|
513
538
|
- ✅ ~~Moltbook integration~~ (Apr 10, 2026)
|
|
514
539
|
- ✅ ~~A2A protocol support~~ (Apr 12, 2026)
|
|
540
|
+
- ✅ ~~File Share~~ (Apr 18, 2026)
|
|
541
|
+
- ✅ ~~Auto Pilot~~ (Apr 21, 2026)
|
|
542
|
+
- ✅ ~~x402 / pay-per-unlock flow (testnet)~~ (Apr 25, 2026)
|
|
543
|
+
- ✅ ~~Remote connection~~ (Apr 28, 2026)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Open mobile control surface for Codex, Claude, Thread Sharing, File Share, Moltbook, and A2A tasks on your trusted LAN.",
|
|
5
5
|
"author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,12 +61,16 @@
|
|
|
61
61
|
"ntfy/docker-compose.yml.example"
|
|
62
62
|
],
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@hazbase/auth": "^0.5.
|
|
64
|
+
"@hazbase/auth": "^0.5.1",
|
|
65
|
+
"@noble/ciphers": "^1.0.0",
|
|
66
|
+
"@noble/curves": "^1.2.0",
|
|
67
|
+
"@noble/hashes": "^1.3.2",
|
|
65
68
|
"ethers": "^6.16.0",
|
|
66
69
|
"qrcode-terminal": "^0.12.0",
|
|
67
70
|
"web-push": "^3.6.7"
|
|
68
71
|
},
|
|
69
72
|
"devDependencies": {
|
|
70
|
-
"@cf-wasm/resvg": "^0.3.3"
|
|
73
|
+
"@cf-wasm/resvg": "^0.3.3",
|
|
74
|
+
"fake-indexeddb": "^6.2.5"
|
|
71
75
|
}
|
|
72
76
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# `scripts/lib/remote-pairing/` — viveworker remote pairing crypto
|
|
2
|
+
|
|
3
|
+
End-to-end encryption layer for connecting phone PWA ↔ PC bridge over an
|
|
4
|
+
**untrusted relay** (Cloudflare Worker + Durable Object). Status: **Phase 0**
|
|
5
|
+
— crypto primitives + handshake + identity key persistence verified by
|
|
6
|
+
unit tests and a two-process E2E demo. Transport (WSS), envelope (replay
|
|
7
|
+
buffer / sequence numbers), and PWA wiring are tracked separately.
|
|
8
|
+
|
|
9
|
+
## Why this exists
|
|
10
|
+
|
|
11
|
+
viveworker today pairs a phone PWA and a Mac bridge over the local LAN
|
|
12
|
+
(`https://<lan-ip>:8810` with mkcert). For users away from the LAN we
|
|
13
|
+
want the same UX without:
|
|
14
|
+
|
|
15
|
+
- shipping a phone-side app (PWA constraint),
|
|
16
|
+
- terminating TLS at a third party (no Cloudflare Tunnel),
|
|
17
|
+
- exposing a public HTTPS endpoint to the internet at large.
|
|
18
|
+
|
|
19
|
+
The chosen architecture is a Cloudflare Worker + Durable Object that
|
|
20
|
+
brokers WSS connections between phone and PC. This module is the
|
|
21
|
+
**E2EE layer that runs on top of that relay**. The relay carries
|
|
22
|
+
ciphertext and routing metadata; it never sees decrypted application
|
|
23
|
+
data and never holds long-term keys.
|
|
24
|
+
|
|
25
|
+
## Protocol choice: `Noise_IK_25519_ChaChaPoly_SHA256`
|
|
26
|
+
|
|
27
|
+
Why IK specifically:
|
|
28
|
+
|
|
29
|
+
- The phone (initiator) knows the PC bridge's static public key from the
|
|
30
|
+
pairing flow that happened over LAN. With that prior knowledge IK
|
|
31
|
+
gives **mutual authentication in 1 round-trip** (msg 1: phone → PC,
|
|
32
|
+
msg 2: PC → phone, then transport).
|
|
33
|
+
- Both sides' static keys are bound into the handshake hash, so a MitM
|
|
34
|
+
who tampers with either side fails to produce a valid AEAD tag.
|
|
35
|
+
- The chosen DH (`X25519`), AEAD (`ChaCha20-Poly1305`), and hash (`SHA256`)
|
|
36
|
+
all map cleanly to Web Crypto / libsodium.js / `@noble/*` so the same
|
|
37
|
+
protocol is implementable in the PWA without a polyfill audit.
|
|
38
|
+
|
|
39
|
+
What we **explicitly didn't pick**:
|
|
40
|
+
|
|
41
|
+
- **Static-static ECDH with a long-term symmetric key**: would lose
|
|
42
|
+
forward secrecy. A relay snapshot + future identity-key compromise
|
|
43
|
+
would decrypt all past sessions.
|
|
44
|
+
- **Signal Double Ratchet**: per-message rotation, but for a request/
|
|
45
|
+
response control plane the cost (more state, more code, more
|
|
46
|
+
failure modes on resume) doesn't earn its keep.
|
|
47
|
+
- **Noise XX**: would work too, but adds a round trip for static-key
|
|
48
|
+
exchange that IK avoids.
|
|
49
|
+
|
|
50
|
+
## Layered key model
|
|
51
|
+
|
|
52
|
+
Three keys, kept strictly separated. Mixing them is a confused-deputy
|
|
53
|
+
hazard (e.g., a wallet-signed device-pairing message becoming a wallet
|
|
54
|
+
authorization).
|
|
55
|
+
|
|
56
|
+
| Layer | Algo | Lifetime | Where | Used for |
|
|
57
|
+
| -------- | ------------------- | ------------------------ | ----------------- | ----------------------------------------- |
|
|
58
|
+
| Wallet | `secp256k1` | Long-term | hazBase / wallet | EVM signing, USDC x402 payments |
|
|
59
|
+
| Identity | `X25519` | Long-term, per device | This module | Noise IK static `s`, device-pair auth |
|
|
60
|
+
| Session | `X25519` ephemeral | Per WS connection | Derived in Noise | AEAD `k`/`n`, **per-session forward sec** |
|
|
61
|
+
|
|
62
|
+
The session key never touches disk. Each new WSS connection runs a fresh
|
|
63
|
+
IK handshake → fresh ephemeral DH → fresh `CipherState`. A relay
|
|
64
|
+
compromise plus future identity-key leak still cannot decrypt past
|
|
65
|
+
recordings.
|
|
66
|
+
|
|
67
|
+
## Files
|
|
68
|
+
|
|
69
|
+
- `noise.mjs` — Noise IK state machine. `HandshakeState`, `CipherState`,
|
|
70
|
+
`SymmetricState`, `NoiseSession`. Pure Uint8Array I/O so the same
|
|
71
|
+
module runs in Node and browser bundlers (no `Buffer` leakage).
|
|
72
|
+
- `keys.mjs` — Identity key generation, hex encoding, persistence to
|
|
73
|
+
`~/.viveworker/remote-pairing.env` (file mode `0o600`, dir `0o700`).
|
|
74
|
+
Includes `fingerprintIdentity()` for human-readable verification
|
|
75
|
+
strings (`XXXX-XXXX-XXXX`, Crockford-ish alphabet).
|
|
76
|
+
|
|
77
|
+
## Wire layout (Phase 0)
|
|
78
|
+
|
|
79
|
+
This module produces and consumes **Noise transport messages only**.
|
|
80
|
+
Envelope-level fields (sequence numbers, message IDs, replay buffer
|
|
81
|
+
indexing) are deliberately not part of this layer — they're added by
|
|
82
|
+
the Phase 1 envelope module so the relay can route without seeing
|
|
83
|
+
ciphertext. End-state target:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
WSS frame body (binary)
|
|
87
|
+
└── outer envelope ← visible to relay; routing-only
|
|
88
|
+
├── seq: u32 ← replay buffer ordering
|
|
89
|
+
├── mid: 16 bytes (UUID) ← deduplication
|
|
90
|
+
├── type: data | ack | ping | resume
|
|
91
|
+
└── payload: bytes ← Noise transport message (this module)
|
|
92
|
+
└── ciphertext + 16-byte Poly1305 tag
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Message sizes today:
|
|
96
|
+
|
|
97
|
+
- **Handshake msg 1** (initiator → responder, `e, es, s, ss + payload`):
|
|
98
|
+
`32 + 32 + 16 + payload_len + 16` bytes (e pub, encrypted s pub, AEAD tag,
|
|
99
|
+
encrypted payload, AEAD tag).
|
|
100
|
+
- **Handshake msg 2** (responder → initiator, `e, ee, se + payload`):
|
|
101
|
+
`32 + payload_len + 16` bytes.
|
|
102
|
+
- **Transport message**: `payload_len + 16` bytes.
|
|
103
|
+
|
|
104
|
+
## Channel binding
|
|
105
|
+
|
|
106
|
+
After handshake both sides agree on a 32-byte hash of the handshake
|
|
107
|
+
transcript. `NoiseSession.getChannelBinding()` returns it. Higher
|
|
108
|
+
layers can pin sensitive operations to this binding — e.g., a
|
|
109
|
+
**WebAuthn challenge that the PWA derives from the channel binding**,
|
|
110
|
+
so a passkey assertion is bound to the specific Noise session, not
|
|
111
|
+
just to "any browser session at this origin."
|
|
112
|
+
|
|
113
|
+
This is how we'll wire Phase 5's "passkey-confirm before USDC payment"
|
|
114
|
+
flow without inventing a separate challenge nonce.
|
|
115
|
+
|
|
116
|
+
## Testing
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Unit tests — protocol semantics (15 tests)
|
|
120
|
+
node --test scripts/test/remote-pairing-noise.test.mjs
|
|
121
|
+
|
|
122
|
+
# End-to-end demo — two real processes over stdio pipes (Phase 0 DoD)
|
|
123
|
+
node scripts/test/remote-pairing-demo.mjs
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The demo prints a transcript of both sides' wire frames in hex; eyeball
|
|
127
|
+
that no plaintext (e.g. `"approve task #42"`) appears anywhere outside
|
|
128
|
+
the decrypted payloads.
|
|
129
|
+
|
|
130
|
+
## Phase 0 → Phase 1 hand-off
|
|
131
|
+
|
|
132
|
+
What this module guarantees today:
|
|
133
|
+
|
|
134
|
+
- ✅ Handshake completes with 1 round-trip given a pre-shared responder static key.
|
|
135
|
+
- ✅ Mutual authentication (wrong responder static key fails handshake).
|
|
136
|
+
- ✅ Forward secrecy per session (ephemeral DH keys, dropped on session end).
|
|
137
|
+
- ✅ AEAD-bound associated data (envelope tampering breaks decryption).
|
|
138
|
+
- ✅ Channel binding for higher-layer auth pinning.
|
|
139
|
+
- ✅ Identity keys persist to disk with strict perms; round-trip verified.
|
|
140
|
+
|
|
141
|
+
What Phase 1 still needs to add (not this module's job):
|
|
142
|
+
|
|
143
|
+
- 🔜 **Envelope** with `seq` / `mid` / `type` for routing + dedup.
|
|
144
|
+
- 🔜 **Replay buffer** in the Durable Object (5-min TTL by default).
|
|
145
|
+
- 🔜 **Reconnect resume** via `RESUME { lastSeq }` → DO drains buffer.
|
|
146
|
+
- 🔜 **At-least-once delivery** with explicit `ACK { mid }` frames.
|
|
147
|
+
- 🔜 **Hibernatable WS** in the Durable Object (cost-critical from day 1).
|
|
148
|
+
- 🔜 **Ping/pong** every 30–45 s to defeat CF's ~100 s WS idle timeout.
|
|
149
|
+
|
|
150
|
+
What Phase 2 still needs to add (PWA side):
|
|
151
|
+
|
|
152
|
+
- 🔜 PWA bundle of `noise.mjs` (already Uint8Array-clean, should JustWork).
|
|
153
|
+
- 🔜 Identity key persistence in browser (IndexedDB; non-extractable
|
|
154
|
+
CryptoKey if the platform supports it, else raw bytes with a passkey
|
|
155
|
+
unlock).
|
|
156
|
+
- 🔜 `visibilitychange` → reconnect-with-resume flow.
|
|
157
|
+
- 🔜 Web Push wake → re-establish WS pipe.
|
|
158
|
+
|
|
159
|
+
## References
|
|
160
|
+
|
|
161
|
+
- Noise spec rev 34: <https://noiseprotocol.org/noise.html>
|
|
162
|
+
- IK pattern security: § 7.4 "Pattern security properties"
|
|
163
|
+
- HKDF: RFC 5869, used as `MixKey` per Noise § 4.3
|
|
164
|
+
- ChaCha20-Poly1305 nonce format: 4 zero bytes + 8-byte LE counter (Noise § 5.1)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _browser-bundle-entry.mjs — Aggregator for the browser bundle.
|
|
3
|
+
*
|
|
4
|
+
* The PWA can't pull npm packages directly (no build step on the
|
|
5
|
+
* served-files side). This module is the input to esbuild — it pulls in
|
|
6
|
+
* noise.mjs + envelope.mjs + keys-core.mjs and their @noble/* dependencies
|
|
7
|
+
* so the bundler can produce a single self-contained ESM file
|
|
8
|
+
* (`web/remote-pairing.bundle.js`).
|
|
9
|
+
*
|
|
10
|
+
* To regenerate the bundle:
|
|
11
|
+
*
|
|
12
|
+
* node scripts/build-remote-pairing-bundle.mjs
|
|
13
|
+
*
|
|
14
|
+
* Anything Node-specific (load/save/ensure helpers from keys.mjs, the
|
|
15
|
+
* stdio demo, the wrangler-driven smoke test) must NOT be re-exported
|
|
16
|
+
* here — those would either fail to bundle or pull node_modules into
|
|
17
|
+
* the browser payload.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Crypto / handshake / session
|
|
21
|
+
export {
|
|
22
|
+
PROTOCOL_NAME,
|
|
23
|
+
HASHLEN,
|
|
24
|
+
KEYLEN,
|
|
25
|
+
DHLEN,
|
|
26
|
+
TAGLEN,
|
|
27
|
+
HandshakeState,
|
|
28
|
+
NoiseSession,
|
|
29
|
+
createInitiator,
|
|
30
|
+
createResponder,
|
|
31
|
+
} from "./noise.mjs";
|
|
32
|
+
|
|
33
|
+
// Wire envelope (frame types, encoders, decoder, helpers)
|
|
34
|
+
export {
|
|
35
|
+
FRAME_DATA,
|
|
36
|
+
FRAME_ACK,
|
|
37
|
+
FRAME_PING,
|
|
38
|
+
FRAME_PONG,
|
|
39
|
+
FRAME_RESUME_REQ,
|
|
40
|
+
FRAME_RESUME_OK,
|
|
41
|
+
FRAME_RESUME_FAIL,
|
|
42
|
+
RESUME_FAIL_BUFFER_EXPIRED,
|
|
43
|
+
RESUME_FAIL_UNKNOWN_PAIRING,
|
|
44
|
+
RESUME_FAIL_HIBERNATED,
|
|
45
|
+
MID_BYTES,
|
|
46
|
+
encodeData,
|
|
47
|
+
encodeAck,
|
|
48
|
+
encodePing,
|
|
49
|
+
encodePong,
|
|
50
|
+
encodeResumeReq,
|
|
51
|
+
encodeResumeOk,
|
|
52
|
+
encodeResumeFail,
|
|
53
|
+
decode,
|
|
54
|
+
generateMid,
|
|
55
|
+
midToHex,
|
|
56
|
+
frameTypeName,
|
|
57
|
+
} from "./envelope.mjs";
|
|
58
|
+
|
|
59
|
+
// Pure key helpers (encoding, derivation, fingerprint). The browser side
|
|
60
|
+
// layers IndexedDB-backed persistence over these in web/remote-pairing/keys.js.
|
|
61
|
+
export {
|
|
62
|
+
IDENTITY_KEY_BYTES,
|
|
63
|
+
bytesToHex,
|
|
64
|
+
hexToBytes,
|
|
65
|
+
generateIdentityKeypair,
|
|
66
|
+
publicFromPrivate,
|
|
67
|
+
fingerprintIdentity,
|
|
68
|
+
} from "./keys-core.mjs";
|
|
69
|
+
|
|
70
|
+
// Application-level RPC framing carried inside the Noise channel. Phone-side
|
|
71
|
+
// rpc-client.js imports these to encode requests / decode responses + events.
|
|
72
|
+
export {
|
|
73
|
+
RPC,
|
|
74
|
+
MAX_RPC_ID_LEN,
|
|
75
|
+
MAX_HEADERS,
|
|
76
|
+
MAX_HEADER_LEN,
|
|
77
|
+
MAX_METHOD_LEN,
|
|
78
|
+
MAX_PATH_LEN,
|
|
79
|
+
MAX_BODY_BYTES,
|
|
80
|
+
MAX_TOPIC_LEN,
|
|
81
|
+
encodeRequest,
|
|
82
|
+
encodeResponse,
|
|
83
|
+
encodeCancel,
|
|
84
|
+
encodeEvent,
|
|
85
|
+
decode as decodeRpc,
|
|
86
|
+
} from "./rpc.mjs";
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audit.mjs — small local audit log for remote-pairing operations.
|
|
3
|
+
*
|
|
4
|
+
* This intentionally stores only human-operational metadata. Relay tokens,
|
|
5
|
+
* request bodies, channel bindings, and raw phone public keys are never
|
|
6
|
+
* written here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { promises as fs } from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
export const REMOTE_PAIRING_AUDIT_FILE = path.join(
|
|
15
|
+
os.homedir(),
|
|
16
|
+
".viveworker",
|
|
17
|
+
"remote-pairing-audit.jsonl",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const MAX_AUDIT_EVENTS = 500;
|
|
21
|
+
const MAX_READ_EVENTS = 100;
|
|
22
|
+
const TYPE_RE = /^[a-z0-9_.:-]{1,64}$/u;
|
|
23
|
+
const OUTCOMES = new Set(["success", "failure", "info"]);
|
|
24
|
+
|
|
25
|
+
export async function appendRemotePairingAuditEvent(event, {
|
|
26
|
+
filePath = REMOTE_PAIRING_AUDIT_FILE,
|
|
27
|
+
} = {}) {
|
|
28
|
+
const record = normalizeAuditEvent(event);
|
|
29
|
+
const dir = path.dirname(filePath);
|
|
30
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
31
|
+
await fs.appendFile(filePath, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
32
|
+
await pruneAuditFile(filePath).catch(() => {});
|
|
33
|
+
return record;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readRemotePairingAuditEvents({
|
|
37
|
+
limit = 20,
|
|
38
|
+
filePath = REMOTE_PAIRING_AUDIT_FILE,
|
|
39
|
+
} = {}) {
|
|
40
|
+
const safeLimit = Math.max(0, Math.min(Number(limit) || 20, MAX_READ_EVENTS));
|
|
41
|
+
if (safeLimit === 0) return [];
|
|
42
|
+
|
|
43
|
+
let text;
|
|
44
|
+
try {
|
|
45
|
+
text = await fs.readFile(filePath, "utf8");
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err.code === "ENOENT") return [];
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const events = [];
|
|
52
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
53
|
+
if (!line.trim()) continue;
|
|
54
|
+
try {
|
|
55
|
+
events.push(normalizeAuditEvent(JSON.parse(line)));
|
|
56
|
+
} catch {
|
|
57
|
+
// Ignore malformed historical lines. The audit log must never break
|
|
58
|
+
// the settings page.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return events
|
|
62
|
+
.sort((a, b) => b.atMs - a.atMs)
|
|
63
|
+
.slice(0, safeLimit);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeAuditEvent(event) {
|
|
67
|
+
const atMs = Number.isFinite(event?.atMs) ? Number(event.atMs) : Date.now();
|
|
68
|
+
const type = TYPE_RE.test(String(event?.type || ""))
|
|
69
|
+
? String(event.type)
|
|
70
|
+
: "unknown";
|
|
71
|
+
const outcome = OUTCOMES.has(String(event?.outcome || ""))
|
|
72
|
+
? String(event.outcome)
|
|
73
|
+
: "info";
|
|
74
|
+
|
|
75
|
+
const out = {
|
|
76
|
+
id: cleanText(event?.id, 80) || `${atMs}-${randomBytes(4).toString("hex")}`,
|
|
77
|
+
atMs,
|
|
78
|
+
type,
|
|
79
|
+
outcome,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
copyText(out, event, "label", 120);
|
|
83
|
+
copyText(out, event, "phoneFingerprint", 32);
|
|
84
|
+
copyText(out, event, "deviceId", 120);
|
|
85
|
+
copyText(out, event, "pairingId", 24);
|
|
86
|
+
copyText(out, event, "state", 32);
|
|
87
|
+
copyText(out, event, "previousState", 32);
|
|
88
|
+
copyText(out, event, "route", 32);
|
|
89
|
+
copyText(out, event, "reason", 160);
|
|
90
|
+
copyText(out, event, "relayHost", 120);
|
|
91
|
+
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function copyText(out, event, key, maxLen) {
|
|
96
|
+
const value = cleanText(event?.[key], maxLen);
|
|
97
|
+
if (value) out[key] = value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function cleanText(value, maxLen) {
|
|
101
|
+
if (typeof value !== "string") return "";
|
|
102
|
+
return value
|
|
103
|
+
.replace(/[\r\n\t]+/gu, " ")
|
|
104
|
+
.replace(/\s{2,}/gu, " ")
|
|
105
|
+
.trim()
|
|
106
|
+
.slice(0, maxLen);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function pruneAuditFile(filePath) {
|
|
110
|
+
let text;
|
|
111
|
+
try {
|
|
112
|
+
text = await fs.readFile(filePath, "utf8");
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const lines = text.split(/\r?\n/u).filter((line) => line.trim());
|
|
117
|
+
if (lines.length <= MAX_AUDIT_EVENTS) return;
|
|
118
|
+
const kept = lines.slice(-MAX_AUDIT_EVENTS).join("\n") + "\n";
|
|
119
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
120
|
+
await fs.writeFile(tmpPath, kept, { mode: 0o600 });
|
|
121
|
+
await fs.rename(tmpPath, filePath);
|
|
122
|
+
}
|