Skip to content

Add admin view toggles, persistent admin dashboard, and lightweight /sshv VPS console; update .gitignore - #72

Merged
gamblecodezcom merged 6 commits into
mainfrom
codex/configure-git-and-update-project-files
Feb 25, 2026
Merged

Add admin view toggles, persistent admin dashboard, and lightweight /sshv VPS console; update .gitignore#72
gamblecodezcom merged 6 commits into
mainfrom
codex/configure-git-and-update-project-files

Conversation

@gamblecodezcom

@gamblecodezcom gamblecodezcom commented Feb 25, 2026

Copy link
Copy Markdown
Owner

User description

Motivation

  • Provide a safer, structured admin experience with an explicit admin-view toggle instead of implicit auth bypass, and make the admin dashboard persistent and self-consistent across menus.
  • Add a lightweight, interactive VPS console for admins to run short shell commands, inspect files (edit/save), and control running child processes without exposing dangerous background operators.
  • Improve UX by cleaning up menu visibility, help pages, and adding fallbacks for stale callback buttons.
  • Tidy repository ignores to avoid committing runtime artifacts and common editor/packaging files.

Description

  • Updated .gitignore to ignore node_modules, logs, temp/build folders, system files, and to consolidate .env patterns and remove checked-in runtime JSON artifacts.
  • Introduced admin-view state management (persistAdminMode, adminModeOn) and updated requireAdmin and multiple menus/help pages so admin UI is shown only when admin view is enabled, with persistent menus deleting their counterpart when opened.
  • Added a lightweight /sshv VPS console feature with session store and TTL (SSHV_SESSION_TTL_MS), session lifecycle helpers (getSshvSession, destroySshvSession), command execution via exec, output rendering (renderSshvConsole), editor mode for nano-like edits, locking/unlocking, Ctrl+C and Ctrl+Z handlers, confirmation for potentially dangerous commands, and an interval-based cleanup of expired sessions.
  • Registered /sshv in admin commands and wired admin keyboards/buttons to open the console and to run/lock/exit/editor actions; added many related callback handlers (sshv_open, sshv_run_prompt, sshv_confirm_run, sshv_ctrl_c, sshv_ctrl_z, sshv_lock, sshv_unlock, sshv_exit, sshv_editor_save, sshv_editor_cancel).
  • Added REGISTERED_COMMANDS set and an unknown-command response when a slash command is typed but not registered, to provide friendly guidance.
  • Added a global fallback bot.action(/.*/, ...) that acknowledges stale callback buttons and directs users back to /menu to recover.
  • Minor API/import adjustments (added exec to child_process imports) and multiple renames/keyboard key changes for admin dashboard actions to keep labels consistent.

Testing

  • No new automated test suite was added for these changes.
  • Performed a module-load smoke check (the runtime file loads cleanly under CI/disabled runtime guard) to ensure no immediate syntax/runtime registration errors during require.

Codex Task


CodeAnt-AI Description

Add persistent admin view toggles, a lightweight /sshv VPS console, and better fallback handling

What Changed

  • Admin UI is now a persistent dashboard in DMs and visible only when "admin view" is enabled; /on and /off toggle the admin view (visibility), not admin identity.
  • Admin commands and menus enforce real admin identity; enabling admin view does not bypass authentication.
  • New lightweight VPS console (/sshv) for admins: run foreground shell commands, send Ctrl+C/Ctrl+Z, lock/unlock session, edit files in a simple editor mode, and close sessions; sessions expire after ~10 minutes with automatic cleanup.
  • Unknown slash commands now return a friendly "Unknown command" hint; stale callback buttons are acknowledged and direct users back to /menu.

Impact

✅ Clearer admin UI visibility (on/off) in private chats
✅ Safer admin access — view toggle no longer bypasses auth
✅ Admins can run short VPS commands and edit files from the dashboard

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features

    • Interactive SSH-like admin console with per-admin sessions, command execution, editor support, and session timeouts.
    • Improved admin-mode controls so admin-only actions and views are toggleable and persist.
  • Chores

    • Consolidated ignore rules for env, logs, build artifacts, node_modules, and system files.
    • Shifted deployment guidance to systemd (removed Docker support) and tightened service runtime permissions.
    • Added example env key for payload verification.
  • Documentation

    • Added LICENSE and SECURITY policy; updated deployment/health docs.

@codeant-ai

codeant-ai Bot commented Feb 25, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai

coderabbitai Bot commented Feb 25, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@gamblecodezcom has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 11 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4cb3ca2 and ec3aa15.

📒 Files selected for processing (3)
  • deploy.sh
  • index.js
  • prod-run.sh
📝 Walkthrough

Walkthrough

Adds a per-admin SSHV (SSH-via-CLI) interactive console with session lifecycle, TTL cleanup, command execution, editor-like flows, admin-mode persistence/refactoring, and many admin UI/menu updates. Also updates repository metadata: .gitignore, Dockerfile removal, .env.example, LICENSE, SECURITY.md, runewager.service, and README deployment notes.

Changes

Cohort / File(s) Summary
Git configuration
/.gitignore
Consolidated env patterns to *.env; expanded log ignores and npm/yarn log rules; kept node_modules/; added dist/, build/, Thumbs.db; removed ignores for runtime JSON/backups and legacy bot state.
SSHV Console & Admin Mode
index.js
Large feature addition: per-admin SSHV sessions with TTL and cleanup, command execution (with timeouts, cd/path handling), nano-style editor flows, keyboard/button callbacks, admin-mode persistence/helpers, safety checks (confirm/block), rendering helpers and many admin UI/menu changes.
Environment
.env.example
Added WEBAPP_HMAC_KEY variable (new secret for WebApp payload verification).
Containerization / Docker
Dockerfile
Removed Dockerfile content (Docker build/run instructions deleted). README Docker section also removed in favor of systemd deployment.
Licensing & Security
LICENSE, SECURITY.md
Added MIT LICENSE and a new SECURITY.md security policy.
Systemd service & Deployment docs
runewager.service, README.md
runewager.service changed to run as runewager user/group, added ReadWritePaths and several hardening directives; README updated to emphasize systemd deployment and health-check behavior.

