usuarios pre configurados

This commit is contained in:
2025-08-15 18:35:09 -06:00
parent 1c0d777699
commit 811f569391
12 changed files with 508 additions and 172 deletions

View File

@@ -8,24 +8,35 @@ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [ routes: [
{ {
path: '/', path: '/:uuid',
name: 'Lobby', name: 'LobbyWithUuid',
component: Lobby component: Lobby
}, },
{ {
path: '/game', path: '/:uuid/game',
name: 'Game', name: 'GameWithUuid',
component: Game component: Game
}, },
{ {
path: '/demo', path: '/:uuid/demo',
name: 'DemoGame', name: 'DemoGameWithUuid',
component: DemoGame component: DemoGame
}, },
{ {
path: '/dashboard', path: '/dashboard',
name: 'Dashboard', name: 'Dashboard',
component: Dashboard component: Dashboard
},
{
path: '/',
redirect: '/missing-uuid'
},
{
// simple fallback for users hitting root without UUID
path: '/missing-uuid',
component: {
template: `<div style="padding:20px;font-family:sans-serif"><h2>Falta UUID</h2><p>Abre el juego escaneando tu código QR: snatchgame.nucleoriofrio.com/{uuid}</p></div>`
}
} }
] ]
}); });

View File

@@ -1,6 +1,5 @@
import { Client, Room } from "colyseus.js"; import { Client, Room } from "colyseus.js";
import { ref, Ref } from "vue"; import { ref, Ref } from "vue";
import { localDB } from "./db";
export interface PlayerData { export interface PlayerData {
sessionId: string; sessionId: string;
@@ -25,8 +24,8 @@ class ColyseusService {
private client: Client; private client: Client;
private currentRoom: Room | null = null; private currentRoom: Room | null = null;
private apiBase: string; private apiBase: string;
private readonly LS_KEY_RECONNECT = "snatch.game.rtoken"; // No local storage tokens: UUID in URL identifies the user
public lobbyRoom: Ref<Room | null> = ref(null); public lobbyRoom: Ref<Room | null> = ref(null);
public gameRoom: Ref<Room | null> = ref(null); public gameRoom: Ref<Room | null> = ref(null);
public playerName: Ref<string> = ref(""); public playerName: Ref<string> = ref("");
@@ -50,10 +49,8 @@ class ColyseusService {
async joinLobby(): Promise<Room> { async joinLobby(): Promise<Room> {
try { try {
// Initialize DB first to get UUID const uuid = this.getUuidFromPath();
await localDB.init();
const uuid = localDB.getUUID();
const room = await this.client.joinOrCreate("lobby", { uuid }); const room = await this.client.joinOrCreate("lobby", { uuid });
this.lobbyRoom.value = room; this.lobbyRoom.value = room;
this.currentRoom = room; this.currentRoom = room;
@@ -69,36 +66,18 @@ class ColyseusService {
this.playerName.value = data.name; this.playerName.value = data.name;
this.nameConfirmed.value = true; this.nameConfirmed.value = true;
} else { } else {
// Initialize local DB and optionally auto-apply saved profile // No client-side persistence; user must confirm name once per session
try {
await localDB.init();
const profile = localDB.getLocalPlayer();
// Apply saved color silently
if (profile?.color && profile.color !== this.playerColor.value) {
this.setPlayerColor(profile.color);
}
if (profile?.name) {
this.playerName.value = profile.name;
try { localDB.setName(profile.name); } catch {}
this.setPlayerName(profile.name);
this.nameConfirmed.value = true;
}
} catch (e) {
console.warn("Local DB init failed", e);
}
} }
}); });
room.onMessage("nameUpdated", (data) => { room.onMessage("nameUpdated", (data) => {
this.playerName.value = data.name; this.playerName.value = data.name;
try { localDB.setName(data.name); } catch {}
this.nameConfirmed.value = true; this.nameConfirmed.value = true;
}); });
room.onMessage("colorUpdated", (data) => { room.onMessage("colorUpdated", (data) => {
if (data?.color) { if (data?.color) {
this.playerColor.value = data.color; this.playerColor.value = data.color;
try { localDB.setColor(data.color); } catch {}
} }
}); });
@@ -110,28 +89,13 @@ class ColyseusService {
} }
async tryReconnectToOngoingGame(): Promise<Room | null> { async tryReconnectToOngoingGame(): Promise<Room | null> {
try { // No stored reconnection tokens; rely on UUID flow and fresh joins
const token = typeof window !== 'undefined' ? (window.localStorage.getItem(this.LS_KEY_RECONNECT) || "") : ""; return Promise.resolve(null);
if (!token) return null;
const room = await this.client.reconnect(token);
this.gameRoom.value = room;
this.currentRoom = room;
// Ensure local session id reflects the active room session
try { this.sessionId.value = (room as any).sessionId || this.sessionId.value; } catch {}
try { if (typeof window !== 'undefined') window.localStorage.setItem(this.LS_KEY_RECONNECT, (room as any).reconnectionToken || token); } catch {}
return room;
} catch (e) {
console.warn('Reconnection failed, clearing tokens');
try {
if (typeof window !== 'undefined') { window.localStorage.removeItem(this.LS_KEY_RECONNECT); }
} catch {}
return null;
}
} }
async setPlayerName(name: string): Promise<void> { async setPlayerName(name: string): Promise<void> {
if (this.lobbyRoom.value) { if (this.lobbyRoom.value) {
const uuid = localDB.getUUID(); const uuid = this.getUuidFromPath();
this.lobbyRoom.value.send("setName", { name, uuid }); this.lobbyRoom.value.send("setName", { name, uuid });
} }
} }
@@ -156,9 +120,11 @@ class ColyseusService {
try { try {
// Join the game room directly using the roomId // Join the game room directly using the roomId
console.log('Joining game room with name:', this.playerName.value); console.log('Joining game room with name:', this.playerName.value);
const uuid = this.getUuidFromPath();
const gameRoom = await this.client.joinById(data.roomId, { const gameRoom = await this.client.joinById(data.roomId, {
playerName: this.playerName.value, playerName: this.playerName.value,
playerColor: this.playerColor.value playerColor: this.playerColor.value,
uuid
}); });
// Ensure the room id is set // Ensure the room id is set
@@ -172,7 +138,6 @@ class ColyseusService {
this.currentRoom = gameRoom; this.currentRoom = gameRoom;
// Update current session id for correct role mapping // Update current session id for correct role mapping
try { this.sessionId.value = (gameRoom as any).sessionId || this.sessionId.value; } catch {} try { this.sessionId.value = (gameRoom as any).sessionId || this.sessionId.value; } catch {}
try { if (typeof window !== 'undefined') window.localStorage.setItem(this.LS_KEY_RECONNECT, (gameRoom as any).reconnectionToken || ""); } catch {}
console.log('gameRoom.value is now:', this.gameRoom.value); console.log('gameRoom.value is now:', this.gameRoom.value);
// Don't register message handlers here - let the Game component handle them // Don't register message handlers here - let the Game component handle them
@@ -198,12 +163,13 @@ class ColyseusService {
async joinGameRoom(roomId: string): Promise<Room> { async joinGameRoom(roomId: string): Promise<Room> {
try { try {
const uuid = this.getUuidFromPath();
const gameRoom = await this.client.joinById(roomId, { const gameRoom = await this.client.joinById(roomId, {
playerName: this.playerName.value, playerName: this.playerName.value,
playerColor: this.playerColor.value playerColor: this.playerColor.value,
uuid
}); });
try { this.sessionId.value = (gameRoom as any).sessionId || this.sessionId.value; } catch {} try { this.sessionId.value = (gameRoom as any).sessionId || this.sessionId.value; } catch {}
try { if (typeof window !== 'undefined') window.localStorage.setItem(this.LS_KEY_RECONNECT, (gameRoom as any).reconnectionToken || ""); } catch {}
this.gameRoom.value = gameRoom; this.gameRoom.value = gameRoom;
this.currentRoom = gameRoom; this.currentRoom = gameRoom;
@@ -291,7 +257,6 @@ class ColyseusService {
this.currentRoom = null; this.currentRoom = null;
} }
} }
try { if (typeof window !== 'undefined') { window.localStorage.removeItem(this.LS_KEY_RECONNECT); } } catch {}
} }
leaveCurrentRoom(): void { leaveCurrentRoom(): void {
@@ -350,6 +315,13 @@ class ColyseusService {
return null; return null;
} }
} }
private getUuidFromPath(): string {
if (typeof window === 'undefined') return '';
const path = window.location.pathname.replace(/^\/+/, '');
const seg = path.split('/')[0] || '';
return seg;
}
} }
export const colyseusService = new ColyseusService(); export const colyseusService = new ColyseusService();

View File

@@ -68,7 +68,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { colyseusService } from '../services/colyseus'; import { colyseusService } from '../services/colyseus';
import { getStateCallbacks } from 'colyseus.js'; import { getStateCallbacks } from 'colyseus.js';
@@ -81,6 +81,8 @@ import PlayerStats from './games/PlayerStats.vue';
import ChatWidget from './games/ChatWidget.vue'; import ChatWidget from './games/ChatWidget.vue';
const router = useRouter(); const router = useRouter();
const route = useRoute();
const routeUuid = computed(() => (route.params as any)?.uuid as string || '');
const players = ref<any[]>([]); const players = ref<any[]>([]);
const gameStatus = ref('waiting'); const gameStatus = ref('waiting');
@@ -126,14 +128,7 @@ const currentComponent = computed(() => componentMap[currentVariant.value]);
onMounted(() => { onMounted(() => {
let room = colyseusService.gameRoom.value; let room = colyseusService.gameRoom.value;
if (!room) { if (!room) {
// Try reconnection first router.push(`/${routeUuid.value}`);
colyseusService.tryReconnectToOngoingGame().then(r => {
if (!r) {
router.push('/');
return;
}
setupRoom(r);
});
return; return;
} }
@@ -181,7 +176,7 @@ onMounted(() => {
$(player).listen("shameTokens", (v: number) => { const p = players.value.find(x => x.sessionId === key); if (p) p.shameTokens = v; }); $(player).listen("shameTokens", (v: number) => { const p = players.value.find(x => x.sessionId === key); if (p) p.shameTokens = v; });
$(player).listen("color", (v: string) => { const p = players.value.find(x => x.sessionId === key); if (p) p.color = v; }); $(player).listen("color", (v: string) => { const p = players.value.find(x => x.sessionId === key); if (p) p.color = v; });
}); });
$(room.state).players.onRemove((player: any, key: string) => { $(room.state).players.onRemove((_: any, key: string) => {
const i = players.value.findIndex(p => p.sessionId === key); const i = players.value.findIndex(p => p.sessionId === key);
if (i !== -1) players.value.splice(i, 1); if (i !== -1) players.value.splice(i, 1);
}); });
@@ -192,7 +187,7 @@ onMounted(() => {
}); });
room.onMessage("gameEnd", () => { room.onMessage("gameEnd", () => {
try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.rtoken'); } } catch {} // no-op for local storage
}); });
// Register additional message handlers to avoid warnings // Register additional message handlers to avoid warnings
@@ -209,7 +204,7 @@ onMounted(() => {
}); });
// Handle room closure/disconnection // Handle room closure/disconnection
room.onLeave((code) => { room.onLeave((code: number) => {
console.log('[DemoGame] Room disconnected with code:', code); console.log('[DemoGame] Room disconnected with code:', code);
// Handle shuffle disconnection specially // Handle shuffle disconnection specially
@@ -223,36 +218,25 @@ onMounted(() => {
} catch {} } catch {}
// Redirect to lobby and let it handle the shuffle redirect // Redirect to lobby and let it handle the shuffle redirect
router.push('/'); router.push(`/${routeUuid.value}`);
return; return;
} }
// Normal disconnection handling // Normal disconnection handling
// Always clean up local storage when room closes // Always clean up local storage when room closes
try { // no-op for local storage cleanup
if (typeof window !== 'undefined') {
window.localStorage.removeItem('snatch.game.roomId');
window.localStorage.removeItem('snatch.game.sessionId');
}
} catch {}
// If not on lobby page, redirect there // If not on lobby page, redirect there
if (router.currentRoute.value.path !== '/') { if (router.currentRoute.value.path !== `/${routeUuid.value}`) {
console.log('[DemoGame] Room closed, redirecting to lobby'); console.log('[DemoGame] Room closed, redirecting to lobby');
router.push('/'); router.push(`/${routeUuid.value}`);
} }
}); });
room.onError((code, message) => { room.onError((code: number, message: any) => {
console.error('[DemoGame] Room error:', code, message); console.error('[DemoGame] Room error:', code, message);
// On error, also redirect to lobby // On error, redirect to lobby
try { router.push(`/${routeUuid.value}`);
if (typeof window !== 'undefined') {
window.localStorage.removeItem('snatch.game.roomId');
window.localStorage.removeItem('snatch.game.sessionId');
}
} catch {}
router.push('/');
}); });
} }
}); });
@@ -276,8 +260,7 @@ function onAssignShame(val: boolean) { colyseusService.assignShame(val); }
function leaveGame() { function leaveGame() {
console.log('[DemoGame] User manually leaving game'); console.log('[DemoGame] User manually leaving game');
colyseusService.leaveGame(); colyseusService.leaveGame();
try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.roomId'); window.localStorage.removeItem('snatch.game.sessionId'); } } catch {} router.push(`/${routeUuid.value}`);
router.push('/');
} }
</script> </script>

