Skip to content
Draft
103 changes: 103 additions & 0 deletions apps/api/__tests__/docs-routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const { apiReferenceMock, generateMock } = vi.hoisted(() => ({
apiReferenceMock: vi.fn(() => () => new Response("openapi-ui")),
generateMock: vi.fn(async () => ({
paths: {
"/v1/ping": {
get: {},
post: {},
},
},
components: {},
})),
}));

vi.mock("@acme/env", () => ({
env: {
NEXT_PUBLIC_API_URL: "https://api.example.com",
},
}));

vi.mock("@acme/api", () => ({
router: {},
}));

vi.mock("@scalar/nextjs-api-reference", () => ({
ApiReference: apiReferenceMock,
}));

vi.mock("@orpc/openapi", () => ({
OpenAPIGenerator: vi.fn(() => ({
generate: generateMock,
})),
}));

vi.mock("@orpc/zod", () => ({
ZodToJsonSchemaConverter: vi.fn(),
}));

import { GET as getDocs } from "../src/app/docs/route";
import { GET as getOpenApiJson } from "../src/app/docs/openapi.json/route";

describe("docs routes", () => {
beforeEach(() => {
apiReferenceMock.mockClear();
});

it("renders the API reference page with the configured base URL", async () => {
const response = await getDocs();

expect(response.status).toBe(200);
await expect(response.text()).resolves.toBe("openapi-ui");
expect(apiReferenceMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "/docs/openapi.json",
baseServerURL: "https://api.example.com",
pageTitle: "F3 Nation API Reference",
favicon: "/favicon.ico",
}),
);
});

it("returns an OpenAPI JSON document with client headers injected", async () => {
const response = await getOpenApiJson(
new Request("https://api.example.com/docs/openapi.json"),
);

expect(response.status).toBe(200);

const spec = (await response.json()) as {
components?: {
parameters?: Record<string, { name: string; in: string }>;
};
paths?: Record<
string,
{
get?: { parameters?: { $ref?: string }[] };
post?: { parameters?: { $ref?: string }[] };
}
>;
};

expect(spec.components?.parameters?.ClientHeader).toMatchObject({
name: "client",
in: "header",
});
expect(spec.paths?.["/v1/ping"]?.get?.parameters?.[0]).toMatchObject({
$ref: "#/components/parameters/ClientHeader",
});
expect(spec.paths?.["/v1/ping"]?.post?.parameters?.[0]).toMatchObject({
$ref: "#/components/parameters/ClientHeader",
});

const generateCalls = generateMock.mock.calls as unknown[][];
const generateOptions = generateCalls[0]?.[1] as
| { filter?: (args: { path: string[] }) => boolean }
| undefined;

expect(generateOptions?.filter).toBeTypeOf("function");
expect(generateOptions?.filter?.({ path: ["slack"] })).toBe(false);
expect(generateOptions?.filter?.({ path: ["ping"] })).toBe(true);
});
});
3 changes: 3 additions & 0 deletions apps/api/src/app/docs/openapi.json/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export async function GET(request: Request) {
});

