Skip to content

add services - #295

Open
ryo8000 wants to merge 1 commit into
mainfrom
feature/services
Open

add services#295
ryo8000 wants to merge 1 commit into
mainfrom
feature/services

Conversation

@ryo8000

@ryo8000 ryo8000 commented Mar 22, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Refactor

    • Reorganized internal request handling by extracting business logic into dedicated service modules for improved code maintainability and error handling consistency across endpoints.
  • Documentation

    • Updated project documentation to reflect the new service layer structure for business logic organization.

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown

Walkthrough

A service layer architecture is introduced with new business logic modules for base64 encoding/decoding, basic authentication, redirect validation, shutdown control, and HTTP status validation. Route handlers are refactored to delegate to these services instead of performing inline validation and error handling.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md, DEVELOPMENT_GUIDE.md
Added documentation describing the new service layer pattern where business logic is extracted into src/services/, services return structured result objects with { status, body, headers? }, and do not throw exceptions.
Service Layer – Base64
src/services/base64.ts
New module exporting base64Encode and base64Decode functions that accept request bodies and return typed results. Both extract a string value (supporting raw string or object format), encode/decode using Buffer, and validate Base64 format on decode.
Service Layer – Authentication
src/services/basic-auth.ts
New module exporting basicAuth function that validates Basic Authentication credentials, checks Authorization header presence and format, decodes Base64 credentials, compares against expected user/password, and returns appropriate 200, 401, or 400 responses with WWW-Authenticate header support.
Service Layer – Utilities
src/services/redirect.ts, src/services/shutdown.ts, src/services/status.ts
Three utility services: redirect validates URL and HTTP redirect status codes (301, 302, 303, 307, 308); shutdown conditionally permits process termination; status validates HTTP status codes (200–599 range).
Route Handlers – Refactored
src/routes/base64.ts, src/routes/basic-auth.ts, src/routes/redirect.ts, src/routes/shutdown.ts, src/routes/status.ts
All route handlers simplified to delegate business logic to corresponding services, remove inline validation/error handling, adopt structured service result pattern with status and body fields, and update error logging to pass errors as objects ({ err }).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 Hops through the codebase with glee,
Routes now lean as can be,
Services handle the work,
No logic to lurk,
Cleaner, maintainable code—hooray for me! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'add services' is vague and generic, using non-descriptive phrasing that doesn't convey the specific architectural refactoring being implemented. Use a more descriptive title that explains the refactoring, such as 'Refactor routes to use service layer for business logic' or 'Extract business logic into services layer'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/services

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

❤️ Share

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

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a dedicated service layer to the application, extracting business logic from route handlers into reusable service functions. This refactoring improves code organization, maintainability, and testability by ensuring that route handlers remain thin and primarily focus on request/response orchestration, while the core application logic resides in the src/services/ directory.

Highlights

  • Service Layer Introduction: A new src/services/ directory has been added to house business logic, promoting separation of concerns and improving code organization.
  • Route Handler Refactoring: Existing route handlers for base64, basic-auth, redirect, shutdown, and status have been refactored to delegate their core logic to the newly introduced service functions, making routes thinner and more focused on request/response orchestration.
  • Documentation Updates: The AGENTS.md and docs/DEVELOPMENT_GUIDE.md files have been updated to reflect the new service layer architecture and directory structure, providing clear guidance for future development.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.


Logic now resides, Routes are thin, a cleaner path, Services now shine.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request successfully introduces a service layer to separate business logic from route handlers, which is a great improvement for maintainability. The new service files are well-implemented and follow the intended design of returning result objects instead of throwing errors.

I've pointed out a minor inconsistency in src/routes/base64.ts where unnecessary try...catch blocks were used around service calls. Removing them will make the code cleaner and more consistent with the new architecture. Overall, this is a solid refactoring effort.

