Add admin view toggles, persistent admin dashboard, and lightweight /sshv VPS console; update .gitignore - #72
Conversation
|
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 · |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
| const user = getUser(ctx); | ||
| const action = user.pendingAction; | ||
|
|
||
| if (action.type === 'await_sshv_command') { |
There was a problem hiding this comment.
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 finished reviewing your PR. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
index.js (3)
934-1036:⚠️ Potential issue | 🟠 MajorAdd
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; cappromoStore.logsat 200 entries; append NDJSON todata/admin-events.log.Also applies to: 4910-5042
900-911:⚠️ Potential issue | 🟠 MajorUse
escapeMarkdownFull()in all MarkdownV2 SSHV responses.These blocks use
parse_mode: 'MarkdownV2'but escape withescapeMarkdownV2(), 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
/sshvinput handling is wired to the wrong handler and currently breaks at runtime.Line [4337] and Line [4344] dereference
action.typebefore null-check, and Line [4340]/Line [4353] usetextthat is undefined in this callback context. Also,bot.on('text')does not handleawait_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 viaProtectSystem=,ProtectHome=, or similar directives. Without them, the directive is silently ignored and the service can still write anywhere therunewageruser 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=strictmakes the entire filesystem read-only except for the paths listed inReadWritePaths=, 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
📒 Files selected for processing (6)
.env.exampleDockerfileLICENSESECURITY.mdindex.jsrunewager.service
✅ Files skipped from review due to trivial changes (2)
- LICENSE
- SECURITY.md
| # Pinned Node base image digest | ||
| FROM node:20.19.5-slim@sha256:6178e78b972f79c335df281f4b7674a2d85071aae2af020ffa39f0a770265435 |
There was a problem hiding this comment.
🧩 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-slimLook 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:
- 1: https://hub.docker.com/layers/library/node/20.19.5-slim/images/sha256-82f479b3adf1c8d226a34128d0958c8e5aa6e5e0922967c854640721791b1407
- 2: https://stackoverflow.com/questions/35186693/what-is-the-docker-registry-v2-api-endpoint-to-get-the-digest-for-an-image/78711220?utm_source=openai
🏁 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 -20Repository: 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.
| COPY --chown=node:node . . | ||
| USER node |
There was a problem hiding this comment.
🧩 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.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
runewager.service (1)
46-50: Good hardening — butProtectSystem=strictwill restrict/sshvconsole commands.The sandbox is correctly configured for the bot's own I/O (
logs/,data/). However, the/sshvadmin VPS console introduced in this PR executes arbitrary shell commands viachild_process.exec. Those child processes inherit the service sandbox, so:
- Writes outside
ReadWritePaths(e.g./etc,/home,/var/www/html/Runewagerroot, any path not underlogs/ordata/) will fail withEROFSorEACCES./tmpis privatised (PrivateTmp=true), so commands that stage files there may produce unexpected results from an admin's perspective.NoNewPrivileges=trueblocks anysetuidhelpers the admin might invoke.If the SSHV console is expected to perform general-purpose VPS administration, either scope
ReadWritePathsmore broadly or document that it operates within these constraints. At minimum, surface clear error messaging in the/sshvoutput 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, andsshv_editor_cancelare significant operational actions but currently skipadminLog(), 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; cappromoStore.logsat 200 entries; append NDJSON todata/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
📒 Files selected for processing (6)
.env.example.gitignoreDockerfileREADME.mdindex.jsrunewager.service
💤 Files with no reviewable changes (1)
- Dockerfile
🚧 Files skipped from review as they are similar to previous changes (1)
- .env.example
| # 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 |
There was a problem hiding this comment.
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.
| # 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.
| const opts = { hostname: '127.0.0.1', port, path: '/health', method: 'GET', timeout: 5000, rejectUnauthorized: false }; | ||
| const req = require('https').request(opts, (res) => { |
There was a problem hiding this comment.
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).
| 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); | ||
| }); |
There was a problem hiding this comment.
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.
| // 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); |
There was a problem hiding this comment.
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.
User description
Motivation
Description
.gitignoreto ignore node_modules, logs, temp/build folders, system files, and to consolidate.envpatterns and remove checked-in runtime JSON artifacts.persistAdminMode,adminModeOn) and updatedrequireAdminand multiple menus/help pages so admin UI is shown only when admin view is enabled, with persistent menus deleting their counterpart when opened./sshvVPS console feature with session store and TTL (SSHV_SESSION_TTL_MS), session lifecycle helpers (getSshvSession,destroySshvSession), command execution viaexec, output rendering (renderSshvConsole), editor mode fornano-like edits, locking/unlocking, Ctrl+C and Ctrl+Z handlers, confirmation for potentially dangerous commands, and an interval-based cleanup of expired sessions./sshvin 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).REGISTERED_COMMANDSset and an unknown-command response when a slash command is typed but not registered, to provide friendly guidance.bot.action(/.*/, ...)that acknowledges stale callback buttons and directs users back to/menuto recover.exectochild_processimports) and multiple renames/keyboard key changes for admin dashboard actions to keep labels consistent.Testing
Codex Task
CodeAnt-AI Description
Add persistent admin view toggles, a lightweight /sshv VPS console, and better fallback handling
What Changed
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:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
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:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
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
Chores
Documentation