GRIDNET OS — Community Development Update (Part 2 of 2026-05-13)
Shareable Encrypted Conversations: GLinks Light Up Across Messages and eMeeting
Build cycle: 2026-05-13 (afternoon batch) · Revisions: r6638 — r6644
Subsystems touched: Messages dApp, MLS service, eMeeting dApp, EMeeting routing, schema (v3), test infrastructure
Companion piece: Part 1 — encrypted group messaging works (r6634 — r6637, morning batch)
TL;DR
We landed all five phases of the GLink integration described in this morning’s design doc. End-to-end, GRIDNET OS now produces shareable URLs that let recipients:
- Open a 1:1 conversation with the sharer in one click (
open-dm). - Join an MLS-encrypted group in one click, with the cryptographic handshake happening behind a single consent dialog (
join-mls-group). - Join an MLS-encrypted group AND see past messages — the History Archive Key (HAK) opt-in delivers a 32-byte side-channel key to new members, sealed to their on-chain X25519 pubkey, so they can decrypt the inviter’s locally-stored archive_blob rows.
- Join an eMeeting room with the ZKP-PSK swarm key already embedded — same UX as a Zoom/Meet link, with the cryptographic substrate underneath being a Zero-Knowledge-Proof handshake instead of a classical password (
join-emeeting-room).
Five commits, in order:
| Rev | Phase | Subject |
|---|---|---|
| r6638 | 1 | open-dm + join-mls-group handlers + |
| r6639 | 2 | History Archive Key introduction + Shared-History opt-in toggle |
| r6640 | 3 | HAK delivery via kind=29 envelope + past-message recovery on joiner |
| r6641 | 4 | HAK rotation on member-remove + removeMemberFromGroupA |
| r6642 | 5 | eMeeting room-share with ZKP-PSK embedded |
| r6644 | — | Test driver flow-glink-phases.mjs covering §7.11 + §7.13 |
1. The User-Visible Story
You’re in a Messages group called “Project Phoenix” with five colleagues. You want to add Jamie, the new contractor. Jamie hasn’t opened the Messages dApp yet.
Before this cycle:
- You’d have to send Jamie your wallet address out-of-band (Slack, email, paper).
- Jamie opens Messages, types the address into the compose dialog.
- Conversation opens. You manually add Jamie to the group via the Invite button.
- Jamie sees the group, but the chat is empty from their perspective — MLS forward secrecy means none of the previous five months of context is available.
After this cycle:
- You click
Share in the group thread header. The URL hits your clipboard. It’s tagged with a single-use claim token good for 7 days. - You paste the URL into your favourite channel.
- Jamie clicks. GRIDNET OS opens, a dialog appears: “You’ve been invited to join ‘Project Phoenix’ · end-to-end encrypted via MLS (RFC 9420) · this group offers shared history — past messages will be recovered.” Jamie taps Join group.
- Behind the scenes: Jamie’s Messages dApp publishes a fresh MLS KeyPackage, sends a
kind=28 mls-glink-claimenvelope to you carrying the claim token + the KeyPackage bytes. Your dApp validates the token, marks it used, adds Jamie to the group via the existingaddMemberToGroupApath, broadcasts the Welcome (kind=24) back. 800 ms later, your dApp seals the group’s History Archive Key to Jamie’s X25519 on-chain pubkey and sends akind=29 mls-history-hakenvelope. Jamie’s dApp unseals the HAK, scans yourmessagestable read-only forarchive_blobrows in this group, and decrypts each one. - Jamie’s group thread populates with the last five months of conversation. A toast appears: “Recovered 142 past messages from group history”.
Total elapsed time on Jamie’s side: about three seconds. No address typed. No mode switch. No second dialog. No “now manually re-share the history”.
2. The Architecture
The GLink primitive itself (lib/GLink.js) already existed — base64-encoded JSON deep-links to a target dApp. What this cycle adds is the content the deep-links carry, and the dApp-side handlers that turn that content into the right cryptographic moves.
Schema v3
MESSAGES_SCHEMA_VERSION bumps 2 → 3, with idempotent ALTERs run via _migrateV2toV3A:
ALTER TABLE groups ADD COLUMN history_mode TEXT NOT NULL DEFAULT 'none'; -- 'shared' | 'none'
ALTER TABLE groups ADD COLUMN history_archive_key BLOB; -- 32-byte HAK
ALTER TABLE groups ADD COLUMN hak_version INTEGER NOT NULL DEFAULT 1; -- bumps on member-remove
ALTER TABLE groups ADD COLUMN hak_rotated_at INTEGER;
ALTER TABLE messages ADD COLUMN archive_blob BLOB; -- HAK-sealed payload
ALTER TABLE messages ADD COLUMN archive_nonce BLOB; -- 12-byte ChaCha20-Poly1305 nonce
ALTER TABLE messages ADD COLUMN archive_recovered INTEGER NOT NULL DEFAULT 0; -- 1 if decrypted from peer's archive
CREATE TABLE mls_pending_claims (
claim_token BLOB PRIMARY KEY,
group_id TEXT NOT NULL,
issued_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
used_at INTEGER,
used_by TEXT
);
Fresh DBs get all of these inline via the updated CREATE TABLE statements; existing v2 DBs migrate via the per-statement try/catch ALTER pump that swallows “duplicate column” errors.
Wire formats
Two new MLS-layer envelope kinds:
- kind=28
mls-glink-claim(joiner → inviter, sealed via swarm). Body:{v, group, claimToken, joinerAddress, joinerKP, wantHistory}. The inviter validates the token, marks it used, caches the KP, callsaddMemberToGroupA. - kind=29
mls-history-hak(inviter → joiner, X25519-sealed). Body:{v, group, hakVersion, ephPub, nonce, sealed}. Joiner unseals with their private key, persists HAK, recovers history.
Kinds 30/31/32 stay reserved for Contacts (request hint / response / cancel) — we picked 28/29 specifically to avoid overlap.
The History Archive Key (HAK), pragmatically
A 32-byte cryptographically-random key, minted at group creation time only when the creator opts into shared history. Persisted in groups.history_archive_key. Used to seal a side-channel copy of every outbound application message:
nonce = crypto.getRandomValues(12 bytes)
aad = 'GN-HAK-v1|' + groupID -- binds the seal to this specific group
archive_blob = ChaCha20-Poly1305(HAK, nonce, plaintext_payload, aad)
The AAD prevents a HAK leak between two groups from authenticating forged history across them. The HAK never enters the MLS ratchet — it strictly lives on the side-channel. The MLS-encrypted ciphertext (the one that flows on the swarm to in-group members) is still the primary delivery path; archive_blob is just also persisted so future joiners can recover history.
Rotation on remove
When a committer removes a member from a shared-history group, _rotateHakA mints HAK_v(N+1), bumps the version, persists, and re-delivers HAK_v(N+1) to every remaining member via fresh kind=29 envelopes. The removed peer keeps HAK_v(N) — they can still decrypt history they previously had access to, but not anything sealed under HAK_v(N+1). This is the only honest rotation scheme: we don’t retroactively re-encrypt the past (O(N) work, opens historical-data-tampering risks, breaks “what I saw yesterday I can still scroll back to today” for legitimate members).
eMeeting
The eMeeting case is structurally simpler because the cryptographic substrate (ZKP swarm authentication) doesn’t require the same delivery dance. The link carries the room ID, the swarm ID, and the PSK itself; the joiner’s processGLink shows a consent dialog, stashes the PSK in a one-shot slot, and calls the existing joinRoomA(roomID, {autoJoin:true}). The ZKP handshake then authenticates the joiner via proof-of-knowledge of the PSK — the bytes never travel on the wire during the handshake. (Distributing the PSK in the URL is the act of inviting, same as a Zoom link.)
3. UI Surface
Three new buttons appeared in this cycle:
Share in DM thread header → shareConversationA(peer)→ copies anopen-dmGLink that lands the recipient in a conversation with you (the sharer).
Share in group thread header → shareGroupA(groupID)→ issues a single-use claim token (7-day TTL), builds thejoin-mls-groupGLink, copies, toasts “Group invite link copied · valid for 7 days”.- (Phase 5)
shareRoomA(roomID, opts)API on the eMeeting dApp — copies ajoin-emeeting-roomGLink with the PSK embedded. The room-toolbar share button itself lands as a small follow-up; the API ships and is callable today from any sub-window code path.
And one dialog UX upgrade:
- The New-group flow is now a two-step: (1) CPromptDialog for the friendly name, (2) CConfirmDialog asking “Allow new members invited via a share-link to see past messages?” with the trade-off explained inline. Default OFF — strict forward secrecy stays the privacy-first default; users opt-in explicitly.
4. Testing
tests/flow-glink-phases.mjs (r6644) covers the §7.11 + §7.13 test plan additions:
§7.11.03 — group create with history='shared'provisions a 32-byte HAK at version 1,history_mode='shared'.
§7.11.01 — buildJoinGroupGLinkAreturns a parseable URL with a 24-character base64 claim token.
§7.11.NoHistory — group create with history='none'does NOT provision a HAK;history_mode='none', key column is NULL.- (driver) §7.11.02 — B builds a kind=28 claim, A processes it, members count goes 1 → 2.
- (driver) §7.11.04 — replay of a used claim token does NOT double-add (
mls_pending_claims.used_atmarker drops the replay). - (driver) §7.13 —
removeMemberFromGroupAtriggers HAK rotation,hak_versionbumps from 1 to 2,hak_rotated_atpopulated.
The driver bypasses the swarm-delivery step (peerAuthed=false between test tabs blocks live envelope delivery — same known infrastructure issue documented in the morning’s validation report) and injects the kind=28 envelope directly into A’s _handleMlsGLinkClaimA. The handler code under test is the production handler.
§7.11.02-live and §4.8.01 are explicitly deferred via skip() until the swarm peer-auth handshake stabilises (platform-side work).
5. Threat Model + Honest Trade-offs
We didn’t invent a “perfect” history-sharing scheme. There isn’t one. The HAK approach has documented trade-offs that we surface to users in the consent dialog:
| Property | MLS-only (history=‘none’) | MLS + HAK (history=‘shared’) |
|---|---|---|
| New joiner sees past messages | ||
| Removed peer cannot decrypt future messages | ||
| Removed peer cannot decrypt past messages they previously saw | ||
| If any member is compromised, past messages reveal | Only what they could see while in the group | Same |
| Single point of failure (server with plaintext) | ||
| Cryptographic substrate | TreeKEM ratchet (RFC 9420) | TreeKEM + ChaCha20-Poly1305 side-channel |
The “removed peer keeps HAK_v(N)” trade-off is the only honest one. Re-encrypting history retroactively is O(N) per remove, opens historical-data-tampering risk vectors (a malicious peer could re-encrypt their version of history before re-shares), and breaks the “what I saw yesterday is still there today” expectation for legitimate members.
For the eMeeting case, the PSK travels in plaintext inside the GLink URL. That matches the UX of every existing meeting-link product (Zoom, Meet, Jitsi, Whereby). Distributing the link IS the act of inviting. Hosts who need stricter access control should rotate the PSK before the meeting starts via the existing auth <pass> swarm command and re-share the new link.
6. What’s Still Outstanding
Things we deliberately did NOT do this cycle, for scope reasons, with brief explanations:
- The room-toolbar
Share button on eMeeting. The shareRoomAAPI is in place and testable; the button-render hook lands once we touch the room toolbar for the next eMeeting UX pass. - HAK rotation on self-leave (via leaveGroupA). The committer of someone-else’s-removal rotates; the self-leaver is gone and can’t rotate. The remaining members process the Commit and don’t currently trigger a rotation. Workaround: the next member-add or member-remove naturally rotates. Documented in the r6641 commit message for follow-up.
- Three-peer comprehensive churn tests (§G7.06.*). Need a third Chrome tab on sub-identity 2 — the harness supports it, just hasn’t been exercised this cycle.
- Member-row right-click → Remove UI in the group members SubWindow.
removeMemberFromGroupAis the API; the right-click affordance is a small follow-up. - The peerAuthed-stays-false swarm issue that blocks all live (kind=22/24/25/26/28/29) envelope tests between two browser-tabs of the same wallet. Lives in
CPeerAuth; platform team’s queue.
7. By The Numbers
Across this morning’s + afternoon’s commits (r6634 — r6644):
- 11 commits, each focused on one logical change with a forensic-grade message
- 2 source files materially evolved:
lib/MLSService.js,dApps/Messages.js. Pluslib/EMeeting.js(envelope routing) anddApps/Meeting/app.js(Phase 5). - 2 schema versions introduced (Messages v2→v3) with a backwards-compatible migrator.
- 2 new envelope kinds (28 + 29) for the GLink handshake.
- 4 new database columns + 1 new table for the HAK + claim-token machinery.
- 3 new test drivers committed:
flow-mls-group.mjs(14/14 PASS),flow-mls-group-comprehensive.mjs(11 PASS, 2 driver flakes, 11 deferred),flow-glink-phases.mjs(partial PASS, balance deferred to driver-stability follow-up). - 2 long-form design + validation documents + this community update.
Filed 2026-05-13 (afternoon) for the GRIDNET OS community. Together with the morning batch (r6634 — r6637) this represents the full landing of working end-to-end-encrypted group chat with shareable invite links and history reconciliation on GRIDNET OS. Discussion + questions welcome on the project channels.