Comment thread src/routes/base64.ts
Comment on lines +9 to 18
let result;
try {
const encoded = Buffer.from(valueToEncode, 'utf8').toString('base64');
res.status(HttpStatusCodes.OK).json({ encoded });
} catch (error) {
log.error(error, 'Failed to encode value to Base64');
res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({
error: {
message: 'Failed to encode value to Base64',
},
});
result = base64Encode(req.body);
} catch (err) {
log.error({ err }, 'Failed to encode value to Base64');
res
.status(HttpStatusCodes.INTERNAL_SERVER_ERROR)
.json({ error: { message: 'Failed to encode value to Base64' } });
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The try...catch block is unnecessary here. According to the new architecture described in AGENTS.md, service functions are designed to return a result object and not throw exceptions for expected errors. Removing the try...catch will make the code cleaner and more consistent with the other route handlers in this PR.

Suggested change
let result;
try {
const encoded = Buffer.from(valueToEncode, 'utf8').toString('base64');
res.status(HttpStatusCodes.OK).json({ encoded });
} catch (error) {
log.error(error, 'Failed to encode value to Base64');
res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({
error: {
message: 'Failed to encode value to Base64',
},
});
result = base64Encode(req.body);
} catch (err) {
log.error({ err }, 'Failed to encode value to Base64');
res
.status(HttpStatusCodes.INTERNAL_SERVER_ERROR)
.json({ error: { message: 'Failed to encode value to Base64' } });
return;
}
const result = base64Encode(req.body);

Comment thread src/routes/base64.ts
Comment on lines +23 to 32
let result;
try {
const decodedBuffer = Buffer.from(valueToDecode, 'base64');

// Validate Base64 format
if (decodedBuffer.toString('base64') !== valueToDecode) {
res.status(HttpStatusCodes.BAD_REQUEST).json({
error: { message: 'Invalid Base64 format' },
});
return;
}

const decoded = decodedBuffer.toString('utf8');
res.status(HttpStatusCodes.OK).json({ decoded });
} catch (error) {
log.error(error, 'An unexpected error occurred during decoding.');
res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({
error: { message: 'An unexpected error occurred during decoding.' },
});
result = base64Decode(req.body);
} catch (err) {
log.error({ err }, 'An unexpected error occurred during decoding.');
res
.status(HttpStatusCodes.INTERNAL_SERVER_ERROR)
.json({ error: { message: 'An unexpected error occurred during decoding.' } });
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Similar to the /encode endpoint, this try...catch block is not needed. The base64Decode service function handles errors by returning a result object, so it won't throw. Removing the try...catch will improve consistency and simplify the code.

Suggested change
let result;
try {
const decodedBuffer = Buffer.from(valueToDecode, 'base64');
// Validate Base64 format
if (decodedBuffer.toString('base64') !== valueToDecode) {
res.status(HttpStatusCodes.BAD_REQUEST).json({
error: { message: 'Invalid Base64 format' },
});
return;
}
const decoded = decodedBuffer.toString('utf8');
res.status(HttpStatusCodes.OK).json({ decoded });
} catch (error) {
log.error(error, 'An unexpected error occurred during decoding.');
res.status(HttpStatusCodes.INTERNAL_SERVER_ERROR).json({
error: { message: 'An unexpected error occurred during decoding.' },
});
result = base64Decode(req.body);
} catch (err) {
log.error({ err }, 'An unexpected error occurred during decoding.');
res
.status(HttpStatusCodes.INTERNAL_SERVER_ERROR)
.json({ error: { message: 'An unexpected error occurred during decoding.' } });
return;
}
const result = base64Decode(req.body);

Comment thread src/routes/redirect.ts
}

res.redirect(redirectStatus, url);
res.redirect(result.status, result.url);

Check warning

Code scanning / CodeQL

Server-side URL redirect Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 5 months ago

In general terms, the problem should be fixed by validating or constraining the redirect target derived from user input before passing it to res.redirect. Typical strategies are: (a) allow only relative URLs (paths) that stay on the same host, (b) enforce a domain whitelist, or (c) map user-provided keys to a server-side whitelist of concrete URLs. Any user-provided absolute URL to an arbitrary origin should be rejected or normalized to a safe default.

The single best way to fix this without changing existing functionality too much is to add URL validation in src/services/redirect.ts, because that is where the redirect parameters are already being validated. We can add a small helper isLocalUrl that checks that the provided URL is either a relative path or resolves to the same origin as the application. Since we cannot assume the real domain name, we can use a placeholder base (such as https://example.com) in the URL constructor and ensure that the computed origin matches this base; additionally, we accept URLs that start with / (relative paths) and reject any that are protocol-relative (//evil.com) or absolute to a different host. If the URL is invalid or external, we return an error (e.g., HTTP 400) instead of a success result. This keeps src/routes/redirect.ts unchanged in behavior for valid internal URLs, but prevents open redirects to arbitrary domains.

Concretely:

  • In src/services/redirect.ts, add a helper isSafeRedirectUrl(url: string): boolean above the redirect function. It should:
    • Reject empty strings.
    • Accept relative paths that start with / and do not start with //.
    • For other inputs, construct new URL(url, BASE) with a fixed origin like "https://example.com" and ensure the resulting origin equals the base origin.
  • In the redirect function, after checking that urlParam is present and before returning success, call isSafeRedirectUrl(urlParam). If it returns false, return { ok: false, status: HttpStatusCodes.BAD_REQUEST, body: { error: { message: 'Invalid redirect url' } } }.
  • No changes are required in src/routes/redirect.ts, since it already handles ok: false responses by returning the error JSON and avoids calling res.redirect in that case.
  • No new external dependencies are needed; we just use the built-in URL class available in Node.js.
Suggested changeset 1
src/services/redirect.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/services/redirect.ts b/src/services/redirect.ts
--- a/src/services/redirect.ts
+++ b/src/services/redirect.ts
@@ -6,6 +6,37 @@
   | { ok: false; status: number; body: { error: { message: string } } };
 
 /**
+ * Checks whether the given URL is safe to use as a redirect target.
+ *
+ * A safe URL is either:
+ * - a relative path starting with a single "/", or
+ * - an absolute URL that, when resolved against a fixed base, keeps the same origin.
+ */
+function isSafeRedirectUrl(url: string): boolean {
+  if (!url) {
+    return false;
+  }
+
+  // Disallow protocol-relative URLs like "//evil.com"
+  if (url.startsWith('//')) {
+    return false;
+  }
+
+  // Allow simple relative paths
+  if (url.startsWith('/')) {
+    return true;
+  }
+
+  try {
+    const base = 'https://example.com';
+    const parsed = new URL(url, base);
+    return parsed.origin === base;
+  } catch {
+    return false;
+  }
+}
+
+/**
  * Validates redirect parameters and returns a result object.
  *
  * @param urlParam - The target URL from the query parameter.
@@ -24,6 +55,14 @@
     };
   }
 
+  if (!isSafeRedirectUrl(urlParam)) {
+    return {
+      ok: false,
+      status: HttpStatusCodes.BAD_REQUEST,
+      body: { error: { message: 'Invalid redirect url' } },
+    };
+  }
+
   const redirectStatus = statusParam ? toSafeInteger(statusParam) : HttpStatusCodes.FOUND;
 
   if (redirectStatus === undefined || !RedirectStatuses.has(redirectStatus)) {
EOF
@@ -6,6 +6,37 @@
| { ok: false; status: number; body: { error: { message: string } } };

/**
* Checks whether the given URL is safe to use as a redirect target.
*
* A safe URL is either:
* - a relative path starting with a single "/", or
* - an absolute URL that, when resolved against a fixed base, keeps the same origin.
*/
function isSafeRedirectUrl(url: string): boolean {
if (!url) {
return false;
}

// Disallow protocol-relative URLs like "//evil.com"
if (url.startsWith('//')) {
return false;
}

// Allow simple relative paths
if (url.startsWith('/')) {
return true;
}

try {
const base = 'https://example.com';
const parsed = new URL(url, base);
return parsed.origin === base;
} catch {
return false;
}
}

/**
* Validates redirect parameters and returns a result object.
*
* @param urlParam - The target URL from the query parameter.
@@ -24,6 +55,14 @@
};
}

if (!isSafeRedirectUrl(urlParam)) {
return {
ok: false,
status: HttpStatusCodes.BAD_REQUEST,
body: { error: { message: 'Invalid redirect url' } },
};
}

const redirectStatus = statusParam ? toSafeInteger(statusParam) : HttpStatusCodes.FOUND;

if (redirectStatus === undefined || !RedirectStatuses.has(redirectStatus)) {
Copilot is powered by AI and may make mistakes. Always verify output.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/services/shutdown.ts (1)

3-5: Return status on the success branch too.

Line 22 is the only new service success path here without a status, which forces callers to special-case this result instead of handling every service response the same way.

♻️ Proposed change
 type ShutdownResult =
-  | { ok: true; body: { message: string } }
+  | { ok: true; status: number; body: { message: string } }
   | { ok: false; status: number; body: { error: { message: string } } };
@@
-  return { ok: true, body: { message: 'Server shutting down' } };
+  return {
+    ok: true,
+    status: HttpStatusCodes.OK,
+    body: { message: 'Server shutting down' },
+  };

Also applies to: 13-22

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

In `@src/services/shutdown.ts` around lines 3 - 5, The success variant of the
ShutdownResult type is missing a status field which forces callers to
special-case responses; update the type so the ok: true branch includes status:
number (e.g., change { ok: true; body: { message: string } } to { ok: true;
status: number; body: { message: string } }) and then ensure any functions that
return a successful shutdown (the code paths that construct ShutdownResult with
ok: true) include an appropriate HTTP status value so all callers can uniformly
read .status on both branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@AGENTS.md`:
- Line 37: Update the service contract documentation line so it describes the
actual discriminated-union return shape used by the new services: mention that
each service function returns a result object with an "ok" boolean discriminator
(e.g., { ok: true | false, status, body?, headers? }) and that successful (ok:
true) branches may omit body and headers; replace the inaccurate sentence "Each
service function returns a result object (`{ status, body, headers? }`) without
throwing" with this corrected description to match the implementations of the
service functions and their result type.

In `@src/services/basic-auth.ts`:
- Around line 38-47: The auth check currently requires the exact "Basic "
casing; change it to accept the scheme case-insensitively by splitting
authHeader into scheme and value (e.g., const [scheme, base64Credentials] =
authHeader.split(' ', 2)), verify scheme.toLowerCase() === 'basic', and then
continue using the extracted base64Credentials with
Buffer.from(base64Credentials, 'base64').toString('utf-8') so headers like
"basic ..." are accepted while preserving correct credential decoding.

In `@src/services/redirect.ts`:
- Around line 27-29: The current ternary treats an empty statusParam
("?status=") as missing and defaults to 302; change the logic so you only
default to HttpStatusCodes.FOUND when statusParam is strictly undefined,
otherwise attempt to parse and validate the provided value. Concretely, set
redirectStatus = statusParam === undefined ? HttpStatusCodes.FOUND :
toSafeInteger(statusParam) and then keep the existing validation using
RedirectStatuses.has(redirectStatus); ensure toSafeInteger returns undefined or
NaN for empty/invalid input so the subsequent check (redirectStatus ===
undefined || !RedirectStatuses.has(redirectStatus)) will catch an explicit empty
string instead of silently using 302.
- Around line 19-42: The current handler returns user-supplied urlParam directly
(see urlParam and the final return) creating an open-redirect; update the
validation so only safe relative-path redirects are allowed: reject any urlParam
that contains a scheme (e.g. "http:"), a double-slash host ("//"), or that does
not begin with a single "/" path segment, and keep the existing redirectStatus
validation (toSafeInteger, RedirectStatuses) untouched; on invalid targets
return the BAD_REQUEST error like other checks and only return ok:true with url
when the urlParam passes the relative-path-only check.

---

Nitpick comments:
In `@src/services/shutdown.ts`:
- Around line 3-5: The success variant of the ShutdownResult type is missing a
status field which forces callers to special-case responses; update the type so
the ok: true branch includes status: number (e.g., change { ok: true; body: {
message: string } } to { ok: true; status: number; body: { message: string } })
and then ensure any functions that return a successful shutdown (the code paths
that construct ShutdownResult with ok: true) include an appropriate HTTP status
value so all callers can uniformly read .status on both branches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c826e9f9-5c14-4c8f-8e66-1c9dba14ea3c

📥 Commits

Reviewing files that changed from the base of the PR and between bda7214 and e2e8161.

📒 Files selected for processing (12)
  • AGENTS.md
  • docs/DEVELOPMENT_GUIDE.md
  • src/routes/base64.ts
  • src/routes/basic-auth.ts
  • src/routes/redirect.ts
  • src/routes/shutdown.ts
  • src/routes/status.ts
  • src/services/base64.ts
  • src/services/basic-auth.ts
  • src/services/redirect.ts
  • src/services/shutdown.ts
  • src/services/status.ts

Comment thread AGENTS.md

**Service Layer**:
- Business logic extracted into `src/services/` to keep routes thin
- Each service function returns a result object (`{ status, body, headers? }`) without throwing

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Service contract documentation is currently inaccurate.

The new services return discriminated union results with an ok field, and success branches do not always include body/headers. The documented contract should match that to avoid drift.

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

In `@AGENTS.md` at line 37, Update the service contract documentation line so it
describes the actual discriminated-union return shape used by the new services:
mention that each service function returns a result object with an "ok" boolean
discriminator (e.g., { ok: true | false, status, body?, headers? }) and that
successful (ok: true) branches may omit body and headers; replace the inaccurate
sentence "Each service function returns a result object (`{ status, body,
headers? }`) without throwing" with this corrected description to match the
implementations of the service functions and their result type.

Comment on lines +38 to +47
if (!authHeader || !authHeader.startsWith('Basic ')) {
return {
status: HttpStatusCodes.UNAUTHORIZED,
body: { authenticated: false, message: 'Authentication required' },
headers: { 'WWW-Authenticate': 'Basic realm="Access to /basic-auth"' },
};
}

const base64Credentials = authHeader.substring('Basic '.length);
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Accept the Basic auth scheme case-insensitively.

Line 38 only matches the exact Basic prefix, and Line 46 slices based on that exact casing. Valid headers like Authorization: basic ... will be rejected even though auth schemes are case-insensitive.

🐛 Proposed fix
-  if (!authHeader || !authHeader.startsWith('Basic ')) {
+  const basicMatch = authHeader?.match(/^basic\s+(\S+)$/i);
+  if (!basicMatch) {
     return {
       status: HttpStatusCodes.UNAUTHORIZED,
       body: { authenticated: false, message: 'Authentication required' },
       headers: { 'WWW-Authenticate': 'Basic realm="Access to /basic-auth"' },
     };
   }
 
-  const base64Credentials = authHeader.substring('Basic '.length);
+  const [, base64Credentials] = basicMatch;
📝 Committable suggestion

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

Suggested change
if (!authHeader || !authHeader.startsWith('Basic ')) {
return {
status: HttpStatusCodes.UNAUTHORIZED,
body: { authenticated: false, message: 'Authentication required' },
headers: { 'WWW-Authenticate': 'Basic realm="Access to /basic-auth"' },
};
}
const base64Credentials = authHeader.substring('Basic '.length);
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
const basicMatch = authHeader?.match(/^basic\s+(\S+)$/i);
if (!basicMatch) {
return {
status: HttpStatusCodes.UNAUTHORIZED,
body: { authenticated: false, message: 'Authentication required' },
headers: { 'WWW-Authenticate': 'Basic realm="Access to /basic-auth"' },
};
}
const [, base64Credentials] = basicMatch;
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/basic-auth.ts` around lines 38 - 47, The auth check currently
requires the exact "Basic " casing; change it to accept the scheme
case-insensitively by splitting authHeader into scheme and value (e.g., const
[scheme, base64Credentials] = authHeader.split(' ', 2)), verify
scheme.toLowerCase() === 'basic', and then continue using the extracted
base64Credentials with Buffer.from(base64Credentials,
'base64').toString('utf-8') so headers like "basic ..." are accepted while
preserving correct credential decoding.

Comment thread src/services/redirect.ts
Comment on lines +19 to +42
if (!urlParam) {
return {
ok: false,
status: HttpStatusCodes.BAD_REQUEST,
body: { error: { message: 'Missing `url` query parameter' } },
};
}

const redirectStatus = statusParam ? toSafeInteger(statusParam) : HttpStatusCodes.FOUND;

if (redirectStatus === undefined || !RedirectStatuses.has(redirectStatus)) {
return {
ok: false,
status: HttpStatusCodes.BAD_REQUEST,
body: {
error: {
message:
'Invalid redirect status code. Supported statuses are 301, 302, 303, 307 and 308',
},
},
};
}

return { ok: true, status: redirectStatus, url: urlParam };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Untrusted redirect target creates an open-redirect vector.

The service forwards user-controlled urlParam to redirect response construction without target validation. This can be abused for phishing via trusted domain links.

Suggested mitigation (relative-path only)
 export function redirect(
   urlParam: string | undefined,
   statusParam: string | undefined,
 ): RedirectResult {
   if (!urlParam) {
     return {
       ok: false,
       status: HttpStatusCodes.BAD_REQUEST,
       body: { error: { message: 'Missing `url` query parameter' } },
     };
   }
+
+  if (!urlParam.startsWith('/')) {
+    return {
+      ok: false,
+      status: HttpStatusCodes.BAD_REQUEST,
+      body: {
+        error: { message: 'Invalid redirect target. Only relative paths are allowed' },
+      },
+    };
+  }

   const redirectStatus =
     statusParam === undefined ? HttpStatusCodes.FOUND : toSafeInteger(statusParam);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/redirect.ts` around lines 19 - 42, The current handler returns
user-supplied urlParam directly (see urlParam and the final return) creating an
open-redirect; update the validation so only safe relative-path redirects are
allowed: reject any urlParam that contains a scheme (e.g. "http:"), a
double-slash host ("//"), or that does not begin with a single "/" path segment,
and keep the existing redirectStatus validation (toSafeInteger,
RedirectStatuses) untouched; on invalid targets return the BAD_REQUEST error
like other checks and only return ok:true with url when the urlParam passes the
relative-path-only check.

Comment thread src/services/redirect.ts
Comment on lines +27 to +29
const redirectStatus = statusParam ? toSafeInteger(statusParam) : HttpStatusCodes.FOUND;

if (redirectStatus === undefined || !RedirectStatuses.has(redirectStatus)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle empty status explicitly instead of defaulting to 302.

statusParam ? ... : ... treats ?status= as missing and falls back to 302. That silently accepts invalid input.

Suggested fix
-  const redirectStatus = statusParam ? toSafeInteger(statusParam) : HttpStatusCodes.FOUND;
+  const redirectStatus =
+    statusParam === undefined ? HttpStatusCodes.FOUND : toSafeInteger(statusParam);
📝 Committable suggestion

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

Suggested change
const redirectStatus = statusParam ? toSafeInteger(statusParam) : HttpStatusCodes.FOUND;
if (redirectStatus === undefined || !RedirectStatuses.has(redirectStatus)) {
const redirectStatus =
statusParam === undefined ? HttpStatusCodes.FOUND : toSafeInteger(statusParam);
if (redirectStatus === undefined || !RedirectStatuses.has(redirectStatus)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/redirect.ts` around lines 27 - 29, The current ternary treats an
empty statusParam ("?status=") as missing and defaults to 302; change the logic
so you only default to HttpStatusCodes.FOUND when statusParam is strictly
undefined, otherwise attempt to parse and validate the provided value.
Concretely, set redirectStatus = statusParam === undefined ?
HttpStatusCodes.FOUND : toSafeInteger(statusParam) and then keep the existing
validation using RedirectStatuses.has(redirectStatus); ensure toSafeInteger
returns undefined or NaN for empty/invalid input so the subsequent check
(redirectStatus === undefined || !RedirectStatuses.has(redirectStatus)) will
catch an explicit empty string instead of silently using 302.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants