| … | … | @@ -0,0 +1,112 @@ |
| 1 | # Bootstrap: Global Registry Room |
| 2 | |
| 3 | One-time setup. Run on tabertron only. The resulting two strings get baked |
| 4 | into godot-keet's `autoloads/keet_service.gd` and hyper-tps's `lib/tui.js` |
| 5 | as `GLOBAL_REGISTRY_ROOM_ID` and `GLOBAL_REGISTRY_INVITE`. |
| 6 | |
| 7 | ## Why hardcoded |
| 8 | |
| 9 | Every user auto-joins this room on first launch via a stateless `readOnly: true` |
| 10 | keet invite — bypasses needing a server-side discovery service while giving |
| 11 | new users somewhere to land. To list rooms inside, a separate WRITE invite |
| 12 | goes to trusted curators. |
| 13 | |
| 14 | ## Procedure |
| 15 | |
| 16 | On tabertron (the curator-of-record machine): |
| 17 | |
| 18 | 1. **Create the registry room** via godot-keet UI (Room Settings → New) or the |
| 19 | hyper-tps TUI (`/create`). Title: `Hyper-Fleeky Global Registry`. |
| 20 | |
| 21 | 2. **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 | |
| 29 | 3. **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 | |
| 37 | 4. **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 | |
| 45 | 5. **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 | |
| 57 | 6. **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 | |
| 63 | 7. **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 | |
| 68 | 1. Create a NEW registry room. Get a NEW readOnly invite. |
| 69 | 2. 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. |
| 73 | 3. 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 | |
| 90 | The keet-core code path for `createInvitation({ readOnly: true })`: |
| 91 | |
| 92 | ```js |
| 93 | // keet-core/index.js _createInvitation |
| 94 | if (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 | |
| 111 | This is exactly the "permanent subscribe link anyone can use" semantic we |
| 112 | want for a public discovery room. |