Sequence Diagram

sequenceDiagram
    actor Admin
    participant Bot
    participant Sessions as SessionMap
    participant System as child_process

    Admin->>Bot: open SSHV (button or /sshv)
    Bot->>Sessions: getSshvSession(adminId, createIfMissing=true)
    Sessions-->>Bot: session {cwd, output, locked, meta}
    Bot->>Admin: renderSshvConsole(session)
    Admin->>Bot: submit command (sshv_run_prompt)
    Bot->>Bot: commandNeedsConfirmation()/commandBlocked()
    alt needs confirmation
        Bot->>Admin: show confirm/cancel
        Admin->>Bot: sshv_confirm_run
    end
    Bot->>System: exec(command, {timeout, maxBuffer})
    System-->>Bot: stdout/stderr, exitCode
    Bot->>Sessions: update session.output, session.cwd
    Bot->>Admin: update console view (renderSshvConsole)
    Admin->>Bot: sshv_exit or inactivity TTL
    Bot->>Sessions: destroySshvSession(adminId)
    Sessions-->>Bot: session removed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

size:XXL

Poem

A rabbit taps the console key,
Hops through sessions, wild and free.
Admins type, the output sings,
TTL trims old, tidy things.
🐰✨ Hopping logs and SSH dreams.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: admin view toggles, persistent admin dashboard, lightweight /sshv VPS console, and .gitignore updates are all prominent features in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/configure-git-and-update-project-files

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Feb 25, 2026
Comment thread index.js Outdated
const user = getUser(ctx);
const action = user.pendingAction;

