whatsapp: M1 foundations
This commit is contained in:
@@ -800,3 +800,114 @@ fails review.
|
||||
| 2026-10-28 | WebSocket client transport | `realtime/websocket/client/websocket_client.h` (exists, but needs a caller-provided transport: the caller does TLS and the HTTP handshake itself) | keep local `WaSocket` on libcurl: live-verified against the real server (connect + frames), libcurl already linked. Revisit if libcurl ws proves insufficient (e.g. M1 keepalive pings). |
|
||||
| 2026-10-28 | media HTTPS upload/download | `clients/` HTTP client, pool, proxy (heavier connection-pool stack) | keep local `WaMedia` on `curl_easy` (`wa_media.cpp`): two calls (one POST, one GET), libcurl already linked |
|
||||
| 2026-10-28 | vault storage for device/sessions/contacts | detz-owned `store::SqlDb` (already the vault; the schema is detz's) | keep local `WaStore` on `store::SqlDb` (`wa_store.h`); no moloapp store facility needed |
|
||||
| 2026-10-28 | M1 keepalive pings (revisit of the M0 transport row) | `realtime/websocket/client/websocket_client.h` `send_ping`, `realtime/websocket/server/heartbeat_manager.h`, `realtime/engineio/heartbeat_scheduler.h` | wrote local `WaKeepAlive` (`wa_keepalive.h`): whatsmeow keepalive is an application iq (`w:p` get), not a websocket ping frame, with whatsmeow-specific timing (20-30 s jitter, 10 s deadline, 3 min horizon); moloapp's facilities are ws-protocol/engine.io level and none fits. `WaSocket` stays on libcurl. |
|
||||
|
||||
## 18. M1 foundations: tokens, socket, iq, Noise (2026-10-28)
|
||||
|
||||
Milestone M1 of `full_transpilation_and_integration.md` (slice
|
||||
`tools/wa-slices/m1-foundations.txt`, 91 entries, 100% mapped; full inventory
|
||||
210/1522, 13.8%). Four pieces, each proven by a Go oracle that calls the pinned
|
||||
whatsmeow itself, plus hostile-input tests and a sanitizer run.
|
||||
|
||||
### 18.1 Harness repairs found by M1
|
||||
|
||||
Three harness repairs surfaced and were fixed in this milestone (all minimal,
|
||||
all required by the M1 gates):
|
||||
|
||||
- `tools/wa-api-inventory.sh` cut `//` comments before stripping string
|
||||
literals, so the `//` inside the `"https://..."` token broke brace-depth
|
||||
counting and the `var (` block in `binary/token/token.go` swallowed the
|
||||
rest of the file. `token.DictVersion`, 3 lookup funcs and 18 wire consts
|
||||
(22 entries) were missing from the scope. Fixed by stripping strings
|
||||
first; the inventory is now 1522 entries (was 1500) with zero removals.
|
||||
- `tools/wa-stub-scan.sh` died silently (exit 1, no verdict) on untracked
|
||||
files without matches: `grep` exits 1, and `set -e`/`pipefail` killed the
|
||||
script. Fixed with `|| true` plus a comment; M0's validation (fail on an
|
||||
injected TODO / silent return) still holds.
|
||||
- `tools/san-whatsapp.sh` compiled only 6 parser TUs, but the test now links
|
||||
the whole module. It compiles all of `src/whatsapp/` plus the support TUs
|
||||
(`json`, `seal_base64`, `sqldb`, `fuzzy`, `qrcodegen`) and links curl and
|
||||
sqlite3.
|
||||
|
||||
### 18.2 Token dictionary freshness
|
||||
|
||||
`tests/whatsapp_data/token_oracle.go` dumps whatsmeow's own `binary/token`
|
||||
tables (version, 236 + 4x256 strings, 18 consts) and whatsmeow-marshaled wire
|
||||
bytes for 14 token-probe nodes (single-byte tokens, one token per double-byte
|
||||
dictionary, an untokenized literal). `build/whatsapp_test` (`test_tokens`)
|
||||
compares every string verbatim, every const, and byte-exact re-encodes all 14
|
||||
probes. Re-running the oracle after a pin move fails loudly on drift. The
|
||||
tables were already correct; the check is the deliverable.
|
||||
|
||||
Two encoding forms differ cosmetically and both are valid: whatsmeow writes a
|
||||
typed server-JID `to` as JID_PAIR while detz writes the same string as its
|
||||
1-byte token, and whatsmeow writes `""` as NIBBLE_8 while detz writes
|
||||
BINARY_8. Both pairs decode identically (proven in `test_iq`/`test_tokens`);
|
||||
the server accepts ours (proven live, section 18.5).
|
||||
|
||||
### 18.3 Socket keepalive pings
|
||||
|
||||
`WaKeepAlive` (`wa_keepalive.h`) is the whatsmeow `keepAliveLoop` schedule as
|
||||
pure timing state over an injected millisecond clock: jittered 20-30 s
|
||||
intervals, error counting, forced reconnect past the 3 min horizon. Caps in
|
||||
`wa_consts.h` (`kWaKeepAliveResponseDeadline` 10 s, `kWaKeepAliveIntervalMin`
|
||||
20 s, `kWaKeepAliveIntervalMax` 30 s, `kWaKeepAliveMaxFailTime` 180 s).
|
||||
`WaClient::run_until` starts it and sends a `w:p` get on schedule via
|
||||
`query_iq`; any reply proves liveness, exactly as `sendKeepAlive`. On a forced
|
||||
reconnect the client logs and disconnects; the redial lands with M9
|
||||
auto-reconnect. The `KeepAliveTimeout`/`KeepAliveRestored` events belong to
|
||||
the M9 event bus; until then failures are loud log lines.
|
||||
|
||||
### 18.4 Full iq request/response
|
||||
|
||||
`WaIq` (`wa_iq.h`) mirrors `request.go` for the synchronous client: `WaIqQuery`
|
||||
plus `build` (id/xmlns/type, `smax_id`, `to`, `target`, one child),
|
||||
`parse_error` (whatsmeow `parseIQError`, overflow-safe code parse, never
|
||||
fails), `is_disconnect_node` / `is_auth_error_disconnect` (401, replaced,
|
||||
device_removed), and whatsmeow-shaped request ids (`new_prefix` draws
|
||||
`<byte0>.<byte1>-`, `generate_id` appends the counter). `WaIqError` carries
|
||||
the 13 `ErrIQ*` sentinels plus `timed_out()` with `message()`/`is()`;
|
||||
`WaDisconnectedError` carries `for_iq()` with `message()`/`is()`.
|
||||
`WaClient::send_iq` now classifies every reply (error replies log code/text
|
||||
and are returned; disconnects and timeouts log loudly), defaults to the 75 s
|
||||
`kWaIqDefaultTimeout`, and `query_iq` fills ids. `ErrClientIsNil` maps to the
|
||||
class itself: methods take references, a nil receiver is unrepresentable.
|
||||
Retry-on-reconnect needs M9 auto-reconnect (`NoRetry` arrives with it); today
|
||||
a disconnect during an iq is a loud `WaDisconnectedError`, never a silent
|
||||
drop. Vectors: `iq_oracle.go` (whatsmeow-marshaled ping, count, 404, 500,
|
||||
bare, result, three disconnects; code/text extracted by whatsmeow's own
|
||||
getters; byte-exact encode checks where deterministic).
|
||||
|
||||
### 18.5 Noise parity
|
||||
|
||||
- `WaNoise::mix_shared_secret` names the X25519-into-key step
|
||||
(`MixSharedSecretIntoKey`); the handshake calls it three times. Behavior is
|
||||
unchanged (existing transcript test) and now differentially proven: the
|
||||
oracle runs whatsmeow's own `NoiseHandshake` over the wa-go trace and our
|
||||
decrypted server static + payload match byte-for-byte.
|
||||
- `WaFraming::kLengthSize` names the 3-byte prefix, and `unframe` uses
|
||||
overflow-safe subtraction checks (it had the forbidden `pos + 3` form).
|
||||
- `WaServerCert` (`wa_cert.h`) verifies the Noise certificate chain exactly
|
||||
as `verifyServerCert`: pinned-key intermediate check, issuer serials,
|
||||
64-byte signatures, key lengths, validity windows, leaf-key-vs-static
|
||||
match. Any failure throws `WaCertError` with the reason, and the handshake
|
||||
fails closed. This caught two real proto bugs: `parse_cert_chain` had leaf
|
||||
and intermediate swapped (fields are leaf = 1, intermediate = 2), and
|
||||
`parse_cert_details` read the wrong fields (serial = 1, issuer = 2, key =
|
||||
3, not_before = 4, not_after = 5). Vectors: `cert_oracle.go` (decrypted
|
||||
bytes, per-field values, per-check verdicts from whatsmeow + libsignal;
|
||||
the captured leaf is expired today, so the test time-travels inside its
|
||||
window for the accept case and asserts expiry now, plus 6 tamper and 4
|
||||
hostile rejections).
|
||||
- `WaSocket` gained the already-open and not-connected guards plus
|
||||
`WaSocketStatusError` (HTTP status on connect failure); `WaClient`
|
||||
refuses a second connect loudly and reports closes through the new
|
||||
`on_disconnect` hook.
|
||||
|
||||
Live verification (2026-10-28, scratch `TAI_HOME`, no phone):
|
||||
`detz --whatsapp-link` completed the Noise handshake against
|
||||
`web.whatsapp.com` with the new certificate verification passing
|
||||
("handshake ok (registration)") and rendered the QR. A 60 s session fired
|
||||
keepalive pings on schedule with server replies (zero unanswered/failed
|
||||
lines, connection stayed open until the timeout kill). The cert accept path
|
||||
and the ping shape are therefore live-proven, not just oracle-proven.
|
||||
|
||||
Reference in New Issue
Block a user