Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ CLARITY_TOKEN=
# [선택] Clarity 에러 알림에 표기할 관리자 이름
ADMIN_NAME=

# ── KOIN 관리자 API ──────────────────────────────────────
# [필수] KOIN 관리자 API 로그인
KOIN_API_BASE_URL=
KOIN_ADMIN_EMAIL=
# KOIN API 명세에 따라 SHA-256으로 처리된 비밀번호
KOIN_ADMIN_PASSWORD=

# ── 로컬 모델(RAG, Ollama 사이드카) ──────────────────────
# [선택] 기본 http://localhost:11434 (봇과 같은 박스의 Ollama). 보통 비워둠
OLLAMA_BASE_URL=
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
ADMIN_NAME: ${{ secrets.ADMIN_NAME }}
GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }}
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
KOIN_API_BASE_URL: ${{ secrets.KOIN_API_BASE_URL }}
KOIN_ADMIN_EMAIL: ${{ secrets.KOIN_ADMIN_EMAIL }}
KOIN_ADMIN_PASSWORD: ${{ secrets.KOIN_ADMIN_PASSWORD }}
run: |
cat <<EOF > .env
SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN
Expand All @@ -76,6 +79,9 @@ jobs:
ADMIN_NAME=$ADMIN_NAME
GOOGLE_REDIRECT_URI=$GOOGLE_REDIRECT_URI
NOTION_TOKEN=$NOTION_TOKEN
KOIN_API_BASE_URL=$KOIN_API_BASE_URL
KOIN_ADMIN_EMAIL=$KOIN_ADMIN_EMAIL
KOIN_ADMIN_PASSWORD=$KOIN_ADMIN_PASSWORD
GDRIVE_FOLDER_IDS=1fMJGkRIuGWBCHnN0pI5Teab9dKtnkw4s,1cxrf10solU3-qPk_1gWmQOSe1KmMM7dA,1XHBKifEhBQdM4DJAOm44YkNxZpMdGAzF,1eKuZ-lzwpxlJu4-9KciF3XYj3GN-lu28,1X2wch3Av6DMYHYufWy6ybol2saRaVdOq,10c1R4a9-HgNrV4t-Vzj6crGhHK1s06HP,1y0yv2oItH3a_Obo5Jvw_ifH3DlbbHbLp,1iEUpsaoquhwCNBLI7hIDznhAuO33YMGo,1dEVWkUaxKpubTdfawzTtE4km-a7LPsRo,1UAAUK7yqHcyCVZI6Xou3u9Eh4YqyAYYc,1XYia9-0r_F-JGU-wLQdmLB8WFllT3ssa,1_HZB6ztDxOloF9Va-Qw2X38iYFiMMPIW,1-aBgwPhmDeSO327VdvR3NFnYqoNu6Anj,1fyrohFc92yjzZ_FuGAs0yssQcxzSgz4t,1J0y8zCdN9-P_ASkZuO3g30_SUKSVRb7r,1MBMf16gw-XPzv9SRrAPq8L-UqWSf7QV9
EOF

Expand Down
24 changes: 24 additions & 0 deletions src/services/koin/adminLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
interface KoinAdminLoginResponse {
token: string;
}
Comment on lines +1 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching adminLogin.ts:"
fd -a 'adminLogin\.ts$' . || true

echo
echo "Relevant file contents:"
for f in $(fd 'adminLogin\.ts$' .); do
  echo "--- $f ---"
  cat -n "$f"
done

echo
echo "Search for KoinAdminLoginResponse:"
rg -n "KoinAdminLoginResponse|admin/user/login|response\.token" . || true

Repository: BCSDLab/B_BOT

Length of output: 1463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Lock/npm files:"
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$' || true

echo
echo "Search for $fetch:"
rg -n "\$fetch" . --glob '!node_modules' --glob '!dist' --glob '!build' || true

Repository: BCSDLab/B_BOT

Length of output: 234


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "package.json deps relevant:"
jq -r '.dependencies, .devDependencies' package.json | sed 's/^/--/; /^null$/d'

echo
echo "Search for fetch implementation:"
rg -n "function \$fetch|const \$fetch|export .*fetch|http|ky|ofetch|unenv" . --glob '!node_modules' --glob '!dist' --glob '!build' || true

Repository: BCSDLab/B_BOT

Length of output: 6385


🌐 Web query:

ofetch 1.4.1 $fetch default parseResponse JSON response null documentation

💡 Result:

