Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 11 additions & 35 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2290,64 +2290,40 @@ async function linkedUsernameError(ctx) {


async function deleteEphemeralBonusPrompt(ctx, user) {
const activePrompt = await runUserMutation(user.id, async () => {
if (!user.ephemeralBonusMsgId || !user.ephemeralBonusChatId) return null;
return { msgId: user.ephemeralBonusMsgId, chatId: user.ephemeralBonusChatId };
});
if (!activePrompt) return;

if (!user.ephemeralBonusMsgId || !user.ephemeralBonusChatId) return;
try {
await ctx.telegram.deleteMessage(activePrompt.chatId, activePrompt.msgId);
await ctx.telegram.deleteMessage(user.ephemeralBonusChatId, user.ephemeralBonusMsgId);
} catch (_) { /* message may already be gone */ }

await runUserMutation(user.id, async () => {
if (user.ephemeralBonusMsgId === activePrompt.msgId && user.ephemeralBonusChatId === activePrompt.chatId) {
user.ephemeralBonusMsgId = null;
user.ephemeralBonusChatId = null;
}
});
user.ephemeralBonusMsgId = null;
user.ephemeralBonusChatId = null;
}

async function sendEphemeralBonusPrompt(ctx, user) {
if (user.claimedPromo) return;
const activeCode = getActivePromoCodeForUser(user);
if (!activeCode) return;
await deleteEphemeralBonusPrompt(ctx, user);

const sent = await ctx.reply(
`🎁 *Claim Your New User Bonus*

Enter promo code *${activeCode.code}* on the Runewager affiliate page to claim your ${activeCode.amountSC} SC new user bonus!`,
Enter promo code *${promoStore.code}* on the Runewager affiliate page to claim your ${promoStore.amountSC} SC new user bonus!`,
{
parse_mode: 'Markdown',
...Markup.inlineKeyboard([
[Markup.button.url('🎁 Enter Promo Code (Mini App)', LINKS.miniAppClaim)],
[Markup.button.callback('✅ I Have Claimed — Next Step', 'promo_confirm_claimed_next')],
[Markup.button.callback('⬅️ Main Menu', 'to_main_menu')],
]),
},
);

await runUserMutation(user.id, async () => {
user.ephemeralBonusMsgId = sent.message_id;
user.ephemeralBonusChatId = sent.chat.id;
});

user.ephemeralBonusMsgId = sent.message_id;
user.ephemeralBonusChatId = sent.chat.id;
setTimeout(async () => {
try {
await ctx.telegram.deleteMessage(sent.chat.id, sent.message_id);
} catch (_) { /* best effort */ }

await runUserMutation(user.id, async () => {
if (user.ephemeralBonusMsgId === sent.message_id && user.ephemeralBonusChatId === sent.chat.id) {
user.ephemeralBonusMsgId = null;
user.ephemeralBonusChatId = null;
}
});
if (user.ephemeralBonusMsgId === sent.message_id && user.ephemeralBonusChatId === sent.chat.id) {
user.ephemeralBonusMsgId = null;
user.ephemeralBonusChatId = null;
}
}, 15 * 1000);
}
Comment on lines +2292 to +2325

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Serialize ephemeral prompt state mutations through the per-user queue.

Lines 2111-2112, 2128-2129, and 2134-2137 mutate user state across async and timer boundaries without runUserMutation, which can race with concurrent handlers for the same user.

🔧 Proposed fix
 async function deleteEphemeralBonusPrompt(ctx, user) {
   if (!user.ephemeralBonusMsgId || !user.ephemeralBonusChatId) return;
+  const msgId = user.ephemeralBonusMsgId;
+  const chatId = user.ephemeralBonusChatId;
   try {
-    await ctx.telegram.deleteMessage(user.ephemeralBonusChatId, user.ephemeralBonusMsgId);
+    await ctx.telegram.deleteMessage(chatId, msgId);
   } catch (_) { /* message may already be gone */ }
-  user.ephemeralBonusMsgId = null;
-  user.ephemeralBonusChatId = null;
+  await runUserMutation(user.id, async () => {
+    if (user.ephemeralBonusMsgId === msgId && user.ephemeralBonusChatId === chatId) {
+      user.ephemeralBonusMsgId = null;
+      user.ephemeralBonusChatId = null;
+    }
+  });
 }
 
 async function sendEphemeralBonusPrompt(ctx, user) {
+  await deleteEphemeralBonusPrompt(ctx, user);
   const sent = await ctx.reply(
@@
-  user.ephemeralBonusMsgId = sent.message_id;
-  user.ephemeralBonusChatId = sent.chat.id;
+  await runUserMutation(user.id, async () => {
+    user.ephemeralBonusMsgId = sent.message_id;
+    user.ephemeralBonusChatId = sent.chat.id;
+  });
   setTimeout(async () => {
     try {
       await ctx.telegram.deleteMessage(sent.chat.id, sent.message_id);
     } catch (_) { /* best effort */ }
-    if (user.ephemeralBonusMsgId === sent.message_id && user.ephemeralBonusChatId === sent.chat.id) {
-      user.ephemeralBonusMsgId = null;
-      user.ephemeralBonusChatId = null;
-    }
+    await runUserMutation(user.id, async () => {
+      if (user.ephemeralBonusMsgId === sent.message_id && user.ephemeralBonusChatId === sent.chat.id) {
+        user.ephemeralBonusMsgId = null;
+        user.ephemeralBonusChatId = null;
+      }
+    });
   }, 15 * 1000);
 }

As per coding guidelines Use per-user mutation queues to prevent race conditions on concurrent bonus/state mutations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.js` around lines 2106 - 2139, Both deleteEphemeralBonusPrompt and
sendEphemeralBonusPrompt mutate user.ephemeralBonusMsgId and
user.ephemeralBonusChatId across async/timer boundaries without serialization;
wrap every read/write that affects those fields (including the async
ctx.telegram.deleteMessage calls and the setTimeout cleanup) inside the per-user
queue by calling runUserMutation (or the project's equivalent) so mutations are
executed serially for that user and compare/update the fields only within those
serialized callbacks to avoid races with concurrent handlers.



const adminEventsLogFile = path.join(dataDir, 'admin-events.log');
const ADMIN_LOG_CAP = 200;

Expand Down
6 changes: 0 additions & 6 deletions prod-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,6 @@ mkdir -p "$PROJECT_DIR/data"
mkdir -p "$PROJECT_DIR/data/backups"
touch "$MAIN_LOG" "$ERROR_LOG" "$ADMIN_EVENTS_LOG" "$SSHV_SESSIONS_FILE" # create if missing; safe on existing files

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The data directory and JSON/log files are created by this script (often run as root), but the Node service typically runs under a non-root service user; without correcting ownership, the service user cannot write to data/admin-events.log, sshv-sessions.json, or other files under data, breaking persistence of admin events and SSHV sessions. [logic error]

Severity Level: Critical 🚨
- ❌ /sshv admin console sessions fail to persist across restarts.
- ❌ Admin events log writes fail or crash admin commands.
- ⚠️ God-mode heal diagnostics see stale admin-events.log data.
- ⚠️ Future JSON persistence features under data/ may break.
Suggested change
# Ensure data directory and files are writable by the service user, if it exists
if id -u "$APP_NAME" >/dev/null 2>&1; then
chown "$APP_NAME:$APP_NAME" "$PROJECT_DIR/data" "$PROJECT_DIR/data/backups" \
"$ADMIN_EVENTS_LOG" "$SSHV_SESSIONS_FILE" || true
fi
Steps of Reproduction ✅
1. On a VPS host, create a dedicated system user `runewager` and group (as implied by the
systemd unit user selection in `prod-run.sh:141-143`, where `svc_user="$APP_NAME"` and
`APP_NAME="runewager"` at `prod-run.sh:25`).

2. As root, run `prod-run.sh` from the project root; it resolves `PROJECT_DIR` and creates
runtime paths at `prod-run.sh:59-62`:

   - `mkdir -p "$PROJECT_DIR/data"` and `"$PROJECT_DIR/data/backups"`

   - `touch "$PROJECT_DIR/data/admin-events.log"` (via `ADMIN_EVENTS_LOG` at
   `prod-run.sh:29`)

   - `touch "$PROJECT_DIR/data/sshv-sessions.json"` (via `SSHV_SESSIONS_FILE` at
   `prod-run.sh:30`)

   These directories and files are owned by root:root with default permissions (typically
   755 for dirs, 644 for files).

3. The same script writes the systemd unit with `User=runewager` and `Group=runewager` in
`ensure_systemd_service()` at `prod-run.sh:133-170` and enables it; systemd then starts
`index.js` as the non-root `runewager` user from `WorkingDirectory=${PROJECT_DIR}`.

4. An admin uses Telegram admin features (e.g., `/sshv` or other admin commands) handled
in `index.js`, which:

   - Defines `const sshvSessionsFile = path.join(dataDir, 'sshv-sessions.json');` at
   `index.js:206` and later calls `saveJson(sshvSessionsFile, entries);` in
   `persistSshvSessions()` at `index.js:969`.

   - Defines `const adminEventsLogFile = path.join(dataDir, 'admin-events.log');` at
   `index.js:2141` and appends events with `fs.appendFileSync(adminEventsLogFile,
   JSON.stringify(entry) + '\n');` at `index.js:2152`.

   - Uses `function saveJson(filePath, data) { ... }` declared at `index.js:2271` to write
   JSON files under `data/`.

   When these writes execute under the `runewager` user, the process lacks write
   permission to the root-owned `data/` directory and files, causing `EACCES` filesystem
   errors. As a result, admin events are not logged, SSHV sessions cannot be persisted,
   and depending on error handling, the admin flows may throw or silently fail to persist
   data.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** prod-run.sh
**Line:** 58:58
**Comment:**
	*Logic Error: The data directory and JSON/log files are created by this script (often run as root), but the Node service typically runs under a non-root service user; without correcting ownership, the service user cannot write to `data/admin-events.log`, `sshv-sessions.json`, or other files under `data`, breaking persistence of admin events and SSHV sessions.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

if id -u "$SERVICE_USER" >/dev/null 2>&1; then
chown -R "$SERVICE_USER:$SERVICE_GROUP" "$LOG_DIR" "$PROJECT_DIR/data"
fi
chmod 0750 "$PROJECT_DIR/data" "$PROJECT_DIR/data/backups" "$LOG_DIR"
chmod 0640 "$MAIN_LOG" "$ERROR_LOG" "$ADMIN_EVENTS_LOG" "$SSHV_SESSIONS_FILE"

say "Ensured runtime directories and log files exist"

# ---------------------------------------------------------
Expand Down
Loading