@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.
- package/CHANGELOG.md +9 -1
- package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs +1 -1
- package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-types.d.mts +12 -1
- package/dist/sync/sync-peer/sync-peer-types.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-v1.d.mts +7 -0
- package/dist/sync/sync-peer/sync-peer-v1.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-v1.mjs +43 -1
- package/dist/sync/sync-peer/sync-peer-v1.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.d.mts +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mjs +15 -5
- package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.d.mts +16 -0
- package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs +223 -79
- package/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs.map +1 -1
- package/dist/sync/sync-saga-context/sync-saga-context-helpers.d.mts.map +1 -1
- package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs +41 -2
- package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs.map +1 -1
- package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts +4 -0
- package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts.map +1 -1
- package/dist/sync/sync-saga-coordinator.d.mts +6 -0
- package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
- package/dist/sync/sync-saga-coordinator.mjs +57 -1
- package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
- package/dist/sync/sync-withid.pingpong.respec.mjs +68 -0
- package/dist/sync/sync-withid.pingpong.respec.mjs.map +1 -1
- package/package.json +1 -1
- package/src/sync/docs/security-3b.md +92 -0
- package/src/sync/docs/security.md +107 -39
- package/src/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mts +1 -1
- package/src/sync/sync-peer/sync-peer-types.mts +11 -1
- package/src/sync/sync-peer/sync-peer-v1.mts +47 -1
- package/src/sync/sync-peer/sync-peer-websocket/README.md +42 -0
- package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mts +14 -5
- package/src/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mts +242 -78
- package/src/sync/sync-saga-context/sync-saga-context-helpers.mts +46 -4
- package/src/sync/sync-saga-context/sync-saga-context-types.mts +5 -0
- package/src/sync/sync-saga-coordinator.mts +69 -1
- package/src/sync/sync-withid.pingpong.respec.mts +74 -1
- 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.
|
|
289
|
-
|
|
290
|
-
- [
|
|
291
|
-
- [
|
|
292
|
-
- [
|
|
293
|
-
- [
|
|
294
|
-
- [
|
|
295
|
-
- [
|
|
296
|
-
- [
|
|
297
|
-
- [
|
|
298
|
-
- [
|
|
299
|
-
- [
|
|
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
|
-
- [
|
|
304
|
-
- [
|
|
305
|
-
- [
|
|
306
|
-
- [
|
|
307
|
-
- [
|
|
308
|
-
- [
|
|
309
|
-
- [
|
|
310
|
-
- [
|
|
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
|
-
- [
|
|
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
|
-
- [
|
|
328
|
-
- [
|
|
329
|
-
- [
|
|
330
|
-
- [
|
|
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]
|
|
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
|
-
- [
|
|
366
|
-
- [
|
|
367
|
-
- [
|
|
368
|
-
- [
|
|
369
|
-
- [
|
|
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
|
-
- [
|
|
374
|
-
- [
|
|
375
|
-
- [
|
|
376
|
-
- [
|
|
377
|
-
- [
|
|
378
|
-
- [
|
|
379
|
-
- [
|
|
380
|
-
- [
|
|
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,
|
|
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
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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;
|