In ofetch (including version 1.4.1), the default response parsing mechanism handles responses as follows when a JSON response is received [1]: 1. Parsing Logic: When the response type is resolved to "json" (which is the default if no Content-Type header is present or if the header indicates JSON) [2][3][1], ofetch calls response.text to get the response body as a string [1]. 2. Handling Empty/Null Responses: If the resulting string is empty or falsy, ofetch skips the parsing step [1]. Specifically, the implementation checks if (data) (where data is the text content) before attempting to parse it with the parseResponse function or the default JSON.parse [1]. 3. Result: Consequently, if the response body is empty, the _data property of the response remains undefined or is not assigned a parsed object, avoiding attempts to call JSON.parse() on an empty string, which would otherwise throw a SyntaxError [1]. If you are encountering a null response and need to ensure it is handled gracefully, you can provide a custom parseResponse function to override this default behavior [4][2][1]: await ofetch('/api/endpoint', { parseResponse: (text) => text? JSON.parse(text): null }) Additionally, note that for specific HTTP status codes (such as 204 No Content), ofetch intentionally excludes the body from being processed [1].

Citations:


🌐 Web query:

ofetch parseResponse JSON parsing response body type JSON.parse default null

💡 Result:

In ofetch, the default behavior for parsing JSON response bodies is to use destr (a secure, optimized JSON parser) rather than JSON.parse directly [1][2]. When the response type is json (which is the default when the Content-Type is application/json or missing), ofetch reads the response body as text [1][3]. It then checks if the response body text is non-empty [3]. If the text is present, it passes the string to the configured parser [3]. Regarding your query on null: 1. Default behavior: If the response body is empty or nullish, ofetch typically avoids attempting to parse it as JSON [3]. 2. Custom parsing: If you provide a custom parseResponse function, ofetch will use that function to handle the text [4][5]. If you explicitly set parseResponse: JSON.parse, and the response body is an empty string, JSON.parse('') will throw a SyntaxError. Therefore, if you are providing a custom parser, you should ensure it handles empty or null body scenarios appropriately [4][5]. In summary, ofetch internally uses destr by default for robustness [2], and only attempts to parse the text if it is not empty [3]. If you override this with JSON.parse, ensure your implementation accounts for empty response bodies [4][5].

Citations:


Guard the response before reading response.token.

$fetch<KoinAdminLoginResponse> is a type assertion and does not validate the returned JSON. If admin/user/login returns an unexpected payload, response.token can throw TypeError before the intended login error. Handle the value as unknown, check that it is a non-null object with a non-empty string token, then return response.token.trim().

🤖 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 `@src/services/koin/adminLogin.ts` around lines 1 - 3, Update the admin login
response handling around KoinAdminLoginResponse and the $fetch call to treat the
payload as unknown rather than trusting the generic assertion. Validate that it
is a non-null object with a non-empty string token before returning the trimmed
token, and route invalid payloads through the existing intended login error path
instead of accessing token unsafely.


export async function loginKoinAdmin(): Promise<string> {
const baseURL = import.meta.env.KOIN_API_BASE_URL;
const email = import.meta.env.KOIN_ADMIN_EMAIL;
const password = import.meta.env.KOIN_ADMIN_PASSWORD;
Comment on lines +6 to +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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expect: loginKoinAdmin is server-only and the project defines an intentional
# runtime configuration or envPrefix strategy for the Koin variables.
rg -n -C 4 \
  'loginKoinAdmin|KOIN_(API_BASE_URL|ADMIN_EMAIL|ADMIN_PASSWORD)|runtimeConfig|useRuntimeConfig|envPrefix|import\.meta\.env|process\.env' . || true

Repository: BCSDLab/B_BOT

Length of output: 18449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository files relevant to build/deploy config =="
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|\.nite|\.nitro|nitro\.config\.(ts|js|mjs|cjs)|nuxt\.config\.(ts|js|mjs|cjs)|wrangler\.toml|vercel\.json|netlify\.toml|package-lock\.json|yarn\.lock|\.env|\.output)' || true

echo
echo "== package scripts and deps =="
python3 - <<'PY'
import json
from pathlib import Path
p=Path('package.json')
if p.exists():
    data=json.loads(p.read_text())
    print("scripts:")
    print(json.dumps(data.get("scripts", {}), indent=2, sort_keys=True))
    print("\ndependencies:")
    for k,v in sorted((data.get("dependencies",{})|data.get("devDependencies",{})).items()):
        if any(s in k.lower() for s in ("nitro","nuxt","vite","ofetch","nuxi")):
            print(f"  {k}: {v}")
PY

echo
echo "== loginKoinAdmin definitions and call sites =="
rg -n -C 6 '\bloginKoinAdmin\b|import\s+.*\bkoin/adminLogin\b|from\s+["\']\s*~\s*(/koin/adminLogin|/services/koin/adminLogin|services/koin/adminLogin)\s?["\']' . || true

