feat: add SpaceRole bridge for cross-module membership sync
Anonymous-first role resolution (PARTICIPANT default for open rooms). Queries EncryptID server for space-linked rooms with 5-minute cache. Capability checks for add_markers, share_location, configure_map. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b26547bd83
commit
97e9922da2
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* Space Role bridge for rMaps
|
||||
*
|
||||
* rMaps uses anonymous-first access (rooms are open by default),
|
||||
* but when a room is linked to a space, roles are resolved from
|
||||
* the EncryptID server. This module provides:
|
||||
*
|
||||
* - Server-side role resolution for API routes
|
||||
* - Client-side role resolution from the auth store
|
||||
* - Capability checks using RMAPS_PERMISSIONS
|
||||
*/
|
||||
|
||||
import {
|
||||
SpaceRole,
|
||||
hasCapability,
|
||||
type ResolvedRole,
|
||||
} from '@encryptid/sdk/types';
|
||||
import { RMAPS_PERMISSIONS } from '@encryptid/sdk/types/modules';
|
||||
|
||||
const ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || 'https://encryptid.jeffemmett.com';
|
||||
|
||||
// In-memory cache (5 minute TTL)
|
||||
const roleCache = new Map<string, { role: ResolvedRole; expires: number }>();
|
||||
const CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Resolve a user's SpaceRole for a room linked to a space.
|
||||
*
|
||||
* @param userDID - The user's DID (from EncryptID auth store)
|
||||
* @param spaceSlug - The space slug this room is linked to
|
||||
* @returns Resolved role with source
|
||||
*/
|
||||
export async function resolveMapSpaceRole(
|
||||
userDID: string | null,
|
||||
spaceSlug: string,
|
||||
): Promise<ResolvedRole> {
|
||||
// Anonymous users get PARTICIPANT on public rooms (rMaps default)
|
||||
if (!userDID) {
|
||||
return { role: SpaceRole.PARTICIPANT, source: 'anonymous' };
|
||||
}
|
||||
|
||||
const cacheKey = `${userDID}:${spaceSlug}`;
|
||||
const cached = roleCache.get(cacheKey);
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
return cached.role;
|
||||
}
|
||||
|
||||
// Query EncryptID server for membership
|
||||
try {
|
||||
const url = `${ENCRYPTID_SERVER}/api/spaces/${encodeURIComponent(spaceSlug)}/members/${encodeURIComponent(userDID)}`;
|
||||
const res = await fetch(url);
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as { role: string };
|
||||
const result: ResolvedRole = { role: data.role as SpaceRole, source: 'membership' };
|
||||
roleCache.set(cacheKey, { role: result, expires: Date.now() + CACHE_TTL });
|
||||
return result;
|
||||
}
|
||||
} catch {
|
||||
// Network error — fall through to default
|
||||
}
|
||||
|
||||
// Authenticated but no membership → PARTICIPANT (rMaps rooms are open)
|
||||
const result: ResolvedRole = { role: SpaceRole.PARTICIPANT, source: 'default' };
|
||||
roleCache.set(cacheKey, { role: result, expires: Date.now() + CACHE_TTL });
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a specific rMaps capability.
|
||||
*
|
||||
* Usage (client-side):
|
||||
* ```ts
|
||||
* const { did } = useAuthStore();
|
||||
* const canAddMarkers = await checkMapCapability(did, roomSlug, 'add_markers');
|
||||
* ```
|
||||
*/
|
||||
export async function checkMapCapability(
|
||||
userDID: string | null,
|
||||
spaceSlug: string,
|
||||
capability: keyof typeof RMAPS_PERMISSIONS.capabilities,
|
||||
): Promise<boolean> {
|
||||
const resolved = await resolveMapSpaceRole(userDID, spaceSlug);
|
||||
return hasCapability(resolved.role, capability, RMAPS_PERMISSIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SpaceRole for an anonymous or unauthenticated user
|
||||
* on an open rMaps room (no space linkage).
|
||||
*/
|
||||
export function getDefaultMapRole(isAuthenticated: boolean): ResolvedRole {
|
||||
return {
|
||||
role: SpaceRole.PARTICIPANT,
|
||||
source: isAuthenticated ? 'default' : 'anonymous',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cached role for a user in a space.
|
||||
*/
|
||||
export function invalidateMapRoleCache(userDID?: string, spaceSlug?: string): void {
|
||||
if (userDID && spaceSlug) {
|
||||
roleCache.delete(`${userDID}:${spaceSlug}`);
|
||||
} else {
|
||||
roleCache.clear();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue