GRIDNET OS — Community Development Update (Part 2 of 2026-05-13)

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 + :link: Share buttons + schema v3
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:

  1. You’d have to send Jamie your wallet address out-of-band (Slack, email, paper).
  2. Jamie opens Messages, types the address into the compose dialog.
  3. Conversation opens. You manually add Jamie to the group via the Invite button.
  4. 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:

  1. You click :link: Share in the group thread header. The URL hits your clipboard. It’s tagged with a single-use claim token good for 7 days.
  2. You paste the URL into your favourite channel.
  3. 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.
  4. Behind the scenes: Jamie’s Messages dApp publishes a fresh MLS KeyPackage, sends a kind=28 mls-glink-claim envelope to you carrying the claim token + the KeyPackage bytes. Your dApp validates the token, marks it used, adds Jamie to the group via the existing addMemberToGroupA path, 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 a kind=29 mls-history-hak envelope. Jamie’s dApp unseals the HAK, scans your messages table read-only for archive_blob rows in this group, and decrypts each one.
  5. 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, calls addMemberToGroupA.
  • 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:

  • :link: Share in DM thread header → shareConversationA(peer) → copies an open-dm GLink that lands the recipient in a conversation with you (the sharer).
  • :link: Share in group thread header → shareGroupA(groupID) → issues a single-use claim token (7-day TTL), builds the join-mls-group GLink, copies, toasts “Group invite link copied · valid for 7 days”.
  • (Phase 5) shareRoomA(roomID, opts) API on the eMeeting dApp — copies a join-emeeting-room GLink 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:

  • :white_check_mark: §7.11.03 — group create with history='shared' provisions a 32-byte HAK at version 1, history_mode='shared'.
  • :white_check_mark: §7.11.01buildJoinGroupGLinkA returns a parseable URL with a 24-character base64 claim token.
  • :white_check_mark: §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_at marker drops the replay).
  • (driver) §7.13removeMemberFromGroupA triggers HAK rotation, hak_version bumps from 1 to 2, hak_rotated_at populated.

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 :cross_mark: No (RFC 9420 invariant) :white_check_mark: Yes
Removed peer cannot decrypt future messages :white_check_mark: Yes (HAK rotates) :white_check_mark: Yes
Removed peer cannot decrypt past messages they previously saw :cross_mark: N/A :cross_mark: No (they kept HAK_v(N))
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) :cross_mark: None :cross_mark: None
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 :link: Share button on eMeeting. The shareRoomA API 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. removeMemberFromGroupA is 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. Plus lib/EMeeting.js (envelope routing) and dApps/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.