game1 / docs / MIGRATE-TO-LIBP2P-PROMPT.md
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# PROMPT: migrate a sueta-core game to the js-libp2p stack

> **Historical document (TypeScript era).** Kept as history: this prompt
> served the sueta-core → libp2p migration of the TS-era games, all six of
> which have long since migrated — and the suite has since completed its
> move to ClojureScript, so the TS tree it describes no longer exists.
> Nothing below was updated. Current canon: `dev/docs/CLJS.md` (the
> migration canon) and `docs/NEW-GAME-PROMPT.md` (building a game today).

You are an agentic developer working on a multiplayer browser game repo built
in the ardegazu.ro house style: static Vite + strict-TypeScript client,
`client/src/lib/` vendored from sueta (mesh / signaling / crypto / turn),
rooms keyed by a URL-fragment secret, one peer refereeing, deployed as an
IPFS-pinned site behind an IPNS-linked gateway domain. **neon-grid has already been migrated** — it
is the reference for everything below:

```bash
git clone https://git.ardegazu.ro/game1.git
```

Your job: replace this repo's vendored `client/src/lib/` stack with neon-grid's
libp2p layer, keeping the game code untouched. Do the steps in order and verify
before deploying.

## 0. Fill in per-game values

```
GAME NAME:  <name>            (protocol prefix becomes /<name>/2/…)
DOMAIN:     <name>.ardegazu.ro
APP_SALT:   "<name>.ardegazu.ro/v2"   (bump v1 → v2; never reuse another app's salt)
```

The relay is shared and already running — do NOT create a new p2p app, hosting
changes are not needed, and the origin allowlist already covers
`*.ardegazu.ro` + `http://localhost:4173` + `http://localhost:5173`.

## 1. What the new stack is (context)

- Browsers are js-libp2p (v3) peers. They dial the app's **circuit-relay-v2**
  peer on `signal.ardegazu.ro` over WSS, take a reservation, discover each
  other through gossipsub pubsub-peer-discovery (app-wide), and upgrade to
  direct WebRTC; the circuit is signaling + a 256 KiB/s fallback path.
- The relay's peer id + multiaddrs are fetched at boot from
  `https://signal.ardegazu.ro/.well-known/ap2p` (compiled-in fallback), so a
  relay identity rotation never needs a client release.
- **Rooms**: discovery is app-wide, so rooms are enforced by (a) a room-scoped
  protocol id `/<name>/2/<first 16 chars of HKDF(secret,"roomid")>` — wrong
  rooms fail protocol negotiation, zero cost — and (b) a **mutual sealed
  hello**: each side sends one AES-256-GCM envelope keyed per-sender from the
  room secret, AAD-bound to the room id and both noise-authenticated peer ids.
  Undecryptable ⇒ not a member ⇒ abort + session blacklist.
- After the hello, game frames are plaintext JSON (varint length-prefixed) on
  one long-lived stream per pair — libp2p's noise layer already encrypts every
  pair end-to-end (even across the relay) and authenticates the sender, which
  is stronger than the old per-frame sealing.
- Peer ids are per-pageload Ed25519 PeerIds (`12D3KooW…`, base58). They are
  still plain comparable strings, so lowest-id host election and split-brain
  resolution work unchanged.

## 2. Do the migration

1. **Copy the net layer** from neon-grid:
   ```bash
   cp -R neon-grid/client/src/net <repo>/client/src/net
   rm -rf <repo>/client/src/lib
   ```
   The only game-specific line is in `net/peers.ts`:
   `this.protocol = \`/neon-grid/2/…\`` → `/<name>/2/…`. Everything else
   (`bootstrap.ts`, `room-crypto.ts`, `turn.ts`, `node.ts`, `index.ts`) is
   game-agnostic — keep it identical so future fixes can be re-copied.
2. **Dependencies**: copy the whole `dependencies` block from neon-grid's
   `client/package.json` (libp2p, @libp2p/websockets, @libp2p/webrtc,
   @libp2p/circuit-relay-v2, @chainsafe/libp2p-noise, @chainsafe/libp2p-yamux,
   **@libp2p/gossipsub**, @libp2p/pubsub-peer-discovery, @libp2p/identify,
   @libp2p/utils, @multiformats/multiaddr; dev: @libp2p/interface).
   ⚠️ Gossipsub must be **`@libp2p/gossipsub`** (the js-libp2p org package).
   `@chainsafe/libp2p-gossipsub` is libp2p-v2-only and crashes at runtime on
   v3 — and floodsub won't work either (the relay doesn't speak it).
