OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet commits / b391b46

keet_service: auto-join hardcoded global registry on first connect
Every godot-keet install now auto-joins the canonical global registry room (created via the keet-daemon bootstrap script on tabertron) using a permanent readOnly invite baked into the source. Anyone can subscribe; adding rooms to the registry requires a separate write invite handed out narrowly to curators. Two constants in keet_service.gd: GLOBAL_REGISTRY_ROOM_ID — the registry room's z32 id GLOBAL_REGISTRY_INVITE — the permanent readOnly subscribe invite On _on_connected: - Check via discovery.list_registries() whether already a member - If not, call room.join with the invite (fire-and-forget, non-fatal) Once joined, the room appears in the GroupBar like any other registry. Curators (write invite holders) can populate it with room listings via the existing room-settings UI. See docs/bootstrap-global-registry.md for the procedure to recreate / rotate the registry, the rationale for readOnly: true as the right primitive, and the curator key management story. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
b391b467e17d52a88252bb9664b9ffb6a7175365
by plankatron <fleeky@gmail.com> · 2026-05-13T17:25:35.000Z

Changed files (2) +150 -0

autoloads/keet_service.gd +38 -0
……@@ -3,10 +3,21 @@ extends Node
33## Daemon lifecycle is handled by the Pear launcher (index.js).
44## Godot just connects to the socket with auto-reconnect.
55
6# --- Global Registry (hardcoded) ---
7# The public "discovery" registry every user auto-joins on first launch.
8# Anyone holding GLOBAL_REGISTRY_INVITE can subscribe (read-only). To list
9# new rooms in the global registry, the curator hands out a separate WRITE
10# invite. To rotate the registry, ship a new build with a new invite string.
11# Bootstrap on tabertron: see docs/bootstrap-global-registry.md.
12const GLOBAL_REGISTRY_ROOM_ID := "8yphx5a55cen3cwzt34z5mcoc5bi31agguuaijjjqmnbimr676eo"
13const GLOBAL_REGISTRY_INVITE := "dnyuog687hp7srbcskmah7m7isegpo4h3cddj3hkwwwzfty4i1xq9rmboacsam5wdxcy8zp4wobfkzooqqojuoxb4db699a1m1wrcgrzjgwqif6hptmtetbgnrebe3mssnbeixda61kkzowfhi4tpsjsa63khymdrhsft8m8uq8p4mwghmsa5o1ikcmci8g397n31wb9mzzkbsostybitto3itghfndqdffwiowagz98qmg4"
14
615signal room_selected(room_id: String, title: String)
716signal daemon_status_changed(status: String) # "connecting", "running", "disconnected"
817signal group_view_changed(group_id: String) # "" = home view, else registry key
918
19var _global_registry_attempted: bool = false
20
1021var client: KeetUdsClient
1122var room_mgr: KeetRoom
1223var chat: KeetChat
……@@ -89,6 +100,7 @@ func select_room(room_id: String) -> void:
89100func _on_connected() -> void:
90101 daemon_status_changed.emit("running")
91102 room_mgr.list_rooms()
103 _maybe_join_global_registry()
92104 if _setup_shown:
93105 return
94106 # Check if identity needs first-run or vault setup
……@@ -110,6 +122,32 @@ func _on_connected() -> void:
110122 )
111123
112124
125## Try to join the hardcoded global registry room exactly once per session.
126## No-op if either constant is empty (pre-bootstrap), or if the user has
127## already joined. Failures are non-fatal — logged + swallowed.
128func _maybe_join_global_registry() -> void:
129 if _global_registry_attempted:
130 return
131 if GLOBAL_REGISTRY_ROOM_ID == "" or GLOBAL_REGISTRY_INVITE == "":
132 return # not bootstrapped yet
133 _global_registry_attempted = true
134 # Check whether we're already in the registry.
135 discovery.list_registries(func(regs: Array):
136 for r in regs:
137 var rid: String = r.get("roomId", "") if r is Dictionary else ""
138 if rid == GLOBAL_REGISTRY_ROOM_ID:
139 return # already joined
140 # Not in — join via the readOnly invite.
141 client.call_rpc("room.join", {"invite": GLOBAL_REGISTRY_INVITE}, func(_result, err):
142 if err != null:
143 print("[keet] global-registry join failed: ", err)
144 else:
145 print("[keet] joined global registry ", GLOBAL_REGISTRY_ROOM_ID)
146 room_mgr.list_rooms() # refresh so UI picks it up
147 )
148 )
149
150
113151func _show_setup_dialog(has_vault: bool = false, existing_username: String = "") -> void:
114152 if _setup_shown:
115153 return
docs/bootstrap-global-registry.md +112 -0
……@@ -0,0 +1,112 @@
1# Bootstrap: Global Registry Room
2
3One-time setup. Run on tabertron only. The resulting two strings get baked
4into godot-keet's `autoloads/keet_service.gd` and hyper-tps's `lib/tui.js`
5as `GLOBAL_REGISTRY_ROOM_ID` and `GLOBAL_REGISTRY_INVITE`.
6
7## Why hardcoded
8
9Every user auto-joins this room on first launch via a stateless `readOnly: true`
10keet invite — bypasses needing a server-side discovery service while giving
11new users somewhere to land. To list rooms inside, a separate WRITE invite
12goes to trusted curators.
13
14## Procedure
15
16On tabertron (the curator-of-record machine):
17
181. **Create the registry room** via godot-keet UI (Room Settings → New) or the
19 hyper-tps TUI (`/create`). Title: `Hyper-Fleeky Global Registry`.
20
212. **Mark it as type=registry** so it shows up in the GroupBar's registry
22 listing:
23 ```
24 $ keet-cli call meta.set '{"roomId":"<ROOM_ID>","metadata":{"type":"registry","topics":["public","discovery"],"discoverable":true}}'
25 ```
26 Or via godot-keet Room Settings → metadata fields (after Phase B of
27 SYSTEMATIC_PLAN.md is implemented).
28
293. **Create the readOnly subscribe invite** (this is what every user joins
30 with). Permanent — never expires, no per-use tracking:
31 ```
32 $ keet-cli call room.invite '{"roomId":"<ROOM_ID>","readOnly":true}'
33 # → { "token": "<z32-string>" }
34 ```
35 Save the token: **this is `GLOBAL_REGISTRY_INVITE`**.
36
374. **Create a separate WRITE invite** for curators (only people who can `add`
38 rooms to the registry). Permanent, but DOES get dispatched into the
39 room's autobase so it's revocable:
40 ```
41 $ keet-cli call room.invite '{"roomId":"<ROOM_ID>","role":"member"}'
42 ```
43 Save this somewhere private. Hand it out narrowly.
44
455. **Wire the constants** into both apps:
46 - `godot-keet/autoloads/keet_service.gd`:
47 ```
48 const GLOBAL_REGISTRY_ROOM_ID := "<ROOM_ID>"
49 const GLOBAL_REGISTRY_INVITE := "<readonly token from step 3>"
50 ```
51 - `hyper-tps/lib/tui.js`:
52 ```js
53 const GLOBAL_REGISTRY_ROOM_ID = '<ROOM_ID>'
54 const GLOBAL_REGISTRY_INVITE = '<readonly token from step 3>'
55 ```
56
576. **Stage tabertron** as the canonical writer-of-record by registering a
58 few seed rooms inside the registry (gives new joiners something to see):
59 ```
60 $ keet-cli call registry.add '{"registryRoomId":"<ROOM_ID>","roomId":"<some-public-room-id>","title":"...","topics":[...]}'
61 ```
62
637. **Commit + publish both apps**. New installs (and updates) auto-join on
64 next `connected` event.
65
66## Rotation procedure (if you need to abandon the current registry)
67
681. Create a NEW registry room. Get a NEW readOnly invite.
692. Add a SECOND constant `GLOBAL_REGISTRY_INVITE_LEGACY` set to the old
70 invite. Modify `_maybe_join_global_registry` to try both — if the user
71 isn't in the new one, join. Keep the legacy join logic for one release
72 so existing users land in both registries during the transition.
733. Stop adding listings to the legacy. After ~2 releases, drop legacy
74 support entirely.
75
76## Failure modes
77
78- **Tabertron loses identity** → can no longer add listings to the registry,
79 but the room itself keeps working as long as ANY writer (curators added
80 via step 4) is alive. Add curators on multiple machines from the start.
81- **All curators offline** → readers can still see the existing listings
82 (registry room replicates fully on join). Nothing breaks; just no new
83 rooms get listed.
84- **Registry room "broken"** in keet-core's broken-room tracker → users
85 joining for the first time get an `AUTOBASE_ERROR`. No recovery short of
86 rotation. This is the same risk every keet room carries.
87
88## Why `readOnly: true` is the right primitive
89
90The keet-core code path for `createInvitation({ readOnly: true })`:
91
92```js
93// keet-core/index.js _createInvitation
94if (readOnly) {
95 return oe('@keet/sendable-invitation', {
96 readonly: { roomKey, encryptionKey, fastForwardTo, seed },
97 additionalNodes
98 })
99 // NOTE: no `await e.dispatch('@keet/add-invitation', ...)` — stateless.
100}
101```
102
103- **No dispatch** → invite is never written into the room's autobase. No
104 per-use record, no anchor, no revocation surface.
105- **No expiration field** → permanent by construction.
106- **Unlimited uses** → no anchor means no usage tracking.
107- **Read-only on the receiver side** → joined users cannot dispatch into
108 the room. They can read all listings and forward them, but cannot ADD
109 new listings without a separate write invite.
110
111This is exactly the "permanent subscribe link anyone can use" semantic we
112want for a public discovery room.