Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: reorganise lexicons

Hugo 3236f75a 2b47e8ce

+157 -133
+17 -15
ARCHITECTURE.md
··· 129 129 130 130 All AT Protocol record types are defined as Lexicon JSON files: 131 131 132 - | Lexicon ID | Published by | Purpose | 133 - | ------------------------------------------ | ------------ | ------------------------------------------------------------------------ | 134 - | `site.exosphere.sphere` | Sphere owner | Sphere declaration (name, slug, visibility, modules) — enables discovery | 135 - | `site.exosphere.sphereMember` | Member | "I am a member of this Sphere" — member-side of bilateral membership | 136 - | `site.exosphere.sphereMemberApproval` | Owner/admin | "This user is an approved member" — admin-side of bilateral membership | 137 - | `site.exosphere.featureRequest` | Author | Feature request content | 138 - | `site.exosphere.featureRequestVote` | Voter | Upvote on a feature request | 139 - | `site.exosphere.featureRequestComment` | Commenter | Comment on a feature request | 140 - | `site.exosphere.featureRequestCommentVote` | Voter | Upvote on a comment | 141 - | `site.exosphere.featureRequestStatus` | Admin/owner | Status change on a feature request | 142 - | `site.exosphere.moderation` | Admin/owner | Moderation action on any content (e.g. comment removal) | 132 + | Lexicon ID | Published by | Purpose | 133 + | ------------------------------------------- | ------------ | ------------------------------------------------------------------------ | 134 + | `site.exosphere.sphere` | Sphere owner | Sphere declaration (name, slug, visibility, modules) — enables discovery | 135 + | `site.exosphere.sphere.member` | Member | "I am a member of this Sphere" — member-side of bilateral membership | 136 + | `site.exosphere.sphere.memberApproval` | Owner/admin | "This user is an approved member" — admin-side of bilateral membership | 137 + | `site.exosphere.sphere.permissions` | Sphere owner | Permission overrides for core Sphere actions | 138 + | `site.exosphere.featureRequest` | Author | Feature request content | 139 + | `site.exosphere.featureRequest.vote` | Voter | Upvote on a feature request | 140 + | `site.exosphere.featureRequest.comment` | Commenter | Comment on a feature request | 141 + | `site.exosphere.featureRequest.commentVote` | Voter | Upvote on a comment | 142 + | `site.exosphere.featureRequest.status` | Admin/owner | Status change on a feature request | 143 + | `site.exosphere.featureRequest.permissions` | Sphere owner | Permission overrides for the feature-requests module | 144 + | `site.exosphere.moderation` | Admin/owner | Moderation action on any content (e.g. comment removal) | 143 145 144 146 ## Data Ownership 145 147 ··· 277 279 278 280 Membership is represented by two complementary AT Protocol records: 279 281 280 - 1. **`site.exosphere.sphereMemberApproval`** — published on the **owner/admin's PDS** when they invite or approve a member. Contains the Sphere AT URI, the member's DID, and their role. 281 - 2. **`site.exosphere.sphereMember`** — published on the **member's PDS** when they accept the invitation. Contains the Sphere AT URI. 282 + 1. **`site.exosphere.sphere.memberApproval`** — published on the **owner/admin's PDS** when they invite or approve a member. Contains the Sphere AT URI, the member's DID, and their role. 283 + 2. **`site.exosphere.sphere.member`** — published on the **member's PDS** when they accept the invitation. Contains the Sphere AT URI. 282 284 283 285 Both records must exist for a membership to be considered fully established. This mirrors how AT Protocol handles social relationships (e.g. follows) and enables third-party indexers to reconstruct the full membership graph from PDS data alone. 284 286 ··· 298 300 299 301 1. A Sphere admin invites a user by their AT Protocol handle or DID. 300 302 2. The handle is resolved to a DID (if needed). 301 - 3. The admin publishes a `sphereMemberApproval` record on their PDS. 303 + 3. The admin publishes a `sphere.memberApproval` record on their PDS. 302 304 4. An invitation record is created in the local membership table (status: invited). 303 305 5. The invited user accepts via the Sphere UI. 304 - 6. On acceptance, the user publishes a `sphereMember` record on their PDS. 306 + 6. On acceptance, the user publishes a `sphere.member` record on their PDS. 305 307 7. Local status is set to active and the user can participate according to the Sphere's write access mode. 306 308 307 309 ### Private Spheres and membership privacy
+5
CLAUDE.md
··· 34 34 35 35 See `ARCHITECTURE.md` for full details on data flow, AT Protocol integration, membership model, and access modes. 36 36 37 + ### Lexicons 38 + 39 + The project lexicons are located in `../landing` and hosted on tnagled: 40 + https://tangled.org/exosphere.site/landing. 41 + 37 42 ## TS/TSX coding style 38 43 39 44 - Strong typing and type safety
+7 -7
packages/app/src/pages/sphere-members.tsx
··· 31 31 if (!data || !handle) return null; 32 32 // Allow access if user has any member management permission 33 33 const canManageMembers = 34 - canDo("sphere", "invite-member") || 35 - canDo("sphere", "revoke-member") || 36 - canDo("sphere", "update-member-role"); 34 + canDo("sphere", "inviteMember") || 35 + canDo("sphere", "revokeMember") || 36 + canDo("sphere", "updateMemberRole"); 37 37 if (!canManageMembers) return null; 38 38 39 39 return <MembersContent handle={handle} sphereName={data.sphere.name} />; ··· 116 116 </div> 117 117 {m.role !== "owner" && 118 118 m.status === "active" && 119 - (canDo("sphere", "update-member-role") || canDo("sphere", "revoke-member")) && ( 119 + (canDo("sphere", "updateMemberRole") || canDo("sphere", "revokeMember")) && ( 120 120 <div class={ui.cluster}> 121 - {canDo("sphere", "update-member-role") && ( 121 + {canDo("sphere", "updateMemberRole") && ( 122 122 <> 123 123 <select 124 124 class={ui.selectCompact} ··· 157 157 )} 158 158 </> 159 159 )} 160 - {canDo("sphere", "revoke-member") && ( 160 + {canDo("sphere", "revokeMember") && ( 161 161 <button class={ui.buttonDanger} onClick={() => handleRevoke(m.did)}> 162 162 Revoke 163 163 </button> ··· 171 171 </div> 172 172 )} 173 173 174 - {canDo("sphere", "invite-member") && ( 174 + {canDo("sphere", "inviteMember") && ( 175 175 <div class={ui.section}> 176 176 <form onSubmit={handleInvite} class={ui.stack}> 177 177 <h2 class={ui.sectionTitle}>Invite member</h2>
+1 -1
packages/app/src/pages/sphere-permissions.tsx
··· 26 26 const handle = sphereHandle.value; 27 27 28 28 if (!data || !handle) return null; 29 - if (!canDo("sphere", "update-permissions")) return null; 29 + if (!canDo("sphere", "updatePermissions")) return null; 30 30 31 31 return <PermissionsContent handle={handle} sphereName={data.sphere.name} />; 32 32 }
+7 -7
packages/app/src/pages/sphere.tsx
··· 32 32 const modules = useQuery(() => getSphereModules(handle), [handle]); 33 33 34 34 const canManageMembers = 35 - canDo("sphere", "invite-member") || 36 - canDo("sphere", "revoke-member") || 37 - canDo("sphere", "update-member-role"); 35 + canDo("sphere", "inviteMember") || 36 + canDo("sphere", "revokeMember") || 37 + canDo("sphere", "updateMemberRole"); 38 38 39 39 const enableModule = async (moduleName: string) => { 40 40 await apiEnableModule(handle, moduleName); ··· 82 82 </a> 83 83 <span class={`${ui.muted} ${ui.inlineTag}`}>enabled</span> 84 84 </div> 85 - {canDo("sphere", "disable-module") && ( 85 + {canDo("sphere", "disableModule") && ( 86 86 <button class={ui.buttonDanger} onClick={() => disableModule(mod.name)}> 87 87 Disable 88 88 </button> ··· 96 96 </div> 97 97 )} 98 98 99 - {canDo("sphere", "enable-module") && availableToEnable.length > 0 && ( 99 + {canDo("sphere", "enableModule") && availableToEnable.length > 0 && ( 100 100 <div class={ui.stackSm}> 101 101 <h3 class={ui.subsectionTitle}>Available modules</h3> 102 102 <div class={ui.stackSm}> ··· 119 119 </div> 120 120 121 121 {/* Settings — visible to users with relevant permissions */} 122 - {(canManageMembers || canDo("sphere", "update-permissions")) && ( 122 + {(canManageMembers || canDo("sphere", "updatePermissions")) && ( 123 123 <div class={ui.section}> 124 124 <h2 class={ui.sectionTitle}>Settings</h2> 125 125 <div class={ui.stackSm}> ··· 129 129 <p class={ui.muted}>Invite, manage roles, and revoke members.</p> 130 130 </a> 131 131 )} 132 - {canDo("sphere", "update-permissions") && ( 132 + {canDo("sphere", "updatePermissions") && ( 133 133 <a href={spherePath("/settings/permissions")} class={ui.cardLink}> 134 134 <strong>Permissions</strong> 135 135 <p class={ui.muted}>Configure who can perform actions in this sphere.</p>
+42 -42
packages/core/src/__tests__/permissions.test.ts
··· 111 111 112 112 describe("getDefaultRole", () => { 113 113 it("returns the default role for a known action", () => { 114 - expect(getDefaultRole(CORE_MODULE, "invite-member")).toBe("admin"); 115 - expect(getDefaultRole(CORE_MODULE, "update-permissions")).toBe("owner"); 114 + expect(getDefaultRole(CORE_MODULE, "inviteMember")).toBe("admin"); 115 + expect(getDefaultRole(CORE_MODULE, "updatePermissions")).toBe("owner"); 116 116 }); 117 117 118 118 it("returns null for an unknown action", () => { ··· 129 129 describe("corePermissions", () => { 130 130 it("defines all expected sphere-level actions", () => { 131 131 const actions = Object.keys(corePermissions); 132 - expect(actions).toContain("invite-member"); 133 - expect(actions).toContain("revoke-member"); 134 - expect(actions).toContain("update-member-role"); 135 - expect(actions).toContain("enable-module"); 136 - expect(actions).toContain("disable-module"); 137 - expect(actions).toContain("update-permissions"); 132 + expect(actions).toContain("inviteMember"); 133 + expect(actions).toContain("revokeMember"); 134 + expect(actions).toContain("updateMemberRole"); 135 + expect(actions).toContain("enableModule"); 136 + expect(actions).toContain("disableModule"); 137 + expect(actions).toContain("updatePermissions"); 138 138 }); 139 139 140 140 it("restricts module and permissions management to owner by default", () => { 141 - expect(corePermissions["enable-module"].defaultRole).toBe("owner"); 142 - expect(corePermissions["disable-module"].defaultRole).toBe("owner"); 143 - expect(corePermissions["update-permissions"].defaultRole).toBe("owner"); 141 + expect(corePermissions["enableModule"].defaultRole).toBe("owner"); 142 + expect(corePermissions["disableModule"].defaultRole).toBe("owner"); 143 + expect(corePermissions["updatePermissions"].defaultRole).toBe("owner"); 144 144 }); 145 145 146 146 it("allows admin for member management by default", () => { 147 - expect(corePermissions["invite-member"].defaultRole).toBe("admin"); 148 - expect(corePermissions["revoke-member"].defaultRole).toBe("admin"); 149 - expect(corePermissions["update-member-role"].defaultRole).toBe("admin"); 147 + expect(corePermissions["inviteMember"].defaultRole).toBe("admin"); 148 + expect(corePermissions["revokeMember"].defaultRole).toBe("admin"); 149 + expect(corePermissions["updateMemberRole"].defaultRole).toBe("admin"); 150 150 }); 151 151 }); 152 152 ··· 155 155 describe("getRequiredRole", () => { 156 156 it("returns module default when no override exists", () => { 157 157 seedSphere(db, { id: SPHERE_ID, handle: SPHERE_HANDLE, ownerDid: OWNER_DID }); 158 - expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "invite-member")).toBe("admin"); 159 - expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "update-permissions")).toBe("owner"); 158 + expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "inviteMember")).toBe("admin"); 159 + expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "updatePermissions")).toBe("owner"); 160 160 }); 161 161 162 162 it("returns override when one exists in DB", () => { 163 163 seedSphere(db, { id: SPHERE_ID, handle: SPHERE_HANDLE, ownerDid: OWNER_DID }); 164 - insertOverride("sphere:invite-member", "owner"); 165 - expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "invite-member")).toBe("owner"); 164 + insertOverride("sphere:inviteMember", "owner"); 165 + expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "inviteMember")).toBe("owner"); 166 166 }); 167 167 168 168 it("falls back to admin for unknown module/action", () => { ··· 172 172 173 173 it("falls back to admin for invalid role in DB override", () => { 174 174 seedSphere(db, { id: SPHERE_ID, handle: SPHERE_HANDLE, ownerDid: OWNER_DID }); 175 - insertOverride("sphere:invite-member", "invalid-role"); 176 - expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "invite-member")).toBe("admin"); 175 + insertOverride("sphere:inviteMember", "invalid-role"); 176 + expect(getRequiredRole(SPHERE_ID, CORE_MODULE, "inviteMember")).toBe("admin"); 177 177 }); 178 178 }); 179 179 ··· 185 185 }); 186 186 187 187 it("allows owner for owner-level actions", () => { 188 - expect(checkPermission(SPHERE_ID, CORE_MODULE, "update-permissions", "owner")).toBe(true); 188 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "updatePermissions", "owner")).toBe(true); 189 189 }); 190 190 191 191 it("denies admin for owner-level actions", () => { 192 - expect(checkPermission(SPHERE_ID, CORE_MODULE, "update-permissions", "admin")).toBe(false); 192 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "updatePermissions", "admin")).toBe(false); 193 193 }); 194 194 195 195 it("allows admin for admin-level actions", () => { 196 - expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", "admin")).toBe(true); 196 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "inviteMember", "admin")).toBe(true); 197 197 }); 198 198 199 199 it("denies member for admin-level actions", () => { 200 - expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", "member")).toBe(false); 200 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "inviteMember", "member")).toBe(false); 201 201 }); 202 202 203 203 it("denies null (unauthenticated) for member-level actions", () => { 204 - expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", null)).toBe(false); 204 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "inviteMember", null)).toBe(false); 205 205 }); 206 206 207 207 it("respects overrides — allows member when override lowers requirement", () => { 208 - insertOverride("sphere:invite-member", "member"); 209 - expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", "member")).toBe(true); 208 + insertOverride("sphere:inviteMember", "member"); 209 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "inviteMember", "member")).toBe(true); 210 210 }); 211 211 212 212 it("respects overrides — denies admin when override raises requirement", () => { 213 - insertOverride("sphere:invite-member", "owner"); 214 - expect(checkPermission(SPHERE_ID, CORE_MODULE, "invite-member", "admin")).toBe(false); 213 + insertOverride("sphere:inviteMember", "owner"); 214 + expect(checkPermission(SPHERE_ID, CORE_MODULE, "inviteMember", "admin")).toBe(false); 215 215 }); 216 216 }); 217 217 ··· 224 224 225 225 it("returns all core permissions for owner", () => { 226 226 const perms = computeUserPermissions(SPHERE_ID, "owner", []); 227 - expect(perms["sphere:invite-member"]).toBe(true); 228 - expect(perms["sphere:update-permissions"]).toBe(true); 229 - expect(perms["sphere:enable-module"]).toBe(true); 227 + expect(perms["sphere:inviteMember"]).toBe(true); 228 + expect(perms["sphere:updatePermissions"]).toBe(true); 229 + expect(perms["sphere:enableModule"]).toBe(true); 230 230 }); 231 231 232 232 it("returns correct permissions for admin", () => { 233 233 const perms = computeUserPermissions(SPHERE_ID, "admin", []); 234 - expect(perms["sphere:invite-member"]).toBe(true); 235 - expect(perms["sphere:update-permissions"]).toBe(false); 236 - expect(perms["sphere:enable-module"]).toBe(false); 234 + expect(perms["sphere:inviteMember"]).toBe(true); 235 + expect(perms["sphere:updatePermissions"]).toBe(false); 236 + expect(perms["sphere:enableModule"]).toBe(false); 237 237 }); 238 238 239 239 it("returns correct permissions for member", () => { 240 240 const perms = computeUserPermissions(SPHERE_ID, "member", []); 241 - expect(perms["sphere:invite-member"]).toBe(false); 242 - expect(perms["sphere:update-permissions"]).toBe(false); 241 + expect(perms["sphere:inviteMember"]).toBe(false); 242 + expect(perms["sphere:updatePermissions"]).toBe(false); 243 243 }); 244 244 245 245 it("applies DB overrides", () => { 246 - insertOverride("sphere:invite-member", "member"); 246 + insertOverride("sphere:inviteMember", "member"); 247 247 const perms = computeUserPermissions(SPHERE_ID, "member", []); 248 - expect(perms["sphere:invite-member"]).toBe(true); 248 + expect(perms["sphere:inviteMember"]).toBe(true); 249 249 }); 250 250 251 251 it("includes enabled module permissions", () => { ··· 277 277 278 278 it("always includes core permissions even if not in enabledModules", () => { 279 279 const perms = computeUserPermissions(SPHERE_ID, "owner", ["some-other-module"]); 280 - expect(perms["sphere:invite-member"]).toBe(true); 280 + expect(perms["sphere:inviteMember"]).toBe(true); 281 281 }); 282 282 283 283 it("ignores invalid DB overrides", () => { 284 - insertOverride("sphere:invite-member", "bogus-role"); 284 + insertOverride("sphere:inviteMember", "bogus-role"); 285 285 // Invalid override should be filtered out, falling back to default ("admin") 286 286 const perms = computeUserPermissions(SPHERE_ID, "admin", []); 287 - expect(perms["sphere:invite-member"]).toBe(true); 287 + expect(perms["sphere:inviteMember"]).toBe(true); 288 288 }); 289 289 });
+16 -16
packages/core/src/generated/lexicon-records.ts
··· 32 32 /** Minimum role to comment on feature requests. */ 33 33 comment?: string; 34 34 /** Minimum role to change feature request status. */ 35 - "change-status"?: string; 35 + changeStatus?: string; 36 36 /** Minimum role to mark feature requests as duplicate. */ 37 - "mark-duplicate"?: string; 37 + markDuplicate?: string; 38 38 /** Minimum role to hide/unhide content. */ 39 39 moderate?: string; 40 40 } ··· 87 87 88 88 export interface SpherePermissionsRecord { 89 89 /** Minimum role to invite members. */ 90 - "invite-member"?: string; 90 + inviteMember?: string; 91 91 /** Minimum role to revoke members. */ 92 - "revoke-member"?: string; 92 + revokeMember?: string; 93 93 /** Minimum role to change member roles. */ 94 - "update-member-role"?: string; 94 + updateMemberRole?: string; 95 95 /** Minimum role to enable modules. */ 96 - "enable-module"?: string; 96 + enableModule?: string; 97 97 /** Minimum role to disable modules. */ 98 - "disable-module"?: string; 98 + disableModule?: string; 99 99 /** Minimum role to update permissions. */ 100 - "update-permissions"?: string; 100 + updatePermissions?: string; 101 101 } 102 102 103 103 export interface PdsRecordMap { 104 104 "site.exosphere.featureRequest": FeatureRequestRecord; 105 - "site.exosphere.featureRequestComment": FeatureRequestCommentRecord; 106 - "site.exosphere.featureRequestCommentVote": FeatureRequestCommentVoteRecord; 107 - "site.exosphere.featureRequestPermissions": FeatureRequestPermissionsRecord; 108 - "site.exosphere.featureRequestStatus": FeatureRequestStatusRecord; 109 - "site.exosphere.featureRequestVote": FeatureRequestVoteRecord; 105 + "site.exosphere.featureRequest.comment": FeatureRequestCommentRecord; 106 + "site.exosphere.featureRequest.commentVote": FeatureRequestCommentVoteRecord; 107 + "site.exosphere.featureRequest.permissions": FeatureRequestPermissionsRecord; 108 + "site.exosphere.featureRequest.status": FeatureRequestStatusRecord; 109 + "site.exosphere.featureRequest.vote": FeatureRequestVoteRecord; 110 110 "site.exosphere.moderation": ModerationRecord; 111 111 "site.exosphere.sphere": SphereRecord; 112 - "site.exosphere.sphereMember": SphereMemberRecord; 113 - "site.exosphere.sphereMemberApproval": SphereMemberApprovalRecord; 114 - "site.exosphere.spherePermissions": SpherePermissionsRecord; 112 + "site.exosphere.sphere.member": SphereMemberRecord; 113 + "site.exosphere.sphere.memberApproval": SphereMemberApprovalRecord; 114 + "site.exosphere.sphere.permissions": SpherePermissionsRecord; 115 115 }
+7 -7
packages/core/src/permissions/core.ts
··· 4 4 export const CORE_MODULE = "sphere"; 5 5 6 6 /** AT Protocol collection ID for core sphere permission overrides. */ 7 - export const CORE_PERMISSIONS_COLLECTION = "site.exosphere.spherePermissions"; 7 + export const CORE_PERMISSIONS_COLLECTION = "site.exosphere.sphere.permissions"; 8 8 9 9 /** Core sphere-level permission actions (configurable between owner and admin). */ 10 10 export const corePermissions = { 11 - "invite-member": { label: "Invite members", defaultRole: "admin" }, 12 - "revoke-member": { label: "Revoke members", defaultRole: "admin" }, 13 - "update-member-role": { label: "Change member roles", defaultRole: "admin" }, 14 - "enable-module": { label: "Enable modules", defaultRole: "owner" }, 15 - "disable-module": { label: "Disable modules", defaultRole: "owner" }, 16 - "update-permissions": { label: "Update permissions", defaultRole: "owner" }, 11 + inviteMember: { label: "Invite members", defaultRole: "admin" }, 12 + revokeMember: { label: "Revoke members", defaultRole: "admin" }, 13 + updateMemberRole: { label: "Change member roles", defaultRole: "admin" }, 14 + enableModule: { label: "Enable modules", defaultRole: "owner" }, 15 + disableModule: { label: "Disable modules", defaultRole: "owner" }, 16 + updatePermissions: { label: "Update permissions", defaultRole: "owner" }, 17 17 } satisfies Record<string, ModulePermission>;
+11 -11
packages/core/src/sphere/api/members.ts
··· 13 13 import { resolveDidHandles } from "../../identity/index.ts"; 14 14 15 15 const SPHERE_COLLECTION = "site.exosphere.sphere"; 16 - const MEMBER_COLLECTION = "site.exosphere.sphereMember"; 17 - const APPROVAL_COLLECTION = "site.exosphere.sphereMemberApproval"; 16 + const MEMBER_COLLECTION = "site.exosphere.sphere.member"; 17 + const APPROVAL_COLLECTION = "site.exosphere.sphere.memberApproval"; 18 18 19 19 const app = new Hono<AuthEnv>(); 20 20 ··· 28 28 // Allow access if user has any member management permission 29 29 const role = getActiveMemberRole(sphere.id, c.var.did); 30 30 if ( 31 - !checkPermission(sphere.id, "sphere", "invite-member", role) && 32 - !checkPermission(sphere.id, "sphere", "revoke-member", role) && 33 - !checkPermission(sphere.id, "sphere", "update-member-role", role) 31 + !checkPermission(sphere.id, "sphere", "inviteMember", role) && 32 + !checkPermission(sphere.id, "sphere", "revokeMember", role) && 33 + !checkPermission(sphere.id, "sphere", "updateMemberRole", role) 34 34 ) { 35 35 return c.json({ error: "Forbidden" }, 403); 36 36 } ··· 59 59 }); 60 60 61 61 // Invite a member (admin/owner only) 62 - // Publishes a sphereMemberApproval record on the inviter's PDS 62 + // Publishes a sphere.memberApproval record on the inviter's PDS 63 63 app.post("/:handle/members", requireAuth, async (c) => { 64 64 const sphere = findSphere(c.req.param("handle")); 65 65 if (!sphere) { ··· 68 68 69 69 const inviterDid = c.var.did; 70 70 const inviterRole = getActiveMemberRole(sphere.id, inviterDid); 71 - if (!checkPermission(sphere.id, "sphere", "invite-member", inviterRole)) { 71 + if (!checkPermission(sphere.id, "sphere", "inviteMember", inviterRole)) { 72 72 return c.json({ error: "Forbidden" }, 403); 73 73 } 74 74 ··· 115 115 }); 116 116 117 117 // Accept an invitation (the invited user calls this) 118 - // Publishes a sphereMember record on the member's PDS 118 + // Publishes a sphere.member record on the member's PDS 119 119 app.post("/:handle/members/accept", requireAuth, async (c) => { 120 120 const sphere = findSphere(c.req.param("handle")); 121 121 if (!sphere) { ··· 164 164 165 165 const revokerDid = c.var.did; 166 166 const revokerRole = getActiveMemberRole(sphere.id, revokerDid); 167 - if (!checkPermission(sphere.id, "sphere", "revoke-member", revokerRole)) { 167 + if (!checkPermission(sphere.id, "sphere", "revokeMember", revokerRole)) { 168 168 return c.json({ error: "Forbidden" }, 403); 169 169 } 170 170 ··· 196 196 } 197 197 } 198 198 199 - // Note: the member's own sphereMember record (pdsUri) lives on the 199 + // Note: the member's own sphere.member record (pdsUri) lives on the 200 200 // member's PDS and can't be deleted by the admin. It stays on-protocol 201 201 // but the revoked status is authoritative via the approval deletion. 202 202 db.update(sphereMembers) ··· 215 215 } 216 216 217 217 const callerRole = getActiveMemberRole(sphere.id, c.var.did); 218 - if (!checkPermission(sphere.id, "sphere", "update-member-role", callerRole)) { 218 + if (!checkPermission(sphere.id, "sphere", "updateMemberRole", callerRole)) { 219 219 return c.json({ error: "Forbidden" }, 403); 220 220 } 221 221
+1 -1
packages/core/src/sphere/api/permissions.ts
··· 71 71 return c.json({ modules }); 72 72 }); 73 73 74 - // Update permission overrides (requires update-permissions permission) 74 + // Update permission overrides (requires updatePermissions permission) 75 75 app.put("/:handle/permissions", requireAuth, async (c) => { 76 76 const sphere = findSphere(c.req.param("handle")); 77 77 if (!sphere) {
+1 -5
packages/core/src/sphere/index.ts
··· 5 5 getPendingInvitations, 6 6 } from "./routes.ts"; 7 7 export { createCoreIndexer } from "./indexer.ts"; 8 - export { 9 - getActiveMemberRole, 10 - registerModerationHandler, 11 - findSphereByAtUri, 12 - } from "./operations.ts"; 8 + export { getActiveMemberRole, registerModerationHandler, findSphereByAtUri } from "./operations.ts"; 13 9 export type { ModerationHandler } from "./operations.ts"; 14 10 export { sphereContext } from "./middleware.ts";
+2 -2
packages/core/src/sphere/indexer.ts
··· 13 13 import { getAllPermissionsCollections } from "../permissions/index.ts"; 14 14 15 15 const SPHERE_COLLECTION = "site.exosphere.sphere"; 16 - const MEMBER_COLLECTION = "site.exosphere.sphereMember"; 17 - const APPROVAL_COLLECTION = "site.exosphere.sphereMemberApproval"; 16 + const MEMBER_COLLECTION = "site.exosphere.sphere.member"; 17 + const APPROVAL_COLLECTION = "site.exosphere.sphere.memberApproval"; 18 18 const MODERATION_COLLECTION = "site.exosphere.moderation"; 19 19 20 20 /** Build the core indexer dynamically — includes permission collections from registered modules. */
+1 -1
packages/core/src/types/index.ts
··· 45 45 indexer?: ModuleIndexer; 46 46 /** Named actions and their default minimum role, displayed in the admin panel */ 47 47 permissions?: Record<string, ModulePermission>; 48 - /** AT Protocol collection ID for this module's permission overrides record (e.g. "site.exosphere.featureRequestPermissions") */ 48 + /** AT Protocol collection ID for this module's permission overrides record (e.g. "site.exosphere.featureRequest.permissions") */ 49 49 permissionsCollection?: string; 50 50 } 51 51
+2 -2
packages/feature-requests/src/api/comments.ts
··· 25 25 unhideComment, 26 26 } from "../db/operations.ts"; 27 27 28 - const COMMENT_COLLECTION = "site.exosphere.featureRequestComment"; 29 - const COMMENT_VOTE_COLLECTION = "site.exosphere.featureRequestCommentVote"; 28 + const COMMENT_COLLECTION = "site.exosphere.featureRequest.comment"; 29 + const COMMENT_VOTE_COLLECTION = "site.exosphere.featureRequest.commentVote"; 30 30 const MODERATION_COLLECTION = "site.exosphere.moderation"; 31 31 32 32 const app = new Hono<AuthEnv & SphereEnv>();
+3 -3
packages/feature-requests/src/api/statuses.ts
··· 12 12 import { updateStatusSchema, markAsDuplicateSchema } from "../schemas/feature-request.ts"; 13 13 import { insertStatusAndUpdateFR } from "../db/operations.ts"; 14 14 15 - const STATUS_COLLECTION = "site.exosphere.featureRequestStatus"; 15 + const STATUS_COLLECTION = "site.exosphere.featureRequest.status"; 16 16 17 17 const app = new Hono<AuthEnv & SphereEnv>(); 18 18 ··· 61 61 // Permission: admin/owner OR author 62 62 const role = getActiveMemberRole(sphereId, did); 63 63 const canMark = 64 - checkPermission(sphereId, "feature-requests", "mark-duplicate", role) || fr.authorDid === did; 64 + checkPermission(sphereId, "feature-requests", "markDuplicate", role) || fr.authorDid === did; 65 65 if (!canMark) { 66 66 return c.json({ error: "Forbidden" }, 403); 67 67 } ··· 133 133 app.post( 134 134 "/:id/status", 135 135 requireAuth, 136 - requirePermission("feature-requests", "change-status"), 136 + requirePermission("feature-requests", "changeStatus"), 137 137 async (c) => { 138 138 const id = c.req.param("id"); 139 139 const body = await c.req.json();
+1 -1
packages/feature-requests/src/api/votes.ts
··· 8 8 import { featureRequests, featureRequestVotes } from "../db/schema.ts"; 9 9 import { insertVote, deleteVoteByAuthor } from "../db/operations.ts"; 10 10 11 - const VOTE_COLLECTION = "site.exosphere.featureRequestVote"; 11 + const VOTE_COLLECTION = "site.exosphere.featureRequest.vote"; 12 12 13 13 const app = new Hono<AuthEnv & SphereEnv>(); 14 14
+3 -3
packages/feature-requests/src/index.ts
··· 10 10 create: { label: "Create feature request", defaultRole: "authenticated" }, 11 11 vote: { label: "Vote on feature requests", defaultRole: "authenticated" }, 12 12 comment: { label: "Comment on feature requests", defaultRole: "authenticated" }, 13 - "change-status": { label: "Change status", defaultRole: "admin" }, 14 - "mark-duplicate": { label: "Mark as duplicate", defaultRole: "admin" }, 13 + changeStatus: { label: "Change status", defaultRole: "admin" }, 14 + markDuplicate: { label: "Mark as duplicate", defaultRole: "admin" }, 15 15 moderate: { label: "Hide/unhide content", defaultRole: "admin" }, 16 16 }, 17 - permissionsCollection: "site.exosphere.featureRequestPermissions", 17 + permissionsCollection: "site.exosphere.featureRequest.permissions", 18 18 };
+4 -4
packages/feature-requests/src/indexer.ts
··· 26 26 27 27 const MODULE_NAME = "feature-requests"; 28 28 const COLLECTION = "site.exosphere.featureRequest"; 29 - const VOTE_COLLECTION = "site.exosphere.featureRequestVote"; 30 - const COMMENT_COLLECTION = "site.exosphere.featureRequestComment"; 31 - const COMMENT_VOTE_COLLECTION = "site.exosphere.featureRequestCommentVote"; 32 - const STATUS_COLLECTION = "site.exosphere.featureRequestStatus"; 29 + const VOTE_COLLECTION = "site.exosphere.featureRequest.vote"; 30 + const COMMENT_COLLECTION = "site.exosphere.featureRequest.comment"; 31 + const COMMENT_VOTE_COLLECTION = "site.exosphere.featureRequest.commentVote"; 32 + const STATUS_COLLECTION = "site.exosphere.featureRequest.status"; 33 33 34 34 function findSphereForAccess( 35 35 sphereOwnerDid: string,
+2 -2
packages/feature-requests/src/ui/pages/feature-request.tsx
··· 554 554 }, [votesQuery.data]); 555 555 556 556 const canModerate = useCanDo("feature-requests", "moderate"); 557 - const canChangeStatus = useCanDo("feature-requests", "change-status"); 558 - const canMarkDuplicate = useCanDo("feature-requests", "mark-duplicate"); 557 + const canChangeStatus = useCanDo("feature-requests", "changeStatus"); 558 + const canMarkDuplicate = useCanDo("feature-requests", "markDuplicate"); 559 559 560 560 const fr = data?.featureRequest; 561 561
+24 -3
scripts/generate-lexicon-types.ts
··· 16 16 17 17 const PREFIX = "site.exosphere."; 18 18 19 + async function collectJsonFiles(dir: string): Promise<string[]> { 20 + const entries = await readdir(dir, { withFileTypes: true }); 21 + const files: string[] = []; 22 + for (const entry of entries) { 23 + const full = resolve(dir, entry.name); 24 + if (entry.isDirectory()) { 25 + files.push(...(await collectJsonFiles(full))); 26 + } else if (entry.name.endsWith(".json")) { 27 + files.push(full); 28 + } 29 + } 30 + return files.sort(); 31 + } 32 + 19 33 interface LexiconProperty { 20 34 type: string; 21 35 format?: string; ··· 49 63 50 64 function toInterfaceName(lexiconId: string): string { 51 65 const name = lexiconId.slice(PREFIX.length); 52 - return name.charAt(0).toUpperCase() + name.slice(1) + "Record"; 66 + return ( 67 + name 68 + .split(".") 69 + .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) 70 + .join("") + "Record" 71 + ); 53 72 } 54 73 55 74 /** Quote property names that aren't valid JS identifiers. */ ··· 77 96 } 78 97 79 98 async function main() { 80 - const files = (await readdir(LEXICON_DIR)).filter((f) => f.endsWith(".json")).sort(); 99 + const files = await collectJsonFiles(LEXICON_DIR); 81 100 82 101 const schemas: LexiconSchema[] = []; 83 102 for (const file of files) { 84 - const content = await Bun.file(resolve(LEXICON_DIR, file)).json(); 103 + const content = await Bun.file(file).json(); 85 104 schemas.push(content as LexiconSchema); 86 105 } 106 + // Sort by lexicon ID for stable output 107 + schemas.sort((a, b) => a.id.localeCompare(b.id)); 87 108 88 109 const lines: string[] = [ 89 110 "// AUTO-GENERATED from landing/lexicons/site/exosphere/*.json",