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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234 | # Changelog for projects built on this stack
One entry per shipped version, oriented at **forks**: what changed, whether it
breaks wire/storage compatibility, and what your app must do to follow. The
guided v9→v12 migration (for apps started from the v1 websocket-era lib) is
[UPGRADING-9-to-12.md](UPGRADING-9-to-12.md); this file is the running record
from that baseline onward. Versions are `client/version.json` integers, shown
in the UI header and served at `/version.json`.
Protocol wire-compat rule of thumb: everything since v10 speaks **protocol
v2** (PROTOCOL.md) and interoperates; v13 adds a validation clients older
than v13 don't enforce, so upgrade rather than pin.
## unreleased · room lists ride the sealed self channel too
The room directory gains a second sync transport: ardegazu-social-kit's self
`fsync` (the `appState {get, apply}` opt-in on `attachSocial` + `syncNow()`
after any list change). Devices holding the same seed now converge through the
identity's own sealed mailbox drop even when they never share a live room; the
directed in-room `roomsync` (v15) stays unchanged, and both feed the same
merge. Kit 1.2.0 also closes a hole: fsync envelopes are applied only when
sent by the receiving identity itself (identity inboxes are publicly
derivable). PROTOCOL.md §6 documents both transports.
## v17 — 2026-08-25 · one identity across the suite (id-kit + the identity bridge)
`lib/identity.ts` moved verbatim into the shared **ardegazu-id-kit** package
(`git+https://git.ardegazu.ro/id-kit.git#v1.0.0`, sha-pinned by npm; plain TS
source, so `optimizeDeps.exclude` it). Binding bytes are unchanged — old and
new clients verify identical assertions, **wire-compatible both ways**.
Boot goes through the **identity bridge**: a hidden same-site iframe at
`https://ardegazu.ro/id/` holding one suite-wide record (seed + profile) on
the apex origin, raced against a 2.5 s timeout with this app's own
`<ns>:id` / `<ns>:name` keys as the local mirror — offline PWA boots are
byte-for-byte the old behavior. Reconciliation rules: adopt when the mirror is
empty, publish when the bridge is empty, and a different-seed conflict
surfaces an explicit chooser in the identity card (adopt suite-wide with the
old seed backed up to `<ns>:id-prev`, or keep local) — never a silent switch.
Profile accents ride the identity: `hello`/`name` frames gain optional
`hue`/`glyph` fields (pre-v17 clients ignore them), message avatars use the
identity-derived hue (stable across sessions — the same h*31 hash, now over
the author pub instead of the session peerId) and chosen glyph, and renames
push through the bridge so every suite app renames at once.
Fork checklist: depend on `ardegazu-id-kit`, delete your vendored
`lib/identity.ts`, boot via `IdBridge` with your NS, and pass profile accents
into your `SelfAssertion`. Storage adds `<ns>:profile`, `<ns>:id-prev`,
`<ns>:id-conflict-dismissed`.
## v16 — 2026-08-21 · offline delivery via the relay node's mailbox
A message posted to an empty room now reaches members who are never online
together with the author. The relay node's per-room store-and-forward
**mailbox** (bearer-token-gated, SHA-256-deduped, 168 h / 512 MB ring buffer)
carries the SAME sealed blocks replication does — kEntry-sealed log entries,
kImg-sealed image blocks, and (kMbx-sealed for transit) identity records —
deposited author-only through a persistent queue and replayed by cursor on
the next visit. The log stays the single source of truth; the node stores
only ciphertext. PROTOCOL.md §9 is the full spec; §2/§3 gain the `mbxRoom` /
`kMbx` derivations and the `…|mbx` AAD; §8 is rewritten honestly (the node
now holds sealed room history for up to the retention window).
- **Opt-in via descriptor**: the feature exists only when `/.well-known/ap2p`
has a `mailbox` block (managed apps: `mailbox_enabled: true` + `mailbox_*`
limit knobs — see RELAY.md). No block ⇒ the code path is never constructed.
**Wire-compatible v10+ both ways**; pre-v16 clients simply don't deposit or
replay. Dev override: `VITE_MAILBOX_URL` + `VITE_MAILBOX_CREDS_URL`.
- **New module** `lib/mailbox.ts` (auth mint/cache, typed HTTP client, sync
orchestrator); `lib/log.ts` gains the export/import surface (verbatim
sealed-block export; verified ingest with a local-only reachability
pre-check so a join never hits a 30 s bitswap timeout; display-only
projection + persistent retry list for entries whose ancestors expired from
the ring). Replay is two-phase: store all blocks, then join in seq order.
Everything is caught — a mailbox failure can never break live chat.
- **Chunker change**: with the mailbox on, new images are chunked with
`fixedSize(min((max_message_kb−1)·1024, 61440))` so every unixfs block fits
one mailbox frame (`ipfs-unixfs-importer` promoted to a direct dep). Old
images are untouched — the CID travels in the entry, no compat break.
Mailbox-ingested entry blocks and image roots are pinned so gc can't eat
blocks that may have no online source.
- **Storage**: per-room localStorage keys `mbx-cursor|queue|sent|retry|ident`
(all `nsKey()`-namespaced). Concurrent tabs: last-write-wins keys +
idempotent consumers — worst case is duplicate billed bytes, never data
loss (a BroadcastChannel lock is a possible follow-up).
- **Forks**: nothing to do if your relay has no mailbox. To adopt it: enable
the mailbox on your node, and your existing v10+ clients keep working while
v16+ clients start depositing. E2e: `e2e/mailbox.e2e.mjs` (self-contained;
spawns its own vite/relay/stub; `MAILBOX_LIVE=1` runs the core scenario
against your production node).
## v15 — 2026-08-18 · iOS-PWA polish + same-identity room sync
UX release: the installed-PWA experience stops fighting the iOS keyboard, and
devices sharing one identity finally share their room lists.
- **Keyboard-aware viewport** (`app/ui.ts`, `app/style.css`, `index.html`):
the visual viewport is tracked on `resize` AND `scroll` (standalone iOS
reports the keyboard as a scroll), any layout-viewport shift the
`scrollTo(0,0)` pin can't undo is compensated by translating `.app` by
`--vvt`, and overlays size to `--vvh` instead of `inset: 0` — modals center
in the *visible* area above the keyboard, not the covered layout viewport.
Viewport meta gains `interactive-widget=resizes-content` (Android).
- **No name prompt on reload**: a stored display name joins silently; the join
modal appears only on first use. Renaming moved to the identity sheet
(roster → your row). E2e reload flows no longer expect `#name-in`.
- **Narrow-screen centering**: modal widths were `min(9Xvw, …)` inside a
padded overlay — off-center below ~440px; now `min(100%, …)` +
`min-width: 0` (grid items refuse to shrink below min-content otherwise).
Header brand truncates instead of clipping the icon row; version badge
hides under 480px.
- **Same-identity room sync** (PROTOCOL.md §6 `roomsync`): when two devices
holding the same identity seed meet in any shared room, they exchange room
directories over the directed pairwise-encrypted stream — never broadcast —
gated on the §7b verified assertion, once per peer session, every field
re-validated. Forgets propagate via tombstones (`<NS>:rooms-gone`); a rejoin
outranks the forget it overrides even across skewed clocks; the open room
can never be tombstoned. Room entries gain optional `lts` (label-rename
timestamp) so a rename beats a mere visit.
**Forks**: pull `app/ui.ts`/`app/style.css`/`app/rooms.ts`/`app/chat.ts` +
`room.ts` wiring. Wire-compatible with v10+ (`roomsync` is ignored by older
clients; note the identity seed now also grants the room list — mention it
wherever you tell users to back up the seed).
## v14 — 2026-08-18 · strangers never touch TURN
Fixes a live failure: with `#secret&relay`, dials to discovery-topic
strangers (other rooms of your app) were relay-forced too, all sharing one
cached coturn username — ~7 stranger dials could exhaust `user-quota=16` and
starve call media (`486 TURN allocate error`).
- Discovered peers are dialed **circuit-only** (no ICE, no TURN allocation,
no candidates revealed). Membership proof is a **directed sealed presence
beacon** on `/sueta/2/msg/1.0`; only proven members get the WebRTC upgrade
dial. Directed streams now run on limited (circuit) connections; gossipsub
deliberately does not, so an established mesh still survives relay loss.
- Privacy upgrade for free: a stranger who dials you (or whom you dial) never
sees any ICE candidate — nothing beyond the ephemeral PeerId it already had.
- `deploy/turnserver.conf`: `user-quota` 16 → 32 with sizing guidance
(budget ≥ 2× room size).
**Forks**: pull `client/src/lib/net.ts`; if you self-host TURN, raise
`user-quota`. Wire-compatible with v10+ (old clients still dial strangers
with webrtc — the fleet quiets down as they upgrade).
## v13 — 2026-08-18 · security + reuse hardening
The deep-review release. Full detail in the commit (`4b54270`) and README §v13.
- **Authorship forgery closed** (the important one): upstream OrbitDB never
checks that an entry's signing key IS the referenced identity's key, so any
room member could publish entries attributed to another member — forged
fingerprint badge included. `lib/access.ts` wraps the IPFS access
controller with that binding; `#authorOf` double-checks it. The wrapper is
**address-identical** — v13 and older clients open the same databases —
but pre-v13 clients still render forged authorship, so upgrade.
- **Derived storage namespace**: all localStorage/IndexedDB names derive from
`APP_SALT` (`VITE_STORAGE_NS` overrides; `client/src/config.ts`). Two forks
can now share an origin. Existing installs migrate automatically once
(`app/migrate-storage.ts`).
- **Per-device authorship without an identity**: identity-less users' entries
chain to OrbitDB's default per-device key — reactions toggle correctly; no
fingerprint badge (unchanged).
- Correctness: replication no longer races the projector (entry hooks are
passed INTO `RoomLog.open`), getUserMedia re-entrancy guards (no orphaned
hot mic), image-fetch timeouts (one dead CID no longer stalls all images),
own-reaction highlight fixed (author-id vs PeerId confusion).
- Hardening: notification gate for replayed history, remote timestamp clamp,
null-prototype TOFU store, anchored invite-link matcher, relay per-IP
connection cap + CLI validation, bounded decrypt-key cache, `Net.close()`
removes its window listeners, O(n) thread rendering.
- Template hygiene: `@libp2p/crypto`/`@libp2p/peer-id` exact-pinned (caret
drift silently breaks gossipsub), `engines: node>=22`, de-branded UI copy,
loud realm/hairpin warnings in `turnserver.conf`, expired images render a
placeholder, bundle-visualizer report moved out of `dist/`.
- Docs: the relayed-fallback state is described honestly everywhere; `legacy`
op injection surface documented (forks without a v1 should drop the op).
**Forks**: take the whole release — the access-controller fix and the
`RoomLog.open` signature change land together. If you fork from v13+, set
`VITE_APP_SALT` and you get collision-free storage automatically.
## v12 — 2026-08-17 · bitswap only, never public gateways
Helia's DEFAULTS race every block fetch against public HTTP gateways
(trustless-gateway.link, 4everland.io), leaking CIDs + client IPs to third
parties. `openHelia` pins `blockBrokers: [bitswap()]` +
`routers: [libp2pRouting(libp2p)]`; the mesh e2e asserts zero third-party
requests. **Forks**: never re-add gateway brokers; if you construct Helia
yourself, copy this configuration.
## v11 — 2026-08-17 · branded infrastructure defaults
Baked relay/TURN defaults moved to the app's own domain
(`signal.ardegazu.ro`), which serves a host-aware `/.well-known/ap2p`
descriptor (same relay PeerId). Clients re-read the descriptor at boot, so
operators can rotate relay keys/TURN endpoints without a client release.
**Forks**: point `VITE_RELAY_MULTIADDR`/`VITE_TURN_CREDS_URL` at your own
host; serve the descriptor if you want key rotation.
## v10 — 2026-08-17 · the libp2p + OrbitDB cutover (protocol v2)
The big one — the entire v1 custom-websocket signaling + datachannel mesh
replaced wholesale (four phases, `9923109…f250d5b`):
- Transport: one libp2p node per session (websockets → relay, webRTC
browser↔browser, circuit-relay-v2, noise, yamux, gossipsub). Presence =
sealed beacons on a derived room topic; membership = decryptability.
- Durable state: the room IS an OrbitDB events DB on Helia — deterministic
address from the secret, whole-entry encryption, entry hashes as message
ids, replication as history sync, images as encrypted unixfs blobs over
bitswap.
- Calls: media-only RTCPeerConnections per pair, signaling as kSig-sealed
envelopes over a libp2p stream (`/sueta/2/call-sig/1.0`).
- The Go relay server is gone; dev/self-host relay is
`deploy/relay/relay.mjs` (~100 lines).
- v1 and v2 clients are mutually invisible (the salt forks every
derivation); room links survive. Last v1 state: tag `v1-ws-final`.
- Shortly before the cutover, v1-wire production signaling+TURN had moved to
the managed ird p2p service (`1e6b11d`) — same wire protocol, different
operator.
**Forks on the v1 lib**: follow [UPGRADING-9-to-12.md](UPGRADING-9-to-12.md)
— it is written as a self-contained migration prompt, including the locked
dependency matrix (libp2p 2.x line ONLY; gossipsub silently breaks on 3.x).
## v9 — 2026-08-15 · baseline
Last polish of the v1 (websocket signaling) line: media-detach race fixed
(per-peer media queue), media carried across pc rebuilds, one-shot
Ubuntu/Debian self-host installer. Everything before v10 speaks the v1 wire
protocol and cannot talk to v10+ clients.
|