echo
echo "== nitro/nuxt config occurrences =="
rg -n -C 4 'envPrefix|runtimeConfig|import\.meta\.env\.KOIN|KOIN_(API_BASE_URL|ADMIN_EMAIL|ADMIN_PASSWORD)|srcDir|serverDir|compatibilityDate' . --glob '!pnpm-lock.yaml' || true

Repository: BCSDLab/B_BOT

Length of output: 514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== nitro.config.ts =="
cat -n nitro.config.ts

echo
echo "== package.json =="
cat -n package.json

echo
echo "== relevant env example entries =="
rg -n -C 4 'KOIN_(API_BASE_URL|ADMIN_EMAIL|ADMIN_PASSWORD)' .env.example || true

echo
echo "== loginKoinAdmin usage =="
rg -n -C 6 '\bloginKoinAdmin\b|from ["\']\s*~(/koin/adminLogin|/services/koin/adminLogin|services/koin/adminLogin)\s?["\']' . || true

echo
echo "== runtimeConfig/envPrefix occurrences (excluding lockfile) =="
rg -n -C 4 'runtimeConfig|envPrefix|import\.meta\.env\.KOIN|KOIN_(API_BASE_URL|ADMIN_EMAIL|ADMIN_PASSWORD)|process\.env\.KOIN' --glob '!pnpm-lock.yaml' . || true

Repository: BCSDLab/B_BOT

Length of output: 2436


Use private nitro runtime config for Koin admin credentials.

KOIN-admin_EMAIL and KOIN admin_PASSWORD are accessed through import.meta.env, so they are compile-time env substitution under Nitro unless runtime config is used. Store these secrets as private nitro runtime config values and read them through useRuntimeConfig() by enforcing a server-only module boundary.

🤖 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 `@src/services/koin/adminLogin.ts` around lines 6 - 8, Update the admin
credential access in the Koin login module to use private Nitro runtime
configuration via useRuntimeConfig() instead of import.meta.env, while
preserving the existing base URL behavior. Enforce a server-only module boundary
so KOIN_ADMIN_EMAIL and KOIN_ADMIN_PASSWORD remain private runtime values.

Source: MCP tools


const response = await $fetch<KoinAdminLoginResponse>("admin/user/login", {
Comment on lines +6 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)adminLogin\.ts$|env\.d\.ts$|admin.*login|Koin|koin' || true

echo "== target file =="
if [ -f src/services/koin/adminLogin.ts ]; then
  cat -n src/services/koin/adminLogin.ts
fi

echo "== env declarations =="
for f in $(git ls-files '*.d.ts' | rg 'env|types'); do
  echo "--- $f"
  sed -n '1,220p' "$f"
done

echo "== other Koin env usage =="
rg -n "import\.meta\.env\.(KOIN_API_BASE_URL|KOIN_ADMIN_EMAIL|KOIN_ADMIN_PASSWORD|Koin|KOIN)" . || true

echo "== fetch behavior probe (Node) =="
node - <<'JS'
const values = [undefined, "", " ", null, "ok"];
console.log(
  JSON.stringify(
    values.map(v => ({
      value: v,
      valueAsItWouldBeSent: v,
      trimResult: v && typeof v === "string" ? v.trim() : undefined,
      filtered: !v?.trim()
    })),
    null,
    2
  )
);
JS

Repository: BCSDLab/B_BOT

Length of output: 2654


Reject missing Koin configuration before the request.

ImportMetaEnv declarations are only TypeScript types. If KOIN_API_BASE_URL, KOIN_ADMIN_EMAIL, or KOIN_ADMIN_PASSWORD is unset or blank, $fetch receives an invalid baseURL, email, or password. Validate these values before the login request and report the missing key names.

🤖 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 `@src/services/koin/adminLogin.ts` around lines 6 - 10, Validate
KOIN_API_BASE_URL, KOIN_ADMIN_EMAIL, and KOIN_ADMIN_PASSWORD in the admin login
flow before invoking $fetch, treating unset or blank values as missing. Reject
with an error that identifies every missing configuration key, while preserving
the existing request behavior when all values are valid.

Source: MCP tools

baseURL,
method: "POST",
body: {
email,
password,
}
});
Comment on lines +10 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
git ls-files | rg '(^|/)adminLogin\.ts$|(^|/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$|nuxt\.config|nuxt\.config\.' || true

echo
echo "== adminLogin.ts =="
if [ -f src/services/koin/adminLogin.ts ]; then
  nl -ba src/services/koin/adminLogin.ts | sed -n '1,120p'
fi

echo
echo "== package/lock references =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n '"?ofetch"?\s*|ofetch@|/ofetch/@|node_modules/ofetch' "$f" -C 2 || true
  fi
done

echo
echo "== fetch implementations =="
rg -n '\$fetch\(|$fetch\(|timeout\s*:' src -S || true

