Monorepo for Aesthetic.Computer aesthetic.computer
at main 138 lines 3.6 kB view raw
1// Profile Stream Publisher, 2026.02.27 2// Sends server-side profile events to session-server /profile-event. 3 4import { connect } from "./database.mjs"; 5 6const PROFILE_SECRET_CACHE_MS = 60 * 1000; 7let profileSecretCacheValue = null; 8let profileSecretCacheAt = 0; 9let profileSecretLoadPromise = null; 10 11function normalizeHandle(handle) { 12 if (!handle) return null; 13 const text = `${handle}`.trim(); 14 if (!text) return null; 15 if (!text.startsWith("@")) return null; 16 return `@${text.replace(/^@+/, "")}`; 17} 18 19function pickProfileStreamSecret(record) { 20 if (!record || typeof record !== "object") return null; 21 const candidates = [ 22 record.secret, 23 record.token, 24 record.profileSecret, 25 record.value, 26 ]; 27 for (const raw of candidates) { 28 if (!raw) continue; 29 const value = `${raw}`.trim(); 30 if (value) return value; 31 } 32 return null; 33} 34 35async function loadProfileStreamSecretFromMongo() { 36 const database = await connect(); 37 try { 38 const record = await database.db 39 .collection("secrets") 40 .findOne({ _id: "profile-stream" }); 41 return pickProfileStreamSecret(record); 42 } finally { 43 await database.disconnect?.(); 44 } 45} 46 47async function resolveProfileStreamSecret() { 48 const now = Date.now(); 49 if (profileSecretCacheAt && now - profileSecretCacheAt < PROFILE_SECRET_CACHE_MS) { 50 return profileSecretCacheValue; 51 } 52 53 if (profileSecretLoadPromise) return profileSecretLoadPromise; 54 55 profileSecretLoadPromise = (async () => { 56 let secret = null; 57 58 try { 59 secret = await loadProfileStreamSecretFromMongo(); 60 } catch (err) { 61 console.warn( 62 "Profile stream secret read from MongoDB failed:", 63 err?.message || err, 64 ); 65 } 66 67 if (!secret) { 68 const envSecret = `${process.env.PROFILE_STREAM_SECRET || ""}`.trim(); 69 secret = envSecret || null; 70 } 71 72 profileSecretCacheValue = secret; 73 profileSecretCacheAt = Date.now(); 74 return profileSecretCacheValue; 75 })(); 76 77 try { 78 return await profileSecretLoadPromise; 79 } finally { 80 profileSecretLoadPromise = null; 81 } 82} 83 84function profileEventBaseUrl() { 85 const configured = 86 process.env.SESSION_SERVER_PROFILE_EVENT_URL || process.env.SESSION_SERVER_URL; 87 if (configured) return `${configured}`.replace(/\/$/, ""); 88 return "https://session-server.aesthetic.computer"; 89} 90 91export async function publishProfileEvent(payload = {}, options = {}) { 92 const handle = normalizeHandle(payload.handle); 93 if (!handle) return false; 94 95 const timeoutMs = Number(options.timeoutMs) || 1500; 96 const body = { 97 ...payload, 98 handle, 99 }; 100 101 const headers = { 102 "Content-Type": "application/json", 103 }; 104 105 const profileSecret = await resolveProfileStreamSecret(); 106 if (profileSecret) { 107 headers["x-profile-secret"] = profileSecret; 108 } 109 110 const controller = new AbortController(); 111 const timer = setTimeout(() => controller.abort(), timeoutMs); 112 113 try { 114 const response = await fetch(`${profileEventBaseUrl()}/profile-event`, { 115 method: "POST", 116 headers, 117 body: JSON.stringify(body), 118 signal: controller.signal, 119 }); 120 121 if (!response.ok) { 122 const text = await response.text().catch(() => ""); 123 console.warn( 124 `Profile event publish failed (${response.status}):`, 125 text || response.statusText, 126 ); 127 return false; 128 } 129 130 return true; 131 } catch (err) { 132 const reason = err?.name === "AbortError" ? "timeout" : err?.message; 133 console.warn("Profile event publish error:", reason); 134 return false; 135 } finally { 136 clearTimeout(timer); 137 } 138}