auth/env hardening and reliability updates - #42
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds Playwright E2E smoke tests and orchestration, pins Bun to 1.3.3 in CI/Docker, hardens CI security auditing, tightens env/relay validation and localhost gating, normalizes nostr REQ handling and subscription keys, isolates route script env, and applies multiple frontend accessibility and type updates. Changes
Sequence Diagram(s)sequenceDiagram
rect rgba(200,200,255,0.5)
actor Setup as Playwright GlobalSetup
end
participant Port as PortResolver
participant Server as igloo-server
participant API as Server API
participant CoSigner as Co-signer Process
participant Relay as Relay
participant State as StateFile
Setup->>Port: resolve available port
Port-->>Setup: port
Setup->>Server: spawn igloo-server (tmp DB + env overrides)
Server-->>Setup: started
Setup->>API: onboarding /admin setup (create session)
API-->>Setup: sessionId
Setup->>API: upload group/share credentials
API-->>Setup: credentials stored
Setup->>API: wait for nodeActive
API-->>Setup: nodeActive confirmed
Setup->>CoSigner: spawn cosigner (shareCred, relayUrl)
CoSigner->>Relay: connect & subscribe
Setup->>API: probe signing readiness
API-->>Setup: signing ready
Setup->>API: create test API key (optional)
API-->>Setup: apiKey
Setup->>State: persist state.json (PIDs, ports, creds, apiKey)
State-->>Setup: state written
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 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 |
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/routes/utils.ts (1)
63-77:⚠️ Potential issue | 🟠 Major
normalizeRelayListForEchoignoresALLOW_LOCALHOST_RELAY, breaking localhost-relay echo in test environments
getValidRelaysnow conditionally allows localhost relays whenALLOW_LOCALHOST_RELAY=true, butnormalizeRelayListForEcho(lines 803–810) unconditionally stripslocalhost,127.0.0.1, and::1. When E2E tests setALLOW_LOCALHOST_RELAY=trueto use a local relay, the credential-update flow will pass relay validation, write the relay to env, but then produce an empty relay list in the echo payload — effectively firing a no-relay echo and silently skipping the connectivity signal.Additionally,
getValidRelayschecks onlylocalhostand127.0.0.1(not::1), whilenormalizeRelayListForEchochecks all three — a separate inconsistency.🐛 Suggested fix
export function normalizeRelayListForEcho(relays: any): string[] | undefined { const validation = validateRelayUrls(relays); if (!validation.valid || !validation.urls || validation.urls.length === 0) return undefined; + const allowLocalhost = process.env['ALLOW_LOCALHOST_RELAY'] === 'true'; const filtered = validation.urls .map((r) => r.trim()) .filter((r) => r.length > 0) .filter((r) => { try { const u = new URL(r); - return (u.protocol === 'ws:' || u.protocol === 'wss:') && - u.hostname !== 'localhost' && u.hostname !== '127.0.0.1' && u.hostname !== '::1'; + if (!allowLocalhost && + (u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '::1')) { + return false; + } + return u.protocol === 'ws:' || u.protocol === 'wss:'; } catch { return false; } });Also align
getValidRelaysto check::1:- if (!allowLocalhost && (url.hostname === 'localhost' || url.hostname === '127.0.0.1')) { + if (!allowLocalhost && (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1')) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.ts` around lines 63 - 77, normalizeRelayListForEcho currently strips localhost relays regardless of ALLOW_LOCALHOST_RELAY and getValidRelays checks only 'localhost' and '127.0.0.1' (missing '::1'), causing inconsistent behavior; update normalizeRelayListForEcho to read process.env['ALLOW_LOCALHOST_RELAY'] === 'true' and skip removing relays whose hostname is 'localhost', '127.0.0.1', or '::1' when that flag is true, and modify getValidRelays to treat '::1' the same as '127.0.0.1' when deciding to exclude localhost relays so both functions are consistent.src/routes/env.ts (2)
347-379:⚠️ Potential issue | 🟠 MajorHeadless
POST /api/envdoes not validateGROUP_CRED/SHARE_CRED— can persist invalid credentials and crash the nodeThe DB-mode path (lines 286–298) now validates credential format, but the headless path for the same endpoint writes
GROUP_CRED/SHARE_CREDdirectly (lines 367–379) without callingvalidateGroup/validateShare. When the write succeeds,createAndConnectServerNodeis called; it fails with invalid credentials, the route returns500, and.envis left containing invalid credentials while the Bifrost node is down. Recovery requires a follow-up API call.The headless
/api/env/sharesPOST already validates both (lines 553–561); apply the same pattern here.🐛 Proposed fix for the headless path
if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { const relayValidation = validateRelayUrls(body.RELAYS); if (!relayValidation.valid) { return Response.json({ success: false, error: relayValidation.error }, { status: 400, headers }); } } + + if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED) { + const groupValidation = validateGroup(body.GROUP_CRED); + if (!groupValidation.isValid) { + return Response.json({ success: false, error: 'Invalid GROUP_CRED' }, { status: 400, headers }); + } + } + + if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED) { + const shareValidation = validateShare(body.SHARE_CRED); + if (!shareValidation.isValid) { + return Response.json({ success: false, error: 'Invalid SHARE_CRED' }, { status: 400, headers }); + } + } + for (const key of validKeys) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 347 - 379, The headless POST /api/env path writes GROUP_CRED and SHARE_CRED directly and can persist invalid credentials; before assigning env[key] and writing, call the same validators used elsewhere (validateGroup for GROUP_CRED and validateShare for SHARE_CRED) and reject the request with a 400 and error message if validation fails (mirror the DB-mode and /api/env/shares behavior). Specifically, inside the headless handling where validKeys are processed (the loop handling body[key] and the updatingCredentials/updatingRelays logic), run validateGroup(body.GROUP_CRED) and validateShare(body.SHARE_CRED) when those keys are present, return Response.json({...}, {status:400, headers}) on invalid results, and only then set env values and update CREDENTIALS_SAVED_AT and call createAndConnectServerNode.
286-311:⚠️ Potential issue | 🟡 MinorCredential format validation runs before the admin privilege check — minor information leak
The new
validateGroup/validateShareblocks return400before the admin check at line 306 returns403. An authenticated but non-admin user can probe whether a credential string is well-formed (400) vs. simply not privileged (403). The pre-existingRELAYSvalidation at line 279 has the same ordering. Move all validation after the privilege check, or at minimum swap the existing order so the403fires first.♻️ Suggested reorder
+ // Privilege gate first — reject non-admins before doing any validation work + const adminSecret = req.headers.get('X-Admin-Secret') ?? req.headers.get('Authorization')?.replace(/^Bearer\s+/i, ''); + const isAdminSecret = await validateAdminSecret(adminSecret); + if (!isAdminSecret && !isRoleAdmin) { + return Response.json( + { error: 'Admin privileges required for environment modifications' }, + { status: 403, headers } + ); + } + if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { ... } if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED) { ... } if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED) { ... } - const adminSecret = ...; - const isAdminSecret = await validateAdminSecret(adminSecret); - if (!isAdminSecret && !isRoleAdmin) { ... }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 286 - 311, The credential format validations (validateGroup and validateShare) run before the admin privilege gate, which leaks info; move the validation blocks for GROUP_CRED, SHARE_CRED (and the existing RELAYS validation) to occur after the admin check that uses validateAdminSecret and isRoleAdmin so the 403 is returned before any 400 credential-format errors, or at minimum swap the order so the admin-secret check (adminSecret, validateAdminSecret, isRoleAdmin) executes before calling validateGroup/validateShare; update the code paths referencing validateGroup, validateShare, RELAYS validation, validateAdminSecret, and isRoleAdmin accordingly.tests/routes/env.db-mode.spec.ts (1)
105-108:⚠️ Potential issue | 🟡 MinorAccepting HTTP 500 weakens the test assertion.
expect([200, 500]).toContain(out.status)means this test passes even when the route throws an internal error. The comment explains it's due to restart failure, but the test named "stamps CREDENTIALS_SAVED_AT" should ideally verify the primary success path. Consider checkinghasStampas the primary assertion (which it does) and logging a warning rather than accepting 500 silently—or better, mock/suppress the restart in the test env so 200 is the expected result.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/env.db-mode.spec.ts` around lines 105 - 108, The test currently allows an HTTP 500 which weakens the assertion; update the "stamps CREDENTIALS_SAVED_AT" test to assert the primary success path by requiring out.status === 200 (instead of expect([200,500]).toContain(out.status)) and either mock/suppress the restart behavior in the test environment or stub the restart function so the route returns 200 reliably; keep the existing expect(out.hasStamp).toBeTrue() as the primary check and, if desired, add a test-specific warning/log only when restart mocking is impossible.
🧹 Nitpick comments (12)
scripts/release.sh (1)
76-84: Duplicate health-check failure blocks can be consolidated.Lines 76–78 print a failure message, then Lines 81–84 re-check the same condition to exit. These can be merged into a single block.
Suggested consolidation
if [ "$SERVER_HEALTHY" = false ]; then echo "❌ Server failed to respond after 5 attempts" -fi - -# Cleanup will be handled by trap, just check if we should fail -if [ "$SERVER_HEALTHY" = false ]; then echo "❌ Server startup test failed - cannot proceed with release" exit 1 fi🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/release.sh` around lines 76 - 84, Consolidate the duplicated SERVER_HEALTHY checks in release.sh into a single block: remove the first standalone if that echoes "❌ Server failed to respond after 5 attempts" and merge its message into the later block that exits, so one if [ "$SERVER_HEALTHY" = false ]; then prints the failure message and then exits with status 1; update any comments to reflect that cleanup is still handled by trap and ensure the variable SERVER_HEALTHY is the single condition used.tests/e2e/state.ts (1)
43-50: Consider validating the parsed state shape.The
as SmokeTestStatecast on Line 47 is an unchecked assertion. If the JSON file is corrupt or out-of-sync with the interface (e.g., after a field rename), tests will silently receiveundefinedfor missing properties and produce confusing failures. A lightweight runtime check (e.g., assert a couple of required fields likesessionIdandbaseUrlare present strings) would surface mismatches early.Low risk since global-setup writes this file in the same PR, but worth considering as the state shape evolves.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/state.ts` around lines 43 - 50, The loadState function currently casts JSON.parse(...) to SmokeTestState without runtime checks; after parsing the file (in loadState where JSON.parse(fs.readFileSync(...)) is called) validate that the result is an object and contains required fields (e.g., sessionId and baseUrl) and that they are non-empty strings; if validation fails, log or throw a clear error (or return STUB) instead of silently returning an invalid object so callers of loadState/SmokeTestState get immediate, actionable feedback about a malformed state file.tests/e2e/specs/07-env.e2e.ts (1)
63-75: Consider adding a test for an emptyRELAYSarray.The current invalid-relay test uses a malformed URL string. An empty array (
RELAYS: []) is another common edge case for relay validation that could be worth covering.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/07-env.e2e.ts` around lines 63 - 75, Add a new e2e test that covers the empty-relays edge case by duplicating the existing "POST /api/env – invalid relay URL returns 400" test pattern: use request.newContext({ baseURL: baseUrl }), include headers with X-Session-ID: sessionId and data with GROUP_CRED: state.groupCredential, SHARE_CRED: state.shareCredentials[0], but set RELAYS: [] and assert res.status() is 400, then dispose the context; name it something like "POST /api/env – empty RELAYS returns 400" to make the intent clear and keep the same setup/teardown used in the existing test.tests/e2e/global-teardown.ts (1)
62-74: Consider verifying the temp directory path before recursive deletion.The
tmpDiron line 66 is derived from the state file contents or its parent directory. In a CI environment this is fine, but as a defensive measure you could validate thattmpDiris actually underos.tmpdir()before callingrmSyncwithrecursive: true. This prevents accidental damage if the state file were ever corrupted or pointed elsewhere.Proposed guard
const tmpDir = state.tmpDir || path.dirname(resolvedStateFile); - if (tmpDir) { + if (tmpDir && tmpDir.startsWith(os.tmpdir())) { try { fs.rmSync(tmpDir, { recursive: true, force: true });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-teardown.ts` around lines 62 - 74, Validate the temporary directory is actually inside the system temp directory before calling fs.rmSync(recursive: true): compute a resolved path for tmpDir (use path.resolve(tmpDir)) and compare it to os.tmpdir() (use path.relative(os.tmpdir(), resolvedTmp) and ensure the result does not start with '..' and is not equal to '' or '.', otherwise skip deletion and log a warning; keep the existing try/catch and console messages but only call fs.rmSync when the safety check passes to avoid accidental deletion outside the temp directory.tests/e2e/specs/08-ui.e2e.ts (1)
48-62: Consider extracting the login flow into a shared helper.The login sequence (fill username, fill password, click submit, wait for network idle) is duplicated between the "login form accepts credentials" test (lines 27–45) and this
beforeEach(lines 50–62). Extracting it into a helper function (e.g., instate.tsor ahelpers.ts) would reduce duplication across this file and make future locator changes easier.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/08-ui.e2e.ts` around lines 48 - 62, Extract the duplicated login sequence (creating usernameField/passwordField locators, filling adminUsername/adminPassword, clicking submitBtn, and awaiting page.waitForLoadState('networkidle')) into a shared helper function (e.g., export async function loginAs(page, username, password) in helpers.ts or state.ts); replace the duplicated code in test.beforeEach and the "login form accepts credentials" test with a single call to loginAs(page, adminUsername, adminPassword); keep the same locator selectors (input[type="text"], input[id*="user"], input[name*="user"], input[type="password"], button[type="submit"], button:has-text("Login"), button:has-text("Sign in")) and ensure the helper clicks the submit button and awaits networkidle before returning.tests/e2e/cosigner.mjs (1)
47-47: Accessing private_filterproperty.
node.client?._filteraccesses an internal/private property of the igloo-core client. This is fine for debug logging in a test utility, but be aware it may silently break (returnundefined) on library upgrades. The fallback chain (?? node.client?.filter ?? '?') mitigates this.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` at line 47, The test is accessing a private property node.client?._filter; change to use the public API node.client?.filter and only fall back to _filter if absolutely necessary: replace the expression with node.client?.filter ?? node.client?._filter ?? '?' (so public API is preferred), and add a short comment noting that _filter is a private internals fallback to avoid silent breakage on library upgrades.tests/e2e/specs/03-nip44-nip04.e2e.ts (1)
86-140: NIP-04 suite is missing a "missing content returns 400" test.The NIP-44 suite includes a
missing content returns 400test (lines 73–81), but the NIP-04 suite does not have an equivalent. If the server applies the same validation for NIP-04 encrypt, consider adding a matching test for consistency.Proposed test to add after line 139
+ + test('missing content returns 400', async () => { + const api = await request.newContext({ baseURL: baseUrl }); + const res = await api.post('/api/nip04/encrypt', { + headers: { 'X-Session-ID': sessionId }, + data: { peer_pubkey: groupPubkeyHex }, + }); + expect(res.status()).toBe(400); + await api.dispose(); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/03-nip44-nip04.e2e.ts` around lines 86 - 140, Add a new test to the "NIP-04 – /api/nip04" suite that posts to '/api/nip04/encrypt' with a valid 'peer_pubkey' but omits 'content' (or sets it to null/undefined) and asserts the response status is 400; mirror the existing NIP-44 "missing content returns 400" test logic so validation consistency is covered, using the same request.newContext(), X-Session-ID header (sessionId), and expect(res.status()).toBe(400) pattern as used in the other tests in this suite.tests/e2e/global-setup.ts (4)
80-109: TOCTOU gap in port resolution — acceptable for tests, document the limitation.
canBindPortbinds → closes → returnstrue, then the server tries to bind the same port later. Another process could claim it in between. Same withreserveRandomPort. This is inherent to the "probe then use" approach and unlikely to cause issues in CI, but worth a brief inline comment so future maintainers don't mistake this for a guarantee.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 80 - 109, Add a concise inline comment above canBindPort, reserveRandomPort, and resolvePort explaining the TOCTOU race: these functions probe by binding/closing (canBindPort) and reserving ephemeral ports (reserveRandomPort) which does not guarantee the port remains free when used later, so this is an accepted, low-risk approach for tests/CI; reference the limitation in the resolvePort warning path so future maintainers understand the potential race and why no extra locking is implemented.
34-40: Hard-coded test secrets violate the coding guideline.
TEST_NSEC_HEX,ADMIN_SECRET,ADMIN_USERNAME, andADMIN_PASSWORDare hard-coded inline. The guideline for**/*.tsfiles states: "Never hard-code secrets; load from environment or data/ fixtures."Consider loading these from environment variables with fallback defaults, or from a
data/fixture file, to stay consistent with the project convention:Proposed fix
-const TEST_NSEC_HEX = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; - -// Meets igloo-server password rules: upper + lower + digit + special(@), no sequences -const ADMIN_SECRET = 'SmokeTestAdmin1'; -const ADMIN_USERNAME = 'testadmin'; -const ADMIN_PASSWORD = 'T3stPass@9'; +const TEST_NSEC_HEX = process.env.SMOKE_TEST_NSEC_HEX + ?? 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + +const ADMIN_SECRET = process.env.SMOKE_ADMIN_SECRET ?? 'SmokeTestAdmin1'; +const ADMIN_USERNAME = process.env.SMOKE_ADMIN_USERNAME ?? 'testadmin'; +const ADMIN_PASSWORD = process.env.SMOKE_ADMIN_PASSWORD ?? 'T3stPass@9';As per coding guidelines:
**/*.{ts,tsx}: "Never hard-code secrets; load from environment or data/ fixtures."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 34 - 40, Replace the hard-coded secrets (TEST_NSEC_HEX, ADMIN_SECRET, ADMIN_USERNAME, ADMIN_PASSWORD) with values loaded from environment variables or a data/fixture loader: read process.env.TEST_NSEC_HEX, process.env.ADMIN_SECRET, process.env.ADMIN_USERNAME, process.env.ADMIN_PASSWORD (with non-sensitive safe defaults only for local CI if absolutely necessary) or load them from a test fixture file in data/, and update any test setup logic in the global setup to use these variables instead of the inline literals so the file no longer contains hard-coded secrets.
304-324: Signing probe: consider logging the co-signer process exit early.If the cosigner crashes immediately after spawn, the 5 × 3 s probe loop will run for the full ~15 s before failing. Checking
cosignerProcess.exitCode !== nullat the top of each iteration would allow an immediate bail-out with a more helpful error message.Proposed improvement
for (let attempt = 1; attempt <= 5; attempt++) { await sleep(3000); + if (cosignerProcess && cosignerProcess.exitCode !== null) { + const cosLog = fs.existsSync(COSIGNER_LOG) ? fs.readFileSync(COSIGNER_LOG, 'utf8') : '(empty)'; + throw new Error(`Co-signer exited early (code ${cosignerProcess.exitCode}).\nLog:\n${cosLog}`); + } const sr = await api.post('/api/sign', {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 304 - 324, The signing probe loop currently waits up to 15s even if the co-signer process crashed; modify the loop that uses signOk/attempt to check cosignerProcess.exitCode !== null at the start of each iteration and if non-null read COSIGNER_LOG (and/or process stdout/stderr) and throw immediately with a descriptive error including the exit code and the log contents; keep the existing retry logic otherwise so successful sr.ok() still breaks the loop.
111-127:pollUntilsilently swallows all exceptions — consider logging on repeated failures.The
catch {}on line 121 discards every exception. For debugging flaky CI runs, at least logging the error on the last attempt (or after N consecutive failures) would be helpful. Not blocking, but worth considering.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 111 - 127, In pollUntil, the empty catch swallows all exceptions from fn; change it to track failures and log the error when it matters (e.g., on the final attempt or after N consecutive failures) so debug info is available without altering retry behavior: inside pollUntil (function name) replace the bare catch with logic that increments a consecutiveFailure counter, reset it after a successful await fn(), and when Date.now() + timeoutMs is about to expire (or counter >= threshold) call the existing logger or console.warn/console.error with a descriptive message including label and the caught error/stack; continue sleeping via sleep(intervalMs) and preserving the same return/throw behavior.llm/implementation/e2e-smoke-tests.md (1)
39-56: Add language specifiers to fenced code blocks.Several fenced code blocks are missing a language identifier, which markdownlint flags as MD040. For example, Lines 39, 93, 101, and 334 should specify a language (e.g.,
text,plaintext, or the appropriate syntax).Also applies to: 93-97, 101-103, 334-340
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@llm/implementation/e2e-smoke-tests.md` around lines 39 - 56, The markdown has several fenced code blocks without language specifiers (MD040); update each affected block (notably the tests/e2e tree block that lists "tests/e2e/" and "playwright.config.ts" and the other blocks at the ranges 93-97, 101-103, and 334-340) by adding an appropriate language tag (e.g., "text", "plaintext", or "bash") right after the opening ``` so markdownlint stops flagging MD040; ensure each block type uses the most semantically correct tag (use "text"/"plaintext" for plain directory trees and "bash" for shell snippets).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 99-104: Replace the mutable reference
trufflesecurity/trufflehog@main with a fixed release tag or commit SHA to avoid
supply-chain risk; update the GitHub Action step that uses
"trufflesecurity/trufflehog@main" to point to a specific version tag (e.g., a
released tag or a pinned commit SHA) and commit that change so the workflow
pulls a known immutable release instead of the main branch.
In `@llm/implementation/e2e-smoke-tests.md`:
- Around line 32-35: Update the prerequisite sentence in e2e-smoke-tests.md to
soften the claim that port 18002 must be free: explain that global-setup.ts uses
resolvePort() to fall back to a random port if 18002 is busy, so the suite will
usually still run, but recommend keeping 18002 free because hard-coded
references may break; mention resolvePort and global-setup.ts so reviewers can
locate the fallback behavior.
In `@llm/implementation/umbrel-implementation.md`:
- Line 74: The path notation is inconsistent: replace the workspace-relative
path "igloo-server-store/igloo-server/docker-compose.yml" referenced in the
instruction about updating the digest with the repo-relative path used earlier
("igloo-server/docker-compose.yml") so the checklist consistently refers to the
same file; update the occurrence in the document (line referencing the digest
update) to "igloo-server/docker-compose.yml" to match the earlier reference and
avoid ambiguity.
In `@test-results/.last-run.json`:
- Around line 1-4: Add the Playwright-generated cache
(test-results/.last-run.json) to version control ignore rules by adding the
test-results/ directory to .gitignore, then stop tracking the already committed
file (e.g., git rm --cached test-results/.last-run.json and commit) so it’s
removed from the repository while remaining locally; ensure the .gitignore entry
specifically excludes the test-results/ directory (or the .last-run.json file)
to prevent future commits.
In `@tests/e2e/cosigner.mjs`:
- Around line 14-17: Replace the hardcoded relative dynamic import with a bare
specifier: change the await
import('../../node_modules/@frostr/igloo-core/dist/index.js') to await
import('@frostr/igloo-core') (or the package's bare entry if it exports a
subpath), and keep the destructuring of createBifrostNode and connectNode so the
test imports those functions from the package using the bare specifier to avoid
node_modules-relative paths.
In `@tests/e2e/global-setup.ts`:
- Around line 141-157: The spawnDetached function leaks the numeric file
descriptor stored in out because fs.openSync(logFile) is never closed in the
parent; after spawn duplicates the fd for the child you should close the
parent's copy. Fix: after creating proc (and after setting up proc.on('error')
if you want), call fs.closeSync(out) to close the parent's fd; keep using the
same stdio configuration (['ignore', out, out]) so the child still writes to the
log via its duplicated fd. Ensure this change is made inside spawnDetached
(referencing spawnDetached, out, proc, and logFile).
In `@tests/e2e/specs/01-auth.e2e.ts`:
- Around line 97-116: The test reads sessionId from loginRes without verifying
login succeeded, so add an assertion that loginRes.status() is 200 before
destructuring sessionId; specifically, after calling api.post('/api/auth/login')
(loginRes) assert expect(loginRes.status()).toBe(200) and only then extract
const { sessionId: tempSession } = await loginRes.json(); leave the rest of the
flow (logoutRes, afterRes) unchanged so failures reflect the real cause.
In `@tests/e2e/specs/02-status-peers.e2e.ts`:
- Around line 36-46: The test 'GET /api/status has valid health object' calls
res.json() without asserting the HTTP status first; update the test to assert
the response status (e.g., expect(res.status()).toBe(200) or
expect(res.ok).toBeTruthy()) immediately after receiving res and before calling
res.json(), so that failures report a clear status mismatch; locate the test
block using the test name and the res variable to apply this change.
In `@tests/e2e/specs/06-event-log.e2e.ts`:
- Around line 32-47: In the "entries have expected shape" test, assert the HTTP
response status before parsing JSON: after calling api.get(...) and receiving
res, add an expectation like expect(res.status()).toBe(200) (or the project's
standard status assertion) immediately before calling await res.json(); this
ensures the test fails with a clear status mismatch if the request failed rather
than throwing on JSON parse or property access.
In `@tests/e2e/specs/08-ui.e2e.ts`:
- Around line 15-19: The assertion using baseUrl + '/' can produce a
double-slash if baseUrl already ends with '/', so update the test named "test('/
renders the login form when not authenticated')" to avoid concatenating '/'
directly: either normalize baseUrl before comparing (e.g., ensure it doesn't end
with '/' or use a URL join/normalization) and then call
expect(page).toHaveURL(normalizedUrl), or drop the exact URL assertion and
instead assert the page loaded (e.g., check page.url() startsWith baseUrl or
verify presence of the login form) to avoid false negatives from a trailing
slash; update references to baseUrl and the expect(page).toHaveURL call
accordingly.
---
Outside diff comments:
In `@src/routes/env.ts`:
- Around line 347-379: The headless POST /api/env path writes GROUP_CRED and
SHARE_CRED directly and can persist invalid credentials; before assigning
env[key] and writing, call the same validators used elsewhere (validateGroup for
GROUP_CRED and validateShare for SHARE_CRED) and reject the request with a 400
and error message if validation fails (mirror the DB-mode and /api/env/shares
behavior). Specifically, inside the headless handling where validKeys are
processed (the loop handling body[key] and the
updatingCredentials/updatingRelays logic), run validateGroup(body.GROUP_CRED)
and validateShare(body.SHARE_CRED) when those keys are present, return
Response.json({...}, {status:400, headers}) on invalid results, and only then
set env values and update CREDENTIALS_SAVED_AT and call
createAndConnectServerNode.
- Around line 286-311: The credential format validations (validateGroup and
validateShare) run before the admin privilege gate, which leaks info; move the
validation blocks for GROUP_CRED, SHARE_CRED (and the existing RELAYS
validation) to occur after the admin check that uses validateAdminSecret and
isRoleAdmin so the 403 is returned before any 400 credential-format errors, or
at minimum swap the order so the admin-secret check (adminSecret,
validateAdminSecret, isRoleAdmin) executes before calling
validateGroup/validateShare; update the code paths referencing validateGroup,
validateShare, RELAYS validation, validateAdminSecret, and isRoleAdmin
accordingly.
In `@src/routes/utils.ts`:
- Around line 63-77: normalizeRelayListForEcho currently strips localhost relays
regardless of ALLOW_LOCALHOST_RELAY and getValidRelays checks only 'localhost'
and '127.0.0.1' (missing '::1'), causing inconsistent behavior; update
normalizeRelayListForEcho to read process.env['ALLOW_LOCALHOST_RELAY'] ===
'true' and skip removing relays whose hostname is 'localhost', '127.0.0.1', or
'::1' when that flag is true, and modify getValidRelays to treat '::1' the same
as '127.0.0.1' when deciding to exclude localhost relays so both functions are
consistent.
In `@tests/routes/env.db-mode.spec.ts`:
- Around line 105-108: The test currently allows an HTTP 500 which weakens the
assertion; update the "stamps CREDENTIALS_SAVED_AT" test to assert the primary
success path by requiring out.status === 200 (instead of
expect([200,500]).toContain(out.status)) and either mock/suppress the restart
behavior in the test environment or stub the restart function so the route
returns 200 reliably; keep the existing expect(out.hasStamp).toBeTrue() as the
primary check and, if desired, add a test-specific warning/log only when restart
mocking is impossible.
---
Nitpick comments:
In `@llm/implementation/e2e-smoke-tests.md`:
- Around line 39-56: The markdown has several fenced code blocks without
language specifiers (MD040); update each affected block (notably the tests/e2e
tree block that lists "tests/e2e/" and "playwright.config.ts" and the other
blocks at the ranges 93-97, 101-103, and 334-340) by adding an appropriate
language tag (e.g., "text", "plaintext", or "bash") right after the opening ```
so markdownlint stops flagging MD040; ensure each block type uses the most
semantically correct tag (use "text"/"plaintext" for plain directory trees and
"bash" for shell snippets).
In `@scripts/release.sh`:
- Around line 76-84: Consolidate the duplicated SERVER_HEALTHY checks in
release.sh into a single block: remove the first standalone if that echoes "❌
Server failed to respond after 5 attempts" and merge its message into the later
block that exits, so one if [ "$SERVER_HEALTHY" = false ]; then prints the
failure message and then exits with status 1; update any comments to reflect
that cleanup is still handled by trap and ensure the variable SERVER_HEALTHY is
the single condition used.
In `@tests/e2e/cosigner.mjs`:
- Line 47: The test is accessing a private property node.client?._filter; change
to use the public API node.client?.filter and only fall back to _filter if
absolutely necessary: replace the expression with node.client?.filter ??
node.client?._filter ?? '?' (so public API is preferred), and add a short
comment noting that _filter is a private internals fallback to avoid silent
breakage on library upgrades.
In `@tests/e2e/global-setup.ts`:
- Around line 80-109: Add a concise inline comment above canBindPort,
reserveRandomPort, and resolvePort explaining the TOCTOU race: these functions
probe by binding/closing (canBindPort) and reserving ephemeral ports
(reserveRandomPort) which does not guarantee the port remains free when used
later, so this is an accepted, low-risk approach for tests/CI; reference the
limitation in the resolvePort warning path so future maintainers understand the
potential race and why no extra locking is implemented.
- Around line 34-40: Replace the hard-coded secrets (TEST_NSEC_HEX,
ADMIN_SECRET, ADMIN_USERNAME, ADMIN_PASSWORD) with values loaded from
environment variables or a data/fixture loader: read process.env.TEST_NSEC_HEX,
process.env.ADMIN_SECRET, process.env.ADMIN_USERNAME, process.env.ADMIN_PASSWORD
(with non-sensitive safe defaults only for local CI if absolutely necessary) or
load them from a test fixture file in data/, and update any test setup logic in
the global setup to use these variables instead of the inline literals so the
file no longer contains hard-coded secrets.
- Around line 304-324: The signing probe loop currently waits up to 15s even if
the co-signer process crashed; modify the loop that uses signOk/attempt to check
cosignerProcess.exitCode !== null at the start of each iteration and if non-null
read COSIGNER_LOG (and/or process stdout/stderr) and throw immediately with a
descriptive error including the exit code and the log contents; keep the
existing retry logic otherwise so successful sr.ok() still breaks the loop.
- Around line 111-127: In pollUntil, the empty catch swallows all exceptions
from fn; change it to track failures and log the error when it matters (e.g., on
the final attempt or after N consecutive failures) so debug info is available
without altering retry behavior: inside pollUntil (function name) replace the
bare catch with logic that increments a consecutiveFailure counter, reset it
after a successful await fn(), and when Date.now() + timeoutMs is about to
expire (or counter >= threshold) call the existing logger or
console.warn/console.error with a descriptive message including label and the
caught error/stack; continue sleeping via sleep(intervalMs) and preserving the
same return/throw behavior.
In `@tests/e2e/global-teardown.ts`:
- Around line 62-74: Validate the temporary directory is actually inside the
system temp directory before calling fs.rmSync(recursive: true): compute a
resolved path for tmpDir (use path.resolve(tmpDir)) and compare it to
os.tmpdir() (use path.relative(os.tmpdir(), resolvedTmp) and ensure the result
does not start with '..' and is not equal to '' or '.', otherwise skip deletion
and log a warning; keep the existing try/catch and console messages but only
call fs.rmSync when the safety check passes to avoid accidental deletion outside
the temp directory.
In `@tests/e2e/specs/03-nip44-nip04.e2e.ts`:
- Around line 86-140: Add a new test to the "NIP-04 – /api/nip04" suite that
posts to '/api/nip04/encrypt' with a valid 'peer_pubkey' but omits 'content' (or
sets it to null/undefined) and asserts the response status is 400; mirror the
existing NIP-44 "missing content returns 400" test logic so validation
consistency is covered, using the same request.newContext(), X-Session-ID header
(sessionId), and expect(res.status()).toBe(400) pattern as used in the other
tests in this suite.
In `@tests/e2e/specs/07-env.e2e.ts`:
- Around line 63-75: Add a new e2e test that covers the empty-relays edge case
by duplicating the existing "POST /api/env – invalid relay URL returns 400" test
pattern: use request.newContext({ baseURL: baseUrl }), include headers with
X-Session-ID: sessionId and data with GROUP_CRED: state.groupCredential,
SHARE_CRED: state.shareCredentials[0], but set RELAYS: [] and assert
res.status() is 400, then dispose the context; name it something like "POST
/api/env – empty RELAYS returns 400" to make the intent clear and keep the same
setup/teardown used in the existing test.
In `@tests/e2e/specs/08-ui.e2e.ts`:
- Around line 48-62: Extract the duplicated login sequence (creating
usernameField/passwordField locators, filling adminUsername/adminPassword,
clicking submitBtn, and awaiting page.waitForLoadState('networkidle')) into a
shared helper function (e.g., export async function loginAs(page, username,
password) in helpers.ts or state.ts); replace the duplicated code in
test.beforeEach and the "login form accepts credentials" test with a single call
to loginAs(page, adminUsername, adminPassword); keep the same locator selectors
(input[type="text"], input[id*="user"], input[name*="user"],
input[type="password"], button[type="submit"], button:has-text("Login"),
button:has-text("Sign in")) and ensure the helper clicks the submit button and
awaits networkidle before returning.
In `@tests/e2e/state.ts`:
- Around line 43-50: The loadState function currently casts JSON.parse(...) to
SmokeTestState without runtime checks; after parsing the file (in loadState
where JSON.parse(fs.readFileSync(...)) is called) validate that the result is an
object and contains required fields (e.g., sessionId and baseUrl) and that they
are non-empty strings; if validation fails, log or throw a clear error (or
return STUB) instead of silently returning an invalid object so callers of
loadState/SmokeTestState get immediate, actionable feedback about a malformed
state file.
parsing, and IPv6 loopback filter
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (7)
tests/e2e/specs/05-admin.e2e.ts (1)
14-137: Same resource leak pattern as07-env.e2e.ts—dispose()not called on assertion failure.All tests in this file use the manual
newContext → assert → disposepattern withouttry/finally. Please apply the same fixture-based ortry/finallyfix described for07-env.e2e.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/05-admin.e2e.ts` around lines 14 - 137, Multiple tests open a Playwright API context via request.newContext(...) and call api.dispose() only at the end, so if an assertion throws the context is leaked; update each test (e.g., the tests named "GET /api/admin/api-keys returns list", "POST /api/admin/api-keys creates a new key", "new API key can authenticate", "revoked API key returns 401", "GET /api/admin/api-keys without auth returns 401", "GET /api/admin/users returns user list", "GET /api/admin/whoami returns admin identity", "GET /api/admin/users without auth returns 401") to ensure api.dispose() always runs by wrapping the test body in try { ... } finally { await api.dispose(); } or convert to using Playwright's test fixture/request fixture so contexts are cleaned automatically; apply the same pattern used to fix 07-env.e2e.ts..github/workflows/ci.yml (1)
99-104: TruffleHog pinned to an immutable commit SHA — the past supply-chain concern is resolved.Switching from
@mainto a full commit SHA (7c0734f9…) is more secure than even a version tag, and droppingcontinue-on-error: truemeans a detected secret now correctly fails the pipeline.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci.yml around lines 99 - 104, Update the "Check for secrets" GitHub Actions step so it is pinned to an immutable commit and will fail the workflow on detected secrets: ensure the uses field references the full commit SHA (trufflesecurity/trufflehog@7c0734f987ad0bb30ee8da210773b800ee2016d3) and remove any continue-on-error: true setting from that step so trufflehog failures correctly fail the pipeline.tests/e2e/specs/08-ui.e2e.ts (1)
17-20: Past review fix confirmed: URL assertion now correctly usesnew URL('/', baseUrl).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/08-ui.e2e.ts` around lines 17 - 20, The URL assertion has been corrected to use new URL('/', baseUrl) when asserting the SPA landed on the root after page.goto(baseUrl); confirm that the check uses expect(page).toHaveURL(new URL('/', baseUrl).toString()) (in the test around page.goto and the expect call) and no further changes are required as this fixes the previous incorrect URL assertion.llm/implementation/e2e-smoke-tests.md (1)
32-35: Past review fix confirmed: port prerequisite is now appropriately softened at line 35.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@llm/implementation/e2e-smoke-tests.md` around lines 32 - 35, The previous concern about hard-coding port 18002 has been addressed; no code change required — confirm the docs note in llm/implementation/e2e-smoke-tests.md remains as-is and that tests/e2e/global-setup.ts uses resolvePort() (and falls back to a random free port) so the softened prerequisite is correct; if you want, add a short note referencing resolvePort() to clarify behavior but do not modify test code.tests/e2e/specs/06-event-log.e2e.ts (1)
32-48: Past review fix confirmed: status assertion is now in place at line 37.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/06-event-log.e2e.ts` around lines 32 - 48, The test "entries have expected shape" already includes the status assertion for res.status() and validates entry properties when body.entries has items, so no code change is required; keep the existing assertions in the test function (variables: api, res, body, sessionId) as-is and mark the change approved.tests/e2e/specs/02-status-peers.e2e.ts (1)
36-47: Past review fix confirmed: status assertion now in place.
expect(res.status()).toBe(200)is correctly asserted at line 41 before callingres.json().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/02-status-peers.e2e.ts` around lines 36 - 47, The test 'GET /api/status has valid health object' correctly asserts the HTTP status with expect(res.status()).toBe(200) before calling await res.json(); leave the status assertion and the rest of the test (including checks for body.health.isConnected and body.health.consecutiveConnectivityFailures) as-is and do not remove or reorder the status check in this test function.tests/e2e/specs/01-auth.e2e.ts (1)
97-117: Logout test now correctly asserts login success before using the session.Line 103 adds the
expect(loginRes.status()).toBe(200)guard that was previously missing. This ensures failures in the login step surface clearly instead of cascading into misleading 401 assertions.,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/01-auth.e2e.ts` around lines 97 - 117, The logout E2E test ("POST /api/auth/logout – returns 200 and clears session") was missing an assertion that login succeeded before using the returned session; add an explicit check after the login request (check loginRes.status() is 200) and only read sessionId into tempSession after that assertion so failures in login surface instead of causing misleading downstream 401s; update the test that uses api, loginRes, tempSession, logoutRes, and afterRes accordingly.
🧹 Nitpick comments (20)
src/class/relay.ts (1)
220-220: Consider aligningsub_idto camelCase (subId).This line touches the snake_case parameter, which conflicts with the repo’s TypeScript naming standard; a follow-up rename would keep the API consistent.
As per coding guidelines, "Use camelCase for variable names".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/class/relay.ts` at line 220, Rename the snake_case parameter sub_id to camelCase subId across the codebase: update the addSub method signature and all places that call it (including this.addSub(sub_id, ...filters) -> this.addSub(subId, ...filters)), adjust any related type annotations, interface definitions, and exported/public API spots (e.g., class Relay methods and event handlers) so names remain consistent, and run the TypeScript compiler to fix any remaining references or imports that need renaming.frontend/components/ui/peer-list.tsx (1)
596-607: Optional: addaria-expandedto the toggle buttonThe header already has
role="button"andtabIndex={0}, and this PR extends its keyboard accessibility further. However,aria-expandedis absent, so screen-reader users receive no audible signal about the collapsed/expanded state of the peer list panel.♿ Suggested addition
<div className="flex flex-col sm:flex-row sm:items-center justify-between bg-gray-800/50 p-2.5 rounded cursor-pointer hover:bg-gray-800/70 transition-colors gap-2 sm:gap-0" onClick={handleToggle} role="button" tabIndex={0} + aria-expanded={isExpanded} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleToggle(); } }} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 596 - 607, The toggle header div with role="button" and onClick={handleToggle} is missing an accessible state indicator; add aria-expanded={/* boolean state controlling the panel (e.g. isOpen, expanded, open) */} to that div so screen readers announce collapsed/expanded state, using the same state variable that determines the peer list visibility and keeping the attribute updated whenever handleToggle toggles it.src/routes/utils.test.ts (1)
35-44:normalizeRelayListForEchotest is nested insidedescribe('getValidRelays').Lines 35–44 test
normalizeRelayListForEcho, notgetValidRelays. Move it into its owndescribeblock to keep test grouping accurate.♻️ Proposed refactor
- it('keeps localhost relay in echo list when explicitly allowed', () => { - const previous = process.env.ALLOW_LOCALHOST_RELAY; - process.env.ALLOW_LOCALHOST_RELAY = 'true'; - try { - expect(normalizeRelayListForEcho(['ws://127.0.0.1:18002'])).toEqual(['ws://127.0.0.1:18002']); - } finally { - if (previous === undefined) delete process.env.ALLOW_LOCALHOST_RELAY; - else process.env.ALLOW_LOCALHOST_RELAY = previous; - } - }); }); + +describe('normalizeRelayListForEcho', () => { + it('keeps localhost relay in echo list when explicitly allowed', () => { + const previous = process.env.ALLOW_LOCALHOST_RELAY; + process.env.ALLOW_LOCALHOST_RELAY = 'true'; + try { + expect(normalizeRelayListForEcho(['ws://127.0.0.1:18002'])).toEqual(['ws://127.0.0.1:18002']); + } finally { + if (previous === undefined) delete process.env.ALLOW_LOCALHOST_RELAY; + else process.env.ALLOW_LOCALHOST_RELAY = previous; + } + }); +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.test.ts` around lines 35 - 44, The test for normalizeRelayListForEcho is currently placed inside the describe('getValidRelays') block; move that it(...) into its own describe('normalizeRelayListForEcho') block so tests are grouped correctly. Locate the it('keeps localhost relay in echo list when explicitly allowed', ...) which calls normalizeRelayListForEcho(['ws://127.0.0.1:18002']) and cut/paste it into a new describe wrapper (or create a sibling describe) dedicated to normalizeRelayListForEcho, leaving the original getValidRelays describe only containing tests for getValidRelays.src/routes/env.ts (2)
294-316: Validation blocks are duplicated between the DB-mode and headless branches.The RELAYS, GROUP_CRED, and SHARE_CRED validation triplet (lines 294–316) is copy-pasted verbatim into the headless path (lines 365–387). Consider extracting a validation helper:
function validateCredentialPayload( body: Record<string, unknown>, validKeys: string[] ): Response | null { /* ... */ }This would halve the surface area for future validation changes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 294 - 316, Duplicate validation logic for RELAYS, GROUP_CRED and SHARE_CRED should be extracted into a reusable helper; create a function (e.g., validateCredentialPayload(body: Record<string, unknown>, validKeys: string[]) : Response | null) that runs validateRelayUrls, validateGroup and validateShare for keys RELAYS, GROUP_CRED, SHARE_CRED and returns a Response when validation fails or null when OK, then call this helper from both the DB-mode and headless branches instead of duplicating the blocks that call validateRelayUrls, validateGroup and validateShare.
283-286: Duplicated admin-secret extraction — extract into a shared helper.Lines 283–286 are byte-for-byte duplicated at lines 668–671 (the
/api/env/deletebranch). Extract the logic once to avoid drift.♻️ Proposed refactor
Add a module-local helper (e.g., near the other
hasValid*closures):+ const extractAdminSecretFromRequest = (r: Request): string | undefined => { + const adminSecretHeader = r.headers.get('X-Admin-Secret'); + if (adminSecretHeader !== null) return adminSecretHeader; + const authz = r.headers.get('Authorization'); + return (authz && /^Bearer\s+/i.test(authz)) + ? authz.replace(/^Bearer\s+/i, '') + : undefined; + };Then replace both call-sites:
- const authHeader = req.headers.get('Authorization'); - const bearerToken = authHeader && /^Bearer\s+/i.test(authHeader) ? authHeader.replace(/^Bearer\s+/i, '') : undefined; - const adminSecret = req.headers.get('X-Admin-Secret') ?? bearerToken; - const isAdminSecret = await validateAdminSecret(adminSecret ?? undefined); + const isAdminSecret = await validateAdminSecret(extractAdminSecretFromRequest(req));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 283 - 286, The admin-secret extraction logic (authHeader, bearerToken, adminSecret, validateAdminSecret) is duplicated; create a module-local helper function (e.g., getAdminSecretFromRequest(req): Promise<boolean> or getAdminSecret(req): string | undefined plus a call to validateAdminSecret) near the other hasValid* closures, move the extraction (Authorization header parsing and X-Admin-Secret fallback) into it, and then replace both call-sites (the current usage and the `/api/env/delete` branch) to call that helper and use its result instead of duplicating the lines.src/routes/utils.ts (1)
35-38:isLoopbackRelayHostcovers only127.0.0.1, not the full127.0.0.0/8range.Addresses like
127.0.0.2–127.255.255.255are also loopback per RFC 5735 but pass through unblocked. For the current use case (blocking incidental self-connection during tests and default deployments) the three explicit entries are adequate, but this could be tightened if relay configs that use non-127.0.0.1loopback IPs ever appear.♻️ Optional improvement
function isLoopbackRelayHost(hostname: string): boolean { const normalized = hostname.replace(/^\[(.*)\]$/, '$1'); - return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1'; + // Covers localhost, the full 127.0.0.0/8 block, and IPv6 loopback + if (normalized === 'localhost' || normalized === '::1') return true; + const parts = normalized.split('.'); + return parts.length === 4 && parts[0] === '127'; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.ts` around lines 35 - 38, The isLoopbackRelayHost function only checks for '127.0.0.1' instead of the full 127.0.0.0/8 range; update it to treat any IPv4 in 127.0.0.0/8 as loopback by checking the normalized host for either 'localhost' or '::1' or an IPv4 that begins with '127.' and has four valid octets (0-255), e.g., parse/split the normalized string on '.' and validate that the first octet === '127' and the remaining three are integers 0–255; keep the existing bracket normalization logic and apply the same expanded check in isLoopbackRelayHost.tests/routes/helpers/script-runner.ts (1)
34-35:as Record<string, string>cast silently retains anyundefinedenv values.
process.envvalues are always strings for existing keys in Bun/Node, so the cast is safe in practice. A compile-time safer alternative is to filter out undefined values explicitly, eliminating the assertion:♻️ Optional type-safety improvement
- const nextEnv: Record<string, string> = { ...process.env } as Record<string, string>; + const nextEnv: Record<string, string> = Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined) + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` around lines 34 - 35, The cast in buildScriptEnv that forces process.env to Record<string,string> can silently keep undefined values; instead build nextEnv by iterating/filtering process.env entries and only include keys whose values are strings (e.g., via Object.entries(process.env).filter(([,v]) => typeof v === 'string') into nextEnv) and then apply overrides, ensuring nextEnv and overrides remain Record<string,string>; update the function buildScriptEnv to remove the unsafe "as Record<string,string>" cast and populate nextEnv safely.tests/e2e/specs/07-env.e2e.ts (1)
16-98:api.dispose()not called on assertion failure — resource leak across all tests.Every test follows the same
newContext → test → disposepattern without atry/finally. If anyexpect()assertion throws beforedispose(), theAPIRequestContextis leaked. Playwright may report open handles at the end of the run.The idiomatic Playwright fix is to use the built-in
requestfixture (auto-disposed after each test):♻️ Proposed fix (example for first test)
- test('GET /api/env returns 401 without auth', async () => { - const api = await request.newContext({ baseURL: baseUrl }); - const res = await api.get('/api/env'); - expect(res.status()).toBe(401); - await api.dispose(); - }); + test('GET /api/env returns 401 without auth', async ({ request: api }) => { + const res = await api.get('/api/env', { baseURL: baseUrl }); + expect(res.status()).toBe(401); + });Apply the same pattern to every test in this file (and
05-admin.e2e.ts). If the config'sbaseURLis pre-set tobaseUrlfor theapiproject, thebaseURLoverride per-call can also be dropped.Alternatively, wrap in
try/finally:const api = await request.newContext({ baseURL: baseUrl }); try { // assertions } finally { await api.dispose(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/07-env.e2e.ts` around lines 16 - 98, Tests create APIRequestContext via request.newContext into the variable "api" and call await api.dispose() at the end, but a failing expect() will skip dispose() and leak the context; fix by using Playwright's built-in "request" fixture (replace request.newContext(...) with the provided "request" per-test fixture and remove manual dispose) or wrap the newContext usage in try/finally so await api.dispose() always runs (apply to each test in 07-env.e2e.ts and similarly in 05-admin.e2e.ts, referencing the "api" variable and the existing test blocks).tests/e2e/state.ts (1)
77-77: Redundant type assertion.
smokeTestStateSchema.safeParsealready infers the output as the schema's type, which is structurally identical toSmokeTestState. Theas SmokeTestStatecast is unnecessary and can be removed.♻️ Proposed fix
- return result.data as SmokeTestState; + return result.data;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/state.ts` at line 77, Remove the redundant type assertion on result.data: since smokeTestStateSchema.safeParse already returns the parsed type, change the return to return result.data (drop the "as SmokeTestState" cast). Update the return in the surrounding function where result is produced (the code calling smokeTestStateSchema.safeParse and assigning to result.data) so it relies on the schema-inferred type rather than casting to SmokeTestState.tests/routes/env.db-mode.spec.ts (1)
81-82: Hardcodednode_modulespath and key seed violate project guidelines.Two issues in these lines:
Line 81 — The import uses a raw
node_modules/…/dist/index.jspath. This bypasses@frostr/igloo-core'sexportsmap and will break silently if the package changes its entry point. The same issue was fixed incosigner.mjs(now uses a bare specifier); the same fix should be applied here, as the subprocess thatrunRouteScriptspawns can still resolve bare specifiers via the project'snode_modules.Line 82 — The hardcoded hex string
'deadbeef...'is key material that should be extracted to adata/fixture file. As per coding guidelines, secrets/seeds should never be hard-coded inline; seed complex test scenarios fromdata/fixtures instead.♻️ Proposed fix
- const { generateKeysetWithSecret } = await import(root + 'node_modules/@frostr/igloo-core/dist/index.js'); - const { groupCredential, shareCredentials } = generateKeysetWithSecret(2, 2, 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'); + const { generateKeysetWithSecret } = await import('@frostr/igloo-core'); + const TEST_KEY_SEED = process.env.TEST_KEY_SEED ?? 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + const { groupCredential, shareCredentials } = generateKeysetWithSecret(2, 2, TEST_KEY_SEED);Or move the seed to
data/fixtures/test-keyset.jsonand read it withreadFileSync.Based on learnings: "Applies to
**/*.{test,spec}.ts?(x): Seed complex test scenarios fromdata/fixtures instead of live services" and "Applies to**/*.{ts,tsx}: Never hard-code secrets; load from environment ordata/fixtures."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/env.db-mode.spec.ts` around lines 81 - 82, Replace the hardcoded node_modules import and inline seed: import the generator via the package bare specifier (use generateKeysetWithSecret from '@frostr/igloo-core') instead of the raw root + 'node_modules/.../dist/index.js', and move the hex seed into a test fixture file (e.g. data/fixtures/test-keyset.json) and load it at test runtime (readFileSync or equivalent) before calling generateKeysetWithSecret(2, 2, seed); keep references to groupCredential and shareCredentials unchanged so the call site still uses the same returned values.tests/e2e/cosigner.mjs (1)
48-48: Accessing an underscore-prefixed private field is fragile.
node.client?._filterreaches into an implementation detail of@frostr/igloo-core. If the library renames or removes this field, the fallback silently returnsundefinedrather than failing noisily — the surroundingJSON.stringify(... ?? '?')masks the gap. The comment acknowledges this, so no change is strictly required, but this line warrants a TODO to remove when the library exposes a public accessor.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` at line 48, The test is reaching into a private field (node.client?._filter) and then masking missing data via JSON.stringify(... ?? '?'); update the log to explicitly detect and surface when the private fallback is used and add a TODO to remove the fallback once `@frostr/igloo-core` exposes a public accessor: inspect node.client?.filter first, if missing check node.client?._filter and log a distinct warning message (including which property was used) instead of silently stringifying '?', and add a clear TODO referencing removal when a public getter exists; keep the existing log content but make the fallback explicit so future breakage is noisier.tests/e2e/specs/05-admin.e2e.ts (1)
28-42: Unrevoked test API keys accumulate on local re-runs.The "creates a new key" (line 28) and "new API key can authenticate" (line 44) tests each create a key without cleanup. In CI this is harmless (each run gets a fresh server), but repeated local test-file re-runs against a long-lived dev server will accumulate keys and could eventually affect the
length >= 1assertion in the listing test. Consider revoking or deleting keys in anafterEach/cleanup step, or accept the current behaviour as a known limitation.Also applies to: 44-61
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/05-admin.e2e.ts` around lines 28 - 42, The tests "POST /api/admin/api-keys creates a new key" and "new API key can authenticate" create API keys but never revoke them; capture the created apiKey.id (from body.apiKey.id) and add a cleanup step (e.g., an afterEach or a finally block) that calls the admin revoke/delete endpoint using the same context/sessionId to remove the key(s); implement by storing IDs in a local array (e.g., createdKeyIds) and in afterEach iterate and call api.delete(`/api/admin/api-keys/${id}`, { headers: { 'X-Session-ID': sessionId } }) or the appropriate revoke route to ensure keys are removed after each test..github/workflows/ci.yml (1)
28-29: CI route tests cover onlytests/routes;src/unit tests are skipped in CI.
bun run test:unit(which runs against bothsrcandtests/routes) is only wired up in the release workflow. The CItestjob only runsbun test tests/routes, meaning any unit tests undersrc/won't be caught on PRs.♻️ Suggested fix
- - name: Run route tests - run: bun test tests/routes + - name: Run unit tests + run: bun run test:unitThis aligns CI with the release workflow and catches
src/test regressions on every push/PR.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci.yml around lines 28 - 29, The CI step currently named "Run route tests" only runs "bun test tests/routes" and skips unit tests under src/; update that step to run the same command used in the release workflow (e.g., "bun run test:unit" which runs tests in both src/ and tests/routes) so PRs run the full unit test suite; locate the workflow step with the name "Run route tests" and replace its run command accordingly (or run both commands) to ensure src/ tests are executed on CI.playwright.config.ts (1)
20-25: ConfigbaseURLis hardcoded to port 18002 but may be stale.
global-setup.tscallsresolvePort()and can fall back to a random port. All existing tests correctly passstate.baseUrl(from the state file) when constructing their own contexts, so this value is currently unused. However, any future test that uses a relative path inpage.goto('/')will silently target 18002 regardless of the resolved port.Consider deriving this from the same env var or removing it to avoid the misleading default:
♻️ Suggested change
use: { - baseURL: 'http://localhost:18002', + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:18002', trace: 'on-first-retry', actionTimeout: 15_000, },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@playwright.config.ts` around lines 20 - 25, The Playwright config hardcodes use.baseURL = 'http://localhost:18002', which can be stale because global-setup.ts uses resolvePort() and tests use state.baseUrl; update the Playwright config so baseURL is derived from the same source (e.g., read process.env.TEST_BASE_URL or export the resolved port into an env variable in global-setup.ts) or remove use.baseURL entirely to avoid misleading defaults; ensure references to baseURL (use.baseURL) and any test usages of page.goto('/') align with state.baseUrl or the env var so relative navigations target the resolved port.tests/e2e/specs/04-sign.e2e.ts (1)
19-19:test.setTimeout(30_000)is redundant with the globaltimeout: 30_000inplaywright.config.ts.It does serve as useful in-code documentation of the latency expectation, so it's fine to keep. Consider a brief comment explaining why, or remove it to reduce noise.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/04-sign.e2e.ts` at line 19, The line test.setTimeout(30_000) in the 04-sign.e2e.ts test is redundant because the global timeout is already set to 30_000 in playwright.config.ts; either remove the test.setTimeout(30_000) call or keep it but add a brief inline comment above it (e.g., "// Explicit per-test timeout for clarity; global timeout is 30_000 in playwright.config.ts") so future readers understand the redundancy; update the file by deleting the call or inserting the single-line comment immediately above the test.setTimeout invocation and commit the change.tests/e2e/specs/08-ui.e2e.ts (1)
58-63: Overly broad locator may produce false positives in the Configure-tab test.
'input, textarea, [data-testid*="cred"]'matches any visible input or textarea on the page — including hidden fields, navigation inputs, or elements from other panels. If any input is visible after clicking the Configure tab (even unrelated ones), this assertion passes while the actual configure panel may not have rendered.Consider a more specific selector that targets known configure-panel content, or at minimum add a scoping ancestor:
♻️ Suggested change
- const configContent = page.locator('input, textarea, [data-testid*="cred"]').first(); + // Scope to the active tab panel so unrelated inputs don't satisfy the assertion + const configContent = page.locator('[role="tabpanel"] input, [role="tabpanel"] textarea, [data-testid*="cred"]').first();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/08-ui.e2e.ts` around lines 58 - 63, The test's locator (configContent) is too broad ('input, textarea, [data-testid*="cred"]') and can match unrelated inputs causing false positives; update the locator to scope it to the configure panel (e.g., target a known ancestor like the configure tab/panel test id or a heading with text "Configure" and then query inputs within that ancestor) or use a more specific data-testid for configure fields, and change the assertion to use that scoped locator (locators referenced: configContent and the configure panel's test id/heading).tests/e2e/helpers.ts (1)
4-6:input[name*="ur"]is an overly broad selector fragment.The substring
"ur"matches far more than intended — e.g., inputs namedcurrency,url,secure,procedure. If any of these appear alongside a password field on a page whereloginAsis called,.first()may silently fill the wrong element. The preceding selectors (input[type="text"],input[id*="user"],input[name*="user"]) already cover the common cases.♻️ Suggested change
const usernameField = page - .locator('input[type="text"], input[id*="user"], input[name*="user"], input[name*="ur"]') + .locator('input[type="text"], input[id*="user"], input[name*="user"]') .first();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/helpers.ts` around lines 4 - 6, The selector for usernameField is too broad because of input[name*="ur"]; update the locator in the usernameField definition to remove the input[name*="ur"] fragment and rely on the remaining, safer selectors (e.g., input[type="text"], input[id*="user"], input[name*="user"]) so the locator (in the usernameField variable) won't accidentally match inputs like currency or url; keep the .first() behavior but ensure the selector only includes the user-specific fragments.tests/e2e/global-setup.ts (2)
152-172:spawnDetachedname is misleading — process is spawned attached (detached: false).The function is named
spawnDetachedbut explicitly setsdetached: falseat Line 162. This could confuse future readers about the child process lifecycle (e.g., whether the child survives the parent).Also, the fd-leak fix from the prior review is properly addressed with the
try/finallyblock — nice.Suggested rename
-function spawnDetached( +function spawnBackgroundProcess(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 152 - 172, The function spawnDetached misleadingly sets detached: false; either rename the function to reflect behavior (e.g., spawnAttached or spawnChild) and update all references to spawnDetached, or change the spawn option to detached: true if the intent is to spawn a detached process; also keep the existing try/finally FD handling and the error handler on the returned ChildProcess (update the function name in tests/usages where spawnDetached is called).
209-215://@ts-ignore`` on dynamic import with hardcodednode_modulespath is fragile.The hardcoded path
../../node_modules/@frostr/igloo-core/dist/index.jswill break if dependencies are hoisted (e.g., pnpm, yarn PnP, or monorepo layouts). Consider usingcreateRequireto resolve the module through Node's standard resolution algorithm, which also removes the need for@ts-ignore.Suggested approach
+import { createRequire } from 'module'; +const require = createRequire(import.meta.url); ... - const { generateKeysetWithSecret, decodeGroup } = await import( - // `@ts-ignore` - '../../node_modules/@frostr/igloo-core/dist/index.js' - ) as { + const { generateKeysetWithSecret, decodeGroup } = require('@frostr/igloo-core') as { generateKeysetWithSecret: (t: number, n: number, sk: string) => { groupCredential: string; shareCredentials: string[] }; decodeGroup: (g: string) => { group_pk: string; threshold: number; commits: unknown[] }; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 209 - 215, The dynamic import currently uses a fragile hardcoded node_modules path; replace it with a Node module-resolution approach (e.g., use createRequire from 'module' or require.resolve) to locate and import the package so it works with hoisting/PNPM/monorepos and removes the need for the // `@ts-ignore`. Specifically, resolve the package '@frostr/igloo-core' via createRequire or require.resolve and then import or require it, and keep using the same exports (generateKeysetWithSecret and decodeGroup) so the rest of the code that references those symbols remains unchanged.tests/e2e/global-teardown.ts (1)
11-28:findLatestStateFilefallback could pick a stale run's state in concurrent CI.If multiple smoke-test jobs run in parallel on the same machine, the mtime-based heuristic might select a state file from a different run. This is documented as a fallback behind
SMOKE_STATE_FILE, so it's a known limitation — just worth noting in case CI parallelism increases.Consider logging the resolved state file path at the top of
globalTeardownfor debuggability:if (!resolvedStateFile || !fs.existsSync(resolvedStateFile)) { console.warn('[teardown] No state file found – nothing to clean up.'); return; } + console.log(`[teardown] Using state file: ${resolvedStateFile}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-teardown.ts` around lines 11 - 28, findLatestStateFile can pick a stale run when CI jobs run in parallel; to improve debuggability, in the globalTeardown function log which state file was resolved (the value returned by findLatestStateFile or the SMOKE_STATE_FILE env var) at the top of globalTeardown before proceeding; update globalTeardown to compute the effective state path (use process.env.SMOKE_STATE_FILE if present otherwise call findLatestStateFile()), then emit a clear processLogger / console log containing that resolved path (or a message when none found) so CI logs show exactly which file was used.
🤖 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 67-68: Remove the duplicate .DS_Store entry from the .gitignore
file: keep the existing macOS .DS_Store ignore (the one already present around
the macOS section) and delete the redundant `.DS_Store` line shown in the diff
so there is only a single `.DS_Store` entry in .gitignore.
In `@llm/implementation/e2e-smoke-tests.md`:
- Line 195: Update the spec coverage map entry that currently says the sign
endpoint response contains `sig` and `pubkey` to match the actual test
assertions: change the documented response fields to `id` and `signature` (as
asserted in tests/e2e/specs/04-sign.e2e.ts lines 69–73) so the documentation and
tests are consistent.
In `@package.json`:
- Around line 38-39: The two npm scripts "test:e2e" and "test:e2e:nightly"
currently invoke the same command; change the "test:e2e:nightly" script to run
Playwright with nightly-appropriate flags (for example enable all
projects/browsers, increase retries, and raise timeouts) instead of exactly "npx
playwright test". Update the package.json scripts so "test:e2e" remains the
standard run (e.g., default projects, shorter timeout) and "test:e2e:nightly"
calls Playwright with explicit flags such as running all projects, a higher
--retries value, and larger --timeout (or any equivalent Playwright options your
test suite uses) to differentiate the nightly job from the regular e2e run.
In `@tests/e2e/specs/02-status-peers.e2e.ts`:
- Around line 68-69: The inline comment above the assertion for body.peers is
inaccurate: update the comment that currently says "2-of-3 keyset: 2 remote
peers (self filtered out)" to reflect the global test setup which uses a 2-of-2
keyset with exactly 1 remote peer; keep the assertion
expect(body.peers.length).toBeGreaterThanOrEqual(1) unchanged and ensure the
comment references the same check (body.peers / the expect call) so it correctly
documents "2-of-2 keyset: 1 remote peer (self filtered out)".
In `@tests/e2e/specs/05-admin.e2e.ts`:
- Around line 44-61: The test calls createRes.json() and destructures { apiKey }
without asserting the create response status, which can hide failures; update
the "new API key can authenticate" test to assert createRes.status() === 201 (or
the expected status) before calling createRes.json() and destructuring apiKey,
and do the same defensive status assertion in the "revoked API key" test that
follows (the block that also reads createRes and destructures apiKey) so
failures surface at the correct step.
In `@tests/e2e/specs/06-event-log.e2e.ts`:
- Around line 40-47: The test currently skips shape assertions when body.entries
is empty, making the spec pass vacuously; update the test in 06-event-log.e2e.ts
to assert that at least one event exists before checking fields (e.g., assert
body.entries.length > 0 or expect(body.entries).not.toHaveLength(0)), then
proceed to validate the first entry's properties (type, message, timestamp) on
body.entries[0]; this ensures the spec fails if no entries were produced and
still preserves the existing property checks.
In `@tests/e2e/state.ts`:
- Line 2: The project imports zod (import { z } from 'zod') in
tests/e2e/state.ts and also uses it in src/util/schema.ts but zod is not listed
in package.json; add zod to package.json devDependencies (choose a
compatible/pinned version) so tests and src/util/schema.ts rely on an explicit
dependency rather than a transitive one.
---
Duplicate comments:
In @.github/workflows/ci.yml:
- Around line 99-104: Update the "Check for secrets" GitHub Actions step so it
is pinned to an immutable commit and will fail the workflow on detected secrets:
ensure the uses field references the full commit SHA
(trufflesecurity/trufflehog@7c0734f987ad0bb30ee8da210773b800ee2016d3) and remove
any continue-on-error: true setting from that step so trufflehog failures
correctly fail the pipeline.
In `@llm/implementation/e2e-smoke-tests.md`:
- Around line 32-35: The previous concern about hard-coding port 18002 has been
addressed; no code change required — confirm the docs note in
llm/implementation/e2e-smoke-tests.md remains as-is and that
tests/e2e/global-setup.ts uses resolvePort() (and falls back to a random free
port) so the softened prerequisite is correct; if you want, add a short note
referencing resolvePort() to clarify behavior but do not modify test code.
In `@tests/e2e/specs/01-auth.e2e.ts`:
- Around line 97-117: The logout E2E test ("POST /api/auth/logout – returns 200
and clears session") was missing an assertion that login succeeded before using
the returned session; add an explicit check after the login request (check
loginRes.status() is 200) and only read sessionId into tempSession after that
assertion so failures in login surface instead of causing misleading downstream
401s; update the test that uses api, loginRes, tempSession, logoutRes, and
afterRes accordingly.
In `@tests/e2e/specs/02-status-peers.e2e.ts`:
- Around line 36-47: The test 'GET /api/status has valid health object'
correctly asserts the HTTP status with expect(res.status()).toBe(200) before
calling await res.json(); leave the status assertion and the rest of the test
(including checks for body.health.isConnected and
body.health.consecutiveConnectivityFailures) as-is and do not remove or reorder
the status check in this test function.
In `@tests/e2e/specs/05-admin.e2e.ts`:
- Around line 14-137: Multiple tests open a Playwright API context via
request.newContext(...) and call api.dispose() only at the end, so if an
assertion throws the context is leaked; update each test (e.g., the tests named
"GET /api/admin/api-keys returns list", "POST /api/admin/api-keys creates a new
key", "new API key can authenticate", "revoked API key returns 401", "GET
/api/admin/api-keys without auth returns 401", "GET /api/admin/users returns
user list", "GET /api/admin/whoami returns admin identity", "GET
/api/admin/users without auth returns 401") to ensure api.dispose() always runs
by wrapping the test body in try { ... } finally { await api.dispose(); } or
convert to using Playwright's test fixture/request fixture so contexts are
cleaned automatically; apply the same pattern used to fix 07-env.e2e.ts.
In `@tests/e2e/specs/06-event-log.e2e.ts`:
- Around line 32-48: The test "entries have expected shape" already includes the
status assertion for res.status() and validates entry properties when
body.entries has items, so no code change is required; keep the existing
assertions in the test function (variables: api, res, body, sessionId) as-is and
mark the change approved.
In `@tests/e2e/specs/08-ui.e2e.ts`:
- Around line 17-20: The URL assertion has been corrected to use new URL('/',
baseUrl) when asserting the SPA landed on the root after page.goto(baseUrl);
confirm that the check uses expect(page).toHaveURL(new URL('/',
baseUrl).toString()) (in the test around page.goto and the expect call) and no
further changes are required as this fixes the previous incorrect URL assertion.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 28-29: The CI step currently named "Run route tests" only runs
"bun test tests/routes" and skips unit tests under src/; update that step to run
the same command used in the release workflow (e.g., "bun run test:unit" which
runs tests in both src/ and tests/routes) so PRs run the full unit test suite;
locate the workflow step with the name "Run route tests" and replace its run
command accordingly (or run both commands) to ensure src/ tests are executed on
CI.
In `@frontend/components/ui/peer-list.tsx`:
- Around line 596-607: The toggle header div with role="button" and
onClick={handleToggle} is missing an accessible state indicator; add
aria-expanded={/* boolean state controlling the panel (e.g. isOpen, expanded,
open) */} to that div so screen readers announce collapsed/expanded state, using
the same state variable that determines the peer list visibility and keeping the
attribute updated whenever handleToggle toggles it.
In `@playwright.config.ts`:
- Around line 20-25: The Playwright config hardcodes use.baseURL =
'http://localhost:18002', which can be stale because global-setup.ts uses
resolvePort() and tests use state.baseUrl; update the Playwright config so
baseURL is derived from the same source (e.g., read process.env.TEST_BASE_URL or
export the resolved port into an env variable in global-setup.ts) or remove
use.baseURL entirely to avoid misleading defaults; ensure references to baseURL
(use.baseURL) and any test usages of page.goto('/') align with state.baseUrl or
the env var so relative navigations target the resolved port.
In `@src/class/relay.ts`:
- Line 220: Rename the snake_case parameter sub_id to camelCase subId across the
codebase: update the addSub method signature and all places that call it
(including this.addSub(sub_id, ...filters) -> this.addSub(subId, ...filters)),
adjust any related type annotations, interface definitions, and exported/public
API spots (e.g., class Relay methods and event handlers) so names remain
consistent, and run the TypeScript compiler to fix any remaining references or
imports that need renaming.
In `@src/routes/env.ts`:
- Around line 294-316: Duplicate validation logic for RELAYS, GROUP_CRED and
SHARE_CRED should be extracted into a reusable helper; create a function (e.g.,
validateCredentialPayload(body: Record<string, unknown>, validKeys: string[]) :
Response | null) that runs validateRelayUrls, validateGroup and validateShare
for keys RELAYS, GROUP_CRED, SHARE_CRED and returns a Response when validation
fails or null when OK, then call this helper from both the DB-mode and headless
branches instead of duplicating the blocks that call validateRelayUrls,
validateGroup and validateShare.
- Around line 283-286: The admin-secret extraction logic (authHeader,
bearerToken, adminSecret, validateAdminSecret) is duplicated; create a
module-local helper function (e.g., getAdminSecretFromRequest(req):
Promise<boolean> or getAdminSecret(req): string | undefined plus a call to
validateAdminSecret) near the other hasValid* closures, move the extraction
(Authorization header parsing and X-Admin-Secret fallback) into it, and then
replace both call-sites (the current usage and the `/api/env/delete` branch) to
call that helper and use its result instead of duplicating the lines.
In `@src/routes/utils.test.ts`:
- Around line 35-44: The test for normalizeRelayListForEcho is currently placed
inside the describe('getValidRelays') block; move that it(...) into its own
describe('normalizeRelayListForEcho') block so tests are grouped correctly.
Locate the it('keeps localhost relay in echo list when explicitly allowed', ...)
which calls normalizeRelayListForEcho(['ws://127.0.0.1:18002']) and cut/paste it
into a new describe wrapper (or create a sibling describe) dedicated to
normalizeRelayListForEcho, leaving the original getValidRelays describe only
containing tests for getValidRelays.
In `@src/routes/utils.ts`:
- Around line 35-38: The isLoopbackRelayHost function only checks for
'127.0.0.1' instead of the full 127.0.0.0/8 range; update it to treat any IPv4
in 127.0.0.0/8 as loopback by checking the normalized host for either
'localhost' or '::1' or an IPv4 that begins with '127.' and has four valid
octets (0-255), e.g., parse/split the normalized string on '.' and validate that
the first octet === '127' and the remaining three are integers 0–255; keep the
existing bracket normalization logic and apply the same expanded check in
isLoopbackRelayHost.
In `@tests/e2e/cosigner.mjs`:
- Line 48: The test is reaching into a private field (node.client?._filter) and
then masking missing data via JSON.stringify(... ?? '?'); update the log to
explicitly detect and surface when the private fallback is used and add a TODO
to remove the fallback once `@frostr/igloo-core` exposes a public accessor:
inspect node.client?.filter first, if missing check node.client?._filter and log
a distinct warning message (including which property was used) instead of
silently stringifying '?', and add a clear TODO referencing removal when a
public getter exists; keep the existing log content but make the fallback
explicit so future breakage is noisier.
In `@tests/e2e/global-setup.ts`:
- Around line 152-172: The function spawnDetached misleadingly sets detached:
false; either rename the function to reflect behavior (e.g., spawnAttached or
spawnChild) and update all references to spawnDetached, or change the spawn
option to detached: true if the intent is to spawn a detached process; also keep
the existing try/finally FD handling and the error handler on the returned
ChildProcess (update the function name in tests/usages where spawnDetached is
called).
- Around line 209-215: The dynamic import currently uses a fragile hardcoded
node_modules path; replace it with a Node module-resolution approach (e.g., use
createRequire from 'module' or require.resolve) to locate and import the package
so it works with hoisting/PNPM/monorepos and removes the need for the //
`@ts-ignore`. Specifically, resolve the package '@frostr/igloo-core' via
createRequire or require.resolve and then import or require it, and keep using
the same exports (generateKeysetWithSecret and decodeGroup) so the rest of the
code that references those symbols remains unchanged.
In `@tests/e2e/global-teardown.ts`:
- Around line 11-28: findLatestStateFile can pick a stale run when CI jobs run
in parallel; to improve debuggability, in the globalTeardown function log which
state file was resolved (the value returned by findLatestStateFile or the
SMOKE_STATE_FILE env var) at the top of globalTeardown before proceeding; update
globalTeardown to compute the effective state path (use
process.env.SMOKE_STATE_FILE if present otherwise call findLatestStateFile()),
then emit a clear processLogger / console log containing that resolved path (or
a message when none found) so CI logs show exactly which file was used.
In `@tests/e2e/helpers.ts`:
- Around line 4-6: The selector for usernameField is too broad because of
input[name*="ur"]; update the locator in the usernameField definition to remove
the input[name*="ur"] fragment and rely on the remaining, safer selectors (e.g.,
input[type="text"], input[id*="user"], input[name*="user"]) so the locator (in
the usernameField variable) won't accidentally match inputs like currency or
url; keep the .first() behavior but ensure the selector only includes the
user-specific fragments.
In `@tests/e2e/specs/04-sign.e2e.ts`:
- Line 19: The line test.setTimeout(30_000) in the 04-sign.e2e.ts test is
redundant because the global timeout is already set to 30_000 in
playwright.config.ts; either remove the test.setTimeout(30_000) call or keep it
but add a brief inline comment above it (e.g., "// Explicit per-test timeout for
clarity; global timeout is 30_000 in playwright.config.ts") so future readers
understand the redundancy; update the file by deleting the call or inserting the
single-line comment immediately above the test.setTimeout invocation and commit
the change.
In `@tests/e2e/specs/05-admin.e2e.ts`:
- Around line 28-42: The tests "POST /api/admin/api-keys creates a new key" and
"new API key can authenticate" create API keys but never revoke them; capture
the created apiKey.id (from body.apiKey.id) and add a cleanup step (e.g., an
afterEach or a finally block) that calls the admin revoke/delete endpoint using
the same context/sessionId to remove the key(s); implement by storing IDs in a
local array (e.g., createdKeyIds) and in afterEach iterate and call
api.delete(`/api/admin/api-keys/${id}`, { headers: { 'X-Session-ID': sessionId }
}) or the appropriate revoke route to ensure keys are removed after each test.
In `@tests/e2e/specs/07-env.e2e.ts`:
- Around line 16-98: Tests create APIRequestContext via request.newContext into
the variable "api" and call await api.dispose() at the end, but a failing
expect() will skip dispose() and leak the context; fix by using Playwright's
built-in "request" fixture (replace request.newContext(...) with the provided
"request" per-test fixture and remove manual dispose) or wrap the newContext
usage in try/finally so await api.dispose() always runs (apply to each test in
07-env.e2e.ts and similarly in 05-admin.e2e.ts, referencing the "api" variable
and the existing test blocks).
In `@tests/e2e/specs/08-ui.e2e.ts`:
- Around line 58-63: The test's locator (configContent) is too broad ('input,
textarea, [data-testid*="cred"]') and can match unrelated inputs causing false
positives; update the locator to scope it to the configure panel (e.g., target a
known ancestor like the configure tab/panel test id or a heading with text
"Configure" and then query inputs within that ancestor) or use a more specific
data-testid for configure fields, and change the assertion to use that scoped
locator (locators referenced: configContent and the configure panel's test
id/heading).
In `@tests/e2e/state.ts`:
- Line 77: Remove the redundant type assertion on result.data: since
smokeTestStateSchema.safeParse already returns the parsed type, change the
return to return result.data (drop the "as SmokeTestState" cast). Update the
return in the surrounding function where result is produced (the code calling
smokeTestStateSchema.safeParse and assigning to result.data) so it relies on the
schema-inferred type rather than casting to SmokeTestState.
In `@tests/routes/env.db-mode.spec.ts`:
- Around line 81-82: Replace the hardcoded node_modules import and inline seed:
import the generator via the package bare specifier (use
generateKeysetWithSecret from '@frostr/igloo-core') instead of the raw root +
'node_modules/.../dist/index.js', and move the hex seed into a test fixture file
(e.g. data/fixtures/test-keyset.json) and load it at test runtime (readFileSync
or equivalent) before calling generateKeysetWithSecret(2, 2, seed); keep
references to groupCredential and shareCredentials unchanged so the call site
still uses the same returned values.
In `@tests/routes/helpers/script-runner.ts`:
- Around line 34-35: The cast in buildScriptEnv that forces process.env to
Record<string,string> can silently keep undefined values; instead build nextEnv
by iterating/filtering process.env entries and only include keys whose values
are strings (e.g., via Object.entries(process.env).filter(([,v]) => typeof v ===
'string') into nextEnv) and then apply overrides, ensuring nextEnv and overrides
remain Record<string,string>; update the function buildScriptEnv to remove the
unsafe "as Record<string,string>" cast and populate nextEnv safely.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (31)
.github/workflows/ci.yml.github/workflows/release.yml.gitignorefrontend/components/ui/peer-list.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright-report/index.htmlplaywright.config.tsscripts/release.shsrc/class/relay.tssrc/routes/env.tssrc/routes/utils.test.tssrc/routes/utils.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.ts
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
tests/routes/helpers/script-runner.ts (2)
59-95:⚠️ Potential issue | 🟡 Minor
runRouteScriptis missing an explicit return type, leaking an implicitanyviaJSON.parse.
JSON.parsereturnsany, so without an explicit return type annotation the function signature silently exportsanyto all callers, undermining TypeScript strict-mode guarantees.As per coding guidelines: "TypeScript strict mode; explicit types, avoid
any".🛡️ Proposed fix
-export function runRouteScript(code: string, env: Record<string, string> = {}) { +export function runRouteScript(code: string, env: Record<string, string> = {}): unknown {Callers that need a specific shape can then narrow with a type guard or a cast at the call site rather than accepting
anysilently.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` around lines 59 - 95, The function runRouteScript currently returns the result of JSON.parse which is implicitly any; update the runRouteScript signature to include an explicit return type (e.g., Promise-like? but here sync—use unknown or a generic type T such as <T = unknown> and return T) so callers don't receive implicit any; change the return expression to parse into unknown (const parsed = JSON.parse(...) as unknown) and then return parsed as the declared return type, and update any callers to narrow or cast as needed; reference the runRouteScript function and the JSON.parse call inside it when making this change.
88-92:⚠️ Potential issue | 🟡 MinorImprove error handling for
JSON.parseon line 92.The project's TypeScript configuration (
"target": "esnext") already supportsArray.prototype.findLastwithout issues, so no lib configuration change is needed.However,
JSON.parseon line 92 can throw a bareSyntaxErrorwith no context when the parsed fragment is invalid, making test failures harder to diagnose. Add error handling to provide the problematic string in the error message:Proposed fix
- return JSON.parse(line.slice(line.indexOf(marker) + marker.length)); + const fragment = line.slice(line.indexOf(marker) + marker.length); + try { + return JSON.parse(fragment); + } catch { + throw new Error(`route script result is not valid JSON: ${fragment}`); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` around lines 88 - 92, Wrap the JSON.parse call that returns the marker payload in a try/catch to catch SyntaxError and rethrow a new Error that includes the problematic JSON substring (the result of line.slice(line.indexOf(marker) + marker.length)) plus a helpful message; locate the code that computes const line = stdout.split('\n').findLast(...) and replace the direct JSON.parse(...) return with parsing inside try/catch so that if JSON.parse fails you include the raw fragment and stdout/context in the thrown error (reference: the variables line, marker and the JSON.parse invocation).frontend/components/ui/peer-list.tsx (5)
776-780: 🛠️ Refactor suggestion | 🟠 Major
as anycast violates strict TypeScript guidelines and may be hiding a type mismatch.
policyBadgeVariantis already typedBadgeProps['variant']at line 730. Ifvariantprop on<Badge>accepts the fullBadgeProps['variant']union, the cast is redundant. If it doesn't, the cast is silently suppressing a real type error that should be resolved at the type-declaration level instead. Either way,as anyshould be removed.As per coding guidelines: "TypeScript strict mode; explicit types, avoid
any".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 776 - 780, The inline cast "as any" on the Badge variant hides a type mismatch; remove the cast and make the types align instead: update the usage in peer-list.tsx to pass policyBadgeVariant directly to <Badge variant={policyBadgeVariant} ...> and if TypeScript then errors, fix the Badge prop typing (or the policyBadgeVariant declaration) so that policyBadgeVariant is typed as BadgeProps['variant'] (or broaden Badge's variant prop to accept that union) rather than suppressing with any; adjust the declaration of policyBadgeVariant or the Badge component props to resolve the type mismatch.
523-544:⚠️ Potential issue | 🟠 Major
pingAllPeers:authHeadersmissing from deps array, andresultis unused dead code.Two issues:
- Same stale-closure pattern as
handlePingPeer—authHeadersis spread into the request (line 530) but not in the deps (line 544).- Line 537 assigns
const result = await response.json()butresultis never consumed; remove it.🐛 Proposed fix
- const result = await response.json(); - - // Refresh peer list after pinging all + // Refresh peer list after pinging all await fetchPeers(); } catch (error) { console.warn('[PeerList] Ping all failed:', error); } - }, [isSignerRunning, peers.length, fetchPeers]); + }, [isSignerRunning, peers.length, fetchPeers, authHeaders]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 523 - 544, The pingAllPeers callback has a stale-closure bug and dead code: it uses authHeaders in the fetch but doesn’t include authHeaders in the dependency array and it assigns an unused result from response.json(). Update the pingAllPeers function to include authHeaders in the dependency array (matching the pattern used by handlePingPeer) and remove the unused const result = await response.json() line; keep the await fetchPeers() call to refresh the list after the POST.
396-437:⚠️ Potential issue | 🟠 Major
authHeadersmissing fromhandlePingPeerdependency array — stale auth credentials on token rotation.
authHeadersis spread into the request headers (line 406) but absent from theuseCallbackdeps (line 437). If the parent updates auth headers (e.g. after a token refresh), this callback silently continues using the old credentials.fetchPeersandfetchSelfPubkeyboth correctly includeauthHeadersin their deps — this is an inconsistency.🐛 Proposed fix
- }, [isSignerRunning]); + }, [isSignerRunning, authHeaders]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 396 - 437, handlePingPeer is missing authHeaders in its useCallback dependency array, causing stale credentials after token rotation; update the dependency list for handlePingPeer to include authHeaders (or a stable memoized form of it) so the callback is recreated when authHeaders change, ensuring the fetch('/api/peers/ping') uses the current headers; reference the handlePingPeer function and the authHeaders variable and ensure you update the dependency array accordingly (or memoize authHeaders before use).
596-662:⚠️ Potential issue | 🟠 Major
actionswrapper at line 659 is missingonKeyDownstop propagation — keyboard Space/Enter on the Refresh button will unexpectedly toggle the panel.The newly added
onKeyDown={e => e.stopPropagation()}at line 615 correctly insulates the Tooltip trigger. However, the siblingactionswrapper (line 659) only prevents click propagation, not keyboard events. When a keyboard user focuses the RefreshIconButtonand presses Space, thekeydownevent bubbles through the unguarded div to the outer div'sonKeyDownhandler (lines 602–606), triggeringhandleToggle()as an unintended side-effect.🐛 Proposed fix
-<div onClick={e => e.stopPropagation()} className="flex-shrink-0"> +<div onClick={e => e.stopPropagation()} onKeyDown={e => e.stopPropagation()} className="flex-shrink-0"> {actions} </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 596 - 662, The actions wrapper currently only stops click propagation which allows keyboard events (Space/Enter) on its children to bubble up and trigger the parent's onKeyDown → handleToggle; update the actions container (the div wrapping the actions variable) to also stop keyboard event propagation by adding an onKeyDown handler that calls e.stopPropagation() so Space/Enter on the Refresh IconButton won't toggle the Peer List; target the div that currently has onClick={e => e.stopPropagation()} and add onKeyDown={e => e.stopPropagation()} to it.
665-669:⚠️ Potential issue | 🟠 MajorCSS-only collapse leaves interactive elements in the tab order when the panel is collapsed.
max-h-0 opacity-0only hides the content visually; all buttons and focusable elements inside remain reachable by keyboard and assistive technology. Add theinertattribute when collapsed, or conditionally unmount the content.♿ Proposed fix using the HTML `inert` attribute
<div className={cn( "transition-all duration-300 ease-in-out overflow-hidden", isExpanded ? "max-h-[400px] opacity-100" : "max-h-0 opacity-0" )} + {...(!isExpanded && { inert: '' })} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 665 - 669, The collapse currently only uses CSS (max-h-0 opacity-0) which leaves inner interactive elements focusable; update the expanded panel div in peer-list.tsx (the element using isExpanded and cn(...)) to either set the inert attribute when collapsed (e.g., inert when isExpanded is false) or conditionally unmount the inner content when !isExpanded so focusable elements are removed from the tab order; ensure you update the same div that uses isExpanded in the peer list component so keyboard/AT users cannot focus hidden controls.
♻️ Duplicate comments (1)
.github/workflows/ci.yml (1)
99-103: TruffleHog pinned to commit SHA7c0734f987ad0bb30ee8da210773b800ee2016d3(v3.93.4) — supply-chain risk addressed.The SHA
7c0734f987ad0bb30ee8da210773b800ee2016d3corresponds to TruffleHog v3.93.4, confirmed by the OpenSUSE package tracker which transitions from the prior revision to this exact commit for the 3.93.4 release. Using an immutable commit SHA is more secure than even a version tag. Past review concern is resolved.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci.yml around lines 99 - 103, This workflow currently pins the TruffleHog action to the immutable commit SHA "trufflesecurity/trufflehog@7c0734f987ad0bb30ee8da210773b800ee2016d3" which is correct and should be kept for supply-chain safety; ensure the action reference remains exactly that SHA (not a tag) in the "uses" field, keep the existing "path" and "extra_args" settings intact, and optionally add a short inline comment above the "uses" line explaining that the SHA corresponds to v3.93.4 and is intentionally pinned for security/auditability.
🧹 Nitpick comments (15)
tests/routes/helpers/script-runner.ts (1)
8-32: Add explicit type annotations (and consideras const) to satisfy the explicit-types guideline.Both constants lack type annotations. Using
as constadditionally narrows the types toreadonlytuples of string literals, preventing accidental widening or mutation.As per coding guidelines: "TypeScript strict mode; explicit types, avoid
any".♻️ Proposed fix
-const ISOLATED_ENV_KEYS = [ +const ISOLATED_ENV_KEYS = [ 'NODE_ENV', // ... 'ENV_FILE_PATH', -]; +] as const; -const ISOLATED_ENV_PREFIXES = [ +const ISOLATED_ENV_PREFIXES = [ 'RATE_LIMIT_', -]; +] as const;If
as constcauses type conflicts elsewhere (e.g.,ISOLATED_ENV_KEYSconsumed asstring[]), usereadonly string[]instead:-const ISOLATED_ENV_KEYS = [ +const ISOLATED_ENV_KEYS: readonly string[] = [🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` around lines 8 - 32, The arrays ISOLATED_ENV_KEYS and ISOLATED_ENV_PREFIXES currently have no type annotations; add explicit types to satisfy the explicit-types guideline by either appending `as const` to both declarations to make them readonly tuples of string literals (narrowing types and preventing mutation) or, if `as const` causes type conflicts where a `string[]` is expected, declare them with explicit types like `: readonly string[]` (or `: string[]` if mutation is required) so the symbols ISOLATED_ENV_KEYS and ISOLATED_ENV_PREFIXES are typed explicitly and safely.src/routes/utils.test.ts (1)
24-57: Tests for::1and127.x.x.xlook correct; consider adding alocalhosthostname case.The new tests cover both IPv6 and 127.0.0.0/8 paths through
isLoopbackRelayHost, but thenormalized === 'localhost'branch has no dedicated coverage. A test forws://localhost:18002being filtered (and allowed whenALLOW_LOCALHOST_RELAY=true) would complete the triangle.➕ Suggested additional test
+ it('filters plain localhost relay when localhost relays are disallowed', () => { + const previous = process.env.ALLOW_LOCALHOST_RELAY; + process.env.ALLOW_LOCALHOST_RELAY = 'false'; + try { + expect(getValidRelays('["ws://localhost:18002"]', { fallbackToDefault: false })).toEqual([]); + } finally { + if (previous === undefined) delete process.env.ALLOW_LOCALHOST_RELAY; + else process.env.ALLOW_LOCALHOST_RELAY = previous; + } + });Based on learnings: "Add targeted tests when behavior changes and monitor coverage for regressions."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.test.ts` around lines 24 - 57, Add tests covering the "localhost" hostname path: in src/routes/utils.test.ts add two cases similar to the existing ones that assert getValidRelays('["ws://localhost:18002"]', { fallbackToDefault: false }) returns [] when process.env.ALLOW_LOCALHOST_RELAY is set to 'false' and that normalizeRelayListForEcho(['ws://localhost:18002']) returns ['ws://localhost:18002'] when process.env.ALLOW_LOCALHOST_RELAY is 'true'; follow the same pattern as the existing tests (save/restore previous env, use try/finally) and reference getValidRelays and normalizeRelayListForEcho so the isLoopbackRelayHost branch for host === 'localhost' is exercised..github/workflows/release.yml (1)
76-80:bun run typecheckvsbun run tsc --noEmit— inconsistent type-check commands across workflows.
ci.ymlline 26 runsbun run tsc --noEmitdirectly, while this step usesbun run typecheck. If thetypecheckscript wrapstscwith extra flags or a differenttsconfig, the two pipelines check different things and one can pass while the other fails. Align both to the same command.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/release.yml around lines 76 - 80, The "Type check" step currently uses "bun run typecheck" which is inconsistent with the other workflow that runs "bun run tsc --noEmit"; update the step to use the exact same command (replace "bun run typecheck" with "bun run tsc --noEmit") or change both workflow steps to call the shared npm script that wraps tsc so both pipelines run identical type-check flags/config; reference the existing step name "Type check" and the commands "bun run typecheck" and "bun run tsc --noEmit" when making the change.src/routes/utils.ts (1)
110-110:ENV_FILE_PATHis a module-level constant — post-load env changes won't be observed.This is intentional for production and works correctly for the subprocess-isolated tests in this PR, but worth noting: any future unit test that tries to override
ENV_FILE_PATHviaprocess.envafter the module has already been imported will see the stale value. Consider readingprocess.env.ENV_FILE_PATHlazily inside the functions that use it if test isolation of the path becomes necessary.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.ts` at line 110, ENV_FILE_PATH is defined once at module load so later changes to process.env.ENV_FILE_PATH (e.g., in tests) won’t be observed; change consumers to read the env path lazily by replacing the module-level constant with a small accessor (e.g., getEnvFilePath()) that returns process.env.ENV_FILE_PATH?.trim() || '.env' and update all references that currently use ENV_FILE_PATH to call this accessor, or alternatively inline process.env.ENV_FILE_PATH?.trim() || '.env' inside the functions that use it so tests can override the value after import..github/workflows/ci.yml (1)
85-97:bun auditretry error message does not distinguish network failures from genuine vulnerabilities.Line 96's
"bun audit failed after retries"is emitted both when audit finds real vulnerabilities (expected failure) and when the audit command itself fails due to network/registry issues (spurious failure). The step will stillexit 1in both cases — correct — but the undifferentiated message can make debugging in CI logs harder.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci.yml around lines 85 - 97, The "Run security audit" step uses bun audit but prints the same message on any non-zero exit; change the retry block to capture bun audit's stdout/stderr (redirect to a temp file or variable) and after a final failure inspect the output/exit code: if it contains registry/network-related keywords (e.g., "network", "ENOTFOUND", "ECONNREFUSED", "registry") log "bun audit failed after retries due to network/registry error: <captured output>" otherwise log "bun audit failed after retries — vulnerabilities detected: <captured output>"; keep the exit 1 behavior but ensure the distinct messages reference bun audit so CI logs clearly distinguish spurious failures from real vulnerabilities.tests/e2e/global-setup.ts (1)
40-55:SetupStateis a structural duplicate ofSmokeTestState— import and reuse it.
SetupState(Lines 40–55) is field-for-field identical toSmokeTestStateexported fromtests/e2e/state.ts. Keeping two copies means they can silently diverge (e.g., adding a field to one but not the other).global-setup.tsshould importSmokeTestStateand use it directly.♻️ Proposed fix
+import type { SmokeTestState } from './state.js'; -type SetupState = { - port: number; - baseUrl: string; - tmpDir: string; - serverPid: number; - cosignerPid: number; - sessionId: string; - apiKey: string | null; - apiKeyId: string | null; - groupCredential: string; - shareCredentials: string[]; - groupPubkeyHex: string; - adminUsername: string; - adminPassword: string; - adminSecret: string; -}; +type SetupState = SmokeTestState;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 40 - 55, SetupState is a field-for-field duplicate of SmokeTestState; remove the local type declaration and import SmokeTestState from tests/e2e/state.ts, then replace usages of SetupState in this file (e.g., any type annotations or function signatures that referenced SetupState) with SmokeTestState (or import it under an alias if you prefer a local name), ensuring exports and references remain consistent.tests/e2e/state.ts (1)
25-40:smokeTestStateSchemashould beSMOKE_TEST_STATE_SCHEMAper UPPER_SNAKE_CASE convention.Module-level
constdeclarations must use UPPER_SNAKE_CASE for constants.♻️ Proposed rename
-const smokeTestStateSchema = z.object({ +const SMOKE_TEST_STATE_SCHEMA = z.object({ ... });Then update the single reference in
loadState():- const result = smokeTestStateSchema.safeParse(parsed); + const result = SMOKE_TEST_STATE_SCHEMA.safeParse(parsed);As per coding guidelines: "Use UPPER_SNAKE_CASE for constants" for
**/*.{ts,tsx}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/state.ts` around lines 25 - 40, Rename the module-level constant smokeTestStateSchema to SMOKE_TEST_STATE_SCHEMA to follow UPPER_SNAKE_CASE convention, and update any reference in loadState() to use SMOKE_TEST_STATE_SCHEMA; ensure the z.object declaration and its export/usage remain identical aside from the identifier change so validation behavior is unchanged.tests/e2e/helpers.ts (1)
3-16: No login-success verification inloginAs.After
submitBtn.click()andwaitForLoadState('networkidle'), there is no assertion that the login actually succeeded. A failed login (wrong credentials, unexpected redirect, server error) would let the helper return silently, causing downstream tests to fail with misleading "not authenticated" errors rather than a clear "login failed" message.Consider asserting navigation to the expected post-login URL or the absence of an error element:
♻️ Suggested addition
await submitBtn.click(); await page.waitForLoadState('networkidle'); + // Verify navigation away from the login page as a basic success check + await expect(page).not.toHaveURL(/\/login/);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/helpers.ts` around lines 3 - 16, The loginAs helper currently fills credentials and clicks submit but never verifies success; update loginAs to assert a successful login after await submitBtn.click() and page.waitForLoadState('networkidle') by checking a post-login indicator (e.g., expected redirect URL via page.url() or presence of a persistent selector like a logout/profile button or the absence of an error element such as '.login-error' or text "invalid"); modify the loginAs function to waitForSelector or poll for that success indicator and throw/assert with a clear "login failed" message if it does not appear within a timeout.src/routes/env.ts (1)
283-316: Duplicated admin-secret extraction and credential-validation blocks.The admin-secret extraction (Lines 283–286) is repeated verbatim at Lines 668–671. Likewise, the RELAYS/GROUP_CRED/SHARE_CRED validation block (Lines 294–316) is duplicated at Lines 365–387 for the headless path.
Extracting both into small inline helpers would keep the two write paths in sync as validation rules evolve.
♻️ Suggested extraction (example)
+ // Extracts the effective admin secret from X-Admin-Secret or Bearer token. + const extractAdminSecret = (r: Request): string | undefined => { + const h = r.headers.get('Authorization'); + const bearer = h && /^Bearer\s+/i.test(h) ? h.replace(/^Bearer\s+/i, '') : undefined; + return r.headers.get('X-Admin-Secret') ?? bearer; + }; + + // Validates RELAYS/GROUP_CRED/SHARE_CRED from a request body, returning an error Response or null. + const validateCredentialFields = ( + validKeys: string[], body: Record<string, unknown> + ): Response | null => { + if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { + const rv = validateRelayUrls(body.RELAYS); + if (!rv.valid) return Response.json({ success: false, error: rv.error }, { status: 400, headers }); + if (!rv.urls?.length) return Response.json({ success: false, error: 'At least one relay URL is required' }, { status: 400, headers }); + } + if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED !== undefined) { + if (!validateGroup(body.GROUP_CRED as string).isValid) + return Response.json({ success: false, error: 'Invalid GROUP_CRED' }, { status: 400, headers }); + } + if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED !== undefined) { + if (!validateShare(body.SHARE_CRED as string).isValid) + return Response.json({ success: false, error: 'Invalid SHARE_CRED' }, { status: 400, headers }); + } + return null; + };Replace the four duplicated blocks with calls to these helpers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 283 - 316, The admin-secret extraction and the RELAYS/GROUP_CRED/SHARE_CRED validation logic are duplicated; create small helpers (e.g., extractAdminSecret(req) that encapsulates the Authorization/X-Admin-Secret/bearer logic and returns the adminSecret and isAdmin flag by calling validateAdminSecret, and validateEnvWriteBody(body, validKeys, headers) that runs validateRelayUrls, validateGroup, validateShare and returns standardized Response-like errors) and replace both occurrences where adminSecret is computed and where the RELAYS/GROUP_CRED/SHARE_CRED checks run with calls to these helpers (use validateAdminSecret, validateRelayUrls, validateGroup, validateShare, validKeys, body, headers and Response.json inside the helpers so both write paths stay in sync).tests/e2e/specs/05-admin.e2e.ts (1)
14-139:api.dispose()is never called if an assertion throws — wrap each context intry/finally.Every test follows the pattern:
const api = await request.newContext({ baseURL: baseUrl }); // ... assertions ... await api.dispose(); // skipped on assertion failureA failed
expect()throws synchronously, soapi.dispose()at the bottom is never reached. Playwright does not automatically releaseAPIRequestContextinstances created viarequest.newContext(). Over multiple test re-runs this leaks TCP connections.♻️ Proposed fix pattern (apply to every test in this file)
test('GET /api/admin/api-keys returns list', async () => { const api = await request.newContext({ baseURL: baseUrl }); + try { const res = await api.get('/api/admin/api-keys', { headers: { 'X-Session-ID': sessionId }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty('apiKeys'); expect(Array.isArray(body.apiKeys)).toBe(true); expect(body.apiKeys.length).toBeGreaterThanOrEqual(1); + } finally { await api.dispose(); + } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/05-admin.e2e.ts` around lines 14 - 139, Each test creates an APIRequestContext with request.newContext(...) and calls await api.dispose() at the end but that call is skipped if an assertion throws; wrap the context usage in a try/finally block so dispose is always called: after creating the context (const api = await request.newContext(...)), put the test body in try { ... } and in finally { await api.dispose(); } for every test that uses request.newContext and api.dispose() (e.g., the tests creating api in "GET /api/admin/api-keys returns list", "POST /api/admin/api-keys creates a new key", "new API key can authenticate", "revoked API key returns 401", "GET /api/admin/api-keys without auth returns 401", and all Admin – Users tests).tests/e2e/specs/06-event-log.e2e.ts (2)
1-4: Seed event-log entries via fixtures instead of relying on sign side effects.This test suite assumes 04-sign produced entries; consider seeding entries in global setup (or mocking responses) to avoid cross-spec coupling and improve determinism.
Based on learnings "Applies to **/*.{test,spec}.ts?(x) : Seed complex test scenarios from data/ fixtures instead of live services".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/06-event-log.e2e.ts` around lines 1 - 4, The event-log E2E suite currently depends on side-effects from 04-sign.spec.ts; instead seed deterministic log entries via fixtures or global setup: add a fixture seeder (e.g., seedEventLogs or createEventLogFixtures) and call it from the test runner's globalSetup or from the suite's before hook in 06-event-log.e2e.ts, or mock the API responses used by the event-log UI (e.g., the event-log fetch handler) so the tests assert against known fixture data rather than relying on previous spec side effects; ensure the seeder produces the same records referenced by the suite's assertions and tear down/cleanup after tests.
9-10: Type is already inferred correctly; annotation is optional.The
loadState()function already has an explicit return type annotation (SmokeTestState), so thestatevariable's type is properly inferred by TypeScript's strict mode without requiring an additional explicit annotation. All other test specs in this directory follow the same pattern without annotation. Adding it here would be inconsistent with the existing codebase style.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/06-event-log.e2e.ts` around lines 9 - 10, Remove the redundant explicit type annotation on the local variable when calling loadState(); instead rely on TypeScript's inferred return type (SmokeTestState) from loadState() — locate the declaration using the symbol state = loadState() and remove any ": SmokeTestState" annotation so the file matches other specs that omit the explicit type.tests/e2e/global-teardown.ts (3)
43-43: Consider reusing theSmokeTestStatetype fromstate.tsinstead of a parallel inline type.The inline type
{ serverPid?: number; cosignerPid?: number; tmpDir?: string }duplicates a subset of theSmokeTestStateinterface already defined intests/e2e/state.ts. IfSmokeTestStatefields are renamed or narrowed in the future, this inline type silently diverges.♻️ Proposed refactor
At the top of the file:
+import type { SmokeTestState } from './state.js';Then at line 43:
- let state: { serverPid?: number; cosignerPid?: number; tmpDir?: string }; + let state: Partial<SmokeTestState>;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-teardown.ts` at line 43, Replace the inline ad-hoc type used for the local variable state with the existing SmokeTestState type from tests/e2e/state.ts: add an import for SmokeTestState at the top of the file and change the declaration "let state: { serverPid?: number; cosignerPid?: number; tmpDir?: string }" to "let state: SmokeTestState" (or SmokeTestState | undefined if needed) so the file reuses the canonical type and stays in sync with future changes.
67-68:if (tmpDir)is always truthy — dead guard.
tmpDiris eitherstate.tmpDir(non-empty string, perSmokeTestState) or the result ofpath.dirname(resolvedStateFile), which always returns a non-empty string. Theifnever evaluates tofalseand can be removed.♻️ Proposed fix
- const tmpDir = state.tmpDir || path.dirname(resolvedStateFile); - if (tmpDir) { - try { + const tmpDir = state.tmpDir || path.dirname(resolvedStateFile); + try { const resolvedTmp = path.resolve(tmpDir); ... - } catch (err) { - console.warn('[teardown] Could not remove temp dir:', err); - } - } + } catch (err) { + console.warn('[teardown] Could not remove temp dir:', err); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-teardown.ts` around lines 67 - 68, The guard "if (tmpDir)" around the tmpDir usage is redundant because tmpDir is always a non-empty string (either state.tmpDir or path.dirname(resolvedStateFile)); remove the if and un-indent its body so code runs directly using tmpDir; update any early-returns or error handling accordingly and keep references to the variables tmpDir, state.tmpDir and resolvedStateFile unchanged.
11-28:findLatestStateFilecan throw uncaught, aborting teardown before any cleanup runs.
fs.readdirSync(tmpRoot)(line 16) has no error handling. On a permission error or other rare fs issue, the exception would propagate out ofglobalTeardown, leaving both child processes alive and the temp directory on disk.🛡️ Proposed fix
function findLatestStateFile(): string | null { const tmpRoot = os.tmpdir(); let latestFile: string | null = null; let latestMtime = 0; - for (const entry of fs.readdirSync(tmpRoot, { withFileTypes: true })) { + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(tmpRoot, { withFileTypes: true }); + } catch { + return null; + } + for (const entry of entries) { if (!entry.isDirectory() || !entry.name.startsWith('igloo-smoke-test')) continue;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-teardown.ts` around lines 11 - 28, The findLatestStateFile function can throw from fs.readdirSync or fs.statSync and abort teardown; wrap the directory read and file stats in try/catch so any FS errors are caught and the function returns null (or otherwise fails gracefully) instead of throwing. Specifically, catch errors around the call to fs.readdirSync(tmpRoot) and around fs.statSync(candidate) (or wrap the whole loop) in findLatestStateFile, log or ignore the error, and return null so globalTeardown can continue cleanup even when permissions/FS errors occur.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@llm/implementation/e2e-smoke-tests.md`:
- Around line 271-273: The "new API key can authenticate" test currently hits
the public GET /api/status endpoint so it doesn't exercise auth; update the test
in 05-admin.e2e.ts (the "new API key can authenticate" test case) to call the
protected GET /api/event-log endpoint instead and send the same API key auth
header used elsewhere (matching how the "revoked API key returns 401" test
constructs its request) so the middleware actually validates the new key.
In `@llm/implementation/node-lifecycle-implementation.md`:
- Line 64: The documentation sentence incorrectly states that setting either
NODE_ALLOW_BENIGN_PUBLISH_SWALLOW or RELAY_ALLOW_BENIGN_SWALLOW to false forces
publish errors to surface; update the text to reflect the actual fallback logic
used by the implementation (the nullish coalescing behavior in the code that
reads NODE_ALLOW_BENIGN_PUBLISH_SWALLOW ?? RELAY_ALLOW_BENIGN_SWALLOW), i.e.,
make clear that RELAY_ALLOW_BENIGN_SWALLOW is only consulted when
NODE_ALLOW_BENIGN_PUBLISH_SWALLOW is undefined (unset) and that any explicit
value on NODE_ALLOW_BENIGN_PUBLISH_SWALLOW — including true or false — takes
precedence.
In `@tests/e2e/cosigner.mjs`:
- Around line 55-64: The listeners for 'bounced', '/sign/handler/rej', and
'subscribed' use JSON.stringify (in the node.on callbacks) which can throw on
circular structures; change those calls to use the locally-defined safeStringify
function (defined earlier as safeStringify) instead of JSON.stringify and keep
the existing .slice(...) truncation logic; ensure you reference
safeStringify(...) in the callbacks for node.on('bounced', ...),
node.on('/sign/handler/rej', ...), and node.on('subscribed', ...) so circular
refs won’t raise TypeError during tests.
In `@tests/e2e/global-setup.ts`:
- Around line 35-38: Replace the hardcoded fallback secrets in the setup file by
loading them from a committed data fixture: create
tests/data/smoke-test-defaults.json with safe default values (non-production),
add/confirm the fixture in git (and update .gitignore if needed to avoid other
secrets), then change the constants TEST_NSEC_HEX, ADMIN_SECRET, ADMIN_USERNAME,
and ADMIN_PASSWORD to read from that JSON (fall back only to values from the
fixture, not string literals) so no credential strings remain inline in
tests/e2e/global-setup.ts.
In `@tests/e2e/specs/04-sign.e2e.ts`:
- Around line 11-13: Annotate the test's loaded state and the event payloads
with explicit types: import and use the SmokeTestState type when declaring state
(replace the untyped const state = loadState() with const state: SmokeTestState
= loadState()), and add a SignEventPayload type for the event objects used in
the test (used by the variables event and invalidEvent around the blocks at
lines ~78–87 and ~114–125) with fields pubkey, kind, created_at, content, and
tags (string[][]) and annotate those constants as SignEventPayload to improve
clarity and maintainability.
In `@tests/e2e/specs/08-ui.e2e.ts`:
- Around line 12-13: Add an explicit type annotation for the loaded state by
changing the declaration that calls loadState() to use the SmokeTestState
interface (i.e., const state: SmokeTestState = loadState()) and import
SmokeTestState from ../state.js if not already imported; update any usages of
baseUrl, adminUsername, adminPassword as needed to satisfy the typed state.
In `@tests/routes/env.db-mode.spec.ts`:
- Around line 80-82: Replace the hardcoded import path that uses root +
'node_modules/@frostr/igloo-core/dist/index.js' with a bare module specifier
import from '@frostr/igloo-core' so package exports and resolution are
respected; update the dynamic import that assigns generateKeysetWithSecret to
import('@frostr/igloo-core') and then call generateKeysetWithSecret(2, 2,
'deadbeef...') as before (this keeps validateGroup/validateShare generation
working and matches other uses like in tests/e2e/global-setup.ts and any
runRouteScript subprocess resolution).
---
Outside diff comments:
In `@frontend/components/ui/peer-list.tsx`:
- Around line 776-780: The inline cast "as any" on the Badge variant hides a
type mismatch; remove the cast and make the types align instead: update the
usage in peer-list.tsx to pass policyBadgeVariant directly to <Badge
variant={policyBadgeVariant} ...> and if TypeScript then errors, fix the Badge
prop typing (or the policyBadgeVariant declaration) so that policyBadgeVariant
is typed as BadgeProps['variant'] (or broaden Badge's variant prop to accept
that union) rather than suppressing with any; adjust the declaration of
policyBadgeVariant or the Badge component props to resolve the type mismatch.
- Around line 523-544: The pingAllPeers callback has a stale-closure bug and
dead code: it uses authHeaders in the fetch but doesn’t include authHeaders in
the dependency array and it assigns an unused result from response.json().
Update the pingAllPeers function to include authHeaders in the dependency array
(matching the pattern used by handlePingPeer) and remove the unused const result
= await response.json() line; keep the await fetchPeers() call to refresh the
list after the POST.
- Around line 396-437: handlePingPeer is missing authHeaders in its useCallback
dependency array, causing stale credentials after token rotation; update the
dependency list for handlePingPeer to include authHeaders (or a stable memoized
form of it) so the callback is recreated when authHeaders change, ensuring the
fetch('/api/peers/ping') uses the current headers; reference the handlePingPeer
function and the authHeaders variable and ensure you update the dependency array
accordingly (or memoize authHeaders before use).
- Around line 596-662: The actions wrapper currently only stops click
propagation which allows keyboard events (Space/Enter) on its children to bubble
up and trigger the parent's onKeyDown → handleToggle; update the actions
container (the div wrapping the actions variable) to also stop keyboard event
propagation by adding an onKeyDown handler that calls e.stopPropagation() so
Space/Enter on the Refresh IconButton won't toggle the Peer List; target the div
that currently has onClick={e => e.stopPropagation()} and add onKeyDown={e =>
e.stopPropagation()} to it.
- Around line 665-669: The collapse currently only uses CSS (max-h-0 opacity-0)
which leaves inner interactive elements focusable; update the expanded panel div
in peer-list.tsx (the element using isExpanded and cn(...)) to either set the
inert attribute when collapsed (e.g., inert when isExpanded is false) or
conditionally unmount the inner content when !isExpanded so focusable elements
are removed from the tab order; ensure you update the same div that uses
isExpanded in the peer list component so keyboard/AT users cannot focus hidden
controls.
In `@tests/routes/helpers/script-runner.ts`:
- Around line 59-95: The function runRouteScript currently returns the result of
JSON.parse which is implicitly any; update the runRouteScript signature to
include an explicit return type (e.g., Promise-like? but here sync—use unknown
or a generic type T such as <T = unknown> and return T) so callers don't receive
implicit any; change the return expression to parse into unknown (const parsed =
JSON.parse(...) as unknown) and then return parsed as the declared return type,
and update any callers to narrow or cast as needed; reference the runRouteScript
function and the JSON.parse call inside it when making this change.
- Around line 88-92: Wrap the JSON.parse call that returns the marker payload in
a try/catch to catch SyntaxError and rethrow a new Error that includes the
problematic JSON substring (the result of line.slice(line.indexOf(marker) +
marker.length)) plus a helpful message; locate the code that computes const line
= stdout.split('\n').findLast(...) and replace the direct JSON.parse(...) return
with parsing inside try/catch so that if JSON.parse fails you include the raw
fragment and stdout/context in the thrown error (reference: the variables line,
marker and the JSON.parse invocation).
---
Duplicate comments:
In @.github/workflows/ci.yml:
- Around line 99-103: This workflow currently pins the TruffleHog action to the
immutable commit SHA
"trufflesecurity/trufflehog@7c0734f987ad0bb30ee8da210773b800ee2016d3" which is
correct and should be kept for supply-chain safety; ensure the action reference
remains exactly that SHA (not a tag) in the "uses" field, keep the existing
"path" and "extra_args" settings intact, and optionally add a short inline
comment above the "uses" line explaining that the SHA corresponds to v3.93.4 and
is intentionally pinned for security/auditability.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 85-97: The "Run security audit" step uses bun audit but prints the
same message on any non-zero exit; change the retry block to capture bun audit's
stdout/stderr (redirect to a temp file or variable) and after a final failure
inspect the output/exit code: if it contains registry/network-related keywords
(e.g., "network", "ENOTFOUND", "ECONNREFUSED", "registry") log "bun audit failed
after retries due to network/registry error: <captured output>" otherwise log
"bun audit failed after retries — vulnerabilities detected: <captured output>";
keep the exit 1 behavior but ensure the distinct messages reference bun audit so
CI logs clearly distinguish spurious failures from real vulnerabilities.
In @.github/workflows/release.yml:
- Around line 76-80: The "Type check" step currently uses "bun run typecheck"
which is inconsistent with the other workflow that runs "bun run tsc --noEmit";
update the step to use the exact same command (replace "bun run typecheck" with
"bun run tsc --noEmit") or change both workflow steps to call the shared npm
script that wraps tsc so both pipelines run identical type-check flags/config;
reference the existing step name "Type check" and the commands "bun run
typecheck" and "bun run tsc --noEmit" when making the change.
In `@src/routes/env.ts`:
- Around line 283-316: The admin-secret extraction and the
RELAYS/GROUP_CRED/SHARE_CRED validation logic are duplicated; create small
helpers (e.g., extractAdminSecret(req) that encapsulates the
Authorization/X-Admin-Secret/bearer logic and returns the adminSecret and
isAdmin flag by calling validateAdminSecret, and validateEnvWriteBody(body,
validKeys, headers) that runs validateRelayUrls, validateGroup, validateShare
and returns standardized Response-like errors) and replace both occurrences
where adminSecret is computed and where the RELAYS/GROUP_CRED/SHARE_CRED checks
run with calls to these helpers (use validateAdminSecret, validateRelayUrls,
validateGroup, validateShare, validKeys, body, headers and Response.json inside
the helpers so both write paths stay in sync).
In `@src/routes/utils.test.ts`:
- Around line 24-57: Add tests covering the "localhost" hostname path: in
src/routes/utils.test.ts add two cases similar to the existing ones that assert
getValidRelays('["ws://localhost:18002"]', { fallbackToDefault: false }) returns
[] when process.env.ALLOW_LOCALHOST_RELAY is set to 'false' and that
normalizeRelayListForEcho(['ws://localhost:18002']) returns
['ws://localhost:18002'] when process.env.ALLOW_LOCALHOST_RELAY is 'true';
follow the same pattern as the existing tests (save/restore previous env, use
try/finally) and reference getValidRelays and normalizeRelayListForEcho so the
isLoopbackRelayHost branch for host === 'localhost' is exercised.
In `@src/routes/utils.ts`:
- Line 110: ENV_FILE_PATH is defined once at module load so later changes to
process.env.ENV_FILE_PATH (e.g., in tests) won’t be observed; change consumers
to read the env path lazily by replacing the module-level constant with a small
accessor (e.g., getEnvFilePath()) that returns process.env.ENV_FILE_PATH?.trim()
|| '.env' and update all references that currently use ENV_FILE_PATH to call
this accessor, or alternatively inline process.env.ENV_FILE_PATH?.trim() ||
'.env' inside the functions that use it so tests can override the value after
import.
In `@tests/e2e/global-setup.ts`:
- Around line 40-55: SetupState is a field-for-field duplicate of
SmokeTestState; remove the local type declaration and import SmokeTestState from
tests/e2e/state.ts, then replace usages of SetupState in this file (e.g., any
type annotations or function signatures that referenced SetupState) with
SmokeTestState (or import it under an alias if you prefer a local name),
ensuring exports and references remain consistent.
In `@tests/e2e/global-teardown.ts`:
- Line 43: Replace the inline ad-hoc type used for the local variable state with
the existing SmokeTestState type from tests/e2e/state.ts: add an import for
SmokeTestState at the top of the file and change the declaration "let state: {
serverPid?: number; cosignerPid?: number; tmpDir?: string }" to "let state:
SmokeTestState" (or SmokeTestState | undefined if needed) so the file reuses the
canonical type and stays in sync with future changes.
- Around line 67-68: The guard "if (tmpDir)" around the tmpDir usage is
redundant because tmpDir is always a non-empty string (either state.tmpDir or
path.dirname(resolvedStateFile)); remove the if and un-indent its body so code
runs directly using tmpDir; update any early-returns or error handling
accordingly and keep references to the variables tmpDir, state.tmpDir and
resolvedStateFile unchanged.
- Around line 11-28: The findLatestStateFile function can throw from
fs.readdirSync or fs.statSync and abort teardown; wrap the directory read and
file stats in try/catch so any FS errors are caught and the function returns
null (or otherwise fails gracefully) instead of throwing. Specifically, catch
errors around the call to fs.readdirSync(tmpRoot) and around
fs.statSync(candidate) (or wrap the whole loop) in findLatestStateFile, log or
ignore the error, and return null so globalTeardown can continue cleanup even
when permissions/FS errors occur.
In `@tests/e2e/helpers.ts`:
- Around line 3-16: The loginAs helper currently fills credentials and clicks
submit but never verifies success; update loginAs to assert a successful login
after await submitBtn.click() and page.waitForLoadState('networkidle') by
checking a post-login indicator (e.g., expected redirect URL via page.url() or
presence of a persistent selector like a logout/profile button or the absence of
an error element such as '.login-error' or text "invalid"); modify the loginAs
function to waitForSelector or poll for that success indicator and throw/assert
with a clear "login failed" message if it does not appear within a timeout.
In `@tests/e2e/specs/05-admin.e2e.ts`:
- Around line 14-139: Each test creates an APIRequestContext with
request.newContext(...) and calls await api.dispose() at the end but that call
is skipped if an assertion throws; wrap the context usage in a try/finally block
so dispose is always called: after creating the context (const api = await
request.newContext(...)), put the test body in try { ... } and in finally {
await api.dispose(); } for every test that uses request.newContext and
api.dispose() (e.g., the tests creating api in "GET /api/admin/api-keys returns
list", "POST /api/admin/api-keys creates a new key", "new API key can
authenticate", "revoked API key returns 401", "GET /api/admin/api-keys without
auth returns 401", and all Admin – Users tests).
In `@tests/e2e/specs/06-event-log.e2e.ts`:
- Around line 1-4: The event-log E2E suite currently depends on side-effects
from 04-sign.spec.ts; instead seed deterministic log entries via fixtures or
global setup: add a fixture seeder (e.g., seedEventLogs or
createEventLogFixtures) and call it from the test runner's globalSetup or from
the suite's before hook in 06-event-log.e2e.ts, or mock the API responses used
by the event-log UI (e.g., the event-log fetch handler) so the tests assert
against known fixture data rather than relying on previous spec side effects;
ensure the seeder produces the same records referenced by the suite's assertions
and tear down/cleanup after tests.
- Around line 9-10: Remove the redundant explicit type annotation on the local
variable when calling loadState(); instead rely on TypeScript's inferred return
type (SmokeTestState) from loadState() — locate the declaration using the symbol
state = loadState() and remove any ": SmokeTestState" annotation so the file
matches other specs that omit the explicit type.
In `@tests/e2e/state.ts`:
- Around line 25-40: Rename the module-level constant smokeTestStateSchema to
SMOKE_TEST_STATE_SCHEMA to follow UPPER_SNAKE_CASE convention, and update any
reference in loadState() to use SMOKE_TEST_STATE_SCHEMA; ensure the z.object
declaration and its export/usage remain identical aside from the identifier
change so validation behavior is unchanged.
In `@tests/routes/helpers/script-runner.ts`:
- Around line 8-32: The arrays ISOLATED_ENV_KEYS and ISOLATED_ENV_PREFIXES
currently have no type annotations; add explicit types to satisfy the
explicit-types guideline by either appending `as const` to both declarations to
make them readonly tuples of string literals (narrowing types and preventing
mutation) or, if `as const` causes type conflicts where a `string[]` is
expected, declare them with explicit types like `: readonly string[]` (or `:
string[]` if mutation is required) so the symbols ISOLATED_ENV_KEYS and
ISOLATED_ENV_PREFIXES are typed explicitly and safely.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (31)
.github/workflows/ci.yml.github/workflows/release.yml.gitignorefrontend/components/ui/peer-list.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright-report/index.htmlplaywright.config.tsscripts/release.shsrc/class/relay.tssrc/routes/env.tssrc/routes/utils.test.tssrc/routes/utils.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.ts
…put, and E2E scaffolding
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (5)
llm/implementation/node-lifecycle-implementation.md (1)
64-64: Documentation now accurately describes the??fallback asymmetry.The revised wording correctly conveys that
RELAY_ALLOW_BENIGN_SWALLOWis only consulted whenNODE_ALLOW_BENIGN_PUBLISH_SWALLOWis unset, and that any explicit value on the primary flag takes precedence. This fully resolves the previous review concern.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@llm/implementation/node-lifecycle-implementation.md` at line 64, Update the documentation text to state that NODE_ALLOW_BENIGN_PUBLISH_SWALLOW is authoritative and RELAY_ALLOW_BENIGN_SWALLOW is only used as a fallback when the primary flag is unset; specifically change the sentence to make the asymmetry explicit and show the evaluation expression as NODE_ALLOW_BENIGN_PUBLISH_SWALLOW ?? RELAY_ALLOW_BENIGN_SWALLOW, and mention that any explicit value (true or false) on NODE_ALLOW_BENIGN_PUBLISH_SWALLOW preempts RELAY_ALLOW_BENIGN_SWALLOW so readers of node-lifecycle-implementation.md understand the precedence.llm/implementation/e2e-smoke-tests.md (1)
32-35: Port prerequisite, sign response fields, and API-key auth endpoint — all three previous concerns resolved.
- Line 35 now correctly notes the
resolvePort()fallback while warning about hard-coded references.- Line 195 correctly documents
idandsignatureas the sign response fields.- Lines 207–208 and 272–273 confirm the "new API key authenticates" test uses
GET /api/event-log(an authenticated endpoint), not the public/api/status.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@llm/implementation/e2e-smoke-tests.md` around lines 32 - 35, Update the e2e-smoke-tests documentation to explicitly note the resolvePort() fallback behavior and warn about hard-coded port usages (refer to resolvePort() and tests/e2e/global-setup.ts), ensure the sign response fields are documented as id and signature (refer to the documented sign response section where id/signature are listed), and confirm the "new API key authenticates" test targets the authenticated endpoint GET /api/event-log (not /api/status) so the test verifies API-key auth semantics.llm/implementation/umbrel-implementation.md (1)
69-74: Path inconsistency resolved; operational context well-documented.Line 74 now consistently uses
igloo-server/docker-compose.yml(matching the reference at line 27), and lines 69-70 add useful context about the digest-pin rationale. Previous review concern fully addressed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@llm/implementation/umbrel-implementation.md` around lines 69 - 74, The documentation updated the path reference but ensure the text and checklist consistently reference the correct compose files: keep "igloo-server/docker-compose.yml" as the file that must be updated with the new `@sha256` digest while retaining the :umbrel-dev tag, and explicitly note that "packages/umbrel/igloo/docker-compose.yml" is the sideload/dev bundle that points to :umbrel-dev without a pinned digest; update any remaining mentions (e.g., the two docker-compose.yml references and the checklist steps) so they uniformly name these two unique artifacts ("igloo-server/docker-compose.yml" and "packages/umbrel/igloo/docker-compose.yml") and clarify the workflow for building/pushing :umbrel-<version> and :umbrel-latest and where to change only the digest.tests/e2e/state.ts (1)
1-82: State loader is well-structured;zoddependency concern from prior review appears resolved.The
safeParse+ structured error messages pattern is solid, and theSTUBshort-circuit correctly avoids schema validation during Playwright discovery. The previous review flaggedzodas absent frompackage.json; the library context now confirmszod@3.25.76is present, so that concern is addressed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/state.ts` around lines 1 - 82, The state loader is correct and no changes are required; keep the STUB constant and the loadState function as-is (including SMOKE_TEST_STATE_SCHEMA, safeParse usage, and the error construction that references stateFile) since zod dependency is present and validation/error messaging for SMOKE_TEST_STATE_SCHEMA and loadState is already handled properly.tests/routes/env.db-mode.spec.ts (1)
80-86:createRequireapproach correctly handles the post-chdirimport problem.After
process.chdir(tmp)at line 61, a bareimport('@frostr/igloo-core')specifier would fail in Bun's evaluated script context because CWD-relative module resolution would look in the temp directory. AnchoringcreateRequiretoroot + 'package.json'and resolving the absolute path before callingimport()is the correct workaround here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/env.db-mode.spec.ts` around lines 80 - 86, The current code must continue using createRequire anchored to root + 'package.json' to avoid post-chdir import failures: use createRequire to build requireFromRoot, call requireFromRoot.resolve('@frostr/igloo-core') to get iglooCorePath, then dynamic import(iglooCorePath) and invoke generateKeysetWithSecret to produce groupCredential and shareCredentials; keep these exact symbols (createRequire, requireFromRoot, iglooCorePath, generateKeysetWithSecret) and the resolution-before-import pattern as implemented.
🧹 Nitpick comments (13)
tests/routes/helpers/script-runner.ts (1)
63-63: ConsiderT = unknowninstead ofT = anyto align with guidelines.The coding guidelines require avoiding
any. Usingunknownas the default generic parameter would enforce type narrowing at call sites, which is safer. Callers that need flexibility can still provide an explicit type argument.-export function runRouteScript<T = any>(code: string, env: Record<string, string> = {}): T { +export function runRouteScript<T = unknown>(code: string, env: Record<string, string> = {}): T {As per coding guidelines:
**/*.{ts,tsx}: "Enable TypeScript strict mode, declare explicit types, and avoid any."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` at line 63, The generic default on runRouteScript uses T = any; change it to T = unknown to comply with strict typing rules—update the function signature export function runRouteScript<T = unknown>(code: string, env: Record<string, string> = {}): T and ensure callers that rely on implicit any provide an explicit type or perform proper narrowing/casting where they consume the return value (reference: runRouteScript).src/routes/utils.ts (1)
817-821: Redundant protocol check insidenormalizeRelayListForEcho.
validateRelayUrls()already short-circuits with{ valid: false }on the first non-ws/wss relay, so by the time execution reaches this inner filter,validation.urlsis guaranteed to contain onlyws:/wss:URLs. The protocol re-check on line 819 is dead code.♻️ Proposed cleanup
.filter((r) => { try { const u = new URL(r); - if (u.protocol !== 'ws:' && u.protocol !== 'wss:') return false; if (!allowLocalhost && isLoopbackRelayHost(u.hostname)) return false; return true; } catch { return false; } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.ts` around lines 817 - 821, The inner protocol check in normalizeRelayListForEcho is redundant because validateRelayUrls already filters out non-ws/wss URLs; remove the u.protocol !== 'ws:' && u.protocol !== 'wss:' condition from the inner filter (the try block that constructs new URL(r) and checks allowLocalhost/isLoopbackRelayHost) so the filter only enforces localhost/loopback rules and returns true for valid entries, keeping validateRelayUrls as the sole protocol validator.src/routes/utils.test.ts (1)
58-79:normalizeRelayListForEchotests only cover the "allow" path — add a filter-path test.Both
getValidRelaysandnormalizeRelayListForEchoshare the sameALLOW_LOCALHOST_RELAYguard, but the newnormalizeRelayListForEchodescribe block only exercisesALLOW_LOCALHOST_RELAY=true. The default (false) filter path fornormalizeRelayListForEchogoes untested, which means a regression inisLoopbackRelayHostwould only be caught by thegetValidRelaystests.♻️ Suggested addition
+ it('filters loopback relay in echo list when localhost relays are disallowed', () => { + const previous = process.env.ALLOW_LOCALHOST_RELAY; + process.env.ALLOW_LOCALHOST_RELAY = 'false'; + try { + expect(normalizeRelayListForEcho(['ws://127.0.0.1:18002'])).toBeUndefined(); + } finally { + if (previous === undefined) delete process.env.ALLOW_LOCALHOST_RELAY; + else process.env.ALLOW_LOCALHOST_RELAY = previous; + } + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.test.ts` around lines 58 - 79, Add a test in the normalizeRelayListForEcho describe block that covers the filter path when ALLOW_LOCALHOST_RELAY is not enabled: temporarily ensure process.env.ALLOW_LOCALHOST_RELAY is undefined or 'false', call normalizeRelayListForEcho with localhost/127.0.0.1 relay URLs (e.g. 'ws://127.0.0.1:18002' and 'ws://localhost:18002') plus a non-loopback relay, assert the returned array does not include the loopback entries but keeps the non-loopback one, and restore the original ALLOW_LOCALHOST_RELAY value in a finally block; reference normalizeRelayListForEcho (and the underlying isLoopbackRelayHost behavior) when adding the assertion..github/workflows/ci.yml (2)
87-107: Audit retry loop is pragmatic; consider preserving the last exit code/message for faster triageThe retry + network/vuln messaging looks good. One small improvement: echo the final
bun auditexit code (and possibly the attempt count) so failures can be grepped quickly in CI logs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci.yml around lines 87 - 107, The CI audit retry block should preserve and emit the final bun audit exit code and attempt count for easier grepping; capture the exit code after the failing bun audit invocation (use the audit_log and the loop variable attempt), store it (e.g., last_exit or last_code) each loop iteration when bun audit fails, and after retries echo something like "bun audit failed after X attempts with exit code Y" along with the existing network/vuln message so the final exit code and attempt count are visible in CI logs before rm -f "$audit_log" and exit 1.
28-30: Good: unit/route tests now run in CI after typecheckThis should catch backend regressions earlier. Consider renaming the step to match the script scope (
test:unitruns more than routes) to reduce confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci.yml around lines 28 - 30, The CI step currently labeled "name: Run route tests" runs the broader script "run: bun run test:unit", which is misleading; update the step's name to accurately reflect the script scope (e.g., "Run unit tests" or "Run test:unit") so it matches the executed command. Locate the step with the manifest "name: Run route tests" and change only the name to match "bun run test:unit" without altering the existing "run: bun run test:unit" line.tests/e2e/specs/05-admin.e2e.ts (1)
38-101: Consider cleaning up keys created in tests (and making labels unique) to keep smoke runs idempotent
POST /api/admin/api-keys creates a new keyandnew API key can authenticatecurrently leave keys behind. If the backend ever enforces max key counts or label uniqueness, reruns can get flaky. A simple pattern is to append a unique suffix and revoke in afinally(like you already do in the revoke test).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/05-admin.e2e.ts` around lines 38 - 101, Tests "POST /api/admin/api-keys creates a new key" and "new API key can authenticate" create API keys but don't clean them up; change those tests to generate unique labels (e.g., append timestamp/uuid) when calling api.post('/api/admin/api-keys') and store the returned apiKey, then revoke that key in a finally block by calling api.post('/api/admin/api-keys/revoke') with { apiKeyId: apiKey.id, reason: 'test cleanup' } so keys are always removed; use the existing withApi helper and the same endpoints to locate and update the logic in those two tests.tests/e2e/specs/03-nip44-nip04.e2e.ts (2)
18-81: Ensure API contexts are disposed even when assertions fail (wrap in try/finally or reusewithApi)Right now
await api.dispose()is after assertions; if anexpect()throws, the context won’t be disposed. Reusing thewithApi()helper (or a shared one intests/e2e/) would fix this and reduce duplication.Also applies to: 86-149
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/03-nip44-nip04.e2e.ts` around lines 18 - 81, Tests create Playwright API contexts with request.newContext and call await api.dispose() after assertions, so a failing expect prevents disposal; update each test (e.g., the 'returns 401 without auth', 'encrypt returns ciphertext', 'encrypt then decrypt round-trips plaintext', 'invalid peer_pubkey returns 400', 'missing content returns 400' cases) to always dispose the context by either wrapping the request.newContext usage in try/finally (call await api.dispose() in finally) or replace with the existing withApi helper to manage lifecycle, ensuring all references to api.dispose are removed or only in the finally and keeping headers/sessionId/baseUrl usage unchanged.
18-150: Confirm policy: these tests intentionally hit/api/nip44/*and/api/nip04/*rather than mockingThese are valid smoke/e2e targets, but it conflicts with prior repo guidance to “mock external calls” to these endpoints in tests. If the intention is “unit tests should mock, but e2e may hit real endpoints”, it’d be good to codify that (e.g., doc note or naming), and keep them scoped to
nightlyif they’re heavier/flakier. Based on learnings: Applies to **/.{test,spec}.ts?(x) : In tests, mock external calls to /api/sign, /api/nip44/, /api/nip04/, and /api/nip46/.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/03-nip44-nip04.e2e.ts` around lines 18 - 150, Tests in tests/e2e/specs/03-nip44-nip04.e2e.ts are making real network calls to /api/nip44/* and /api/nip04/* inside the test.describe blocks ("NIP-44 – /api/nip44" and "NIP-04 – /api/nip04"), which conflicts with the repo policy to mock those external endpoints; update these tests to stub/mock requests to /api/sign, /api/nip44/*, /api/nip04/* and /api/nip46/* (e.g., intercept the .post calls created via request.newContext and return canned responses for encrypt/decrypt and error cases) or move the file to a nightly/e2e-only suite and add a doc note clarifying that e2e tests may hit real services, ensuring the tests no longer perform uncontrolled external network requests while preserving the current assertions (status codes and response shapes).src/routes/env.ts (2)
283-287: Trim/normalize Bearer extraction and avoid emptyX-Admin-Secretoverriding a valid Bearer tokenTwo small edge cases:
Authorization: Bearer <token>(extra whitespace) will fail without.trim().- An empty
X-Admin-Secret:header will “win” over a valid Bearer token because??doesn’t treat''as absent.Proposed fix (apply in both locations)
- const authHeader = req.headers.get('Authorization'); - const bearerToken = authHeader && /^Bearer\s+/i.test(authHeader) ? authHeader.replace(/^Bearer\s+/i, '') : undefined; - const adminSecret = req.headers.get('X-Admin-Secret') ?? bearerToken; + const authHeader = req.headers.get('Authorization'); + const bearerToken = authHeader && /^Bearer\s+/i.test(authHeader) + ? authHeader.replace(/^Bearer\s+/i, '').trim() + : undefined; + const headerSecretRaw = req.headers.get('X-Admin-Secret'); + const headerSecret = headerSecretRaw && headerSecretRaw.trim().length > 0 + ? headerSecretRaw.trim() + : undefined; + const adminSecret = headerSecret || bearerToken; const isAdminSecret = await validateAdminSecret(adminSecret ?? undefined);Also applies to: 668-672
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 283 - 287, Trim and normalize the extracted Bearer token and ensure an empty X-Admin-Secret header doesn't override a valid Bearer token: when reading Authorization, call .trim() on the extracted token (authHeader and bearerToken logic) so "Bearer token " works; when selecting adminSecret, treat X-Admin-Secret as absent if it's null/undefined or an empty/whitespace-only string (e.g. read req.headers.get('X-Admin-Secret') into a trimmed variable and only use it if non-empty), otherwise fall back to bearerToken and then pass that into validateAdminSecret; apply the same change at both occurrences that use authHeader, bearerToken, adminSecret and validateAdminSecret.
294-317: Good: validation-first for RELAYS / GROUP_CRED / SHARE_CRED; consider deduping the duplicated validation blockNice guardrails (including “at least one relay”). Since the DB-mode and headless-mode blocks are nearly identical, consider extracting a small helper to keep the rules from drifting (and to centralize any future “normalize before write” logic).
Also applies to: 370-387
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 294 - 317, The three similar validation blocks for RELAYS / GROUP_CRED / SHARE_CRED duplicate logic; extract a helper (e.g., validateAndRespond) that takes the key name, the input value (body.RELAYS / body.GROUP_CRED / body.SHARE_CRED), the validator function (validateRelayUrls / validateGroup / validateShare) and the error messages, so you run common checks (call validator, check .valid/.isValid, ensure non-empty relay URLs) and return the same Response.json on failure; replace the three blocks with calls to that helper and use it for the other duplicate block elsewhere to centralize any future normalize-before-write logic.tests/e2e/helpers.ts (1)
4-21: RemovewaitForLoadState('networkidle')in favour of explicit UI assertion
waitForLoadState('networkidle')is explicitly discouraged by Playwright because it waits for no network activity for 500ms—a state that never arrives in apps with WebSockets, SSE, or long-polling. The code already has a deterministic post-login element assertion with timeout, which is the recommended approach and sufficient for test readiness.Also consider migrating selectors from
locator()with complex CSS patterns togetByRole()(for interactive elements),getByLabel()(for form inputs), orgetByTestId()for test stability.Proposed tweak
await passwordField.fill(password); await submitBtn.click(); - await page.waitForLoadState('networkidle'); await expect( page.locator('[role="tab"], .tab, button:has-text("Signer"), button:has-text("Configure")').first(), 'login failed: expected dashboard tabs after submit' ).toBeVisible({ timeout: 10_000 });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/helpers.ts` around lines 4 - 21, The test helper loginAs uses page.waitForLoadState('networkidle') which should be removed because networkidle is flaky with WebSockets; delete the await page.waitForLoadState('networkidle') call and rely on the existing deterministic post-login assertion (the expect on page.locator(...).toBeVisible). While here, improve selector stability by replacing complex locator(...) uses (the usernameField/passwordField/submitBtn definitions) with semantic Playwright queries such as page.getByLabel/getByRole or page.getByTestId for the username/password inputs and submit button to make loginAs more robust.tests/e2e/global-setup.ts (2)
35-36:path.resolve(...)is CWD-dependent; preferimport.meta-relative resolution.
path.resolve('tests/e2e/smoke-test-defaults.json')resolves fromprocess.cwd(), which is the project root when Playwright is invoked normally—but will fail if the CWD differs (e.g., directbuninvocations from a subdirectory).♻️ Proposed fix
-const smokeDefaultsPath = path.resolve('tests/e2e/smoke-test-defaults.json'); +// Resolve relative to this file, not process.cwd() +const smokeDefaultsPath = path.resolve( + new URL('.', import.meta.url).pathname, + 'smoke-test-defaults.json', +);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 35 - 36, Replace the CWD-dependent path.resolve call that sets smokeDefaultsPath (and the subsequent smokeDefaultsRaw read) with an import.meta.url–relative resolution: derive the test file's directory from import.meta.url (using fileURLToPath and path.dirname or the URL constructor) and resolve 'tests/e2e/smoke-test-defaults.json' relative to that directory so the file is found regardless of process.cwd(); update the symbol smokeDefaultsPath to use this import.meta.url–based path before calling fs.readFileSync to populate smokeDefaultsRaw.
213-217: Import TypeScript types directly from@frostr/igloo-coreinstead of hand-rolling them inline.The library ships TypeScript declarations (exposed via
"types": "dist/index.d.ts"in itspackage.json). Hand-rolling the type assertion bypasses those exported types, risking silent divergence if the library API changes (e.g., renamed fields, changed return shapes). Import the types directly from the library instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 213 - 217, Replace the inline type assertion on the dynamic import with the library's exported types: change the destructuring line so the import is typed as typeof import('@frostr/igloo-core') (or import the specific exported types) instead of the hand-rolled shape. Concretely, update the line that assigns generateKeysetWithSecret and decodeGroup to use "as typeof import('@frostr/igloo-core')" or import the proper return/type interfaces from '@frostr/igloo-core' and use them for typing generateKeysetWithSecret and decodeGroup to rely on the package's official declarations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/components/ui/peer-list.tsx`:
- Around line 613-618: The Tooltip trigger is made focusable but uses an
icon-only HelpCircle (trigger prop) which lacks an accessible name; update the
trigger so screen readers can identify it — either add an aria-label to the
interactive element (e.g., aria-label="Help" or a more specific description) or
wrap the HelpCircle inside a semantic button with accessible text/aria-label,
keeping the existing onClick/onKeyDown stopPropagation handlers; ensure the
Tooltip component still receives the updated trigger prop.
In `@tests/e2e/global-setup.ts`:
- Around line 321-347: The sign probe loop (uses TEST_MSG, cosignerProcess,
COSIGNER_LOG and api.post('/api/sign')) can run up to ~90s because each
/api/sign uses FROSTR_SIGN_TIMEOUT (15s) plus a 3s pre-sleep for 5 attempts,
which exceeds Playwright's global timeout; fix by either increasing Playwright's
global timeout in playwright.config.ts to >=120s, or reduce the per-probe sign
timeout during setup by ensuring the server process is started with a lower
FROSTR_SIGN_TIMEOUT (e.g., '5000') in the env used when launching the test
server (or reduce attempts/sleep) so api.post('/api/sign') returns faster and
the loop completes within the test timeout.
In `@tests/e2e/specs/01-auth.e2e.ts`:
- Around line 5-9: Add an explicit SmokeTestState type annotation to the state
variable: import the SmokeTestState type from the module that exports loadState
(same module as loadState) and change the declaration to annotate state (e.g.,
const state: SmokeTestState = loadState()). Update the import list to include
the SmokeTestState type alongside loadState so the test file uses strict typing
consistently.
In `@tests/e2e/specs/02-status-peers.e2e.ts`:
- Around line 5-9: The test file currently assigns state without an explicit
type; update the declaration to use the SmokeTestState type for strict TS
compliance by importing or referencing SmokeTestState and changing the line
using loadState to a typed form (e.g., const state: SmokeTestState =
loadState(); or const state = loadState() as SmokeTestState;), keeping the
existing usage of baseUrl and sessionId unchanged and ensuring SmokeTestState is
available in the scope where loadState() is called.
In `@tests/e2e/specs/06-event-log.e2e.ts`:
- Around line 6-10: The variable `state` should have an explicit SmokeTestState
type to match strict-mode patterns; change the declaration to annotate `state`
(e.g., make `state` use `SmokeTestState` with `const state: SmokeTestState =
loadState()`), leaving the existing destructuring of `baseUrl` and `sessionId`
from `state` unchanged so downstream uses remain the same.
In `@tests/e2e/specs/07-env.e2e.ts`:
- Around line 9-13: Declare the variable `state` with the explicit
`SmokeTestState` type when assigning from `loadState()` to satisfy strict
TypeScript rules: change the `state` declaration to include the `:
SmokeTestState` annotation (so usages like `baseUrl` and `sessionId` remain
typed), ensuring the `loadState` return is treated as `SmokeTestState`
throughout the `tests/e2e/specs/07-env.e2e.ts` file.
---
Duplicate comments:
In `@llm/implementation/e2e-smoke-tests.md`:
- Around line 32-35: Update the e2e-smoke-tests documentation to explicitly note
the resolvePort() fallback behavior and warn about hard-coded port usages (refer
to resolvePort() and tests/e2e/global-setup.ts), ensure the sign response fields
are documented as id and signature (refer to the documented sign response
section where id/signature are listed), and confirm the "new API key
authenticates" test targets the authenticated endpoint GET /api/event-log (not
/api/status) so the test verifies API-key auth semantics.
In `@llm/implementation/node-lifecycle-implementation.md`:
- Line 64: Update the documentation text to state that
NODE_ALLOW_BENIGN_PUBLISH_SWALLOW is authoritative and
RELAY_ALLOW_BENIGN_SWALLOW is only used as a fallback when the primary flag is
unset; specifically change the sentence to make the asymmetry explicit and show
the evaluation expression as NODE_ALLOW_BENIGN_PUBLISH_SWALLOW ??
RELAY_ALLOW_BENIGN_SWALLOW, and mention that any explicit value (true or false)
on NODE_ALLOW_BENIGN_PUBLISH_SWALLOW preempts RELAY_ALLOW_BENIGN_SWALLOW so
readers of node-lifecycle-implementation.md understand the precedence.
In `@llm/implementation/umbrel-implementation.md`:
- Around line 69-74: The documentation updated the path reference but ensure the
text and checklist consistently reference the correct compose files: keep
"igloo-server/docker-compose.yml" as the file that must be updated with the new
`@sha256` digest while retaining the :umbrel-dev tag, and explicitly note that
"packages/umbrel/igloo/docker-compose.yml" is the sideload/dev bundle that
points to :umbrel-dev without a pinned digest; update any remaining mentions
(e.g., the two docker-compose.yml references and the checklist steps) so they
uniformly name these two unique artifacts ("igloo-server/docker-compose.yml" and
"packages/umbrel/igloo/docker-compose.yml") and clarify the workflow for
building/pushing :umbrel-<version> and :umbrel-latest and where to change only
the digest.
In `@tests/e2e/state.ts`:
- Around line 1-82: The state loader is correct and no changes are required;
keep the STUB constant and the loadState function as-is (including
SMOKE_TEST_STATE_SCHEMA, safeParse usage, and the error construction that
references stateFile) since zod dependency is present and validation/error
messaging for SMOKE_TEST_STATE_SCHEMA and loadState is already handled properly.
In `@tests/routes/env.db-mode.spec.ts`:
- Around line 80-86: The current code must continue using createRequire anchored
to root + 'package.json' to avoid post-chdir import failures: use createRequire
to build requireFromRoot, call requireFromRoot.resolve('@frostr/igloo-core') to
get iglooCorePath, then dynamic import(iglooCorePath) and invoke
generateKeysetWithSecret to produce groupCredential and shareCredentials; keep
these exact symbols (createRequire, requireFromRoot, iglooCorePath,
generateKeysetWithSecret) and the resolution-before-import pattern as
implemented.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 87-107: The CI audit retry block should preserve and emit the
final bun audit exit code and attempt count for easier grepping; capture the
exit code after the failing bun audit invocation (use the audit_log and the loop
variable attempt), store it (e.g., last_exit or last_code) each loop iteration
when bun audit fails, and after retries echo something like "bun audit failed
after X attempts with exit code Y" along with the existing network/vuln message
so the final exit code and attempt count are visible in CI logs before rm -f
"$audit_log" and exit 1.
- Around line 28-30: The CI step currently labeled "name: Run route tests" runs
the broader script "run: bun run test:unit", which is misleading; update the
step's name to accurately reflect the script scope (e.g., "Run unit tests" or
"Run test:unit") so it matches the executed command. Locate the step with the
manifest "name: Run route tests" and change only the name to match "bun run
test:unit" without altering the existing "run: bun run test:unit" line.
In `@src/routes/env.ts`:
- Around line 283-287: Trim and normalize the extracted Bearer token and ensure
an empty X-Admin-Secret header doesn't override a valid Bearer token: when
reading Authorization, call .trim() on the extracted token (authHeader and
bearerToken logic) so "Bearer token " works; when selecting adminSecret, treat
X-Admin-Secret as absent if it's null/undefined or an empty/whitespace-only
string (e.g. read req.headers.get('X-Admin-Secret') into a trimmed variable and
only use it if non-empty), otherwise fall back to bearerToken and then pass that
into validateAdminSecret; apply the same change at both occurrences that use
authHeader, bearerToken, adminSecret and validateAdminSecret.
- Around line 294-317: The three similar validation blocks for RELAYS /
GROUP_CRED / SHARE_CRED duplicate logic; extract a helper (e.g.,
validateAndRespond) that takes the key name, the input value (body.RELAYS /
body.GROUP_CRED / body.SHARE_CRED), the validator function (validateRelayUrls /
validateGroup / validateShare) and the error messages, so you run common checks
(call validator, check .valid/.isValid, ensure non-empty relay URLs) and return
the same Response.json on failure; replace the three blocks with calls to that
helper and use it for the other duplicate block elsewhere to centralize any
future normalize-before-write logic.
In `@src/routes/utils.test.ts`:
- Around line 58-79: Add a test in the normalizeRelayListForEcho describe block
that covers the filter path when ALLOW_LOCALHOST_RELAY is not enabled:
temporarily ensure process.env.ALLOW_LOCALHOST_RELAY is undefined or 'false',
call normalizeRelayListForEcho with localhost/127.0.0.1 relay URLs (e.g.
'ws://127.0.0.1:18002' and 'ws://localhost:18002') plus a non-loopback relay,
assert the returned array does not include the loopback entries but keeps the
non-loopback one, and restore the original ALLOW_LOCALHOST_RELAY value in a
finally block; reference normalizeRelayListForEcho (and the underlying
isLoopbackRelayHost behavior) when adding the assertion.
In `@src/routes/utils.ts`:
- Around line 817-821: The inner protocol check in normalizeRelayListForEcho is
redundant because validateRelayUrls already filters out non-ws/wss URLs; remove
the u.protocol !== 'ws:' && u.protocol !== 'wss:' condition from the inner
filter (the try block that constructs new URL(r) and checks
allowLocalhost/isLoopbackRelayHost) so the filter only enforces
localhost/loopback rules and returns true for valid entries, keeping
validateRelayUrls as the sole protocol validator.
In `@tests/e2e/global-setup.ts`:
- Around line 35-36: Replace the CWD-dependent path.resolve call that sets
smokeDefaultsPath (and the subsequent smokeDefaultsRaw read) with an
import.meta.url–relative resolution: derive the test file's directory from
import.meta.url (using fileURLToPath and path.dirname or the URL constructor)
and resolve 'tests/e2e/smoke-test-defaults.json' relative to that directory so
the file is found regardless of process.cwd(); update the symbol
smokeDefaultsPath to use this import.meta.url–based path before calling
fs.readFileSync to populate smokeDefaultsRaw.
- Around line 213-217: Replace the inline type assertion on the dynamic import
with the library's exported types: change the destructuring line so the import
is typed as typeof import('@frostr/igloo-core') (or import the specific exported
types) instead of the hand-rolled shape. Concretely, update the line that
assigns generateKeysetWithSecret and decodeGroup to use "as typeof
import('@frostr/igloo-core')" or import the proper return/type interfaces from
'@frostr/igloo-core' and use them for typing generateKeysetWithSecret and
decodeGroup to rely on the package's official declarations.
In `@tests/e2e/helpers.ts`:
- Around line 4-21: The test helper loginAs uses
page.waitForLoadState('networkidle') which should be removed because networkidle
is flaky with WebSockets; delete the await page.waitForLoadState('networkidle')
call and rely on the existing deterministic post-login assertion (the expect on
page.locator(...).toBeVisible). While here, improve selector stability by
replacing complex locator(...) uses (the usernameField/passwordField/submitBtn
definitions) with semantic Playwright queries such as page.getByLabel/getByRole
or page.getByTestId for the username/password inputs and submit button to make
loginAs more robust.
In `@tests/e2e/specs/03-nip44-nip04.e2e.ts`:
- Around line 18-81: Tests create Playwright API contexts with
request.newContext and call await api.dispose() after assertions, so a failing
expect prevents disposal; update each test (e.g., the 'returns 401 without
auth', 'encrypt returns ciphertext', 'encrypt then decrypt round-trips
plaintext', 'invalid peer_pubkey returns 400', 'missing content returns 400'
cases) to always dispose the context by either wrapping the request.newContext
usage in try/finally (call await api.dispose() in finally) or replace with the
existing withApi helper to manage lifecycle, ensuring all references to
api.dispose are removed or only in the finally and keeping
headers/sessionId/baseUrl usage unchanged.
- Around line 18-150: Tests in tests/e2e/specs/03-nip44-nip04.e2e.ts are making
real network calls to /api/nip44/* and /api/nip04/* inside the test.describe
blocks ("NIP-44 – /api/nip44" and "NIP-04 – /api/nip04"), which conflicts with
the repo policy to mock those external endpoints; update these tests to
stub/mock requests to /api/sign, /api/nip44/*, /api/nip04/* and /api/nip46/*
(e.g., intercept the .post calls created via request.newContext and return
canned responses for encrypt/decrypt and error cases) or move the file to a
nightly/e2e-only suite and add a doc note clarifying that e2e tests may hit real
services, ensuring the tests no longer perform uncontrolled external network
requests while preserving the current assertions (status codes and response
shapes).
In `@tests/e2e/specs/05-admin.e2e.ts`:
- Around line 38-101: Tests "POST /api/admin/api-keys creates a new key" and
"new API key can authenticate" create API keys but don't clean them up; change
those tests to generate unique labels (e.g., append timestamp/uuid) when calling
api.post('/api/admin/api-keys') and store the returned apiKey, then revoke that
key in a finally block by calling api.post('/api/admin/api-keys/revoke') with {
apiKeyId: apiKey.id, reason: 'test cleanup' } so keys are always removed; use
the existing withApi helper and the same endpoints to locate and update the
logic in those two tests.
In `@tests/routes/helpers/script-runner.ts`:
- Line 63: The generic default on runRouteScript uses T = any; change it to T =
unknown to comply with strict typing rules—update the function signature export
function runRouteScript<T = unknown>(code: string, env: Record<string, string> =
{}): T and ensure callers that rely on implicit any provide an explicit type or
perform proper narrowing/casting where they consume the return value (reference:
runRouteScript).
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (32)
.github/workflows/ci.yml.github/workflows/release.yml.gitignorefrontend/components/ui/peer-list.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright-report/index.htmlplaywright.config.tsscripts/release.shsrc/class/relay.tssrc/routes/env.tssrc/routes/utils.test.tssrc/routes/utils.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/smoke-test-defaults.jsontests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.ts
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
tests/e2e/cosigner.mjs (2)
55-64:safeStringifynow used consistently — resolved from prior review.All event listeners (
bounced,/sign/handler/rej,subscribed) now usesafeStringifyinstead of rawJSON.stringify, preventing potential crashes on circular structures.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` around lines 55 - 64, Replace any remaining uses of JSON.stringify in the cosigner event listeners with the provided safeStringify to avoid crashes on circular structures; verify safeStringify is imported or defined and used for the node.on handlers for 'bounced', '/sign/handler/rej', and 'subscribed' (and any other node.on logging) so all logs call safeStringify(args).slice(...) where needed instead of JSON.stringify.
14-17: Bare import specifier — resolved from prior review.Now correctly uses
await import('@frostr/igloo-core')instead of the hardcodednode_modulesdist path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` around lines 14 - 17, Replace any remaining hardcoded dist/node_modules paths with a dynamic import for the package so the module resolution uses the package name; specifically ensure the test uses await import('@frostr/igloo-core') and that createBifrostNode and connectNode are obtained from that dynamic import (locate occurrences of createBifrostNode and connectNode in tests/e2e/cosigner.mjs and replace imports that reference explicit file paths with the await import('@frostr/igloo-core') pattern).package.json (1)
36-42: Test scripts are well-organized with clear separation of concerns.The
test:e2e:nightlyis now properly differentiated fromtest:e2ewith--retries=2 --timeout=60000, addressing the prior review concern. Thetest:e2e:smokesubset is a nice addition for quick CI feedback.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 36 - 42, The package.json scripts block has a trailing comma after the "test:e2e:report" entry which makes the JSON invalid; remove the trailing comma and run a JSON lint/parse to verify the file, and while there confirm the script keys ("test:unit", "test:e2e:smoke", "test:e2e", "test:e2e:nightly", "test:e2e:ui", "test:e2e:api", "test:e2e:report") are correctly quoted and preserved.
🧹 Nitpick comments (5)
llm/implementation/umbrel-implementation.md (1)
75-75: Keep path notation consistent forumbrel-app.yml.The key files list uses repo-relative paths (e.g.,
igloo-server/umbrel-app.yml), while this checklist item uses a workspace-relative path. Consider standardizing the notation to avoid ambiguity during release steps.📝 Proposed tweak
-3. Update `igloo-server-store/igloo-server/umbrel-app.yml` version and release notes. +3. Update `igloo-server/umbrel-app.yml` version and release notes (in the igloo-server-store repo).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@llm/implementation/umbrel-implementation.md` at line 75, The checklist mixes repo-relative and workspace-relative path notation for umbrel-app.yml; standardize to repo-relative paths like used elsewhere (e.g., "igloo-server/umbrel-app.yml") so all references are consistent. Update the checklist line "Update `igloo-server-store/igloo-server/umbrel-app.yml` version and release notes." to use the same repo-relative form used in the key files list (e.g., change to `igloo-server/umbrel-app.yml`), and scan the surrounding document for any other occurrences of workspace-relative paths and make them match the repo-relative convention.src/routes/utils.test.ts (1)
24-96: Consider a small helper to reduce the repeated env-var save/restore boilerplate.The
process.env.ALLOW_LOCALHOST_RELAYsave/restore pattern is duplicated 6 times identically. A helper likewithEnv(key, value, fn)would reduce noise and make intent clearer.♻️ Example helper
function withEnv(key: string, value: string, fn: () => void): void { const previous = process.env[key]; process.env[key] = value; try { fn(); } finally { if (previous === undefined) delete process.env[key]; else process.env[key] = previous; } }Then each test body simplifies to:
it('filters IPv6 localhost relay when localhost relays are disallowed', () => { withEnv('ALLOW_LOCALHOST_RELAY', 'false', () => { expect(getValidRelays('["ws://[::1]:18002"]', { fallbackToDefault: false })).toEqual([]); }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.test.ts` around lines 24 - 96, Introduce a small test helper to avoid repeating the process.env save/restore boilerplate: add a withEnv(key: string, value: string, fn: () => void) helper in the test file and use it to wrap each assertion that toggles process.env.ALLOW_LOCALHOST_RELAY; replace the six try/finally blocks around getValidRelays(...) and normalizeRelayListForEcho(...) tests with calls to withEnv('ALLOW_LOCALHOST_RELAY', 'true'|'false', () => { /* expectation */ }) so the tests remain identical but the repeated save/restore logic is centralized.tests/e2e/state.ts (1)
25-40: Consider addingmin(1)constraints to credential fields in the schema.Fields like
groupCredential,adminSecret, andadminPasswordaccept empty strings, which would let a corrupt state file (with blanks) silently pass validation and cause confusing downstream failures. Adding.min(1)on the critical fields would surface the problem earlier. Not blocking since the state file is written by a trusted global-setup.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/state.ts` around lines 25 - 40, SMOKE_TEST_STATE_SCHEMA currently allows empty strings for critical credential fields; update the schema by adding .min(1, 'must be non-empty') to the string validators for groupCredential, adminPassword, and adminSecret (and make the array elements non-empty by changing shareCredentials to z.array(z.string().min(1))) so the schema will reject blank values; locate the SMOKE_TEST_STATE_SCHEMA object and modify those field validators accordingly.tests/routes/helpers/script-runner.ts (1)
59-63: Avoidanyas the default generic type.Defaulting to
anyundermines type safety for callers; considerunknown(or a minimal record type) and require explicit typing where needed.♻️ Suggested change
- * Uses T=any by default so callers without an explicit type can access result properties (e.g. out.status). + * Uses T=unknown by default; callers can supply a concrete type when needed. -export function runRouteScript<T = any>(code: string, env: Record<string, string> = {}): T { +export function runRouteScript<T = unknown>(code: string, env: Record<string, string> = {}): T {As per coding guidelines: TypeScript strict mode; explicit types, avoid any.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` around lines 59 - 63, The generic default for runRouteScript<T = any> undermines type safety; change the default to unknown (e.g., runRouteScript<T = unknown>) or a safer shape like Record<string, unknown>, update the function signature and any internal assumptions that rely on properties of T, and update callers/tests to explicitly specify the expected result type or narrow/validate the unknown return before accessing properties; reference the runRouteScript function to locate and modify the signature and any direct property accesses that assume T has known fields.tests/e2e/global-teardown.ts (1)
51-94: Consider cleanup fallback even if state parsing fails.
When JSON parsing fails, teardown returns early and leaves temp artifacts. You can still remove the temp directory using the state file’s directory even if process PIDs are unavailable.♻️ Suggested tweak
- let state: SmokeTestState; + let state: SmokeTestState | null = null; try { - state = JSON.parse(fs.readFileSync(resolvedStateFile, 'utf8')) as SmokeTestState; + state = JSON.parse(fs.readFileSync(resolvedStateFile, 'utf8')) as SmokeTestState; } catch { console.warn('[teardown] Could not parse state file.'); - return; } - for (const [label, pid] of [['co-signer', state.cosignerPid], ['server', state.serverPid]] as const) { - if (!pid) continue; - try { - process.kill(pid, 'SIGTERM'); - console.log(`[teardown] Sent SIGTERM to ${label} (pid ${pid})`); - } catch (err: unknown) { - // ESRCH = process already gone, which is fine - if ((err as NodeJS.ErrnoException).code !== 'ESRCH') { - console.warn(`[teardown] Could not kill ${label} (pid ${pid}):`, err); - } - } - } + if (state) { + for (const [label, pid] of [['co-signer', state.cosignerPid], ['server', state.serverPid]] as const) { + if (!pid) continue; + try { + process.kill(pid, 'SIGTERM'); + console.log(`[teardown] Sent SIGTERM to ${label} (pid ${pid})`); + } catch (err: unknown) { + // ESRCH = process already gone, which is fine + if ((err as NodeJS.ErrnoException).code !== 'ESRCH') { + console.warn(`[teardown] Could not kill ${label} (pid ${pid}):`, err); + } + } + } + } - const tmpDir = state.tmpDir || path.dirname(resolvedStateFile); + const tmpDir = state?.tmpDir || path.dirname(resolvedStateFile);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-teardown.ts` around lines 51 - 94, When JSON.parse of resolvedStateFile fails, don't return early; instead set a minimal fallback state so teardown can still remove temp artifacts — e.g., create a fallback SmokeTestState with tmpDir = path.dirname(resolvedStateFile) inside the catch block that currently logs "[teardown] Could not parse state file.", so the later loop over state.cosignerPid/state.serverPid and the temp-dir removal logic (tmpDir, resolvedTmp, isInsideTemp) still run; keep existing PID checks (if (!pid) continue) so missing PIDs are ignored but temp cleanup proceeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@llm/implementation/e2e-smoke-tests.md`:
- Line 269: Typo: the endpoint string `GET /api/sign` is incorrect — update the
documentation entry to `POST /api/sign` so the list correctly reflects the sign
endpoint's HTTP method; locate the line containing `GET /api/sign`, replace it
with `POST /api/sign`, and keep the surrounding items (`GET /api/event-log`,
NIP-44/NIP-04) unchanged.
In `@tests/e2e/global-setup.ts`:
- Around line 205-210: The TMP_DIR cleanup in global-setup.ts currently calls
fs.rmSync(TMP_DIR, ...) without ensuring TMP_DIR is actually inside the system
temp directory; update the setup to mirror the teardown guard by checking that
TMP_DIR is a subpath of os.tmpdir() before calling fs.rmSync: verify TMP_DIR
startsWith or is contained within os.tmpdir() (and only then run
fs.existsSync/TMP_DIR removal), so references to TMP_DIR/SMOKE_TEST_TMP_DIR are
protected from accidental deletion of arbitrary paths.
- Around line 62-81: The helper functions sleep, writeState, and
terminateProcess lack explicit return types; update their signatures to declare
them explicitly (sleep(...) : Promise<void>, writeState(...) : void,
terminateProcess(...) : void) and keep existing parameter types (e.g., Sleep's
ms: number, writeState(state: SmokeTestState), terminateProcess(proc:
ChildProcess | null, label: string)); no other logic changes are needed—just add
the return type annotations to the function declarations.
---
Duplicate comments:
In `@package.json`:
- Around line 36-42: The package.json scripts block has a trailing comma after
the "test:e2e:report" entry which makes the JSON invalid; remove the trailing
comma and run a JSON lint/parse to verify the file, and while there confirm the
script keys ("test:unit", "test:e2e:smoke", "test:e2e", "test:e2e:nightly",
"test:e2e:ui", "test:e2e:api", "test:e2e:report") are correctly quoted and
preserved.
In `@tests/e2e/cosigner.mjs`:
- Around line 55-64: Replace any remaining uses of JSON.stringify in the
cosigner event listeners with the provided safeStringify to avoid crashes on
circular structures; verify safeStringify is imported or defined and used for
the node.on handlers for 'bounced', '/sign/handler/rej', and 'subscribed' (and
any other node.on logging) so all logs call safeStringify(args).slice(...) where
needed instead of JSON.stringify.
- Around line 14-17: Replace any remaining hardcoded dist/node_modules paths
with a dynamic import for the package so the module resolution uses the package
name; specifically ensure the test uses await import('@frostr/igloo-core') and
that createBifrostNode and connectNode are obtained from that dynamic import
(locate occurrences of createBifrostNode and connectNode in
tests/e2e/cosigner.mjs and replace imports that reference explicit file paths
with the await import('@frostr/igloo-core') pattern).
---
Nitpick comments:
In `@llm/implementation/umbrel-implementation.md`:
- Line 75: The checklist mixes repo-relative and workspace-relative path
notation for umbrel-app.yml; standardize to repo-relative paths like used
elsewhere (e.g., "igloo-server/umbrel-app.yml") so all references are
consistent. Update the checklist line "Update
`igloo-server-store/igloo-server/umbrel-app.yml` version and release notes." to
use the same repo-relative form used in the key files list (e.g., change to
`igloo-server/umbrel-app.yml`), and scan the surrounding document for any other
occurrences of workspace-relative paths and make them match the repo-relative
convention.
In `@src/routes/utils.test.ts`:
- Around line 24-96: Introduce a small test helper to avoid repeating the
process.env save/restore boilerplate: add a withEnv(key: string, value: string,
fn: () => void) helper in the test file and use it to wrap each assertion that
toggles process.env.ALLOW_LOCALHOST_RELAY; replace the six try/finally blocks
around getValidRelays(...) and normalizeRelayListForEcho(...) tests with calls
to withEnv('ALLOW_LOCALHOST_RELAY', 'true'|'false', () => { /* expectation */ })
so the tests remain identical but the repeated save/restore logic is
centralized.
In `@tests/e2e/global-teardown.ts`:
- Around line 51-94: When JSON.parse of resolvedStateFile fails, don't return
early; instead set a minimal fallback state so teardown can still remove temp
artifacts — e.g., create a fallback SmokeTestState with tmpDir =
path.dirname(resolvedStateFile) inside the catch block that currently logs
"[teardown] Could not parse state file.", so the later loop over
state.cosignerPid/state.serverPid and the temp-dir removal logic (tmpDir,
resolvedTmp, isInsideTemp) still run; keep existing PID checks (if (!pid)
continue) so missing PIDs are ignored but temp cleanup proceeds.
In `@tests/e2e/state.ts`:
- Around line 25-40: SMOKE_TEST_STATE_SCHEMA currently allows empty strings for
critical credential fields; update the schema by adding .min(1, 'must be
non-empty') to the string validators for groupCredential, adminPassword, and
adminSecret (and make the array elements non-empty by changing shareCredentials
to z.array(z.string().min(1))) so the schema will reject blank values; locate
the SMOKE_TEST_STATE_SCHEMA object and modify those field validators
accordingly.
In `@tests/routes/helpers/script-runner.ts`:
- Around line 59-63: The generic default for runRouteScript<T = any> undermines
type safety; change the default to unknown (e.g., runRouteScript<T = unknown>)
or a safer shape like Record<string, unknown>, update the function signature and
any internal assumptions that rely on properties of T, and update callers/tests
to explicitly specify the expected result type or narrow/validate the unknown
return before accessing properties; reference the runRouteScript function to
locate and modify the signature and any direct property accesses that assume T
has known fields.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (32)
.github/workflows/ci.yml.github/workflows/release.yml.gitignorefrontend/components/ui/peer-list.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright-report/index.htmlplaywright.config.tsscripts/release.shsrc/class/relay.tssrc/routes/env.tssrc/routes/utils.test.tssrc/routes/utils.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/smoke-test-defaults.jsontests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.ts
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/class/relay.ts (1)
189-198:⚠️ Potential issue | 🟡 MinorInvalid events are logged twice at debug level; prefer template literal on line 194.
Two separate issues in this segment:
Duplicate debug log: Line 191 unconditionally logs
this.log.debug('event:', event)for every event. When validation fails, line 195 also logsthis.log.debug('event details:', event)for the sameeventobject — producing two identical debug entries. Line 195 is redundant; remove it.String concatenation vs template literal: Line 194 uses
+concatenation while the rest of the file consistently uses template literals (e.g. lines 87, 143, 206, 234).♻️ Proposed fix
- this.log.info('event failed validation (id=' + event.id.slice(0, 8) + ' kind=' + event.kind + ')') - this.log.debug('event details:', event) + this.log.info(`event failed validation (id=${event.id.slice(0, 8)} kind=${event.kind})`)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/class/relay.ts` around lines 189 - 198, In _onevent, remove the redundant debug log that duplicates event details (delete the second this.log.debug('event details:', event)) so the event is only debug-logged once, and change the info string concatenation in the Nostr.verify_event failure branch to a template literal (use this.log.info(`event failed validation (id=${event.id.slice(0,8)} kind=${event.kind})`)) to match the file's style; keep the existing this.send([ 'OK', event.id, false, 'event failed validation' ]) and Nostr.verify_event usage intact.
♻️ Duplicate comments (2)
frontend/components/ui/peer-list.tsx (1)
593-627: Nice accessibility polish on the header controls.aria-expanded plus the labeled help button and propagation guards improve keyboard/screen-reader behavior.
Also applies to: 665-667
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 593 - 627, The help-button wrapper properly stops click/keyboard event propagation around the header (see the div with onClick={e => e.stopPropagation()} onKeyDown={e => e.stopPropagation()} wrapping the Tooltip/HelpCircle), but the same propagation-guard pattern is missing for the duplicate header instance later in the file; update the other header block (where Tooltip and HelpCircle are rendered around the Peer list help button) to include the same onClick and onKeyDown handlers that stop propagation (and optionally call e.preventDefault() for space/Enter) so the help button doesn't toggle the collapsible when activated; reference handleToggle, isExpanded, Tooltip and HelpCircle to locate the blocks to change.tests/e2e/specs/05-admin.e2e.ts (1)
65-92: Status assertion before JSON destructuring is now in place (line 74).The previous review concern about missing
expect(createRes.status()).toBe(201)has been addressed in both the "new API key can authenticate" and "revoked API key returns 401" tests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/05-admin.e2e.ts` around lines 65 - 92, Remove the stray "[duplicate_comment]" token from the review/comment to avoid noise; the tests "new API key can authenticate" (and the related "revoked API key returns 401") already include the added status assertions, so just delete the duplicate marker from the comment content and re-submit the review.
🧹 Nitpick comments (5)
tests/routes/helpers/script-runner.ts (2)
92-92: Unnecessary array spread.
stdout.split('\n')already returns anArray; the spread into a new array is redundant.Suggested fix
- const line = [...stdout.split('\n')].reverse().find(l => l.includes(marker)); + const line = stdout.split('\n').reverse().find(l => l.includes(marker));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` at line 92, The spread operator is redundant when creating `line` because `stdout.split('\n')` already returns an array; update the expression in the `script-runner` helper where `const line = [...stdout.split('\n')].reverse().find(l => l.includes(marker));` is defined to remove the unnecessary spread and operate directly on the array returned by `stdout.split('\n')` (e.g., call `.reverse().find(...)` on that array) so functionality remains identical but without the extra allocation.
63-63:T = anydefault type undermines strict mode.Per coding guidelines,
anyshould be avoided. Consider defaulting tounknownto force callers to type-narrow, or at leastRecord<string, unknown>.Suggested fix
-export function runRouteScript<T = any>(code: string, env: Record<string, string> = {}): T { +export function runRouteScript<T = unknown>(code: string, env: Record<string, string> = {}): T {As per coding guidelines: "TypeScript strict mode; explicit types, avoid
any".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` at line 63, The generic default for runRouteScript currently uses T = any which violates strict-mode guidelines; change the generic default to T = unknown (or T = Record<string, unknown> if callers expect object shapes) in the runRouteScript declaration and update any internal casts/returns to properly narrow or assert T where needed so callers must explicitly type-assert or narrow the result; locate the function named runRouteScript and adjust its signature and any immediate usages to compile under strict type checking.src/routes/utils.ts (1)
809-825:normalizeRelayListForEcho— consider deduplicating the localhost-filtering logic.The loopback-check +
ALLOW_LOCALHOST_RELAYguard is copy-pasted fromgetValidRelays(lines 72-81). Consider extracting a sharedfilterLoopbackRelays(urls: string[]): string[]helper to avoid the two implementations drifting apart over time.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.ts` around lines 809 - 825, The localhost filtering and ALLOW_LOCALHOST_RELAY guard used in normalizeRelayListForEcho is duplicated from getValidRelays; extract that logic into a shared helper (e.g., filterLoopbackRelays(urls: string[]): string[]) and replace the duplicate code in both normalizeRelayListForEcho and getValidRelays to call the new helper, ensuring the helper reads process.env['ALLOW_LOCALHOST_RELAY'] and uses isLoopbackRelayHost to remove loopback hosts while preserving trimming, empty-string filtering, and deduplication behavior.src/routes/env.ts (1)
304-326: Duplicated credential/relay validation between DB-mode and headless-mode POST branches.The validation blocks for
RELAYS,GROUP_CRED, andSHARE_CRED(lines 304–326 and 375–397) are nearly identical. Consider extracting a shared helper likevalidateEnvMutationBody(validKeys, body)to reduce drift risk between the two code paths.Also applies to: 375-397
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 304 - 326, The three repeated validation blocks for RELAYS, GROUP_CRED, and SHARE_CRED (using validateRelayUrls, validateGroup, validateShare with validKeys and body) should be extracted into a single helper (e.g., validateEnvMutationBody(validKeys, body)) that performs the relay and credential checks and returns a standardized result or throws a ValidationError; replace the duplicated blocks in both the DB-mode POST branch and the headless-mode POST branch with a call to this helper and handle its result (convert to the same Response.json({ success: false, error }) pattern) so both branches share identical validation logic and avoid drift.src/routes/utils.test.ts (1)
35-78: Good coverage of loopback filtering for bothgetValidRelaysandnormalizeRelayListForEcho.Tests cover IPv6, 127.x.x.x range, and hostname forms, plus the
ALLOW_LOCALHOST_RELAYtoggle. Consider adding one edge-case test forgetValidRelayswhereALLOW_LOCALHOST_RELAY=trueto verify localhost relays are retained — currently that positive path is only tested fornormalizeRelayListForEcho.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.test.ts` around lines 35 - 78, Add a positive-path unit test that verifies getValidRelays preserves localhost relays when ALLOW_LOCALHOST_RELAY is true: use withEnv('ALLOW_LOCALHOST_RELAY','true', ...) and call getValidRelays with a localhost entry (e.g. '["ws://127.0.0.1:18002"]' or '["ws://localhost:18002"]' and { fallbackToDefault: false }) and assert the returned array includes the same localhost URL; place this alongside the existing getValidRelays tests to mirror the normalizeRelayListForEcho positive tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/class/relay.ts`:
- Around line 158-160: The payload normalization in the REQ handler in
src/class/relay.ts (the block that checks payload.length === 2 &&
Array.isArray(payload[1]) and expands payload to [payload[0], ...payload[1]])
silently turns ["REQ", id, []] into a subscription with zero filters; update the
REQ handling to detect an empty filters array (Array.isArray(payload[1]) &&
payload[1].length === 0) and explicitly reject or ignore the request instead of
normalizing it (e.g., send an error NOTICE or drop the REQ), so you don’t
register a catch-all subscription; make sure to keep the existing normalization
behavior for non-empty arrays and adjust any code that relies on
sub_schema.rest() accepting zero filter objects accordingly.
In `@tests/e2e/global-setup.ts`:
- Around line 35-61: The JSON.parse call currently yields an implicit any for
smokeDefaultsRaw; change the declaration of smokeDefaultsRaw so the parse result
is explicitly typed as unknown (e.g., declare smokeDefaultsRaw: unknown =
JSON.parse(...)) and keep the existing runtime type-narrowing that checks typeof
smokeDefaultsRaw and null; then continue casting to raw (const raw =
smokeDefaultsRaw as Record<string, unknown>) and validating
requiredKeys/testNsecHex/adminSecret/adminUsername/adminPassword as before so
strict typing is preserved while the later checks still work.
In `@tests/e2e/specs/03-nip44-nip04.e2e.ts`:
- Around line 28-155: The tests in the "NIP-44 – /api/nip44" and "NIP-04 –
/api/nip04" describe blocks call the real endpoints via withApi(...) and
api.post('/api/nip44/...') and api.post('/api/nip04/...'); replace those live
calls with mocked responses (or a test HTTP stub) inside the withApi harness so
each api.post for '/api/nip44/encrypt', '/api/nip44/decrypt',
'/api/nip04/encrypt', '/api/nip04/decrypt' returns deterministic fixtures (200
with { result: <ciphertext> } and decrypt returning PLAINTEXT, 401 for missing
auth, 400 for invalid peer_pubkey/missing content, and ensure NIP-04 encrypt
fixtures include the "?iv=" suffix), and likewise add mocks for '/api/sign' and
'/api/nip46/*' per guidelines so tests use the stubbed responses instead of
hitting real endpoints.
In `@tests/routes/env.db-mode.spec.ts`:
- Around line 80-87: The test currently hard-codes the keyset seed when calling
generateKeysetWithSecret (producing groupCredential and shareCredentials);
replace that literal with a secret loaded from a secure source (e.g.,
process.env.TEST_KEYSET_SECRET or a fixture file under tests/fixtures) and
validate fallback behavior if missing. Update the call to
generateKeysetWithSecret(2, 2, secret) where secret is read at top of the test
(or required from a fixture module) and ensure any test setup documents or loads
the fixture so CI and developers can rotate the value without editing the test.
---
Outside diff comments:
In `@src/class/relay.ts`:
- Around line 189-198: In _onevent, remove the redundant debug log that
duplicates event details (delete the second this.log.debug('event details:',
event)) so the event is only debug-logged once, and change the info string
concatenation in the Nostr.verify_event failure branch to a template literal
(use this.log.info(`event failed validation (id=${event.id.slice(0,8)}
kind=${event.kind})`)) to match the file's style; keep the existing this.send([
'OK', event.id, false, 'event failed validation' ]) and Nostr.verify_event usage
intact.
---
Duplicate comments:
In `@frontend/components/ui/peer-list.tsx`:
- Around line 593-627: The help-button wrapper properly stops click/keyboard
event propagation around the header (see the div with onClick={e =>
e.stopPropagation()} onKeyDown={e => e.stopPropagation()} wrapping the
Tooltip/HelpCircle), but the same propagation-guard pattern is missing for the
duplicate header instance later in the file; update the other header block
(where Tooltip and HelpCircle are rendered around the Peer list help button) to
include the same onClick and onKeyDown handlers that stop propagation (and
optionally call e.preventDefault() for space/Enter) so the help button doesn't
toggle the collapsible when activated; reference handleToggle, isExpanded,
Tooltip and HelpCircle to locate the blocks to change.
In `@tests/e2e/specs/05-admin.e2e.ts`:
- Around line 65-92: Remove the stray "[duplicate_comment]" token from the
review/comment to avoid noise; the tests "new API key can authenticate" (and the
related "revoked API key returns 401") already include the added status
assertions, so just delete the duplicate marker from the comment content and
re-submit the review.
---
Nitpick comments:
In `@src/routes/env.ts`:
- Around line 304-326: The three repeated validation blocks for RELAYS,
GROUP_CRED, and SHARE_CRED (using validateRelayUrls, validateGroup,
validateShare with validKeys and body) should be extracted into a single helper
(e.g., validateEnvMutationBody(validKeys, body)) that performs the relay and
credential checks and returns a standardized result or throws a ValidationError;
replace the duplicated blocks in both the DB-mode POST branch and the
headless-mode POST branch with a call to this helper and handle its result
(convert to the same Response.json({ success: false, error }) pattern) so both
branches share identical validation logic and avoid drift.
In `@src/routes/utils.test.ts`:
- Around line 35-78: Add a positive-path unit test that verifies getValidRelays
preserves localhost relays when ALLOW_LOCALHOST_RELAY is true: use
withEnv('ALLOW_LOCALHOST_RELAY','true', ...) and call getValidRelays with a
localhost entry (e.g. '["ws://127.0.0.1:18002"]' or '["ws://localhost:18002"]'
and { fallbackToDefault: false }) and assert the returned array includes the
same localhost URL; place this alongside the existing getValidRelays tests to
mirror the normalizeRelayListForEcho positive tests.
In `@src/routes/utils.ts`:
- Around line 809-825: The localhost filtering and ALLOW_LOCALHOST_RELAY guard
used in normalizeRelayListForEcho is duplicated from getValidRelays; extract
that logic into a shared helper (e.g., filterLoopbackRelays(urls: string[]):
string[]) and replace the duplicate code in both normalizeRelayListForEcho and
getValidRelays to call the new helper, ensuring the helper reads
process.env['ALLOW_LOCALHOST_RELAY'] and uses isLoopbackRelayHost to remove
loopback hosts while preserving trimming, empty-string filtering, and
deduplication behavior.
In `@tests/routes/helpers/script-runner.ts`:
- Line 92: The spread operator is redundant when creating `line` because
`stdout.split('\n')` already returns an array; update the expression in the
`script-runner` helper where `const line =
[...stdout.split('\n')].reverse().find(l => l.includes(marker));` is defined to
remove the unnecessary spread and operate directly on the array returned by
`stdout.split('\n')` (e.g., call `.reverse().find(...)` on that array) so
functionality remains identical but without the extra allocation.
- Line 63: The generic default for runRouteScript currently uses T = any which
violates strict-mode guidelines; change the generic default to T = unknown (or T
= Record<string, unknown> if callers expect object shapes) in the runRouteScript
declaration and update any internal casts/returns to properly narrow or assert T
where needed so callers must explicitly type-assert or narrow the result; locate
the function named runRouteScript and adjust its signature and any immediate
usages to compile under strict type checking.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (32)
.github/workflows/ci.yml.github/workflows/release.yml.gitignorefrontend/components/ui/peer-list.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright-report/index.htmlplaywright.config.tsscripts/release.shsrc/class/relay.tssrc/routes/env.tssrc/routes/utils.test.tssrc/routes/utils.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/smoke-test-defaults.jsontests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.ts
|
@CodeRabbit full review |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/class/relay.ts (1)
237-248:⚠️ Potential issue | 🟠 Major
limit_countis decremented even when the event does not match the filter.Inside the cache iteration loop,
limit_countis decremented unconditionally (Line 246) regardless of whethermatch_filterreturnedtrue. This means the limit counts total iterations, not matched events. If the intent is to cap the number of matched events sent to the client (per NIP-01 semantics oflimit), the decrement should be inside the match branch.Proposed fix
if (limit_count === undefined || limit_count > 0) { if (Nostr.match_filter(event, filter)) { this.send(['EVENT', sub_id, event]) this.log.client(`event matched in cache: ${event.id}`) this.log.client(`event matched subscription: ${sub_id}`) + if (limit_count !== undefined) limit_count -= 1 } - if (limit_count !== undefined) limit_count -= 1 }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/class/relay.ts` around lines 237 - 248, The loop currently decrements limit_count regardless of matches, so change the logic in the cache-iteration block around Nostr.match_filter(event, filter) to only decrement limit_count when a match is found and an EVENT is sent; keep the outer guard (if limit_count === undefined || limit_count > 0) but move the `limit_count -= 1` into the branch after this.send(['EVENT', sub_id, event]) (and if limit_count becomes 0 after decrement, stop sending further events for this subscription). Ensure references are to limit_count, Nostr.match_filter, this.send, sub_id, and event so the change is applied in the correct method.tests/routes/helpers/script-runner.ts (1)
99-117:⚠️ Potential issue | 🟠 MajorAvoid emitting full subprocess output in thrown errors.
Line 101 and Line 117 include full
stdout/stderr/rawJsonin exceptions. In CI, this can leak sensitive values produced by scripts and create very noisy logs.🔧 Suggested hardening
+function sanitizeErrorOutput(raw: string, maxLen = 4000): string { + const redacted = raw + .replace(/\b(ADMIN_SECRET|SESSION_SECRET|API_KEY|BASIC_AUTH_PASS|BASIC_AUTH_USER)=\S+/g, '$1=[REDACTED]'); + return redacted.length > maxLen ? `${redacted.slice(0, maxLen)}…[truncated]` : redacted; +} + export function runRouteScript<T = Record<string, unknown>>(code: string, env: Record<string, string> = {}): T { @@ if (result.exitCode !== 0) { + const stderr = sanitizeErrorOutput(result.stderr.toString()); + const stdout = sanitizeErrorOutput(result.stdout.toString()); throw new Error( - `route script failed: status=${result.exitCode} stderr="${result.stderr.toString()}" stdout="${result.stdout.toString()}"` + `route script failed: status=${result.exitCode} stderr="${stderr}" stdout="${stdout}"` ); } @@ } catch (error) { const detail = error instanceof Error ? error.message : String(error); - throw new Error(`route script returned invalid JSON marker payload: ${detail}; raw="${rawJson}"; stdout="${stdout}"`); + throw new Error( + `route script returned invalid JSON marker payload: ${detail}; ` + + `raw="${sanitizeErrorOutput(rawJson)}"; stdout="${sanitizeErrorOutput(stdout)}"` + ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` around lines 99 - 117, The thrown errors in the script-runner (checks around result.exitCode, missing marker and JSON parse failure) currently include full result.stderr/result.stdout/rawJson which can leak secrets; update the three Error constructors to sanitize or truncate outputs instead: for the non-zero exit use only exitCode and a short/sanitized tail of stderr/stdout (e.g., first 200 chars + "...(truncated)"), for the missing marker error include only a trimmed/length-limited preview of stdout rather than the whole string, and for the JSON parse error include the error message plus a length-limited or redacted preview of rawJson (or replace contents with "<redacted>" when rawJson is large/suspect) while still returning the parsed value flow from the try/catch around JSON.parse.
♻️ Duplicate comments (14)
tests/e2e/cosigner.mjs (3)
105-107: Error handling now usesinstanceof Errorpattern — addresses prior review.Consistent with the pattern used elsewhere in the test infrastructure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` around lines 105 - 107, Update the catch block in the cosigner start sequence to use the same instanceof Error pattern used across tests: in the catch(err) of the start routine (the catch surrounding the start logic in tests/e2e/cosigner.mjs) log the error using err instanceof Error ? err.message : String(err) and include err.stack when available (e.g., append err.stack when err is an Error) before calling process.exit(2) so the output is consistent and contains stack details for debugging.
14-17: Bare specifier import addresses prior review concern.Now uses
'@frostr/igloo-core'instead of a hardcodednode_modulespath.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` around lines 14 - 17, The import in tests/e2e/cosigner.mjs should use the package bare specifier instead of a hardcoded node_modules path; replace any lingering file-system imports with the bare specifier '@frostr/igloo-core' (as shown using createBifrostNode and connectNode) and ensure the package is listed in package.json so the runtime resolver can find it; also remove any duplicated import/comment blocks that repeat this change to avoid duplicate_comment noise.
79-88:safeStringifynow used for all event listeners — addresses prior review.Lines 79, 85, and 88 all use
safeStringifyinstead ofJSON.stringify, preventing potentialTypeErroron circular references from Bifrost event arguments.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` around lines 79 - 88, Replace any remaining uses of JSON.stringify in the cosigner event listeners with safeStringify and ensure the node.on handlers for 'message', '/sign/handler/rej' and 'subscribed' (and the bounced/subscription logs) call safeStringify(...).slice(...) on their arguments (as done for other handlers) so circular refs won't throw; also remove the stray "[duplicate_comment]" marker from the review text to avoid confusion.frontend/components/ui/peer-list.tsx (1)
636-654: Accessible help trigger with semantic button — addresses prior review.The
<button>wrapper witharia-label="Peer list help"makes the tooltip trigger keyboard-navigable and screen-reader-friendly. ThestopPropagationwrapper on the parentdivprevents the click from toggling the collapsible section.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 636 - 654, Remove the redundant outer <div> wrapper by moving the event handlers onto the Tooltip trigger button: remove the parent div that currently has onClick={e => e.stopPropagation()} and onKeyDown={e => e.stopPropagation()}, and add those handlers to the <button aria-label="Peer list help"> used as the Tooltip trigger so the stopPropagation behavior is preserved while keeping the trigger semantic and keyboard-accessible (references: Tooltip, the trigger <button aria-label="Peer list help">, and the stopPropagation handlers).src/routes/utils.test.ts (1)
71-81: Expanded IPv6 loopback forms now covered — addresses prior review suggestion.Tests for
0:0:0:0:0:0:0:1and0:0:0:0:0:ffff:7f00:1are present at Lines 71–81, providing regression coverage for fully expanded IPv6 forms.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.test.ts` around lines 71 - 81, These two tests add regression coverage for fully expanded IPv6 loopback forms but were flagged as duplicates; deduplicate by either removing the redundant test(s) or consolidating both cases into a single parameterized test (e.g., use test.each) that calls getValidRelays with the two address strings under the same withEnv('ALLOW_LOCALHOST_RELAY','false') block and asserts an empty array; reference getValidRelays, withEnv, and the ALLOW_LOCALHOST_RELAY env var when making the change.tests/e2e/specs/03-nip44-nip04.e2e.ts (1)
29-156: E2E tests call live NIP-44/NIP-04 endpoints without mocking.The coding guidelines state to mock external calls to
/api/nip44/*and/api/nip04/*in tests. However, as an E2E smoke suite, exercising the live server is the explicit intent. This was flagged in a prior review cycle.As per coding guidelines, "
**/*.{test,spec}.ts?(x): In tests, mock external calls to /api/sign, /api/nip44/, /api/nip04/, and /api/nip46/*."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/03-nip44-nip04.e2e.ts` around lines 29 - 156, The tests are calling live /api/nip44/* and /api/nip04/* endpoints (see test.describe blocks "NIP-44 – /api/nip44" and "NIP-04 – /api/nip04" and individual tests like "encrypt returns ciphertext" and "encrypt then decrypt round-trips plaintext"); update these specs to mock those endpoints instead of hitting the real server by intercepting requests inside the withApi helper (or using Playwright's api.route/route.fulfill) to return deterministic JSON responses for encrypt/decrypt (including the NIP-04 "?iv=" format) and error cases (400/401) so each test asserts against the mocked payloads. Ensure mocks cover success, invalid peer_pubkey, and missing content scenarios and use the same route patterns /api/nip44/* and /api/nip04/* so individual tests need minimal change.tests/e2e/specs/02-status-peers.e2e.ts (1)
1-110: LGTM — all previously flagged issues resolved.
SmokeTestStateannotation is present,res.status()is asserted before parsing JSON in the health test, and the peer-list comment accurately reflects the 2-of-2 keyset topology.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/02-status-peers.e2e.ts` around lines 1 - 110, The reviewer marked this test file as LGTM with issues resolved; no code changes are required—leave the SmokeTestState annotation, the res.status() assertions before JSON parsing in tests like the GET /api/status health check, and the peer-list expectations (peers length check, total/online types, and groupPubkey check) as-is; simply proceed to approve/merge the PR without modifying functions like withApi or the test blocks in the Status and Peers describe suites.tests/routes/helpers/script-runner.spec.ts (1)
1-71: LGTM — all previously flagged issues resolved.The
find()overISOLATED_ENV_KEYS(line 52) correctly excludes forced keys, the assertion usestoBeUndefined()(line 65), andprocess.envis properly restored infinally. No new issues.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.spec.ts` around lines 1 - 71, Tests in script-runner.spec.ts are correct and require no changes: leave the tests as-is (the use of ISOLATED_ENV_KEYS with find(), assertions like toBeUndefined(), and the process.env preservation/restoration around buildScriptEnv are all correct), so approve the change and do not modify buildScriptEnv, ISOLATED_ENV_KEYS, ISOLATED_ENV_PREFIXES, or the test logic.tests/e2e/specs/04-sign.e2e.ts (1)
1-151: LGTM — all previously flagged issues resolved.
SignEventPayloadtype is defined and applied,SmokeTestStateannotation is in place, andwithApiensures context disposal on any assertion failure.EVENT_ID_A/Bfollow UPPER_SNAKE_CASE.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/04-sign.e2e.ts` around lines 1 - 151, All flagged issues are resolved—no code changes required: the SignEventPayload type, SmokeTestState annotation, proper disposal via withApi, and UPPER_SNAKE_CASE constants EVENT_ID_A and EVENT_ID_B are correct; keep the current implementations of SignEventPayload, SmokeTestState, withApi, and the tests as-is (no further fixes needed).tests/e2e/specs/08-ui.e2e.ts (1)
1-113: LGTM — all previously flagged issues resolved.The URL assertion uses
new URL('/', baseUrl).toString()to avoid double-slash;SmokeTestStateis annotated; the Signer tab test now asserts actual status-text content (server signer: running/starting/stopped,node active/inactive); and the onboarding absence test uses two specific regexes that match the rendered copy rather than a broad string.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/08-ui.e2e.ts` around lines 1 - 113, No changes required — the review approves the updates: retain the URL assertion using new URL('/', baseUrl).toString(), keep the SmokeTestState annotation, preserve the Signer tab content assertions in the test 'Signer tab is visible and shows node status indicator' (which checks /server signer: (running|starting|stopped)/i and /\bnode\s+(active|inactive)\b/i), and keep the onboarding absence regex checks in the '/ does not show onboarding when DB is initialised' test.tests/e2e/global-setup.ts (1)
1-465: LGTM — all previously flagged issues resolved.Key fixes confirmed:
fs.closeSync(out)infinallyeliminates the fd leak;requireNonEmptyStringwith env/fixture sourcing replaces all hardcoded fallback secrets;smokeDefaultsRawis typedunknown; helper functions carry explicit return types;TMP_DIRdeletion is guarded by theisInsideTemppath check; fallback port is range-validated; andFROSTR_SIGN_TIMEOUTis reduced to'5000'giving a worst-case sign-probe window of 40 s (5 attempts × (3 s sleep + 5 s timeout)), which is within Playwright's global-setup timeout budget (separate from the 30 s per-test limit).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 1 - 465, All flagged issues have been resolved—no further code changes required: confirm and approve the PR; specifically verify the fd is closed in spawnDetached (fs.closeSync), secrets are sourced via requireNonEmptyString, smokeDefaultsRaw is typed unknown, TMP_DIR cleanup guards (isInsideTemp) are present, reserve/resolvePort validate fallback range, and FROSTR_SIGN_TIMEOUT is set to '5000' to keep sign-probe timing within global-setup budget before merging.tests/e2e/specs/01-auth.e2e.ts (1)
1-129: LGTM — all previously flagged issues resolved.
SmokeTestStateannotation is present,loginRes.status()is asserted before destructuringsessionId, andwithApiensures per-test context cleanup. TheapiKey!non-null assertion in the Bearer test (line 93) is safe becausetest.skip(!apiKey)guards the test body.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/01-auth.e2e.ts` around lines 1 - 129, All requested issues are resolved: keep the SmokeTestState type annotation, the loginRes.status() assertion before destructuring sessionId in the test that uses loginRes, and the per-test API cleanup in withApi; no code changes required—approve and merge the PR as-is (noting the apiKey! non-null assertion in the Bearer test is safe because test.skip(!apiKey) guards that test).llm/implementation/e2e-smoke-tests.md (1)
1-372: LGTM — all previously flagged documentation inaccuracies resolved.The sign-probe count/interval (5 attempts, 3 s apart) matches global-setup.ts; response field names (
id/signature) match04-sign.e2e.ts; the API-key auth verification uses the protected/api/event-logendpoint; the hardcodedADMIN_SECRETvalue is replaced with env/fixture sourcing; and the admin credentials prerequisite is documented. The static-analysis spelling warnings for "FROSTR" are false positives (proper noun).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@llm/implementation/e2e-smoke-tests.md` around lines 1 - 372, The documentation changes in e2e-smoke-tests.md look correct and require no code edits: approve the PR as-is, ensure the doc references (sign-probe retry behavior in global-setup.ts, response fields used by 04-sign.e2e.ts: id and signature, use of /api/event-log for API-key checks, and sourcing of ADMIN_SECRET) remain accurate, and remove any duplicate review markers like [duplicate_comment] or stray approval tokens from the review comment before merging.tests/e2e/specs/06-event-log.e2e.ts (1)
1-109: LGTM — all previously flagged issues resolved.The
beforeAllseeder eliminates the cross-spec ordering dependency;withApiensures context disposal on assertion failure; status assertions precede JSON parsing; both the shape and NDJSON tests guard against vacuous passes with.toBeGreaterThan(0).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/06-event-log.e2e.ts` around lines 1 - 109, No changes required: tests are correct and address prior issues; keep the seeder in test.beforeAll and the request helper withApi as-is. If you still want to tweak, ensure withApi (request.newContext / api.dispose) continues to dispose contexts on failures and that the seeding call in test.beforeAll posts to /api/sign with headers X-Session-ID using state.sessionId so subsequent tests (GET /api/event-log and GET /api/event-log/export) can assert status codes before parsing body and verify non-empty results.
🧹 Nitpick comments (6)
scripts/release.sh (1)
31-37: Consider unifying typecheck invocation with workflow pipelines.This script uses
bun run typecheckwhile CI/release workflows runbun run tsc --noEmit; standardizing avoids drift.♻️ Suggested minimal alignment
- bun run typecheck + bun run tsc --noEmit🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/release.sh` around lines 31 - 37, Replace the current invocation of "bun run typecheck" in the release script with the CI-standard TypeScript check command "bun run tsc --noEmit" so the release pipeline uses the same typecheck behavior as the workflows; locate the line containing "bun run typecheck" in the script and change it to call "bun run tsc --noEmit" (or refactor both pipeline and release to reference a single npm script name used everywhere) to avoid divergence between local/release and CI checks.src/routes/utils.ts (1)
35-78: Loopback detection now handles IPv4-mapped IPv6 — addresses prior review concern.The
extractIpv4MappedIpv6helper correctly handles::ffff:and::ffff:0:prefixes, anddecodeMappedIpv4handles both dotted-decimal and hex-pair representations. The expanded IPv6 loopback0:0:0:0:0:0:0:1is explicitly checked.Note: the expanded IPv4-mapped form
0:0:0:0:0:ffff:7f00:1is not directly matched byextractIpv4MappedIpv6. The tests pass becausenew URL(...)normalizes IPv6 hostnames to their compressed form (e.g.,::ffff:7f00:1). This is correct but worth documenting as a comment, sinceisLoopbackRelayHostitself relies on the caller having pre-normalized the hostname via URL parsing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.ts` around lines 35 - 78, isLoopbackRelayHost relies on the hostname being URL-normalized (new URL(...)) because extractIpv4MappedIpv6 only matches compressed forms like ::ffff:... and not expanded forms such as 0:0:0:0:0:ffff:7f00:1; add a concise comment above isLoopbackRelayHost (and/or above extractIpv4MappedIpv6/decodeMappedIpv4) stating this limitation and instructing callers to pass URL-normalized hostnames (e.g., via new URL(host).hostname) so IPv4-mapped IPv6 addresses are handled correctly, referencing the functions isLoopbackRelayHost, extractIpv4MappedIpv6, and decodeMappedIpv4.src/routes/env.ts (2)
27-27: Parameter typed asany— consider a narrower type.
env: anyweakens type safety. ARecord<string, unknown>or a dedicatedEnvConfiginterface would catch misuse at compile time.As per coding guidelines, "
**/*.{ts,tsx}: Enable TypeScript strict mode, declare explicit types, and avoid any".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` at line 27, The createAndConnectServerNode function currently types its env parameter as any; replace that with a narrower type (either Record<string, unknown> or a dedicated interface EnvConfig describing the expected keys) and update the function signature to async function createAndConnectServerNode(env: EnvConfig, context: PrivilegedRouteContext): Promise<void>; adjust any internal accesses to use the declared properties (or index into the Record with proper type guards), add a small type-guard or validation where values are read if they may be optional, and update any call sites that pass env to conform to the new EnvConfig shape.
307-329: Duplicate credential/relay validation blocks across DB-mode and headless POST paths.Lines 307–329 (DB-mode) and 370–392 (headless) contain identical validation logic for
RELAYS,GROUP_CRED, andSHARE_CRED. Extracting a shared helper would reduce maintenance burden and divergence risk.♻️ Suggested refactor — extract a shared validator
+// Place near the top of handleEnvRoute or as a file-level helper +function validateCredentialFields( + body: Record<string, unknown>, + validKeys: string[], + headers: Record<string, string>, +): Response | null { + if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { + const relayValidation = validateRelayUrls(body.RELAYS); + if (!relayValidation.valid) { + return Response.json({ success: false, error: relayValidation.error }, { status: 400, headers }); + } + if (!relayValidation.urls || relayValidation.urls.length === 0) { + return Response.json({ success: false, error: 'At least one relay URL is required' }, { status: 400, headers }); + } + } + if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED !== undefined) { + const groupValidation = validateGroup(body.GROUP_CRED); + if (!groupValidation.isValid) { + return Response.json({ success: false, error: 'Invalid GROUP_CRED' }, { status: 400, headers }); + } + } + if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED !== undefined) { + const shareValidation = validateShare(body.SHARE_CRED); + if (!shareValidation.isValid) { + return Response.json({ success: false, error: 'Invalid SHARE_CRED' }, { status: 400, headers }); + } + } + return null; +}Then in both code paths:
- if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { ... } - if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED !== undefined) { ... } - if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED !== undefined) { ... } + const validationError = validateCredentialFields(body, validKeys, headers); + if (validationError) return validationError;Also applies to: 370-392
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 307 - 329, Extract the duplicate RELAYS/GROUP_CRED/SHARE_CRED validation into a single helper (e.g., validateRelaysAndCreds) that accepts the request body and validKeys and uses the existing helpers validateRelayUrls, validateGroup, and validateShare to perform the checks and return a uniform error object (or null) when validation fails; then call this helper from both the DB-mode POST path and the headless POST path and, if it returns an error, return Response.json({ success: false, error: ... }, { status: 400, headers }) as before. Ensure the helper name and calls reference validateRelayUrls, validateGroup, and validateShare so it’s easy to locate and replace the duplicated blocks.playwright.config.ts (1)
29-31: Nit: glob0[1-7]will need updating when spec count exceeds 07.If future specs like
09-*.e2e.tsor10-*.e2e.tsare added as API tests, they won't be matched by**/0[1-7]-*.e2e.ts. Consider a broader pattern or a negative match against the UI spec if this suite is expected to grow.♻️ Example future-proof alternative
- testMatch: ['**/0[1-7]-*.e2e.ts'], + testMatch: ['**/*.e2e.ts'], + testIgnore: ['**/08-ui.e2e.ts'],🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@playwright.config.ts` around lines 29 - 31, The testMatch glob for the 'api' suite ("testMatch" in playwright.config.ts) is currently limited to '**/0[1-7]-*.e2e.ts' and will stop matching new numbered specs beyond 07; update the pattern to a future-proof one (for example use a two-digit numeric prefix like '**/[0-9][0-9]-*.e2e.ts' or a more permissive '**/[0-9]*-*.e2e.ts', or switch to a negative match that excludes the UI spec) so new API specs such as 09-*.e2e.ts or 10-*.e2e.ts are included under the 'api' suite.tests/routes/helpers/script-runner.ts (1)
52-69: Consider allow-listing inherited env vars for stronger isolation.Right now the helper copies almost all of
process.envexcept blocked entries. A small allowlist (plus forced keys) is usually more deterministic for smoke tests and avoids host-env drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` around lines 52 - 69, The current build of nextEnv by copying most of process.env then removing blocked keys is brittle; change it to construct nextEnv from an explicit allowlist of environment variable names plus any forced keys (e.g., NODE_ENV, PATH, CI, etc.) instead of iterating over Object.entries(process.env); update the logic where nextEnv is created (replace the for ... of Object.entries(process.env) loop and the isBlockedEnvKey filtering) to only add keys present in the allowlist and then merge sanitizedOverrides (sanitizedOverrides = sanitizeOverrides(overrides)) as before, ensuring the allowlist is declared near this code and document which keys are forced so tests remain deterministic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/e2e/global-teardown.ts`:
- Around line 111-124: The teardown loop over ['co-signer', cosignerPid] and
['server', serverPid] currently only checks isProcessRunning(pid) before sending
SIGTERM, which risks killing a reused PID; instead, retrieve and compare a
persisted identity token from setup (e.g., stored start-time, startup PID token,
or command line) for each target (cosignerPid, serverPid) and verify the live
process matches that identity before calling process.kill in the loop that uses
isProcessRunning; add a helper (e.g., validateProcessIdentity(pid,
expectedStartTimeOrToken)) that reads process start time or command (procfs or
platform API) and only send SIGTERM if it matches the stored value for the
corresponding label.
---
Outside diff comments:
In `@src/class/relay.ts`:
- Around line 237-248: The loop currently decrements limit_count regardless of
matches, so change the logic in the cache-iteration block around
Nostr.match_filter(event, filter) to only decrement limit_count when a match is
found and an EVENT is sent; keep the outer guard (if limit_count === undefined
|| limit_count > 0) but move the `limit_count -= 1` into the branch after
this.send(['EVENT', sub_id, event]) (and if limit_count becomes 0 after
decrement, stop sending further events for this subscription). Ensure references
are to limit_count, Nostr.match_filter, this.send, sub_id, and event so the
change is applied in the correct method.
In `@tests/routes/helpers/script-runner.ts`:
- Around line 99-117: The thrown errors in the script-runner (checks around
result.exitCode, missing marker and JSON parse failure) currently include full
result.stderr/result.stdout/rawJson which can leak secrets; update the three
Error constructors to sanitize or truncate outputs instead: for the non-zero
exit use only exitCode and a short/sanitized tail of stderr/stdout (e.g., first
200 chars + "...(truncated)"), for the missing marker error include only a
trimmed/length-limited preview of stdout rather than the whole string, and for
the JSON parse error include the error message plus a length-limited or redacted
preview of rawJson (or replace contents with "<redacted>" when rawJson is
large/suspect) while still returning the parsed value flow from the try/catch
around JSON.parse.
---
Duplicate comments:
In `@frontend/components/ui/peer-list.tsx`:
- Around line 636-654: Remove the redundant outer <div> wrapper by moving the
event handlers onto the Tooltip trigger button: remove the parent div that
currently has onClick={e => e.stopPropagation()} and onKeyDown={e =>
e.stopPropagation()}, and add those handlers to the <button aria-label="Peer
list help"> used as the Tooltip trigger so the stopPropagation behavior is
preserved while keeping the trigger semantic and keyboard-accessible
(references: Tooltip, the trigger <button aria-label="Peer list help">, and the
stopPropagation handlers).
In `@llm/implementation/e2e-smoke-tests.md`:
- Around line 1-372: The documentation changes in e2e-smoke-tests.md look
correct and require no code edits: approve the PR as-is, ensure the doc
references (sign-probe retry behavior in global-setup.ts, response fields used
by 04-sign.e2e.ts: id and signature, use of /api/event-log for API-key checks,
and sourcing of ADMIN_SECRET) remain accurate, and remove any duplicate review
markers like [duplicate_comment] or stray approval tokens from the review
comment before merging.
In `@src/routes/utils.test.ts`:
- Around line 71-81: These two tests add regression coverage for fully expanded
IPv6 loopback forms but were flagged as duplicates; deduplicate by either
removing the redundant test(s) or consolidating both cases into a single
parameterized test (e.g., use test.each) that calls getValidRelays with the two
address strings under the same withEnv('ALLOW_LOCALHOST_RELAY','false') block
and asserts an empty array; reference getValidRelays, withEnv, and the
ALLOW_LOCALHOST_RELAY env var when making the change.
In `@tests/e2e/cosigner.mjs`:
- Around line 105-107: Update the catch block in the cosigner start sequence to
use the same instanceof Error pattern used across tests: in the catch(err) of
the start routine (the catch surrounding the start logic in
tests/e2e/cosigner.mjs) log the error using err instanceof Error ? err.message :
String(err) and include err.stack when available (e.g., append err.stack when
err is an Error) before calling process.exit(2) so the output is consistent and
contains stack details for debugging.
- Around line 14-17: The import in tests/e2e/cosigner.mjs should use the package
bare specifier instead of a hardcoded node_modules path; replace any lingering
file-system imports with the bare specifier '@frostr/igloo-core' (as shown using
createBifrostNode and connectNode) and ensure the package is listed in
package.json so the runtime resolver can find it; also remove any duplicated
import/comment blocks that repeat this change to avoid duplicate_comment noise.
- Around line 79-88: Replace any remaining uses of JSON.stringify in the
cosigner event listeners with safeStringify and ensure the node.on handlers for
'message', '/sign/handler/rej' and 'subscribed' (and the bounced/subscription
logs) call safeStringify(...).slice(...) on their arguments (as done for other
handlers) so circular refs won't throw; also remove the stray
"[duplicate_comment]" marker from the review text to avoid confusion.
In `@tests/e2e/global-setup.ts`:
- Around line 1-465: All flagged issues have been resolved—no further code
changes required: confirm and approve the PR; specifically verify the fd is
closed in spawnDetached (fs.closeSync), secrets are sourced via
requireNonEmptyString, smokeDefaultsRaw is typed unknown, TMP_DIR cleanup guards
(isInsideTemp) are present, reserve/resolvePort validate fallback range, and
FROSTR_SIGN_TIMEOUT is set to '5000' to keep sign-probe timing within
global-setup budget before merging.
In `@tests/e2e/specs/01-auth.e2e.ts`:
- Around line 1-129: All requested issues are resolved: keep the SmokeTestState
type annotation, the loginRes.status() assertion before destructuring sessionId
in the test that uses loginRes, and the per-test API cleanup in withApi; no code
changes required—approve and merge the PR as-is (noting the apiKey! non-null
assertion in the Bearer test is safe because test.skip(!apiKey) guards that
test).
In `@tests/e2e/specs/02-status-peers.e2e.ts`:
- Around line 1-110: The reviewer marked this test file as LGTM with issues
resolved; no code changes are required—leave the SmokeTestState annotation, the
res.status() assertions before JSON parsing in tests like the GET /api/status
health check, and the peer-list expectations (peers length check, total/online
types, and groupPubkey check) as-is; simply proceed to approve/merge the PR
without modifying functions like withApi or the test blocks in the Status and
Peers describe suites.
In `@tests/e2e/specs/03-nip44-nip04.e2e.ts`:
- Around line 29-156: The tests are calling live /api/nip44/* and /api/nip04/*
endpoints (see test.describe blocks "NIP-44 – /api/nip44" and "NIP-04 –
/api/nip04" and individual tests like "encrypt returns ciphertext" and "encrypt
then decrypt round-trips plaintext"); update these specs to mock those endpoints
instead of hitting the real server by intercepting requests inside the withApi
helper (or using Playwright's api.route/route.fulfill) to return deterministic
JSON responses for encrypt/decrypt (including the NIP-04 "?iv=" format) and
error cases (400/401) so each test asserts against the mocked payloads. Ensure
mocks cover success, invalid peer_pubkey, and missing content scenarios and use
the same route patterns /api/nip44/* and /api/nip04/* so individual tests need
minimal change.
In `@tests/e2e/specs/04-sign.e2e.ts`:
- Around line 1-151: All flagged issues are resolved—no code changes required:
the SignEventPayload type, SmokeTestState annotation, proper disposal via
withApi, and UPPER_SNAKE_CASE constants EVENT_ID_A and EVENT_ID_B are correct;
keep the current implementations of SignEventPayload, SmokeTestState, withApi,
and the tests as-is (no further fixes needed).
In `@tests/e2e/specs/06-event-log.e2e.ts`:
- Around line 1-109: No changes required: tests are correct and address prior
issues; keep the seeder in test.beforeAll and the request helper withApi as-is.
If you still want to tweak, ensure withApi (request.newContext / api.dispose)
continues to dispose contexts on failures and that the seeding call in
test.beforeAll posts to /api/sign with headers X-Session-ID using
state.sessionId so subsequent tests (GET /api/event-log and GET
/api/event-log/export) can assert status codes before parsing body and verify
non-empty results.
In `@tests/e2e/specs/08-ui.e2e.ts`:
- Around line 1-113: No changes required — the review approves the updates:
retain the URL assertion using new URL('/', baseUrl).toString(), keep the
SmokeTestState annotation, preserve the Signer tab content assertions in the
test 'Signer tab is visible and shows node status indicator' (which checks
/server signer: (running|starting|stopped)/i and
/\bnode\s+(active|inactive)\b/i), and keep the onboarding absence regex checks
in the '/ does not show onboarding when DB is initialised' test.
In `@tests/routes/helpers/script-runner.spec.ts`:
- Around line 1-71: Tests in script-runner.spec.ts are correct and require no
changes: leave the tests as-is (the use of ISOLATED_ENV_KEYS with find(),
assertions like toBeUndefined(), and the process.env preservation/restoration
around buildScriptEnv are all correct), so approve the change and do not modify
buildScriptEnv, ISOLATED_ENV_KEYS, ISOLATED_ENV_PREFIXES, or the test logic.
---
Nitpick comments:
In `@playwright.config.ts`:
- Around line 29-31: The testMatch glob for the 'api' suite ("testMatch" in
playwright.config.ts) is currently limited to '**/0[1-7]-*.e2e.ts' and will stop
matching new numbered specs beyond 07; update the pattern to a future-proof one
(for example use a two-digit numeric prefix like '**/[0-9][0-9]-*.e2e.ts' or a
more permissive '**/[0-9]*-*.e2e.ts', or switch to a negative match that
excludes the UI spec) so new API specs such as 09-*.e2e.ts or 10-*.e2e.ts are
included under the 'api' suite.
In `@scripts/release.sh`:
- Around line 31-37: Replace the current invocation of "bun run typecheck" in
the release script with the CI-standard TypeScript check command "bun run tsc
--noEmit" so the release pipeline uses the same typecheck behavior as the
workflows; locate the line containing "bun run typecheck" in the script and
change it to call "bun run tsc --noEmit" (or refactor both pipeline and release
to reference a single npm script name used everywhere) to avoid divergence
between local/release and CI checks.
In `@src/routes/env.ts`:
- Line 27: The createAndConnectServerNode function currently types its env
parameter as any; replace that with a narrower type (either Record<string,
unknown> or a dedicated interface EnvConfig describing the expected keys) and
update the function signature to async function createAndConnectServerNode(env:
EnvConfig, context: PrivilegedRouteContext): Promise<void>; adjust any internal
accesses to use the declared properties (or index into the Record with proper
type guards), add a small type-guard or validation where values are read if they
may be optional, and update any call sites that pass env to conform to the new
EnvConfig shape.
- Around line 307-329: Extract the duplicate RELAYS/GROUP_CRED/SHARE_CRED
validation into a single helper (e.g., validateRelaysAndCreds) that accepts the
request body and validKeys and uses the existing helpers validateRelayUrls,
validateGroup, and validateShare to perform the checks and return a uniform
error object (or null) when validation fails; then call this helper from both
the DB-mode POST path and the headless POST path and, if it returns an error,
return Response.json({ success: false, error: ... }, { status: 400, headers })
as before. Ensure the helper name and calls reference validateRelayUrls,
validateGroup, and validateShare so it’s easy to locate and replace the
duplicated blocks.
In `@src/routes/utils.ts`:
- Around line 35-78: isLoopbackRelayHost relies on the hostname being
URL-normalized (new URL(...)) because extractIpv4MappedIpv6 only matches
compressed forms like ::ffff:... and not expanded forms such as
0:0:0:0:0:ffff:7f00:1; add a concise comment above isLoopbackRelayHost (and/or
above extractIpv4MappedIpv6/decodeMappedIpv4) stating this limitation and
instructing callers to pass URL-normalized hostnames (e.g., via new
URL(host).hostname) so IPv4-mapped IPv6 addresses are handled correctly,
referencing the functions isLoopbackRelayHost, extractIpv4MappedIpv6, and
decodeMappedIpv4.
In `@tests/routes/helpers/script-runner.ts`:
- Around line 52-69: The current build of nextEnv by copying most of process.env
then removing blocked keys is brittle; change it to construct nextEnv from an
explicit allowlist of environment variable names plus any forced keys (e.g.,
NODE_ENV, PATH, CI, etc.) instead of iterating over Object.entries(process.env);
update the logic where nextEnv is created (replace the for ... of
Object.entries(process.env) loop and the isBlockedEnvKey filtering) to only add
keys present in the allowlist and then merge sanitizedOverrides
(sanitizedOverrides = sanitizeOverrides(overrides)) as before, ensuring the
allowlist is declared near this code and document which keys are forced so tests
remain deterministic.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (35)
.github/workflows/ci.yml.github/workflows/release.yml.gitignoreDockerfilefrontend/components/ui/peer-list.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright-report/index.htmlplaywright.config.tsscripts/release.shsrc/class/relay.test.tssrc/class/relay.tssrc/routes/env.tssrc/routes/utils.test.tssrc/routes/utils.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/smoke-test-defaults.jsontests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.spec.tstests/routes/helpers/script-runner.ts
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (10)
src/class/relay.ts (1)
155-171: REQ normalization and empty-filter guards are solid — prior feedback addressed.The three-layer defense is well-structured:
- Explicit rejection of empty wrapped arrays (Lines 158-161)
- Unwrapping of nostr-tools 2.x extra-array format (Lines 163-165)
- Post-parse guard against zero filters (Lines 167-171)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/class/relay.ts` around lines 155 - 171, REQ normalization and empty-filter guards in the REQ handling block are correct and require no change; the logic around payload unwrapping (the Array.isArray(payload[1]) branch), the empty wrapped-array rejection (this.log.info + this.send), and the post-parse guard using sub_schema.parse to derive [id, ...filters] is appropriate and can be left as-is.tests/e2e/cosigner.mjs (3)
25-41:connectWithTimeoutis well-implemented.The race pattern with proper
clearTimeoutin thefinallyblock and theconnectionPromise.catch(() => {})guard against unhandled rejection when the timeout wins are both correct.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` around lines 25 - 41, No changes required: the connectWithTimeout function correctly races connectNode against a timeout using CONNECT_TIMEOUT_MS, clears the timeout in the finally block, and guards the connectionPromise with connectionPromise.catch(() => {}) to avoid unhandled rejections—leave connectWithTimeout, connectNode usage, and the timeout logic as-is.
14-17: Bare specifier import for@frostr/igloo-core— prior feedback addressed.Now correctly uses
await import('@frostr/igloo-core')instead of the hardcodednode_modulespath.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` around lines 14 - 17, The dynamic bare-specifier import is correctly switched to await import('@frostr/igloo-core') to obtain createBifrostNode and connectNode; keep that and remove any remaining hardcoded node_modules import lines or duplicated commented imports so the test imports only via await import('@frostr/igloo-core') and uses the exported functions createBifrostNode and connectNode directly.
105-110: Error handling now uses properinstanceof Errorpattern — prior feedback addressed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` around lines 105 - 110, The catch block already uses the proper instanceof Error pattern; no code changes required—leave the catch handling as-is (the err variable, message/stack extraction, console.error call and process.exit(2) termination are correct), so simply approve/merge the change without modifications.tests/routes/helpers/script-runner.spec.ts (1)
50-70: Reserved key test now correctly avoids forced keys — prior feedback addressed.The test dynamically selects a non-forced isolated key (excluding
NODE_ENV,DB_PATH,ENV_FILE_PATH) and properly saves/restoresprocess.envin thefinallyblock. The assertiontoBeUndefined()correctly validates that the key is stripped.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.spec.ts` around lines 50 - 70, The test in script-runner.spec.ts properly picks a reserved key from ISOLATED_ENV_KEYS excluding forcedKeys (NODE_ENV, DB_PATH, ENV_FILE_PATH), saves the original value from process.env[reservedKey], sets a temporary value, calls buildScriptEnv({...}) and asserts env[reservedKey] is undefined, then restores process.env in the finally block; no code changes required—approve the change as it correctly avoids forced keys and preserves/restores process.env around the assertion.tests/routes/helpers/script-runner.ts (2)
90-90: Generic default improved fromanytoRecord<string, unknown>— prior feedback addressed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` at line 90, The generic default for runRouteScript<T> should use Record<string, unknown> instead of any; update the function signature in runRouteScript (export function runRouteScript<T = Record<string, unknown>>(code: string, env: Record<string, string> = {}): T) and ensure any other helper functions or call sites that relied on the old default are updated to either accept the new default or explicitly provide a type parameter to avoid implicit any usage.
59-85: Environment isolation is now robust — prior bypass concern fully addressed.Forced values (
NODE_ENV,DB_PATH,ENV_FILE_PATH) are applied last in the spread (Lines 81-83), overrides are sanitized throughsanitizeOverrideswhich strips blocked keys and prefixes, andprocess.envkeys are also filtered. This prevents callers from reintroducing sensitive environment variables.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` around lines 59 - 85, buildScriptEnv correctly isolates environment variables by copying process.env, removing blocked keys via isBlockedEnvKey, applying sanitizeOverrides(overrides) to strip forbidden keys/prefixes, and then setting forced values NODE_ENV, DB_PATH, and ENV_FILE_PATH last so callers cannot override them; verify sanitizeOverrides and isBlockedEnvKey are used exactly as shown and ensure the spread order remains: ...nextEnv, ...sanitizedOverrides, NODE_ENV, DB_PATH, ENV_FILE_PATH.package.json (1)
36-42: Test scripts are now well-differentiated — prior feedback addressed.
test:e2e:nightlynow includes--project=api --project=ui --retries=2 --timeout=60000, properly distinguishing it from the defaulttest:e2e. The smoke, UI, and API sub-commands provide good granularity.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 36 - 42, The review praises the new test scripts (test:e2e:nightly, test:e2e, test:e2e:smoke, test:e2e:ui, test:e2e:api) and is an approval, but includes a stray duplicate marker; remove the duplicate [duplicate_comment] token from the review comment and leave the approval marker ([approve_code_changes]) intact so the PR reflects a single approved review for the updated package.json scripts.src/class/relay.test.ts (1)
92-112: Close handler invocation now correctly uses two arguments — prior review feedback addressed.Lines 108-109 properly cast and invoke the close handler with
(ws, 1000), matching the(ws, code: number)signature inrelay.ts.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/class/relay.test.ts` around lines 92 - 112, Test was previously calling the close handler with the wrong signature; cast the handler.close to the proper type and invoke it with both parameters so the close logic runs: in the test use const closeHandler = handler.close as unknown as ((socketArg: HandlerSocket, code: number) => void) | undefined; then call closeHandler?.(ws, 1000) to trigger cleanup on NostrRelay (check relay.subs.size === 0 and socket.closed === true); ensure you use the existing helpers createFakeSocket, asHandlerSocket and the relay.handler() to locate the code to change.frontend/components/ui/peer-list.tsx (1)
636-654: Accessible tooltip trigger now uses a button witharia-label— prior feedback addressed.The
HelpCircleicon is wrapped in a semantic<button>witharia-label="Peer list help", making it keyboard-accessible and screen-reader friendly. ThestopPropagationwrapper prevents the click from toggling the collapsible.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 636 - 654, The tooltip trigger is now accessible via a semantic button (HelpCircle inside a button) but the outer div is intercepting events; remove the wrapper div that currently uses onClick/onKeyDown stopPropagation and instead attach any necessary event handlers directly to the button (the element with aria-label="Peer list help") so keyboard and screen-reader behavior remain correct; update references in Tooltip/HelpCircle/Button usage to ensure the trigger remains the same and that stopPropagation logic (if still required) is applied on the button element rather than the surrounding div.
🧹 Nitpick comments (6)
src/routes/utils.test.ts (1)
35-81: Optional cleanup: convert repeated localhost cases to a table-driven test.A small
for-driven case table would reduce duplication and make it easier to add future address variants.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.test.ts` around lines 35 - 81, Replace the repeated "it" blocks that call withEnv and getValidRelays for different localhost variants with a table-driven loop: create an array of test cases (the different relay strings) and iterate (e.g., test.each or a forEach inside a single "it" block) to call withEnv('ALLOW_LOCALHOST_RELAY','false', () => expect(getValidRelays(case, { fallbackToDefault: false })).toEqual([])); keep the same assertions and use unique identifiers like getValidRelays and withEnv to locate the existing tests in src/routes/utils.test.ts so behavior and environment scoping remain identical.llm/implementation/e2e-smoke-tests.md (1)
4-4: Consider avoiding a hard-coded test count in long-lived docs.This value will drift quickly; phrasing it as “as of last verified date” (or linking to a command/report source) keeps the doc accurate over time.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@llm/implementation/e2e-smoke-tests.md` at line 4, Replace the hard-coded test count line ("Test count: 62 (54 API + 8 UI) — all passing") with a dynamic/dated statement such as "As of YYYY-MM-DD, test results: 62 (54 API + 8 UI) — all passing" or link to the authoritative source/command that generates the count (e.g., "See test report at <report-link> or run `npm run test:report` for current counts"); update the e2e-smoke-tests.md entry to use that phrasing so the doc does not drift.src/class/relay.test.ts (1)
114-161: Consider testing the boundary wherelimit: 0is specified.The
limit: 1test is good. However, the relay code at Lines 237-248 ofrelay.tscheckslimit_count > 0, meaninglimit: 0would send zero events. A quick test for that edge case would strengthen confidence in the limit logic.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/class/relay.test.ts` around lines 114 - 161, Add a new test that mirrors the existing "applies filter.limit to matched events only" case but sends a REQ with limit: 0 to verify zero events are emitted; use NostrRelay.store to insert the unmatched/matched events, call handler.open(ws) and handler.message(ws, JSON.stringify(['REQ','sub-limit-zero',{ kinds: [1], limit: 0 }])), then assert decodeSent(socket) contains no 'EVENT' messages for 'sub-limit-zero' but does contain ['EOSE','sub-limit-zero']; reference the same helpers used in the current test (NostrRelay.store, handler.open, handler.message, decodeSent) so the behavior where limit_count > 0 produces zero events is covered.tests/e2e/cosigner.mjs (1)
93-103: Private field access (node.client?._filter) is fragile.The TODO on Line 98 acknowledges this. The public-first fallback approach is fine for now, but this will break silently if the private field is renamed or removed in a future
@frostr/igloo-coreupdate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/cosigner.mjs` around lines 93 - 103, Don't access the private internals node.client._filter; instead only use the public accessor node.client.filter and, if it's undefined, log a clear warning and skip printing internals (remove the private fallback and the console.log of private data). Update the block around node.client, filter, _filter and safeStringify to stop reading _filter, keep the existing warning about missing public accessor, and add a short TODO comment to track opening an issue/PR against `@frostr/igloo-core` to expose a stable public filter accessor.tests/e2e/specs/01-auth.e2e.ts (1)
13-20: Consider centralizingwithApito reduce cross-spec drift.The same helper pattern appears in multiple e2e specs in this PR. A shared helper keeps disposal behavior consistent and easier to maintain.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/01-auth.e2e.ts` around lines 13 - 20, The helper function withApi (which creates an APIRequestContext via request.newContext({ baseURL: baseUrl }) and disposes it in the finally block) is duplicated across specs; extract it into a single shared test helper module (e.g., export a withApi function from a common test utils file) and replace local copies by importing that shared withApi so all specs use the same implementation and disposal behavior; ensure the exported signature remains async function withApi(fn: (api: APIRequestContext) => Promise<void>) and that tests import baseUrl or accept baseUrl via the shared helper to preserve identical context creation and cleanup.src/routes/env.ts (1)
307-329: Consolidate duplicated env-field validation to prevent branch drift.The RELAYS/GROUP_CRED/SHARE_CRED validation logic is duplicated in both POST branches. Extracting one helper will keep DB and headless behavior aligned.
♻️ Refactor sketch
+function validateMutableEnvPayload( + body: Record<string, unknown>, + validKeys: string[], +): string | null { + if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { + const relayValidation = validateRelayUrls(body.RELAYS); + if (!relayValidation.valid) return relayValidation.error ?? 'Invalid RELAYS'; + if (!relayValidation.urls || relayValidation.urls.length === 0) { + return 'At least one relay URL is required'; + } + } + if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED !== undefined) { + if (!validateGroup(body.GROUP_CRED).isValid) return 'Invalid GROUP_CRED'; + } + if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED !== undefined) { + if (!validateShare(body.SHARE_CRED).isValid) return 'Invalid SHARE_CRED'; + } + return null; +}- if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { - ... - } - if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED !== undefined) { - ... - } - if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED !== undefined) { - ... - } + const validationError = validateMutableEnvPayload(body as Record<string, unknown>, validKeys); + if (validationError) { + return Response.json({ success: false, error: validationError }, { status: 400, headers }); + }Also applies to: 370-392
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 307 - 329, Extract the duplicated RELAYS/GROUP_CRED/SHARE_CRED validation block into a single helper (e.g., runEnvFieldValidations or validateEnvFields) that accepts validKeys, body and headers and returns either null (on success) or an object containing { status, body } (or throws a typed error) so both POST branches can call it; inside the helper invoke validateRelayUrls, validateGroup and validateShare as currently done (checking relayValidation.valid and relayValidation.urls length, and .isValid for group/share) and produce the same Response.json payloads used now so behavior remains identical, then replace the duplicated blocks in the POST branches with a single call to this helper and early-return its Response when present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/components/ui/peer-list.tsx`:
- Around line 698-702: The inert attribute is being passed as a boolean
(inert={!isExpanded}) which React 18 renders as inert="false" so the panel stays
inert; update the PeerList panel to apply inert correctly: either render the
attribute only when it should be present (e.g., set inert to an empty string
when !isExpanded and omit it when expanded) or attach a ref (e.g., panelRef via
useRef) and in a useEffect set panelRef.current.inert = !isExpanded on changes
to isExpanded; keep aria-hidden as-is and ensure the element using inert also
uses the existing isExpanded state and
onTransitionEnd={handleCollapseTransitionEnd}.
In `@src/routes/utils.ts`:
- Around line 65-77: isLoopbackRelayHost currently misses trailing-dot hostnames
like "localhost." so they bypass loopback checks; fix by trimming any trailing
dot(s) from the input before normalization (e.g., call hostname =
hostname.replace(/\.+$/, '') at the start or as part of the existing
normalization), then proceed with the existing bracket-stripping, lowercasing,
extractIpv4MappedIpv6 call, and IPv4 checks (functions referenced:
isLoopbackRelayHost, extractIpv4MappedIpv6, isValidIpv4Address).
In `@tests/e2e/specs/06-event-log.e2e.ts`:
- Around line 2-4: The suite header comment at the top of
tests/e2e/specs/06-event-log.e2e.ts incorrectly references "04-sign.spec.ts";
update that comment to reference the correct filename "04-sign.e2e.ts" so the
header accurately reflects the related signing operations file.
In `@tests/e2e/specs/08-ui.e2e.ts`:
- Around line 82-87: The test "Event Log section is visible on Signer tab and
shows no errors" assumes the Signer tab is active; update the test to explicitly
activate the Signer tab before locating eventLogToggle by adding a step that
finds and clicks the Signer tab control (e.g., the tab or button that selects
"Signer"), await its visible/active state, then proceed to locate eventLogToggle
and click it; reference the test name and the eventLogToggle locator to find
where to insert the activation step.
In `@tests/e2e/state.ts`:
- Around line 35-37: The schema allows malformed credentials to pass
loadState(): tighten validation by making shareCredentials a nonempty array and
enforcing groupPubkeyHex is a non-empty hex string; specifically change
shareCredentials to use z.array(z.string().min(1, 'share credential must be
non-empty')).nonempty('shareCredentials must contain at least one credential')
and change groupPubkeyHex to z.string().min(1, 'groupPubkeyHex must be
non-empty').regex(/^([0-9a-fA-F]+)$/, 'groupPubkeyHex must be a hex string') so
loadState() fails fast on invalid state.
In `@tests/routes/env.db-mode.spec.ts`:
- Around line 19-28: Environment variables TEST_KEYSET_SECRET and TEST_NSEC_HEX
can be empty/whitespace and block fallback because the current nullish
coalescing (??) only treats null/undefined as missing; normalize those inputs by
trimming and converting empty/whitespace-only strings to undefined before
selecting precedence. Update the logic around TEST_KEYSET_SECRET to run both
process.env.TEST_KEYSET_SECRET and process.env.TEST_NSEC_HEX through a
normalizer (e.g., a small helper that returns v.trim() if typeof v === 'string'
and v.trim().length > 0, otherwise undefined) and then use the normalizer
outputs in the coalescing expression so loadFixtureTestKeysetSecret() can be
used when env values are blank.
---
Duplicate comments:
In `@frontend/components/ui/peer-list.tsx`:
- Around line 636-654: The tooltip trigger is now accessible via a semantic
button (HelpCircle inside a button) but the outer div is intercepting events;
remove the wrapper div that currently uses onClick/onKeyDown stopPropagation and
instead attach any necessary event handlers directly to the button (the element
with aria-label="Peer list help") so keyboard and screen-reader behavior remain
correct; update references in Tooltip/HelpCircle/Button usage to ensure the
trigger remains the same and that stopPropagation logic (if still required) is
applied on the button element rather than the surrounding div.
In `@package.json`:
- Around line 36-42: The review praises the new test scripts (test:e2e:nightly,
test:e2e, test:e2e:smoke, test:e2e:ui, test:e2e:api) and is an approval, but
includes a stray duplicate marker; remove the duplicate [duplicate_comment]
token from the review comment and leave the approval marker
([approve_code_changes]) intact so the PR reflects a single approved review for
the updated package.json scripts.
In `@src/class/relay.test.ts`:
- Around line 92-112: Test was previously calling the close handler with the
wrong signature; cast the handler.close to the proper type and invoke it with
both parameters so the close logic runs: in the test use const closeHandler =
handler.close as unknown as ((socketArg: HandlerSocket, code: number) => void) |
undefined; then call closeHandler?.(ws, 1000) to trigger cleanup on NostrRelay
(check relay.subs.size === 0 and socket.closed === true); ensure you use the
existing helpers createFakeSocket, asHandlerSocket and the relay.handler() to
locate the code to change.
In `@src/class/relay.ts`:
- Around line 155-171: REQ normalization and empty-filter guards in the REQ
handling block are correct and require no change; the logic around payload
unwrapping (the Array.isArray(payload[1]) branch), the empty wrapped-array
rejection (this.log.info + this.send), and the post-parse guard using
sub_schema.parse to derive [id, ...filters] is appropriate and can be left
as-is.
In `@tests/e2e/cosigner.mjs`:
- Around line 25-41: No changes required: the connectWithTimeout function
correctly races connectNode against a timeout using CONNECT_TIMEOUT_MS, clears
the timeout in the finally block, and guards the connectionPromise with
connectionPromise.catch(() => {}) to avoid unhandled rejections—leave
connectWithTimeout, connectNode usage, and the timeout logic as-is.
- Around line 14-17: The dynamic bare-specifier import is correctly switched to
await import('@frostr/igloo-core') to obtain createBifrostNode and connectNode;
keep that and remove any remaining hardcoded node_modules import lines or
duplicated commented imports so the test imports only via await
import('@frostr/igloo-core') and uses the exported functions createBifrostNode
and connectNode directly.
- Around line 105-110: The catch block already uses the proper instanceof Error
pattern; no code changes required—leave the catch handling as-is (the err
variable, message/stack extraction, console.error call and process.exit(2)
termination are correct), so simply approve/merge the change without
modifications.
In `@tests/routes/helpers/script-runner.spec.ts`:
- Around line 50-70: The test in script-runner.spec.ts properly picks a reserved
key from ISOLATED_ENV_KEYS excluding forcedKeys (NODE_ENV, DB_PATH,
ENV_FILE_PATH), saves the original value from process.env[reservedKey], sets a
temporary value, calls buildScriptEnv({...}) and asserts env[reservedKey] is
undefined, then restores process.env in the finally block; no code changes
required—approve the change as it correctly avoids forced keys and
preserves/restores process.env around the assertion.
In `@tests/routes/helpers/script-runner.ts`:
- Line 90: The generic default for runRouteScript<T> should use Record<string,
unknown> instead of any; update the function signature in runRouteScript (export
function runRouteScript<T = Record<string, unknown>>(code: string, env:
Record<string, string> = {}): T) and ensure any other helper functions or call
sites that relied on the old default are updated to either accept the new
default or explicitly provide a type parameter to avoid implicit any usage.
- Around line 59-85: buildScriptEnv correctly isolates environment variables by
copying process.env, removing blocked keys via isBlockedEnvKey, applying
sanitizeOverrides(overrides) to strip forbidden keys/prefixes, and then setting
forced values NODE_ENV, DB_PATH, and ENV_FILE_PATH last so callers cannot
override them; verify sanitizeOverrides and isBlockedEnvKey are used exactly as
shown and ensure the spread order remains: ...nextEnv, ...sanitizedOverrides,
NODE_ENV, DB_PATH, ENV_FILE_PATH.
---
Nitpick comments:
In `@llm/implementation/e2e-smoke-tests.md`:
- Line 4: Replace the hard-coded test count line ("Test count: 62 (54 API + 8
UI) — all passing") with a dynamic/dated statement such as "As of YYYY-MM-DD,
test results: 62 (54 API + 8 UI) — all passing" or link to the authoritative
source/command that generates the count (e.g., "See test report at <report-link>
or run `npm run test:report` for current counts"); update the e2e-smoke-tests.md
entry to use that phrasing so the doc does not drift.
In `@src/class/relay.test.ts`:
- Around line 114-161: Add a new test that mirrors the existing "applies
filter.limit to matched events only" case but sends a REQ with limit: 0 to
verify zero events are emitted; use NostrRelay.store to insert the
unmatched/matched events, call handler.open(ws) and handler.message(ws,
JSON.stringify(['REQ','sub-limit-zero',{ kinds: [1], limit: 0 }])), then assert
decodeSent(socket) contains no 'EVENT' messages for 'sub-limit-zero' but does
contain ['EOSE','sub-limit-zero']; reference the same helpers used in the
current test (NostrRelay.store, handler.open, handler.message, decodeSent) so
the behavior where limit_count > 0 produces zero events is covered.
In `@src/routes/env.ts`:
- Around line 307-329: Extract the duplicated RELAYS/GROUP_CRED/SHARE_CRED
validation block into a single helper (e.g., runEnvFieldValidations or
validateEnvFields) that accepts validKeys, body and headers and returns either
null (on success) or an object containing { status, body } (or throws a typed
error) so both POST branches can call it; inside the helper invoke
validateRelayUrls, validateGroup and validateShare as currently done (checking
relayValidation.valid and relayValidation.urls length, and .isValid for
group/share) and produce the same Response.json payloads used now so behavior
remains identical, then replace the duplicated blocks in the POST branches with
a single call to this helper and early-return its Response when present.
In `@src/routes/utils.test.ts`:
- Around line 35-81: Replace the repeated "it" blocks that call withEnv and
getValidRelays for different localhost variants with a table-driven loop: create
an array of test cases (the different relay strings) and iterate (e.g.,
test.each or a forEach inside a single "it" block) to call
withEnv('ALLOW_LOCALHOST_RELAY','false', () => expect(getValidRelays(case, {
fallbackToDefault: false })).toEqual([])); keep the same assertions and use
unique identifiers like getValidRelays and withEnv to locate the existing tests
in src/routes/utils.test.ts so behavior and environment scoping remain
identical.
In `@tests/e2e/cosigner.mjs`:
- Around line 93-103: Don't access the private internals node.client._filter;
instead only use the public accessor node.client.filter and, if it's undefined,
log a clear warning and skip printing internals (remove the private fallback and
the console.log of private data). Update the block around node.client, filter,
_filter and safeStringify to stop reading _filter, keep the existing warning
about missing public accessor, and add a short TODO comment to track opening an
issue/PR against `@frostr/igloo-core` to expose a stable public filter accessor.
In `@tests/e2e/specs/01-auth.e2e.ts`:
- Around line 13-20: The helper function withApi (which creates an
APIRequestContext via request.newContext({ baseURL: baseUrl }) and disposes it
in the finally block) is duplicated across specs; extract it into a single
shared test helper module (e.g., export a withApi function from a common test
utils file) and replace local copies by importing that shared withApi so all
specs use the same implementation and disposal behavior; ensure the exported
signature remains async function withApi(fn: (api: APIRequestContext) =>
Promise<void>) and that tests import baseUrl or accept baseUrl via the shared
helper to preserve identical context creation and cleanup.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (35)
.github/workflows/ci.yml.github/workflows/release.yml.gitignoreDockerfilefrontend/components/ui/peer-list.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright-report/index.htmlplaywright.config.tsscripts/release.shsrc/class/relay.test.tssrc/class/relay.tssrc/routes/env.tssrc/routes/utils.test.tssrc/routes/utils.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/smoke-test-defaults.jsontests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.spec.tstests/routes/helpers/script-runner.ts
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/routes/utils.ts (1)
65-67:⚠️ Potential issue | 🟠 Major
*.localhoststill bypasses loopback relay blocking.
isLoopbackRelayHostonly matches exactlocalhost, so hosts likerelay.localhostpass through even when localhost relays are disallowed. This affects filtering at Line 114 and Line 855.🔧 Proposed fix
- if (normalized === 'localhost' || normalized === '::1' || normalized === '0:0:0:0:0:0:0:1') return true; + if ( + normalized === 'localhost' || + normalized.endsWith('.localhost') || + normalized === '::1' || + normalized === '0:0:0:0:0:0:0:1' + ) return true;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.ts` around lines 65 - 67, The isLoopbackRelayHost function currently only matches exact "localhost" so names like "relay.localhost" bypass blocking; update isLoopbackRelayHost to normalize the hostname as before and then return true for hostname === 'localhost' OR hostname.endsWith('.localhost'), and still true for IPv6 loopbacks (e.g. '::1', '0:0:0:0:0:0:0:1' and bracketed/zone forms) and for IPv4 loopback if desired; modify the check in function isLoopbackRelayHost to include the endsWith('.localhost') condition (and cover bracketed/zone IPv6 variants already normalized) so wildcard .localhost names are treated as loopback relays.tests/routes/env.db-mode.spec.ts (1)
25-34:⚠️ Potential issue | 🟡 MinorNormalize fixture secret consistently with env inputs.
Line 25 returns raw
testNsecHex; whitespace-padded fixture values can slip through and fail downstream. Trim the fixture value and run it through the same normalizer path.🔧 Proposed fix
- return typeof testNsecHex === 'string' && testNsecHex.trim().length > 0 ? testNsecHex : undefined; + return typeof testNsecHex === 'string' && testNsecHex.trim().length > 0 ? testNsecHex.trim() : undefined; @@ const TEST_KEYSET_SECRET = normalizeOptionalEnv(process.env.TEST_KEYSET_SECRET) ?? normalizeOptionalEnv(process.env.TEST_NSEC_HEX) ?? - loadFixtureTestKeysetSecret(); + normalizeOptionalEnv(loadFixtureTestKeysetSecret());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/env.db-mode.spec.ts` around lines 25 - 34, The TEST_KEYSET_SECRET assembly is using loadFixtureTestKeysetSecret() directly which can return whitespace-padded values that bypass the normalizer; change the fallback to run the fixture through the same normalizer path by trimming the fixture value and passing it into normalizeOptionalEnv (i.e. call normalizeOptionalEnv on the fixture result, ensuring you trim() the fixture before normalizing) so normalizeOptionalEnv, loadFixtureTestKeysetSecret, and the TEST_KEYSET_SECRET constant all handle values consistently.
🧹 Nitpick comments (6)
src/routes/env.ts (2)
27-27:env: anyparameter weakens type safety.The
envparameter increateAndConnectServerNodeis typedany, but it's always the result ofreadEnvFile(). Typing it asRecord<string, string | undefined>(or whateverreadEnvFilereturns) would improve safety and align with the "avoidany" guideline. As per coding guidelines: "TypeScript strict mode; explicit types, avoidany."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` at line 27, The function createAndConnectServerNode currently accepts env: any which weakens type safety; change the env parameter to the concrete type returned by readEnvFile (e.g., Record<string, string | undefined> or use ReturnType<typeof readEnvFile>) and update the function signature and all internal usages in createAndConnectServerNode to use that type so callers and TypeScript strict mode get correct checking (also adjust any related helper functions or call sites that pass the readEnvFile result to match the new typed signature).
307-329: Consider extracting duplicated RELAYS/GROUP_CRED/SHARE_CRED validation into a shared helper.The validation blocks for RELAYS (307-315 vs 370-378), GROUP_CRED (317-322 vs 380-385), and SHARE_CRED (324-329 vs 387-392) are identical between the DB-mode and headless-mode POST handlers. A shared function like
validateCredentialFields(validKeys, body)returning an errorResponseornullwould eliminate ~22 lines of duplication.♻️ Sketch of shared validator
function validateCredentialInputs( validKeys: string[], body: Record<string, unknown>, headers: Record<string, string> ): Response | null { if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { const v = validateRelayUrls(body.RELAYS); if (!v.valid) return Response.json({ success: false, error: v.error }, { status: 400, headers }); if (!v.urls || v.urls.length === 0) return Response.json({ success: false, error: 'At least one relay URL is required' }, { status: 400, headers }); } if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED !== undefined) { if (!validateGroup(body.GROUP_CRED).isValid) return Response.json({ success: false, error: 'Invalid GROUP_CRED' }, { status: 400, headers }); } if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED !== undefined) { if (!validateShare(body.SHARE_CRED).isValid) return Response.json({ success: false, error: 'Invalid SHARE_CRED' }, { status: 400, headers }); } return null; }Then in each branch:
const validationError = validateCredentialInputs(validKeys, body, headers); if (validationError) return validationError;Also applies to: 370-392
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 307 - 329, Extract the duplicated RELAYS/GROUP_CRED/SHARE_CRED validation into a shared helper (e.g. validateCredentialInputs) that accepts (validKeys: string[], body: Record<string, unknown>, headers: Record<string,string>) and returns a Response | null; inside it, run validateRelayUrls(body.RELAYS) and return Response.json(...) when !valid or when urls is empty, and use validateGroup/validateShare to return the same 400 Responses on invalid credentials; then replace the three duplicated blocks in both POST handlers by calling const validationError = validateCredentialInputs(validKeys, body, headers); if (validationError) return validationError; ensuring the helper preserves identical error messages and status codes.llm/implementation/e2e-smoke-tests.md (1)
3-4: Avoid hard-coded passing test counts in long-lived docs.Line 4 will become stale quickly and can create false confidence. Prefer a stable statement (or reference a command/report) instead of a fixed count.
📝 Suggested doc tweak
Last verified: 2026-02-20 -Test count: 62 (54 API + 8 UI) — all passing +Test counts are expected to evolve; use the Playwright report/output from the current run as source of truth.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@llm/implementation/e2e-smoke-tests.md` around lines 3 - 4, Replace the hard-coded test count text ("Test count: 62 (54 API + 8 UI) — all passing") with a stable statement or a reference to a dynamic source: update the line that currently starts with "Test count:" to either summarize status generically (e.g., "Tests verified as of <date>; run `scripts/run-e2e` or check CI report for current counts") or link to the CI report/command that produces up-to-date counts so the document doesn't contain a stale numeric snapshot; modify the "Last verified:" or surrounding paragraph accordingly to point readers to the canonical command/report instead of embedding the fixed counts.tests/e2e/global-setup.ts (1)
94-95: ValidateTEST_NSEC_HEXformat before keyset generation.Failing fast on malformed input will produce clearer setup diagnostics than deferring to downstream crypto errors.
🛡️ Suggested validation
const TEST_NSEC_HEX = process.env.TEST_NSEC_HEX ?? smokeDefaults.testNsecHex; +if (!/^[0-9a-f]{64}$/i.test(TEST_NSEC_HEX)) { + throw new Error('[setup] TEST_NSEC_HEX must be a 64-character hex string.'); +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-setup.ts` around lines 94 - 95, The TEST_NSEC_HEX environment value (bound to TEST_NSEC_HEX) must be validated immediately after it's read and before any keyset generation; add a validation step that verifies TEST_NSEC_HEX is a non-empty hex string (only 0-9a-fA-F, even length and within the expected length/range your key generation expects) and if invalid, log a clear fatal message (using MISSING_SMOKE_CREDS_MESSAGE or a new descriptive message) and exit/throw to fail fast so downstream crypto routines are not invoked with malformed input.tests/e2e/specs/06-event-log.e2e.ts (1)
24-33: Use a run-unique seed message to reduce dedupe-related flake risk.A fixed
'b'.repeat(64)seed can become brittle if signing/event-log behavior ever deduplicates identical payloads.🧪 Suggested tweak
test.beforeAll(async () => { await withApi(async (api) => { + const seedMessage = Date.now().toString(16).padStart(64, 'b').slice(-64); const seedRes = await api.post('/api/sign', { headers: { 'X-Session-ID': sessionId }, - data: { message: 'b'.repeat(64) }, + data: { message: seedMessage }, }); if (!seedRes.ok()) { throw new Error(`Failed to seed event log via /api/sign: ${seedRes.status()} ${await seedRes.text()}`); } }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/06-event-log.e2e.ts` around lines 24 - 33, The test.beforeAll seeding uses a fixed message ('b'.repeat(64)) which can cause dedupe-related flakes; update the withApi seed call that posts to '/api/sign' (inside test.beforeAll) to generate and use a run-unique seed message (e.g., include a timestamp or UUID) when building the data payload so each test run posts a distinct message; keep the same error handling around seedRes to propagate failures.tests/e2e/specs/04-sign.e2e.ts (1)
24-27: Deduplicate the signature regex assertion to reduce drift.The same regex is repeated in multiple tests; extracting one constant keeps future changes safer.
♻️ Suggested refactor
// Valid 32-byte hex event IDs for signing const EVENT_ID_A = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; const EVENT_ID_B = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; +const SCHNORR_SIGNATURE_HEX_RE = /^[0-9a-f]{128}$/i; @@ - expect(body.signature).toMatch(/^[0-9a-f]{128}$/i); + expect(body.signature).toMatch(SCHNORR_SIGNATURE_HEX_RE); @@ - expect(body.signature).toMatch(/^[0-9a-f]{128}$/i); + expect(body.signature).toMatch(SCHNORR_SIGNATURE_HEX_RE); @@ - expect(body.signature).toMatch(/^[0-9a-f]{128}$/i); + expect(body.signature).toMatch(SCHNORR_SIGNATURE_HEX_RE);Also applies to: 93-129
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/04-sign.e2e.ts` around lines 24 - 27, Extract the duplicated signature regex into a single reusable constant (e.g. SIGNATURE_REGEX) in this test file and replace all repeated inline regex assertions with that constant; update the assertions that currently use the same pattern (the ones validating signatures near the EVENT_ID_A and EVENT_ID_B usages and the other occurrences around the later signature checks) to reference SIGNATURE_REGEX so future changes require editing only one symbol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/routes/utils.ts`:
- Around line 65-67: The isLoopbackRelayHost function currently only matches
exact "localhost" so names like "relay.localhost" bypass blocking; update
isLoopbackRelayHost to normalize the hostname as before and then return true for
hostname === 'localhost' OR hostname.endsWith('.localhost'), and still true for
IPv6 loopbacks (e.g. '::1', '0:0:0:0:0:0:0:1' and bracketed/zone forms) and for
IPv4 loopback if desired; modify the check in function isLoopbackRelayHost to
include the endsWith('.localhost') condition (and cover bracketed/zone IPv6
variants already normalized) so wildcard .localhost names are treated as
loopback relays.
In `@tests/routes/env.db-mode.spec.ts`:
- Around line 25-34: The TEST_KEYSET_SECRET assembly is using
loadFixtureTestKeysetSecret() directly which can return whitespace-padded values
that bypass the normalizer; change the fallback to run the fixture through the
same normalizer path by trimming the fixture value and passing it into
normalizeOptionalEnv (i.e. call normalizeOptionalEnv on the fixture result,
ensuring you trim() the fixture before normalizing) so normalizeOptionalEnv,
loadFixtureTestKeysetSecret, and the TEST_KEYSET_SECRET constant all handle
values consistently.
---
Nitpick comments:
In `@llm/implementation/e2e-smoke-tests.md`:
- Around line 3-4: Replace the hard-coded test count text ("Test count: 62 (54
API + 8 UI) — all passing") with a stable statement or a reference to a dynamic
source: update the line that currently starts with "Test count:" to either
summarize status generically (e.g., "Tests verified as of <date>; run
`scripts/run-e2e` or check CI report for current counts") or link to the CI
report/command that produces up-to-date counts so the document doesn't contain a
stale numeric snapshot; modify the "Last verified:" or surrounding paragraph
accordingly to point readers to the canonical command/report instead of
embedding the fixed counts.
In `@src/routes/env.ts`:
- Line 27: The function createAndConnectServerNode currently accepts env: any
which weakens type safety; change the env parameter to the concrete type
returned by readEnvFile (e.g., Record<string, string | undefined> or use
ReturnType<typeof readEnvFile>) and update the function signature and all
internal usages in createAndConnectServerNode to use that type so callers and
TypeScript strict mode get correct checking (also adjust any related helper
functions or call sites that pass the readEnvFile result to match the new typed
signature).
- Around line 307-329: Extract the duplicated RELAYS/GROUP_CRED/SHARE_CRED
validation into a shared helper (e.g. validateCredentialInputs) that accepts
(validKeys: string[], body: Record<string, unknown>, headers:
Record<string,string>) and returns a Response | null; inside it, run
validateRelayUrls(body.RELAYS) and return Response.json(...) when !valid or when
urls is empty, and use validateGroup/validateShare to return the same 400
Responses on invalid credentials; then replace the three duplicated blocks in
both POST handlers by calling const validationError =
validateCredentialInputs(validKeys, body, headers); if (validationError) return
validationError; ensuring the helper preserves identical error messages and
status codes.
In `@tests/e2e/global-setup.ts`:
- Around line 94-95: The TEST_NSEC_HEX environment value (bound to
TEST_NSEC_HEX) must be validated immediately after it's read and before any
keyset generation; add a validation step that verifies TEST_NSEC_HEX is a
non-empty hex string (only 0-9a-fA-F, even length and within the expected
length/range your key generation expects) and if invalid, log a clear fatal
message (using MISSING_SMOKE_CREDS_MESSAGE or a new descriptive message) and
exit/throw to fail fast so downstream crypto routines are not invoked with
malformed input.
In `@tests/e2e/specs/04-sign.e2e.ts`:
- Around line 24-27: Extract the duplicated signature regex into a single
reusable constant (e.g. SIGNATURE_REGEX) in this test file and replace all
repeated inline regex assertions with that constant; update the assertions that
currently use the same pattern (the ones validating signatures near the
EVENT_ID_A and EVENT_ID_B usages and the other occurrences around the later
signature checks) to reference SIGNATURE_REGEX so future changes require editing
only one symbol.
In `@tests/e2e/specs/06-event-log.e2e.ts`:
- Around line 24-33: The test.beforeAll seeding uses a fixed message
('b'.repeat(64)) which can cause dedupe-related flakes; update the withApi seed
call that posts to '/api/sign' (inside test.beforeAll) to generate and use a
run-unique seed message (e.g., include a timestamp or UUID) when building the
data payload so each test run posts a distinct message; keep the same error
handling around seedRes to propagate failures.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (36)
.github/workflows/ci.yml.github/workflows/release.yml.gitignoreDockerfilefrontend/components/ui/peer-list.tsxfrontend/components/ui/tooltip.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright-report/index.htmlplaywright.config.tsscripts/release.shsrc/class/relay.test.tssrc/class/relay.tssrc/routes/env.tssrc/routes/utils.test.tssrc/routes/utils.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/smoke-test-defaults.jsontests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.spec.tstests/routes/helpers/script-runner.ts
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (9)
src/routes/utils.test.ts (1)
97-121: Consider adding IPv6 and IPv4-mapped test cases for normalizeRelayListForEcho.The
normalizeRelayListForEchotests cover the basic cases, but for parity withgetValidRelaystests, consider adding coverage for IPv6 and IPv4-mapped forms.🧪 Suggested additional test cases
it('keeps localhost hostname in echo list when explicitly allowed', async () => { await withEnv('ALLOW_LOCALHOST_RELAY', 'true', () => { expect(normalizeRelayListForEcho(['ws://localhost:18002'])).toEqual(['ws://localhost:18002']); }); }); + + it('filters IPv6 loopback relay in echo list when disallowed', async () => { + await withEnv('ALLOW_LOCALHOST_RELAY', 'false', () => { + expect(normalizeRelayListForEcho(['ws://[::1]:18002', 'wss://relay.example.com'])) + .toEqual(['wss://relay.example.com']); + }); + }); + + it('filters IPv4-mapped IPv6 relay in echo list when disallowed', async () => { + await withEnv('ALLOW_LOCALHOST_RELAY', 'false', () => { + expect(normalizeRelayListForEcho(['ws://[::ffff:127.0.0.1]:18002'])) + .toEqual(undefined); + }); + }); });Based on learnings: Add targeted tests when behavior changes and monitor coverage for regressions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/utils.test.ts` around lines 97 - 121, The tests for normalizeRelayListForEcho lack IPv6 and IPv4-mapped address coverage; add cases that assert IPv6 localhost (e.g. 'ws://[::1]:18002') and IPv4-mapped IPv6 (e.g. 'ws://[::ffff:127.0.0.1]:18002') are filtered out when ALLOW_LOCALHOST_RELAY is 'false' and retained when it's 'true'. Update the describe block for normalizeRelayListForEcho to include one test that runs with withEnv('ALLOW_LOCALHOST_RELAY','false') expecting only non-local relays (e.g. 'wss://relay.example.com'), and two tests running with withEnv('ALLOW_LOCALHOST_RELAY','true') that expect the IPv6 literal and the IPv4-mapped form to be preserved in the returned array; reference normalizeRelayListForEcho to locate where to add these assertions.src/class/relay.test.ts (1)
57-69: Add one test for canonical no-filter REQ (["REQ", "<sub_id>"]).You cover empty wrapped filters, but not the explicit no-filter branch (
filters.length === 0) insrc/class/relay.ts(Line 167). Adding this keeps the guard regression-safe.➕ Suggested test addition
describe('NostrRelay REQ handling', () => { + it('rejects canonical REQ payloads with no filters', () => { + const relay = new NostrRelay({ info: false, debug: false }); + const socket = createFakeSocket(); + const ws = asHandlerSocket(socket); + const handler = relay.handler(); + + handler.open?.(ws); + handler.message?.(ws, JSON.stringify(['REQ', 'sub-no-filters'])); + + expect(relay.subs.size).toBe(0); + const messages = decodeSent(socket); + expect(messages).toContainEqual(['NOTICE', '', 'REQ requires at least one filter']); + }); + it('rejects REQ with an empty wrapped filter array', () => {Based on learnings: Add targeted tests when behavior changes and monitor coverage for regressions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/class/relay.test.ts` around lines 57 - 69, Add a unit test that exercises the explicit no-filter REQ branch by sending a message with the shape ["REQ", "<sub_id>"] (i.e., no third argument) to the relay handler returned by NostrRelay.prototype.handler() — mirror the existing test for empty wrapped filters in src/class/relay.test.ts but call handler.message?(ws, JSON.stringify(['REQ', 'sub-empty-no-filters'])) or equivalent so the code path in Relay.handleRequest (the branch checking filters.length === 0) is executed; assert that relay.subs.size remains 0 and that the socket received the NOTICE message 'REQ requires at least one filter' to keep the guard regression-safe.scripts/release.sh (1)
32-32: Usebun cifor deterministic release installs.At line 32,
bun installmay update lockfiles during release. Bun's documentation recommendsbun cifor CI/release pipelines, which enforces reproducible installs by failing ifpackage.jsonandbun.lockdiverge.Suggested patch
-bun install +bun ci🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/release.sh` at line 32, Replace the non-deterministic installer call "bun install" in the release script with the CI-safe command "bun ci": locate the line that invokes bun install in scripts/release.sh and change it to run bun ci so the release pipeline fails if package.json and bun.lock.json diverge and avoids updating lockfiles during releases.frontend/components/ui/tooltip.tsx (1)
13-13: Add a focused test for the new accessibility contract.Please add/extend a component test to cover focusable tooltip trigger naming behavior (
aria-labelpresent when required).Based on learnings, "Add targeted tests when behavior changes and monitor coverage for regressions".
Also applies to: 128-128
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/tooltip.tsx` at line 13, Add/extend a unit test for the Tooltip component to assert the accessible name is applied to a focusable trigger: render the Tooltip/TooltipTrigger (use the component symbols Tooltip and TooltipTrigger or the exported default) with ariaLabel set (ariaLabel="My tip") and ensure the focusable trigger element (e.g., a button or element with tabIndex/role) receives aria-label="My tip" when focusable (simulate keyboard focus or query by role and check attribute). Also add a complementary assertion that when ariaLabel is omitted the trigger falls back to the child text (render with no ariaLabel and expect accessible name from inner content). Use testing-library queries (getByRole/getByText) and toHaveAttribute/toHaveAccessibleName assertions.frontend/components/ui/peer-list.tsx (1)
629-630: Addaria-controls/idlinkage for the collapsible region.Line 629 sets
aria-expanded, but there’s no programmatic link to the controlled panel. Addingaria-controls+ matching panelidimproves SR navigation context.♿ Suggested patch
+ const panelId = 'peer-list-panel'; + return ( <div className={cn("space-y-2", className)}> @@ <div @@ role="button" aria-expanded={isExpanded} + aria-controls={panelId} tabIndex={0} @@ <div + id={panelId} ref={panelRef} className={cn(Also applies to: 699-706
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/peer-list.tsx` around lines 629 - 630, The trigger element sets aria-expanded (uses isExpanded) but lacks an aria-controls/id link to the collapsible panel; add an aria-controls attribute to the trigger (e.g., aria-controls={panelId}) and give the collapsible panel a matching id (panelId), generating a stable unique id per item (use an existing unique value like peer.id or derive one in the component) so screen readers can associate the control with the region; apply this change for both trigger instances (the one using aria-expanded and the other block around lines 699-706) and ensure the panel element that becomes hidden/shown has the matching id and appropriate role (region or group) if not already present.src/routes/env.ts (1)
307-329: Extract duplicated env-write validation into one helper.The RELAYS / GROUP_CRED / SHARE_CRED validation block appears twice (DB and headless branches). Pulling it into a shared validator will keep behavior in sync and reduce drift risk.
Also applies to: 370-392
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 307 - 329, Duplicate validation logic for RELAYS, GROUP_CRED and SHARE_CRED appears in two places; extract it into a single helper (e.g., validateSpecialEnvKeys or validateEnvWriteKeys) that accepts the body and validKeys (and headers if needed) and performs the three checks using validateRelayUrls, validateGroup and validateShare; the helper should return a Response (with the same JSON shape and status 400) on validation failure or null/void on success. Replace both inline blocks that currently call validateRelayUrls/validateGroup/validateShare with a single call to this helper (or early-return the helper's Response) so behavior stays identical and no duplicated logic remains.tests/e2e/specs/08-ui.e2e.ts (1)
49-50: Consider extracting the repeated Signer-tab selector into a constant.The same locator appears in multiple tests; a shared constant lowers maintenance drift.
♻️ Suggested refactor
+const SIGNER_TAB_SELECTOR = '[role="tab"]:has-text("Signer"), button:has-text("Signer"), a:has-text("Signer")'; + test.describe('UI – Authenticated app', () => { @@ - const signerTab = page.locator('[role="tab"]:has-text("Signer"), button:has-text("Signer"), a:has-text("Signer")').first(); + const signerTab = page.locator(SIGNER_TAB_SELECTOR).first(); @@ - const signerTab = page.locator('[role="tab"]:has-text("Signer"), button:has-text("Signer"), a:has-text("Signer")').first(); + const signerTab = page.locator(SIGNER_TAB_SELECTOR).first();Also applies to: 79-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/08-ui.e2e.ts` around lines 49 - 50, Extract the repeated Signer tab locator string into a shared constant (e.g., SIGNER_TAB_SELECTOR) or a helper function (e.g., getSignerTab(page)) and replace in tests where you currently create signerTab with page.locator('[role="tab"]:has-text("Signer"), button:has-text("Signer"), a:has-text("Signer")').first(); update usages at the current location and the other occurrence around lines 79-80 to reference the new constant/helper to avoid duplication and simplify future maintenance.tests/e2e/specs/01-auth.e2e.ts (1)
13-20: ExtractwithApiinto shared e2e helpers to remove repeated boilerplate.This helper is duplicated across multiple specs (
01-auth.e2e.ts,02-status-peers.e2e.ts,03-nip44-nip04.e2e.ts,04-sign.e2e.ts,06-event-log.e2e.ts). Centralizing it will reduce drift and simplify future changes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/01-auth.e2e.ts` around lines 13 - 20, Extract the duplicated withApi helper into a single shared e2e helper module: move the function async function withApi(fn: (api: APIRequestContext) => Promise<void>) { const api = await request.newContext({ baseURL: baseUrl }); try { await fn(api); } finally { await api.dispose(); } } into a common helpers file and export it, then replace the local definitions in 01-auth.e2e.ts, 02-status-peers.e2e.ts, 03-nip44-nip04.e2e.ts, 04-sign.e2e.ts and 06-event-log.e2e.ts with an import of withApi; ensure the shared helper imports/uses request, APIRequestContext and baseUrl (or accepts baseUrl as a parameter) and retains proper disposal logic so existing tests calling withApi continue to work.tests/e2e/helpers.ts (1)
6-7: Harden the usernameidselector with case-insensitive matching.This avoids brittle behavior when apps use
id="UserName"or other case variants.🔧 Suggested tweak
- .locator('input[autocomplete="username"], input[id*="user"], input[name*="user" i], input[placeholder*="user" i]') + .locator('input[autocomplete="username"], input[id*="user" i], input[name*="user" i], input[placeholder*="user" i]')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/helpers.ts` around lines 6 - 7, Update the username locator in tests/e2e/helpers.ts to use case-insensitive ID matching: modify the .locator(...) selector so any id-based checks use attribute selectors with the "i" flag (e.g., input[id*="user" i] or input[id*="username" i]) alongside the existing autocomplete/name/placeholder checks; ensure the .locator call still picks the .first() element so tests remain stable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/components/ui/tooltip.tsx`:
- Line 13: The Tooltip props currently allow ariaLabel to be optional which
permits a focusable icon-only trigger without an accessible name; update the
prop typing and add a runtime guard so that when focusable is true an ariaLabel
is required. Specifically, change the props definition (where ariaLabel?: string
appears) to a discriminated union such as { focusable: true; ariaLabel: string }
| { focusable?: false; ariaLabel?: string } and add a runtime check in the
Tooltip/TooltipTrigger component (e.g., inside TooltipTrigger or the component
rendering the icon trigger) to throw or console.error if props.focusable &&
!props.ariaLabel. Ensure the same change is applied to the other occurrences
referenced (lines ~24 and ~122-129) so TypeScript enforces and runtime fails
fast when a focusable tooltip lacks an accessible name.
In `@src/routes/env.ts`:
- Around line 95-99: The code path computing authenticatedNumericUserId converts
auth.userId to BigInt without validating it's a positive integer; update the
IIFE that defines authenticatedNumericUserId so the same guard used elsewhere
(auth.userId > 0) is applied: for the numeric branch (typeof auth.userId ===
'number') require auth.userId > 0 before returning BigInt(auth.userId), and for
the string branch ensure the parsed numeric value is > 0 (or the string
represents a positive integer) before returning BigInt(auth.userId); keep the
existing HEADLESS and auth?.authenticated checks intact.
In `@tests/e2e/cosigner.mjs`:
- Line 77: The current node.on('closed', ...) handler only logs and allows the
test process to hang due to the setInterval, so change the handler in
tests/e2e/cosigner.mjs (the node.on('closed', ...) callback and the similar
handler block at the 108-117 range) to fail fast: after logging call
process.exit(1) or throw an Error with a clear message so the E2E run terminates
immediately on unexpected node closure; ensure both occurrences (the node
'closed' event listeners) are updated so the process doesn't remain alive due to
the interval.
In `@tests/e2e/global-setup.ts`:
- Around line 115-118: The writeState function currently writes sensitive
smoke-test secrets to STATE_FILE with default permissive modes; change it to
create the containing temp directory with restrictive permissions (mode 0o700)
if it doesn't exist and write the state file with restrictive file permissions
(mode 0o600) so only the owner can read/write; reference the writeState function
and STATE_FILE variable to locate the code and apply the same hardening to the
other temp-dir/state creation code paths noted elsewhere (the other location
that creates the temp directory and state file) so both the directory and the
file use owner-only permissions.
In `@tests/e2e/specs/06-event-log.e2e.ts`:
- Around line 2-4: Update the stale header comment at the top of
06-event-log.e2e.ts to reflect that this suite now seeds its own log entries in
the beforeAll hook rather than relying on entries produced by 04-sign.e2e.ts;
locate the file header comment and the beforeAll function reference and replace
the sentence mentioning 04-sign.e2e.ts with a brief note that the suite seeds
its own entries in beforeAll.
In `@tests/routes/helpers/script-runner.ts`:
- Around line 45-47: The current .replace chain that builds the redacted preview
(the two .replace calls operating on redacted and returning truncated by
maxChars) only matches unquoted key=value/token: value forms and misses
JSON-style quoted keys/values like "token":"..."; update the first .replace
pattern used when creating redacted to also match quoted JSON-style keys and
quoted values (e.g., allow optional surrounding double/single quotes around the
key and around the value and still replace the captured secret with <redacted>),
and ensure the bearer regex still applies; modify the regex used in the .replace
on the redacted variable (and keep the same replacement behavior) so keys like
"token", "api_key", "api-key", "password", "admin_secret", and "session_secret"
are caught in both JSON and key=value forms before truncation by maxChars.
---
Nitpick comments:
In `@frontend/components/ui/peer-list.tsx`:
- Around line 629-630: The trigger element sets aria-expanded (uses isExpanded)
but lacks an aria-controls/id link to the collapsible panel; add an
aria-controls attribute to the trigger (e.g., aria-controls={panelId}) and give
the collapsible panel a matching id (panelId), generating a stable unique id per
item (use an existing unique value like peer.id or derive one in the component)
so screen readers can associate the control with the region; apply this change
for both trigger instances (the one using aria-expanded and the other block
around lines 699-706) and ensure the panel element that becomes hidden/shown has
the matching id and appropriate role (region or group) if not already present.
In `@frontend/components/ui/tooltip.tsx`:
- Line 13: Add/extend a unit test for the Tooltip component to assert the
accessible name is applied to a focusable trigger: render the
Tooltip/TooltipTrigger (use the component symbols Tooltip and TooltipTrigger or
the exported default) with ariaLabel set (ariaLabel="My tip") and ensure the
focusable trigger element (e.g., a button or element with tabIndex/role)
receives aria-label="My tip" when focusable (simulate keyboard focus or query by
role and check attribute). Also add a complementary assertion that when
ariaLabel is omitted the trigger falls back to the child text (render with no
ariaLabel and expect accessible name from inner content). Use testing-library
queries (getByRole/getByText) and toHaveAttribute/toHaveAccessibleName
assertions.
In `@scripts/release.sh`:
- Line 32: Replace the non-deterministic installer call "bun install" in the
release script with the CI-safe command "bun ci": locate the line that invokes
bun install in scripts/release.sh and change it to run bun ci so the release
pipeline fails if package.json and bun.lock.json diverge and avoids updating
lockfiles during releases.
In `@src/class/relay.test.ts`:
- Around line 57-69: Add a unit test that exercises the explicit no-filter REQ
branch by sending a message with the shape ["REQ", "<sub_id>"] (i.e., no third
argument) to the relay handler returned by NostrRelay.prototype.handler() —
mirror the existing test for empty wrapped filters in src/class/relay.test.ts
but call handler.message?(ws, JSON.stringify(['REQ', 'sub-empty-no-filters']))
or equivalent so the code path in Relay.handleRequest (the branch checking
filters.length === 0) is executed; assert that relay.subs.size remains 0 and
that the socket received the NOTICE message 'REQ requires at least one filter'
to keep the guard regression-safe.
In `@src/routes/env.ts`:
- Around line 307-329: Duplicate validation logic for RELAYS, GROUP_CRED and
SHARE_CRED appears in two places; extract it into a single helper (e.g.,
validateSpecialEnvKeys or validateEnvWriteKeys) that accepts the body and
validKeys (and headers if needed) and performs the three checks using
validateRelayUrls, validateGroup and validateShare; the helper should return a
Response (with the same JSON shape and status 400) on validation failure or
null/void on success. Replace both inline blocks that currently call
validateRelayUrls/validateGroup/validateShare with a single call to this helper
(or early-return the helper's Response) so behavior stays identical and no
duplicated logic remains.
In `@src/routes/utils.test.ts`:
- Around line 97-121: The tests for normalizeRelayListForEcho lack IPv6 and
IPv4-mapped address coverage; add cases that assert IPv6 localhost (e.g.
'ws://[::1]:18002') and IPv4-mapped IPv6 (e.g. 'ws://[::ffff:127.0.0.1]:18002')
are filtered out when ALLOW_LOCALHOST_RELAY is 'false' and retained when it's
'true'. Update the describe block for normalizeRelayListForEcho to include one
test that runs with withEnv('ALLOW_LOCALHOST_RELAY','false') expecting only
non-local relays (e.g. 'wss://relay.example.com'), and two tests running with
withEnv('ALLOW_LOCALHOST_RELAY','true') that expect the IPv6 literal and the
IPv4-mapped form to be preserved in the returned array; reference
normalizeRelayListForEcho to locate where to add these assertions.
In `@tests/e2e/helpers.ts`:
- Around line 6-7: Update the username locator in tests/e2e/helpers.ts to use
case-insensitive ID matching: modify the .locator(...) selector so any id-based
checks use attribute selectors with the "i" flag (e.g., input[id*="user" i] or
input[id*="username" i]) alongside the existing autocomplete/name/placeholder
checks; ensure the .locator call still picks the .first() element so tests
remain stable.
In `@tests/e2e/specs/01-auth.e2e.ts`:
- Around line 13-20: Extract the duplicated withApi helper into a single shared
e2e helper module: move the function async function withApi(fn: (api:
APIRequestContext) => Promise<void>) { const api = await request.newContext({
baseURL: baseUrl }); try { await fn(api); } finally { await api.dispose(); } }
into a common helpers file and export it, then replace the local definitions in
01-auth.e2e.ts, 02-status-peers.e2e.ts, 03-nip44-nip04.e2e.ts, 04-sign.e2e.ts
and 06-event-log.e2e.ts with an import of withApi; ensure the shared helper
imports/uses request, APIRequestContext and baseUrl (or accepts baseUrl as a
parameter) and retains proper disposal logic so existing tests calling withApi
continue to work.
In `@tests/e2e/specs/08-ui.e2e.ts`:
- Around line 49-50: Extract the repeated Signer tab locator string into a
shared constant (e.g., SIGNER_TAB_SELECTOR) or a helper function (e.g.,
getSignerTab(page)) and replace in tests where you currently create signerTab
with page.locator('[role="tab"]:has-text("Signer"), button:has-text("Signer"),
a:has-text("Signer")').first(); update usages at the current location and the
other occurrence around lines 79-80 to reference the new constant/helper to
avoid duplication and simplify future maintenance.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (36)
.github/workflows/ci.yml.github/workflows/release.yml.gitignoreDockerfilefrontend/components/ui/peer-list.tsxfrontend/components/ui/tooltip.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright-report/index.htmlplaywright.config.tsscripts/release.shsrc/class/relay.test.tssrc/class/relay.tssrc/routes/env.tssrc/routes/utils.test.tssrc/routes/utils.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/smoke-test-defaults.jsontests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.spec.tstests/routes/helpers/script-runner.ts
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
frontend/components/ui/input-with-validation.tsx (1)
40-46:⚠️ Potential issue | 🟡 MinorPrevent
requiredfrom being overridden by...props.Line 40 sets
required={isRequired}, but Line 46 spreads...propsafterward. Sincerequiredis not excluded from the interface'sOmitclause, it can silently overrideisRequiredif passed downstream.Suggested fix
-interface InputWithValidationProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> { +interface InputWithValidationProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'required'> {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/input-with-validation.tsx` around lines 40 - 46, The component sets required={isRequired} but then spreads ...props which can contain its own required and silently override isRequired; in the InputWithValidation component destructure props to pull out required (e.g. const { required, ...rest } = props or include required in the prop list) and then spread ...rest instead of ...props, keeping required={isRequired} as the authoritative prop; update any usages of the component signature (e.g. the component function params and where ...props is referenced) so required cannot be passed through and override isRequired.src/routes/user.ts (1)
284-293:⚠️ Potential issue | 🟡 MinorTrim relay values before persisting to ensure consistency with validation.
The
isValidWebSocketUrl()function validates trimmed input (line 46), butupdates.relays = body.relays(line 289) stores raw, untrimmed values. Relay URLs with leading/trailing whitespace will pass validation and persist malformed into the database. While downstream code likenormalizeRelayListForEcho()re-trims on retrieval, this creates data inconsistency and pollution. Normalize at the point of storage instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/user.ts` around lines 284 - 293, The relays array is being validated with isValidWebSocketUrl (which trims inputs) but updates.relays is assigned the raw body.relays, allowing whitespace-padded URLs to be persisted; change the assignment in the branch handling 'relays' so that when body.relays is an array you map each entry to its trimmed string before setting updates.relays (and keep null as-is), ensuring stored relay URLs are normalized at write-time (this aligns with normalizeRelayListForEcho and the earlier validation using isValidWebSocketUrl).
♻️ Duplicate comments (1)
tests/e2e/state.ts (1)
38-41:⚠️ Potential issue | 🟡 MinorTighten
groupPubkeyHexto exactly 64 hex chars.Line 38-41 currently accepts any non-empty hex length, which can defer invalid-state failures into later tests. Enforce fixed length at load time.
Suggested patch
- groupPubkeyHex: z - .string() - .min(1, 'groupPubkeyHex must be non-empty') - .regex(/^([0-9a-fA-F]+)$/, 'groupPubkeyHex must be a hex string'), + groupPubkeyHex: z + .string() + .regex(/^[0-9a-fA-F]{64}$/, 'groupPubkeyHex must be a 64-char hex string'),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/state.ts` around lines 38 - 41, Update the zod schema for groupPubkeyHex in tests/e2e/state.ts (symbol: groupPubkeyHex) to require exactly 64 hex characters instead of any non-empty hex length; replace the current .min(1) and loose regex with a length check and/or a regex enforcing ^[0-9a-fA-F]{64}$ so invalid lengths fail at load time. Ensure the schema error message reflects the exact-64-hex requirement.
🧹 Nitpick comments (9)
frontend/components/nip46/Requests.tsx (1)
47-53: Good security practice; consider adding a brief comment to clarify intent.The sanitization correctly strips C0/C1 control characters and bidirectional text overrides (preventing display manipulation attacks). The static analysis warning about control characters in the regex is a false positive—matching them is the explicit purpose here.
A short inline comment would help future maintainers understand the intentional use and silence linter confusion:
💡 Optional: add explanatory comment
const sanitizePreview = (value: string): string => { return value + // Strip C0/C1 control chars and bidirectional overrides to prevent display issues .replace(/[\u0000-\u001F\u007F-\u009F]/g, '') .replace(/[\u202A-\u202E\u2066-\u2069]/g, '') .replace(/\s+/g, ' ') .trim() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/nip46/Requests.tsx` around lines 47 - 53, The sanitizePreview function intentionally strips C0/C1 control characters and Unicode bidirectional overrides to prevent display manipulation; add a concise inline comment above sanitizePreview explaining that those specific regex ranges are deliberate (to remove control characters and BIDI overrides), note that the static analysis warning is a false positive for this pattern, and optionally include a reference/link or short rationale so future maintainers and linters understand the intent (refer to sanitizePreview and its three replace() regexes).src/config/crypto.ts (1)
7-11: Make PBKDF2 iterations configurable with a validated floor.
600000is a solid hardening step, but keeping it fixed can create auth latency/CPU pressure across CI and low-spec hosts. Consider env-driven tuning with minimum bounds.Proposed refactor
+const DEFAULT_PBKDF2_ITERATIONS = 600000; +const MIN_PBKDF2_ITERATIONS = 200000; +const configuredPbkdf2Iterations = Number( + process.env.PBKDF2_ITERATIONS ?? DEFAULT_PBKDF2_ITERATIONS, +); + export const PBKDF2_CONFIG = { - ITERATIONS: 600000, // OWASP-aligned baseline for PBKDF2-HMAC-SHA256 + ITERATIONS: + Number.isInteger(configuredPbkdf2Iterations) && + configuredPbkdf2Iterations >= MIN_PBKDF2_ITERATIONS + ? configuredPbkdf2Iterations + : DEFAULT_PBKDF2_ITERATIONS, KEY_LENGTH: 32, // 256 bits ALGORITHM: 'sha256', // Hash algorithm } as const;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/crypto.ts` around lines 7 - 11, Make PBKDF2 iterations configurable by reading an environment value (e.g., PBKDF2_ITERATIONS) and validate it against a minimum floor before assigning to PBKDF2_CONFIG. Add a constant MIN_PBKDF2_ITERATIONS (set to 600000) and parseInt the env var, ensure it's a safe integer >= MIN_PBKDF2_ITERATIONS, otherwise fall back to MIN_PBKDF2_ITERATIONS; use this validated value for PBKDF2_CONFIG. Update references to PBKDF2_CONFIG.ITERATIONS and ensure non-numeric or out-of-range env values do not override the minimum.frontend/components/ui/input-with-validation.tsx (1)
14-55: Use a PascalCase filename for this React component.Component name is
InputWithValidation, but file name isinput-with-validation.tsx. Recommend renaming toInputWithValidation.tsx.As per coding guidelines,
frontend/**/*.tsx: React component file names use PascalCase (e.g.,Configure.tsx).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/input-with-validation.tsx` around lines 14 - 55, Rename the component file from input-with-validation.tsx to PascalCase InputWithValidation.tsx and update any imports that reference the old filename to use the new name; the component identifier InputWithValidation, its export, and the default/non-default export usage should remain unchanged, so search for imports of "input-with-validation" and replace them with "InputWithValidation" to keep module resolution consistent across the codebase.frontend/components/Configure.tsx (1)
565-566: ParseJSON.parseresult tounknowninstead of relying on implicitanytype.With
noImplicitAny: truein tsconfig.json, declare the parsed result explicitly asunknownand narrow with type guards:const parsed: unknown = JSON.parse(advancedSettings.RELAYS); if (Array.isArray(parsed) && parsed.every(relay => typeof relay === 'string') && parsed.length > 0) { return parsed; }This aligns with the strict TypeScript guideline to explicitly declare types and avoid
any. Similar patterns appear in Requests.tsx, Signer.tsx, Recover.tsx, and App.tsx.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/Configure.tsx` around lines 565 - 566, Change the implicit any from JSON.parse to an explicit unknown and narrow it before use: declare the result as const parsed: unknown = JSON.parse(advancedSettings.RELAYS) and then use the existing type guards (Array.isArray(parsed) && parsed.every(relay => typeof relay === 'string') && parsed.length > 0) to safely return the string[]; apply the same pattern in Requests.tsx, Signer.tsx, Recover.tsx, and App.tsx where JSON.parse is used to avoid noImplicitAny violations.src/routes/status.ts (1)
62-62: Simplify redundantBigIntconversion.Line 62 has identical ternary branches; this can be reduced to a single conversion for clarity.
♻️ Proposed simplification
- const dbUserId = typeof parsedUserId === 'string' ? BigInt(parsedUserId) : BigInt(parsedUserId); + const dbUserId = BigInt(parsedUserId);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/status.ts` at line 62, The ternary assigning dbUserId redundantly converts parsedUserId to BigInt in both branches; simplify by replacing the ternary with a single conversion (use BigInt(parsedUserId)) where dbUserId is set so the code in the status route uses a single, clear conversion of parsedUserId to a BigInt.src/routes/onboarding.ts (1)
297-301: Use fixed-length digest comparison for stricter timing-attack mitigation.The early-return on length mismatch (line 297) leaks the admin secret's length to an observer. While this pattern is widely used when length isn't sensitive, a more conservative approach is to hash both secrets first, ensuring a fixed-length comparison that reveals nothing about the original secret.
🔧 Proposed refactor
-import { timingSafeEqual } from 'crypto'; +import { timingSafeEqual, createHash } from 'crypto'; @@ - const providedSecret = Buffer.from(adminSecretStr); - const expectedSecret = Buffer.from(ADMIN_SECRET); - - // Timing-safe comparison - if (providedSecret.length !== expectedSecret.length) { - return false; - } - - return timingSafeEqual(providedSecret, expectedSecret); + const providedDigest = createHash('sha256').update(adminSecretStr, 'utf8').digest(); + const expectedDigest = createHash('sha256').update(String(ADMIN_SECRET), 'utf8').digest(); + return timingSafeEqual(providedDigest, expectedDigest);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/onboarding.ts` around lines 297 - 301, The current early-return length check on providedSecret vs expectedSecret leaks secret length; instead remove the length comparison and compute fixed-length digests of both secrets (e.g., SHA-256) and then call timingSafeEqual on those digests so comparison is always constant-length; update the code paths using providedSecret, expectedSecret, and timingSafeEqual to hash both inputs into fixed-size buffers before invoking timingSafeEqual and ensure any encoding conversion yields equal-length inputs.package.json (1)
83-89: Transitive dependency overrides for security hardening.Pinning
glob,minimatch,js-yaml,undici,ajv, andfast-xml-parseraddresses known vulnerabilities in older transitive versions. Consider documenting the rationale (e.g., in a comment or SECURITY.md) so future maintainers understand why these pins exist.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 83 - 89, The package.json contains an "overrides" block pinning transitive deps (glob, minimatch, js-yaml, undici, ajv, fast-xml-parser) for security hardening but lacks documentation; add a short comment in the repository (e.g., SECURITY.md or a comment near the "overrides" block) that explains why these specific packages are pinned, references the CVEs or advisory links, and notes the date and author of the change so future maintainers (looking at "overrides" in package.json) understand the rationale and when to revisit the pins.tests/e2e/specs/03-nip44-nip04.e2e.ts (1)
18-25: Consider centralizing duplicatedwithApihelper.The same helper pattern appears across multiple e2e specs; moving it to
tests/e2e/helpers.tswould reduce repetition and keep setup changes in one place.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/specs/03-nip44-nip04.e2e.ts` around lines 18 - 25, Extract the duplicated async helper function withApi (which calls request.newContext({ baseURL: baseUrl }), invokes the passed fn: (api: APIRequestContext) => Promise<void>, and finally disposes the context) into a single shared helpers module (e.g., helpers.ts), export it, and replace the inline definitions in each e2e spec with an import of that exported withApi; ensure the helper still accepts the same signature, references the same baseUrl and APIRequestContext types, and continues to call api.dispose() in a finally block so teardown behavior remains identical.src/routes/env.ts (1)
312-334: Consider extracting duplicate validation logic.The RELAYS, GROUP_CRED, and SHARE_CRED validation blocks are duplicated between the DB-mode branch (lines 312-334) and HEADLESS branch (lines 375-397). Consider extracting this into a shared helper function to reduce duplication and ensure consistent validation behavior.
♻️ Proposed refactor
+function validateEnvMutationPayload( + body: Record<string, unknown>, + validKeys: string[] +): { valid: true } | { valid: false; error: string; status: number } { + if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { + const relayValidation = validateRelayUrls(body.RELAYS); + if (!relayValidation.valid) { + return { valid: false, error: relayValidation.error!, status: 400 }; + } + if (!relayValidation.urls || relayValidation.urls.length === 0) { + return { valid: false, error: 'At least one relay URL is required', status: 400 }; + } + } + if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED !== undefined) { + const groupValidation = validateGroup(body.GROUP_CRED); + if (!groupValidation.isValid) { + return { valid: false, error: 'Invalid GROUP_CRED', status: 400 }; + } + } + if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED !== undefined) { + const shareValidation = validateShare(body.SHARE_CRED); + if (!shareValidation.isValid) { + return { valid: false, error: 'Invalid SHARE_CRED', status: 400 }; + } + } + return { valid: true }; +}Then use in both branches:
const payloadValidation = validateEnvMutationPayload(body, validKeys); if (!payloadValidation.valid) { return Response.json({ success: false, error: payloadValidation.error }, { status: payloadValidation.status, headers }); }Also applies to: 375-397
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 312 - 334, Duplicate validation blocks for RELAYS, GROUP_CRED and SHARE_CRED are present in both branches; extract them into a shared helper (e.g., validateEnvMutationPayload) that calls validateRelayUrls, validateGroup and validateShare and returns a unified { valid:boolean, error?:string, status?:number } result; replace the in-branch checks (the blocks referencing validateRelayUrls, validateGroup, validateShare) with a single call to that helper in both DB-mode and HEADLESS branches and short-circuit with Response.json when the helper returns valid: false, using the provided error and status values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/openapi/openapi.yaml`:
- Line 1960: The AuthStatus.methods enum in the OpenAPI spec is inconsistent
with the implementation: either add "bearer" back into the enum (so
AuthStatus.methods: ["api-key","basic-auth","session","bearer"]) to reflect that
bearerAuth is supported, or remove all OpenAPI bearerAuth references and any
endpoint security: - bearerAuth entries if bearer is being deprecated; locate
references by the symbols AuthStatus.methods and bearerAuth (and endpoints that
include security: - bearerAuth: []) and reconcile them with the implemented
parsing in src/routes/onboarding.ts and src/routes/env.ts so the spec and code
agree.
In `@frontend/components/Configure.tsx`:
- Around line 560-567: The current save logic prefers existingRelays before
parsing advancedSettings.RELAYS, which can persist stale relay values; update
the logic so you first check and parse advancedSettings.RELAYS (trim, JSON.parse
inside try/catch, verify it's a non-empty array of strings) and return that if
valid, and only if advancedSettings.RELAYS is missing/invalid fall back to
existingRelays; adjust the block that references existingRelays and
advancedSettings.RELAYS accordingly so parsed advanced settings take precedence
during save.
In `@src/routes/auth.ts`:
- Around line 1104-1115: The 405 branch uses the shared headers object that
includes cookie-clearing, so non-POST requests inadvertently log users out;
update the logic in the auth handler so Response.json for the method-not-allowed
case uses a headers set that does NOT include the cookie-clearing entries (e.g.,
create a new minimalHeaders or clone headers and delete the cookie/Set-Cookie
entries) or move the cookie-clear assignment into the successful POST branch
(after checking req.method === 'POST'); reference the existing req.method check,
the shared headers variable, and the Response.json call to locate where to
change the headers used for the 405 response.
In `@tests/routes/helpers/script-runner.ts`:
- Around line 66-87: buildScriptEnv currently copies the entire process.env into
nextEnv then blacklists keys; instead initialize nextEnv from a minimal
allowlist baseline (only essential keys like PATH, HOME, TMPDIR if needed)
rather than copying all process.env, then apply sanitizeOverrides(overrides) and
forced values (DB_PATH, ENV_FILE_PATH, NODE_ENV: 'test'); remove the loop that
copies process.env and the isBlockedEnvKey-based deletions, keep use of
sanitizeOverrides and ensure functions/variables referenced are buildScriptEnv,
sanitizeOverrides, isBlockedEnvKey (if still needed elsewhere) and nextEnv so
the subprocess environment is tightly scoped and sensitive host/CI vars are not
forwarded.
---
Outside diff comments:
In `@frontend/components/ui/input-with-validation.tsx`:
- Around line 40-46: The component sets required={isRequired} but then spreads
...props which can contain its own required and silently override isRequired; in
the InputWithValidation component destructure props to pull out required (e.g.
const { required, ...rest } = props or include required in the prop list) and
then spread ...rest instead of ...props, keeping required={isRequired} as the
authoritative prop; update any usages of the component signature (e.g. the
component function params and where ...props is referenced) so required cannot
be passed through and override isRequired.
In `@src/routes/user.ts`:
- Around line 284-293: The relays array is being validated with
isValidWebSocketUrl (which trims inputs) but updates.relays is assigned the raw
body.relays, allowing whitespace-padded URLs to be persisted; change the
assignment in the branch handling 'relays' so that when body.relays is an array
you map each entry to its trimmed string before setting updates.relays (and keep
null as-is), ensuring stored relay URLs are normalized at write-time (this
aligns with normalizeRelayListForEcho and the earlier validation using
isValidWebSocketUrl).
---
Duplicate comments:
In `@tests/e2e/state.ts`:
- Around line 38-41: Update the zod schema for groupPubkeyHex in
tests/e2e/state.ts (symbol: groupPubkeyHex) to require exactly 64 hex characters
instead of any non-empty hex length; replace the current .min(1) and loose regex
with a length check and/or a regex enforcing ^[0-9a-fA-F]{64}$ so invalid
lengths fail at load time. Ensure the schema error message reflects the
exact-64-hex requirement.
---
Nitpick comments:
In `@frontend/components/Configure.tsx`:
- Around line 565-566: Change the implicit any from JSON.parse to an explicit
unknown and narrow it before use: declare the result as const parsed: unknown =
JSON.parse(advancedSettings.RELAYS) and then use the existing type guards
(Array.isArray(parsed) && parsed.every(relay => typeof relay === 'string') &&
parsed.length > 0) to safely return the string[]; apply the same pattern in
Requests.tsx, Signer.tsx, Recover.tsx, and App.tsx where JSON.parse is used to
avoid noImplicitAny violations.
In `@frontend/components/nip46/Requests.tsx`:
- Around line 47-53: The sanitizePreview function intentionally strips C0/C1
control characters and Unicode bidirectional overrides to prevent display
manipulation; add a concise inline comment above sanitizePreview explaining that
those specific regex ranges are deliberate (to remove control characters and
BIDI overrides), note that the static analysis warning is a false positive for
this pattern, and optionally include a reference/link or short rationale so
future maintainers and linters understand the intent (refer to sanitizePreview
and its three replace() regexes).
In `@frontend/components/ui/input-with-validation.tsx`:
- Around line 14-55: Rename the component file from input-with-validation.tsx to
PascalCase InputWithValidation.tsx and update any imports that reference the old
filename to use the new name; the component identifier InputWithValidation, its
export, and the default/non-default export usage should remain unchanged, so
search for imports of "input-with-validation" and replace them with
"InputWithValidation" to keep module resolution consistent across the codebase.
In `@package.json`:
- Around line 83-89: The package.json contains an "overrides" block pinning
transitive deps (glob, minimatch, js-yaml, undici, ajv, fast-xml-parser) for
security hardening but lacks documentation; add a short comment in the
repository (e.g., SECURITY.md or a comment near the "overrides" block) that
explains why these specific packages are pinned, references the CVEs or advisory
links, and notes the date and author of the change so future maintainers
(looking at "overrides" in package.json) understand the rationale and when to
revisit the pins.
In `@src/config/crypto.ts`:
- Around line 7-11: Make PBKDF2 iterations configurable by reading an
environment value (e.g., PBKDF2_ITERATIONS) and validate it against a minimum
floor before assigning to PBKDF2_CONFIG. Add a constant MIN_PBKDF2_ITERATIONS
(set to 600000) and parseInt the env var, ensure it's a safe integer >=
MIN_PBKDF2_ITERATIONS, otherwise fall back to MIN_PBKDF2_ITERATIONS; use this
validated value for PBKDF2_CONFIG. Update references to PBKDF2_CONFIG.ITERATIONS
and ensure non-numeric or out-of-range env values do not override the minimum.
In `@src/routes/env.ts`:
- Around line 312-334: Duplicate validation blocks for RELAYS, GROUP_CRED and
SHARE_CRED are present in both branches; extract them into a shared helper
(e.g., validateEnvMutationPayload) that calls validateRelayUrls, validateGroup
and validateShare and returns a unified { valid:boolean, error?:string,
status?:number } result; replace the in-branch checks (the blocks referencing
validateRelayUrls, validateGroup, validateShare) with a single call to that
helper in both DB-mode and HEADLESS branches and short-circuit with
Response.json when the helper returns valid: false, using the provided error and
status values.
In `@src/routes/onboarding.ts`:
- Around line 297-301: The current early-return length check on providedSecret
vs expectedSecret leaks secret length; instead remove the length comparison and
compute fixed-length digests of both secrets (e.g., SHA-256) and then call
timingSafeEqual on those digests so comparison is always constant-length; update
the code paths using providedSecret, expectedSecret, and timingSafeEqual to hash
both inputs into fixed-size buffers before invoking timingSafeEqual and ensure
any encoding conversion yields equal-length inputs.
In `@src/routes/status.ts`:
- Line 62: The ternary assigning dbUserId redundantly converts parsedUserId to
BigInt in both branches; simplify by replacing the ternary with a single
conversion (use BigInt(parsedUserId)) where dbUserId is set so the code in the
status route uses a single, clear conversion of parsedUserId to a BigInt.
In `@tests/e2e/specs/03-nip44-nip04.e2e.ts`:
- Around line 18-25: Extract the duplicated async helper function withApi (which
calls request.newContext({ baseURL: baseUrl }), invokes the passed fn: (api:
APIRequestContext) => Promise<void>, and finally disposes the context) into a
single shared helpers module (e.g., helpers.ts), export it, and replace the
inline definitions in each e2e spec with an import of that exported withApi;
ensure the helper still accepts the same signature, references the same baseUrl
and APIRequestContext types, and continues to call api.dispose() in a finally
block so teardown behavior remains identical.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (60)
.github/workflows/ci.yml.github/workflows/release.yml.gitignoreDockerfiledocs/openapi/README.mddocs/openapi/openapi.jsondocs/openapi/openapi.yamlfrontend/components/Configure.tsxfrontend/components/Signer.tsxfrontend/components/nip46/RelaySettings.tsxfrontend/components/nip46/Requests.tsxfrontend/components/ui/card.tsxfrontend/components/ui/collapsible.tsxfrontend/components/ui/input-with-validation.tsxfrontend/components/ui/peer-list.tsxfrontend/components/ui/tooltip.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright.config.tsscripts/patch-zod-compat.mjsscripts/release.shsrc/class/relay.test.tssrc/class/relay.tssrc/config/crypto.tssrc/db/migrator.tssrc/routes/admin.tssrc/routes/auth.tssrc/routes/env.tssrc/routes/index.tssrc/routes/nip04.tssrc/routes/nip44.tssrc/routes/nip46.tssrc/routes/onboarding.tssrc/routes/status.tssrc/routes/user.tssrc/routes/utils.test.tssrc/routes/utils.tssrc/server.tssrc/utils/rate-limiter.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/smoke-test-defaults.jsontests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/admin.whoami.session.spec.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.spec.tstests/routes/helpers/script-runner.ts
|
@CodeRabbit full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
tests/routes/helpers/script-runner.ts (1)
66-77:⚠️ Potential issue | 🟠 MajorTighten subprocess env isolation to avoid host-variable leakage.
Line 67 starts from the full parent environment and removes only a blacklist. That still forwards unrelated CI/host variables into test subprocesses, which weakens isolation and can leak sensitive context.
🔧 Suggested fix (allowlist baseline)
+const PASSTHROUGH_ENV_KEYS = [ + 'PATH', + 'HOME', + 'TMPDIR', + 'TMP', + 'TEMP', + 'SystemRoot', + 'COMSPEC', + 'BUN_INSTALL', +] as const; + export function buildScriptEnv( overrides: Record<string, string>, forced: { dbPath: string; envFilePath: string } ): Record<string, string> { const nextEnv: Record<string, string> = {}; - for (const [key, value] of Object.entries(process.env)) { - if (typeof value === 'string') { - nextEnv[key] = value; - } - } - - for (const key of Object.keys(nextEnv)) { - if (isBlockedEnvKey(key)) { - delete nextEnv[key]; - } + for (const key of PASSTHROUGH_ENV_KEYS) { + const value = process.env[key]; + if (typeof value === 'string' && !isBlockedEnvKey(key)) { + nextEnv[key] = value; + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/routes/helpers/script-runner.ts` around lines 66 - 77, The current logic in the script-runner builds nextEnv by copying all of process.env then deleting blocked keys, which can leak unrelated host/CI variables; instead, construct nextEnv from a minimal allowlist baseline (or start empty) and explicitly add only required variables (e.g., PATH, HOME, NODE_ENV or any specific vars your tests need) rather than inheriting everything and using isBlockedEnvKey to prune; update the code around nextEnv creation (the nextEnv variable and the loop that populates it) to populate only allowed keys and keep isBlockedEnvKey for any additional safety checks.
🧹 Nitpick comments (7)
src/routes/status.ts (1)
62-62: Redundant ternary—both branches are identical.The condition
typeof parsedUserId === 'string'is pointless here since both branches callBigInt(parsedUserId).BigInt()accepts both numeric strings and numbers, so simplify to a direct call.♻️ Suggested simplification
- const dbUserId = typeof parsedUserId === 'string' ? BigInt(parsedUserId) : BigInt(parsedUserId); + const dbUserId = BigInt(parsedUserId);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/status.ts` at line 62, The ternary assigning dbUserId is redundant; replace the expression in the status route that uses parsedUserId (the const dbUserId assignment) with a single direct call to BigInt(parsedUserId) so you remove the unnecessary typeof check and simplify to const dbUserId = BigInt(parsedUserId).frontend/components/nip46/RelaySettings.tsx (1)
78-84: Variableerrorshadows the component'serrorprop.The catch variable
erroron line 81 shadows theerrorprop destructured in the component signature (line 16). While scoped to this arrow function, it reduces clarity. Additionally, this handler logs errors to console whereashandleAdd(lines 25-27) silently swallows them—consider unifying the pattern.🔧 Suggested fix: rename catch variable
onClick={async () => { try { await onRemove(relay) - } catch (error) { - console.error('[RelaySettings] Failed to remove relay:', error) + } catch (err) { + console.error('[RelaySettings] Failed to remove relay:', err) } }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/nip46/RelaySettings.tsx` around lines 78 - 84, The catch variable in the onClick handler for the onRemove(relay) call shadows the component prop named error; rename the catch binding (e.g., to removalError) and update the handler to use the same error-handling approach as handleAdd (instead of logging to console) — i.e., call the component's existing error setter/handler used by handleAdd (or follow the same silent swallow behavior) after catching removalError to keep handling consistent with handleAdd.frontend/components/ui/collapsible.tsx (1)
76-76: Minor: Trailing space at end of file.Line 76 has a trailing space after the semicolon. Additionally, per coding guidelines, React component files should use PascalCase (e.g.,
Collapsible.tsxinstead ofcollapsible.tsx).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/ui/collapsible.tsx` at line 76, Remove the trailing space after the semicolon in the export statement for the Collapsible symbol and rename the file to use PascalCase (e.g., Collapsible.tsx) to follow React component naming guidelines; update any imports that reference the old filename to the new PascalCase name and ensure the exported symbol remains "Collapsible" (export { Collapsible }) with no trailing whitespace.src/routes/user.ts (1)
286-293: Unify relay normalization across both user relay update paths.Line 292 trims relays in
/api/user/credentials, but/api/user/relaysstill writes raw values after validation. The same relay payload can be persisted differently depending on endpoint.♻️ Suggested refactor
+function normalizeRelayUrls(input: unknown): string[] | null { + if (!Array.isArray(input)) return null; + const normalized: string[] = []; + for (const relay of input) { + if (typeof relay !== 'string' || !isValidWebSocketUrl(relay)) return null; + normalized.push(relay.trim()); + } + return normalized; +} if ('relays' in body) { // Validate relays format if (body.relays === null) { updates.relays = null; - } else if ( - Array.isArray(body.relays) && - body.relays.every((r: unknown): r is string => typeof r === 'string' && isValidWebSocketUrl(r)) - ) { - updates.relays = body.relays.map((relay: string) => relay.trim()); } else { + const normalizedRelays = normalizeRelayUrls(body.relays); + if (!normalizedRelays) { + return Response.json( + { error: 'Invalid relay URLs. Must use ws:// or wss://' }, + { status: 400, headers } + ); + } + updates.relays = normalizedRelays; - return Response.json( - { error: 'Invalid relay URLs. Must use ws:// or wss://' }, - { status: 400, headers } - ); } }Apply the same helper in the
/api/user/relaysPOST/PUT path beforeupdateUserCredentials.Also applies to: 295-295
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/user.ts` around lines 286 - 293, The relay normalization is inconsistent: in the /api/user/credentials path you trim relays before storing (updates.relays = body.relays.map(...).trim()), but in the /api/user/relays POST/PUT path validated relays are written raw. Update the /api/user/relays handler to apply the same normalization helper (trim each entry and preserve null handling) to body.relays before calling updateUserCredentials so both paths set updates.relays the same way; reuse the same logic used around updates.relays and body.relays in the credentials handler (or extract it to a small helper and call it from the /api/user/relays flow).tests/e2e/global-teardown.ts (1)
35-46: Consider platform compatibility forgetProcessCommand.The
pscommand is Unix-specific and won't work on Windows. This is acceptable for typical CI environments but worth noting.💡 Optional: Add Windows support if needed
If Windows support becomes necessary, you could add a platform check:
function getProcessCommand(pid: number): string | null { if (process.platform === 'win32') { // Windows: use wmic or PowerShell return null; // Or implement Windows-specific logic } // Existing Unix implementation try { const output = execFileSync('ps', ['-o', 'command=', '-p', String(pid)], ...); // ... } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/e2e/global-teardown.ts` around lines 35 - 46, The getProcessCommand function currently calls the Unix-only ps tool; add a platform check at the start of getProcessCommand (process.platform === 'win32') and handle Windows gracefully—either return null immediately or implement Windows-specific logic (e.g., wmic/PowerShell) to retrieve the command for the given pid; keep the existing execFileSync-based Unix branch (the current 'ps' call) for non-win32 platforms and preserve error handling so behavior stays consistent across OSes.src/routes/env.ts (1)
312-334: Extract repeated env credential validation into one helper.The validation is solid, but it’s duplicated across DB/headless branches and can diverge over time.
Refactor sketch
+function validateEnvCredentialInputs( + body: Record<string, unknown>, + validKeys: readonly string[] +): string | null { + if (validKeys.includes('RELAYS') && body.RELAYS !== undefined) { + const relayValidation = validateRelayUrls(body.RELAYS); + if (!relayValidation.valid) return relayValidation.error; + if (!relayValidation.urls || relayValidation.urls.length === 0) { + return 'At least one relay URL is required'; + } + } + + if (validKeys.includes('GROUP_CRED') && body.GROUP_CRED !== undefined) { + if (!validateGroup(body.GROUP_CRED).isValid) return 'Invalid GROUP_CRED'; + } + + if (validKeys.includes('SHARE_CRED') && body.SHARE_CRED !== undefined) { + if (!validateShare(body.SHARE_CRED).isValid) return 'Invalid SHARE_CRED'; + } + + return null; +}-// duplicated RELAYS/GROUP_CRED/SHARE_CRED checks... +const validationError = validateEnvCredentialInputs(body as Record<string, unknown>, validKeys); +if (validationError) { + return Response.json({ success: false, error: validationError }, { status: 400, headers }); +}Also applies to: 380-397
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/env.ts` around lines 312 - 334, Create a single helper function (e.g., validateEnvCredentials) to centralize the repeated credential validation logic currently duplicated around validateRelayUrls, validateGroup, and validateShare; the helper should accept the request body and headers (or validKeys) and return a uniform result object (success flag, error message, and normalized values like relayValidation.urls) so callers in both DB/headless branches can call validateEnvCredentials instead of repeating the blocks that call validateRelayUrls(body.RELAYS), validateGroup(body.GROUP_CRED), and validateShare(body.SHARE_CRED); update the code paths that currently return Response.json(...) on validation failures to use the helper’s result and short-circuit the same way when result indicates failure.docs/openapi/openapi.json (1)
3003-3009: Consider clarifying bearer-as-api-key semantics in description text.After narrowing
methodsto exclude"bearer", a short note that Bearer is an API-key transport form would prevent client confusion.Also applies to: 4067-4072
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/openapi/openapi.json` around lines 3003 - 3009, Update the OpenAPI schema description for the enum that currently lists "api-key", "basic-auth", "session" (the "methods" schema around the shown enum) to add a short clarifying sentence that "Bearer" is treated as an API-key transport form (i.e., bearer tokens are conveyed via an Authorization: Bearer header and therefore map to api-key semantics), and apply the same clarification to the other identical enum occurrence referenced at lines 4067-4072 so clients understand why "bearer" was excluded and how to use bearer tokens.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/routes/nip46.ts`:
- Around line 205-213: Move the CORS preflight handling to run before calling
initializeNip46DB (i.e., check for method === 'OPTIONS' and return the preflight
Response with headers immediately), and when catching errors from
initializeNip46DB avoid returning raw error.message to clients—log the full
error server-side (console.error) and return a generic error payload like {
error: 'DB_INIT_FAILED', message: 'Internal server error' } in the Response.json
call while preserving status and headers; update the code around
initializeNip46DB, the catch block, and the Response.json invocation
accordingly.
---
Duplicate comments:
In `@tests/routes/helpers/script-runner.ts`:
- Around line 66-77: The current logic in the script-runner builds nextEnv by
copying all of process.env then deleting blocked keys, which can leak unrelated
host/CI variables; instead, construct nextEnv from a minimal allowlist baseline
(or start empty) and explicitly add only required variables (e.g., PATH, HOME,
NODE_ENV or any specific vars your tests need) rather than inheriting everything
and using isBlockedEnvKey to prune; update the code around nextEnv creation (the
nextEnv variable and the loop that populates it) to populate only allowed keys
and keep isBlockedEnvKey for any additional safety checks.
---
Nitpick comments:
In `@docs/openapi/openapi.json`:
- Around line 3003-3009: Update the OpenAPI schema description for the enum that
currently lists "api-key", "basic-auth", "session" (the "methods" schema around
the shown enum) to add a short clarifying sentence that "Bearer" is treated as
an API-key transport form (i.e., bearer tokens are conveyed via an
Authorization: Bearer header and therefore map to api-key semantics), and apply
the same clarification to the other identical enum occurrence referenced at
lines 4067-4072 so clients understand why "bearer" was excluded and how to use
bearer tokens.
In `@frontend/components/nip46/RelaySettings.tsx`:
- Around line 78-84: The catch variable in the onClick handler for the
onRemove(relay) call shadows the component prop named error; rename the catch
binding (e.g., to removalError) and update the handler to use the same
error-handling approach as handleAdd (instead of logging to console) — i.e.,
call the component's existing error setter/handler used by handleAdd (or follow
the same silent swallow behavior) after catching removalError to keep handling
consistent with handleAdd.
In `@frontend/components/ui/collapsible.tsx`:
- Line 76: Remove the trailing space after the semicolon in the export statement
for the Collapsible symbol and rename the file to use PascalCase (e.g.,
Collapsible.tsx) to follow React component naming guidelines; update any imports
that reference the old filename to the new PascalCase name and ensure the
exported symbol remains "Collapsible" (export { Collapsible }) with no trailing
whitespace.
In `@src/routes/env.ts`:
- Around line 312-334: Create a single helper function (e.g.,
validateEnvCredentials) to centralize the repeated credential validation logic
currently duplicated around validateRelayUrls, validateGroup, and validateShare;
the helper should accept the request body and headers (or validKeys) and return
a uniform result object (success flag, error message, and normalized values like
relayValidation.urls) so callers in both DB/headless branches can call
validateEnvCredentials instead of repeating the blocks that call
validateRelayUrls(body.RELAYS), validateGroup(body.GROUP_CRED), and
validateShare(body.SHARE_CRED); update the code paths that currently return
Response.json(...) on validation failures to use the helper’s result and
short-circuit the same way when result indicates failure.
In `@src/routes/status.ts`:
- Line 62: The ternary assigning dbUserId is redundant; replace the expression
in the status route that uses parsedUserId (the const dbUserId assignment) with
a single direct call to BigInt(parsedUserId) so you remove the unnecessary
typeof check and simplify to const dbUserId = BigInt(parsedUserId).
In `@src/routes/user.ts`:
- Around line 286-293: The relay normalization is inconsistent: in the
/api/user/credentials path you trim relays before storing (updates.relays =
body.relays.map(...).trim()), but in the /api/user/relays POST/PUT path
validated relays are written raw. Update the /api/user/relays handler to apply
the same normalization helper (trim each entry and preserve null handling) to
body.relays before calling updateUserCredentials so both paths set
updates.relays the same way; reuse the same logic used around updates.relays and
body.relays in the credentials handler (or extract it to a small helper and call
it from the /api/user/relays flow).
In `@tests/e2e/global-teardown.ts`:
- Around line 35-46: The getProcessCommand function currently calls the
Unix-only ps tool; add a platform check at the start of getProcessCommand
(process.platform === 'win32') and handle Windows gracefully—either return null
immediately or implement Windows-specific logic (e.g., wmic/PowerShell) to
retrieve the command for the given pid; keep the existing execFileSync-based
Unix branch (the current 'ps' call) for non-win32 platforms and preserve error
handling so behavior stays consistent across OSes.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (60)
.github/workflows/ci.yml.github/workflows/release.yml.gitignoreDockerfiledocs/openapi/README.mddocs/openapi/openapi.jsondocs/openapi/openapi.yamlfrontend/components/Configure.tsxfrontend/components/Signer.tsxfrontend/components/nip46/RelaySettings.tsxfrontend/components/nip46/Requests.tsxfrontend/components/ui/card.tsxfrontend/components/ui/collapsible.tsxfrontend/components/ui/input-with-validation.tsxfrontend/components/ui/peer-list.tsxfrontend/components/ui/tooltip.tsxfrontend/types/index.tsllm/implementation/e2e-smoke-tests.mdllm/implementation/node-lifecycle-implementation.mdllm/implementation/umbrel-implementation.mdpackage.jsonplaywright.config.tsscripts/patch-zod-compat.mjsscripts/release.shsrc/class/relay.test.tssrc/class/relay.tssrc/config/crypto.tssrc/db/migrator.tssrc/routes/admin.tssrc/routes/auth.tssrc/routes/env.tssrc/routes/index.tssrc/routes/nip04.tssrc/routes/nip44.tssrc/routes/nip46.tssrc/routes/onboarding.tssrc/routes/status.tssrc/routes/user.tssrc/routes/utils.test.tssrc/routes/utils.tssrc/server.tssrc/utils/rate-limiter.tstests/e2e/cosigner.mjstests/e2e/global-setup.tstests/e2e/global-teardown.tstests/e2e/helpers.tstests/e2e/smoke-test-defaults.jsontests/e2e/specs/01-auth.e2e.tstests/e2e/specs/02-status-peers.e2e.tstests/e2e/specs/03-nip44-nip04.e2e.tstests/e2e/specs/04-sign.e2e.tstests/e2e/specs/05-admin.e2e.tstests/e2e/specs/06-event-log.e2e.tstests/e2e/specs/07-env.e2e.tstests/e2e/specs/08-ui.e2e.tstests/e2e/state.tstests/routes/admin.whoami.session.spec.tstests/routes/env.db-mode.spec.tstests/routes/helpers/script-runner.spec.tstests/routes/helpers/script-runner.ts
|
Superseded by #44 (same changes on renamed branch |
Summary by CodeRabbit
New Features
Tests
Bug Fixes / Validation
Chores
Documentation