# Ibgib Sync Security

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).

## Core Concepts

* **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).
* **Session Keystone (S)**: An ephemeral, short-lived keystone generated locally by the Sender explicitly for a single sync session.
* **Target Domain (X)**: The specific ibgib timeline or payload that is the subject of the sync.
* **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.

## Development Nuances & Implementation TODOs

This section serves as a working scratchpad to capture specific code-level nuances, terminology standardization, and pending tasks before jumping into implementation.

### ✅ Agreed: Secret Naming — `senderSecret` & `sessionSecret`

* **`senderSecret`**: The master secret corresponding to `senderIdentity`. Drives the KDF to produce the session secret. Replaces the old `nonSessionSecret` / `identitySecret` / `domainSecret` names.
* **`sessionSecret`**: Deterministically derived via `KDF(senderSecret, sagaId)`. Ephemeral and saga-specific. Used only within `establishSessionIdentity` to create the session keystone genesis.
* *Rationale*: `sender` pairs naturally with `senderIdentity`; `session` pairs naturally with `sessionIdentity`.

### ✅ Agreed: Identity Naming — `senderIdentity` & `sessionIdentity`

* **`senderIdentity`**: The long-lived Domain Keystone (I) representing the Sender (Alice). Optional — a sync can run with just a `domainSecret` and no named identity.
  * Using `senderIdentity` (not `domainIdentity` or `primaryIdentity`) because "domain" becomes ambiguous when Bob eventually has his own domain identity in a future symmetric model.
* **`sessionIdentity`**: The ephemeral keystone (S) generated per-saga. Used consistently as both:
  * A **param/property name** (e.g., `peer.sessionIdentity`, `createSessionIdentity()`).
  * A **rel8n name** on both the sync saga ibgib and the context ibgib (e.g., `syncSagaIbGib.rel8ns.sessionIdentity`, `contextIbGib.rel8ns.sessionIdentity`).

### ✅ Agreed: Identity Hard-linking Strategy

* **Sync Saga Frame** (`ibgib.data`, soft-link):
  * `data` should record identity details as soft references (addresses in `ibgib.data`, not hard-linked `ibgib.rel8ns`):
    * `senderIdentity` TJP addr.
    * The exact `senderIdentity` frame addr that signed/authorized the session (i.e., the new frame post-`establishSessionIdentity`).
    * `sessionIdentity` TJP addr.
  * No hard-link (`rel8ns`) to `sessionIdentity` from the sync saga ibgib.
* **Context IbGib** (`ibgib.rel8ns`, hard-link):
  * `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.
  * The `signedSessionKeystone` property on the context carries the **newly evolved** frame (the current turn's signature).
  * **No transition pool**: The prior symmetric "transition pool" (for the receiver's signing turn) is eliminated. Only the Sender signs.
  * **Sovereign Broker response**: The Receiver includes the *same, unevolved* `sessionIdentity` addr in its response context's `rel8ns.sessionIdentity` — it does not evolve the keystone.

### ✅ Agreed: Session Keystone Pool Configuration

The session keystone has **two dedicated pools**, each with a matching `poolId` and `verb`:

* **`connect` pool** (`poolId: "connect"`, `verb: "connect"`):
  * Used exclusively during `peer.connect()` — the in-band WebSocket challenge/response handshake.
  * The receiver issues challenges from this pool; the sender solves them to prove possession of S.
* **`sync` pool** (`poolId: "sync"`, `verb: "sync"`):
  * Used to sign each outgoing context frame during the sync ping-pong (Init, Ack, Delta, Commit turns).
  * Pool separation is intentional — the `connect` handshake and per-turn signing are distinct operations with distinct lifetimes.
* [ ] Add `"connect"` and `"sync"` verbs to `keystone-constants.mts`.
* [ ] Create standard pool config factories for both in [`keystone-config-builder.mts`](../../keystone/keystone-config-builder.mts).

Note: The `senderIdentity` Domain Keystone also uses the `"sync"` verb in its claim when it signs/authorizes the session keystone genesis during `establishSessionIdentity`.

### ✅ Agreed: Sync Peer Lifetime — One Peer per Saga

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)`).

The `sagaId` is passed to the peer via `initializeOpts` so that `establishSessionIdentity` can derive the correct `sessionSecret` before any other phase begins.

### ✅ Agreed: `establishSessionIdentity` — Pre-Connect Phase

A new `establishSessionIdentity` method on the sync peer base class is the **mandatory first step** before `connect`. It encapsulates:

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.
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`.
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`).
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.
5. **After this point**: `senderIdentity` IS the new frame. The session keystone takes over authorization for all subsequent sync turns.

### ✅ Agreed: Per-Turn Transmission Protocol

* **Sender → Receiver (each turn)**: Only the **current evolved frame of `sessionIdentity`** (S) is transmitted. `senderIdentity` is never re-transmitted after `establishSessionIdentity`.
* **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.
  * This is possible on every turn because `establishSessionIdentity` guarantees the domain and `I^Itjp` already exist on the Receiver.
* **Receiver → Sender (each turn)**: The Receiver's response context echoes the same (unevolved) `sessionIdentity` addr — it never produces a new keystone evolution.

### 🔲 Pending: `authenticateContext` Placement

The receiver-side validation that runs on each incoming context has three steps:
1. **Transition validity**: Replay the session keystone evolution — verify `data.n` is sequential, challenge solutions are valid, pool not exhausted, etc.
2. **Target binding**: Verify that the incoming `signedSessionIdentity`'s proof actually targets *this* context's exact address (`proof.claim.target === contextAddr`).
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".

**Open**: Where does this code live?
* **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.
* **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).

The peer placement is currently preferred since it co-locates the space knowledge with the validation logic. Finalize during implementation.

### ✅ Agreed: `KeystoneService_V1` — No Injection Needed

