posimai-root/docs/server-refactor-plan.md

17 KiB
Raw Blame History

server.js リファクタリング計画

最終更新: 2026-04-10 対象: Claude Code / Cursor / Gemini / 全 AI エージェント

このドキュメントは server.js の現状分析・分割設計・実施タイミング判断基準を一元管理します。 リファクタリング作業を始める前に必ず読んでください。


0. 現状サマリー

項目 内容
ファイル /server.js(リポジトリルート)
行数 約3130行2026-04-10 計測・セキュリティ修正追加後)
役割 VPS 上で動く Node.js/Express バックエンド。全アプリの API を1ファイルに集約
問題 テストが書けない・障害切り分けが困難・新機能追加のたびにファイルが肥大化
状態 本番稼働中・障害なし。2026-04-10 にセキュリティ修正7件適用済み。
セキュリティ SSRF ガード追加・レスポンスサイズ上限追加・DB pool 設定改善済み

1. 現在の内部構造(セクション別行数)

server.js (約3130行)
├── L127      require / app 初期化
├── L2884     Auth helpersWebAuthn dynamic import、インメモリレートリミッター
├── L85126    JWT config + session helpers
├── L127173   Express middlewareCORS など)
├── L174300   DB pool (pg) + Gemini + API Key 認証マップ
│              ※ pool.max 15・タイムアウト設定・pool.on('error') 追加済み
├── L261520   共有ユーティリティextractSource、charset 正規化、
│              isSsrfSafeNEW、fetchMetaSSRF+2MB上限追加、
│              fetchFullTextViaJinaSSRF+1MB上限追加、analyzeWithGemini
├── L521760   initDBPostgreSQL スキーマ全定義 — 240行
├── L761809   Express Router 生成 + /health
│
├── ── ルートグループ ─────────────────────────────────────────────
├── L8101146  Auth: Magic Link + session337行
├── L9661146  Auth: Google OAuth / GitHub OAuth181行※上記と連続
├── L11471394 Auth: Passkey / WebAuthn248行
│              合計 Auth routes: 585行
│
├── L13951680 Brain articlesGET/POST/PATCH/DELETE + save即時保存+非同期AI+ history
│              ※ POST /save: 即時INSERT → setImmediate() で fetchMeta/AI をバックグラウンド実行
├── L16811760 Journal routesposts + public + AI tag suggestion + upload
├── L17611860 Site Config routes
├── L18611960 Habit routes
├── L19612010 Pulse routes
├── L20112050 Lens history routes
│
├── L20512210 Feed Media + Feed Articles routes
├── L22112390 TTSVOICEVOXroutes + サーバー側自動プリウォーム
│
├── L23912430 Events前処理
├── L24312900 Together routesgroups / join / share / feed / comments / search
│              ※ POST /together/share: isSsrfSafe() 適用済み
│              ※ archiveShare(): isSsrfSafe() + 1MB上限適用済み
├── L27402900 Atlas proxyGitHub / Vercel / Tailscale scan※上記と連続
│
├── L29013010 Doorkeeper + connpassapp-level、router 外)
├── L30113015 静的ファイル(/uploads
├── L30173115 Stripe webhook
│
└── L31163130 router マウント + サーバー起動 + Feed 背景ジョブ

2. 共有変数の依存マップ

分割時に「どの変数をどのファイルに置くか」の判断基準。

変数 / 関数 宣言行 参照箇所数 参照元セクション
pool (pg Pool) L174 108箇所 全ルートファイル
authMiddleware L197付近 46箇所 全認証必須ルート
genAI (Gemini) L191 2箇所 brain.js のみ
webauthnChallenges (Map) L40 15箇所 auth routes のみ
rateLimitStores / rateLimit() L51 多数 auth routes 中心
isSsrfSafe() L294付近 7箇所 brain.js / together.js
fetchMeta() L304付近 brain + together
fetchFullTextViaJina() L372付近 brain + together
analyzeWithGemini() L416付近 brain のみ
JWT_SECRET L86 auth 全体
WEBAUTHN_RP_* L92 auth/passkey のみ

結論: poolauthMiddleware を共有モジュールに切り出せば、残りのルート分割は機械的に行える。


3. 目標とする分割後の構造

server.js                      ← 100行以内エントリポイント + マウントのみ)
lib/
  db.js                        ← pool + initDB をエクスポート
  auth.js                      ← JWT_SECRET / createToken / authMiddleware / session helpers
  rateLimit.js                 ← インメモリレートリミッターwebauthnChallenges も含める)
  fetch.js                     ← isSsrfSafe / fetchMeta / fetchFullTextViaJina / analyzeWithGemini
routes/
  auth.js                      ← Magic Link + OAuthGoogle/GitHub+ Passkey/WebAuthn
  brain.js                     ← articles / save / history / AI 分析
  journal.js                   ← journal posts + site config
  habit.js                     ← habit + pulse + lens history
  feed.js                      ← feed media + feed articles + TTS + feed バックグラウンドジョブ
  together.js                  ← groups / join / share / comments / search
  events.js                    ← Doorkeeper + connpassapp-level も含めて統合)
  atlas.js                     ← GitHub / Vercel / Tailscale scan proxy
  stripe.js                    ← webhook のみ

各ファイルの想定行数: 100600行。神ファイルは完全に消える。


4. 実施タイミングの判断基準

やらなくていい条件(すべて揃っている間は待つ)

  • 本番で分割に起因する障害が出ていない
  • 新しいルートグループを追加する予定がない
  • 商用化タスクStripe 課金・ユーザー招待・モバイル)が残っている

開始するべきタイミングどれか1つ該当したら着手

トリガー 理由
新ルートグループを追加するとき(例: posimai-station の API 新ファイルを作るついでに既存も整理。追加コストがほぼゼロ
auth.js だけバグが続いて server.js 内のデバッグが辛い 問題のある1ルートだけ先に切り出す部分分割でも OK
Node.js バージョンアップや pg ライブラリ更新のとき どうせ動作確認が必要なので、そのタイミングで進める
server.js が 3500行を超えたとき 純粋に限界。この時点で強制着手

5. 実施手順(着手するときはこの順番で)

フェーズ 1: 共有レイヤーの切り出し低リスク・12時間

ルートは一切触らない。共有変数をモジュールに切り出すだけ。

mkdir lib routes

lib/db.js

'use strict';
const { Pool } = require('pg');

const pool = new Pool({
    host:    process.env.DB_HOST || 'db',
    port:    parseInt(process.env.DB_PORT || '5432'),
    user:    process.env.DB_USER || 'gitea',   // TODO: 'posimai' に変更
    password: process.env.DB_PASSWORD || '',
    database: 'posimai_brain',
    max: 15,
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 5000,
});
pool.on('error', (err) => console.error('[DB] Unexpected pool error:', err.message));

async function initDB() { /* server.js の initDB をそのまま移動 */ }

module.exports = { pool, initDB };

lib/auth.js

// JWT_SECRET, JWT_TTL_SECONDS, createToken, authMiddleware, session helpers
module.exports = { JWT_SECRET, createToken, authMiddleware };

lib/rateLimit.js

// rateLimitStores, rateLimit(), webauthnChallenges, cleanup interval
module.exports = { rateLimit, webauthnChallenges };

lib/fetch.js

// isSsrfSafe, fetchMeta, fetchFullTextViaJina, analyzeWithGemini, extractSource
// ※ isSsrfSafe は 2026-04-10 に server.js 内で実装済み。コピーして移動するだけ。
module.exports = { isSsrfSafe, fetchMeta, fetchFullTextViaJina, analyzeWithGemini };

server.js 側の変更は require 置き換えのみ:

const { pool, initDB } = require('./lib/db');
const { authMiddleware } = require('./lib/auth');

動作確認:

node server.js
curl http://localhost:PORT/api/health

フェーズ 2: ルートの切り出し中リスク・1ルート1PR

独立度が高い順(この順番で進める):

1. routes/stripe.js    99行  依存: pool のみ。最も安全
2. routes/atlas.js    151行  依存: なし外部API プロキシのみ)
3. routes/events.js   141行  依存: なし外部API フェッチのみ)
4. routes/together.js 455行  依存: pool + authMiddleware + isSsrfSafe
5. routes/feed.js     321行  依存: pool + authMiddlewarefeed job は起動後アタッチ)
6. routes/habit.js    185行  依存: pool + authMiddleware
7. routes/brain.js    258行  依存: pool + authMiddleware + fetch + genAI
8. routes/journal.js  266行  依存: pool + authMiddleware
9. routes/auth.js     585行  依存: webauthnChallenges + rateLimitフェーズ1完了後

各ルートファイルの形式:

'use strict';
const { Router } = require('express');
const { pool } = require('../lib/db');
const { authMiddleware } = require('../lib/auth');
// ...

const router = Router();
router.get('/xxx', authMiddleware, async (req, res) => { ... });
module.exports = router;

server.js でのマウント:

const stripeRouter = require('./routes/stripe');
app.use('/brain/api/stripe', stripeRouter);
app.use('/api/stripe', stripeRouter);

フェーズ 2 の注意点(ハマりやすい箇所)

  1. feed バックグラウンドジョブは routes/feed.js に移すが、startFeedJob() の呼び出しは server.js の起動後に行う必要がある。module.exports = { router, startFeedJob } の形式にする。

  2. Doorkeeper / connpass の二重定義router 外の app-levelは events.js に統合し、router 内に移動する。/brain/api/events/*/api/events/* の両マウントを忘れずに。

  3. auth routes の webauthnChallenges は auth.js ルートファイルと lib/rateLimit.jsまたはlib/auth.jsが共有するため、フェーズ1の lib 切り出し完了後でないと移動できない。必ずフェーズ1の後に行う。

  4. POST /save の setImmediate() バックグラウンド処理2026-04-10 追加): brain.js に移動する際、setImmediate() コールバック内の pool.query は routes/brain.js スコープで pool にアクセスできる必要がある。lib/db.js から require する形にすれば問題ない。


6. 2026-04-10 適用済みセキュリティ・信頼性修正

リファクタリング前に既に対処済みの問題。分割作業時はこの修正が維持されていることを確認すること。

# 修正内容 箇所 リスク(修正前)
1 DB pool max: 5 → 15 + idleTimeoutMillis: 30000 + connectionTimeoutMillis: 5000 追加 pool 初期化 setImmediate() 並列実行時に pool 枯渇
2 pool.on('error', ...) ハンドラ追加 pool 初期化直後 未捕捉エラーでプロセスクラッシュ
3 isSsrfSafe() 関数追加 共有ユーティリティ冒頭 ユーザー入力 URL を VPS から fetch → イントラネット攻撃
4 fetchMeta() に SSRF ガード + 2MB上限 fetchMeta 関数 内部 IP への GET・無制限レスポンスでメモリ枯渇
5 fetchFullTextViaJina() に SSRF ガード + 1MB上限 fetchFullTextViaJina 関数 Jina 経由の SSRF・巨大レスポンスでメモリ枯渇
6 POST /together/shareisSsrfSafe() 適用 Together routes Together 投稿 URL 経由の SSRF
7 archiveShare()isSsrfSafe() + 1MB上限 archiveShare 関数 アーカイブ処理での SSRF・サイズ無制限

isSsrfSafe() の実装lib/fetch.js に移動予定)

const SSRF_BLOCKED = /^(127\.|localhost$|::1$|0\.0\.0\.0$|169\.254\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|100\.100\.100\.100|metadata\.google\.internal)/i;

function isSsrfSafe(rawUrl) {
    let parsed;
    try { parsed = new URL(rawUrl); } catch { return false; }
    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false;
    if (SSRF_BLOCKED.test(parsed.hostname)) return false;
    return true;
}

7. POST /save の非同期処理パターン2026-04-10 実装)

記事保存の即時性改善のため、POST /save と GET /quick-save を以下のパターンに変更済み。 分割時にこのパターンを維持すること。

// 1. 即時 INSERTdomain を仮タイトルとして使用)
await pool.query(`INSERT INTO articles (...) VALUES (...)`, [userId, url, domain, null, null, ...]);
res.json({ ok: true, article: { id, title: domain, ... } });

// 2. バックグラウンドで fetchMeta → DB UPDATE → Jina → DB UPDATE → Gemini → DB UPDATE
setImmediate(async () => {
    try {
        const meta = await fetchMeta(url);
        await pool.query(`UPDATE articles SET title=$1, favicon=$2 WHERE id=$3`, [meta.title, meta.favicon, articleId]);
        const fullText = await fetchFullTextViaJina(url);
        await pool.query(`UPDATE articles SET full_text=$1 WHERE id=$2`, [fullText, articleId]);
        const summary = await analyzeWithGemini(fullText || meta.description || '');
        await pool.query(`UPDATE articles SET summary=$1 WHERE id=$2`, [summary, articleId]);
    } catch (e) {
        console.error('[save/background]', e.message);
    }
});

8. やらないこと(スコープ外)

項目 理由
TypeScript 化 全書き直しになる。現在の優先度では対費用効果が出ない
テストコード追加 分割と同時にやると範囲が広がりすぎる。分割後の別タスク
DB_USER: 'gitea' の修正 サーバー側 PostgreSQL ユーザー変更が伴う。別タスク
console.log 整理 分割と無関係。別タスク
インメモリ state の永続化Redis 等) webauthnChallenges / rateLimitStores をDB/Redisに移行。スキーマ変更が必要・mai 確認必須

9. 検討中の追加改善(分割後の次フェーズ候補)

  • DB_USER を 'gitea' から 'posimai' に変更PostgreSQL ユーザー作成が必要)
  • webauthnChallenges / rateLimitStores の Redis 移行(再起動耐性・スケール対応)
  • console.log を構造化ログpino 等)に統一
  • ルートごとのエラーハンドリング統一(現状は各ルートで個別 try/catch
  • isSsrfSafe() の DNS 解決後チェック追加DNS rebinding 対策。現状はホスト名の正規表現のみ)

10. 次ステップ(優先順)

商用化とリファクタリングは並列トラックで管理する。

トラック A: 商用化(最優先)

ステップ 担当 前提
どのアプリに premium 機能を実装するか決める mai 決定必要
特商法ページ記入(事業者名・住所・電話番号) mai 手動作業
Store デザイン確定A/B/C/D Eiji + mai
各アプリへの purchased フラグ実装 AI 対応可 mai の対象決定後
Stripe 本番モード切り替え AI + mai 確認 Store 確定・特商法完了後

トラック B: リファクタリング(タイミング待ち)

セクション 4 の「開始するべきタイミング」に該当するまで待つ。 現時点で最も近いトリガーは「server.js が 3500行を超えたとき」現在 約3130行

新機能追加posimai-station API 等が発生した場合は、そのタイミングでフェーズ1から着手する。


このドキュメントは作業開始時・完了時に更新すること。 分割を実施した AI は「フェーズ X 完了」「完了日」「担当 AI」を末尾に追記してください。


完了履歴

フェーズ 完了日 担当 AI 内容
セキュリティ修正フェーズ0相当 2026-04-10 Claude Sonnet 4.6 SSRF ガード・サイズ上限・pool 設定改善・即時保存パターン全7件