ComiCine
comicine.com で稼働する、完全サーバーレスの個人ウェブサイト兼管理システムです。公開ページ、人間検証ゲート、ビジターパネル、Apple Account マネージャー、監視、ステータスページは、すべて Cloudflare(Pages Functions、Workers、D1、R2)上で動作します。
機能
- 公開ページ:ホーム、サービス、お問い合わせ、ポートフォリオ — 4 言語で事前レンダリングされ、ダーク/ライトテーマの切り替えに対応
- 人間検証(human verification):Turnstile ゲートページは、どのホストでも要求された URL にその場でレンダリングされます(クロスドメインへの遷移なし)— ゲートの挙動を参照
- ビジターパネル(visitor panel)(
visitor.comicine.com):Pages Function + D1 によるアクセス解析。ハッシュ化された訪問者識別子(一覧系 API は生の IP を返しません)、国、取得できる場合は ISP/ASN、ステータスコードの分布を表示し、ログインで保護されています - 初回読み込み後は、訪問者ステータスのチップが新たな
/api/visitorsリクエストなしにローカルで絞り込まれます。API はディープリンク用に?status=<3-digit>も引き続き受け付けます。 - Apple Account マネージャー(
account.comicine.com/apple):Pages Function + D1 による CRUD で、ドラッグ&ドロップによる並べ替えに対応。パスワードは Fernet で暗号化して保存し、一覧にはマスクされたパスワードのみを表示、一時的な平文表示も可能 - 認証とロール:単一の
ComiCineAccess アプリがidentity.comicine.com/loginを保護します。identity ホストの Pages Function は JWT を検証し、登録済み送信元ホストに結び付いた短時間・1 回限りの D1 ハンドオフで本人情報を返します。送信元サイトの callback がローカル設定のロールを適用します。現在の両サイトは admin セッションを必要とします。 - 共有ログイン遷移:未認証のブラウザーが保護ページを開くと HTTP 401 HTML を返し、同一オリジンの外部スクリプトが
identity.comicine.com/loginへ遷移します。その後 Cloudflare Access がlimooo.cloudflareaccess.com経由でログインさせます。API の認証失敗は JSON 401 のままです。 - 監視(monitoring):
limooo-statusWorker が HTTP/D1 のターゲットを毎分プローブ(probe)し、プローブがダウンしている間は 10 秒ごとに再確認して、結果を D1 に保存します。アラートは webhook(フォールバックとして Email binding)で送信されます - ステータスページ(
status.comicine.com):同じ Worker が D1 からサーバーサイドレンダリングし、4 言語と明示的な?lang=上書きに対応、プローブのエラーコードは 4 言語の対応表を持ち、ブランド化された 404/503 の故障ページも表示します。日ごとの稼働率はprobe_uptime_dailyロールアップから取得します - 自動 IP ブロック:
- D1 の
blocked_ipsが権威データです。sync-workerが有効な行を Cloudflare IP List に反映し、エッジで遮断します(オリジンがないため ipset/iptables はありません) - フォールバックとしてアプリケーション層のグローバルフィルタ — ブロックされた IP は直接 403 になります
- D1 の
- 統一リダイレクトページ(
redirect.comicine.com/?to=<https-url>、/rも可):任意の HTTPS 宛先へリダイレクトする前に中間ページを表示します - ポートフォリオ画像の扱い:原本は一切公開されません。公開されるのはウォーターマーク入りのフル画像(
image.comicine.com/portfolio/*)とクリーンなサムネイルのみです
技術スタック
| レイヤー | 技術 | 実行環境 |
|---|---|---|
| 公開ページ、ゲート、ビジターパネル、Apple Account マネージャー、ステータスページ | Pages Functions(functions/)+ 事前レンダリングされた静的 HTML | Cloudflare エッジ |
| データ | D1(訪問者分析、ブロックリスト、Apple Account アカウント、認証セッション、プローブ/ハートビート) | Cloudflare |
| 人間検証 | Cloudflare Turnstile、ゲートページをその場でレンダリング | Cloudflare + ブラウザ |
| 認証 | Cloudflare Access(Zero Trust JWT、Pages Function が自ら検証) | Cloudflare |
| 監視とデータ保持 | ops/status-worker(Cron Triggers + Durable Object alarms) | Cloudflare Workers |
| D1 アーカイブ | ops/d1-archive(毎日の cron) | Cloudflare Workers |
| ブロックリスト同期 | ops/sync-worker(日次 cron → Cloudflare IP List) | Cloudflare Workers |
| 画像ウォーターマーク | ops/image-watermark(パスを正規化するプロキシ) | Cloudflare Workers |
| 静的アセット/原本バックアップ | Pages アセットサーバー + プライベート R2 バケット limooo-originals | Cloudflare |
| デプロイ | ops/deploy.sh(git + ops/pages_deploy.sh + ops/workers_deploy.sh) | ローカル |
プロジェクト構成
├── src/
│ ├── config.py # unified config: paths, languages, domains, DB/IP utils (consumes config-contract.json)
│ ├── auto_block.py # manual blocklist.txt → D1 reconciliation (D1 is the authority)
│ ├── cidr.py # CIDR parsing helpers
│ ├── portfolio.py # portfolio image / thumbnail helpers
│ ├── render_app.py # build-time read-only renderer used by src/build.py
│ ├── services_pricing.py # CSV-driven price list for services.comicine.com
│ ├── build.py # Pages static build (python3 src/build.py)
│ ├── static/ # static css/js/fonts + icons/portfolio/QR codes; files/ maps to /public/
│ └── templates/ # Jinja2 page templates
├── README.md # this file
├── LICENSE.txt # GNU AGPL v3 公式英文本文
├── ADDITIONAL_TERMS.txt # Section 7 の追加条項
├── ADDITIONAL_TERMS_zh_CN.txt # 簡体字中国語訳
├── ADDITIONAL_TERMS_ja_JP.txt # 日本語訳
├── ADDITIONAL_TERMS_ko_KR.txt # 韓国語訳
├── COPYRIGHT.md # 著作権およびライセンス(英語)
├── COPYRIGHT_zh_CN.md # 簡体字中国語訳
├── COPYRIGHT_ja_JP.md # 日本語訳
├── COPYRIGHT_ko_KR.md # 韓国語訳
├── THIRD_PARTY_NOTICES.md # 第三者表示(英語)
├── THIRD_PARTY_NOTICES_zh_CN.md # 簡体字中国語訳
├── THIRD_PARTY_NOTICES_ja_JP.md # 日本語訳
├── THIRD_PARTY_NOTICES_ko_KR.md # 韓国語訳
├── LICENSE_zh_CN.txt # AGPL-3.0 (Simplified Chinese)
├── LICENSE_ja_JP.txt # AGPL-3.0 (Japanese)
├── LICENSE_ko_KR.txt # AGPL-3.0 (Korean)
├── config-contract.json # single source of domains / languages / cookies / TTLs
├── package.json # npm scripts: build / test / typecheck
├── package-lock.json # locked Node dependency tree (npm ci)
├── requirements.lock # pinned Python dependencies (read by ops/build.sh)
├── wrangler.toml # Cloudflare Pages project + D1 binding
├── tsconfig.json # TypeScript config for functions/ and the Workers in ops/
├── vitest.config.ts # vitest config (functions/**/*.test.ts)
├── pytest.ini # pytest config (tests/)
├── pyproject.toml # ruff / mypy config
├── .gitignore # ignore rules shipped with the repository
├── .github/ # GitHub Actions workflows
│ └── workflows/
│ └── tests.yml # CI: build, then typecheck / migrations / vitest / pytest
├── .githooks/ # git hooks (installed by ops/install_git_hooks.sh)
│ └── pre-push # runs ops/ci_check.sh on the commit being pushed
├── data/ # runtime databases and caches (generated; git-ignored)
├── secrets/ # secrets & certificates, git-ignored
│ ├── webauthn.env # env file read by the deploy scripts
│ └── apple_account_encryption.key # Apple Account password encryption key
├── ops/ # deployment & ops tooling
│ ├── deploy.sh # the single deploy entry point (commit/push + Pages + docs + Worker)
│ ├── build.sh # Pages build: venv, contract checks, public/manifest.json
│ ├── pages_deploy.sh # Cloudflare Pages build + Wrangler deploy
│ ├── docs_deploy.sh # docs.comicine.com build + deploy (VitePress → Pages project comicine-docs)
│ ├── docs_headers.py # generates the docs site `_headers` (CSP hashes + cache)
│ ├── ci_check.sh # local replica of .github/workflows/tests.yml
│ ├── run-tests.sh # test runner used locally
│ ├── install_git_hooks.sh # installs the pre-push hook
│ ├── readme_facts.py # re-reads the facts on this page (--check / --live)
│ ├── migrate_d1.sh # D1 migrations (--dry-run / --remote)
│ ├── workers_deploy.sh # standalone Worker deploys
│ ├── export_d1.py # unified D1 import SQL/JSON export (apple-account | blocklist)
│ ├── prune_d1.py # manual D1 retention and aggregation (scheduled retention runs in status-worker)
│ ├── d1_client.py # Cloudflare API client for D1
│ ├── upload_originals.sh # private R2 backup of portfolio originals
│ ├── security-headers.json # single source of the response-header baseline
│ ├── tailwind.config.js # Tailwind config for the prebuilt CSS
│ ├── check_config_contract.py / check_gate_trust.py / check_security_headers.py / check_ip_rays.py / check_ray_id.py / check_visitor_id.py
│ ├── migrations/ # D1 schema migrations
│ ├── waf/ # WAF rule snapshots
│ ├── cloudflare/ # Cloudflare resource inventory (declarative)
│ ├── email-templates/ # transactional email framework + i18n copy
│ ├── fonts/ # gate-diagnostic font subset
│ ├── status-worker/ # Worker: probes, status page, alerting, D1 retention
│ ├── image-watermark/ # Worker: image.comicine.com watermark normalizer
│ ├── d1-archive/ # Worker: D1 snapshot/archive (cron 00:00)
│ ├── sync-worker/ # Worker: D1 blocked_ips → Cloudflare IP List (cron 03:30)
│ ├── requirements.txt # Python dependencies
├── functions/ # Cloudflare Pages Functions
│ ├── _middleware.ts # gate/redirect/blocklist/visitors/ray orchestration (test: _middleware.test.ts)
│ ├── _lib/ # config, d1, cidr, gate, access, session, fernet, routing
│ ├── _data/ # generated i18n/runtime modules (do not hand-edit)
│ ├── api/ # apple-account, auth, i18n, ray, visitors endpoints
│ ├── types.d.ts # shared Env / PagesFunction types
│ └── login.ts / logout.ts # Access login callback and logout (tests: *.test.ts)
├── tests/ # pytest suites + fixtures
├── locales/ # i18n / translation catalogs
├── public/ # Pages build output (git keeps only .gitkeep)
├── preview/ # local preview (build-generated; git keeps only .gitkeep)
└── docs/ # docs.comicine.com VitePress root + services.comicine.com price-list CSVsクイックスタート
# package-lock.json で固定された Node 依存関係をインストールします
npm ci
# 静的サイトをビルドします(Python 仮想環境 + コントラクト検証 + public/ + preview/)
npm run buildnpm run build は ops/build.sh です。リポジトリ外に /tmp/comicine-build-venv を作成し、requirements.lock の固定依存関係をインストールして、 レンダリング前に実行環境間の設定を検証します。作品の透かしは Pillow と src/static/icons/watermark.png で生成するため、システムの Cairo ライブラリは不要です。 レンダラーを直接実行する場合は、固定された Python 依存関係をインストールしてから COMICINE_BUILD=1 python3 src/build.py を実行してください。
生成されたサイトをローカルでプレビューするには、ビルドして preview/ の出力を開きます。
ビルド、テスト、デプロイ
クリーンなローカルビルドには、デプロイスクリプトと同じ依存関係セットを使用します:
cd site
npm ci
npm run buildsrc/build.py は public/<lang>/*.html、public/static/、 functions/_data/i18n.ts、functions/_data/runtime.ts、preview/ を再生成します。 これらの出力を手で編集しないでください。locales/*.json、テンプレート、静的ソースを変更して再ビルドします。 src/static/tailwind.css はリポジトリにコミット済みの Tailwind ビルド済み出力です。 npm run build は ops/build.sh を使用し、これが /tmp/comicine-build-venv を作成して public/manifest.json(ビルド成果物のハッシュ証跡)を生成します。
Pages の出力だけをデプロイするには:
# build + validate only, no Cloudflare writes
bash ops/pages_deploy.sh --build-only
# build + validate + deploy Pages + smoke test (/_health must be 200)
bash ops/pages_deploy.sh
# full deploy: (1) commit (2) push (3) deploy Pages + docs
bash ops/deploy.sh --allデプロイ入口:ops/deploy.sh が唯一のデプロイ入口です — commit + push + Pages + docs を実行し、--worker=<name> で単一の Worker も配布できます。 ops/pages_deploy.sh がビルド + Pages を担当します。 --dry-run はどのリモートにも書き込まずに計画を出力します。
既定では deploy.sh は静か(quiet)です。1 ステップにつきステータス 1 行を出力し、そのステップの完全なログは 失敗したときにだけ吐き出されます。--full を付けるとすべてをストリーム出力します(ビルドマニフェスト、 成果物の数、wrangler のアップロード進捗)。どちらでもフラグとステップは同一で、 --full は冗長さだけを変えます。
認証情報はローカルの secrets/webauthn.env から読み込みます(コミットされず、 エコーもされません)。ssh / rsync / リモート systemd のステップはありません。 ops/migrate_d1.sh と ops/workers_deploy.sh も --dry-run に対応しています。
自動テストの入口を用意しています:
# Python テスト
python3 -m pytest
# Pages Functions, Workers and image-watermark tests
npm test環境変数
デプロイスクリプトが secrets/webauthn.env から読み込み、Git にはコミットされません。Pages ランタイムは独自のシークレット群を持ちます(後述のエッジのセクションを参照)。
| 変数 | 説明 |
|---|---|
GATE_HMAC_KEY | __gate cookie 用の HMAC-SHA256 鍵。エッジで発行・検証します |
CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID | ops/d1_client.py、ops/pages_deploy.sh、ops/workers_deploy.sh が使う Cloudflare API アクセス |
鍵素材はリポジトリの外に置きます。apple_account_encryption.key は Apple Account のパスワードと 暗号化された訪問者 IP カラムに使う Fernet 鍵です。
Cron ジョブ
スケジューリングはすべて Cloudflare 上にあり、各 Worker の wrangler.toml で 宣言された Worker Cron Triggers として動作します:
| スケジュール | Worker | ジョブ |
|---|---|---|
* * * * * | ops/status-worker | 毎分の HTTP/D1 プローブ。プローブがダウンしている間は Durable Object のアラームが 10 秒ごとに再確認し、状態変化時にアラートします |
47 3 * * * | ops/status-worker | D1 のデータ保持(src/retention.ts):ray_log_v2(7 日)、visitors_v2(30 日)、visitor_rollups(30 日)、heartbeats(30 日)、events(90 日)、probe_uptime_daily(90 日)、auth_sessions(60 日)、identity_handoffs(1 日)を削除します |
30 3 * * * | ops/sync-worker | 有効な D1 blocked_ips の行を Cloudflare IP List limooo_blocklist に反映します |
0 0 * * * | ops/d1-archive | D1 のスナップショット/アーカイブ |
python3 ops/readme_facts.py --check は上記のスケジュール、保持期間、TTL、ディレクトリツリーを リポジトリから読み直し、このページと食い違えば失敗します。--live はデプロイ済みの状態 (D1 の件数、Pages のホストとシークレット、DNS、WAF、Worker のスケジュール)を Cloudflare から読みます。
TLS 証明書は Cloudflare が Pages のカスタムドメイン向けに発行・更新します。 ops/migrate_d1.sh と ops/workers_deploy.sh は手動実行用で、 --dry-run に対応したメンテナンスの入口です。
デプロイ
デプロイ対象は Cloudflare Pages プロジェクト comicine(デフォルトエイリアス limooo.pages.dev)と独立した Workers で、すべて Cloudflare API 経由です。 リポジトリのルートから:
cd site
bash ops/deploy.sh # with no arguments = --all (full deploy, see below)
bash ops/deploy.sh --all # commit + push + deploy Pages + docs
bash ops/deploy.sh --pages # deploy the main Pages project only
bash ops/deploy.sh --docs # deploy docs.comicine.com only
bash ops/deploy.sh --worker=status-worker # deploy one standalone Worker
bash ops/deploy.sh --all --full # same, but stream every step's outputops/deploy.sh を引数なしで実行すると、それは正確に --all です:commit + push + Pages + docs、すなわち「完全デプロイ(full deploy)」の契約です。すでにローカルでコミット済みのものだけを配信するには、 --pages / --docs を明示的に指定します。
出力は既定で静かです(1 ステップにつきステータス 1 行)。--full を付けると全工程を確認できます。 --dry-run は上記のフラグのどの組み合わせでも動作します。
認証情報はローカルの secrets/webauthn.env から読み込みます。すべてのステップは Cloudflare API か Git 経由で実行されます。
ドキュメントサイト(docs.comicine.com)
site/docs/ はサブドメインごとのコンテナです:docs/ には docs.comicine.com の VitePress ルート(1 ページ 1 言語につき 1 つの markdown ファイル)が入り、services/ には services.comicine.com の価格表 CSV が入ります。言語コードは URL の最後のセグメントで、 内容ページは常に明示的に付けます(/README/zh-cn、/README/en-us)。サフィックスなしの /README は /README/zh-cn へ 302 で転送されます。ドキュメントサイトのルート / は https://comicine.com へ 302 で転送されます。
.vitepress/rewrites.json(コンテンツの隣)がソースをサフィックス付きルートにマッピングし、 .vitepress/config.mts が additionalConfig を通じて各ページに固有の lang / themeConfig を与えます。 ページを追加するには、各言語ディレクトリに markdown ファイルを 1 つ置き、 rewrites のエントリと public/_redirects のサフィックスなしパス用 302 を 1 行足します — markdown ファイルに HTML が出力されないと ops/docs_check_output.py がビルドを失敗させます。
サイトはフォーク ComiCine/vitepress の VitePress でビルドします。ヘッダーとフッターはフォーク側 (VPComiCineNav.vue / VPComiCineFooter.vue)にあり、メインサイトの base.html / _footer.html を忠実に再現しています — 編集はフォークで行えば次のデプロイが反映します(フォークはコミットが変わったときだけ再ビルドされます。 フォークの作業ツリーからビルドするには COMICINE_VITEPRESS_FETCH=0 を使います)。 サイトは独立した Cloudflare Pages プロジェクト(comicine-docs)なので メインの limooo 成果物には一切触れず、 user_lang_preference / comicine_theme cookie をメインサイトと共有します。
bash ops/docs_deploy.sh --build-only # build + validate only
bash ops/docs_deploy.sh --dev # local VitePress dev serverセキュリティ設計
- セッション cookie は
Secure+HttpOnly+SameSite=Laxを使用し、現在のホストのみに限定されます。これによりセッション cookie を他のサイトで再利用できません。 すべてのセッションは D1auth_sessionsに記録されたランダムなsidを持ち、requireAuthは 失効・期限切れのセッションを拒否します。実行時に HMAC 鍵がない場合やauth_sessionsテーブルが利用できない場合は、 503 でフェイルクローズ(fail closed)します。 - ゲートも同様にフェイルクローズします。
TURNSTILE_SECRET、GATE_HMAC_KEY、SESSION_HMAC_KEYのいずれかが空の場合、エッジはページをレンダリングしたり署名のない cookie を発行したりせず、503 を返します。 __gatecookie は<unix-expiry>.<HMAC-SHA256 hex>です(TTL 1 時間、HttpOnly、Domain=.comicine.com)。発行と検証はすべてエッジで行います。- 未認証で保護ページを開くと、送信元サイトは短時間有効な state cookie を設定した HTTP 401 HTML を返します。同一オリジンの外部スクリプトが
identity.comicine.com/loginへ遷移し、Cloudflare Access がlimooo.cloudflareaccess.com経由でログインさせます。API の認証失敗は JSON 401 のままです。identity ホストの Pages Function はCf-Access-Jwt-Assertion(RS256、JWKS は 1 時間キャッシュ)を検証し、送信元ホストと state に結び付いたハッシュ化済みの一回限りの D1 コードを作成します。送信元サイトの callback がサーバー間でコードを消費し、ローカル設定のロールを適用します。handoff にロールは含まれません。現在の visitor と account の両サイトでは API が admin セッションを要求します。 - 鍵と暗号文は別々に保管し、リポジトリには決してコミットしません
- ブロックの層:アプリ層の D1 ブロックリストがエッジで 403 を返し、
ops/sync-workerが 有効な行を Cloudflare IP Listlimooo_blocklistに追随させます(WAF ルールが使う想定ですが、 そのカスタムルールは現在デプロイされていません) - 管理操作(作成/更新/削除)には admin ロールが必要で、viewer は読み取り専用です
- 訪問者の IP を一覧系 API が返すことはありません。完全な IP は
visitor_rollups.ip_encに Fernet で暗号化して保存し、admin 向けに 1 行ずつ復号します
ホワイトリスト
信頼するソースは docs/lists/own-ips.txt で手動管理し、デプロイ時に生成ファイル docs/lists/whitelist.txt へ反映します。
| エントリ | 効果 |
|---|---|
ASN/<number> | 任意の Cloudflare ゾーン単位 ASN Access Rule エントリです。ゾーンルールを変更するには bash ops/waf_whitelist.sh --apply を手動実行します。ASN エントリは Pages Functions のゲートを迂回しません。 |
IP-CIDR/<ip>/<mask> | Pages エッジで完全に許可されるソースです(例:IP-CIDR/97.64.18.11/32)。IPv4 は /24–/32、IPv6 は /64–/128 のプレフィックスのみ許可され、ブロックリストとチャレンジゲートの両方をスキップします。 |
現在の生成リストには ASN が 0 件、IP-CIDR が 2 件あります。通常のデプロイはローカルのホワイトリストを生成しますが、ゾーン単位の ASN Access Rules は変更しません。own-ips.txt の編集後、デプロイ時に ops/sync_whitelist.py を実行し、その後ビルドで ops/check_gate_trust.py --emit がエッジ用ファイルを生成します。
唯一の情報源
- ユーザー向けの文字列:
locales/*.json。functions/_data/*と API の i18n ルートはそこから生成されます。 - 共有ランタイム定数:
config-contract.jsonがランタイム横断で合意された契約です。src/config.pyと生成物functions/_lib/config.tsの双方がこれを消費し、ops/check_config_contract.pyが一致を強制します。 - ゲート/リダイレクトの文言:
locales/*.jsonをfunctions/_data/runtime.ts経由で。src/build.pyがそれを組み立てます。 - D1 のスキーマとマイグレーション:
ops/migrations/*.sql。ブロックの権威はblocked_ipsのみです。 - セキュリティレスポンスヘッダーの基準(有効時):
ops/security-headers.json。 - デプロイとランタイムの境界:
ops/deploy.sh。ランタイムは Cloudflare Pages と Workers のみです。
Cloudflare Pages ランタイム
公開サイト(ホーム/サービス/お問い合わせ、ゲート、ビジターパネル、Apple Account マネージャー、images.comicine.com 静的アセットホスト、リダイレクト中継)は Cloudflare Pages Functions 上で動作し、ステータスページとすべてのスケジュールジョブも同様です。主サイトの Pages プロジェクト名は comicine ですが、現在のデフォルトエイリアスは limooo.pages.dev のままです。ドキュメントプロジェクト comicine-docs も limooo-docs.pages.dev のままで、エイリアス移行は未完了です。
ランタイムの分担:
| レイヤー | 技術 |
|---|---|
| エッジ/人間検証 | Pages Functions(functions/_middleware.ts) |
| Pages | ビルド時に事前レンダリングされた静的 HTML(多言語) |
| データ | D1(訪問者分析、ブロックリスト、Apple Account 管理、認証セッション) |
| 人間検証 | Cloudflare Turnstile、ゲートページをその場でレンダリング |
| 独立した Workers | status.comicine.com(limooo-status:プローブ、ステータスページ、データ保持)、image.comicine.com(image-watermark)、limooo-blocklist-sync |
ビルドとディレクトリ構成
python3 src/build.py:4 言語のページをpublic/に事前レンダリングし、locales/*.jsonをfunctions/api/i18n/[lang].tsとしてインライン化し、共有のfunctions/_data/runtime.ts(ゲート/リダイレクトの i18n + プリロードアセット)を生成しますops/migrations/001_init.sql:D1 の初期スキーマ(apple_accounts/blocked_ips/visitors)ops/export_d1.py:D1 取り込み用 SQL を生成(出力はops/out/、git 管理外)ops/migrations/007_visitor_status_indexes.sql:訪問者ステータスの絞り込み用に(status, ts)と(status, ip_hash, ts)インデックスを追加しますops/sync-worker/:毎日 03:30 の Worker cron が有効な D1blocked_ipsの行を Cloudflare IP List に同期します。auto_block.py cfは明示的なメンテナンス専用です
環境変数
Pages プロジェクト設定 → 環境変数 → Encrypt(Secret)で設定し、リポジトリにはコミットしません:
| 変数 | 目的 |
|---|---|
TURNSTILE_SITEKEY | ゲートページの Turnstile ウィジェットの公開 sitekey |
TURNSTILE_SECRET | サーバー側 siteverify のシークレット |
GATE_HMAC_KEY | __gate cookie 用の HMAC-SHA256 署名鍵(openssl rand -hex 32) |
ACCESS_TEAM_DOMAIN | Cloudflare Access のチームドメイン。検証に使う JWT の iss でもあります |
ACCESS_IDENTITY_AUD | 単一 ComiCine Access アプリの AUD。ロールは config-contract.json の送信元ホストマップで決まります |
VISITOR_IP_KEY | 暗号化された訪問者 IP 完全版カラム(visitor_rollups.ip_enc)用の Fernet 鍵 |
OBSERVABILITY_HMAC_KEY | プライバシー最小化された訪問者 IP ハッシュの HMAC 鍵(functions/_lib/tracking.ts)。未設定だとゲートがフェイルクローズします |
SESSION_HMAC_KEY | Pages のセッション cookie 署名鍵(GATE_HMAC_KEY とは別) |
APPLE_ACCOUNT_ENCRYPTION_KEY | Fernet 鍵(secrets/apple_account_encryption.key から) |
ローカル開発:.dev.vars.example を .dev.vars にコピーして実際の値を記入します(git 管理外)。ops/sync-worker 用の CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID は wrangler secret put で設定します。
ゲートの挙動
すべてのリクエストで署名済み __gate cookie を確認します。Cloudflare の botManagement.verifiedBot は検証済み検索エンジンの信頼シグナルとして受理されますが、任意の Googlebot/GPTBot の User-Agent 文字列やクライアントが送ってきた cf_clearance cookie がゲートを迂回することはありません。エッジコードが完全なバイパスを認めるのは、生成されたホワイトリスト(docs/lists/whitelist.txt → functions/_data/gateTrust.ts)にある IP-CIDR だけです。
未検証のリクエストには Turnstile ゲートページをその場で返します。ホストもパスも変わりません。ミドルウェアは要求された URL に public/<lang>/auth.html をステータス 403 でレンダリングし、POST /__gate/verify が同じオリジンで Set-Cookie: __gate=…(1 時間、Domain=.comicine.com)を返し、その後ページが元のターゲットを再読み込みします。auth.comicine.com はゲートのホストです — 自身のルートで同じページを配信し、/__gate/config|diag|verify エンドポイントを持ちます。
ゲートをレンダリングするすべてのホストは同じエッジコードで配信されるため、同期させる 2 つ目のゲート実装はありません。
ミドルウェアは正規化された D1 ブロックリストも適用し、プライバシーを最小化した訪問者分析を記録します。ゲートページは no-store/noindex で、ダーク/ライトテーマの切り替えに対応します。Turnstile ウィジェットのホスト名許可リストには、実際に利用するサイトを含める必要があります。現在 .com のゲートでは検証に失敗しています。設定 API と Turnstile スクリプトは読み込まれますが、/gate/verify に token が届く前にチャレンジが失敗します。auth.js は手動再試行ボタンを表示し、ブラウザーコンソールに Turnstile の数値エラーコードを記録します。設定済みの Cloudflare API 資格情報では widget 更新が HTTP 403 / error 10000 となるため、許可ホスト名はまだ確認・修正できていません。ゲートを無効にせず、クライアントエラーコードを確認し、Turnstile Sites Write 権限のある資格情報でホスト名許可を確認してください。
ページ配信(クリーン URL、言語パスプレフィックスなし)
src/build.py が public/<lang>/ を 4 言語で事前レンダリングします。ゲート通過後、ミドルウェアは cookie > Accept-Language > CF region > en-US の順で言語を選び、env.ASSETS.fetch() で該当ページを取得して URL をクリーンに保ちます:
comicine.com/→ ホームページ、comicine.com/services/comicine.com/contact→ それぞれ対応するページservices.comicine.com/→ サービスページ、contact.comicine.com/→ お問い合わせページ(サブドメインはメインサイトへの 301 なしで直接コンテンツを配信します)visitor.comicine.com/→ ビジターパネル(要ログイン)、account.comicine.com/apple→ Apple Account マネージャー(要ログイン)images.comicine.com/→comicine.com/へ 301、その他のパスはポートフォリオ画像、favicon、ロゴ、QR などの静的アセットを配信します。image.comicine.com/portfolio/<img>→ 正規化 Worker によるウォーターマーク版(そのルートもcomicine.com/へ 301)www.comicine.com→ メインサイトへ 301- ルート直下の
x.htmlは、xが公開ホームページを持つ稼働中のサブドメイン(services、contact、docs、status)ならx.comicine.com/へ 302 で転送し、不明な名前なら現在のサイトのホームへ 302 で転送します。index.htmlは現在のホストの正規ホームへ転送します。 - ナビのリンクは絶対サブドメイン URL(
https://services.comicine.comなど)を維持します。言語切り替えは純粋にフロントエンドのapplyLang()で、リロードも URL 変更もありません
services.comicine.com の価格表(CSV 駆動)
サービスの価格表はマークアップではなくデータです:src/build.py はレンダリングのたびに docs/services/*.csv を読み(src/services_pricing.py)、src/templates/services.html の プラングリッドを埋めます。価格を変更するには CSV を編集してデプロイするだけです — テンプレートの編集は不要です。
| ファイル | 列 | 埋める内容 |
|---|---|---|
docs/services/convention.csv | shots,price[,bookable] | 01 Convention、撮影枚数のティアごとにカード 1 枚 |
docs/services/outdoor.csv | type,people,price[,bookable] | 02 Outdoor、スタジオ/屋外 × 1 人/2 人 |
CSV から取得するのは数値、ティアの集合、受付可否だけです。ラベル、 単位の接尾辞、注記ブロックは引き続き locales/*.json から取得します (plan_studio_solo、unit_per_shot など)。
bookable(任意の列) は、取り消し線付きの価格スタイルと 注記ブロックのスタジオ行の両方を制御します:
- ティアごと:
no→ そのティアの価格はclass="plan-price strikethrough"でレンダリングされます。yes、空のセル、または列自体が 存在しない → 通常の価格 - 注記ブロック:すべての
studioティアがnoなら、スタジオ行は一時停止の文言 (studio_paused)を表示します。予約可能なスタジオティアが 1 つでもあれば、代わりにstudio_bookableを表示します
つまり「一時的に受付停止」は端から端までデータです:outdoor.csv で studio を no から yes に変えれば、 次のデプロイで取り消し線と一時停止の注記の両方が消え、 テンプレートもロケールも編集する必要はありません。両者が食い違うことは決してありません。 それ以外の値(例:maybe)は黙って推測せず、ビルドを失敗させます。
price が正の整数でない場合は - としてレンダリングされます。 プレースホルダーの 許可リストはありません。空欄、N/A、TBD、打ち間違い(1OO)、0、負の数は いずれも「価格未公開」を意味します。カードは CNY プレフィックスと単位の接尾辞を 保ったまま数値だけが - になります(CNY - / 枚)。レイアウトは数値のティアと まったく同じです。- は 0 でも「無料」でもありません。1 つのファイルにこのような セルと数値の行を混在させても問題ありません。
契約の残りの部分は次のとおりです:
- convention の行は CSV の撮影枚数の昇順で並びます。ティアの数は 固定ではありません(12 枚の行を追加してもコード変更は不要)
- convention のティアに単位の接尾辞が付くのは
CONVENTION_UNIT_KEYSに登録されている場合だけです。未登録のティアは価格のみをレンダリングします - outdoor の行は 4 つの
type/peopleの組み合わせをすべて網羅する必要があります。行の順序に関係なく 固定の順序(スタジオ 1 人/2 人、次に屋外 1 人/2 人)でレンダリングされます
ファイルの欠落、列の誤り、ティアの重複、未知のティア、認識できない bookable の値は ビルドを失敗させます — 誤った価格表はビルドの失敗よりも悪いからです。価格セルだけは ビルドを失敗させません。正の整数として解釈できない内容はすべて - として レンダリングされます。 tests/test_services_pricing.py はこれらのすべてのケースと、コミットされた CSV に対するラウンドトリップ 検査をカバーします。
docs/services/ は VitePress のビルドから除外されるため(docs/.vitepress/config.mts の srcExclude)、CSV はデータソースのままで、 docs.comicine.com に公開されることはありません。
パフォーマンス/エッジキャッシュ
public/_routes.jsonは/static/*とルートの静的アセットを Pages Functions から除外するため、CSS/JS/フォントは Pages アセットサーバーが直接配信します。src/static/files/内のファイルはビルド出力の/public/files/に元の階層を保ってミラーされ、src/static/files/<path>はhttps://comicine.com/public/files/<path>で配信されます。/public/files/*は Pages Functions を迂回し、静的アセットと同じ1日キャッシュを使用し、存在しないパスは404を返します。public/_headersはバージョン付きの静的アセットにstale-while-revalidate付きの長いブラウザキャッシュを与えます。public/_routes.jsonと_headersは どちらもsrc/build.pyが生成します。- 事前レンダリングされた公開 HTML はプロセス内ページキャッシュで再利用されます (キーは host + 言語)。レスポンスは
public, max-age=300, stale-while-revalidate=3600とVary: Accept-Languageを通知します。ここでエッジキャッシュは主張しません: Cloudflare は Pages Functions のレスポンスをキャッシュしないため (同じ URL への連続リクエストでもcf-cache-statusはDYNAMICのまま)、 共有キャッシュ向けの寿命ディレクティブは死んだ設定になります。言語 cookie もVaryに入れません (ブラウザキャッシュが細分化されるだけだからです)。 - ファーストパーティのポートフォリオサムネイルと favicon は、 ウォーターマーク Worker ではなく
images.comicine.com/static/...(静的エッジキャッシュ、Functions を迂回) を使用します。QR コードと外部から直リンクされた画像は 引き続きimage.comicine.comを使用します。 - ポートフォリオの原本は一切公開されません:
/static/portfolio/<img>は 404 を返します。 公開されるのはクリーンなサムネイル(images.comicine.com/static/portfolio/thumbs/<img>-<width>.{webp,avif})と、image.comicine.comの正規化・ウォーターマーク済み/portfolio/<img>だけです。 原本は git 管理外のsrc/static/portfolio/とプライベート R2 バケットlimooo-originals(ops/upload_originals.sh)に残ります。 - Turnstile の検証にはサーバー側で 3 秒のタイムアウトがあるため、 Cloudflare のチャレンジ基盤の障害時はユーザーを最大 8 秒待たせず、 素早くフェイルクローズします。
本番の状態
デプロイ済みの構成です。python3 ops/readme_facts.py --live でいつでも読み直せます:
- Pages:主プロジェクト
comicineは現在もlimooo.pages.dev経由で配信します。ドキュメントプロジェクトcomicine-docsのデフォルトエイリアスはlimooo-docs.pages.devのままです。主プロジェクトはcomicine.com、www、services、contact、auth、visitor、account、identity、images、image、redirectを配信し、ドキュメントホストはdocs.comicine.com、fonts.comicine.comは R2 が支えます - D1:データベース
limooo(APAC)がプロジェクトにDBとしてバインドされ、schema_versionが適用済みマイグレーションを記録します。022 は適用済みです。021 は既存の Apple Account 識別子を書き換えるため、設計どおり保留中です。apple_accountsが Apple Account データを保存し、blocked_ipsがブロックの権威です。論理削除行は監査用に保持します - シークレットは Pages → Settings → Environment variables → Encrypt にあります:Turnstile の 2 つ、
GATE_HMAC_KEY、SESSION_HMAC_KEY、OBSERVABILITY_HMAC_KEY、VISITOR_IP_KEY、APPLE_ACCOUNT_ENCRYPTION_KEY、ACCESS_TEAM_DOMAIN、ACCESS_IDENTITY_AUD。旧ACCESS_ADMIN_AUDSとACCESS_VIEWER_AUDSは設定に残っていますが、ランタイムでは読みません。APPLEID_ENCRYPTION_KEYと 2 つのAUTHENTIK_*も同様に未使用です - Access:
ComiCineがidentity.comicine.com/loginのみを保護します。visitor.comicine.comとaccount.comicine.comは共有 identity handoff の後、それぞれの Pages ホスト専用セッションを使います。Dashboard には未使用のComiCine-Adminアプリが残っていますが、admin.limooo.cnには DNS レコードも Pages カスタムドメインもありません。自前の IdP も独自のログインフォームもありません - Worker は独立して動作します:
limooo-status、limooo-blocklist-sync、limooo-d1-archive、image-watermark。status.comicine.comとsink.comicine.comは Worker のカスタムドメインです - Cloudflare のルール:カスタムファイアウォールのフェーズは空です(
limooo_blocklistIP List はルールを待っている状態)。キャッシュのフェーズでは静的アセット、旧画像パス、ウォーターマーク Worker の応答を 1 年間キャッシュし、2 つの HTML ページキャッシュルールは無効です。動的リダイレクトのフェーズは空で、ops/waf/rules.snapshot.jsonはその空のフェーズを更新時点で写したものです(削除されたルールは git の履歴にのみ残ります)limooo.cn/feishu/<path>は 301 でcomicine.com/feishu/<path>に進み、その後 Pages がhttps://comicine.feishu.cn/<path>へ転送します。 - スモークチェック:
https://comicine.com/_health→ 200、https://comicine.com/?challenge=1→ 403、https://docs.comicine.com/→https://comicine.comへ 302、https://status.comicine.com/→ 200
注意:?challenge=1 の 403 はゲートページが表示されたことだけを確認し、Turnstile の検証成功を示しません。ブラウザーで実際にチャレンジを完了し、/gate/verify が成功することを確認してください。.com のチャレンジはまだ復旧していません。
ゲートはどのホストでもその場でレンダリングされ、ゲートページとログに実際の訪問者 IP が表示され、Access が唯一のアイデンティティ源であり続けます。
ライセンス
ComiCine が著作権を有し、ライセンスする権限を有するオリジナル資料は、GNU Affero General Public License 第 3 版またはそれ以降のバージョン(AGPL-3.0-or-later)に基づいて提供されます。GNU AGPL v3 第 7 条に基づく追加条項は ADDITIONAL_TERMS_ja_JP.txt を、GNU AGPL v3 の本文は LICENSE.txt を参照してください。追加条項は GNU AGPL v3 を選択する場合に適用され、後続バージョンでの適用はそのバージョンの条件によります。
ComiCine は、本プロジェクトで使用する開発ブランドおよび公開上の表示です。著作権は、法的に該当する権利を有する実際の権利者に帰属します。再配布時には、適用される作者表示および著作権表示(合理的な出典表示を含む)を保持してください。適用される AGPL バージョンの条件に従う限り、商用利用が認められます。
追加の許諾または商標許可に関するお問い合わせ:[email protected](旧メールボックスは引き続き使用します)。適用される GNU AGPL バージョンに従う商用利用に追加の許諾申請は必要ありません。
第三者フォント、依存関係、および QR コード画像内のプラットフォーム標章については第三者表示を、著作権の範囲については著作権表示を参照してください。