`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.

### ✅ Agreed: Context Property — `signedSessionIdentity`

The context ibgib carries the newly evolved `sessionIdentity` frame in a property named **`signedSessionIdentity`** (replacing the old `signedSessionKeystone`).

Context ibgib now has a clean, consistent pair:
* `rel8ns.sessionIdentity` → addr of the **previous** frame (before signing this turn)
* `signedSessionIdentity` → the **newly evolved** frame (current turn's cryptographic proof)



## High Level Authorization Flow

1. [ ] **`establishSessionIdentity`** *(new pre-connect phase)*:
   [ ] * Sender locally generates `sessionIdentity` (S) and signs `senderIdentity` (I &rarr; I1) with a `sync` claim targeting `S^Stjp`.
   [ ] * 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.
2. [ ] **`peer.connect()`** *(transport handshake)*:
   [ ] * Sender opens the transport channel (e.g., WebSocket).
   [ ] * Receiver issues challenges from S's **`connect` pool**.
   [ ] * Sender solves and returns proof-of-possession. Session is authorized.
3. [ ] **Asymmetric Sync (Ping-Pong)**:
   [ ] * Sender signs all outgoing contexts (Init, Ack, Delta, Commit) using S's **`sync` pool**, evolving S each turn.
   [ ] * Only the latest evolved S frame is transmitted per turn — never `senderIdentity`.
   [ ] * Receiver validates by independently loading `I^Itjp`, tracing to `S^Stjp`, and verifying the incoming frame.
   [ ] * Receiver responds with unsigned contexts (same S addr echoed, no new evolution).


## Attack Vectors & Mitigations

| Attack Vector | Description | Mitigation |
|---------------|-------------|------------|
| **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 |
| **Session Fixation** | Attacker pre-generates session keystone | • Session secret derived via KDF (strategy in keystone metadata)<br>• `sessionSecret = KDF(masterSecret, sagaId, strategy)` |
| **Identity Spoofing** | Impersonation via fake session keystone | • Session keystone includes `derivedFrom` master address<br>• Receiver validates master keystone is known/trusted |
| **Challenge Grinding** | Brute-force challenge solutions for target binding | • Hash pre-imaging computationally infeasible<br>• Stochastic selection adds randomness |
| **DoS (Pool Exhaustion)** | Excessive syncs deplete challenge pools | • Top-up replenishment refills challenges<br>• Rate limiting per identity<br>• Pool separation for critical ops |
| **Saga Hijacking** | Mid-flight frame injection | • All frames signed with session keystone<br>• `past` rel8n creates tamper-evident chain |

## Trust Model

**Asymmetric Identity (Domain-Based Authorization)**:
- **Sender Signs**: The Sender is responsible for cryptographically signing all requests using their ephemeral Session Keystone.
- **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.
- **Delegated Authority**: Trust is explicitly delegated from a Domain Keystone (I) to a Session Keystone (S) via a `sync` claim evolution.

**Session Keystones**:
- Ephemeral identity per sync saga.
- Secret derived via `KDF(senderSecret, sagaId)`.
- Contains two isolated pools: `connect` (for transport handshake) and `sync` (for per-turn frame signing).
- Validates via proof-of-work (solving challenges from the appropriate pool).

**Propagation**:
- Sync does NOT rely on global PKI or certificate authorities.
- Trust propagates through sync sessions (Alice syncs with Bob &rarr; Bob witnesses Alice's keystone).
- Revocation propagates same way (Alice revokes → syncs revocation to Bob → Bob sees revoked timeline)

**Threat Boundaries**:
- ✅ Protects against: MITM replay, impersonation, frame tampering
- ⚠️ Does NOT protect against: Compromised endpoint (live memory access during active session)
- ⚠️ 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
- ⚠️ Transport security: Use TLS for network layer encryption (ibgib protocol provides authentication/integrity, not confidentiality)

## Best Practices

1. **Master Secret Protection**: Store master secrets in secure enclaves/keychains
2. **Session Lifetime**: Limit saga duration, revoke sessions post-completion
3. **Rate Limiting**: Implement per-identity sync rate limits
4. **Freshness**: Reject frames older than 60 seconds
5. **Audit Logs**: Persist saga timelines for post-hoc validation (saga timelines are ibgibs, so integrity is built-in via content addressing)
6. **TLS**: Always use TLS for network transport (defense-in-depth)

## ❓ Open Questions

### Payload Scope Validation: Can the Protocol Police What ibGibs Are Transmitted?

**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?

**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:
- A control ibgib (sync saga, context, substone frames), or
- Part of the dependency graph rooted at some frame of X's timeline (`X^Xtjp`)

**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.

**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.

**Tracked in**: `space-gib.sync-walkthrough.md`

---

## respec-gib Testing Framework

Because `ibgib` has unique execution environments and a high need for custom asynchronous block handling, it uses a custom testing framework located in `@ibgib/helper-gib/dist/respec-gib`.

### Key Framework APIs
* **`respecfully`**: The equivalent of `describe` or an outer test suite block. It is async and MUST be awaited.
* **`ifWe`**: The equivalent of `it`, used for defining a specific test case block. It is async and MUST be awaited. It cannot be nested.
* **`ifWeMight`**: A focused test case (equivalent to `fit` or `it.only`). It is async, MUST be awaited, and cannot be nested.
* **`iReckon`**: The assertion fluent API (equivalent to `expect`). It is synchronous and should NOT be awaited. Available assertions include: `willEqual(expected)`, `isGonnaBe(expected)`, `isGonnaBeTruthy()`, `isGonnaBeFalsy()`, `isGonnaBeTrue()`, `isGonnaBeFalse()`, `isGonnaBeUndefined()`, and `includes(expected)`. You can modify assertions using `.not` and `.asTo(label)`.

### Crucial Execution & Logging Nuances
> [!IMPORTANT]
> **File Focus vs. Code Execution**:
> The test runner scans all test files for the presence of `ifWeMight` (or `respecfullyDear`) to determine which *files* get selected and executed. However, once a file is selected and run, **the entire file executes**.
>
> **The Logging Caveat**:
> The `ifWeMight` filter **only** determines which test results are ultimately *reported* in the test summary output. This means all code and `console.log` statements in any standard `ifWe` blocks within the same file will **still execute and write to the logs**.
> This behavior can make test logs extremely noisy and confusing. If you are focusing on a single test, you should temporarily change all other `ifWeMight` blocks to `ifWe`, and use a single `ifWeMight` block or comment out other logs/tests to keep console output readable.

---

## Implementation Plan

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).

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.

_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._

---

### Phase 1 — `establishSessionIdentity` (Pre-Connect)

**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.

`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.

#### Phase 1A — Innerspace unit test (`sync-withid.establish.respec.mts`)

- [x] Create `sync-withid.establish.respec.mts` with scaffold:
  - `Metaspace_Innerspace` + two `InnerSpace_V1` (source/dest) + `SyncSagaCoordinator` pair + `newTestPeer()` factory
  - A `senderSecret` constant (test-only plaintext)
  - A `KeystoneService_V1` instance (new'd inline, no injection needed)
- [x] Add `respecfully` block: `"Phase 1: establishSessionIdentity"`
  - [x] `ifWeMight` — `"creates sessionIdentity genesis (S) locally"`: assert `S^Stjp` was generated and exists in `sourceSpace`
  - [x] `ifWeMight` — `"evolves senderIdentity (I → I1) with sync claim"`: assert `I1` frame has a proof whose `claim.verb === 'sync'` and `claim.target === S^Stjp`
  - [x] `ifWeMight` — `"posts I1 and S to destSpace (receiver)"`: assert both addrs are retrievable from `destSpace`

#### Phase 1B — Space-Gib integrated (manual) (`dev-tools.mts`)

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:

- [x] Button: **"new identity"** — creates a fresh `senderIdentity` (I) keystone locally
- [x] Button: **"confirm sender identity"** — asserts I exists in the sender's local space
- [x] Button: **"confirm receiver identity"** — asserts I (or its absence) in the receiver's domain registry
- [x] Button: **"sync - establish"** — calls `sync(...)` (which internally runs `establishSessionIdentity`)
- [x] Button: **"confirm sender identity evolved"** — asserts I1 (evolved frame) is in sender's space
- [x] Button: **"confirm receiver identity evolved"** — asserts I1 is in receiver's domain registry
- [x] Button: **"confirm sender session identity"** — asserts S genesis exists in sender's space
- [x] Button: **"confirm receiver session identity"** — asserts S genesis exists in receiver's domain registry
- [x] Clean up buttons / consolidate confirms once the flow is stable

##### 1) WebSocket Peer Sender (`SyncPeerWebSocketSender_V1`)
- [x] Define options and data types (`wsUrl` for sync, `httpEvolveUrl` for pre-connect establish routing).
- [x] Implement `postEstablishToReceiver` to execute Phase 1 HTTP POST/PUT of evolved $I_1$ and session genesis $S$ to the registry.
- [x] Implement `connectImpl` to:
  - [x] Consistently load the handshake solution from local storage/memory.
  - [x] Perform the RFC 6455 upgrade request with `sAddr` and upfront `solution` parameters.
  - [x] Listen and respond to multi-turn JSON RPC handshake frames (`auth-challenge-init` -> `auth-init` -> `auth-challenge` -> `auth-proof` -> `auth-ok`).
- [x] Implement `sendContextRequest` to push context and transactional payloads down the active WebSocket pipe.

##### 2) WebSocket Peer Receiver (`SyncPeerWebSocketReceiver_V1`)
- [x] Review dev-tools.mts and especially sync-upgrade.handler.mts, moving what code we can from the handler into this peer receiver.
- [x] Define server-side receiver options matching core registry namespaces.
- [x] Implement robust event/message routers to handle stateful context payloads during the established synchronization session.
- [x] Design standard static/pure functions to manage Keystone verification and registration during the establish phase without requiring an active saga connection instance.

##### 3) Serve-Gib Establish Handler (`KeystoneEvolveHandler`)
- [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)
- [x] Cleanly handle the pre-connect registry POST.
- [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.
- [x] Ensure it remains entirely stateless, coordinating purely through persistent database/filesystem states.

##### 4) Serve-Gib Connect/Sync Handler (`SyncChannelHandler`)
- [x] Refactor and rename the WebSocket handler (moving beyond "upgrade" to reflect its status as the Sync Peer Connect & Transport Channel wrapper).
- [x] Implement upfront picket-fence validation on upgrade (`validateUpfrontConnect`) by retrieving $S$ from durable space and checking the solution parameter.
- [x] Manage the stateful WebSocket challenge handshake generically.
- [x] Instantiate `SyncPeerWebSocketReceiver_V1` and wire it to the active socket stream upon successful authentication.

##### 5) Downstream App Registration (`space-gib`)
- [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.
- [x] Wire the Dev Tools perform-sync button to instantiate the unified `SyncSagaCoordinator` and `SyncPeerWebSocketSender_V1` to execute the full sequence.

---

### Phase 2 — `peer.connect()` (Transport Handshake)

**Goal**: Open the transport channel (if applicable to the peer implementation) and sign/deplete S's `connect` pool as proof-of-possession.

We still call `senderCoordinator.sync(...)` — the phase focus is on what `peer.connect()` does internally, not on a separate call.

#### Phase 2A — Innerspace unit test (`sync-withid.connect.respec.mts`)

- [x] Create `sync-withid.connect.respec.mts` (separate file to isolate connect-phase logging)
- [x] Add `respecfully` block: `"Phase 2: connect"`
  - [x] `ifWeMight` — `"connect completes without error"`: call sync and assert no exception at/before the connect phase
  - [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
  - [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.

#### Phase 2B — Space-Gib integrated (manual)

- [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)
- [x] Confirm server issues and accepts challenge from S's `connect` pool — verify via server logs or debugger
- [x] Confirm `connect` pool is exhausted in S after handshake (inspect S's latest frame in debugger)
- [x] Confirm session is considered active server-side after handshake

---

### Phase 3 — Basic Single-Timeline Sync with Identity

**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.

#### Phase 3A — Innerspace unit test (`sync-withid.pingpong.respec.mts`)

- [x] Create `sync-withid.pingpong.respec.mts`
  - [x] Create `r1_alpha_v0_source` (represented by `xStone` test domain ibgib)
  - [x] Call `senderCoordinator.sync({ domainIbGibs: [alpha], senderIdentity, senderSecret, ... })`
  - [x] `ifWeMight` — `"alpha dep graph matches on source and dest"`: use `getDependencyGraph` + `graphsAreEquivalent`
  - [x] `ifWeMight` — `"sessionIdentity evolved the expected number of times"`: assert `S.data.n` equals the known deterministic turn count
    - [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
  - [x] `ifWeMight` — `"sender durable space has I (evolved frame)"`: assert I1 addr is in `sourceSpace`
  - [x] `ifWeMight` — `"receiver durable space has I (evolved frame)"`: assert I1 addr is in `destSpace`
  - [x] `ifWeMight` — `"sender durable space has full S dep graph"`: use `getDependencyGraph` on S in `sourceSpace`
  - [x] `ifWeMight` — `"receiver durable space has full S dep graph"`: use `getDependencyGraph` on S in `destSpace`

#### Phase 3B — Space-Gib integrated (manual)

- [x] Trigger a sync of a known test ibgib via `dev-tools.mts` (add "sync - fullsync" button to a new row)
- [x] Confirm S evolves on each outgoing turn (inspect frame `data.n` in debugger)
- [x] Confirm X's full dependency graph is present on the client after commit
- [x] Confirm X's full dependency graph is present on the server after commit
- [x] Confirm S's full dependency graph is present on the client after commit
- [x] Confirm S's full dependency graph is present on the server after commit
- [x] Confirm I's evolved frame (I1) is present on the client after commit
- [x] Confirm I's evolved frame (I1) is present on the server after commit


---

### Phase 1 — `establishSessionIdentity` (Pre-Connect)

**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.

The sync proper will not be called. The test only cares that the pre-connect identity handoff succeeded.

#### Phase 1A — Innerspace unit test

- [x] Create `sync-withid.establish.respec.mts` with scaffold:
  - `Metaspace_Innerspace` + two `InnerSpace_V1` (source/dest) + `SyncSagaCoordinator` pair + `newTestPeer()` factory
  - A `senderSecret` constant (test-only plaintext)
  - A `KeystoneService_V1` instance (new'd inline)
- [x] Add `respecfully` block: `"Phase 1: establishSessionIdentity"`
  - [x] `ifWeMight` — `"creates sessionIdentity genesis (S) locally"`: assert `S^Stjp` was generated and exists in `sourceSpace`
  - [x] `ifWeMight` — `"evolves senderIdentity (I → I1) with sync claim"`: assert `I1` frame has a proof whose `claim.verb === 'sync'` and `claim.target === S^Stjp`
  - [x] `ifWeMight` — `"posts I1 and S to destSpace (receiver)"`: assert both addrs are retrievable from `destSpace`

#### Phase 1B — Space-Gib integrated (manual)

- [x] Confirm `dev-tools.mts` "Create Session Keystone (S)" button produces a valid genesis keystone (inspect in browser debugger)
- [x] Confirm "Evolve Domain Keystone (I → I1)" button produces `I1` with correct `sync` claim targeting `S^Stjp`
- [x] Confirm "Post Evolution" (`putEvolveKeystone`) is accepted by the server (HTTP 200 / success response)
- [x] Confirm server stores both `I1` and `S` — verify via GET or server-side debugger

---

### Phase 2 — `peer.connect()` (Transport H andshake)

**Goal**: Open the transport channel, if applicable, and sign session identity's `connect` pool.

#### Phase 2A — Innerspace unit test

- [x] Create `sync-withid.connect.respec.mts` with a `respecfully` block: `"Phase 2: connect"`
  - [x] `ifWeMight` — `"connect completes without error"`: call `peer.connect()` and assert no exception
  - [x] `ifWeMight` — `"connect pool is depleted by exactly one challenge"`: assert `S.data.pools['connect'].used` increased by the expected count

#### Phase 2B — Space-Gib integrated (manual)

- [x] Confirm "Connect (WS H andshake)" button in `dev-tools.mts` opens WebSocket successfully
- [x] Confirm server issues challenge from S's `connect` pool and accepts the solution
- [x] Confirm session is considered active server-side after handshake

---

### Phase 3 — Basic Single-Timeline Sync with Identity

**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.

#### Phase 3A — Innerspace unit test

- [x] Create `sync-withid.pingpong.respec.mts`
  - [x] Create `r1_alpha_v0_source` via `TestTransformer` (represented by `xStone` test domain ibgib)
  - [x] Call `senderCoordinator.sync({ domainIbGibs: [alpha], senderIdentity, senderSecret, ... })`
  - [x] `ifWeMight` — `"alpha dep graph matches on source and dest"`: use `getDependencyGraph` + `graphsAreEquivalent`
  - [x] `ifWeMight` — `"sessionIdentity evolved once per sync turn"`: assert `S.data.n` equals number of outgoing turns

#### Phase 3B — Space-Gib integrated (manual)

- [x] Trigger a sync of a known test ibgib via `dev-tools.mts` or equivalent UI
- [x] Confirm S evolves on each outgoing turn (inspect frame `data.n` in debugger)
- [x] Confirm X's full dependency graph is present on the client after commit
- [x] Confirm X's full dependency graph is present on the server after commit
- [x] Confirm S's full dependency graph is present on the client after commit
- [x] Confirm S's full dependency graph is present on the server after commit
- [x] Confirm I's full dependency graph is present on the client after commit
- [x] Confirm I's full dependency graph is present on the server after commit

---

### Phase 4 — Advanced Data and Conflict Scenarios

**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.

> [!IMPORTANT]
> **Guidelines & Testing Reminders**:
> - Before starting the implementation of any automated test (A) or manual verification (B) in Phase 4, you **MUST** scan/re-scan the variable naming and structural guidelines in [testing.md](file:///c:/Users/billm/antigravity/ibgib/libs/core-gib/src/sync/docs/testing.md) to align the codebase structure and naming conventions.
> - **Manual Testing (B) Rule**: The AI assistant **MUST NOT** use the browser subagent to perform manual browser/UI testing for any "B" manual verification phases in space-gib. The user will run all B testing themselves to conserve tokens.
> - **Executing Focused Tests**: After changing previous test phase file's ifWeMight blocks -> ifWe to defocus that file, and after you have setup the new test file with ifWeMight blocks, use `npm run test:core-gib` to execute the new *.respec.mts file. (NOTE: If you search files for these blocks, be sure to include the open paren, e.g., "ifWeMight(", because often ifWeMight remains unused in import statements). After you have executed the tests and they pass, do NOT change it back to ifWe blocks immediately, so I can execute and look at the tests myself.

For each scenario:
1. **Automated Verification (A)**: Copy the grandfathered non-identity unit test file (e.g., `sync-innerspace.respec.mts`) to a new file named with `.withid.` in the filename (e.g., `sync-innerspace.withid.respec.mts`). Incorporate identity setup (referencing `sync-withid.pingpong.respec.mts` for setup pattern) and ensure the test runs and passes.
   * **Test naming/structure convention**: The new `.withid.respec.mts` tests must follow the variable and structural naming guidelines outlined in [testing.md](file:///c:/Users/billm/antigravity/ibgib/libs/core-gib/src/sync/docs/testing.md).
   * **Identity Verification Checks**: Each `.withid.respec.mts` test must verify that the session identity `S` evolved the expected number of times, the user identity `I` evolved the expected number of times on both spaces, and that the full dependency graphs for `S` and `I` are correctly synced to both spaces.
2. **Manual Verification (B)**: Create manual websocket peer tests with corresponding buttons/UI elements integrated via Dev Tools.
   * **Dont use browser testing**: We must minimize token use, so I will do all of the manual testing in the browser. Do not do automated browser testing in B phases.

#### 1. Sync InnerSpace (Basic Push Sync)
- [x] Phase 4.1A — Automated InnerSpace Verification (`sync-innerspace.withid.respec.mts`)
- [x] Phase 4.1B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests basic push sync of a single timeline and its mutations from Source to Destination.
  * Verifies that the target timeline tip and its dependency graph successfully arrive at the destination.

#### 2. Sync Constants (No TJP)
- [x] Phase 4.2A — Automated InnerSpace Verification (`sync-innerspace-constants.withid.respec.mts`)
- [x] Phase 4.2B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests syncing "constants" (ibGibs without timelines/TJPs) and verifies that dependencies (linked constants) are resolved.
  * Ensures idempotency/smart diffing (payloads are not re-sent if they already exist in the destination space).

#### 3. Sync Destination with Partial History
- [x] Phase 4.3A — Automated InnerSpace Verification (`sync-innerspace-partial-update.withid.respec.mts`)
- [x] Phase 4.3B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests push synchronization when the destination already contains a partial timeline history (e.g., up to V1), but the sender has newer frames (V2).
  * Verifies that only the delta is transmitted and correctly linked to the existing history at the destination.
- [x] INTERSTITIAL — Refactor `dev-tools.mts` to split it into a `dev-tools` directory containing modular sub-files to reduce file size.

#### 4. Sync Deep History Updates
- [x] Phase 4.4A — Automated InnerSpace Verification (`sync-innerspace-deep-updates.withid.respec.mts`)
- [x] Phase 4.4B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests push synchronization of a timeline tip with a deep mutation history (multiple updates).
  * Verifies that the destination receives all intermediate frames, preserving the full timeline history and graph integrity.

#### 5. Sync Destination Ahead (Remote Newer)
- [x] Phase 4.5A — Automated InnerSpace Verification (`sync-innerspace-dest-ahead.withid.respec.mts`)
- [x] Phase 4.5B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests synchronization when the destination (remote) actually has a newer frame (V2) than the sender's local tip (V1).
  * Verifies that the sender learns of the remote tip and pulls it down so both client and server converge to the dest-ahead tip.

#### 6. Sync Multiple Independent Timelines
- [x] Phase 4.6A — Automated InnerSpace Verification (`sync-innerspace-multiple-timelines.withid.respec.mts`)
- [x] Phase 4.6B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests synchronization of multiple independent timelines (e.g., Timeline A and Timeline B) in a single sync coordinator call.
  * Verifies that both timeline tips and their respective dependency graphs are transferred and updated correctly.

#### 7. Basic Divergence Conflict Resolution
- [x] Phase 4.7A — Automated InnerSpace Verification (`sync-conflict-basic-divergence.withid.respec.mts`)
- [x] Phase 4.7B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests divergence when source and destination edit different fields on a shared base timeline simultaneously.
  * Verifies that optimistic merge automatically produces a merged tip (V3) containing both edits and records the graft metadata (`graftbase`, `graftorphan`).

#### 8. Divergence with Related Timelines
- [x] Phase 4.8A — Automated InnerSpace Verification (`sync-conflict-basic-multitimelines.withid.respec.mts`)
- [x] Phase 4.8B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests advanced divergence where one of the divergent updates also references/relates a new independent timeline (Beta).
  * Verifies that during optimistic merge, the dependent timeline is synced automatically alongside the merged tip.

#### 9. Text Field Merge (LCS) Conflict Resolution
- [x] Phase 4.9A — Automated InnerSpace Verification (`sync-conflict-text-merge.withid.respec.mts`)
- [x] Phase 4.9B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests automatic merging of conflicting string edits within the `ibgib.data.text` field using the LCS (Longest Common Subsequence) merge algorithm.
  * Verifies merging of simple appends (beginning vs. end) and interleaved edits in different paragraphs.

#### 10. Advanced Multi-Round/Timeline Permutations
- [x] Phase 4.10A — Automated InnerSpace Verification (`sync-conflict-adv-multitimelines.withid.respec.mts`)
- [x] Phase 4.10B — WebSocket Peer Manual Verification (integrated via Dev Tools)
  * Tests complex multi-round/timeline permutations with parallel divergent edits across multiple rounds.
  * Verifies that nested or sequential merges resolve correctly using the optimistic graft strategy.
