bzl

self-hosted ephemeral community engine
Log | Files | Refs | README | LICENSE

commit 666b227a62281f0ec872e0b848bbe87d1aa6535b
parent ffd926c06cb492ccdf199450475bcd886b1ac303
Author: SageAzakaela <106701693+SageAzakaela@users.noreply.github.com>
Date:   Sun, 22 Feb 2026 15:36:57 -0700

fixed map plugins avatar preset management bug

Server fix (delete/apply/update by normalized ID): server.js (line 641)
Added findAvatarPresetIndexById() and now use it for:
upsertAvatarPreset
deleteAvatarPreset
applyAvatarPreset
This avoids silent mismatches when IDs aren’t perfectly aligned.
Server load hardening: server.js (line 843)
Added dedupe on preset IDs when loading from disk so corrupted/duplicate entries don’t poison behavior.
Better delete feedback: server.js (line 1450)
If a preset ID isn’t found, server now returns Preset not found. instead of silently doing nothing.
Client selection fix: client.js (line 5528)
After preset list refresh, if the selected preset no longer exists (e.g., deleted), it auto-selects the first available preset instead of leaving selection empty.
Synced runtime copies:
server.js
client.js
Validation:

server.js
client.js

Diffstat:
Adocs/SECURITY_UPGRADE_PRIVATE.md | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplugins_dev/maps/client.js | 2+-
Mplugins_dev/maps/server.js | 28++++++++++++++++++++++------
3 files changed, 128 insertions(+), 7 deletions(-)

