API エンドポイント(SSR on Cloudflare Workers)の規約

このディレクトリは Astro の On-demand rendering で Cloudflare Workers ランタイム上で実行される API エンドポイントを置く。サイト全体は静的生成(SSG)だが、ここだけ Worker で動的処理する。

必須事項

1. prerender = false を必ず付ける

export const prerender = false;

export const POST: APIRoute = async ({ request }) => { ... };

これを忘れるとビルド時に静的 HTML 化されて Worker で動かない。

2. 秘密情報は cloudflare:workersenv 経由で取得

import { env } from 'cloudflare:workers';

const apiKey = env.RESEND_API_KEY;

Astro.locals.runtime.env は Astro 6 / @astrojs/cloudflare v13 で廃止されたので使わない。

ローカル開発: .dev.vars(Wrangler が読む、.gitignore 対象) 本番: npx wrangler secret put <KEY>

3. レスポンスヘッダーを endpoint 内で明示

public/_headers静的アセットにしか効かないので、API レスポンスのヘッダーは Worker 内で設定する:

return new Response(JSON.stringify({ ok: true }), {
  status: 200,
  headers: {
    'Content-Type': 'application/json',
    'Cache-Control': 'no-store',
  },
});

4. エラー時は汎用メッセージ + サーバーログ

catch (error) {
  console.error('Internal error:', error);  // Worker ログのみ
  return new Response(
    JSON.stringify({ error: 'リクエストの処理中にエラーが発生しました' }),
    { status: 500, headers: { 'Content-Type': 'application/json' } }
  );
}

スタックトレースをクライアントに返さない。


お問い合わせフォーム (contact.ts) の防御層

5 段階で守っている。新しいフォーム系 API を追加するときも同じ層で考える:

  1. Origin ヘッダー検証 — CSRF 緩和
  2. Honeypot フィールド website — オフスクリーン + aria-hidden + tabindex="-1"。ボットが埋めたら拒否
  3. 入力バリデーション — 必須・文字数・メール形式(isValidEmail()
  4. Turnstile siteverify — Cloudflare Turnstile のサーバー側検証(公式必須)
  5. メールヘッダーインジェクション対策sanitizeHeader() で CR/LF を除去

メールヘッダーインジェクション

Resend で送信する subject / from / reply_to に CR/LF が混入すると追加ヘッダーを注入される。sanitizeHeader() で除去:

function sanitizeHeader(s: string): string {
  return s.replace(/[\r\n]/g, '');
}

reply_to に使う emailisValidEmail() の正規表現で改行を弾いている(多重防御)。


CSP の影響

public/_headers ではなく ../../../astro.config.mjssecurity.csp で生成される CSP が静的ページに適用される。新しい外部サービスを呼び出すフォームを追加するときは:

外部サービス連携時は API キーを漏らさないため可能な限り Worker 経由を選ぶ。