@ibgib/core-gib 0.1.55 → 0.1.58
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 +1 -0
- package/dist/keystone/keystone-config-builder.d.mts +12 -1
- package/dist/keystone/keystone-config-builder.d.mts.map +1 -1
- package/dist/keystone/keystone-config-builder.mjs +58 -4
- package/dist/keystone/keystone-config-builder.mjs.map +1 -1
- package/dist/keystone/keystone-constants.d.mts +40 -5
- package/dist/keystone/keystone-constants.d.mts.map +1 -1
- package/dist/keystone/keystone-constants.mjs +39 -5
- package/dist/keystone/keystone-constants.mjs.map +1 -1
- package/dist/keystone/keystone-helpers.d.mts +11 -1
- package/dist/keystone/keystone-helpers.d.mts.map +1 -1
- package/dist/keystone/keystone-helpers.mjs +37 -1
- package/dist/keystone/keystone-helpers.mjs.map +1 -1
- package/dist/keystone/keystone-policy-types.d.mts +23 -0
- package/dist/keystone/keystone-policy-types.d.mts.map +1 -0
- package/dist/keystone/keystone-policy-types.mjs +2 -0
- package/dist/keystone/keystone-policy-types.mjs.map +1 -0
- package/dist/sync/graft-info/graft-info-helpers.respec.mjs +8 -8
- package/dist/sync/graft-info/graft-info-helpers.respec.mjs.map +1 -1
- package/dist/sync/sync-conflict-adv-multitimelines.respec.mjs +22 -22
- package/dist/sync/sync-conflict-adv-multitimelines.respec.mjs.map +1 -1
- package/dist/sync/sync-conflict-basic-divergence.respec.mjs +3 -3
- package/dist/sync/sync-conflict-basic-divergence.respec.mjs.map +1 -1
- package/dist/sync/sync-conflict-basic-multitimelines.respec.mjs +6 -6
- package/dist/sync/sync-conflict-basic-multitimelines.respec.mjs.map +1 -1
- package/dist/sync/sync-conflict-text-merge.respec.mjs +26 -26
- package/dist/sync/sync-conflict-text-merge.respec.mjs.map +1 -1
- package/dist/sync/sync-helpers.d.mts +19 -0
- package/dist/sync/sync-helpers.d.mts.map +1 -1
- package/dist/sync/sync-helpers.mjs +51 -1
- package/dist/sync/sync-helpers.mjs.map +1 -1
- package/dist/sync/sync-innerspace-constants.respec.mjs +2 -2
- package/dist/sync/sync-innerspace-constants.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs +2 -2
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-dest-ahead.respec.mjs +4 -4
- package/dist/sync/sync-innerspace-dest-ahead.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-multiple-timelines.respec.mjs +2 -2
- package/dist/sync/sync-innerspace-multiple-timelines.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-partial-update.respec.mjs +3 -3
- package/dist/sync/sync-innerspace-partial-update.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace.respec.mjs +4 -4
- package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-http-receiver/sync-peer-http-receiver-v1.d.mts +5 -0
- package/dist/sync/sync-peer/sync-peer-http-receiver/sync-peer-http-receiver-v1.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-http-receiver/sync-peer-http-receiver-v1.mjs +18 -0
- package/dist/sync/sync-peer/sync-peer-http-receiver/sync-peer-http-receiver-v1.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-http-sender/sync-peer-http-sender-v1.d.mts +5 -0
- package/dist/sync/sync-peer/sync-peer-http-sender/sync-peer-http-sender-v1.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-http-sender/sync-peer-http-sender-v1.mjs +21 -3
- package/dist/sync/sync-peer/sync-peer-http-sender/sync-peer-http-sender-v1.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.d.mts +12 -0
- package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs +34 -0
- 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 +69 -1
- package/dist/sync/sync-peer/sync-peer-types.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-v1.d.mts +30 -0
- package/dist/sync/sync-peer/sync-peer-v1.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-v1.mjs +88 -1
- package/dist/sync/sync-peer/sync-peer-v1.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-types.d.mts +30 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-types.d.mts.map +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-types.mjs +2 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-types.mjs.map +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.d.mts +66 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.d.mts.map +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mjs +280 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mjs.map +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-websocket-peer-helpers.d.mts +85 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-websocket-peer-helpers.d.mts.map +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs +332 -0
- package/dist/sync/sync-peer/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs.map +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-types.d.mts +29 -0
- package/dist/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-types.d.mts.map +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-types.mjs +2 -0
- package/dist/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-types.mjs.map +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.d.mts +42 -0
- package/dist/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.d.mts.map +1 -0
- package/dist/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs +282 -0
- package/dist/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mjs.map +1 -0
- package/dist/sync/sync-saga-coordinator.d.mts +35 -1
- package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
- package/dist/sync/sync-saga-coordinator.mjs +62 -1
- package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
- package/dist/sync/sync-withid.connect.respec.d.mts +12 -0
- package/dist/sync/sync-withid.connect.respec.d.mts.map +1 -0
- package/dist/sync/sync-withid.connect.respec.mjs +205 -0
- package/dist/sync/sync-withid.connect.respec.mjs.map +1 -0
- package/dist/sync/sync-withid.establish.respec.d.mts +19 -0
- package/dist/sync/sync-withid.establish.respec.d.mts.map +1 -0
- package/dist/sync/sync-withid.establish.respec.mjs +322 -0
- package/dist/sync/sync-withid.establish.respec.mjs.map +1 -0
- package/package.json +1 -1
- package/src/keystone/keystone-config-builder.mts +73 -4
- package/src/keystone/keystone-constants.mts +42 -6
- package/src/keystone/keystone-helpers.mts +44 -2
- package/src/keystone/keystone-policy-types.mts +25 -0
- package/src/keystone/keystone-policy.schema.json +51 -0
- package/src/keystone/keystone-service-v1.mts +3 -3
- package/src/sync/README.md +1 -104
- package/src/sync/docs/architecture.md +28 -8
- package/src/sync/docs/security.md +380 -0
- package/src/sync/graft-info/graft-info-helpers.respec.mts +7 -7
- package/src/sync/sync-conflict-adv-multitimelines.respec.mts +21 -21
- package/src/sync/sync-conflict-basic-divergence.respec.mts +2 -2
- package/src/sync/sync-conflict-basic-multitimelines.respec.mts +5 -5
- package/src/sync/sync-conflict-text-merge.respec.mts +25 -25
- package/src/sync/sync-helpers.mts +51 -1
- package/src/sync/sync-innerspace-constants.respec.mts +1 -1
- package/src/sync/sync-innerspace-deep-updates.respec.mts +1 -1
- package/src/sync/sync-innerspace-dest-ahead.respec.mts +3 -3
- package/src/sync/sync-innerspace-multiple-timelines.respec.mts +1 -1
- package/src/sync/sync-innerspace-partial-update.respec.mts +2 -2
- package/src/sync/sync-innerspace.respec.mts +3 -3
- package/src/sync/sync-peer/sync-peer-http-receiver/sync-peer-http-receiver-v1.mts +20 -0
- package/src/sync/sync-peer/sync-peer-http-sender/sync-peer-http-sender-v1.mts +23 -3
- package/src/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mts +38 -1
- package/src/sync/sync-peer/sync-peer-types.mts +70 -1
- package/src/sync/sync-peer/sync-peer-v1.mts +94 -1
- package/src/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-types.mts +36 -0
- package/src/sync/sync-peer/sync-peer-websocket-receiver/sync-peer-websocket-receiver-v1.mts +337 -0
- package/src/sync/sync-peer/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mts +388 -0
- package/src/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-types.mts +35 -0
- package/src/sync/sync-peer/sync-peer-websocket-sender/sync-peer-websocket-sender-v1.mts +321 -0
- package/src/sync/sync-saga-coordinator.mts +84 -0
- package/src/sync/sync-withid.connect.respec.mts +243 -0
- package/src/sync/sync-withid.establish.respec.mts +361 -0
- package/src/sync/unused-identity-backup.mts.md +1 -1
- package/dist/sync/sync-innerspace-dest-ahead-withid.respec.d.mts +0 -2
- package/dist/sync/sync-innerspace-dest-ahead-withid.respec.d.mts.map +0 -1
- package/dist/sync/sync-innerspace-dest-ahead-withid.respec.mjs +0 -310
- package/dist/sync/sync-innerspace-dest-ahead-withid.respec.mjs.map +0 -1
- package/src/sync/sync-innerspace-dest-ahead-withid.respec.mts +0 -364
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# Ibgib Sync Security
|
|
2
|
+
|
|
3
|
+
We use ibgib's unique Merkle-DAG-based protocol and innovative "keystone" construct (see keystone [README.md](../../keystone/README.md) and other [docs](../../keystone/docs/)) to maximize security while minimizing surface area (code).
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
* **Domain Keystone (I)**: The primary, long-lived identity keystone representing the data owner (e.g., Alice). This dictates high-level access and is registered with a domain provider (e.g., Space-Gib as an initial reference implementation).
|
|
8
|
+
* **Session Keystone (S)**: An ephemeral, short-lived keystone generated locally by the Sender explicitly for a single sync session.
|
|
9
|
+
* **Target Domain (X)**: The specific ibgib timeline or payload that is the subject of the sync.
|
|
10
|
+
* **Sovereign Broker (Receiver)**: The domain provider acts as a passive, high-integrity validator. It does not generate or sign sync frames with its own session identity; it relies entirely on the Sender's cryptographic proofs to authorize actions. While we may treat the provider as "honest" practically, it is ultimately a sovereign entity over its data, and its "honesty" is driven by long-term self-interest.
|
|
11
|
+
|
|
12
|
+
## Development Nuances & Implementation TODOs
|
|
13
|
+
|
|
14
|
+
This section serves as a working scratchpad to capture specific code-level nuances, terminology standardization, and pending tasks before jumping into implementation.
|
|
15
|
+
|
|
16
|
+
### ✅ Agreed: Secret Naming — `senderSecret` & `sessionSecret`
|
|
17
|
+
|
|
18
|
+
* **`senderSecret`**: The master secret corresponding to `senderIdentity`. Drives the KDF to produce the session secret. Replaces the old `nonSessionSecret` / `identitySecret` / `domainSecret` names.
|
|
19
|
+
* **`sessionSecret`**: Deterministically derived via `KDF(senderSecret, sagaId)`. Ephemeral and saga-specific. Used only within `establishSessionIdentity` to create the session keystone genesis.
|
|
20
|
+
* *Rationale*: `sender` pairs naturally with `senderIdentity`; `session` pairs naturally with `sessionIdentity`.
|
|
21
|
+
|
|
22
|
+
### ✅ Agreed: Identity Naming — `senderIdentity` & `sessionIdentity`
|
|
23
|
+
|
|
24
|
+
* **`senderIdentity`**: The long-lived Domain Keystone (I) representing the Sender (Alice). Optional — a sync can run with just a `domainSecret` and no named identity.
|
|
25
|
+
* Using `senderIdentity` (not `domainIdentity` or `primaryIdentity`) because "domain" becomes ambiguous when Bob eventually has his own domain identity in a future symmetric model.
|
|
26
|
+
* **`sessionIdentity`**: The ephemeral keystone (S) generated per-saga. Used consistently as both:
|
|
27
|
+
* A **param/property name** (e.g., `peer.sessionIdentity`, `createSessionIdentity()`).
|
|
28
|
+
* A **rel8n name** on both the sync saga ibgib and the context ibgib (e.g., `syncSagaIbGib.rel8ns.sessionIdentity`, `contextIbGib.rel8ns.sessionIdentity`).
|
|
29
|
+
|
|
30
|
+
### ✅ Agreed: Identity Hard-linking Strategy
|
|
31
|
+
|
|
32
|
+
* **Sync Saga Frame** (`ibgib.data`, soft-link):
|
|
33
|
+
* `data` should record identity details as soft references (addresses in `ibgib.data`, not hard-linked `ibgib.rel8ns`):
|
|
34
|
+
* `senderIdentity` TJP addr.
|
|
35
|
+
* The exact `senderIdentity` frame addr that signed/authorized the session (i.e., the new frame post-`establishSessionIdentity`).
|
|
36
|
+
* `sessionIdentity` TJP addr.
|
|
37
|
+
* No hard-link (`rel8ns`) to `sessionIdentity` from the sync saga ibgib.
|
|
38
|
+
* **Context IbGib** (`ibgib.rel8ns`, hard-link):
|
|
39
|
+
* `rel8ns.sessionIdentity` hard-links to the **previous** session keystone frame (the frame *before* the current signing step). This is the "point forward, sign backward" pattern: the signed (evolved) keystone's claim targets *this context's addr*, so the context can only reference the *prior* frame.
|
|
40
|
+
* The `signedSessionKeystone` property on the context carries the **newly evolved** frame (the current turn's signature).
|
|
41
|
+
* **No transition pool**: The prior symmetric "transition pool" (for the receiver's signing turn) is eliminated. Only the Sender signs.
|
|
42
|
+
* **Sovereign Broker response**: The Receiver includes the *same, unevolved* `sessionIdentity` addr in its response context's `rel8ns.sessionIdentity` — it does not evolve the keystone.
|
|
43
|
+
|
|
44
|
+
### ✅ Agreed: Session Keystone Pool Configuration
|
|
45
|
+
|
|
46
|
+
The session keystone has **two dedicated pools**, each with a matching `poolId` and `verb`:
|
|
47
|
+
|
|
48
|
+
* **`connect` pool** (`poolId: "connect"`, `verb: "connect"`):
|
|
49
|
+
* Used exclusively during `peer.connect()` — the in-band WebSocket challenge/response handshake.
|
|
50
|
+
* The receiver issues challenges from this pool; the sender solves them to prove possession of S.
|
|
51
|
+
* **`sync` pool** (`poolId: "sync"`, `verb: "sync"`):
|
|
52
|
+
* Used to sign each outgoing context frame during the sync ping-pong (Init, Ack, Delta, Commit turns).
|
|
53
|
+
* Pool separation is intentional — the `connect` handshake and per-turn signing are distinct operations with distinct lifetimes.
|
|
54
|
+
* [ ] Add `"connect"` and `"sync"` verbs to `keystone-constants.mts`.
|
|
55
|
+
* [ ] Create standard pool config factories for both in [`keystone-config-builder.mts`](../../keystone/keystone-config-builder.mts).
|
|
56
|
+
|
|
57
|
+
Note: The `senderIdentity` Domain Keystone also uses the `"sync"` verb in its claim when it signs/authorizes the session keystone genesis during `establishSessionIdentity`.
|
|
58
|
+
|
|
59
|
+
### ✅ Agreed: Sync Peer Lifetime — One Peer per Saga
|
|
60
|
+
|
|
61
|
+
A `SyncPeer` instance is **scoped to exactly one sync saga**. A new peer must be created for each call to `senderCoordinator.sync(...)`. Reusing a peer across multiple sagas is not supported and would compromise the identity security model, since the session keystone `S` is saga-specific (derived from `KDF(senderSecret, sagaId)`).
|
|
62
|
+
|
|
63
|
+
The `sagaId` is passed to the peer via `initializeOpts` so that `establishSessionIdentity` can derive the correct `sessionSecret` before any other phase begins.
|
|
64
|
+
|
|
65
|
+
### ✅ Agreed: `establishSessionIdentity` — Pre-Connect Phase
|
|
66
|
+
|
|
67
|
+
A new `establishSessionIdentity` method on the sync peer base class is the **mandatory first step** before `connect`. It encapsulates:
|
|
68
|
+
|
|
69
|
+
1. **Generate session keystone**: `sessionIdentity` genesis (S^Stjp) is created locally from `KDF(senderSecret, sagaId)`. The genesis contains the exact target domain addresses (`targetAddrs` in `frameDetails`) to cryptographically bind the session to those specific target domain timelines, preventing it from being used as a "blank check" for other domains.
|
|
70
|
+
2. **Sign `senderIdentity`**: The Sender signs their own `senderIdentity` keystone with a `sync` claim targeting `S^Stjp`. The result (`newSenderIdentity`) is the evolved sender frame that proves delegation. The name `newSenderIdentity` is **only** used within `establishSessionIdentity` and its helper — after this method returns, it becomes the active `senderIdentity`.
|
|
71
|
+
3. **Post to domain provider**: Both `newSenderIdentity` and `sessionIdentity` genesis are transmitted to the Receiver via a pre-connect API call (cf. `putEvolveKeystone` in `dev-tools.mts`).
|
|
72
|
+
4. **Receiver validates**: The Receiver independently loads the **latest known tip** of `I^Itjp` from its own registry (never trusting `newSenderIdentity.rel8ns.past`). It validates the evolution and, if authorized, stores both keystones in the domain.
|
|
73
|
+
5. **After this point**: `senderIdentity` IS the new frame. The session keystone takes over authorization for all subsequent sync turns.
|
|
74
|
+
|
|
75
|
+
### ✅ Agreed: Per-Turn Transmission Protocol
|
|
76
|
+
|
|
77
|
+
* **Sender → Receiver (each turn)**: Only the **current evolved frame of `sessionIdentity`** (S) is transmitted. `senderIdentity` is never re-transmitted after `establishSessionIdentity`.
|
|
78
|
+
* **Receiver validation (each turn)**: The Receiver independently loads the latest `senderIdentity` tip from its domain registry, finds the `S^Stjp` addr from the `sync` claim within it, loads the known session keystone graph, and validates the incoming evolved S frame against it.
|
|
79
|
+
* This is possible on every turn because `establishSessionIdentity` guarantees the domain and `I^Itjp` already exist on the Receiver.
|
|
80
|
+
* **Receiver → Sender (each turn)**: The Receiver's response context echoes the same (unevolved) `sessionIdentity` addr — it never produces a new keystone evolution.
|
|
81
|
+
|
|
82
|
+
### 🔲 Pending: `authenticateContext` Placement
|
|
83
|
+
|
|
84
|
+
The receiver-side validation that runs on each incoming context has three steps:
|
|
85
|
+
1. **Transition validity**: Replay the session keystone evolution — verify `data.n` is sequential, challenge solutions are valid, pool not exhausted, etc.
|
|
86
|
+
2. **Target binding**: Verify that the incoming `signedSessionIdentity`'s proof actually targets *this* context's exact address (`proof.claim.target === contextAddr`).
|
|
87
|
+
3. **Domain target binding**: Verify that the first frame of the session keystone `S` ($S^{Stjp}$) contains `targetAddrs` (array) in its `frameDetails`, and that this array contains the exact addresses of the domain ibgibs being synchronized. This must be checked at least in the first sync turn (e.g., `handleInitFrame`) to authenticate that the session is not a "blank check".
|
|
88
|
+
|
|
89
|
+
**Open**: Where does this code live?
|
|
90
|
+
* **On the sync peer** (e.g., as `peer.authenticateContext(...)`): the peer already knows its local durable space, which is needed to load the latest `senderIdentity` tip. This makes the tip-lookup natural.
|
|
91
|
+
* **As a pure helper function** (`sync-peer-helpers.mts`): would need the durable space passed as a parameter (plus whatever else the tip-lookup requires).
|
|
92
|
+
|
|
93
|
+
The peer placement is currently preferred since it co-locates the space knowledge with the validation logic. Finalize during implementation.
|
|
94
|
+
|
|
95
|
+
### ✅ Agreed: `KeystoneService_V1` — No Injection Needed
|
|
96
|
+
|
|
97
|
+
`KeystoneService_V1` is stateless. It can be `new`-ed on demand wherever validation or signing is needed. There is no need to pass it as a constructor parameter or inject it into `authenticateContext` or any related helper. Just instantiate inline.
|
|
98
|
+
|
|
99
|
+
### ✅ Agreed: Context Property — `signedSessionIdentity`
|
|
100
|
+
|
|
101
|
+
The context ibgib carries the newly evolved `sessionIdentity` frame in a property named **`signedSessionIdentity`** (replacing the old `signedSessionKeystone`).
|
|
102
|
+
|
|
103
|
+
Context ibgib now has a clean, consistent pair:
|
|
104
|
+
* `rel8ns.sessionIdentity` → addr of the **previous** frame (before signing this turn)
|
|
105
|
+
* `signedSessionIdentity` → the **newly evolved** frame (current turn's cryptographic proof)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
## High Level Authorization Flow
|
|
110
|
+
|
|
111
|
+
1. [ ] **`establishSessionIdentity`** *(new pre-connect phase)*:
|
|
112
|
+
[ ] * Sender locally generates `sessionIdentity` (S) and signs `senderIdentity` (I → I1) with a `sync` claim targeting `S^Stjp`.
|
|
113
|
+
[ ] * Both keystones are posted to the domain provider. Receiver validates the evolution against its own known tip of `I^Itjp` and stores them if authorized.
|
|
114
|
+
2. [ ] **`peer.connect()`** *(transport handshake)*:
|
|
115
|
+
[ ] * Sender opens the transport channel (e.g., WebSocket).
|
|
116
|
+
[ ] * Receiver issues challenges from S's **`connect` pool**.
|
|
117
|
+
[ ] * Sender solves and returns proof-of-possession. Session is authorized.
|
|
118
|
+
3. [ ] **Asymmetric Sync (Ping-Pong)**:
|
|
119
|
+
[ ] * Sender signs all outgoing contexts (Init, Ack, Delta, Commit) using S's **`sync` pool**, evolving S each turn.
|
|
120
|
+
[ ] * Only the latest evolved S frame is transmitted per turn — never `senderIdentity`.
|
|
121
|
+
[ ] * Receiver validates by independently loading `I^Itjp`, tracing to `S^Stjp`, and verifying the incoming frame.
|
|
122
|
+
[ ] * Receiver responds with unsigned contexts (same S addr echoed, no new evolution).
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## Attack Vectors & Mitigations
|
|
126
|
+
|
|
127
|
+
| Attack Vector | Description | Mitigation |
|
|
128
|
+
|---------------|-------------|------------|
|
|
129
|
+
| **MITM Replay** | Attacker replays captured saga frames | • Frame signatures include timestamp<br>• Freshness checks reject old frames<br>• Challenge depletion prevents reuse<br>• sagaId deduplication |
|
|
130
|
+
| **Session Fixation** | Attacker pre-generates session keystone | • Session secret derived via KDF (strategy in keystone metadata)<br>• `sessionSecret = KDF(masterSecret, sagaId, strategy)` |
|
|
131
|
+
| **Identity Spoofing** | Impersonation via fake session keystone | • Session keystone includes `derivedFrom` master address<br>• Receiver validates master keystone is known/trusted |
|
|
132
|
+
| **Challenge Grinding** | Brute-force challenge solutions for target binding | • Hash pre-imaging computationally infeasible<br>• Stochastic selection adds randomness |
|
|
133
|
+
| **DoS (Pool Exhaustion)** | Excessive syncs deplete challenge pools | • Top-up replenishment refills challenges<br>• Rate limiting per identity<br>• Pool separation for critical ops |
|
|
134
|
+
| **Saga Hijacking** | Mid-flight frame injection | • All frames signed with session keystone<br>• `past` rel8n creates tamper-evident chain |
|
|
135
|
+
|
|
136
|
+
## Trust Model
|
|
137
|
+
|
|
138
|
+
**Asymmetric Identity (Domain-Based Authorization)**:
|
|
139
|
+
- **Sender Signs**: The Sender is responsible for cryptographically signing all requests using their ephemeral Session Keystone.
|
|
140
|
+
- **Receiver Validates**: The Receiver acts strictly as an "Honest Broker". It validates the Sender's proofs and domain boundaries but does not maintain or sign a symmetric session identity.
|
|
141
|
+
- **Delegated Authority**: Trust is explicitly delegated from a Domain Keystone (I) to a Session Keystone (S) via a `sync` claim evolution.
|
|
142
|
+
|
|
143
|
+
**Session Keystones**:
|
|
144
|
+
- Ephemeral identity per sync saga.
|
|
145
|
+
- Secret derived via `KDF(senderSecret, sagaId)`.
|
|
146
|
+
- Contains two isolated pools: `connect` (for transport handshake) and `sync` (for per-turn frame signing).
|
|
147
|
+
- Validates via proof-of-work (solving challenges from the appropriate pool).
|
|
148
|
+
|
|
149
|
+
**Propagation**:
|
|
150
|
+
- Sync does NOT rely on global PKI or certificate authorities.
|
|
151
|
+
- Trust propagates through sync sessions (Alice syncs with Bob → Bob witnesses Alice's keystone).
|
|
152
|
+
- Revocation propagates same way (Alice revokes → syncs revocation to Bob → Bob sees revoked timeline)
|
|
153
|
+
|
|
154
|
+
**Threat Boundaries**:
|
|
155
|
+
- ✅ Protects against: MITM replay, impersonation, frame tampering
|
|
156
|
+
- ⚠️ Does NOT protect against: Compromised endpoint (live memory access during active session)
|
|
157
|
+
- ⚠️ Master secrets: Raw secrets should NOT stored; keystones master secrets to keystones should be derived secrets via key stretching passwords. raw secret files of course can drive these passwords and must be protected
|
|
158
|
+
- ⚠️ Transport security: Use TLS for network layer encryption (ibgib protocol provides authentication/integrity, not confidentiality)
|
|
159
|
+
|
|
160
|
+
## Best Practices
|
|
161
|
+
|
|
162
|
+
1. **Master Secret Protection**: Store master secrets in secure enclaves/keychains
|
|
163
|
+
2. **Session Lifetime**: Limit saga duration, revoke sessions post-completion
|
|
164
|
+
3. **Rate Limiting**: Implement per-identity sync rate limits
|
|
165
|
+
4. **Freshness**: Reject frames older than 60 seconds
|
|
166
|
+
5. **Audit Logs**: Persist saga timelines for post-hoc validation (saga timelines are ibgibs, so integrity is built-in via content addressing)
|
|
167
|
+
6. **TLS**: Always use TLS for network transport (defense-in-depth)
|
|
168
|
+
|
|
169
|
+
## ❓ Open Questions
|
|
170
|
+
|
|
171
|
+
### Payload Scope Validation: Can the Protocol Police What ibGibs Are Transmitted?
|
|
172
|
+
|
|
173
|
+
**Question**: During a sync session authorized by session substone S (itself authorized by domain keystone I with `claim.target = S^Stjp` and sync subject `X^X10.Xtjp`), should the server validate that all Delta payload ibgibs are genuinely related to X's timeline? Or is it sufficient that they arrive under a valid substone?
|
|
174
|
+
|
|
175
|
+
**Context**: The substone's `frameDetails` records both the parent identity `I^Itjp` and the sync subject `X^X10.Xtjp`. In theory the server could walk the dependency graph of each incoming Delta payload and confirm every ibgib is either:
|
|
176
|
+
- A control ibgib (sync saga, context, substone frames), or
|
|
177
|
+
- Part of the dependency graph rooted at some frame of X's timeline (`X^Xtjp`)
|
|
178
|
+
|
|
179
|
+
**Problem**: This check may not be enforceable at the protocol layer. A client who controls the keystone (has the master secret) could always evolve X's timeline to hard-link any arbitrary ibgib, and that ibgib would then pass the "is in X's dependency graph" check. So the constraint can be trivially bypassed by the legitimate key-holder, making it only useful against *illegitimate* actors — who are already blocked by the keystone proof requirement.
|
|
180
|
+
|
|
181
|
+
**Current position**: This is likely a **higher-layer business rule** rather than a sync protocol concern. The sync protocol's job is to ensure the session is cryptographically authorized (via keystone proofs). What is stored under an authorized session is the domain owner's responsibility. Enforcement of content policies (e.g., "only ibgibs of type X are allowed") belongs in the server's `authorizeContext` hook, not in the core sync coordinator.
|
|
182
|
+
|
|
183
|
+
**Tracked in**: `space-gib.sync-walkthrough.md`
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Implementation Plan
|
|
188
|
+
|
|
189
|
+
Each phase below is a discrete, verifiable step. We proceed **one checkbox at a time**, **reviewing/discussing** code/results before moving to the next. Each phase has two targets: **innerspace** (automated unit test) and **space-gib** (manual/integrated e2e).
|
|
190
|
+
|
|
191
|
+
The innerspace tests live in `libs/core-gib/src/sync/`. The space-gib integrated tests are done via the `dev-tools.mts` UI in the browser. Separating identity phases into their own `*.respec.mts` file keeps logs clean and complexity isolated.
|
|
192
|
+
|
|
193
|
+
_Note: respec-gib `*.respec.mts` files execute the **entire** file if any `respecfullyDear`/`ifWeMight` blocks are found. Within that file, any `ifWe` block (without the `Might`) will **still** execute and affect logging. `ifWeMight` **only** affects reporting pass/fail once the file is selected to execute. This is why we isolate individual files as needed._
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Phase 1 — `establishSessionIdentity` (Pre-Connect)
|
|
198
|
+
|
|
199
|
+
**Goal**: Get `I^Itjp` onto the domain provider, generate `S^Stjp` locally, evolve `I → I1` with a `sync` claim targeting `S^Stjp`, and post both `I1` and `S` to the provider. Verify both keystones are in the appropriate durable spaces at the right times.
|
|
200
|
+
|
|
201
|
+
`senderCoordinator.sync(...)` **will** be called; we are not mocking this out. We expect it may throw at first, and we are examining side-effects (keystone presence in durable spaces) rather than end-to-end correctness. As phases succeed and sync no longer throws, test assertions will be adjusted accordingly.
|
|
202
|
+
|
|
203
|
+
#### Phase 1A — Innerspace unit test (`sync-withid.establish.respec.mts`)
|
|
204
|
+
|
|
205
|
+
- [x] Create `sync-withid.establish.respec.mts` with scaffold:
|
|
206
|
+
- `Metaspace_Innerspace` + two `InnerSpace_V1` (source/dest) + `SyncSagaCoordinator` pair + `newTestPeer()` factory
|
|
207
|
+
- A `senderSecret` constant (test-only plaintext)
|
|
208
|
+
- A `KeystoneService_V1` instance (new'd inline, no injection needed)
|
|
209
|
+
- [x] Add `respecfully` block: `"Phase 1: establishSessionIdentity"`
|
|
210
|
+
- [x] `ifWeMight` — `"creates sessionIdentity genesis (S) locally"`: assert `S^Stjp` was generated and exists in `sourceSpace`
|
|
211
|
+
- [x] `ifWeMight` — `"evolves senderIdentity (I → I1) with sync claim"`: assert `I1` frame has a proof whose `claim.verb === 'sync'` and `claim.target === S^Stjp`
|
|
212
|
+
- [x] `ifWeMight` — `"posts I1 and S to destSpace (receiver)"`: assert both addrs are retrievable from `destSpace`
|
|
213
|
+
|
|
214
|
+
#### Phase 1B — Space-Gib integrated (manual) (`dev-tools.mts`)
|
|
215
|
+
|
|
216
|
+
Add a new horizontal flexbox row dedicated to the "establish" test flow. Existing debug button rows stay as-is; the new row activates and disables the existing rows (page refresh required to reset). The new row contains:
|
|
217
|
+
|
|
218
|
+
- [x] Button: **"new identity"** — creates a fresh `senderIdentity` (I) keystone locally
|
|
219
|
+
- [x] Button: **"confirm sender identity"** — asserts I exists in the sender's local space
|
|
220
|
+
- [x] Button: **"confirm receiver identity"** — asserts I (or its absence) in the receiver's domain registry
|
|
221
|
+
- [x] Button: **"sync - establish"** — calls `sync(...)` (which internally runs `establishSessionIdentity`)
|
|
222
|
+
- [x] Button: **"confirm sender identity evolved"** — asserts I1 (evolved frame) is in sender's space
|
|
223
|
+
- [x] Button: **"confirm receiver identity evolved"** — asserts I1 is in receiver's domain registry
|
|
224
|
+
- [x] Button: **"confirm sender session identity"** — asserts S genesis exists in sender's space
|
|
225
|
+
- [x] Button: **"confirm receiver session identity"** — asserts S genesis exists in receiver's domain registry
|
|
226
|
+
- [x] Clean up buttons / consolidate confirms once the flow is stable
|
|
227
|
+
|
|
228
|
+
##### 1) WebSocket Peer Sender (`SyncPeerWebSocketSender_V1`)
|
|
229
|
+
- [x] Define options and data types (`wsUrl` for sync, `httpEvolveUrl` for pre-connect establish routing).
|
|
230
|
+
- [x] Implement `postEstablishToReceiver` to execute Phase 1 HTTP POST/PUT of evolved $I_1$ and session genesis $S$ to the registry.
|
|
231
|
+
- [x] Implement `connectImpl` to:
|
|
232
|
+
- [x] Consistently load the handshake solution from local storage/memory.
|
|
233
|
+
- [x] Perform the RFC 6455 upgrade request with `sAddr` and upfront `solution` parameters.
|
|
234
|
+
- [x] Listen and respond to multi-turn JSON RPC handshake frames (`auth-challenge-init` -> `auth-init` -> `auth-challenge` -> `auth-proof` -> `auth-ok`).
|
|
235
|
+
- [x] Implement `sendContextRequest` to push context and transactional payloads down the active WebSocket pipe.
|
|
236
|
+
|
|
237
|
+
##### 2) WebSocket Peer Receiver (`SyncPeerWebSocketReceiver_V1`)
|
|
238
|
+
- [x] Review dev-tools.mts and especially sync-upgrade.handler.mts, moving what code we can from the handler into this peer receiver.
|
|
239
|
+
- [x] Define server-side receiver options matching core registry namespaces.
|
|
240
|
+
- [x] Implement robust event/message routers to handle stateful context payloads during the established synchronization session.
|
|
241
|
+
- [x] Design standard static/pure functions to manage Keystone verification and registration during the establish phase without requiring an active saga connection instance.
|
|
242
|
+
|
|
243
|
+
##### 3) Serve-Gib Establish Handler (`KeystoneEvolveHandler`)
|
|
244
|
+
- [x] Should delegate to sync peer receiver static methods or pure functions in sync-websocket-peer-helpers.mts (or whatever the exact filename turns out to be for the sync peer's websocket helper functions)
|
|
245
|
+
- [x] Cleanly handle the pre-connect registry POST.
|
|
246
|
+
- [x] Validate signature proof of identity evolution ($I \to I_1$) and store both $I_1$ and $S$ into the domain's server-side durable space registry.
|
|
247
|
+
- [x] Ensure it remains entirely stateless, coordinating purely through persistent database/filesystem states.
|
|
248
|
+
|
|
249
|
+
##### 4) Serve-Gib Connect/Sync Handler (`SyncChannelHandler`)
|
|
250
|
+
- [x] Refactor and rename the WebSocket handler (moving beyond "upgrade" to reflect its status as the Sync Peer Connect & Transport Channel wrapper).
|
|
251
|
+
- [x] Implement upfront picket-fence validation on upgrade (`validateUpfrontConnect`) by retrieving $S$ from durable space and checking the solution parameter.
|
|
252
|
+
- [x] Manage the stateful WebSocket challenge handshake generically.
|
|
253
|
+
- [x] Instantiate `SyncPeerWebSocketReceiver_V1` and wire it to the active socket stream upon successful authentication.
|
|
254
|
+
|
|
255
|
+
##### 5) Downstream App Registration (`space-gib`)
|
|
256
|
+
- [x] Update `server.mts` (or equivalent serve-gib orchestrator) to mount the generalized `KeystoneEvolveHandler` (Establish) and `SyncChannelHandler` (Connect/Sync) with simple, clean `regex` + `verb` routing declarations.
|
|
257
|
+
- [x] Wire the Dev Tools perform-sync button to instantiate the unified `SyncSagaCoordinator` and `SyncPeerWebSocketSender_V1` to execute the full sequence.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### Phase 2 — `peer.connect()` (Transport Handshake)
|
|
262
|
+
|
|
263
|
+
**Goal**: Open the transport channel (if applicable to the peer implementation) and sign/deplete S's `connect` pool as proof-of-possession.
|
|
264
|
+
|
|
265
|
+
We still call `senderCoordinator.sync(...)` — the phase focus is on what `peer.connect()` does internally, not on a separate call.
|
|
266
|
+
|
|
267
|
+
#### Phase 2A — Innerspace unit test (`sync-withid.connect.respec.mts`)
|
|
268
|
+
|
|
269
|
+
- [x] Create `sync-withid.connect.respec.mts` (separate file to isolate connect-phase logging)
|
|
270
|
+
- [x] Add `respecfully` block: `"Phase 2: connect"`
|
|
271
|
+
- [x] `ifWeMight` — `"connect completes without error"`: call sync and assert no exception at/before the connect phase
|
|
272
|
+
- [x] `ifWeMight` — `"connect pool is fully depleted after connect"`: assert `S.data.pools['connect']` is exhausted (all challenges consumed), since after connect only the `sync` pool remains active
|
|
273
|
+
- [x] 🔲 **Open question**: Does innerspace issue demanded challenge IDs for the `connect` pool, or is this behavior strictly peer-specific (WebSocket only)? Confirm during implementation — if innerspace skips challenge issuance, the depletion check may not apply and we document the asymmetry.
|
|
274
|
+
|
|
275
|
+
#### Phase 2B — Space-Gib integrated (manual)
|
|
276
|
+
|
|
277
|
+
- [x] Confirm WebSocket connection is established — check the browser's Network tab (WS frames) rather than adding any server-side detection endpoint (avoids introducing insecure surface)
|
|
278
|
+
- [x] Confirm server issues and accepts challenge from S's `connect` pool — verify via server logs or debugger
|
|
279
|
+
- [x] Confirm `connect` pool is exhausted in S after handshake (inspect S's latest frame in debugger)
|
|
280
|
+
- [x] Confirm session is considered active server-side after handshake
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### Phase 3 — Basic Single-Timeline Sync with Identity
|
|
285
|
+
|
|
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
|
+
|
|
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`
|
|
300
|
+
|
|
301
|
+
#### Phase 3B — Space-Gib integrated (manual)
|
|
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
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
### Phase 1 — `establishSessionIdentity` (Pre-Connect)
|
|
316
|
+
|
|
317
|
+
**Goal**: Get `I^Itjp` onto the domain provider, generate `S^Stjp` locally, evolve `I → I1` with a `sync` claim targeting `S^Stjp`, and post both `I1` and `S` to the provider. Verify the provider accepted and stored them.
|
|
318
|
+
|
|
319
|
+
The sync proper will not be called. The test only cares that the pre-connect identity handoff succeeded.
|
|
320
|
+
|
|
321
|
+
#### Phase 1A — Innerspace unit test
|
|
322
|
+
|
|
323
|
+
- [ ] Create `sync-with-identity-basic.respec.mts` with scaffold:
|
|
324
|
+
- `Metaspace_Innerspace` + two `InnerSpace_V1` (source/dest) + `SyncSagaCoordinator` pair + `newTestPeer()` factory
|
|
325
|
+
- A `senderSecret` constant (test-only plaintext)
|
|
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`
|
|
331
|
+
|
|
332
|
+
#### Phase 1B — Space-Gib integrated (manual)
|
|
333
|
+
|
|
334
|
+
- [x] Confirm `dev-tools.mts` "Create Session Keystone (S)" button produces a valid genesis keystone (inspect in browser debugger)
|
|
335
|
+
- [x] Confirm "Evolve Domain Keystone (I → I1)" button produces `I1` with correct `sync` claim targeting `S^Stjp`
|
|
336
|
+
- [x] Confirm "Post Evolution" (`putEvolveKeystone`) is accepted by the server (HTTP 200 / success response)
|
|
337
|
+
- [x] Confirm server stores both `I1` and `S` — verify via GET or server-side debugger
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
### Phase 2 — `peer.connect()` (Transport H andshake)
|
|
342
|
+
|
|
343
|
+
**Goal**: Open the transport channel, if applicable, and sign session identity's `connect` pool.
|
|
344
|
+
|
|
345
|
+
#### Phase 2A — Innerspace unit test
|
|
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`)
|
|
348
|
+
- [x] `ifWeMight` — `"connect completes without error"`: call `peer.connect()` and assert no exception
|
|
349
|
+
- [x] `ifWeMight` — `"connect pool is depleted by exactly one challenge"`: assert `S.data.pools['connect'].used` increased by the expected count
|
|
350
|
+
|
|
351
|
+
#### Phase 2B — Space-Gib integrated (manual)
|
|
352
|
+
|
|
353
|
+
- [x] Confirm "Connect (WS H andshake)" button in `dev-tools.mts` opens WebSocket successfully
|
|
354
|
+
- [x] Confirm server issues challenge from S's `connect` pool and accepts the solution
|
|
355
|
+
- [x] Confirm session is considered active server-side after handshake
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
### Phase 3 — Basic Single-Timeline Sync with Identity
|
|
360
|
+
|
|
361
|
+
**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 and that after sync, both sender and receiver durable spaces contain the same identity I, full session keystone S dependency graph, and full X dependency graph.
|
|
362
|
+
|
|
363
|
+
#### Phase 3A — Innerspace unit test
|
|
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
|
|
370
|
+
|
|
371
|
+
#### Phase 3B — Space-Gib integrated (manual)
|
|
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
|
|
@@ -8,7 +8,7 @@ await respecfully(sir, 'Graft Info Helpers', async () => {
|
|
|
8
8
|
|
|
9
9
|
await respecfully(sir, 'mergeTextLCS', async () => {
|
|
10
10
|
|
|
11
|
-
await
|
|
11
|
+
await ifWe(sir, 'mergeTextLCS should interleave unique lines (A then B)', async () => {
|
|
12
12
|
const textA = "Line 1\nLine A\nLine 3";
|
|
13
13
|
const textB = "Line 1\nLine B\nLine 3";
|
|
14
14
|
const res = await mergeTextByLongestCommonSubsequence({ textA, textB });
|
|
@@ -16,7 +16,7 @@ await respecfully(sir, 'Graft Info Helpers', async () => {
|
|
|
16
16
|
iReckon(sir, res).asTo('result').isGonnaBe(expected);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
await
|
|
19
|
+
await ifWe(sir, 'mergeTextLCS should handle insertions', async () => {
|
|
20
20
|
const textA = "Line 1\nLine 3";
|
|
21
21
|
const textB = "Line 1\nLine 2\nLine 3";
|
|
22
22
|
const res = await mergeTextByLongestCommonSubsequence({ textA, textB });
|
|
@@ -24,7 +24,7 @@ await respecfully(sir, 'Graft Info Helpers', async () => {
|
|
|
24
24
|
iReckon(sir, res).asTo('result').isGonnaBe(expected);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
await
|
|
27
|
+
await ifWe(sir, 'mergeTextLCS should handle appends', async () => {
|
|
28
28
|
const textA = "Line 1";
|
|
29
29
|
const textB = "Line 1\nLine 2";
|
|
30
30
|
const res = await mergeTextByLongestCommonSubsequence({ textA, textB });
|
|
@@ -32,7 +32,7 @@ await respecfully(sir, 'Graft Info Helpers', async () => {
|
|
|
32
32
|
iReckon(sir, res).asTo('result').isGonnaBe(expected);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
await
|
|
35
|
+
await ifWe(sir, 'mergeTextLCS should handle simultaneous list additions', async () => {
|
|
36
36
|
const textA = "- Item 1\n- Item 2\n- Item A";
|
|
37
37
|
const textB = "- Item 1\n- Item 2\n- Item B";
|
|
38
38
|
const res = await mergeTextByLongestCommonSubsequence({ textA, textB });
|
|
@@ -42,7 +42,7 @@ await respecfully(sir, 'Graft Info Helpers', async () => {
|
|
|
42
42
|
iReckon(sir, res).asTo('result').isGonnaBe(expected);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
await
|
|
45
|
+
await ifWe(sir, 'mergeTextLCS should handle distinct modifications in code block', async () => {
|
|
46
46
|
const textA = `function foo() {
|
|
47
47
|
console.log("start");
|
|
48
48
|
doA();
|
|
@@ -64,7 +64,7 @@ await respecfully(sir, 'Graft Info Helpers', async () => {
|
|
|
64
64
|
iReckon(sir, res).asTo('result').isGonnaBe(expected);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
await
|
|
67
|
+
await ifWe(sir, 'mergeTextLCS should return same string if identical', async () => {
|
|
68
68
|
const textA = "Hello";
|
|
69
69
|
const res = await mergeTextByLongestCommonSubsequence({ textA, textB: textA });
|
|
70
70
|
iReckon(sir, res).asTo('result').isGonnaBe(textA);
|
|
@@ -75,7 +75,7 @@ await respecfully(sir, 'Graft Info Helpers', async () => {
|
|
|
75
75
|
await respecfully(sir, 'graftTimelines', async () => {
|
|
76
76
|
// We need a more complex setup to test this properly (mock space, etc.)
|
|
77
77
|
// For now, this placeholder ensures the test file runs.
|
|
78
|
-
await
|
|
78
|
+
await ifWe(sir, 'graftTimelines placeholders', async () => {
|
|
79
79
|
iReckon(sir, true).isGonnaBeTrue();
|
|
80
80
|
});
|
|
81
81
|
});
|
|
@@ -127,7 +127,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
127
127
|
// #endregion r1 setup
|
|
128
128
|
|
|
129
129
|
await respecfully(sir, `r1 verify pre`, async () => {
|
|
130
|
-
await
|
|
130
|
+
await ifWe(sir, 'dest should NOT have alpha', async () => {
|
|
131
131
|
const resGet = await getFromSpace({
|
|
132
132
|
space: destSpace,
|
|
133
133
|
addr: alpha_tjpAddr,
|
|
@@ -150,7 +150,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
150
150
|
if (logalot) { console.log(`${lc} r1_syncSaga Complete.`); }
|
|
151
151
|
|
|
152
152
|
await respecfully(sir, `r1 verify post`, async () => {
|
|
153
|
-
await
|
|
153
|
+
await ifWe(sir, 'dest should have alpha', async () => {
|
|
154
154
|
// alpha's full dep graph should exist on dest
|
|
155
155
|
const [alpha_dest] = await getIbGibsFromCache_fallbackToSpaces({
|
|
156
156
|
addrs: [r1_alpha_v0_source.addr],
|
|
@@ -230,7 +230,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
230
230
|
// Verify Receiver has correct KV (Pre-Sync Check)
|
|
231
231
|
// This ensures the conflict precondition exists.
|
|
232
232
|
await respecfully(sir, `r2 verify pre`, async () => {
|
|
233
|
-
await
|
|
233
|
+
await ifWe(sir, 'dest has alpha v1 common', async () => {
|
|
234
234
|
try {
|
|
235
235
|
const destKV = await receiverCoordinator.getKnowledgeMap({
|
|
236
236
|
space: destSpace,
|
|
@@ -308,7 +308,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
308
308
|
if (!alpha_dest_tipAddr) {
|
|
309
309
|
throw new Error(`dest Space missing timeline tip for ${alpha_tjpAddr} (E: 30b018c6349917aa28c9f538fa567826)`);
|
|
310
310
|
}
|
|
311
|
-
await
|
|
311
|
+
await ifWe(sir, 'tip addrs', async () => {
|
|
312
312
|
iReckon(sir, alpha_source_tipAddr).asTo('source/dest have same tip addrs').isGonnaBe(alpha_dest_tipAddr);
|
|
313
313
|
})
|
|
314
314
|
|
|
@@ -334,7 +334,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
334
334
|
// #endregion DEBUG SANITY CHECK
|
|
335
335
|
|
|
336
336
|
|
|
337
|
-
await
|
|
337
|
+
await ifWe(sir, 'r2 basics of alpha merge', async () => {
|
|
338
338
|
iReckon(sir, alpha_source_tipAddr)
|
|
339
339
|
.asTo(`Source Tip (${alpha_source_tipAddr}) should NOT be r2_alpha_v3_source_rel8dBeta`)
|
|
340
340
|
.not.isGonnaBe(r2_alpha_v3_source_rel8dBeta.addr);
|
|
@@ -357,7 +357,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
357
357
|
iReckon(sir, gotten_alpha_source_tipIbGib.data![r2_dest_mut8Info.key!]).asTo(`New Tip has dest field ${r2_dest_mut8Info.key!}`).isGonnaBe(r2_dest_mut8Info.value!);
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
-
await
|
|
360
|
+
await ifWe(sir, 'r2 alpha and deps synced', async () => {
|
|
361
361
|
// alpha's full dep graph should exist on dest, even though its
|
|
362
362
|
// timeline was grafted
|
|
363
363
|
const [alpha_dest] = await getIbGibsFromCache_fallbackToSpaces({
|
|
@@ -390,7 +390,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
390
390
|
}
|
|
391
391
|
});
|
|
392
392
|
|
|
393
|
-
await
|
|
393
|
+
await ifWe(sir, 'r2 beta and deps synced', async () => {
|
|
394
394
|
// beta's full dep graph should exist on dest
|
|
395
395
|
const [beta_dest] = await getIbGibsFromCache_fallbackToSpaces({
|
|
396
396
|
addrs: [r2_beta_v0_source.addr],
|
|
@@ -510,7 +510,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
510
510
|
// #endregion r3 dest edits
|
|
511
511
|
|
|
512
512
|
await respecfully(sir, `r3 verify pre`, async () => {
|
|
513
|
-
await
|
|
513
|
+
await ifWe(sir, 'dest has both alpha and beta tips (pre-conflict)', async () => {
|
|
514
514
|
try {
|
|
515
515
|
const destKV = await receiverCoordinator.getKnowledgeMap({
|
|
516
516
|
space: destSpace,
|
|
@@ -585,13 +585,13 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
585
585
|
});
|
|
586
586
|
const r3_beta_dest_tipAddr = beta_destKV_afterR3[beta_tjpAddr];
|
|
587
587
|
if (!r3_beta_dest_tipAddr) {
|
|
588
|
-
await
|
|
588
|
+
await ifWe(sir, 'r3_beta_dest_tipAddr is falsy?', async () => {
|
|
589
589
|
iReckon(sir, true).asTo('fail').isGonnaBeFalse();
|
|
590
590
|
});
|
|
591
591
|
return; /* <<<< returns early */
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
await
|
|
594
|
+
await ifWe(sir, 'r3 tip addrs match (both timelines)', async () => {
|
|
595
595
|
iReckon(sir, r3_alpha_source_tipAddr).asTo('alpha source/dest have same tip addrs').isGonnaBe(r3_alpha_dest_tipAddr);
|
|
596
596
|
iReckon(sir, r3_beta_source_tipAddr).asTo('beta source/dest have same tip addrs').isGonnaBe(r3_beta_dest_tipAddr);
|
|
597
597
|
});
|
|
@@ -610,7 +610,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
610
610
|
const resGet_r3_beta_tip = await getFromSpace({ space: sourceSpace, addr: r3_beta_source_tipAddr });
|
|
611
611
|
const r3_beta_tipIbGib = resGet_r3_beta_tip.ibGibs![0] as IbGib_V1<TestData>;
|
|
612
612
|
|
|
613
|
-
await
|
|
613
|
+
await ifWe(sir, 'r3 alpha merge has all four fields (commonField, fieldA, fieldB, fieldC, fieldD)', async () => {
|
|
614
614
|
// Should have new graft point with different addr than before
|
|
615
615
|
iReckon(sir, r3_alpha_source_tipAddr)
|
|
616
616
|
.asTo(`Alpha R3 tip should NOT be r3_alpha_v4_source_mut8fieldC`)
|
|
@@ -631,7 +631,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
631
631
|
.isGonnaBe(r3_alpha_v4_dest_mut8Info.value!);
|
|
632
632
|
});
|
|
633
633
|
|
|
634
|
-
await
|
|
634
|
+
await ifWe(sir, 'r3 beta merge has both fields (betaFieldA, betaFieldB)', async () => {
|
|
635
635
|
// Should have new graft point with different addr than before
|
|
636
636
|
iReckon(sir, r3_beta_source_tipAddr)
|
|
637
637
|
.asTo(`Beta R3 tip should NOT be r3_beta_v1_source`)
|
|
@@ -652,7 +652,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
652
652
|
.isGonnaBe(r3_dest_beta_mut8Info.value!);
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
-
await
|
|
655
|
+
await ifWe(sir, 'r3 alpha and deps synced', async () => {
|
|
656
656
|
const [alpha_dest] = await getIbGibsFromCache_fallbackToSpaces({
|
|
657
657
|
// addrs: [r1_alpha_v0_source.addr],
|
|
658
658
|
addrs: [r3_alpha_source_tipAddr],
|
|
@@ -684,7 +684,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
684
684
|
}
|
|
685
685
|
});
|
|
686
686
|
|
|
687
|
-
await
|
|
687
|
+
await ifWe(sir, 'r3 beta and deps synced', async () => {
|
|
688
688
|
const [beta_dest] = await getIbGibsFromCache_fallbackToSpaces({
|
|
689
689
|
// addrs: [r2_beta_v0_source.addr],
|
|
690
690
|
addrs: [r3_beta_dest_tipAddr],
|
|
@@ -836,7 +836,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
836
836
|
// #endregion r4 dest edits
|
|
837
837
|
|
|
838
838
|
await respecfully(sir, `r4 verify pre`, async () => {
|
|
839
|
-
await
|
|
839
|
+
await ifWe(sir, 'dest has alpha and beta post-R3 tips', async () => {
|
|
840
840
|
try {
|
|
841
841
|
const destKV = await receiverCoordinator.getKnowledgeMap({
|
|
842
842
|
space: destSpace,
|
|
@@ -913,7 +913,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
913
913
|
});
|
|
914
914
|
const r4_beta_dest_tipAddr = r4_beta_destKV[beta_tjpAddr];
|
|
915
915
|
if (!r4_beta_dest_tipAddr) {
|
|
916
|
-
await
|
|
916
|
+
await ifWe(sir, 'r4_beta_dest_tipAddr is falsy?', async () => {
|
|
917
917
|
iReckon(sir, true).asTo('fail').isGonnaBeFalse();
|
|
918
918
|
});
|
|
919
919
|
return; /* <<<< returns early */
|
|
@@ -933,7 +933,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
933
933
|
});
|
|
934
934
|
const r4_gamma_dest_tipAddr = r4_gamma_destKV[gamma_tjpAddr];
|
|
935
935
|
|
|
936
|
-
await
|
|
936
|
+
await ifWe(sir, 'r4 tip addrs match (alpha, beta, gamma)', async () => {
|
|
937
937
|
iReckon(sir, r4_alpha_source_tipAddr).asTo('alpha source/dest have same tip addrs').isGonnaBe(r4_alpha_dest_tipAddr);
|
|
938
938
|
iReckon(sir, r4_beta_source_tipAddr).asTo('beta source/dest have same tip addrs').isGonnaBe(r4_beta_dest_tipAddr);
|
|
939
939
|
iReckon(sir, r4_gamma_source_tipAddr).asTo('gamma source/dest have same tip addrs').isGonnaBe(r4_gamma_dest_tipAddr);
|
|
@@ -958,7 +958,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
958
958
|
const resGet_r4_gamma_tip = await getFromSpace({ space: sourceSpace, addr: r4_gamma_source_tipAddr });
|
|
959
959
|
const r4_gamma_tipIbGib = resGet_r4_gamma_tip.ibGibs![0] as IbGib_V1<TestData>;
|
|
960
960
|
|
|
961
|
-
await
|
|
961
|
+
await ifWe(sir, 'r4 alpha has complete chain of edits and gamma relation', async () => {
|
|
962
962
|
// Should have new graft point with different addr than either pre-graft version
|
|
963
963
|
iReckon(sir, r4_alpha_source_tipAddr)
|
|
964
964
|
.asTo(`Alpha R4 tip should NOT be r4_alpha_v7_source`)
|
|
@@ -994,7 +994,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
994
994
|
.isGonnaBeTrue();
|
|
995
995
|
});
|
|
996
996
|
|
|
997
|
-
await
|
|
997
|
+
await ifWe(sir, 'r4 beta has all edits', async () => {
|
|
998
998
|
// Beta should have new graft from R4
|
|
999
999
|
iReckon(sir, r4_beta_source_tipAddr)
|
|
1000
1000
|
.asTo(`Beta R4 tip should NOT be r4_beta_v2_source (source version pre-graft)`)
|
|
@@ -1015,7 +1015,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
1015
1015
|
.isGonnaBe(r4_dest_beta_mut8Info.value!);
|
|
1016
1016
|
});
|
|
1017
1017
|
|
|
1018
|
-
await
|
|
1018
|
+
await ifWe(sir, 'r4 gamma synced with deps', async () => {
|
|
1019
1019
|
const [gamma_dest] = await getIbGibsFromCache_fallbackToSpaces({
|
|
1020
1020
|
addrs: [r4_gamma_v0_source.addr],
|
|
1021
1021
|
space: destSpace,
|
|
@@ -1046,7 +1046,7 @@ await respecfully(sir, `Multi-round/timeline permutations`, async () => {
|
|
|
1046
1046
|
}
|
|
1047
1047
|
});
|
|
1048
1048
|
|
|
1049
|
-
await
|
|
1049
|
+
await ifWe(sir, 'r4 all timelines dep graphs synced', async () => {
|
|
1050
1050
|
// Verify alpha dep graphs
|
|
1051
1051
|
const [alpha_dest] = await getIbGibsFromCache_fallbackToSpaces({
|
|
1052
1052
|
addrs: [r4_alpha_source_tipAddr],
|