diff --git a/docs/SECURITY_UPGRADE_PRIVATE.md b/docs/SECURITY_UPGRADE_PRIVATE.md @@ -0,0 +1,105 @@ +# PRIVATE: Security Upgrade Plan (Core) + +Last updated: 2026-02-22 + +This document is for internal planning and prioritization. + +## Scope + +Core server and first-party client: +- `server.js` +- `public/app.js` +- auth/session, plugin loading, WebSocket abuse controls, upload surface, role controls + +## Risk priorities + +### P0 (immediate) + +1. Tighten plugin trust boundary + - Move plugin install/enable/uninstall/reload from moderator to admin/owner. + - Add plugin signing + trusted publisher checks. + - Add optional `PLUGIN_INSTALL_DISABLED=1` hard lock for production. + +2. Add message flood controls + - Add rate limits for: + - `newPost` + - `chatMessage` + - `dmSend` + - `dmSendMod` + - Include server-side hard caps + backoff events. + +3. Raise password baseline + - Increase minimum from 4 to 10+. + - Add optional strong policy mode (length + complexity). + - Add migration note for existing instances. + +### P1 (near-term) + +4. Move session storage out of `localStorage` + - Switch to secure session cookies: + - `HttpOnly` + - `Secure` + - `SameSite=Lax` (or `Strict` if compatible) + - Keep token rotation and invalidation. + +5. WebSocket origin + payload hardening + - Add explicit origin allowlist env (`WS_ORIGIN_ALLOWLIST`). + - Reject unknown origins at WS handshake. + - Set explicit WS max payload and close on overflow. + +6. TURN credential hardening + - Move from static TURN credentials to short-lived credentials. + - Add relay abuse monitoring and quotas. + +### P2 (mid-term) + +7. Security headers and deployment defaults + - Add HSTS when HTTPS is confirmed. + - Re-check CSP for least privilege after plugin API updates. + +8. Audit and tamper logging + - Security log stream for: + - role changes + - plugin install/uninstall + - repeated failed auth + - unusual upload spikes + +9. Safe plugin execution model + - Evaluate plugin sandbox strategy: + - isolate process + - permission-gated API + - no raw filesystem/process access by default + +## New role model update (current change) + +Added base role: +- `admin` (between `moderator` and `owner`) + +Intent: +- Admin can manage plugins and access owner-level views. +- Owner remains final authority for destructive or ownership-sensitive operations. + +## Implementation checkpoints + +### Phase A (this release train) +- [x] Add `admin` role in core role hierarchy. +- [x] Restrict plugin management to admin/owner. +- [ ] Add RL buckets for post/chat/dm sends. + +### Phase B +- [ ] Cookie-based session migration. +- [ ] WS origin allowlist. +- [ ] WS payload limit hard cap. + +### Phase C +- [ ] Plugin signing and trust policy. +- [ ] Optional plugin sandbox architecture draft. + +## Validation checklist per phase + +- Unit/smoke test auth flows. +- Verify moderation matrix (member/mod/admin/owner). +- Load test chat + DM flood controls. +- Confirm plugin lifecycle still works for admin/owner only. +- Verify no regression in mobile/desktop UX around auth and chat. + diff --git a/plugins_dev/maps/client.js b/plugins_dev/maps/client.js @@ -5528,7 +5528,7 @@ avatarPresets = normalizeAvatarPresetList(msg?.presets); avatarPresetsCanManage = Boolean(msg?.canManage); if (!avatarPresets.some((preset) => preset.id === avatarPresetSelectedId)) { - avatarPresetSelectedId = ""; + avatarPresetSelectedId = avatarPresets[0]?.id || ""; } if (mode === "map" && avatarEditorOpen) renderMapView(); return; diff --git a/plugins_dev/maps/server.js b/plugins_dev/maps/server.js @@ -646,6 +646,12 @@ module.exports = function init(api) { ws.send(JSON.stringify({ type: "plugin:maps:avatarPresets", presets, canManage })); } + function findAvatarPresetIndexById(rawId) { + const targetId = normId(rawId || ""); + if (!targetId) return -1; + return avatarPresets.findIndex((preset) => normId(preset?.id || "") === targetId); + } + function sanitizeMapChatText(text) { const raw = typeof text === "string" ? text : ""; return raw.replace(/\s+/g, " ").trim().slice(0, 420); @@ -840,9 +846,12 @@ module.exports = function init(api) { } customMaps = next; const nextPresets = []; + const seenPresetIds = new Set(); for (const rawPreset of presetList) { const preset = normalizeAvatarPreset(rawPreset || {}, rawPreset?.updatedBy || rawPreset?.createdBy || ""); if (!preset) continue; + if (seenPresetIds.has(preset.id)) continue; + seenPresetIds.add(preset.id); nextPresets.push(preset); } avatarPresets = nextPresets; @@ -1411,7 +1420,7 @@ module.exports = function init(api) { ws.send(JSON.stringify({ type: "plugin:maps:error", message: "Invalid avatar preset." })); return; } - const idx = avatarPresets.findIndex((preset) => preset.id === normalized.id); + const idx = findAvatarPresetIndexById(normalized.id); if (idx >= 0) { const prior = avatarPresets[idx]; avatarPresets[idx] = { @@ -1449,9 +1458,12 @@ module.exports = function init(api) { } const id = normId(msg?.id || ""); if (!id) return; - const before = avatarPresets.length; - avatarPresets = avatarPresets.filter((preset) => preset.id !== id); - if (avatarPresets.length === before) return; + const idx = findAvatarPresetIndexById(id); + if (idx < 0) { + ws.send(JSON.stringify({ type: "plugin:maps:error", message: "Preset not found." })); + return; + } + avatarPresets.splice(idx, 1); try { saveCustomMapsToDisk(); } catch { @@ -1467,8 +1479,12 @@ module.exports = function init(api) { if (!username) return; const id = normId(msg?.id || ""); if (!id) return; - const preset = avatarPresets.find((item) => item.id === id); - if (!preset) return; + const idx = findAvatarPresetIndexById(id); + if (idx < 0) { + ws.send(JSON.stringify({ type: "plugin:maps:error", message: "Preset not found." })); + return; + } + const preset = avatarPresets[idx]; if (!preset.published && !canManageAvatarPresets(ws)) { ws.send(JSON.stringify({ type: "plugin:maps:error", message: "Preset unavailable." })); return;