3. **`client/src/game/config.ts`**: delete `SIGNAL_URL`; bump `APP_SALT` to
   `/v2`; keep `TURN_URL`; add `AP2P_DESCRIPTOR_URL` and `RELAY_FALLBACK`
   copied from neon-grid's config.ts (same shared relay, same values).
4. **`client/src/main.ts`**: mirror neon-grid's main.ts —
   - replace the RoomCrypto/Signaling/Mesh wiring with one `createNet(cfg, callbacks)`
     call; map the five callbacks straight to the game's existing
     `onRoomRoster` / `onRelayError`(now unused) / `onPeerOpen` / `onPeerGone`
     / `handleMsg` (keep the `isGameFrame` guard);
   - keep the game's `NetLike` injection identical (`myId`, `roomPeers`,
     `relayConnected`, `openPeers`, `send`, `broadcast`);
   - add the **simultaneous-boot backstop**: ~5.5 s after boot, if the game
     still has no host, call `game.onRoomRoster([])` to force a claim. This is
     needed because the old relay handed the first joiner an instantly-empty
     roster; libp2p discovery takes 3–6 s, so two peers booting together might
     otherwise never elect a host. Double claims converge via the roster
     frame's precedence rule — **an active round outranks an idle claim, ties
     break to lowest id** (`live?: 0|1` on neon-grid's "ro" frame; copy that
     scheme, including the bystander branch, so a cold-booting low id can't
     void a round in progress).
5. **`vite.config.ts`**: add `chunkSizeWarningLimit: 1500` to `build` (bundle
   grows to ~280 KB gz). Keep `base: "./"`, es2022, port 4173 strictPort.
6. **Check the game code for old-stack assumptions** (grep before assuming):
   - anything that used the mesh's **binary path** (`broadcastBinary`) or
     **media calls** (`attachMedia`/`track`) has no equivalent in `net/` —
     stop and report if this game used them;
   - anything assuming 32-hex peer ids (nick fallbacks that slice ids, fixed
     widths) — base58 ids are longer, slice from the end;
   - the relay's 16-peer `room-full` error no longer exists; if the game needs
     a player cap, enforce it in game code (refuse `hi` past the cap);
   - `roomPeers()` now equals the verified-member set (no "seen on signaling
     but not yet connected" state). Fine if the game only uses it for
     host-gone checks and empty-room host claims; adapt if it leaned on
     richer roster semantics.
   - keep every frame well under 64 KiB (the net layer's frame cap).

## 3. Verify (all on two+ tabs before deploying)

Dev server must be on port 4173 or 5173. Beware: changing only the `#hash`
does NOT reload a tab — always force a real reload when switching rooms.
`window.__p2p` exposes the libp2p node in the console.

1. Two tabs, same `#secret`: lobby shows both within ~6 s, a full round plays,
   scores propagate.
2. `window.__p2p.getConnections()` for the other peer shows a `/webrtc`
   connection carrying the `/<name>/2/…` stream after ~10 s (direct upgrade —
   relayed bytes stop billing).
3. Different `#secret` tab never appears in the roster.
4. Kill the host tab mid-round → survivor becomes host (~15–25 s), round voids,
   next round works.
5. Late joiner mid-round gets the snapshot, spectates, plays next round.
6. Point the descriptor URL at a 404 temporarily → boot still connects via the
   compiled-in fallback; revert.
7. `npm run build` green (strict tsc).

## 4. Deploy & republish

Same pipeline as before, nothing new to create:

1. `cd client && npm run build`
2. `ird ipfs add client/dist --json --yes` → CID, then
   `ird ipfs ipns publish <name>.ardegazu.ro <cid>` (unpin the previous CID).
3. Play a real round on the live URL.
4. Update the repo's README / landing page / docs to describe the libp2p
   architecture (model on neon-grid's), commit **anonymously**
   (`<name> <name@noreply.local>` — the publish gate enforces it),
   then republish the source root:
   `../git/assemble.sh <name>` (runs `deploy/publish-repo.sh`
   with its anonymity gate, pins, re-points git.ardegazu.ro).
5. Verify `git clone https://git.ardegazu.ro/<name>.git` and an
   anonymous-only `git log`.

## 5. Report

State: what was replaced, bundle size before/after, the verification results
(including the `/webrtc` upgrade check), the live URL round, and any game code
that needed adapting beyond the standard steps (binary/media usage, id-format
assumptions, player caps).

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/game1.git