const spec = (await generator.generate(router, {
filter: ({ path }) => path[0] !== "slack",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section of the OpenAPI route file (around line 57 and 110-124)
FILE="apps/api/src/app/docs/openapi.json/route.ts"
if [ -f "$FILE" ]; then
  echo "=== $FILE (lines 40-90) ==="
  nl -ba "$FILE" | sed -n '40,90p'
  echo
  echo "=== $FILE (lines 95-150) ==="
  nl -ba "$FILE" | sed -n '95,150p'
else
  echo "File not found: $FILE"
fi

# Find where "openapi.json" is built/filters are applied (if any other occurrences exist)
rg -n "openapi\.json|filter:\s*\(\{\s*path\s*\}|\bslack\b" apps/api/src/app/docs/openapi.json/route.ts apps/api/src -S || true

# Look for mounting/version prefix logic (verify whether it's apps/api/src/index.ts or packages/api/src/index.ts)
# Search for "v1" and route mounting and also for "apps/api/src/index.ts" / "packages/api/src/index.ts".
rg -n "mount|app\.use|/v1|\\bv1\\b|createServer|listen|next|app\\.route|OpenAPI|openapi" -S apps/api/src packages/api/src 2>/dev/null || true
ls -la apps/api/src || true
ls -la packages/api/src 2>/dev/null || true

# If index.ts exists in either location, print key sections
for IDX in apps/api/src/index.ts packages/api/src/index.ts; do
  if [ -f "$IDX" ]; then
    echo
    echo "=== $IDX (first 200 lines) ==="
    nl -ba "$IDX" | sed -n '1,200p'
  fi
done

Repository: F3-Nation/f3-nation

Length of output: 190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/api/src/app/docs/openapi.json/route.ts"

echo "== exists? $FILE =="
ls -la "$FILE" 2>/dev/null || true
echo

if [ -f "$FILE" ]; then
  echo "=== $FILE (lines 40-90) ==="
  sed -n '40,90p' "$FILE" | cat -n

  echo
  echo "=== $FILE (lines 95-150) ==="
  sed -n '95,150p' "$FILE" | cat -n
fi

echo
echo "== grep for slack + filter/path in that file =="
rg -n "filter:\s*\(\{\s*path\s*\}|slack|path\[0\]|path\[1\]" "$FILE" -S || true

echo
echo "== find other OpenAPI generation/filtering =="
rg -n "openapi\.json|OpenAPI|swagger|filter:\s*\(\{\s*path\s*\}" apps/api/src packages/api/src -S || true

echo
echo "== check for /v1 mounting (index/route mounting) =="
rg -n "(/v1\b|\\bv1\\b|app\.use|router\.use|next|express|createServer|mount)" apps/api/src packages/api/src -S || true

echo
echo "== show likely index.ts files (first 200 lines) =="
for IDX in apps/api/src/index.ts packages/api/src/index.ts; do
  if [ -f "$IDX" ]; then
    echo "---- $IDX ----"
    sed -n '1,200p' "$IDX" | cat -n
  fi
done

Repository: F3-Nation/f3-nation

Length of output: 15106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/api/src/app/docs/openapi.json/route.ts"
echo "wc -l: $(wc -l < "$FILE")"
echo

echo "=== filter line (around 45-70) with file line numbers ==="
cat -n "$FILE" | sed -n '45,70p'

echo
echo "=== search for typo word 'restr' in the file (with file line numbers) ==="
rg -n "restr" "$FILE" -S || true
# Print a bit around the match area
MATCH_LINE="$(rg -n "restr" "$FILE" -S | head -n1 | cut -d: -f1 || true)"
if [ -n "${MATCH_LINE:-}" ]; then
  START=$((MATCH_LINE-10))
  END=$((MATCH_LINE+10))
  echo
  echo "=== lines ${START}-${END} around first match ==="
  cat -n "$FILE" | sed -n "${START},${END}p"
fi

Repository: F3-Nation/f3-nation

Length of output: 3153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show imports + router variable usage in openapi route
FILE="apps/api/src/app/docs/openapi.json/route.ts"
echo "=== top of $FILE (1-80) ==="
sed -n '1,80p' "$FILE" | cat -n

echo
echo "=== find where router is imported/declared in $FILE ==="
rg -n "from .*router|import .*router|const router|generator\.generate\(" "$FILE" -S || true

# 2) Locate API_PREFIX_V1 definition
echo
echo "=== find API_PREFIX_V1 definition ==="
rg -n "API_PREFIX_V1" -S . || true

Repository: F3-Nation/f3-nation

Length of output: 4726


🌐 Web query:

@orpc/openapi OpenAPIGenerator filter ({ path }) signature path array includes prefix

💡 Result:

In the @orpc/openapi package, the OpenAPIGenerator filter option allows you to include or exclude procedures from the generated OpenAPI specification [1][2]. The filter function receives an object containing path as one of its arguments, which is a readonly string[] array representing the path to the procedure within your router [3][4]. You can check if the path includes a specific prefix by inspecting the elements of this array [1][2]. For example, if you want to exclude any procedures under an "internal" path, you can use the following approach [1][2]: const spec = await generator.generate(router, { filter: ({ path }) => path!== 'internal', }) In this example, path[0] === 'internal' checks if the first segment of the path is 'internal' [1][2]. Because path is an array of strings, you can use standard JavaScript array methods like includes() if you need to check for a prefix that might exist at any level of the path hierarchy, or simply index into the array if the prefix is always at a specific depth [1][2]. The filter function should return true to include the procedure or false to exclude it [1][3][2].

Citations:


Fix OpenAPI Slack filtering to handle the /v1 prefix (and correct docs typo).

  • apps/api/src/app/docs/openapi.json/route.ts builds the spec from router imported from @acme/api, whose packages/api/src/index.ts prefixes all routes with API_PREFIX_V1 (/v1). That means Slack endpoints are typically under path[1], so path[0] !== "slack" won’t filter them out.
  • Fix the user-facing typo restrcitedrestricted in the OpenAPI info.description.
Suggested fix
-    filter: ({ path }) => path[0] !== "slack",
+    filter: ({ path }) => {
+      const first = path[0];
+      const second = path[1];
+      return !(first === "slack" || second === "slack");
+    },
📝 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
filter: ({ path }) => path[0] !== "slack",
filter: ({ path }) => {
const first = path[0];
const second = path[1];
return !(first === "slack" || second === "slack");
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/app/docs/openapi.json/route.ts` at line 57, The OpenAPI
generator's path filter in route.ts currently checks path[0] !== "slack" but
routes are prefixed with API_PREFIX_V1 ("/v1"), so update the filter used when
building the spec (the filter callback referenced in route.ts that receives {
path }) to account for the "/v1" prefix (e.g., check path[0] !== "slack" &&
path[1] !== "slack" or normalize/remove API_PREFIX_V1 before checking) so Slack
endpoints are excluded; also correct the typo in the OpenAPI info.description
from "restrcited" to "restricted" so the user-facing docs string is accurate.

info: {
title: "F3 Nation API",
version: packageJson.version,
Expand Down Expand Up @@ -106,6 +107,7 @@ API keys inherit the roles and permissions of their owner. Access levels include
- **Editor**: Can view and modify data within assigned organizations
- **Admin**: Same permissions as Editor, plus the ability to add/remove other Admins and Editors


As of February 1, 2026, regional admins can only create read-only API keys. If you want an API Key edit access to your region, you will need to contact an F3 Nation admin. Right now, regional edit access is highly restrcited. This is because the API is still new and there are almost certainly gaps in security - meaning that a region has the potential to mess up data for other regions.

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 | ⚡ Quick win

Fix typo in public API policy text.

“restrcited” should be “restricted”.

Suggested fix
-Right now, regional edit access is highly restrcited.
+Right now, regional edit access is highly restricted.
📝 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
As of February 1, 2026, regional admins can only create read-only API keys. If you want an API Key edit access to your region, you will need to contact an F3 Nation admin. Right now, regional edit access is highly restrcited. This is because the API is still new and there are almost certainly gaps in security - meaning that a region has the potential to mess up data for other regions.
As of February 1, 2026, regional admins can only create read-only API keys. If you want an API Key edit access to your region, you will need to contact an F3 Nation admin. Right now, regional edit access is highly restricted. This is because the API is still new and there are almost certainly gaps in security - meaning that a region has the potential to mess up data for other regions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/app/docs/openapi.json/route.ts` at line 111, Typo in the public
API policy text: update the policy string in route.ts (the OpenAPI/public API
policy description) by replacing the misspelled word "restrcited" with the
correct "restricted" so the sentence reads "...regional edit access is highly
restricted."; locate the policy text used to build the OpenAPI doc (the public
API policy description) and fix that string literal.


## Best Practices
Expand All @@ -119,6 +121,7 @@ As of February 1, 2026, regional admins can only create read-only API keys. If y
- **401 Unauthorized**: Missing, invalid, revoked, or expired API key
- **403 Forbidden**: Valid API key but insufficient permissions for the requested resource
- **429 Too Many Requests**: Rate limit exceeded (200 requests per 60 seconds)`,

contact: {
name: "F3 Nation",
url: "https://f3nation.com",
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { orgRouter } from "./router/org";
import { pingRouter } from "./router/ping";
import { positionRouter } from "./router/position";
import { requestRouter } from "./router/request";
import { slackRouter } from "./router/slack";
import { userRouter } from "./router/user";

// Re-export webhook event types for external use
Expand All @@ -39,5 +40,6 @@ export const router = os.prefix(API_PREFIX_V1).router({
org: os.prefix("/org").router(orgRouter),
position: os.prefix("/position").router(positionRouter),
request: os.prefix("/request").router(requestRouter),
slack: os.prefix("/slack").router(slackRouter),
user: os.prefix("/user").router(userRouter),
});
Loading