if (action.type === 'await_sshv_command') {

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 confirm_yes_username handler now accesses action.type before checking whether user.pendingAction exists and also references an undefined text variable in the SSHV branches; if this callback is triggered when pendingAction is null/undefined (for example, from a stale button), it will throw at action.type, and the SSHV branches are dead code that would also throw due to text being undefined. This breaks previously safe behavior where stale confirmations were handled gracefully. [null pointer]

Severity Level: Major ⚠️
- ⚠️ Stale username-confirm buttons now crash this callback.
- ⚠️ Admins using /sshv can hit undefined `text` path.
- ⚠️ Global stale-callback fallback is bypassed for this button.

@codeant-ai

codeant-ai Bot commented Feb 25, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Around line 6-11: Update the .gitignore to exclude the runtime state and
explicit admin log: add an entry for .last_rollback and add
data/admin-events.log (or re-pin the explicit path) so the runtime
.last_rollback file and the admin-events log are not tracked; modify the
existing ignore block that lists logs/*.log and related entries to include these
two specific patterns (i.e., .last_rollback and data/admin-events.log) so they
are properly ignored by git.

In `@index.js`:
- Around line 4307-4339: The handler wired in bot.action('confirm_yes_username')
dereferences action.type without null-check and uses an undefined text variable;
update the code to first ensure user.pendingAction exists before reading
action.type, and replace undefined text with the incoming message text (e.g.,
extract from ctx.message.text or ctx.update.message.text) when calling
executeSshvCommand and when assigning session.editorMode.draft; also make sure
the bot.on('text') handler consults user.pendingAction for 'await_sshv_command'
and 'await_sshv_editor_content' and delegates to requireAdmin, getSshvSession,
executeSshvCommand, renderSshvConsole, and the editor save flow so /sshv inputs
are handled consistently.
- Around line 932-1034: The SSHV handler executeSshvCommand currently performs
important actions (entering editor via the nanoMatch branch, changing directory
via resolveCdPath, and running arbitrary commands with exec) without recording
them; add calls to adminLog() at each of these points to create NDJSON entries
in data/admin-events.log: log entering editor (include user id/name,
filePath/fileArg, session.cwd) when setting session.editorMode and
user.pendingAction (sshv_editor_*), log cd attempts and outcomes (success or
error) when resolving and stat-ing nextPath and updating session.cwd, and log
command executions and final status (stdout/stderr truncated/summary and error
flag) when starting and when the exec callback completes (include session.cwd,
command, and whether it errored). Ensure adminLog entries are concise, include
actor metadata, and are appended as NDJSON to the admin-events log so operations
are auditable.
- Around line 898-910: The SSHV console and related editor/command flows
currently call escapeMarkdownV2() which doesn't escape all MarkdownV2 special
characters; update each occurrence to use escapeMarkdownFull() instead: replace
escapeMarkdownV2() calls inside renderSshvConsole (the text array building), the
editor file display code path, the danger confirmation prompt, the command
execution message, and the editor draft capture logic (the five locations noted)
so all user-supplied paths/commands/filenames are escaped with
escapeMarkdownFull(); keep parse_mode: 'MarkdownV2' unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a57f2ca and 115fc38.

📒 Files selected for processing (2)
  • .gitignore
  • index.js

Comment thread .gitignore
Comment thread index.js
Comment thread index.js
Comment thread index.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (3)
index.js (3)

934-1036: ⚠️ Potential issue | 🟠 Major

Add adminLog() for SSHV command/session/editor operations.

SSHV runs, lock/unlock, exit, and editor save are significant admin actions but are not recorded in admin-events.log, leaving audit gaps.

💡 Suggested additions
 async function executeSshvCommand(ctx, user, session, commandText) {
   const command = String(commandText || '').trim();
+  adminLog('sshv_command_requested', { by: user.id, cwd: session.cwd, command });

@@
   const cdTarget = parseCdTarget(command);
   if (cdTarget !== null) {
@@
       session.cwd = nextPath;
+      adminLog('sshv_cd_success', { by: user.id, cwd: nextPath, command });
@@
     } catch (e) {
+      adminLog('sshv_cd_failed', { by: user.id, cwd: session.cwd, command, error: e.message });
@@
   const child = exec(command, { cwd: session.cwd, timeout: 8000, maxBuffer: 2 * 1024 * 1024 }, async (err, stdout, stderr) => {
@@
+    adminLog('sshv_command_finished', { by: user.id, cwd: session.cwd, command, ok: !err });
     await renderSshvConsole(ctx, session, err ? 'Command finished with errors.' : 'Command finished.');
 bot.action('sshv_lock', async (ctx) => {
@@
   session.locked = true;
+  adminLog('sshv_lock', { by: user.id });
@@
 bot.action('sshv_unlock', async (ctx) => {
@@
   session.locked = false;
+  adminLog('sshv_unlock', { by: user.id });
@@
 bot.action('sshv_exit', async (ctx) => {
@@
   destroySshvSession(user.id);
+  adminLog('sshv_exit', { by: user.id });
@@
 bot.action('sshv_editor_save', async (ctx) => {
@@
   fs.writeFileSync(session.editorMode.filePath, draft, 'utf8');
+  adminLog('sshv_editor_save', { by: user.id, filePath: session.editorMode.filePath });

Based on learnings: Applies to **/index.js : Use adminLog() to log significant operations; cap promoStore.logs at 200 entries; append NDJSON to data/admin-events.log.

Also applies to: 4910-5042


900-911: ⚠️ Potential issue | 🟠 Major

Use escapeMarkdownFull() in all MarkdownV2 SSHV responses.

These blocks use parse_mode: 'MarkdownV2' but escape with escapeMarkdownV2(), which does not cover all MarkdownV2-special characters and can break rendering/injection safety for paths and commands.

💡 Proposed fix
- `Path: \`${escapeMarkdownV2(session.cwd)}\``,
+ `Path: \`${escapeMarkdownFull(session.cwd)}\``,

- note ? escapeMarkdownV2(note) : 'Enter command:',
+ note ? escapeMarkdownFull(note) : 'Enter command:',

- `File: \`${escapeMarkdownV2(filePath)}\``,
+ `File: \`${escapeMarkdownFull(filePath)}\``,

- `⚠️ Potentially dangerous command detected:\n\`${escapeMarkdownV2(command)}\`\n\nConfirm execution?`,
+ `⚠️ Potentially dangerous command detected:\n\`${escapeMarkdownFull(command)}\`\n\nConfirm execution?`,

- await ctx.reply(`⏳ Running: \`${escapeMarkdownV2(command)}\``, { parse_mode: 'MarkdownV2' });
+ await ctx.reply(`⏳ Running: \`${escapeMarkdownFull(command)}\``, { parse_mode: 'MarkdownV2' });

- `📝 Draft captured for \`${escapeMarkdownV2(session.editorMode.filePath)}\`. Save changes?`,
+ `📝 Draft captured for \`${escapeMarkdownFull(session.editorMode.filePath)}\`. Save changes?`,

As per coding guidelines: Use escapeMarkdownFull() for complete MarkdownV2 character escaping (escapes all special chars: _ * [ ] ( ) ~ \ > # + - = | { } . !`).

Also applies to: 963-966, 991-994, 1024-1024, 4356-4356

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

In `@index.js` around lines 900 - 911, The MarkdownV2 responses for the SSHV
console are escaping with escapeMarkdownV2() which misses some special
characters; update renderSshvConsole to use escapeMarkdownFull() for
session.cwd, note and the SSH output (the string returned by
sshvOutputText(session.buffer)) before building the text and calling
ctx.reply(..., { parse_mode: 'MarkdownV2', ... }); similarly replace other SSHV
response calls that currently use escapeMarkdownV2 (any places that embed
session.cwd, sshvOutputText(...) or user-provided note/command into MarkdownV2
replies) so all user/path/output content is passed through escapeMarkdownFull()
to ensure full MarkdownV2 escaping.

4333-4365: ⚠️ Potential issue | 🔴 Critical

/sshv input handling is wired to the wrong handler and currently breaks at runtime.

Line [4337] and Line [4344] dereference action.type before null-check, and Line [4340]/Line [4353] use text that is undefined in this callback context. Also, bot.on('text') does not handle await_sshv_command / await_sshv_editor_content, so SSHV input never reaches the executor/editor flow.

💡 Proposed fix
 bot.action('confirm_yes_username', async (ctx) => {
   const user = getUser(ctx);
   const action = user.pendingAction;
-
-  if (action.type === 'await_sshv_command') {
-    if (!requireAdmin(ctx)) return;
-    const session = getSshvSession(user.id, { createIfMissing: true });
-    await executeSshvCommand(ctx, user, session, text);
-    return;
-  }
-
-  if (action.type === 'await_sshv_editor_content') {
-    if (!requireAdmin(ctx)) return;
-    const session = getSshvSession(user.id, { createIfMissing: true });
-    if (!session.editorMode) {
-      user.pendingAction = { type: 'await_sshv_command' };
-      await ctx.reply('Editor session expired. Back to console commands.');
-      await renderSshvConsole(ctx, session);
-      return;
-    }
-    session.editorMode.draft = text;
-    session.lastActivity = Date.now();
-    await ctx.reply(
-      `📝 Draft captured for \`${escapeMarkdownV2(session.editorMode.filePath)}\`. Save changes?`,
-      {
-        parse_mode: 'MarkdownV2',
-        ...Markup.inlineKeyboard([
-          [Markup.button.callback('💾 Save', 'sshv_editor_save'), Markup.button.callback('❌ Cancel', 'sshv_editor_cancel')],
-        ]),
-      },
-    );
-    return;
-  }
   if (!action || action.type !== 'await_username_confirm') {
     await ctx.answerCbQuery('Nothing to confirm.');
     return;
   }
 bot.on('text', async (ctx) => {
   const user = getUser(ctx);
   const text = (ctx.message.text || '').trim();
+  const action = user.pendingAction;
+
+  if (action && action.type === 'await_sshv_command') {
+    if (!requireAdmin(ctx)) return;
+    const session = getSshvSession(user.id, { createIfMissing: true });
+    await executeSshvCommand(ctx, user, session, text);
+    return;
+  }
+
+  if (action && action.type === 'await_sshv_editor_content') {
+    if (!requireAdmin(ctx)) return;
+    const session = getSshvSession(user.id, { createIfMissing: true });
+    if (!session || !session.editorMode) {
+      user.pendingAction = { type: 'await_sshv_command' };
+      await ctx.reply('Editor session expired. Back to console commands.');
+      if (session) await renderSshvConsole(ctx, session);
+      return;
+    }
+    session.editorMode.draft = text;
+    session.lastActivity = Date.now();
+    await ctx.reply(
+      `📝 Draft captured for \`${escapeMarkdownFull(session.editorMode.filePath)}\`. Save changes?`,
+      {
+        parse_mode: 'MarkdownV2',
+        ...Markup.inlineKeyboard([
+          [Markup.button.callback('💾 Save', 'sshv_editor_save'), Markup.button.callback('❌ Cancel', 'sshv_editor_cancel')],
+        ]),
+      },
+    );
+    return;
+  }
 
   // existing handler branches...

Also applies to: 5749-5807

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

In `@index.js` around lines 4333 - 4365, The confirm_yes_username callback
currently dereferences user.pendingAction and uses an undefined variable text;
move SSHV input handling out of bot.action('confirm_yes_username') and into the
text handler (bot.on('text')) so incoming messages are routed to the SSHV flow
where text is available; in the action handler only perform the confirm action
(no direct command/editor processing) and add null checks: verify getUser(ctx)
returns a user and user.pendingAction exists before accessing action.type; when
implementing the text handler use getUser(ctx), requireAdmin(ctx) check, then
for action.type === 'await_sshv_command' call getSshvSession(user.id,
{createIfMissing:true}) and executeSshvCommand(ctx, user, session, text); for
action.type === 'await_sshv_editor_content' ensure session.editorMode exists,
update session.editorMode.draft = text and session.lastActivity, then reply
using escapeMarkdownV2(session.editorMode.filePath) and the same Markup buttons;
remove or simplify logic in bot.action('confirm_yes_username') so it no longer
references text.
🧹 Nitpick comments (1)
runewager.service (1)

46-46: ReadWritePaths= is a no-op without a filesystem namespace.

ReadWritePaths= only takes effect when systemd has established a private filesystem namespace via ProtectSystem=, ProtectHome=, or similar directives. Without them, the directive is silently ignored and the service can still write anywhere the runewager user has Unix permissions to — it provides no actual confinement today.

Consider pairing it with standard hardening directives:

🛡️ Suggested hardening additions
 ReadWritePaths=/var/www/html/Runewager/logs /var/www/html/Runewager/data
+ProtectSystem=strict
+ProtectHome=true
+PrivateTmp=true
+NoNewPrivileges=true

ProtectSystem=strict makes the entire filesystem read-only except for the paths listed in ReadWritePaths=, which is exactly the intent here.

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

In `@runewager.service` at line 46, ReadWritePaths=/var/www/html/Runewager/logs
/var/www/html/Runewager/data is currently a no-op because there is no private
filesystem namespace; update the runewager.service unit to enable a namespace
(e.g., add ProtectSystem=strict and optionally ProtectHome=yes) so
ReadWritePaths takes effect, keeping the existing ReadWritePaths line and
ensuring the service still runs as the runewager user.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Line 8: Add an inline comment for the WEBAPP_HMAC_KEY entry to explain its
purpose and how to generate a valid value: state that WEBAPP_HMAC_KEY is used to
HMAC-sign webapp payloads/csrf tokens (or similar), recommend an expected
format/length (e.g., 256-bit/32-byte secret encoded as hex or base64), give a
one-liner generation example (openssl rand -hex 32 or openssl rand -base64 32)
and a short security note to keep it secret and not checked into git; update the
.env.example line for WEBAPP_HMAC_KEY accordingly so contributors know how to
create and store the key.

In `@Dockerfile`:
- Around line 10-11: The Dockerfile currently uses a broad context copy via the
COPY --chown=node:node . . instruction which will include local secrets like
.env files; create a .dockerignore at the repository root that at minimum
excludes .env, .env.local, and .env*.local (and any other local secret/config
files) so those files are not included in the image build; commit the
.dockerignore alongside the Dockerfile (which contains COPY --chown=node:node .
. and USER node) to ensure sensitive environment files are never copied into the
image.
- Around line 1-2: The pinned base image digest in the Dockerfile's FROM line
(node:20.19.5-slim@sha256:6178e78b972f79c335df281f4b7674a2d85071aae2af020ffa39f0a770265435)
is invalid and causes pulls to fail; update that FROM reference to a valid
digest by replacing the current digest with the provided manifest-list digest
sha256:9e70124bd00f47dd023e349cd587132ae61892acc0e47ed641416c3e18f401c3 (or, if
you need to target linux/amd64 only, use
sha256:d08621e478133b0492bd661ceee5d13a22b8c55297f3dbbb57f1c15d0c214942) so the
Dockerfile's FROM line points to a published, resolvable digest.

In `@index.js`:
- Around line 926-932: The current confirmation logic in
commandNeedsConfirmation is too narrow (only checks for '|' and ';') and lets
high-risk single commands through; update commandNeedsConfirmation to also
detect dangerous operators and destructive keywords by matching whole-word,
case-insensitive patterns like rm\b, mv\b, dd\b, shred\b, mkfs\b, chown\b,
chmod\b, sudo\b, shutdown\b, reboot\b, kill\b, curl\s+.*\|\s*sh\b,
wget\s+.*\|\s*sh\b, redirection operators (>, >>), and other shell
metacharacters; use word-boundary regexes and case-insensitive flags to avoid
false positives, and keep commandBlocked's regex for ampersand/&& checks but
ensure both functions are consistent (refer to commandNeedsConfirmation and
commandBlocked).
- Around line 7968-7970: Wrap the synchronous TLS file reads (fs.readFileSync)
for tlsKeyPath and tlsCertPath (and their validateSafePath calls) in a try-catch
inside the startup sequence where key and cert are read; on error push a
descriptive message into _startupWarnings (matching the style in
loadRuntimeState) and proceed with HTTPS disabled/HTTP-only fallback instead of
letting the exception crash startup, ensuring any variables or logic that check
for key/cert existence handle the absent values gracefully.

---

Duplicate comments:
In `@index.js`:
- Around line 900-911: The MarkdownV2 responses for the SSHV console are
escaping with escapeMarkdownV2() which misses some special characters; update
renderSshvConsole to use escapeMarkdownFull() for session.cwd, note and the SSH
output (the string returned by sshvOutputText(session.buffer)) before building
the text and calling ctx.reply(..., { parse_mode: 'MarkdownV2', ... });
similarly replace other SSHV response calls that currently use escapeMarkdownV2
(any places that embed session.cwd, sshvOutputText(...) or user-provided
note/command into MarkdownV2 replies) so all user/path/output content is passed
through escapeMarkdownFull() to ensure full MarkdownV2 escaping.
- Around line 4333-4365: The confirm_yes_username callback currently
dereferences user.pendingAction and uses an undefined variable text; move SSHV
input handling out of bot.action('confirm_yes_username') and into the text
handler (bot.on('text')) so incoming messages are routed to the SSHV flow where
text is available; in the action handler only perform the confirm action (no
direct command/editor processing) and add null checks: verify getUser(ctx)
returns a user and user.pendingAction exists before accessing action.type; when
implementing the text handler use getUser(ctx), requireAdmin(ctx) check, then
for action.type === 'await_sshv_command' call getSshvSession(user.id,
{createIfMissing:true}) and executeSshvCommand(ctx, user, session, text); for
action.type === 'await_sshv_editor_content' ensure session.editorMode exists,
update session.editorMode.draft = text and session.lastActivity, then reply
using escapeMarkdownV2(session.editorMode.filePath) and the same Markup buttons;
remove or simplify logic in bot.action('confirm_yes_username') so it no longer
references text.

---

Nitpick comments:
In `@runewager.service`:
- Line 46: ReadWritePaths=/var/www/html/Runewager/logs
/var/www/html/Runewager/data is currently a no-op because there is no private
filesystem namespace; update the runewager.service unit to enable a namespace
(e.g., add ProtectSystem=strict and optionally ProtectHome=yes) so
ReadWritePaths takes effect, keeping the existing ReadWritePaths line and
ensuring the service still runs as the runewager user.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 115fc38 and ff21845.

📒 Files selected for processing (6)
  • .env.example
  • Dockerfile
  • LICENSE
  • SECURITY.md
  • index.js
  • runewager.service
✅ Files skipped from review due to trivial changes (2)
  • LICENSE
  • SECURITY.md

Comment thread .env.example
Comment thread Dockerfile Outdated
Comment on lines +1 to +2
# Pinned Node base image digest
FROM node:20.19.5-slim@sha256:6178e78b972f79c335df281f4b7674a2d85071aae2af020ffa39f0a770265435

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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

node:20.19.5-slim docker hub manifest sha256 digest

💡 Result:

I couldn’t reliably retrieve the manifest (multi-arch) digest for node:20.19.5-slim from Docker Hub via web sources: the public “layers” page for that tag is JS-rendered and the digest isn’t present in the HTML returned to crawlers. (hub.docker.com)

The most reliable way to get the exact manifest digest is to query the registry from your machine:

docker buildx imagetools inspect node:20.19.5-slim

Look for the line:

  • Digest: sha256:... (this is the manifest list digest for the tag)

Alternative (HTTP) approach: fetch the manifest and read the Docker-Content-Digest header / compute the sha256 of the manifest payload (requires registry auth token flow for Docker Hub). (stackoverflow.com)

Citations:


🏁 Script executed:

#!/bin/bash
# Query Docker Registry V2 API for the manifest digest of node:20.19.5-slim
# The Docker-Content-Digest header contains the manifest list digest

curl -sS -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
  -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
  -i "https://registry-1.docker.io/v2/library/node/manifests/20.19.5-slim" 2>&1 | \
  grep -i "docker-content-digest\|^HTTP\|^<"

Repository: gamblecodezcom/Runewager

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Get a temporary token from Docker Hub's auth service
TOKEN=$(curl -sS "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/node:pull" | jq -r '.token')

# Now query the manifest with the token
curl -sS -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
  -H "Authorization: Bearer $TOKEN" \
  -i "https://registry-1.docker.io/v2/library/node/manifests/20.19.5-slim" 2>&1 | \
  grep -E "^HTTP|docker-content-digest|^\{" | head -20

Repository: gamblecodezcom/Runewager

Length of output: 6667


🏁 Script executed:

#!/bin/bash
# Check the current state of the Dockerfile in the repository
head -5 Dockerfile 2>/dev/null || echo "Dockerfile not found at repo root"

Repository: gamblecodezcom/Runewager

Length of output: 232


Update the pinned image digest — the current digest does not exist in the Docker registry.

The digest sha256:6178e78b972f79c335df281f4b7674a2d85071aae2af020ffa39f0a770265435 does not match any published digest for node:20.19.5-slim. Docker pull will fail immediately with a "digest not found" error.

Use the manifest list digest sha256:9e70124bd00f47dd023e349cd587132ae61892acc0e47ed641416c3e18f401c3 (supported across all architectures), or the platform-specific digest for linux/amd64: sha256:d08621e478133b0492bd661ceee5d13a22b8c55297f3dbbb57f1c15d0c214942.

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

In `@Dockerfile` around lines 1 - 2, The pinned base image digest in the
Dockerfile's FROM line
(node:20.19.5-slim@sha256:6178e78b972f79c335df281f4b7674a2d85071aae2af020ffa39f0a770265435)
is invalid and causes pulls to fail; update that FROM reference to a valid
digest by replacing the current digest with the provided manifest-list digest
sha256:9e70124bd00f47dd023e349cd587132ae61892acc0e47ed641416c3e18f401c3 (or, if
you need to target linux/amd64 only, use
sha256:d08621e478133b0492bd661ceee5d13a22b8c55297f3dbbb57f1c15d0c214942) so the
Dockerfile's FROM line points to a published, resolvable digest.

Comment thread Dockerfile Outdated
Comment on lines +10 to +11
COPY --chown=node:node . .
USER node

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for .dockerignore
echo "=== Checking for .dockerignore ==="
fd -H '.dockerignore' 

# Show contents if found
echo -e "\n=== Contents of .dockerignore ==="
fd -H '.dockerignore' --exec cat {} \;

# Check for .env files in the repository
echo -e "\n=== Checking for .env files ==="
fd -H '\.env' --max-depth 2

# Check the Dockerfile to verify the context
echo -e "\n=== Dockerfile snippet ==="
fd -H 'Dockerfile' --exec head -15 {} \;

Repository: gamblecodezcom/Runewager

Length of output: 790


Create .dockerignore to prevent .env files from being copied into the Docker image.

.dockerignore does not exist. Since COPY --chown=node:node . . copies the entire build context, any .env file created locally during development will be baked into the image, exposing secrets like BOT_TOKEN and WEBAPP_HMAC_KEY.

Add a .dockerignore file at the repository root with at least:

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

In `@Dockerfile` around lines 10 - 11, The Dockerfile currently uses a broad
context copy via the COPY --chown=node:node . . instruction which will include
local secrets like .env files; create a .dockerignore at the repository root
that at minimum excludes .env, .env.local, and .env*.local (and any other local
secret/config files) so those files are not included in the image build; commit
the .dockerignore alongside the Dockerfile (which contains COPY
--chown=node:node . . and USER node) to ensure sensitive environment files are
never copied into the image.

Comment thread index.js
Comment thread index.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
runewager.service (1)

46-50: Good hardening — but ProtectSystem=strict will restrict /sshv console commands.

The sandbox is correctly configured for the bot's own I/O (logs/, data/). However, the /sshv admin VPS console introduced in this PR executes arbitrary shell commands via child_process.exec. Those child processes inherit the service sandbox, so:

  • Writes outside ReadWritePaths (e.g. /etc, /home, /var/www/html/Runewager root, any path not under logs/ or data/) will fail with EROFS or EACCES.
  • /tmp is privatised (PrivateTmp=true), so commands that stage files there may produce unexpected results from an admin's perspective.
  • NoNewPrivileges=true blocks any setuid helpers the admin might invoke.

If the SSHV console is expected to perform general-purpose VPS administration, either scope ReadWritePaths more broadly or document that it operates within these constraints. At minimum, surface clear error messaging in the /sshv output when an exec fails due to sandboxing.

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

In `@runewager.service` around lines 46 - 50, ProtectSystem=strict (and
PrivateTmp=true / NoNewPrivileges=true) prevents /sshv child_process.exec
commands from writing outside ReadWritePaths and can cause
EROFS/EACCES/permission failures; either expand ReadWritePaths in the unit to
include the admin paths the console needs or, if you intend to keep the sandbox
tight, update the /sshv execution path (the code that calls child_process.exec
for admin commands) to detect sandbox-related errors (check errno names like
EROFS/EACCES and failures that look like NoNewPrivileges/PrivateTmp side
effects) and surface a clear, actionable error message to the user explaining
the sandbox restriction and suggesting allowed paths or changes to the service
unit.
index.js (1)

4950-4978: Add audit entries for remaining SSHV control actions.

sshv_ctrl_c, sshv_ctrl_z, and sshv_editor_cancel are significant operational actions but currently skip adminLog(), so audit coverage is incomplete.

💡 Suggested fix
 bot.action('sshv_ctrl_c', async (ctx) => {
@@
   if (session.runningProcess && !session.runningProcess.killed) {
     session.runningProcess.kill('SIGINT');
     session.buffer = 'SIGINT sent to running process.';
+    adminLog('sshv_ctrl_c', { adminId: user.id, cwd: session.cwd, command: session.lastCommand || '' });
@@
 bot.action('sshv_ctrl_z', async (ctx) => {
@@
   if (session.runningProcess && !session.runningProcess.killed) {
     try { session.runningProcess.kill('SIGTSTP'); } catch (_) { session.runningProcess.kill('SIGTERM'); }
     session.buffer = 'Stop signal sent to running process.';
+    adminLog('sshv_ctrl_z', { adminId: user.id, cwd: session.cwd, command: session.lastCommand || '' });
@@
 bot.action('sshv_editor_cancel', async (ctx) => {
@@
   session.editorMode = null;
+  adminLog('sshv_editor_cancel', { adminId: user.id });

Based on learnings: Applies to **/index.js : Use adminLog() to log significant operations; cap promoStore.logs at 200 entries; append NDJSON to data/admin-events.log.

Also applies to: 5053-5062

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

In `@index.js` around lines 4950 - 4978, Add audit logging calls for the SSHV
control actions by invoking adminLog(...) in the handlers for
bot.action('sshv_ctrl_c'), bot.action('sshv_ctrl_z') (and similarly for
'sshv_editor_cancel' elsewhere) after verifying requireAdmin(ctx) and
before/after sending the callback answer; include the acting user (from
getUser(ctx)), the session id/user.id (from getSshvSession(user.id)), and a
concise event message like "sshv_ctrl_c" or "sshv_ctrl_z" and outcome ("sent" or
"no running process"); ensure the adminLog usage matches existing audit patterns
(and that promoStore.logs stays capped at 200 entries and admin events are
appended to data/admin-events.log as NDJSON).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Around line 1-26: Restore the runtime data guards in .gitignore by re-adding
patterns that exclude generated JSON and backup files (e.g., add entries like
data/*.json and data/backups/**) so runtime artifacts such as users.json,
giveaways.json, promo.json, analytics JSON, and backups are never staged; update
the .gitignore file (look for the existing data/admin-events.log entry) to
include those patterns and any similar runtime artifacts to prevent accidental
commits of PII.

In `@index.js`:
- Around line 3366-3367: The health probe is always using the HTTPS client
(require('https').request) even when the server is running in HTTP-only mode;
update the probe to pick the HTTP module dynamically and to only set
TLS-specific options when TLS is enabled: detect the server TLS mode (the same
flag/config used to choose the server fallback), choose const client = isTls ?
require('https') : require('http'), call client.request(opts, ...), and remove
or conditionally add rejectUnauthorized (and any other TLS-only options) from
opts when isTls is false so /health works in HTTP-only mode; update both
occurrences that create opts/req (including the one around req =
require('https').request and the other similar block at the other location).
- Around line 847-849: The requireAdmin() call currently flips admin view by
calling persistAdminMode(user, true) when user.adminModeOn is false; remove that
side-effect so requireAdmin() only performs authorization checks (e.g., verify
user.isAdmin and throw/return an auth error) and do not mutate user.adminModeOn
or call persistAdminMode from within requireAdmin(); keep any visibility toggles
exclusively in the explicit handlers for "/on", "/off" or the admin menu actions
that already call persistAdminMode.
- Around line 8816-8824: The module-level setInterval creating the SSHV GC keeps
the Node event loop alive; change its creation so it doesn't block process exit
by either moving the interval into the runtime startup/init code path (so it
only runs when the service starts) or call .unref() on the interval timer
returned from setInterval. Locate the setInterval that iterates sshvSessions and
uses SSHV_SESSION_TTL_MS and destroySshvSession, capture its return value (const
timer = setInterval(...)) and invoke timer.unref() (or relocate the entire block
into the runtime guard/startup function) so load-only or smoke-check processes
can exit normally.
- Around line 5034-5051: The sshv_editor_save handler currently lets
fs.writeFileSync throw, so wrap the write in a try/catch around the
fs.writeFileSync call in the bot.action('sshv_editor_save') handler (keeping
requireAdmin, getUser, getSshvSession, adminLog and renderSshvConsole
references), and on error: adminLog the error with context (adminId and
filePath), set session.buffer to a clear failure message, keep
session.editorMode intact (so the user can retry), set user.pendingAction to
await_sshv_command or appropriate retry state, call ctx.answerCbQuery with a
failure message and then await renderSshvConsole(ctx, session) to show the error
to the user; on success continue to clear editorMode, set buffer to success, log
and render as before.

---

Nitpick comments:
In `@index.js`:
- Around line 4950-4978: Add audit logging calls for the SSHV control actions by
invoking adminLog(...) in the handlers for bot.action('sshv_ctrl_c'),
bot.action('sshv_ctrl_z') (and similarly for 'sshv_editor_cancel' elsewhere)
after verifying requireAdmin(ctx) and before/after sending the callback answer;
include the acting user (from getUser(ctx)), the session id/user.id (from
getSshvSession(user.id)), and a concise event message like "sshv_ctrl_c" or
"sshv_ctrl_z" and outcome ("sent" or "no running process"); ensure the adminLog
usage matches existing audit patterns (and that promoStore.logs stays capped at
200 entries and admin events are appended to data/admin-events.log as NDJSON).

In `@runewager.service`:
- Around line 46-50: ProtectSystem=strict (and PrivateTmp=true /
NoNewPrivileges=true) prevents /sshv child_process.exec commands from writing
outside ReadWritePaths and can cause EROFS/EACCES/permission failures; either
expand ReadWritePaths in the unit to include the admin paths the console needs
or, if you intend to keep the sandbox tight, update the /sshv execution path
(the code that calls child_process.exec for admin commands) to detect
sandbox-related errors (check errno names like EROFS/EACCES and failures that
look like NoNewPrivileges/PrivateTmp side effects) and surface a clear,
actionable error message to the user explaining the sandbox restriction and
suggesting allowed paths or changes to the service unit.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff21845 and 4cb3ca2.

📒 Files selected for processing (6)
  • .env.example
  • .gitignore
  • Dockerfile
  • README.md
  • index.js
  • runewager.service
💤 Files with no reviewable changes (1)
  • Dockerfile
🚧 Files skipped from review as they are similar to previous changes (1)
  • .env.example

Comment thread .gitignore
Comment on lines 1 to +26
# Environment files
.env
*.env
**/*.env
.env.*
env
env.*

# Runtime JSON state (do not commit)
/data/*.json
/users.json
/giveaways.json
/promo.json
/env.json

# Legacy runtime state
bot_state.json
*.env

# Generated runtime backups
data/backups/*
!data/backups/.gitkeep
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Service log artifacts
runewager-*.log
crash.log
# Node modules
node_modules/

# Runtime admin event log (append-only, not for version control)
data/admin-events.log
# Temp + cache
.tmp/
.cache/
dist/
build/

# Rollback marker (runtime artifact)
# System files
.DS_Store
Thumbs.db
.last_rollback
data/admin-events.log

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

Re-add runtime data/ JSON guards — removal risks accidental PII commit.

The PR removes all data/*.json / data/backups/* gitignore patterns ("remove checked-in runtime JSON artifacts"), but the bot continues to generate these files at runtime (users.json, giveaways.json, promo.json, analytics JSON, etc. — as documented in the README). Without explicit ignores, a routine git add . or git add data/ during future maintenance will silently stage files containing Telegram user IDs, usernames, and region data into version history — a clear GDPR/CCPA compliance exposure.

🛡️ Proposed fix — restore data/ runtime guards
 # System files
 .DS_Store
 Thumbs.db
 .last_rollback
 data/admin-events.log
+
+# Runtime state — never commit generated user/promo data
+data/*.json
+data/backups/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Environment files
.env
*.env
**/*.env
.env.*
env
env.*
# Runtime JSON state (do not commit)
/data/*.json
/users.json
/giveaways.json
/promo.json
/env.json
# Legacy runtime state
bot_state.json
*.env
# Generated runtime backups
data/backups/*
!data/backups/.gitkeep
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Service log artifacts
runewager-*.log
crash.log
# Node modules
node_modules/
# Runtime admin event log (append-only, not for version control)
data/admin-events.log
# Temp + cache
.tmp/
.cache/
dist/
build/
# Rollback marker (runtime artifact)
# System files
.DS_Store
Thumbs.db
.last_rollback
data/admin-events.log
# Environment files
.env
.env.*
*.env
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Node modules
node_modules/
# Temp + cache
.tmp/
.cache/
dist/
build/
# System files
.DS_Store
Thumbs.db
.last_rollback
data/admin-events.log
# Runtime state — never commit generated user/promo data
data/*.json
data/backups/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 1 - 26, Restore the runtime data guards in
.gitignore by re-adding patterns that exclude generated JSON and backup files
(e.g., add entries like data/*.json and data/backups/**) so runtime artifacts
such as users.json, giveaways.json, promo.json, analytics JSON, and backups are
never staged; update the .gitignore file (look for the existing
data/admin-events.log entry) to include those patterns and any similar runtime
artifacts to prevent accidental commits of PII.

Comment thread index.js Outdated
Comment thread index.js
Comment on lines +3366 to +3367
const opts = { hostname: '127.0.0.1', port, path: '/health', method: 'GET', timeout: 5000, rejectUnauthorized: false };
const req = require('https').request(opts, (res) => {

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

Health probes are hardwired to HTTPS and break when server falls back to HTTP.

The health server explicitly supports HTTP-only fallback, but both probe callsites always use an HTTPS client. In HTTP-only mode this fails even when /health is healthy.

💡 Suggested fix
- const req = require('https').request(opts, (res) => {
+ const requestHealth = (client) => new Promise((resolve, reject) => {
+   const req = client.request(opts, (res) => {
+     let body = '';
+     res.on('data', (c) => { body += c; });
+     res.on('end', () => resolve({ status: res.statusCode, data: body }));
+   });
+   req.on('error', reject);
+   req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
+   req.end();
+ });
+
+ let data;
+ try {
+   ({ data } = await requestHealth(require('https')));
+ } catch (_) {
+   ({ data } = await requestHealth(require('http')));
+ }

Also applies to: 4082-4085

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

In `@index.js` around lines 3366 - 3367, The health probe is always using the
HTTPS client (require('https').request) even when the server is running in
HTTP-only mode; update the probe to pick the HTTP module dynamically and to only
set TLS-specific options when TLS is enabled: detect the server TLS mode (the
same flag/config used to choose the server fallback), choose const client =
isTls ? require('https') : require('http'), call client.request(opts, ...), and
remove or conditionally add rejectUnauthorized (and any other TLS-only options)
from opts when isTls is false so /health works in HTTP-only mode; update both
occurrences that create opts/req (including the one around req =
require('https').request and the other similar block at the other location).

Comment thread index.js
Comment on lines +5034 to +5051
bot.action('sshv_editor_save', async (ctx) => {
if (!requireAdmin(ctx)) return;
const user = getUser(ctx);
const session = getSshvSession(user.id);
if (!session || !session.editorMode) { await ctx.answerCbQuery('No editor session.'); return; }
const draft = session.editorMode.draft;
if (typeof draft !== 'string') {
await ctx.answerCbQuery('Send edited file content first.');
return;
}
fs.writeFileSync(session.editorMode.filePath, draft, 'utf8');
adminLog('sshv_editor_save', { adminId: user.id, filePath: session.editorMode.filePath, size: draft.length });
session.buffer = `Saved ${session.editorMode.filePath}`;
session.editorMode = null;
user.pendingAction = { type: 'await_sshv_command' };
await ctx.answerCbQuery('Saved');
await renderSshvConsole(ctx, session);
});

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

Handle editor save failures explicitly.

A failed fs.writeFileSync currently bubbles into global error handling and does not provide a user-facing save error path, which can leave the session flow inconsistent.

💡 Suggested fix
-  fs.writeFileSync(session.editorMode.filePath, draft, 'utf8');
+  try {
+    fs.writeFileSync(session.editorMode.filePath, draft, 'utf8');
+  } catch (e) {
+    session.buffer = `Save failed: ${e.message}`;
+    await ctx.answerCbQuery('Save failed');
+    await renderSshvConsole(ctx, session, 'Editor save failed. Fix and retry, or cancel.');
+    return;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.js` around lines 5034 - 5051, The sshv_editor_save handler currently
lets fs.writeFileSync throw, so wrap the write in a try/catch around the
fs.writeFileSync call in the bot.action('sshv_editor_save') handler (keeping
requireAdmin, getUser, getSshvSession, adminLog and renderSshvConsole
references), and on error: adminLog the error with context (adminId and
filePath), set session.buffer to a clear failure message, keep
session.editorMode intact (so the user can retry), set user.pendingAction to
await_sshv_command or appropriate retry state, call ctx.answerCbQuery with a
failure message and then await renderSshvConsole(ctx, session) to show the error
to the user; on success continue to clear editorMode, set buffer to success, log
and render as before.

Comment thread index.js
Comment on lines +8816 to +8824
// Cleanup expired /sshv sessions every minute to keep memory usage bounded.
setInterval(() => {
const now = Date.now();
for (const [adminId, session] of sshvSessions.entries()) {
if (now - (session.lastActivity || session.createdAt || 0) > SSHV_SESSION_TTL_MS) {
destroySshvSession(adminId);
}
}
}, 60 * 1000);

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 | 🟡 Minor

Module-scope SSHV GC interval should not keep load-only processes alive.

This timer is outside the runtime guard and can keep the event loop active during smoke/load checks. Make it non-blocking (or move under runtime startup).

💡 Suggested fix
-setInterval(() => {
+const sshvGcTimer = setInterval(() => {
   const now = Date.now();
   for (const [adminId, session] of sshvSessions.entries()) {
     if (now - (session.lastActivity || session.createdAt || 0) > SSHV_SESSION_TTL_MS) {
       destroySshvSession(adminId);
     }
   }
 }, 60 * 1000);
+sshvGcTimer.unref();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.js` around lines 8816 - 8824, The module-level setInterval creating the
SSHV GC keeps the Node event loop alive; change its creation so it doesn't block
process exit by either moving the interval into the runtime startup/init code
path (so it only runs when the service starts) or call .unref() on the interval
timer returned from setInterval. Locate the setInterval that iterates
sshvSessions and uses SSHV_SESSION_TTL_MS and destroySshvSession, capture its
return value (const timer = setInterval(...)) and invoke timer.unref() (or
relocate the entire block into the runtime guard/startup function) so load-only
or smoke-check processes can exit normally.

@gamblecodezcom
gamblecodezcom merged commit fb25ca9 into main Feb 25, 2026
3 of 5 checks passed
@gamblecodezcom
gamblecodezcom deleted the codex/configure-git-and-update-project-files branch February 25, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant