@ibgib/core-gib 0.1.59 → 0.1.60

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs +1 -1
  3. package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs.map +1 -1
  4. package/dist/sync/sync-peer/sync-peer-types.d.mts +12 -1
  5. package/dist/sync/sync-peer/sync-peer-types.d.mts.map +1 -1
  6. package/dist/sync/sync-peer/sync-peer-v1.d.mts +7 -0
  7. package/dist/sync/sync-peer/sync-peer-v1.d.mts.map +1 -1
  8. package/dist/sync/sync-peer/sync-peer-v1.mjs +43 -1
  9. package/dist/sync/sync-peer/sync-peer-v1.mjs.map +1 -1
  10. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.d.mts +1 -0
  11. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.d.mts.map +1 -1
  12. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mjs +15 -5
  13. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mjs.map +1 -1
  14. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.d.mts +16 -0
  15. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.d.mts.map +1 -1
  16. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs +223 -79
  17. package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs.map +1 -1
  18. package/dist/sync/sync-saga-context/sync-saga-context-helpers.d.mts.map +1 -1
  19. package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs +41 -2
  20. package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs.map +1 -1
  21. package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts +4 -0
  22. package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts.map +1 -1
  23. package/dist/sync/sync-saga-coordinator.d.mts +6 -0
  24. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  25. package/dist/sync/sync-saga-coordinator.mjs +57 -1
  26. package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
  27. package/dist/sync/sync-withid.pingpong.respec.mjs +68 -0
  28. package/dist/sync/sync-withid.pingpong.respec.mjs.map +1 -1
  29. package/package.json +1 -1
  30. package/src/sync/docs/security-3b.md +92 -0
  31. package/src/sync/docs/security.md +107 -39
  32. package/src/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mts +1 -1
  33. package/src/sync/sync-peer/sync-peer-types.mts +11 -1
  34. package/src/sync/sync-peer/sync-peer-v1.mts +47 -1
  35. package/src/sync/sync-peer/sync-peer-websocket/README.md +42 -0
  36. package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mts +14 -5
  37. package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mts +242 -78
  38. package/src/sync/sync-saga-context/sync-saga-context-helpers.mts +46 -4
  39. package/src/sync/sync-saga-context/sync-saga-context-types.mts +5 -0
  40. package/src/sync/sync-saga-coordinator.mts +69 -1
  41. package/src/sync/sync-withid.pingpong.respec.mts +74 -1
  42. package/src/sync/docs/ping_pong_plan.md +0 -147
@@ -0,0 +1,92 @@
1
+ # Implementation Plan: Phase 3B — Basic Single-Timeline Sync with Identity (Integrated)
2
+
3
+ This document outlines the implementation plan for Phase 3B of identity integration, focusing on integrating the cryptographic session identity into the WebSocket sync peer architecture, and wiring up the Row 3B dev-tools test buttons.
4
+
5
+ ## 1. Goal
6
+ The goal of Phase 3B is to achieve successful end-to-end synchronization of a test ibgib `X` over a stateful WebSocket connection using asymmetric session identity validation.
7
+ At the end of the sync saga:
8
+ - Both sender and receiver durable spaces must contain the same evolved Domain Keystone (I1).
9
+ - Both spaces must contain the full session identity keystone (S) graph evolved to the correct turn count ($n = 3$, or potentially $n = 4$ depending on exact commit phase timeline evolution).
10
+ - Both spaces must contain the identical target ibgib `X` dependency graph.
11
+
12
+ ---
13
+
14
+ ## 2. Technical Challenge: Asymmetric Authentication in WebSocket Peers
15
+ In V1, only the sender (Alice) signs the synchronization context. The receiver (Bob) skips signing the response context (`skipSign: true`), relying on transport-level security (TLS) for the return path.
16
+ Consequently:
17
+ - Alice's request context contains a `signedSessionIdentity`.
18
+ - Bob's response context has `signedSessionIdentity` set to `undefined`.
19
+
20
+ ### Current Issue in `SyncPeerWebSocketSender_V1`
21
+ In `SyncPeerWebSocketSender_V1.handleRuntimeMessage`, the sender currently attempts to authenticate Bob's response context:
22
+ ```typescript
23
+ if (msg.type === SyncWebSocketMsgType.sync_frame_response) {
24
+ const responseContext = msg.context as SyncSagaContextIbGib_V1;
25
+
26
+ // Validate and authenticate Bob's response context first
27
+ await this.authenticateAndValidate({ context: responseContext });
28
+ ```
29
+ Because `authenticateAndValidate` calls `authenticateContextIntrinsically` which expects a signature when `sagaFrame.data.sessionIdentityTjpAddr` is present, this call will throw an error since Bob's response context has no `signedSessionIdentity`.
30
+
31
+ ### Solution
32
+ Change `SyncPeerWebSocketSender_V1.handleRuntimeMessage` to run structural/intrinsic validation instead of full authentication on the response context:
33
+ ```typescript
34
+ // Ensure the response context and saga frame are valid structurally
35
+ const validationErrors = await validateContextAndSagaFrame({ context: responseContext });
36
+ if (validationErrors.length > 0) {
37
+ throw new Error(`Invalid response context received. Errors: ${validationErrors.join(', ')}`);
38
+ }
39
+ ```
40
+ The client-side `SyncSagaCoordinator` will then run the turn-by-turn continuation, sequence, and identity validation checks via `validateReturnContext(...)`. This provides a robust stopgap validation for V1 before the receiver-side signing is fully implemented in future versions.
41
+
42
+
43
+ ---
44
+
45
+ ## 3. UI Integration: Dev Tools Row 3B
46
+ We will wire up the three buttons in `apps/space-gib/src/client/dev-tools.mts` under the `init3bPlaceholder` placeholder handler:
47
+
48
+ ### 3.1. `btn-3b-setup`
49
+ - **Actions**:
50
+ 1. Generates a new long-lived Domain Keystone (I) for Alice using master secret `'test-sender-secret-phase3'` and unique salts (`'senderidentitysyncsaltphase3'`, `'senderidentitymanagesaltphase3'`).
51
+ 2. Registers the genesis Domain Keystone (I) on the server via `apiBridge.postGenesisKeystone(domainI)`.
52
+ 3. Creates a local dummy Target IbGib (X) with distinct test data: `{ hello: 'world3', random: Math.random() }`.
53
+ 4. Stores `domainI`, `domainIMasterSecret`, and `targetX` in `debugState`.
54
+ 5. Updates button text to `✓ 3B Setup Complete` and enables `btn-3b-sync`.
55
+
56
+ ### 3.2. `btn-3b-sync`
57
+ - **Actions**:
58
+ 1. Instantiates `SyncPeerWebSocketSender_V1` targeting the server's WS and evolution endpoints.
59
+ 2. Initializes peer options with Alice's identity, secret, and a newly generated `sagaId`.
60
+ 3. Instantiates `SyncSagaCoordinator`.
61
+ 4. Calls `coordinator.sync(...)` to synchronize Target X to the server.
62
+ 5. Awaits completion (`await syncSaga.done`). This must run without errors.
63
+ 6. Updates button text to `✓ 3B Sync Run` and enables `btn-3b-check`.
64
+
65
+ ### 3.3. `btn-3b-check`
66
+ - **Actions**:
67
+ 1. Resolves the client's latest Target X address and fetches its local dependency graph.
68
+ 2. Resolves the server's latest Target X address and fetches its server-side dependency graph using a new API bridge helper.
69
+ 3. Asserts the client and server Target X graphs are equivalent:
70
+ - The latest address of X is identical.
71
+ - Graph node counts are identical.
72
+ - All keys and values match.
73
+ 4. Resolves the latest session identity (S) address and asserts it evolved to `n = 2` (Init at $n=0$, Connect Handshake at $n=1$, Sync Context Sign at $n=2$).
74
+ 5. Verifies the latest S address and graph are identical on both client and server.
75
+ 6. Verifies the evolved Domain Keystone (I1) is present in both client and server spaces.
76
+ 7. If all checks pass, logs `🎉 ALL PHASE 3B CHECKS PASSED FLAWLESSLY! ✓` and updates button text to `✓ 3B All Passed`.
77
+
78
+ ---
79
+
80
+ ## 4. Concrete Checklist
81
+
82
+ ### [ ] API Bridge Extension
83
+ - [ ] Add `getIbGibGraph(domainAddr, ibGibAddr)` to `SpaceGibApiBridge` (`apps/space-gib/src/client/api/space-gib-api-bridge.mts`) to retrieve dependency graphs with `getGraph=true`.
84
+
85
+ ### [ ] WebSocket Peer Adjustments
86
+ - [ ] Modify `SyncPeerWebSocketSender_V1.handleRuntimeMessage` to bypass `authenticateAndValidate` on Bob's response context, using `validateContextAndSagaFrame` instead.
87
+
88
+ ### [ ] Dev Tools UI (Row 3B) Wiring
89
+ - [ ] Implement `init3bSetupButton` inside `dev-tools.mts`.
90
+ - [ ] Implement `init3bSyncButton` inside `dev-tools.mts`.
91
+ - [ ] Implement `init3bCheckButton` inside `dev-tools.mts`.
92
+ - [ ] Update `initDevTools` to initialize the new button handlers and remove `init3bPlaceholder`.
@@ -285,29 +285,29 @@ We still call `senderCoordinator.sync(...)` — the phase focus is on what `peer
285
285
 
286
286
  **Goal**: With identity fully established and transport connected, sync a single ibgib `X` from source to dest. Verify S evolves correctly per turn (`sync` pool), and X's full dependency graph arrives on dest. After commit, both sender and receiver durable spaces must contain: the same evolved I, the full S dependency graph, and the full X dependency graph.
287
287
 
288
- #### Phase 3A — Innerspace unit test (`sync-withid.fullsync.respec.mts`)
289
-
290
- - [ ] Create `sync-withid.fullsync.respec.mts`
291
- - [ ] Create `r1_alpha_v0_source` via `TestTransformer`
292
- - [ ] Call `senderCoordinator.sync({ domainIbGibs: [alpha], senderIdentity, senderSecret, ... })`
293
- - [ ] `ifWeMight` — `"alpha dep graph matches on source and dest"`: use `getDependencyGraph` + `graphsAreEquivalent`
294
- - [ ] `ifWeMight` — `"sessionIdentity evolved the expected number of times"`: assert `S.data.n` equals the known deterministic turn count
295
- - [ ] 🔲 **TODO**: Manually inspect the full session keystone graph after a first run and count the exact number of outgoing turns (Init + Delta(s) + Commit), then hard-code that number into this assertion
296
- - [ ] `ifWeMight` — `"sender durable space has I (evolved frame)"`: assert I1 addr is in `sourceSpace`
297
- - [ ] `ifWeMight` — `"receiver durable space has I (evolved frame)"`: assert I1 addr is in `destSpace`
298
- - [ ] `ifWeMight` — `"sender durable space has full S dep graph"`: use `getDependencyGraph` on S in `sourceSpace`
299
- - [ ] `ifWeMight` — `"receiver durable space has full S dep graph"`: use `getDependencyGraph` on S in `destSpace`
288
+ #### Phase 3A — Innerspace unit test (`sync-withid.pingpong.respec.mts`)
289
+
290
+ - [x] Create `sync-withid.pingpong.respec.mts`
291
+ - [x] Create `r1_alpha_v0_source` (represented by `xStone` test domain ibgib)
292
+ - [x] Call `senderCoordinator.sync({ domainIbGibs: [alpha], senderIdentity, senderSecret, ... })`
293
+ - [x] `ifWeMight` — `"alpha dep graph matches on source and dest"`: use `getDependencyGraph` + `graphsAreEquivalent`
294
+ - [x] `ifWeMight` — `"sessionIdentity evolved the expected number of times"`: assert `S.data.n` equals the known deterministic turn count
295
+ - [x] **TODO**: Manually inspect the full session keystone graph after a first run and count the exact number of outgoing turns (Init + Delta(s) + Commit), then hard-code that number into this assertion
296
+ - [x] `ifWeMight` — `"sender durable space has I (evolved frame)"`: assert I1 addr is in `sourceSpace`
297
+ - [x] `ifWeMight` — `"receiver durable space has I (evolved frame)"`: assert I1 addr is in `destSpace`
298
+ - [x] `ifWeMight` — `"sender durable space has full S dep graph"`: use `getDependencyGraph` on S in `sourceSpace`
299
+ - [x] `ifWeMight` — `"receiver durable space has full S dep graph"`: use `getDependencyGraph` on S in `destSpace`
300
300
 
301
301
  #### Phase 3B — Space-Gib integrated (manual)
302
302
 
303
- - [ ] Trigger a sync of a known test ibgib via `dev-tools.mts` (add "sync - fullsync" button to a new row)
304
- - [ ] Confirm S evolves on each outgoing turn (inspect frame `data.n` in debugger)
305
- - [ ] Confirm X's full dependency graph is present on the client after commit
306
- - [ ] Confirm X's full dependency graph is present on the server after commit
307
- - [ ] Confirm S's full dependency graph is present on the client after commit
308
- - [ ] Confirm S's full dependency graph is present on the server after commit
309
- - [ ] Confirm I's evolved frame (I1) is present on the client after commit
310
- - [ ] Confirm I's evolved frame (I1) is present on the server after commit
303
+ - [x] Trigger a sync of a known test ibgib via `dev-tools.mts` (add "sync - fullsync" button to a new row)
304
+ - [x] Confirm S evolves on each outgoing turn (inspect frame `data.n` in debugger)
305
+ - [x] Confirm X's full dependency graph is present on the client after commit
306
+ - [x] Confirm X's full dependency graph is present on the server after commit
307
+ - [x] Confirm S's full dependency graph is present on the client after commit
308
+ - [x] Confirm S's full dependency graph is present on the server after commit
309
+ - [x] Confirm I's evolved frame (I1) is present on the client after commit
310
+ - [x] Confirm I's evolved frame (I1) is present on the server after commit
311
311
 
312
312
 
313
313
  ---
@@ -320,14 +320,14 @@ The sync proper will not be called. The test only cares that the pre-connect ide
320
320
 
321
321
  #### Phase 1A — Innerspace unit test
322
322
 
323
- - [ ] Create `sync-with-identity-basic.respec.mts` with scaffold:
323
+ - [x] Create `sync-withid.establish.respec.mts` with scaffold:
324
324
  - `Metaspace_Innerspace` + two `InnerSpace_V1` (source/dest) + `SyncSagaCoordinator` pair + `newTestPeer()` factory
325
325
  - A `senderSecret` constant (test-only plaintext)
326
326
  - A `KeystoneService_V1` instance (new'd inline)
327
- - [ ] Add `respecfully` block: `"Phase 1: establishSessionIdentity"`
328
- - [ ] `ifWeMight` — `"creates sessionIdentity genesis (S) locally"`: assert `S^Stjp` was generated and exists in `sourceSpace`
329
- - [ ] `ifWeMight` — `"evolves senderIdentity (I → I1) with sync claim"`: assert `I1` frame has a proof whose `claim.verb === 'sync'` and `claim.target === S^Stjp`
330
- - [ ] `ifWeMight` — `"posts I1 and S to destSpace (receiver)"`: assert both addrs are retrievable from `destSpace`
327
+ - [x] Add `respecfully` block: `"Phase 1: establishSessionIdentity"`
328
+ - [x] `ifWeMight` — `"creates sessionIdentity genesis (S) locally"`: assert `S^Stjp` was generated and exists in `sourceSpace`
329
+ - [x] `ifWeMight` — `"evolves senderIdentity (I → I1) with sync claim"`: assert `I1` frame has a proof whose `claim.verb === 'sync'` and `claim.target === S^Stjp`
330
+ - [x] `ifWeMight` — `"posts I1 and S to destSpace (receiver)"`: assert both addrs are retrievable from `destSpace`
331
331
 
332
332
  #### Phase 1B — Space-Gib integrated (manual)
333
333
 
@@ -344,7 +344,7 @@ The sync proper will not be called. The test only cares that the pre-connect ide
344
344
 
345
345
  #### Phase 2A — Innerspace unit test
346
346
 
347
- - [x] Extend `sync-with-identity-basic.respec.mts` with a `respecfully` block: `"Phase 2: connect"` (Note: implemented in separate file `sync-withid.connect.respec.mts`)
347
+ - [x] Create `sync-withid.connect.respec.mts` with a `respecfully` block: `"Phase 2: connect"`
348
348
  - [x] `ifWeMight` — `"connect completes without error"`: call `peer.connect()` and assert no exception
349
349
  - [x] `ifWeMight` — `"connect pool is depleted by exactly one challenge"`: assert `S.data.pools['connect'].used` increased by the expected count
350
350
 
@@ -362,19 +362,87 @@ The sync proper will not be called. The test only cares that the pre-connect ide
362
362
 
363
363
  #### Phase 3A — Innerspace unit test
364
364
 
365
- - [ ] Extend `sync-with-identity-basic.respec.mts` or create `sync-with-identity-singletimeline.respec.mts`
366
- - [ ] Create `r1_alpha_v0_source` via `TestTransformer`
367
- - [ ] Call `senderCoordinator.sync({ domainIbGibs: [alpha], senderIdentity, senderSecret, ... })`
368
- - [ ] `ifWeMight` — `"alpha dep graph matches on source and dest"`: use `getDependencyGraph` + `graphsAreEquivalent`
369
- - [ ] `ifWeMight` — `"sessionIdentity evolved once per sync turn"`: assert `S.data.n` equals number of outgoing turns
365
+ - [x] Create `sync-withid.pingpong.respec.mts`
366
+ - [x] Create `r1_alpha_v0_source` via `TestTransformer` (represented by `xStone` test domain ibgib)
367
+ - [x] Call `senderCoordinator.sync({ domainIbGibs: [alpha], senderIdentity, senderSecret, ... })`
368
+ - [x] `ifWeMight` — `"alpha dep graph matches on source and dest"`: use `getDependencyGraph` + `graphsAreEquivalent`
369
+ - [x] `ifWeMight` — `"sessionIdentity evolved once per sync turn"`: assert `S.data.n` equals number of outgoing turns
370
370
 
371
371
  #### Phase 3B — Space-Gib integrated (manual)
372
372
 
373
- - [ ] Trigger a sync of a known test ibgib via `dev-tools.mts` or equivalent UI
374
- - [ ] Confirm S evolves on each outgoing turn (inspect frame `data.n` in debugger)
375
- - [ ] Confirm X's full dependency graph is present on the client after commit
376
- - [ ] Confirm X's full dependency graph is present on the server after commit
377
- - [ ] Confirm S's full dependency graph is present on the client after commit
378
- - [ ] Confirm S's full dependency graph is present on the server after commit
379
- - [ ] Confirm I's full dependency graph is present on the client after commit
380
- - [ ] Confirm I's full dependency graph is present on the server after commit
373
+ - [x] Trigger a sync of a known test ibgib via `dev-tools.mts` or equivalent UI
374
+ - [x] Confirm S evolves on each outgoing turn (inspect frame `data.n` in debugger)
375
+ - [x] Confirm X's full dependency graph is present on the client after commit
376
+ - [x] Confirm X's full dependency graph is present on the server after commit
377
+ - [x] Confirm S's full dependency graph is present on the client after commit
378
+ - [x] Confirm S's full dependency graph is present on the server after commit
379
+ - [x] Confirm I's full dependency graph is present on the client after commit
380
+ - [x] Confirm I's full dependency graph is present on the server after commit
381
+
382
+ ---
383
+
384
+ ### Phase 4 — Advanced Data and Conflict Scenarios
385
+
386
+ **Goal**: Extend the WebSocket peer and Space-Gib interface to manually execute and verify all advanced data and conflict scenarios currently covered by the InnerSpace unit tests.
387
+
388
+ For each scenario, verify automated innerspace tests (A) are fully functional, and create manual websocket peer tests (B) with corresponding buttons/UI elements.
389
+
390
+ #### 1. Sync InnerSpace (Basic Push Sync)
391
+ - [ ] Phase 4.1A — Automated InnerSpace Verification (`sync-innerspace.respec.mts`)
392
+ - [ ] Phase 4.1B — WebSocket Peer Manual Verification (integrated via Dev Tools)
393
+ * Tests basic push sync of a single timeline and its mutations from Source to Destination.
394
+ * Verifies that the target timeline tip and its dependency graph successfully arrive at the destination.
395
+
396
+ #### 2. Sync Constants (No TJP)
397
+ - [ ] Phase 4.2A — Automated InnerSpace Verification (`sync-innerspace-constants.respec.mts`)
398
+ - [ ] Phase 4.2B — WebSocket Peer Manual Verification (integrated via Dev Tools)
399
+ * Tests syncing "constants" (ibGibs without timelines/TJPs) and verifies that dependencies (linked constants) are resolved.
400
+ * Ensures idempotency/smart diffing (payloads are not re-sent if they already exist in the destination space).
401
+
402
+ #### 3. Sync Destination with Partial History
403
+ - [ ] Phase 4.3A — Automated InnerSpace Verification (`sync-innerspace-partial-update.respec.mts`)
404
+ - [ ] Phase 4.3B — WebSocket Peer Manual Verification (integrated via Dev Tools)
405
+ * Tests push synchronization when the destination already contains a partial timeline history (e.g., up to V1), but the sender has newer frames (V2).
406
+ * Verifies that only the delta is transmitted and correctly linked to the existing history at the destination.
407
+
408
+ #### 4. Sync Deep History Updates
409
+ - [ ] Phase 4.4A — Automated InnerSpace Verification (`sync-innerspace-deep-updates.respec.mts`)
410
+ - [ ] Phase 4.4B — WebSocket Peer Manual Verification (integrated via Dev Tools)
411
+ * Tests push synchronization of a timeline tip with a deep mutation history (multiple updates).
412
+ * Verifies that the destination receives all intermediate frames, preserving the full timeline history and graph integrity.
413
+
414
+ #### 5. Sync Destination Ahead (Remote Newer)
415
+ - [ ] Phase 4.5A — Automated InnerSpace Verification (`sync-innerspace-dest-ahead.respec.mts`)
416
+ - [ ] Phase 4.5B — WebSocket Peer Manual Verification (integrated via Dev Tools)
417
+ * Tests synchronization when the destination (remote) actually has a newer frame (V2) than the sender's local tip (V1).
418
+ * Verifies that the sender learns of the remote tip and pulls it down so both client and server converge to the dest-ahead tip.
419
+
420
+ #### 6. Sync Multiple Independent Timelines
421
+ - [ ] Phase 4.6A — Automated InnerSpace Verification (`sync-innerspace-multiple-timelines.respec.mts`)
422
+ - [ ] Phase 4.6B — WebSocket Peer Manual Verification (integrated via Dev Tools)
423
+ * Tests synchronization of multiple independent timelines (e.g., Timeline A and Timeline B) in a single sync coordinator call.
424
+ * Verifies that both timeline tips and their respective dependency graphs are transferred and updated correctly.
425
+
426
+ #### 7. Basic Divergence Conflict Resolution
427
+ - [ ] Phase 4.7A — Automated InnerSpace Verification (`sync-conflict-basic-divergence.respec.mts`)
428
+ - [ ] Phase 4.7B — WebSocket Peer Manual Verification (integrated via Dev Tools)
429
+ * Tests divergence when source and destination edit different fields on a shared base timeline simultaneously.
430
+ * Verifies that optimistic merge automatically produces a merged tip (V3) containing both edits and records the graft metadata (`graftbase`, `graftorphan`).
431
+
432
+ #### 8. Divergence with Related Timelines
433
+ - [ ] Phase 4.8A — Automated InnerSpace Verification (`sync-conflict-basic-multitimelines.respec.mts`)
434
+ - [ ] Phase 4.8B — WebSocket Peer Manual Verification (integrated via Dev Tools)
435
+ * Tests advanced divergence where one of the divergent updates also references/relates a new independent timeline (Beta).
436
+ * Verifies that during optimistic merge, the dependent timeline is synced automatically alongside the merged tip.
437
+
438
+ #### 9. Text Field Merge (LCS) Conflict Resolution
439
+ - [ ] Phase 4.9A — Automated InnerSpace Verification (`sync-conflict-text-merge.respec.mts`)
440
+ - [ ] Phase 4.9B — WebSocket Peer Manual Verification (integrated via Dev Tools)
441
+ * Tests automatic merging of conflicting string edits within the `ibgib.data.text` field using the LCS (Longest Common Subsequence) merge algorithm.
442
+ * Verifies merging of simple appends (beginning vs. end) and interleaved edits in different paragraphs.
443
+
444
+ #### 10. Advanced Multi-Round/Timeline Permutations
445
+ - [ ] Phase 4.10A — Automated InnerSpace Verification (`sync-conflict-adv-multitimelines.respec.mts`)
446
+ - [ ] Phase 4.10B — WebSocket Peer Manual Verification (integrated via Dev Tools)
447
+ * Tests complex multi-round/timeline permutations with parallel divergent edits across multiple rounds.
448
+ * Verifies that nested or sequential merges resolve correctly using the optimistic graft strategy.
@@ -325,7 +325,7 @@ export class SyncPeerInnerspace_V1 extends SyncPeer_V1<ConnectSyncPeerInnerspace
325
325
  if (!msgResponse.data) { throw new Error(`(UNEXPECTED) sync saga message ibgib.data falsy? (E: 61ec18743988ad3cbab2072d1dd69826)`); }
326
326
 
327
327
  const responsePayloadIbGibsControl = [
328
- msgResponse, responseCtx.sagaFrame, context
328
+ msgResponse, responseCtx.sagaFrame, responseCtx
329
329
  ].map(x => toDto({ ibGib: x }));
330
330
  // ...put into sender's durable space
331
331
  await putInSpace({
@@ -198,7 +198,17 @@ export interface SyncPeerWitness<TInitializeOpts extends InitializeSyncPeerOpts
198
198
  // getSenderIdentity(): KeystoneIbGib_V1 | undefined;
199
199
 
200
200
  /**
201
- * Evolves the session identity (S_n -> S_n+1) and signs targeting contextAddr.
201
+ * Evolves the session identity (S_n -> S_n+1) and signs targeting
202
+ * contextAddr for the **sync** verb. This is in contrast to signing for
203
+ * the "connect" verb, for which you use {@link signContextConnect}
202
204
  */
203
205
  signContext(opts: { contextAddr: IbGibAddr }): Promise<KeystoneIbGib_V1 | undefined>;
206
+
207
+ /**
208
+ * Evolves the session identity (S_n -> S_n+1) solving the demanded connect
209
+ * challenges. This is NOT for signing session when doing the ping pong of
210
+ * sync protocol. For that, you use {@link signContext}
211
+ */
212
+ signContextConnect(opts: { challengeUuid: string, demandedIds: string[] }): Promise<KeystoneIbGib_V1 | undefined>;
213
+
204
214
  }
@@ -23,7 +23,7 @@ import { getFullSyncSagaHistory, deriveSessionSecret } from '../sync-helpers.mjs
23
23
  import { SessionGenesisFrameDetails, SyncSagaFrameDependencyGraph } from '../sync-types.mjs';
24
24
  import { KeystoneService_V1 } from '../../keystone/keystone-service-v1.mjs';
25
25
  import { KeystoneIbGib_V1 } from '../../keystone/keystone-types.mjs';
26
- import { KEYSTONE_VERB_SYNC, } from '../../keystone/keystone-constants.mjs';
26
+ import { KEYSTONE_VERB_CONNECT, KEYSTONE_VERB_SYNC, POOL_ID_CONNECT, } from '../../keystone/keystone-constants.mjs';
27
27
  import { getTjpAddr } from '../../common/other/ibgib-helper.mjs';
28
28
  import { IbGibAddr } from '@ibgib/ts-gib/dist/types.mjs';
29
29
 
@@ -99,6 +99,52 @@ export abstract class SyncPeer_V1<
99
99
  }
100
100
  }
101
101
 
102
+ /**
103
+ * Evolves the session identity (S_n -> S_n+1) solving the demanded connect challenges.
104
+ */
105
+ public async signContextConnect({
106
+ challengeUuid,
107
+ demandedIds,
108
+ }: {
109
+ challengeUuid: string;
110
+ demandedIds: string[];
111
+ }): Promise<KeystoneIbGib_V1 | undefined> {
112
+ const lc = `${this.lc}[${this.signContextConnect.name}]`;
113
+ try {
114
+ if (!this.currentSessionIdentity) {
115
+ return undefined;
116
+ }
117
+ if (!this.opts) { throw new Error(`opts not initialized. (E: bcf5978aed789b0ebcbdc51971ebe826)`); }
118
+ const { fnSenderSecret, sagaId, localMetaspace, localSpace } = this.opts;
119
+ if (!fnSenderSecret) { throw new Error(`fnSenderSecret not initialized. (E: 207fd292a2e8c53c05fd0a74a4ae6d26)`); }
120
+ if (!sagaId) { throw new Error(`sagaId not initialized. (E: f2e35cc13ed873b638116188119d1826)`); }
121
+
122
+ const senderSecret = await fnSenderSecret();
123
+ const sessionSecret = await deriveSessionSecret({ senderSecret, sagaId });
124
+
125
+ const keystoneSvc = new KeystoneService_V1();
126
+ const evolved = await keystoneSvc.sign({
127
+ latestKeystone: this.currentSessionIdentity,
128
+ masterSecret: sessionSecret,
129
+ poolId: POOL_ID_CONNECT,
130
+ requiredChallengeIds: demandedIds,
131
+ claim: {
132
+ verb: KEYSTONE_VERB_CONNECT,
133
+ target: challengeUuid,
134
+ },
135
+ metaspace: localMetaspace,
136
+ space: localSpace,
137
+ });
138
+
139
+ this.currentSessionIdentity = evolved;
140
+ return evolved;
141
+ } catch (error) {
142
+ console.error(`${lc} ${extractErrorMsg(error)}`);
143
+ throw error;
144
+ }
145
+ }
146
+
147
+
102
148
  get classname(): string {
103
149
  if (!this.data) { throw new Error(`(UNEXPECTED) this.data falsy? (E: 1ab1841e9338b54f3aa615fa37024826)`); }
104
150
  if (!this.data.classname) { throw new Error(`invalid peer. this.data.classname is falsy (E: b0ee28a0abb84a06588d9de7afcef826)`); }
@@ -0,0 +1,42 @@
1
+ # WebSocket Sync Peers
2
+
3
+ This directory contains the WebSocket client (sender) and server (receiver) peer implementations.
4
+
5
+ ## WebSocket Resource Management & Teardown
6
+
7
+ Since WebSockets manage active OS-level TCP connections, they must be cleanly closed to prevent socket descriptor leaks on the client and resource exhaustion (DoS) on the server.
8
+
9
+ ### Analysis (2026-06-12T09:45:21-05:00)
10
+
11
+ Our analysis highlights several areas where WebSocket connections are currently left open:
12
+
13
+ 1. **Sender Connect Failures**: If connection handshake fails or times out, the client rejects the promise but does not close the socket.
14
+ 2. **Sender Runtime Failures**: If runtime message validation fails, we throw an error but leave the socket open.
15
+ 3. **Sender Saga Completion**: When the sync loop finishes successfully, the socket is left open indefinitely because there is no `disconnect` method.
16
+ 4. **Receiver Message Handling Failures**: If incoming messages fail validation/authentication, the server sends a `sync_error` or `auth_fail` frame, but relies on the client to close the connection. A buggy/malicious client can leave it open forever.
17
+
18
+ #### Design Questions & Ideas
19
+ * **Idempotent `disconnect()`**: The disconnect method on the sender should clear `this.handshakeMessageListener` and set it to `undefined` so that multiple calls to `disconnect()` are safe.
20
+ * **Reuse `disconnect()`**: In connection failure handlers (like `handleHandshakeAuthFail`), we can call `this.disconnect()` to ensure uniform cleanup.
21
+ * **Server-side Close on Error (best-effort `send`)**: If a message fails, we want to notify the client first and then close the connection. If the `send()` itself throws, we want a safe cleanup. In JavaScript, we can nest `try..catch..finally` inside a `catch` block, or use a short `setTimeout` to push the socket close operation to the next event loop tick, ensuring the outgoing frame is written before the socket is terminated.
22
+
23
+ ---
24
+
25
+ ### Implementation TODOs
26
+
27
+ - [x] **Implement `disconnect` method on `SyncPeerWebSocketSender_V1`**
28
+ - Implement a synchronous or asynchronous `disconnect()` method.
29
+ - Safely remove the message event listener `this.handshakeMessageListener`.
30
+ - Set `this.handshakeMessageListener = undefined` to make it idempotent.
31
+ - Close the WebSocket `this.ws.close()`.
32
+
33
+ - [x] **Clean up socket on Sender connection failures**
34
+ - Call `this.disconnect()` inside `handleHandshakeAuthFail`.
35
+ - Call `this.disconnect()` inside `handleHandshakeSyncError`.
36
+
37
+ - [x] **Close socket on Sender runtime message validation failures**
38
+ - In `handleRuntimeMessage` and `handleRuntimeSyncFrameResponse`, catch validation errors, trigger `this.disconnect()`, and rethrow.
39
+
40
+ - [x] **Close socket on Receiver message handling failures**
41
+ - In the catch block of `handleIncomingMessage`, call `this.socketWrapper?.close()` (or equivalent wrapper cleanup) to force-close connections after sending error frames.
42
+ - Wrap the `send()` call in a nested try/catch or use `setTimeout` to ensure best-effort message transmission before terminating the socket wrapper.
@@ -34,6 +34,7 @@ export interface IWebSocketWrapper {
34
34
  send(data: string): void;
35
35
  onMessage(callback: (data: string) => void): void;
36
36
  onClose(callback: () => void): void;
37
+ close(): void;
37
38
  }
38
39
 
39
40
  /**
@@ -213,7 +214,8 @@ export class SyncPeerWebSocketReceiver_V1
213
214
  // First, validate and authenticate the context
214
215
  const allControlIbGibs: IbGib_V1[] = [
215
216
  toDto({ ibGib: context }),
216
- context.sagaFrame
217
+ context.sagaFrame,
218
+ context.sagaFrameMsg
217
219
  ];
218
220
  if (context.signedSessionIdentity) {
219
221
  allControlIbGibs.push(context.signedSessionIdentity);
@@ -262,10 +264,16 @@ export class SyncPeerWebSocketReceiver_V1
262
264
  }
263
265
  } catch (error) {
264
266
  console.error(`${lc} message frame handling failed: ${extractErrorMsg(error)}`);
265
- this.socketWrapper?.send(JSON.stringify({
266
- type: this.isAuthenticated ? SyncWebSocketMsgType.sync_error : SyncWebSocketMsgType.auth_fail,
267
- message: extractErrorMsg(error)
268
- }));
267
+ try {
268
+ this.socketWrapper?.send(JSON.stringify({
269
+ type: this.isAuthenticated ? SyncWebSocketMsgType.sync_error : SyncWebSocketMsgType.auth_fail,
270
+ message: extractErrorMsg(error)
271
+ }));
272
+ } catch (nestedError) {
273
+ console.error(`${lc}[nested catch] failed to send error frame: ${extractErrorMsg(nestedError)}`);
274
+ } finally {
275
+ this.socketWrapper?.close();
276
+ }
269
277
  }
270
278
  }
271
279
 
@@ -327,6 +335,7 @@ export class SyncPeerWebSocketReceiver_V1
327
335
 
328
336
  // Persist the newly validated evolved session keystone tip
329
337
  await metaspace.put({ ibGibs: [proofFrame], space });
338
+ await metaspace.registerNewIbGib({ ibGib: proofFrame, space });
330
339
 
331
340
  if (logalot) { console.log(`${lc} connect validation successful! Connection upgraded to active sync session.`); }
332
341
  this.isAuthenticated = true;