Repository: BCSDLab/B_BOT

Length of output: 301


🌐 Web query:

unjs ofetch disabled timeout by default request timeout option ofetch

💡 Result:

In unjs/ofetch, the timeout feature is disabled by default [1][2][3]. You can enable request timeouts by providing a timeout option in milliseconds to your fetch call [4][1][5]. When this option is set, ofetch uses the native AbortSignal.timeout() API to automatically abort the request if it exceeds the specified duration [4]. Key details regarding the timeout configuration include: - Global Configuration: You can set a default timeout for all requests created via $fetch.create() [4]. - Per-Request Override: You can override or disable the timeout for individual requests by passing a new value or undefined to the timeout option [4]. - Error Handling: If a request times out, it will be aborted with a TimeoutError [4]. - Retries: Note that the timeout functionality does not prevent retries by default; if retries are enabled, ofetch will retry the request even after a timeout occurs [4]. To prevent this, you should set retry: false or retry: 0 [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== adminLogin.ts =="
awk '{printf "%6d  %s\n", NR, $0}' src/services/koin/adminLogin.ts | sed -n '1,120p'

echo
echo "== package.json relevant deps =="
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const keys = ['dependencies','devDependencies','peerDependencies','optionalDependencies'];
for (const k of keys) {
  console.log(`-- ${k} --`);
  if (pkg[k]) {
    for (const name of Object.keys(pkg[k]).filter(n => /nuxt|ofetch|fetch/i.test(n))) {
      console.log(`${name}: ${pkg[k][name]}`);
    }
  }
}
JS

echo
echo "== lockfile ofetch versions =="
awk '{printf "%6d  %s\n", NR, $0}' pnpm-lock.yaml | rg -n -C 3 '(^|/npm/ofetch|/ofetch@|/@nuxt/deeplink:|ofetch)' || true

echo
echo "== fetch and timeout usage in src =="
rg -n '\$fetch\(|fetch\(|\.timeout\(|timeout\s*:' src -S || true

Repository: BCSDLab/B_BOT

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== git diff focused on adminLogin =="
git diff -- src/services/koin/adminLogin.ts | sed -n '1,160p'

echo
echo "== any ofetch lock entries =="
arg {printf "%6d  %s\n", NR, $0}' pnpm-lock.yaml | rg -n -C 2 'ofetch@|/npm/ofetch|unjs/ofetch|^  /:|^  npm:|^  file:' || true

echo
echo "== local timeout usage =="
rg -n '\btimeout\s*:\s*[0-9]+|AbortSignal\.timeout|timeout' src package.json pnpm-lock.yaml -S || true

Repository: BCSDLab/B_BOT

Length of output: 280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== focused git diff for adminLogin =="
git diff -- src/services/koin/adminLogin.ts | sed -n '1,160p' || true

echo
echo "== focused lockfile entries for ofetch =="
rg -n -C 3 'ofetch@|/npm/ofetch|unjs/ofetch|`@nuxt/deep`' pnpm-lock.yaml || true

echo
echo "== focused timeout usage =="
rg -n '\btimeout\s*:\s*[0-9]+|AbortSignal\.timeout|timeout' src package.json pnpm-lock.yaml -S || true

Repository: BCSDLab/B_BOT

Length of output: 1141


Bound the Koin login request.

$fetch has no timeout configured here, and ofetch disables timeouts by default. Set a bounded timeout that matches the service SLA and treat an abort as a failed login. ofetch is locked as ofetch@1.4.1, so the per-request timeout option applies.

🤖 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 `@src/services/koin/adminLogin.ts` around lines 10 - 17, Update the $fetch call
in the admin login flow to include the service SLA as its per-request timeout,
using ofetch’s timeout option. Ensure timeout-triggered aborts are handled as
failed logins through the existing error path.

Source: MCP tools


if (typeof response.token !== "string" || response.token.length === 0) {
throw new Error("KOIN 관리자 로그인 응답에 액세스 토큰이 없습니다.");
}

return response.token;
}
5 changes: 4 additions & 1 deletion src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ interface ImportMetaEnv {
readonly GOOGLE_PROJECT_ID: string;
readonly CREDENTIALS_PATH: string;
readonly ADMIN_NAME: string;
readonly KOIN_API_BASE_URL: string;
readonly KOIN_ADMIN_EMAIL: string;
readonly KOIN_ADMIN_PASSWORD: string;
readonly OLLAMA_BASE_URL?: string;
readonly OLLAMA_GEN_MODEL?: string;
readonly NOTION_TOKEN?: string;
readonly GDRIVE_FOLDER_IDS?: string;
}
interface ImportMeta {
env: ImportMetaEnv;
}
}