View File

@@ -75,12 +75,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'; import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { colyseusService } from '../services/colyseus'; import { colyseusService } from '../services/colyseus';
import { localDB } from '../services/db';
import { getStateCallbacks } from 'colyseus.js'; import { getStateCallbacks } from 'colyseus.js';
const router = useRouter(); const router = useRouter();
const route = useRoute();
const routeUuid = computed(() => (route.params as any)?.uuid as string || '');
const players = ref<any[]>([]); const players = ref<any[]>([]);
const gameStatus = ref('waiting'); const gameStatus = ref('waiting');
const timeRemaining = ref(600); const timeRemaining = ref(600);
@@ -99,16 +100,8 @@ onMounted(() => {
console.log('Current game room:', room); console.log('Current game room:', room);
if (!room) { if (!room) {
console.warn('No game room found, trying reconnection...'); console.warn('No game room found, redirecting to lobby...');
// Attempt reconnection if tokens are available router.push(`/${routeUuid.value}`);
colyseusService.tryReconnectToOngoingGame().then((r) => {
if (!r) {
console.error('Reconnection failed, redirecting to lobby...');
router.push('/');
return;
}
setupRoom(r);
});
return; return;
} }
@@ -120,7 +113,7 @@ onMounted(() => {
const $ = getStateCallbacks(room); const $ = getStateCallbacks(room);
// Wait for the initial state sync (do not manually push players here to avoid duplicates) // Wait for the initial state sync (do not manually push players here to avoid duplicates)
room.onStateChange.once((state) => { room.onStateChange.once((state: any) => {
console.log('Initial state received:', state); console.log('Initial state received:', state);
gameStatus.value = state.gameStatus || 'waiting'; gameStatus.value = state.gameStatus || 'waiting';
timeRemaining.value = state.timeRemaining || 600; timeRemaining.value = state.timeRemaining || 600;
@@ -164,14 +157,14 @@ onMounted(() => {
}); });
}); });
$(room.state).players.onRemove((player: any, key: string) => { $(room.state).players.onRemove((_: any, key: string) => {
const index = players.value.findIndex(p => p.sessionId === key); const index = players.value.findIndex(p => p.sessionId === key);
if (index !== -1) { if (index !== -1) {
players.value.splice(index, 1); players.value.splice(index, 1);
} }
}); });
room.onMessage("playerInfo", (info) => { room.onMessage("playerInfo", (info: any) => {
console.log('Received playerInfo:', info); console.log('Received playerInfo:', info);
colyseusService.sessionId.value = info.sessionId; colyseusService.sessionId.value = info.sessionId;
colyseusService.playerName.value = info.name; colyseusService.playerName.value = info.name;
@@ -182,10 +175,9 @@ onMounted(() => {
gameStatus.value = 'playing'; gameStatus.value = 'playing';
}); });
room.onMessage("gameEnd", (data) => { room.onMessage("gameEnd", () => {
console.log("Game ended!", data); console.log("Game ended!");
gameStatus.value = 'finished'; gameStatus.value = 'finished';
try { if (typeof window !== 'undefined') { window.localStorage.removeItem('snatch.game.rtoken'); } } catch {}
}); });
room.onMessage("gamePaused", () => { room.onMessage("gamePaused", () => {
@@ -207,18 +199,17 @@ onUnmounted(() => {
} }
}); });
function handleClick() { function handleClick() {
if (gameStatus.value !== 'playing') return; if (gameStatus.value !== 'playing') return;
colyseusService.sendClick(); colyseusService.sendClick();
try { localDB.incClicks(1); } catch {}
isClicking.value = true;
isClicking.value = true; clearTimeout(clickTimeout);
clearTimeout(clickTimeout); clickTimeout = setTimeout(() => {
clickTimeout = setTimeout(() => { isClicking.value = false;
isClicking.value = false; }, 100);
}, 100); }
}
function formatTime(seconds: number): string { function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
@@ -228,7 +219,7 @@ function formatTime(seconds: number): string {
function leaveGame() { function leaveGame() {
colyseusService.leaveGame(); colyseusService.leaveGame();
router.push('/'); router.push(`/${routeUuid.value}`);
} }
</script> </script>

View File

@@ -83,12 +83,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'; import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import PlayerStats from './games/PlayerStats.vue'; import PlayerStats from './games/PlayerStats.vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { colyseusService } from '../services/colyseus'; import { colyseusService } from '../services/colyseus';
import { getStateCallbacks } from 'colyseus.js'; import { getStateCallbacks } from 'colyseus.js';
import { localDB } from '../services/db';
const router = useRouter(); const router = useRouter();
const route = useRoute();
const routeUuid = computed(() => (route.params as any)?.uuid as string || '');
const inputName = ref(''); const inputName = ref('');
const isJoining = ref(false); const isJoining = ref(false);
const colorInput = ref('#667eea'); const colorInput = ref('#667eea');
@@ -111,25 +112,9 @@ const previewPlayer = computed(() => ({
onMounted(async () => { onMounted(async () => {
try { try {
// Try reconnect to an ongoing game first
const reconnected = await colyseusService.tryReconnectToOngoingGame();
if (reconnected) {
router.push('/demo');
return;
}
const room = await colyseusService.joinLobby(); const room = await colyseusService.joinLobby();
colorInput.value = colyseusService.playerColor.value || '#667eea'; colorInput.value = colyseusService.playerColor.value || '#667eea';
// Initialize local DB and prefill inputs if available
try {
await localDB.init();
const profile = localDB.getLocalPlayer();
if (profile?.name) inputName.value = profile.name;
if (profile?.color) colorInput.value = profile.color;
} catch (e) {
console.warn('Local DB not available', e);
}
// Keep color input synced with server-updated color // Keep color input synced with server-updated color
watch(() => colyseusService.playerColor.value, (c) => { watch(() => colyseusService.playerColor.value, (c) => {
if (c && c !== colorInput.value) colorInput.value = c; if (c && c !== colorInput.value) colorInput.value = c;
@@ -190,7 +175,6 @@ onUnmounted(() => {
async function updateName() { async function updateName() {
// Send even if empty; server will assign a default unique name when empty // Send even if empty; server will assign a default unique name when empty
const name = inputName.value.trim(); const name = inputName.value.trim();
try { localDB.setName(name); } catch {}
await colyseusService.setPlayerName(name); await colyseusService.setPlayerName(name);
} }
@@ -205,7 +189,7 @@ async function handleQuickPlay() {
console.log('Starting quickPlay...'); console.log('Starting quickPlay...');
try { try {
const gameRoom = await colyseusService.quickPlay(); const gameRoom = await colyseusService.quickPlay();
console.log('Game room joined:', gameRoom?.id, 'Full room object:', gameRoom); console.log('Game room joined. Full room object:', gameRoom);
// Leave the lobby room before navigating // Leave the lobby room before navigating
if (colyseusService.lobbyRoom.value) { if (colyseusService.lobbyRoom.value) {
@@ -214,8 +198,8 @@ async function handleQuickPlay() {
colyseusService.lobbyRoom.value = null; colyseusService.lobbyRoom.value = null;
} }
console.log('Navigating to /demo...'); console.log('Navigating to /:uuid/demo...');
await router.push('/demo'); await router.push(`/${routeUuid.value}/demo`);
console.log('Navigation complete'); console.log('Navigation complete');
} catch (error) { } catch (error) {
console.error('Failed to join game:', error); console.error('Failed to join game:', error);
@@ -234,7 +218,7 @@ async function joinRoom(roomId: string) {
colyseusService.lobbyRoom.value.leave(); colyseusService.lobbyRoom.value.leave();
colyseusService.lobbyRoom.value = null; colyseusService.lobbyRoom.value = null;
} }
router.push('/demo'); router.push(`/${routeUuid.value}/demo`);
} catch (error) { } catch (error) {
console.error('Failed to join room:', error); console.error('Failed to join room:', error);
isJoining.value = false; isJoining.value = false;

View File

@@ -6,15 +6,21 @@ export default defineConfig({
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
port: 3004, port: 3004,
allowedHosts: ['z590.interno.com'], allowedHosts: ['z590.interno.com', 'snatchgame.interno.com'],
cors: { cors: {
origin: ['http://localhost:3004', 'http://z590.interno.com:3004'] origin: ['http://localhost:3004', 'http://z590.interno.com:3004']
}, },
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: process.env.VITE_DEV_API_PROXY_TARGET || 'http://localhost:3000',
changeOrigin: true changeOrigin: true
},
'/ws': {
target: process.env.VITE_DEV_WS_PROXY_TARGET || 'http://localhost:3000',
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(/^\/ws/, '')
} }
} }
} }
}) })

View File

@@ -11,7 +11,8 @@
"dev:client": "cd client && npm run dev", "dev:client": "cd client && npm run dev",
"build": "npm run build:server && npm run build:client", "build": "npm run build:server && npm run build:client",
"build:server": "cd server && npm run build", "build:server": "cd server && npm run build",
"build:client": "cd client && npm run build" "build:client": "cd client && npm run build",
"generate:uuids": "node scripts/generate-uuids.js --count 210"
}, },
"author": "", "author": "",
"license": "MIT" "license": "MIT"

92
scripts/generate-uuids.js Normal file
View File

@@ -0,0 +1,92 @@
#!/usr/bin/env node
"use strict";
// Generate UUIDs and write them to server/src/config/uuids.json.
// Usage:
// node scripts/generate-uuids.js --count 210 [--force|--append]
// Defaults: count=210, no overwrite if file has entries.
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
function parseArgs(argv) {
const args = { count: 210, force: false, append: false };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === "--force") args.force = true;
else if (a === "--append") args.append = true;
else if (a === "--count") {
const n = Number(argv[++i]);
if (!Number.isFinite(n) || n <= 0) throw new Error("--count must be a positive number");
args.count = Math.floor(n);
} else if (/^--count=/.test(a)) {
const n = Number(a.split("=")[1]);
if (!Number.isFinite(n) || n <= 0) throw new Error("--count must be a positive number");
args.count = Math.floor(n);
} else {
throw new Error(`Unknown argument: ${a}`);
}
}
return args;
}
function genUuid() {
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
// Fallback: RFC4122 v4-ish
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (crypto.randomBytes(1)[0] % 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function uniqueUuids(n, existing = new Set()) {
const set = new Set(existing);
while (set.size < n) set.add(genUuid());
return Array.from(set);
}
function main() {
const { count, force, append } = parseArgs(process.argv);
const target = path.resolve(__dirname, "../server/src/config/uuids.json");
let current = [];
if (fs.existsSync(target)) {
try {
const raw = fs.readFileSync(target, "utf8");
const arr = JSON.parse(raw);
if (Array.isArray(arr)) current = arr.filter(x => typeof x === "string");
} catch {}
}
if (!force && !append && current.length > 0) {
console.log(`uuids.json already has ${current.length} entries. Use --force to overwrite or --append to add.`);
process.exit(0);
}
let out = [];
if (force) {
out = uniqueUuids(count);
} else if (append) {
const needed = Math.max(count, current.length + count);
const set = new Set(current);
out = uniqueUuids(needed, set);
} else {
// initial write when empty
out = uniqueUuids(count);
}
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, JSON.stringify(out, null, 2) + "\n", "utf8");
console.log(`Wrote ${out.length} UUIDs to ${path.relative(process.cwd(), target)}`);
}
try {
main();
} catch (err) {
console.error(err.message || err);
process.exit(1);
}

View File

@@ -0,0 +1,212 @@
[
"8df02e8c-01b5-48be-a3e4-00eaf98324ba",
"295da55d-9273-4ac1-be95-cda7ad8a9437",
"62f2e8bc-e440-48ef-a909-c02ec1631bea",
"c76e9864-6c8e-4289-9e0c-5b72996b2246",
"fa430689-838c-4463-aa03-69aad1147c33",
"8bda0768-1a51-47de-903b-017726436cdb",
"1a1f8a8b-cd26-4ec6-a0c8-0041be19aa29",
"8d23a06b-0374-4df0-b04d-7a752731b88b",
"942368e1-3222-42e4-8843-a566820bdbe5",
"08177310-2120-4a92-9400-651b101e01f5",
"df82db78-fa3f-48ae-bc39-2baac6dfe136",
"713d6450-d7b5-4780-8a46-92b7e402ea6f",
"c437cd56-abc9-4018-9a6f-3a6154f1cefd",
"0d5c455c-d4e6-4fd1-a941-13e98ff01892",
"1512b77d-20c3-4297-a3c5-186f2ba07c60",
"fe094c84-53b8-4341-9d1d-faa56c11b5f4",
"9a1e97ac-d55a-491d-addf-278c339d948f",
"9cb42819-14a0-4e2c-900a-53d3077446df",
"69eddab2-0a7e-451c-b57c-e6d44f3233b0",
"4e87cb12-c7ee-4fec-8160-c1b137c5a543",
"fbdbcb18-edb4-4cb7-badd-9a143279db9d",
"efa9cda2-d493-4594-b43b-d27ecee55888",
"fccd77dd-80dc-4de6-ab22-3be4ae5249d2",
"107ce00b-8b40-4e6a-a43b-c6eba1e8b37d",
"517a172f-ce37-48ad-9c88-74d0892e4353",
"f15a682e-407a-48c3-986d-bf088ec3e771",
"5384b49e-c8a6-4d4f-a6b4-4a2af39d96db",
"e7e5597d-4795-4693-a837-d2ff5a1f0cc6",
"d9d59c56-f187-4de6-9a2b-3ea362cd963d",
"7dfb28b8-4eac-427c-a035-756927714c26",
"db679d7d-dd96-49cb-8d12-70bc9df3373c",
"98f91e36-9dfb-406c-9535-3c32aa247c66",
"d61c6569-e989-41aa-9751-6f560c983811",
"3fb1eca8-81fa-4d0d-92a8-908ce3df93a7",
"8b954ba6-22e7-4263-af82-7f2765b39cfc",
"0b4831c6-7d74-4871-b768-7b72ec09ada4",
"72da1235-919f-4f9a-8b5e-135398b0eede",
"9d716b0a-a890-4a07-85c1-54158edd817e",
"18540b84-ff5c-4cd7-8bb6-f6c56153da6e",
"73ec5ef7-33db-46e3-a3c0-7a7957869bd6",
"14b12ee6-1e00-4874-9538-cc8c87c2627d",
"2060d21a-522c-4982-ac0b-a8880a3d6cdf",
"6690d45e-e66a-466b-b974-1c310682c6fd",
"bf49a5e5-7a7a-4249-958d-8617d067c1c7",
"3a71e8ff-5cf9-4d4e-bbe6-96afa5274be4",
"a3548476-81e6-4443-a836-1720d7d05d6e",
"07b633bb-7abd-4748-a68d-87efde1c6c51",
"de405584-d2de-4b94-b263-d13f3757a95c",
"7615cd00-b8c9-4e8d-b521-cc4add933fa7",
"1a76009c-fd75-43b6-9f0a-c6b00f4456ab",
"c3e8977f-aed9-478e-92ad-c459e6e3a558",
"2898b6c8-553a-4756-ba70-9d867ff1a5fd",
"7e83f1af-55a5-46ae-98a8-96ea88dfebb9",
"b4c6bca6-65af-489b-8617-b1d85dba86e2",
"e47def64-e799-4883-af2d-c3611e41e76e",
"e18446ee-2ecd-45d6-bbf5-8afe3bcc15a3",
"bb86fc01-4d33-463d-9d2b-663371e52808",
"b3c3a8b7-d1f6-4ef6-a10d-bde179b35122",
"e0d32bc9-b8a6-4ed0-b153-c45ca1ece6be",
"d2f66ef8-54be-4ae3-ac8b-0e4a17140d84",
"d94ea1fa-21ba-4c81-ab61-479f604b76f6",
"a5ae508a-e8c4-4db3-b27d-e7be4b0f2d63",
"7b9aff5c-840f-4b35-85cd-0cbd00badc52",
"c67968f2-db78-41d9-bcd7-f87a8ac27d26",
"3b248115-658d-435c-8eaa-1b47dbe60a19",
"b4375a97-ced1-42de-91db-e3c2ed476149",
"69fb71a8-a95f-4462-9221-c68621f27f9f",
"767c6049-c892-46c3-955f-7931b6ff6973",
"0b9f828e-56ed-44c7-b995-7ebec6eaf9f2",
"6031717b-3433-475d-be83-1b52c005df44",
"51849e97-1212-4d2f-a308-f7b552be5a53",
"537c40dd-d06a-464e-bc21-f216524fb27f",
"339e5a3e-261b-40c4-bda9-0e49dce30f80",
"e3663772-b3d8-4b27-b01e-5b88ee43dc85",
"4280c9ba-fd6d-40e5-b6f3-968d338457c6",
"b0e16d48-d4e6-4129-b2f9-d070855dd107",
"b6e375cb-edac-4e43-be3c-b16d332162c3",
"bdefc162-9a36-4b74-a055-3b0146f3483c",
"3d422adf-1533-4f65-a35e-400bdc412295",
"73e0c5bb-8c2c-449e-a6c1-f3f5dbca1302",
"b59ec692-eead-4935-a0c0-aa38e562e0f4",
"4116f677-3386-47ce-aec5-72abb20bebef",
"e22229f5-dee5-4823-b0dd-7f7fa702b433",
"86ff368c-591e-42cd-b70f-d9019cde0277",
"23298dc5-64ea-4c14-9014-afe9577befff",
"ab596ec4-8016-4327-b61a-974ffd87d11c",
"0517b0c1-2546-4ec6-a224-c268622bdde5",
"2cd241b0-60cf-4413-a644-11d4a955842f",
"90a5b4c8-e86d-484c-8c63-47884c2ec96d",
"79e022ab-c068-456b-8045-d6a29d33e01d",
"39815855-fd67-414b-9ec7-cb65379984b5",
"aab80f37-f4e0-43f0-bf13-3627cf1b29ce",
"798de169-ba78-4ea6-85a7-19cf8061c76a",
"84d3cb6d-5f1f-4462-bafb-0ba7dc4ee31c",
"0fd349cb-979c-41a5-8f9d-d315905b8f58",
"694b7501-8b75-490f-bf47-e7fad08c8d05",
"a4384446-3a60-4fe7-9141-eee5a13cbc00",
"fa8337a3-fd52-49bb-a37b-429f131c98a1",
"6e1d1689-0d35-4dc2-a29b-cf7084f5d6b5",
"3fba6cf9-f0f4-40df-8102-66bf215730c6",
"72b553a7-d32d-4dc7-8d49-8a7a160142d1",
"049f9df1-e4c0-4d80-b7c7-9f4696dd915b",
"bfab4629-199d-4903-bcfb-e335fd0ae6a7",
"ff894277-9a98-43df-add4-349dc4929042",
"2933833d-98f2-41bd-838a-d80cf481142b",
"7cedb724-c4d0-4b70-b9a7-7f5b6dfe8521",
"1adbece5-e0e8-43a2-89bd-0b2d0883f435",
"9cf256fd-a6c9-4089-ab2a-8f6539197996",
"02511ad5-2503-4091-8606-79155bb79786",
"570c966a-882f-498e-b988-8408c74d16ac",
"ddbf38df-398b-4633-8d33-9acdc61b0150",
"95532a62-2ae3-47c0-af26-0e52d531a5c8",
"20aee4d3-75ee-484b-bc92-65c682981ebe",
"7395a1fe-2ab3-432f-a888-bcc9daaa08ef",
"d660de2c-57d9-4366-a149-94810abecb82",
"07dda1ba-dc8a-4fe3-9188-3d644a1992b4",
"e2cd81bb-1511-47a5-b560-f0c282f30bda",
"42051ccf-bd08-4e43-a32d-c96c9649e329",
"50fda467-da61-4cef-8d86-650eacdeaaca",
"89f33d75-90d6-43af-9fd4-e1e481c274ff",
"8da6c0ad-447e-470d-8f98-891e518f942b",
"421b4858-137a-4935-816d-3ee5f856f007",
"47465c1c-5955-48ea-ba46-64a323ba7f70",
"54d8d4c6-3c18-4d19-877b-d41b632fc81e",
"cb93a50f-5585-4d84-9405-85598bb30deb",
"831252bd-8e18-4471-a4d5-01c84256159e",
"81f68488-7cff-44ab-ae08-73c51aac44a3",
"55ed1c19-7648-4a17-83da-507ed81798e3",
"2d3e55e5-991f-4122-a0fa-260402c7a5b5",
"3795b64a-eb15-4bc9-a7b2-9d8f9e0d2708",
"390f0af1-81a6-46c1-b2fc-70c5ec2cba37",
"9d026d2b-5a58-45d2-bf74-a79fb9b733e9",
"7f16bc71-25e9-4986-8e77-8e6f7bc2c1e3",
"2e3ea8e0-c776-481b-ac55-df6aa628772a",
"1ad99851-acea-4d20-b8d3-b891b24c949d",
"9dd15f53-cc4c-4a39-9a4c-33d37152b487",
"85f42124-c6d5-4cc2-93c6-6530d2c0a262",
"125a9ff8-71f7-432c-b368-d488c05bbea4",
"18064098-20c0-4bc6-9296-221dbe0cd91b",
"7506497e-975f-4bd8-a8cf-26b33de53936",
"b87f44f6-26ba-4251-9541-9c3e6d520f6e",
"b344d20f-738a-4361-ac64-752dd859ecf4",
"c836291f-c1eb-4d7e-8f0f-6f19113b0624",
"5e1dcd2c-3e8e-4970-b336-475fd10d9723",
"116f07da-dc77-48fe-a49b-8d904cad34b1",
"830086b5-89d7-4740-b99a-bfd35a4256df",
"b7a375f6-a2fd-415e-bb7d-545208e211ed",
"a129bf82-a975-4d52-97b6-635873217883",
"6f6c5a5d-402d-4b62-b70c-1877c43d21d3",
"f2760c22-0b06-4a31-bf9e-e07a39d23396",
"50b6b35e-1d63-4e00-9e85-9974efecb3af",
"8307716d-b4e2-4424-95b2-cac3c3fe8fbd",
"f7f023b2-a557-435d-9749-82ff3cae7aaf",
"b80b1799-c1b2-475a-94e2-fa7704dc570b",
"7914a266-ec02-4ecf-b789-0ca7ddf5b5fd",
"e8bfaf2e-2ca8-4efc-917e-d9b06a793fd0",
"4408781d-951b-4f0b-b33b-08bdc2f1ebdf",
"8cfb3846-d177-4273-a5dc-1687a9651059",
"52326d11-1f13-4642-9536-ec72e779742c",
"a1f39982-1353-4ad6-a7e8-5f52d1433bca",
"512ffbf1-581c-4ab5-a062-acee5a27ab8b",
"dac86456-9028-4205-b589-069b25444675",
"8a2e57fe-5fb3-4322-a34d-053b5a57f542",
"a57972b6-0e86-4294-925c-30e1316693aa",
"9e36b551-0956-4861-80c4-72bef2e4d788",
"375d5b6e-8690-4fce-aa0b-61f146fcada4",
"c9a48043-502a-4872-8302-43408cc1629c",
"d59b6da9-a887-487d-96c2-c0f3e48d6faa",
"f928ae74-7187-4025-8d97-083d22741383",
"0b094fc1-fcea-4bc6-a980-379621ea8717",
"610a1eff-b097-499e-9861-4fc06a17d60e",
"eb1a6f6e-65b7-4b8d-ad41-76edc64188aa",
"75da10be-c838-4b45-9bba-df745b34d62f",
"fb158cb8-bd69-4378-9a7c-6aeb9efa7bed",
"3d64be5f-2e9b-47f5-a084-b52680766b6f",
"fee4f458-c4d9-4dd5-902b-892862a70156",
"241671bb-45f6-4e88-8bd1-3910bbd8b9b3",
"13a3af48-d534-420f-82ed-7a5c57470789",
"b1fc197e-b583-4226-88aa-19e09b5c9412",
"8b4ff431-e3e6-4446-8a31-227c7e4ac5f9",
"780fcf48-9b8d-47c6-97f7-5bbf069e9631",
"ae780028-e98e-4c78-bead-3e31f50c5d50",
"5e440039-f316-443a-a0df-782a7766d8e1",
"9a9db992-0417-4c4f-a544-d96c6d529134",
"1fa1499f-1e5e-4439-be7e-bd72778d0df7",
"93b12ad8-8313-4c67-ac51-be35dfbd6112",
"85503d1c-3fc3-4dac-a4b5-9b4f2d8eb851",
"b44ee060-4eda-4a5e-8c1e-50b820152b7d",
"df73713b-7f80-4790-95c2-74002dc0d59a",
"c012d355-b501-4563-84a5-089e5e9acb10",
"5d1d9afc-0d77-416f-a06d-1a8c29014bf7",
"9312e135-6aea-49f8-8734-092696641aab",
"6573c846-9974-4a44-99ff-db5b002c43d9",
"fd7c1b34-a6dd-4b99-adb5-3ad4e1c50e4c",
"4d5f7d8d-3b17-4eee-b4fa-b08f07509d34",
"b0d05423-9cbd-4904-862b-3c8f8b47e6e7",
"c7909ee2-6334-4965-b107-b82c3116552b",
"fe52489b-bc66-406f-b2be-2635b35002ee",
"b2eb5dcf-b7c8-4e50-ab3d-49b5a05319d2",
"e40e6b77-d032-44d9-943a-bf6fe1a5e152",
"82a3c8ee-94ef-4a16-bb1b-5ee6aa20a23c",
"3a8a1962-f1b4-44a2-8532-ea17dfbf6724",
"60c07203-dc60-47df-a1c5-fd6761da7a8d",
"776936b8-7f3f-4357-a73e-11cd3367d681",
"9cfa7d17-f595-4d2d-90ef-287337d6eaa8",
"7743cd91-bf64-4ba7-9188-5343adfa8268",
"f0bde278-86aa-41f3-8a6a-fc2cc83e9cfb",
"6209690c-7f04-42fb-b2ae-e266c0589eb9",
"21cea9b2-78ed-4da2-a277-b266643e92be",
"92e0c4d0-b98e-4238-b80a-436b12dec827"
]

View File

@@ -1,6 +1,7 @@
import { Room, Client, matchMaker } from "colyseus"; import { Room, Client, matchMaker } from "colyseus";
import { LobbyState, AvailableRoom } from "./schemas/LobbyState"; import { LobbyState, AvailableRoom } from "./schemas/LobbyState";
import { NameManager } from "../utils/nameManager"; import { NameManager } from "../utils/nameManager";
import { isUuidAllowed } from "../utils/uuidRegistry";
export class LobbyRoom extends Room<LobbyState> { export class LobbyRoom extends Room<LobbyState> {
private updateInterval?: NodeJS.Timeout; private updateInterval?: NodeJS.Timeout;
@@ -33,6 +34,12 @@ export class LobbyRoom extends Room<LobbyState> {
onJoin(client: Client, options: any) { onJoin(client: Client, options: any) {
console.log(`[LobbyRoom] ${client.sessionId} joined lobby with UUID: ${options.uuid}`); console.log(`[LobbyRoom] ${client.sessionId} joined lobby with UUID: ${options.uuid}`);
// Enforce UUID presence and allowlist (if configured)
if (!options.uuid || !isUuidAllowed(options.uuid)) {
try { client.send("error", { message: "UUID inválido o faltante" }); } catch {}
try { client.leave(1000); } catch {}
return;
}
// Store UUID mapping if provided // Store UUID mapping if provided
if (options.uuid) { if (options.uuid) {
@@ -47,6 +54,11 @@ export class LobbyRoom extends Room<LobbyState> {
// Add player temporarily to lobby state // Add player temporarily to lobby state
const existingName = NameManager.getInstance().getPlayerName(options.uuid); const existingName = NameManager.getInstance().getPlayerName(options.uuid);
this.state.addPlayer(client.sessionId, existingName || ""); this.state.addPlayer(client.sessionId, existingName || "");
const existingColor = NameManager.getInstance().getPlayerColor(options.uuid);
if (existingColor) {
const p = this.state.players.get(client.sessionId);
if (p) p.color = existingColor;
}
// Send welcome first // Send welcome first
client.send("welcome", { client.send("welcome", {
@@ -68,19 +80,17 @@ export class LobbyRoom extends Room<LobbyState> {
// Check if this UUID already has a name // Check if this UUID already has a name
const existingName = NameManager.getInstance().getPlayerName(options.uuid); const existingName = NameManager.getInstance().getPlayerName(options.uuid);
this.state.addPlayer(client.sessionId, existingName || ""); this.state.addPlayer(client.sessionId, existingName || "");
const existingColor = NameManager.getInstance().getPlayerColor(options.uuid);
if (existingColor) {
const p = this.state.players.get(client.sessionId);
if (p) p.color = existingColor;
}
client.send("welcome", { client.send("welcome", {
sessionId: client.sessionId, sessionId: client.sessionId,
name: existingName || "", name: existingName || "",
color: this.state.players.get(client.sessionId)?.color || "#667eea" color: this.state.players.get(client.sessionId)?.color || "#667eea"
}); });
} else {
// Fallback for clients without UUID (shouldn't happen in normal flow)
this.state.addPlayer(client.sessionId, "");
client.send("welcome", {
sessionId: client.sessionId,
color: this.state.players.get(client.sessionId)?.color || "#667eea"
});
} }
this.updateAvailableRooms(); this.updateAvailableRooms();
@@ -116,7 +126,7 @@ export class LobbyRoom extends Room<LobbyState> {
} }
const uuid = this.sessionToUuid.get(client.sessionId) || client.sessionId; const uuid = this.sessionToUuid.get(client.sessionId) || client.sessionId;
const uniqueName = NameManager.getInstance().generateUniquePlayerName(data.name, uuid); const uniqueName = NameManager.getInstance().setPlayerName(uuid, data.name);
currentPlayer.name = uniqueName; currentPlayer.name = uniqueName;
@@ -134,6 +144,8 @@ export class LobbyRoom extends Room<LobbyState> {
return; return;
} }
currentPlayer.color = sanitized; currentPlayer.color = sanitized;
const uuid = this.sessionToUuid.get(client.sessionId) || client.sessionId;
NameManager.getInstance().setPlayerColor(uuid, sanitized);
client.send("colorUpdated", { color: sanitized }); client.send("colorUpdated", { color: sanitized });
} }
@@ -233,4 +245,4 @@ export class LobbyRoom extends Room<LobbyState> {
console.error("[LobbyRoom] Error updating available rooms:", error); console.error("[LobbyRoom] Error updating available rooms:", error);
} }
} }
} }

View File

@@ -1,6 +1,7 @@
export class NameManager { export class NameManager {
private static instance: NameManager; private static instance: NameManager;
private uuidToName: Map<string, string> = new Map(); private uuidToName: Map<string, string> = new Map();
private uuidToColor: Map<string, string> = new Map();
// For shuffle functionality // For shuffle functionality
private roomAssignments: Map<string, { roomId: string; role: 'P1' | 'P2' }> = new Map(); private roomAssignments: Map<string, { roomId: string; role: 'P1' | 'P2' }> = new Map();
@@ -15,31 +16,27 @@ export class NameManager {
return NameManager.instance; return NameManager.instance;
} }
// Legacy method used for first-time assignment; keeps global uniqueness.
generateUniquePlayerName(baseName: string, uuid: string): string { generateUniquePlayerName(baseName: string, uuid: string): string {
// If UUID already has a name, return it const normalizedName = (baseName || '').trim().toLowerCase() || 'guest';
const existingName = this.uuidToName.get(uuid); return this.setPlayerName(uuid, normalizedName);
if (existingName) { }
return existingName;
}
const normalizedName = baseName.trim().toLowerCase(); // Explicitly set/update the name for a UUID, ensuring uniqueness across other UUIDs.
if (!normalizedName) { setPlayerName(uuid: string, baseName: string): string {
// Default base name when none is provided const normalizedName = (baseName || '').trim().toLowerCase() || 'guest';
return this.generateUniquePlayerName('guest', uuid);
}
// Try exact name if not in use; otherwise, append incremental suffixes const isInUseByOther = (name: string) => {
const isInUse = (name: string) => { for (const [k, v] of this.uuidToName.entries()) {
for (const val of this.uuidToName.values()) { if (k !== uuid && v === name) return true;
if (val === name) return true;
} }
return false; return false;
}; };
let uniqueName = normalizedName; let uniqueName = normalizedName;
if (isInUse(uniqueName)) { if (isInUseByOther(uniqueName)) {
let n = 2; let n = 2;
while (isInUse(`${normalizedName}-${n}`)) n++; while (isInUseByOther(`${normalizedName}-${n}`)) n++;
uniqueName = `${normalizedName}-${n}`; uniqueName = `${normalizedName}-${n}`;
} }
@@ -56,6 +53,17 @@ export class NameManager {
return this.uuidToName.get(uuid); return this.uuidToName.get(uuid);
} }
// Color persistence per UUID
setPlayerColor(uuid: string, color: string): void {
const sanitized = (color || '').toString().trim();
if (!sanitized) return;
this.uuidToColor.set(uuid, sanitized);
}
getPlayerColor(uuid: string): string | undefined {
return this.uuidToColor.get(uuid);
}
getAllActivePlayers(): string[] { getAllActivePlayers(): string[] {
return Array.from(this.uuidToName.values()); return Array.from(this.uuidToName.values());
} }
@@ -90,4 +98,4 @@ export class NameManager {
getAllRoomAssignments(): Map<string, { roomId: string; role: 'P1' | 'P2' }> { getAllRoomAssignments(): Map<string, { roomId: string; role: 'P1' | 'P2' }> {
return new Map(this.roomAssignments); return new Map(this.roomAssignments);
} }
} }

View File

@@ -0,0 +1,64 @@
// UUID allowlist registry.
// Sources:
// - server/src/config/uuids.json (array of strings)
// - ENV `UUID_ALLOWLIST` (comma-separated)
// If both are provided, they are merged.
// If the final set is empty, any UUID is accepted (dev-friendly).
import uuidsFile from "../config/uuids.json";
let allowedUuids = new Set<string>();
function normalizeUuid(u: string): string | null {
const s = (u || "").trim();
if (!s) return null;
// Basic UUID v4 format guard; relax if your UUIDs differ
const re = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
return re.test(s) ? s.toLowerCase() : s.toLowerCase();
}
function loadFromFile(): string[] {
try {
if (Array.isArray(uuidsFile)) {
return uuidsFile.filter(x => typeof x === "string") as string[];
}
} catch {}
return [];
}
function loadFromEnv(): string[] {
try {
const raw = process.env.UUID_ALLOWLIST || "";
if (!raw.trim()) return [];
return raw.split(",").map(s => s.trim()).filter(Boolean);
} catch {}
return [];
}
function buildAllowlist(): Set<string> {
const set = new Set<string>();
const union = [...loadFromFile(), ...loadFromEnv()];
union.forEach(u => {
const n = normalizeUuid(u);
if (n) set.add(n);
});
return set;
}
export function reloadUuidAllowlist(): void {
allowedUuids = buildAllowlist();
}
// initialize on module load
reloadUuidAllowlist();
export function isUuidAllowed(uuid: string | undefined | null): boolean {
if (!uuid) return false;
if (allowedUuids.size === 0) return true;
const n = normalizeUuid(uuid);
return !!(n && allowedUuids.has(n));
}
export function getAllowedUuidCount(): number {
return allowedUuids.size;
}