diff --git a/.agents/skills/cloudflare-zedi/references/secrets-template.md b/.agents/skills/cloudflare-zedi/references/secrets-template.md index 5548c974..6e093c74 100644 --- a/.agents/skills/cloudflare-zedi/references/secrets-template.md +++ b/.agents/skills/cloudflare-zedi/references/secrets-template.md @@ -53,8 +53,7 @@ Never commit secret values; use `wrangler secret put` for Workers._ | `BETTER_AUTH_SECRET` | セッション・署名 | 同値(**必ず同一** — Hocuspocus / MCP と共有) | | `BETTER_AUTH_URL` | OAuth コールバック base | Worker URL に更新(DNS 切替 PR で) | | `CORS_ORIGIN` | CSRF / trusted origins | フロント URL(Phase 3 後に Workers ドメインへ) | -| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Google OAuth | 同値 | -| `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | GitHub OAuth | 同値 | +| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Google OAuth(唯一の social IdP) | 同値 | | `STORAGE_ENDPOINT` | R2 S3 互換 endpoint(presign 用) | 同値 | | `STORAGE_ACCESS_KEY` / `STORAGE_SECRET_KEY` | R2 API token(presign 用) | 同値(binding とは別 — presign に必要) | | `STORAGE_BUCKET_NAME` | バケット名 | `zedi-storage-dev` / `zedi-storage-prod` | @@ -224,6 +223,7 @@ bunx wrangler secret put BETTER_AUTH_SECRET --env dev ## 関連 / Related +- [worker-secret-values.md](worker-secret-values.md) — 各値の**取得方法**(どこから取るか) - [migration-plan.md](migration-plan.md) — フェーズ定義 - [token-scopes.md](token-scopes.md) — API トークンスコープ - `server/api/.env.example` — 鍵名とコメント diff --git a/.agents/skills/cloudflare-zedi/references/token-scopes.md b/.agents/skills/cloudflare-zedi/references/token-scopes.md index fd6f5090..4777fdc3 100644 --- a/.agents/skills/cloudflare-zedi/references/token-scopes.md +++ b/.agents/skills/cloudflare-zedi/references/token-scopes.md @@ -19,6 +19,37 @@ _Create one dedicated migration token; keep it separate from the existing Terraf | DNS (Edit) | Zone (`zedi-note.app`) | `custom_domain` による DNS 自動作成 | Phase 2〜3 | | Workers Routes (Edit) | Zone (`zedi-note.app`) | カスタムドメインのルート設定 | Phase 2〜3 | +## 確定事項 / Decided (2026-07-18, #1091) + +- **種別**: アカウント API トークン(Account Owned Token)。ユーザートークンは使わない + — CI 用資格情報を個人ユーザーに紐づけないため。必要権限は全て Account/Zone レベルで賄える。 +- **トークン名**: `zedi-migration` +- **作成ページ**: Cloudflare ダッシュボード → 対象アカウント → + **Manage Account → Account API Tokens** + (`https://dash.cloudflare.com//api-tokens`)。 + プロフィール配下の API Tokens は**ユーザートークン側**なので使わない。 +- **Phase 2 時点の権限**(現行 CI が壊れない最小集合): + - Account / **Workers Scripts (Edit)** — `deploy-api-worker-dev.yml` / `deploy-mcp-worker-dev.yml` + - Account / **Cloudflare Pages (Edit)** — ⚠️ GitHub Secret `CLOUDFLARE_API_TOKEN` は + `deploy-dev.yml` / `deploy-prod.yml` の Pages デプロイと共用のため、これを外すと + フロント/admin のデプロイが壊れる + - Account / **Workers Tail (Read)** — `wrangler tail` での dev 検証 + - Account / **Workers R2 Storage (Edit)** — バケット管理(既存 #1089 分の運用) +- D1 / DNS / Workers Routes は該当フェーズ(#1090 / prod 切替)到達時にトークンを + **Roll ではなく編集**で拡張する(Account API Tokens は権限の追記が可能)。 + +## 設定手順 / Setup steps + +1. 上記ページで `zedi-migration` を作成(TTL は無期限 or 1 年。Client IP 制限は CI ランナー + が可変 IP のため付けない)。 +2. GitHub Secrets を差し替え: + `gh secret set CLOUDFLARE_API_TOKEN --repo otomatty/zedi`(値をペースト)。 + `CLOUDFLARE_ACCOUNT_ID` がアカウント ID と一致しているかも確認。 +3. 検証: `gh workflow run deploy-api-worker-dev.yml --ref develop` → run が green になること。 + 併せて `deploy-dev.yml`(Pages 側)が次の develop push で green のままであること。 +4. ローカルは wrangler の OAuth ログインで足りている場合、トークン設定は不要。 + 必要になったら `.env.local`(gitignore)へ。 + ## 注意 / Notes - トークンは最小権限から始め、フェーズ進行に合わせて拡張する。 diff --git a/.agents/skills/cloudflare-zedi/references/worker-secret-values.md b/.agents/skills/cloudflare-zedi/references/worker-secret-values.md new file mode 100644 index 00000000..37da9436 --- /dev/null +++ b/.agents/skills/cloudflare-zedi/references/worker-secret-values.md @@ -0,0 +1,130 @@ +# Worker secrets の値の取得方法 / How to obtain each Worker secret + +`server/api/.env.worker.dev`(および `.env.worker.production`)を埋めるときの、 +**各値をどこから取る/どう作るか**の実務ガイド。何のための値か・ライフサイクルは +[secrets-template.md](secrets-template.md) を、鍵名の一覧は +`server/api/.env.worker.dev.example` を正とする。 + +> このガイドは **dev Worker を新規セットアップする**前提(Railway からのコピーではなく +> dev 用の値を新たに用意する)。秘密値は **Issue / PR / チャットに貼らない**。 +> `.env.worker.dev`(gitignore 済み)にだけ書く。 + +## まず deploy を通す最小構成 / Minimum to unblock deploy + +`wrangler deploy` の起動検証は必須 6 項目が **非空である**ことだけを見る(値の正しさは +検証しない)。認証フロー・DB 到達の dev 検証は #1090(D1)後なので、**いま deploy を +通すだけなら**次の割り切りが可能: + +| 項目 | 最小構成での値 | +| ------------------------- | ---------------------------------------------------------------- | +| `BETTER_AUTH_SECRET` | **新規生成した実値**(署名に使うので本物が必要) | +| `BETTER_AUTH_URL` | `https://zedi-api-dev.otomatty.workers.dev`(実 URL) | +| `CORS_ORIGIN` | 実オリジン(CORS を正しく動かすなら本物、boot だけなら仮でも可) | +| `GOOGLE_CLIENT_ID/SECRET` | OAuth を試すまでは仮文字列で可(例 `placeholder`) | +| `DATABASE_URL` | プレースホルダのまま(例ファイル既定) | + +OAuth プロバイダの実値は sign-in を実際に試すとき(#1090 後)に差し替える。 + +--- + +## 必須 / Required at boot(未設定だと `wrangler deploy` が失敗する) + +### BETTER_AUTH_SECRET + +- **新規生成**(元の値は失われているため作り直す): + ```bash + openssl rand -base64 32 # 44 文字。Better Auth の最低 32 文字要件を満たす + ``` +- ⚠️ 同一環境で共有する他サービス(dev の MCP / hocuspocus Worker を将来立てる場合)とは + **同一値**にすること。dev Worker 単体で動かす今は独立でよいが、生成した値は控えておく。 +- Railway 本番の secret とは別物(別環境)。本番切替時に本番用を別途決める。 + +### BETTER_AUTH_URL + +- **値**: `https://zedi-api-dev.otomatty.workers.dev`(workers.dev サブドメイン = `otomatty`)。 + - サブドメインが未設定なら Cloudflare dashboard → **Workers & Pages** → 右側 + **サブドメイン**で `otomatty` を設定(アカウントに 1 つ)。 + - custom domain(`api-dev.zedi-note.app` 等)を後で使う場合はそちらに差し替え。 +- ⚠️ OAuth コールバック base になるため、OAuth を実際に試すときはこの URL を + Google/GitHub の redirect URI にも登録する(下記)。 + +### CORS_ORIGIN + +- **値**: dev で API を叩くフロント/admin のオリジンをカンマ区切りで新規指定。 + - 例: `https://zedi-dev.pages.dev,https://zedi-admin-dev.pages.dev` + - ローカルから叩くなら `http://localhost:5173,http://localhost:30001` を足す。 +- Chrome 拡張を使うなら `chrome-extension://`(`chrome://extensions` で ID 確認)。 + +### GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET + +- OAuth sign-in を dev で試すときに必要。それまでは仮文字列で boot を通せる。 + ソーシャルログインは **Google のみ**。 +- **新規作成/取得**: [Google Cloud Console](https://console.cloud.google.com/apis/credentials) + → APIs & Services → **Credentials** → **Create Credentials → OAuth client ID** + (Web application)。または既存クライアントの ID / Secret を使う。 +- **Authorized redirect URIs** に `/api/auth/callback/google` を追加。 + +### DATABASE_URL + +- **プレースホルダのままで可**(例ファイル既定 + `postgresql://placeholder:unused@127.0.0.1:5432/unused`)。 +- 理由: auth.ts がモジュールロード時に pg.Pool を構築するため起動時に必須だが、実接続は + DB 依存ルートを叩くまで発生しない(遅延接続)。dev の DB 依存経路の検証は #1090 後。 + +--- + +## 任意 / Optional(未設定なら no-op、またはその機能だけ無効) + +### SENTRY_DSN_API + +- **新規取得**: [Sentry](https://sentry.io) → 対象プロジェクト → Settings → + **Client Keys (DSN)**。空のままなら Sentry 送信は no-op(起動に影響しない)。 + +### STORAGE_ENDPOINT / STORAGE_ACCESS_KEY / STORAGE_SECRET_KEY / STORAGE_BUCKET_NAME + +- presigned URL 生成にのみ必要(R2 の読み書きは `STORAGE_BUCKET` binding が処理)。 +- **STORAGE_ENDPOINT**: `https://175c04a4465bcc9815b21176a852f0c0.r2.cloudflarestorage.com`。 +- **STORAGE_ACCESS_KEY / STORAGE_SECRET_KEY**: R2 の S3 互換キー。 + - `zedi-migration` トークン作成時に発行された **アクセスキー ID / シークレット + アクセスキー**を使える(`.env` の `CLOUDFLARE_R2_ACCESS_KEY_ID` / + `CLOUDFLARE_R2_SECRET_ACCESS_KEY`)。 + - もしくは Cloudflare dashboard → **R2 → Manage R2 API Tokens** で専用トークンを新規発行。 +- **STORAGE_BUCKET_NAME**: dev は `zedi-storage-dev`(例ファイル既定)。 + +### RESEND_API_KEY / RESEND_FROM_EMAIL + +- **新規取得**: [Resend](https://resend.com/api-keys)。未設定ならメール送信が無効になるだけ。 + +### MCP_REDIRECT_URI_ALLOW / MCP_JWT_EXP_DAYS + +- 外部 MCP 連携を dev で使う場合のみ。未設定だと `/api/mcp/authorize-code` が全 redirect を + 拒否する(MCP 機能のみ無効)。例: `http://127.0.0.1:,https://zedi-note.app`。 + +### OPENROUTER_API_KEY / YOUTUBE_DATA_API_KEY + +- それぞれ AI モデル料金 sync / YouTube クリップ用の任意キー。使う機能があれば新規取得。 + +--- + +## 投入と確認 / Put & verify + +```bash +cd server/api +bun run worker:secrets:put -- --env dev --dry-run # 埋めた項目の確認(空値はスキップされる) +bun run worker:secrets:put -- --env dev # 一括投入 +bunx wrangler secret list --env dev # Worker 側に載った secret 名の確認 +``` + +- ⚠️ `worker:secrets:put` は **空値をスキップ**する。必須 6 項目に値(プレースホルダ含む)が + 入っていないとアップロードされず、deploy が `... must be set` で失敗する。`--dry-run` で + 6 項目が出ることを確認する。 +- ⚠️ **順序**: secrets 投入 → その後 `wrangler deploy`(deploy 時の起動検証で必須値を読む)。 +- 投入後、CI の `deploy-api-worker-dev.yml` を再実行するか develop への push で dev デプロイ。 + +--- + +## 関連 / Related + +- [secrets-template.md](secrets-template.md) — 全 secret のライフサイクル台帳 +- [token-scopes.md](token-scopes.md) — API トークンのスコープと作成手順 +- `server/api/.env.worker.dev.example` — 鍵名と必須/任意の区別 diff --git a/.env.example b/.env.example index 3226a932..5e302457 100644 --- a/.env.example +++ b/.env.example @@ -93,6 +93,21 @@ POLAR_PRO_YEARLY_PRODUCT_ID=YOUR_POLAR_YEARLY_PRODUCT_ID # VITE_ADMIN_SENTRY_DSN=https://@o0.ingest.sentry.io/ # SENTRY_DSN_API=https://@o0.ingest.sentry.io/ +# Cloudflare migration token `zedi-migration` (#1088/#1091 — Account API Token). +# 作成手順・権限は .agents/skills/cloudflare-zedi/references/token-scopes.md を参照。 +# ローカル wrangler は通常 OAuth ログインで足りるため、これらは CI 復旧・保管用の控え。 +# 値を設定すると bunx wrangler がこのトークンで動く(OAuth より優先される)。 +# GitHub Secrets 側: CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID に同じ値を設定する。 +# CLOUDFLARE_ACCOUNT_ID= +# CLOUDFLARE_API_TOKEN= +# +# 上記トークン作成時に発行される R2 S3 互換キー(アクセスキー ID = トークン ID、 +# シークレット = トークン値のハッシュ)。presign 用の STORAGE_ACCESS_KEY / +# STORAGE_SECRET_KEY として server/api/.env.worker.* に転記して使える。 +# Railway 稼働中の既存 STORAGE_*(別の R2 トークン由来)はこれとは独立。 +# CLOUDFLARE_R2_ACCESS_KEY_ID= +# CLOUDFLARE_R2_SECRET_ACCESS_KEY= + # Docker Compose (docker-compose.dev.yml) # Override defaults for local dev; required in shared/production. Do not commit real secrets. # POSTGRES_USER=zedi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59c93b2a..9c471db8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -286,6 +286,13 @@ jobs: working-directory: server/api run: bunx tsc --noEmit + # Worker バンドル検査 (#1091 FR-1.2): dry-run(資格情報不要)で + # LangGraph / @hono/node-server / @sentry/node / ioredis の不在を fail-fast。 + # Credential-free Worker bundle guard (dry-run) — forbidden deps must be absent. + - name: Worker bundle check + working-directory: server/api + run: bun run worker:bundle:check + api-test: name: API Tests if: github.event_name != 'pull_request' || !github.event.pull_request.draft @@ -316,6 +323,12 @@ jobs: working-directory: server/api run: bunx vitest run --coverage + # workerd 実行テスト (#1091 FR-5): Worker エントリを実ランタイムで検証。 + # Worker-runtime tests on workerd via vitest-pool-workers. + - name: Run worker-runtime tests + working-directory: server/api + run: bun run test:worker + - name: Upload API coverage report if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.github/workflows/deploy-api-worker-dev.yml b/.github/workflows/deploy-api-worker-dev.yml index ae614605..4aeac557 100644 --- a/.github/workflows/deploy-api-worker-dev.yml +++ b/.github/workflows/deploy-api-worker-dev.yml @@ -34,6 +34,13 @@ jobs: - name: Install dependencies working-directory: server/api run: bun install --frozen-lockfile + # ci.yml とは並列起動のため、デプロイ前実行を機械的に保証するのはこのステップ + # (#1091 LC-5)。dry-run のため Cloudflare 資格情報は不要。 + # Runs the credential-free bundle guard right before deploy — ci.yml runs + # in parallel on the same push, so this step is the ordering guarantee. + - name: Worker bundle check + working-directory: server/api + run: bun run worker:bundle:check - name: Deploy to Cloudflare Workers (dev) uses: nick-fields/retry@v4 env: diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/aidlc-state.md b/aidlc/spaces/default/intents/260718-api-workers-migration/aidlc-state.md new file mode 100644 index 00000000..95db5de9 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/aidlc-state.md @@ -0,0 +1,100 @@ +# AI-DLC State Tracking + +## Project Information +- **Project**: https://github.com/otomatty/zedi/issues/1091 こちらのissueに取り組んでください +- **Project Type**: Brownfield +- **Scope**: infra +- **Start Date**: 2026-07-18T02:00:49Z +- **State Version**: 7 +- **Active Agent**: aidlc-operations-agent +- **Worktree Path**: +- **Bolt Refs**: +- **Practices Affirmed Timestamp**: 2026-07-18T03:56:48Z + +## Scope Configuration +- **Stages to Execute**: 0.1, 0.2, 0.3, 2.2, 2.3, 3.2, 3.3, 3.4, 3.7, 4.1, 4.2, 4.3, 4.4 +- **Stages to Skip**: 1.1 (intent-capture), 1.2 (market-research), 1.3 (feasibility), 1.4 (scope-definition), 1.5 (team-formation), 1.6 (rough-mockups), 1.7 (approval-handoff), 2.1 (reverse-engineering), 2.4 (user-stories), 2.5 (refined-mockups), 2.6 (application-design), 2.7 (units-generation), 2.8 (delivery-planning), 3.1 (functional-design), 3.5 (code-generation), 3.6 (build-and-test), 4.5 (incident-response), 4.6 (performance-validation), 4.7 (feedback-optimization) +- **Depth**: Standard +- **Test Strategy**: Standard + +## Workspace State +- **Project Root**: C:\Users\saedg\apps\zedi +- **Languages**: TypeScript +- **Frameworks**: Vite, React +- **Build System**: bun (package.json) + +## Execution Plan Summary +- **Total Stages**: 13 +- **Completed**: 13 +- **In Progress**: none + +## Runtime State +- **Revision Count**: 0 + +- **Skeleton Stance**: on +## Phase Progress + + +- **Initialization**: Verified +- **Ideation**: Skipped +- **Inception**: Verified +- **Construction**: Verified +- **Operation**: Verified + +## Stage Progress + + +### INITIALIZATION PHASE +- [x] workspace-scaffold — EXECUTE +- [x] workspace-detection — EXECUTE +- [x] state-init — EXECUTE + +### IDEATION PHASE +- [ ] intent-capture — SKIP +- [ ] market-research — SKIP +- [ ] feasibility — SKIP +- [ ] scope-definition — SKIP +- [ ] team-formation — SKIP +- [ ] rough-mockups — SKIP +- [ ] approval-handoff — SKIP + +### INCEPTION PHASE +- [ ] reverse-engineering — SKIP +- [x] practices-discovery — EXECUTE +- [x] requirements-analysis — EXECUTE +- [ ] user-stories — SKIP +- [ ] refined-mockups — SKIP +- [ ] application-design — SKIP +- [ ] units-generation — SKIP +- [ ] delivery-planning — SKIP + +### CONSTRUCTION PHASE +Per unit: [TBD] +- [ ] functional-design — SKIP +- [x] nfr-requirements — EXECUTE +- [x] nfr-design — EXECUTE +- [x] infrastructure-design — EXECUTE +- [ ] code-generation — SKIP +- [ ] build-and-test — SKIP +- [x] ci-pipeline — EXECUTE + +### OPERATION PHASE +- [x] deployment-pipeline — EXECUTE +- [x] environment-provisioning — EXECUTE +- [x] deployment-execution — EXECUTE +- [x] observability-setup — EXECUTE +- [ ] incident-response — SKIP +- [ ] performance-validation — SKIP +- [ ] feedback-optimization — SKIP + +## Current Status +- **Lifecycle Phase**: OPERATION +- **Current Stage**: observability-setup +- **Next Stage**: none +- **Status**: Completed +- **Last Updated**: 2026-07-18T08:12:11Z + +## Session Resume Point +- **Last Completed Stage**: observability-setup +- **Next Action**: Workflow complete +- **Pending Artifacts**: none diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/audit/saedgewell-353e06d8a0b4.md b/aidlc/spaces/default/intents/260718-api-workers-migration/audit/saedgewell-353e06d8a0b4.md new file mode 100644 index 00000000..1900d594 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/audit/saedgewell-353e06d8a0b4.md @@ -0,0 +1,1012 @@ +# AI-DLC Audit Log + +## Workflow Start +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: WORKFLOW_STARTED +**Scope**: infra +**Request**: /aidlc https://github.com/otomatty/zedi/issues/1091 こちらのissueに取り組んでください + +--- + +## Phase Start +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: PHASE_STARTED +**Phase**: initialization +**Stage count**: 3 +**Scope**: infra + +--- + +## Phase Skip +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: PHASE_SKIPPED +**Phase**: ideation +**Scope**: infra +**Reason**: scope infra excludes ideation + +--- + +## Stage Start +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: STAGE_STARTED +**Stage**: workspace-scaffold +**Agent**: orchestrator + +--- + +## Workspace Scaffolded +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: WORKSPACE_SCAFFOLDED +**Request**: /aidlc https://github.com/otomatty/zedi/issues/1091 こちらのissueに取り組んでください +**Details**: Per-intent artifact dirs + space-level knowledge/ ensured (shell shipped by SEED) + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: STAGE_COMPLETED +**Stage**: workspace-scaffold +**Details**: Per-intent artifact dirs + space-level knowledge/ ensured + +--- + +## Stage Start +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: STAGE_STARTED +**Stage**: workspace-detection +**Agent**: orchestrator + +--- + +## Workspace Scanned +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: WORKSPACE_SCANNED +**Project Type**: Brownfield +**Languages**: TypeScript +**Frameworks**: Vite, React +**Build System**: bun (package.json) +**Details**: Deterministic rule-based scan + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: STAGE_COMPLETED +**Stage**: workspace-detection +**Details**: Classified Brownfield; languages=TypeScript; frameworks=Vite, React + +--- + +## Stage Start +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: STAGE_STARTED +**Stage**: state-init +**Agent**: orchestrator + +--- + +## Workspace Initialised +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: WORKSPACE_INITIALISED +**Request**: /aidlc https://github.com/otomatty/zedi/issues/1091 こちらのissueに取り組んでください +**Project Type**: Brownfield +**Scope**: infra +**Languages**: TypeScript +**Frameworks**: Vite, React +**Build System**: bun (package.json) +**Details**: 13 stages in scope, routing to practices-discovery + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: STAGE_COMPLETED +**Stage**: state-init +**Details**: State initialized: infra scope, 13 stages, routing to practices-discovery + +--- + +## Phase Completion +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: PHASE_COMPLETED +**From phase**: initialization +**To phase**: inception +**Stages completed**: 3 + +--- + +## Phase Verification +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: PHASE_VERIFIED +**Phase boundary**: initialization → inception + +--- + +## Phase Start +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: PHASE_STARTED +**Phase**: inception +**Scope**: infra + +--- + +## Stage Start +**Timestamp**: 2026-07-18T02:00:49Z +**Event**: STAGE_STARTED +**Stage**: practices-discovery +**Agent**: aidlc-pipeline-deploy-agent + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T02:04:27Z +**Event**: DECISION_RECORDED +**Stage**: practices-discovery +**Decision**: Interview: 5 gap questions (way of working, walking skeleton stance, prod deploy gating, required status checks, coverage enforcement) + interaction mode choice +**Options**: Guide me,I'll edit the file,Chat + +--- + +## Error Logged +**Timestamp**: 2026-07-18T02:25:14Z +**Event**: ERROR_LOGGED +**Tool**: aidlc-log +**Command**: aidlc-log answer --stage practices-discovery --details Mode choice: Guide me +**Error**: Refusing to record this answer: a real human has not acted at this checkpoint this turn. Type your answer in the session (which records a human turn) before logging it. + +--- + +## Error Logged +**Timestamp**: 2026-07-18T03:19:51Z +**Event**: ERROR_LOGGED +**Tool**: aidlc-log +**Command**: aidlc-log answer --stage practices-discovery --details Guided batch 1-2 (Q1-Q5): Q1=A solo+AI agents; Q2=A walking skeleton always; Q3=A prod deploy fully automated; Q4=A required status checks configured server-side; Q5=A coverage 80% stays advisory, mutation score primary +**Error**: Refusing to record this answer: a real human has not acted at this checkpoint this turn. Type your answer in the session (which records a human turn) before logging it. + +--- + +## Practices Discovered +**Timestamp**: 2026-07-18T03:30:36Z +**Event**: PRACTICES_DISCOVERED +**Sources Scanned**: git history, .github/workflows, wrangler.jsonc, stryker.config.mjs, eslint.config.js, AGENTS.md, playwright.config.ts, dependabot.yml +**Drafts**: team-practices.md, discovered-rules.md + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T03:30:48Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: practices-discovery + +--- + +## Guardrail Loaded +**Timestamp**: 2026-07-18T03:31:15Z +**Event**: GUARDRAIL_LOADED +**Scope**: all +**Path**: .codex/aidlc-rules/ +**Rule count**: 7 + +--- + +## Health Check +**Timestamp**: 2026-07-18T03:31:15Z +**Event**: HEALTH_CHECKED +**Request**: /aidlc --doctor +**Details**: 42 passed, 0 failed + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T03:31:45Z +**Event**: DECISION_RECORDED +**Stage**: practices-discovery +**Decision**: Affirmation gate: review team-practices.md + discovered-rules.md, promote on approve; plus learnings ritual (2 candidates) +**Options**: Approve,Edit-then-approve,Reject and rewrite + +--- + +## Rule Learned +**Timestamp**: 2026-07-18T03:56:31Z +**Event**: RULE_LEARNED +**Stage**: practices-discovery +**Candidate-ID**: c1 +**Destination**: C:\Users\saedg\apps\zedi\aidlc\spaces\default\memory\project.md +**Heading**: ## AI-DLC Execution Notes +**Source**: orchestrator + +--- + +## Rule Learned +**Timestamp**: 2026-07-18T03:56:31Z +**Event**: RULE_LEARNED +**Stage**: practices-discovery +**Candidate-ID**: c2 +**Destination**: C:\Users\saedg\apps\zedi\aidlc\spaces\default\memory\project.md +**Heading**: ## AI-DLC Execution Notes +**Source**: orchestrator + +--- + +## Practices Affirmed +**Timestamp**: 2026-07-18T03:56:42Z +**Event**: PRACTICES_AFFIRMED +**Affirming User**: otomatty +**Sections Written**: Way of Working, Walking Skeleton, Testing Posture, Deployment, Code Style +**Mandated Rules Appended**: 8 +**Forbidden Rules Appended**: 7 +**Timestamp**: 2026-07-18T03:56:42Z + +--- + +## Error Logged +**Timestamp**: 2026-07-18T03:56:48Z +**Event**: ERROR_LOGGED +**Tool**: aidlc-state +**Command**: aidlc-state approve practices-discovery --user-input Approve --project-dir C:\Users\saedg\apps\zedi +**Error**: Refusing to approve "practices-discovery": a real human has not acted at this gate since it opened. The approval gate requires a typed human turn before it can commit. Acknowledge the gate as a human, then approve. (autonomous Construction is exempt) + +--- + +## Error Logged +**Timestamp**: 2026-07-18T04:00:16Z +**Event**: ERROR_LOGGED +**Tool**: aidlc-state +**Command**: aidlc-state approve practices-discovery --user-input approve --project-dir C:\Users\saedg\apps\zedi +**Error**: Refusing to approve "practices-discovery": a real human has not acted at this gate since it opened. The approval gate requires a typed human turn before it can commit. Acknowledge the gate as a human, then approve. (autonomous Construction is exempt) + +--- + +## Human Turn +**Timestamp**: 2026-07-18T04:01:21Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T04:01:22Z +**Event**: GATE_APPROVED +**Stage**: practices-discovery +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T04:01:22Z +**Event**: STAGE_COMPLETED +**Stage**: practices-discovery +**Details**: Stage Practices Discovery approved by gate + +--- + +## Stage Start +**Timestamp**: 2026-07-18T04:01:22Z +**Event**: STAGE_STARTED +**Stage**: requirements-analysis +**Agent**: aidlc-product-agent + +--- + +## Human Turn +**Timestamp**: 2026-07-18T04:04:31Z +**Event**: HUMAN_TURN + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T04:05:38Z +**Event**: DECISION_RECORDED +**Stage**: requirements-analysis +**Decision**: 8 clarifying questions (DoD, pg/Hyperdrive, LangGraph separation, Sentry, clientIp, presign, test strategy, prod workflow) + interaction mode choice +**Options**: Guide me,I'll edit the file,Chat + +--- + +## Human Turn +**Timestamp**: 2026-07-18T05:26:09Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-18T05:27:53Z +**Event**: HUMAN_TURN + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T05:28:02Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: requirements-analysis + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T05:28:09Z +**Event**: DECISION_RECORDED +**Stage**: requirements-analysis +**Decision**: Approval gate: requirements.md (READY after 1 revision) + learnings ritual (2 candidates) +**Options**: Approve,Request Changes,Add User Stories + +--- + +## Rule Learned +**Timestamp**: 2026-07-18T05:37:20Z +**Event**: RULE_LEARNED +**Stage**: requirements-analysis +**Candidate-ID**: c1 +**Destination**: C:\Users\saedg\apps\zedi\aidlc\spaces\default\memory\project.md +**Heading**: ## Issue 1091 Scope Decisions +**Source**: orchestrator + +--- + +## Rule Learned +**Timestamp**: 2026-07-18T05:37:20Z +**Event**: RULE_LEARNED +**Stage**: requirements-analysis +**Candidate-ID**: c2 +**Destination**: C:\Users\saedg\apps\zedi\aidlc\spaces\default\memory\project.md +**Heading**: ## Issue 1091 Scope Decisions +**Source**: orchestrator + +--- + +## Human Turn +**Timestamp**: 2026-07-18T05:59:00Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T05:59:09Z +**Event**: GATE_APPROVED +**Stage**: requirements-analysis +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T05:59:09Z +**Event**: STAGE_COMPLETED +**Stage**: requirements-analysis +**Details**: Stage Requirements Analysis approved by gate + +--- + +## Phase Completion +**Timestamp**: 2026-07-18T05:59:09Z +**Event**: PHASE_COMPLETED +**From phase**: inception +**To phase**: construction +**Stages completed**: 5 + +--- + +## Phase Verification +**Timestamp**: 2026-07-18T05:59:09Z +**Event**: PHASE_VERIFIED +**Phase boundary**: inception → construction + +--- + +## Phase Start +**Timestamp**: 2026-07-18T05:59:09Z +**Event**: PHASE_STARTED +**Phase**: construction +**Scope**: infra + +--- + +## Stage Start +**Timestamp**: 2026-07-18T05:59:09Z +**Event**: STAGE_STARTED +**Stage**: nfr-requirements +**Agent**: aidlc-architect-agent + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T06:00:29Z +**Event**: DECISION_RECORDED +**Stage**: nfr-requirements +**Decision**: 2 gap questions: Workers plan tier (bundle/CPU budgets), performance stance (parity vs explicit targets) +**Options**: guided + +--- + +## Human Turn +**Timestamp**: 2026-07-18T06:29:12Z +**Event**: HUMAN_TURN + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T06:29:36Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: nfr-requirements + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T06:29:53Z +**Event**: DECISION_RECORDED +**Stage**: nfr-requirements +**Decision**: Approval gate: 5 NFR artifacts READY + learnings ritual (3 candidates) +**Options**: Approve,Request Changes + +--- + +## Rule Learned +**Timestamp**: 2026-07-18T06:37:54Z +**Event**: RULE_LEARNED +**Stage**: nfr-requirements +**Candidate-ID**: c1 +**Destination**: C:\Users\saedg\apps\zedi\aidlc\spaces\default\memory\project.md +**Heading**: ## AI-DLC Execution Notes +**Source**: orchestrator + +--- + +## Human Turn +**Timestamp**: 2026-07-18T06:52:34Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T06:52:43Z +**Event**: GATE_APPROVED +**Stage**: nfr-requirements +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T06:52:43Z +**Event**: STAGE_COMPLETED +**Stage**: nfr-requirements +**Details**: Stage NFR Requirements approved by gate + +--- + +## Stage Start +**Timestamp**: 2026-07-18T06:52:43Z +**Event**: STAGE_STARTED +**Stage**: nfr-design +**Agent**: aidlc-architect-agent + +--- + +## Human Turn +**Timestamp**: 2026-07-18T06:54:27Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-18T07:02:36Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-18T07:06:09Z +**Event**: HUMAN_TURN + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T07:06:25Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: nfr-design + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T07:06:26Z +**Event**: DECISION_RECORDED +**Stage**: nfr-design +**Decision**: Approval gate: 5 design artifacts READY after 1 revision (3 major fixed) + learnings ritual +**Options**: Approve,Request Changes + +--- + +## Rule Learned +**Timestamp**: 2026-07-18T07:08:08Z +**Event**: RULE_LEARNED +**Stage**: nfr-design +**Candidate-ID**: c2 +**Destination**: C:\Users\saedg\apps\zedi\aidlc\spaces\default\memory\project.md +**Heading**: ## Cloudflare Workers Bundle Boundaries +**Source**: orchestrator + +--- + +## Rule Learned +**Timestamp**: 2026-07-18T07:08:09Z +**Event**: RULE_LEARNED +**Stage**: nfr-design +**Candidate-ID**: c3 +**Destination**: C:\Users\saedg\apps\zedi\aidlc\spaces\default\memory\project.md +**Heading**: ## Cloudflare Workers Bundle Boundaries +**Source**: orchestrator + +--- + +## Human Turn +**Timestamp**: 2026-07-18T07:34:12Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T07:34:20Z +**Event**: GATE_APPROVED +**Stage**: nfr-design +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T07:34:20Z +**Event**: STAGE_COMPLETED +**Stage**: nfr-design +**Details**: Stage NFR Design approved by gate + +--- + +## Stage Start +**Timestamp**: 2026-07-18T07:34:20Z +**Event**: STAGE_STARTED +**Stage**: infrastructure-design +**Agent**: aidlc-aws-platform-agent + +--- + +## Human Turn +**Timestamp**: 2026-07-18T07:39:03Z +**Event**: HUMAN_TURN + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T07:39:27Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: infrastructure-design + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T07:39:28Z +**Event**: DECISION_RECORDED +**Stage**: infrastructure-design +**Decision**: Approval gate: 5 infra design artifacts READY (2 minors, 1 fixed inline) + learnings ritual +**Options**: Approve,Request Changes + +--- + +## Rule Learned +**Timestamp**: 2026-07-18T07:40:25Z +**Event**: RULE_LEARNED +**Stage**: infrastructure-design +**Candidate-ID**: c2 +**Destination**: C:\Users\saedg\apps\zedi\aidlc\spaces\default\memory\project.md +**Heading**: ## Cloudflare Workers Bundle Boundaries +**Source**: orchestrator + +--- + +## Rule Learned +**Timestamp**: 2026-07-18T07:40:25Z +**Event**: RULE_LEARNED +**Stage**: infrastructure-design +**Candidate-ID**: c1 +**Destination**: C:\Users\saedg\apps\zedi\aidlc\spaces\default\memory\project.md +**Heading**: ## AI-DLC Execution Notes +**Source**: orchestrator + +--- + +## Human Turn +**Timestamp**: 2026-07-18T07:44:27Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T07:44:35Z +**Event**: GATE_APPROVED +**Stage**: infrastructure-design +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T07:44:35Z +**Event**: STAGE_COMPLETED +**Stage**: infrastructure-design +**Details**: Stage Infrastructure Design approved by gate + +--- + +## Stage Start +**Timestamp**: 2026-07-18T07:44:35Z +**Event**: STAGE_STARTED +**Stage**: ci-pipeline +**Agent**: aidlc-pipeline-deploy-agent + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T07:46:09Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: ci-pipeline + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T07:46:10Z +**Event**: DECISION_RECORDED +**Stage**: ci-pipeline +**Decision**: Approval gate: ci-config.md + quality-gates.md (no reviewer declared) + learnings ritual +**Options**: Approve,Request Changes + +--- + +## Human Turn +**Timestamp**: 2026-07-18T07:48:41Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T07:48:48Z +**Event**: GATE_APPROVED +**Stage**: ci-pipeline +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T07:48:48Z +**Event**: STAGE_COMPLETED +**Stage**: ci-pipeline +**Details**: Stage CI Pipeline approved by gate + +--- + +## Phase Completion +**Timestamp**: 2026-07-18T07:48:48Z +**Event**: PHASE_COMPLETED +**From phase**: construction +**To phase**: operation +**Stages completed**: 9 + +--- + +## Phase Verification +**Timestamp**: 2026-07-18T07:48:48Z +**Event**: PHASE_VERIFIED +**Phase boundary**: construction → operation + +--- + +## Phase Start +**Timestamp**: 2026-07-18T07:48:48Z +**Event**: PHASE_STARTED +**Phase**: operation +**Scope**: infra + +--- + +## Stage Start +**Timestamp**: 2026-07-18T07:48:48Z +**Event**: STAGE_STARTED +**Stage**: deployment-pipeline +**Agent**: aidlc-pipeline-deploy-agent + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T07:49:51Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: deployment-pipeline + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T07:49:52Z +**Event**: DECISION_RECORDED +**Stage**: deployment-pipeline +**Decision**: Approval gate: cd-config.md, deployment-strategy.md, rollback-runbook.md + learnings ritual +**Options**: Approve,Request Changes + +--- + +## Human Turn +**Timestamp**: 2026-07-18T07:54:41Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T07:54:51Z +**Event**: GATE_APPROVED +**Stage**: deployment-pipeline +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T07:54:51Z +**Event**: STAGE_COMPLETED +**Stage**: deployment-pipeline +**Details**: Stage Deployment Pipeline approved by gate + +--- + +## Stage Start +**Timestamp**: 2026-07-18T07:54:51Z +**Event**: STAGE_STARTED +**Stage**: environment-provisioning +**Agent**: aidlc-aws-platform-agent + +--- + +## Memory Empty +**Timestamp**: 2026-07-18T07:58:40Z +**Event**: MEMORY_EMPTY +**Stage**: deployment-pipeline + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T07:58:41Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: environment-provisioning + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T07:58:43Z +**Event**: DECISION_RECORDED +**Stage**: environment-provisioning +**Decision**: Approval gate: environment-inventory.md + validation-report.md (live-verified) + learnings ritual +**Options**: Approve,Request Changes + +--- + +## Human Turn +**Timestamp**: 2026-07-18T08:04:19Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T08:04:31Z +**Event**: GATE_APPROVED +**Stage**: environment-provisioning +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T08:04:31Z +**Event**: STAGE_COMPLETED +**Stage**: environment-provisioning +**Details**: Stage Environment Provisioning approved by gate + +--- + +## Stage Start +**Timestamp**: 2026-07-18T08:04:31Z +**Event**: STAGE_STARTED +**Stage**: deployment-execution +**Agent**: aidlc-pipeline-deploy-agent + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T08:06:10Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: deployment-execution + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T08:06:11Z +**Event**: DECISION_RECORDED +**Stage**: deployment-execution +**Decision**: Approval gate: deployment-log, health-check-report, smoke-test-results (execution deferred to implementation PR) + learnings +**Options**: Approve,Request Changes + +--- + +## Human Turn +**Timestamp**: 2026-07-18T08:06:56Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T08:07:03Z +**Event**: GATE_APPROVED +**Stage**: deployment-execution +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T08:07:03Z +**Event**: STAGE_COMPLETED +**Stage**: deployment-execution +**Details**: Stage Deployment Execution approved by gate + +--- + +## Stage Start +**Timestamp**: 2026-07-18T08:07:03Z +**Event**: STAGE_STARTED +**Stage**: observability-setup +**Agent**: aidlc-operations-agent + +--- + +## Stage Awaiting Approval +**Timestamp**: 2026-07-18T08:08:26Z +**Event**: STAGE_AWAITING_APPROVAL +**Stage**: observability-setup + +--- + +## Decision Recorded +**Timestamp**: 2026-07-18T08:08:27Z +**Event**: DECISION_RECORDED +**Stage**: observability-setup +**Decision**: Final approval gate: 7 observability artifacts (dev-scope: deliberate none + prod re-evaluation paths) + learnings +**Options**: Approve,Request Changes + +--- + +## Human Turn +**Timestamp**: 2026-07-18T08:11:58Z +**Event**: HUMAN_TURN + +--- + +## Gate Approved +**Timestamp**: 2026-07-18T08:12:11Z +**Event**: GATE_APPROVED +**Stage**: observability-setup +**User Input**: approve + +--- + +## Stage Completion +**Timestamp**: 2026-07-18T08:12:11Z +**Event**: STAGE_COMPLETED +**Stage**: observability-setup +**Details**: Stage Observability Setup approved by gate + +--- + +## Phase Completion +**Timestamp**: 2026-07-18T08:12:11Z +**Event**: PHASE_COMPLETED +**From phase**: operation +**To phase**: (end) +**Stages completed**: 13 + +--- + +## Phase Verification +**Timestamp**: 2026-07-18T08:12:11Z +**Event**: PHASE_VERIFIED +**Phase boundary**: operation → end + +--- + +## Workflow Completion +**Timestamp**: 2026-07-18T08:12:11Z +**Event**: WORKFLOW_COMPLETED +**Scope**: infra +**Details**: Scope: infra, 13 stages completed + +--- + +## Human Turn +**Timestamp**: 2026-07-18T09:40:03Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-18T10:29:03Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-18T10:29:15Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-19T13:07:28Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-19T13:24:46Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-19T13:36:51Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-19T13:38:11Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-19T13:53:06Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-19T13:54:07Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-19T13:56:27Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-19T13:59:18Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-20T00:58:09Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-20T04:58:35Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-20T07:29:01Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-20T09:31:22Z +**Event**: HUMAN_TURN + +--- + +## Human Turn +**Timestamp**: 2026-07-20T11:36:55Z +**Event**: HUMAN_TURN + +--- diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/cicd-pipeline.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/cicd-pipeline.md new file mode 100644 index 00000000..57920f9a --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/cicd-pipeline.md @@ -0,0 +1,20 @@ +# CI/CD Pipeline — api-worker + +前提: performance-design.md(検証設計)・logical-components.md(LC-5/LC-6)を CI に落とす。application-design / business-logic-model.md は infra スコープにより不在。 + +## 既存(不変) + +- `ci.yml`: `api-typecheck` / `api-test` ジョブ(bun install --frozen-lockfile → typecheck / vitest)。 +- `deploy-api-worker-dev.yml`: `develop` push(paths: `server/api/**`)→ `wrangler deploy --env dev` + `curl --fail` health-poll(`WORKER_API_BASE_URL` 未設定時スキップ)。**CI の Cloudflare トークンは要修復(C-2)— 本 Issue ではワークフロー定義を変更せず、実デプロイ検証は資格情報復旧後。** + +## 追加(本 Issue) + +| 追加 | 実行場所 | 内容 | +|------|---------|------| +| `worker:bundle:check` script | `ci.yml` の api 系ジョブに 1 ステップ追加、**かつ `deploy-api-worker-dev.yml` の deploy 直前にも同ステップを置く**(ci.yml とは並列起動のため、デプロイ前実行を機械的に保証するのは deploy ジョブ内のステップ) | LC-5: `wrangler deploy --dry-run --outdir` 出力に `@langchain` / `src/agents` / `@hono/node-server` / `@sentry/node` が**不在**なことを fail-fast 検査。資格情報不要 | +| `test:worker` script | 同上(`api-test` に追加 or 併設ステップ) | LC-6: workerd テスト(vitest-pool-workers。互換フォールバックは code-generation で決定) | + +## 作らないもの + +- prod 用デプロイワークフロー(Q8=B — 切替判断時の別 PR)。 +- `wrangler versions upload` による PR プレビュー(migration-plan の将来項目。本 Issue では見送り)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/deployment-architecture.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/deployment-architecture.md new file mode 100644 index 00000000..f61c6285 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/deployment-architecture.md @@ -0,0 +1,54 @@ +# Deployment Architecture — api-worker + +前提: nfr-design 5 成果物(logical-components.md の LC-1〜6、performance/security/scalability/reliability-design.md)を実現する配置。application-design(components.md / services.md)と functional-design(business-logic-model.md)は infra スコープにより不在 — logical-components.md が正本。 + +## 構成トポロジ(本 Issue 完了時点) + +``` +[Browser/Clients] + | + +--> Railway: zedi api (src/index.ts, Node) <- 本番トラフィック(不変, A-3) + | +--> Railway Postgres / Redis / R2(S3互換) + | + +--> Cloudflare: zedi-api-dev (src/worker.ts) <- dev 検証専用 + +--> R2 binding STORAGE_BUCKET (zedi-storage-dev) + +--> DO binding KV_DO (KvDurableObject, SQLite classes) + +--> (DB なし — #1090 まで DB 依存経路は マスク済み 5xx) +``` + + + +## Worker 環境(wrangler.jsonc 実体、確認済み) + +| env | Worker 名 | R2 | DO | vars | +|-----|----------|----|----|------| +| dev | `zedi-api-dev` | `zedi-storage-dev` | `KV_DO` → `KvDurableObject` | `ENVIRONMENT=development` | +| production | `zedi-api` | `zedi-storage-prod` | 同上 | `ENVIRONMENT=production`(本 Issue ではデプロイしない — Q8=B) | + +- `compatibility_flags: ["nodejs_compat"]`、`compatibility_date: 2026-06-25`、`observability.enabled: true`(実体確認済み)。 +- **追加変更**: `RUNTIME: "cloudflare-workers"` var を全 env に追加(LC-3 のランタイム判定シグナル。`ENVIRONMENT` は dev/prod の区別であってランタイム判別ではないため別 var とする)。 +- DNS / custom_domain の変更はしない(prod 切替は Out of Scope)。 + +## エントリポイントと成果物 + +- Worker: `src/worker.ts` = `withSentry(...)` でラップした Hono app(agent ルートなし、LC-1/LC-2)+ `KvDurableObject` named export。 +- Node: `src/index.ts` 無変更(Railway 並行稼働、NFR-2)。 +- デプロイ単位: `wrangler deploy --env dev`(CI)。バンドルは LC-5 の dry-run 検査を通過したもののみ。 + +## Review + +**Verdict**: READY +**Reviewer**: aidlc-architecture-reviewer-agent +**Date**: 2026-07-18 + +### Findings + +(adversarial 照合: 5 成果物の全リポジトリ事実主張を実体と突き合わせ。blocker / major なし。) + +- **[verified] wrangler.jsonc 主張は全一致。** `server/api/wrangler.jsonc` 実体: env 名 `zedi-api-dev` / `zedi-api`、R2 `zedi-storage-dev` / `zedi-storage-prod`(binding `STORAGE_BUCKET`)、DO `KV_DO` → `KvDurableObject`、migrations `v1`(`new_sqlite_classes`)、`nodejs_compat`、`compatibility_date: 2026-06-25`、`observability.enabled: true`、`ENVIRONMENT` vars(development/production)。`RUNTIME` var は**未存在** — 「追加変更」フレーミングと整合(既存と偽っていない)。 +- **[verified] CI/CD 主張は実体一致。** `deploy-api-worker-dev.yml`: `develop` push + paths `server/api/**` → `wrangler deploy --env dev`、health-poll は `curl --fail` かつ `vars.WORKER_API_BASE_URL != ''` ゲート(「未設定時スキップ」記述どおり)。`ci.yml`: `api-typecheck`(`bun install --frozen-lockfile` → `bunx tsc --noEmit`, L265-287)/ `api-test`(→ `bunx vitest run --coverage`, L289-317)実在。 +- **[verified] Secrets 経路。** `server/api/scripts/putWorkerSecrets.ts` 実在、`wrangler secret bulk` 一括投入(`worker:secrets:put`)。`.env.worker.{dev,production}.example` に `SENTRY_DSN_API` は**未記載** — infrastructure-services.md の「追記する」は正しい(既存と偽っていない)。実ファイル `.env.worker.dev` / `.env.worker.production` の gitignore を `git check-ignore` で確認(SR-4 整合)。`/api/health` の `runtime` フィールドは `health.ts:35` に実在(monitoring-design の手動検証対象として妥当)。 +- **[verified] スコープ逸脱なし。** prod route / custom_domain / DNS 変更なし(Q8=B、Out of Scope 準拠)、Hyperdrive・D1 は明示的不作成(TS-3 / #1090)、Terraform 凍結維持(C-3)、prod デプロイワークフロー不作成(cicd-pipeline.md)。`RUNTIME` var の production env への追加は構成編集のみでデプロイを伴わず、C-3(wrangler.jsonc 正本)の範囲内。 +- **[verified] 上流整合。** LC-1〜6 / SR-1〜7 / PR-1〜6 / SC-1〜4 / RL-1〜5 / TS-3 / TS-9 / C-1〜4 / A-3 の参照は全て nfr-design(READY 済み)・nfr-requirements・requirements.md に解決。application-design / functional-design の不在は infra スコープの設計どおりで、5 成果物とも logical-components.md を正本と明記。ゼロ質問根拠(全判断が確定済み・新規リソースなし)は上記実体確認と矛盾しない。 +- **[minor] LC-5 検査とデプロイの順序は機械的に強制されない。** 「バンドルは LC-5 の dry-run 検査を通過したもののみ」とあるが、`worker:bundle:check` は `ci.yml` 追加であり、`deploy-api-worker-dev.yml` は同じ develop push で**並列に**起動する(ci.yml の成功を待たない)。dev 検証専用 Worker のため実害は小さいが、保証したいなら deploy job 内に dry-run 検査ステップを置くか、ブランチ保護で担保する旨を一行明記すべき。 +- **[minor] 資格情報前提の明示は十分だが検証は未完。** C-2(別アカウント wrangler / CI トークン失効)により本設計の実デプロイ経路は現時点で end-to-end 未検証。設計は資格情報不要な検査(dry-run、workerd テスト)を CI に置く構成で C-2 を正しく回避しており、復旧後の手動確認手段(`wrangler tail` / MCP)も monitoring-design に記載済み — 設計上の欠陥ではなく残存リスクとしての記録。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/infrastructure-design-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/infrastructure-design-questions.md new file mode 100644 index 00000000..05f90ec2 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/infrastructure-design-questions.md @@ -0,0 +1,15 @@ +# Infrastructure Design — Questions (unit: api-worker) + +質問なし(0 件)。 + +インフラ判断は全て確定済み: 構成の正本 = `server/api/wrangler.jsonc`(C-3、実体確認済み)、dev のみデプロイ(Q8=B)、Terraform 凍結、リソースは既存(R2 バケット・KV DO 作成済み、#1089/#1093)。application-design 成果物(components.md / services.md)は infra スコープにより不在(expected)— nfr-design の logical-components.md がコンポーネント正本。 + +新規リソースの作成・変更を伴わない(既存 binding の設計固定 + `RUNTIME` var 追加のみ)ため、人間への確認事項はない。 + +--- + +## G1. Approval gate (typed turn required) + +Infrastructure Design 5成果物(READY)の承認ゲート。チャットに approve / request changes をタイプしてください。 + +[Answer]: approve (typed, 2026-07-18) diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/infrastructure-services.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/infrastructure-services.md new file mode 100644 index 00000000..71fc5d35 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/infrastructure-services.md @@ -0,0 +1,25 @@ +# Infrastructure Services — api-worker + +前提: logical-components.md(LC-1〜6)・security-design.md・reliability-design.md の要求をサービス面に落とす。application-design / business-logic-model.md は infra スコープにより不在。 + +## 使用サービス(すべて既存 — 新規プロビジョニングなし) + +| サービス | リソース | 状態 | 用途 | +|---------|---------|------|------| +| Workers | `zedi-api-dev` | 作成済み(deploy-api-worker-dev.yml がデプロイ中) | dev 検証 | +| R2 | `zedi-storage-dev` / `zedi-storage-prod` | 作成済み(#1089) | メディア・サムネ・PDF | +| Durable Objects | `KvDurableObject`(SQLite classes, migration v1) | 定義済み(#1093/#1118) | レート制限 / ワンタイムコード / deny-list | +| Secrets (Worker) | `wrangler secret bulk`(putWorkerSecrets.ts) | 仕組み導入済み(#1089) | BETTER_AUTH_SECRET, SENTRY_DSN_API, STORAGE_*(presign 用) | + +## 変更点(本 Issue) + +- `wrangler.jsonc` に `RUNTIME` var を追加(deployment-architecture.md 参照)。 +- Worker secrets に `SENTRY_DSN_API` を追加登録(`.env.worker.*.example` に鍵名を追記。値は非コミット — SR-4)。 +- **登録しないもの**: `DATABASE_URL`(D1 直行方針により Worker には不要 — TS-3。`REDIS_URL` も不要のまま、#1093 済)。 + +## 明示的に作らないもの + +- Hyperdrive 設定(TS-3 で不採用)。 +- D1 データベース(#1090)。 +- prod Worker への route / custom_domain(Out of Scope)。 +- Terraform リソース(凍結中、C-3)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/monitoring-design.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/monitoring-design.md new file mode 100644 index 00000000..93393433 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/monitoring-design.md @@ -0,0 +1,21 @@ +# Monitoring Design — api-worker + +前提: reliability-design.md(RL-3〜5)・logical-components.md(LC-4)の可観測性要求を実現する。application-design / business-logic-model.md は infra スコープにより不在。 + +## エラー監視 + +- Sentry: Worker は `withSentry` ラッパ(LC-4、`SENTRY_DSN_API` secret)。Node/Railway は既存 `@sentry/node` 不変。DSN 未設定なら no-op。 +- スクラブ(`scrubSentryEvent`、`sendDefaultPii: false`)は両ランタイム共通実装。 + +## プラットフォーム可観測性 + +- `wrangler.jsonc` の `observability.enabled: true`(設定済み)により Workers Logs / invocation logs が有効。 +- 資格情報復旧後の手動確認手段: `wrangler tail` / Cloudflare MCP `query_worker_observability`(cloudflare-zedi スキルの使い分け表に従う)。 + +## ヘルスチェック + +- `/api/health`: CI 自動は HTTP 成功判定(`deploy-api-worker-dev.yml` の `curl --fail`)、`runtime: "cloudflare-workers"` フィールドの確認は手動 dev 検証(RL-5 の帰属どおり)。 + +## 本 Issue で追加しないもの + +- アラート・ダッシュボード・SLO 監視(dev 検証専用のため。prod 切替時に設計)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/shared-infrastructure.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/shared-infrastructure.md new file mode 100644 index 00000000..2be49d33 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/infrastructure-design/shared-infrastructure.md @@ -0,0 +1,19 @@ +# Shared Infrastructure — api-worker + +前提: security-design.md(SR-2)・deployment-architecture.md と Epic #1088 の全体設計(cloudflare-zedi スキル resource-map)に整合させる。application-design / business-logic-model.md は infra スコープにより不在。 + +## サービス間で共有されるもの + +| 共有物 | 共有先 | 本 Issue での扱い | +|--------|--------|------------------| +| `BETTER_AUTH_SECRET` | api ↔ mcp ↔ hocuspocus | 値の同期は運用ルールのまま(自動強制なし、既知ギャップ)。Worker へは `wrangler secret bulk` で供給 | +| R2 バケット(zedi-storage-*) | api Worker binding + Railway API(S3 互換エンドポイント) | 同一バケットを両ランタイムから利用(#1089 の設計どおり、変更なし) | +| Cloudflare アカウント / トークン | 全 Worker + Pages deploy | **要修復(C-2)**: ローカル wrangler が別アカウント、CI トークンも失効。復旧は本 Issue のコード実装とは独立に進められる | +| `KvDurableObject` クラス定義 | api Worker 専有(mcp は別 Worker で別途) | migration v1 のまま変更なし | + +## 他 Issue との境界 + +- #1092(mcp Workers 化): 本 Issue のモジュール境界原則・withSentry 方式(project.md の learnings に永続化済み)を再利用する。 +- #1090(D1): DB binding・`drizzle-migration-check` の D1 化はここに含めない。 +- #1094(hocuspocus DO)/ #1095(LangGraph Workflows): 接点なし(ルート分割で境界のみ確定)。 +- Terraform: 凍結。DNS・ゾーンは現状維持(Phase 5 で撤去)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/logical-components.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/logical-components.md new file mode 100644 index 00000000..d03b92ea --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/logical-components.md @@ -0,0 +1,73 @@ +# Logical Components — api-worker + +前提: business-logic-model.md は infra スコープにより未作成(functional-design スキップ)。本設計は nfr-requirements 5 成果物(performance/security/scalability/reliability-requirements.md、tech-stack-decisions.md)とコード実査(file:line 付き)から導出。 + +## コンポーネント一覧 + +| ID | コンポーネント | 変更 | 対応要件 | +|----|--------------|------|---------| +| LC-1 | `createApp(options)` — `src/app.ts` | オプション追加 | FR-1(ルート分割) | +| LC-2 | Worker エントリ — `src/worker.ts` | agent 無効で生成 + Sentry 初期化 | FR-1, FR-3 | +| LC-3 | clientIp — `src/lib/clientIp.ts` | Workers 分岐追加 | FR-2 | +| LC-4 | エラー捕捉 DI — `src/middleware/errorHandler.ts` + 両エントリ | capture 関数の注入化 | FR-3 | +| LC-5 | バンドル検査スクリプト — `server/api/scripts/`(新規) | 新規 | FR-1.2 | +| LC-6 | workerd テスト基盤 — vitest 併存構成 | 新規 | FR-5 | + +## LC-1: createApp オプション + +`createApp(): Hono`(`app.ts:59`、現状引数なし)に `{ includeAgentRoutes?: boolean }`(default `true`)を追加。`false` のとき以下を **登録しない**: + +- graph 登録 5 件(`app.ts:69-73`: stub / researchLoop / wikiCompose / ingestPlanner / wikiMaintenance) +- `app.route("/api/pages", composeSessionRoutes)`(`app.ts:168`) +- `app.route("/api/ingest", ingestRoutes)`(`app.ts:192`) + +境界は**モジュールレベル**で切る(静的 import のままでは bundler がフラグ値に関係なく取り込み、wrangler の esbuild 単一ファイルバンドルはローカルモジュールへの動的 import も取り込むため、実行時分岐・動的 import では除外できない): agent 系の import(`app.ts:47`, `49-53`)・graph 登録・マウントを `src/appAgents.ts` に分離し、`createApp` は登録フック `registerAgentRoutes?: (app: Hono) => void` を受け取る。`index.ts` だけが `appAgents.ts` を import してフックを渡し、`worker.ts` は渡さない。 + +除外の完全性根拠: `../agents` / `@langchain` を参照する routes/** は `composeSessions.ts`(+ `composeSessionProjection.ts`, `composeSessionRunLocale.ts`)と `ingest.ts` の 4 ファイルのみ(grep 検証済み)。 + +## LC-2: worker.ts + +`const app = createApp()` (`worker.ts:13`) → agent フックなし(LC-1)+ capture DI(LC-4)で生成し、default export を `@sentry/cloudflare` の **`withSentry(env => ({...}), app)` ラッパ**で包む(OQ-2 の決定 — 下記 LC-4)。`KvDurableObject` re-export(`worker.ts:11`)は維持(`withSentry` は fetch handler をラップするのみで、named export の DO クラスに影響しない)。 + +## LC-3: clientIp + +`extractClientIp(c)`(`clientIp.ts:81`)に Workers 分岐: **ランタイムが Workers のとき** `CF-Connecting-IP` ヘッダを第一候補にする。ランタイム判定はヘッダ存在ではなく binding/明示 var で行う(Node 環境で攻撃者が同名ヘッダを送る偽装を防ぐ。SR-5)。判定手段は `health.ts` の既存パターン(R2 binding 存在)と同型の明示的シグナルとし、実装時に `RUNTIME` var(wrangler.jsonc `[vars]`)を推奨。Node 側フォールバック順(`x-forwarded-for` → `x-real-ip` → socket、`clientIp.ts:83-88`)は不変。 + +**conninfo のバンドル除外機構**(FR-2.3、LC-1 と同じモジュールレベル境界): `@hono/node-server/conninfo` への静的 import(`clientIp.ts:18`)を新規 `src/lib/clientIpNode.ts` に移し、socket フォールバックを**注入可能な resolver** にする。`clientIp.ts` はヘッダ解決 + 注入された resolver 呼び出しのみのランタイム中立モジュールになり、`index.ts` だけが `clientIpNode.ts` を import して resolver を配線する(`worker.ts` は配線しない → socket フォールバックは Workers では単に無し)。共有利用箇所(`rateLimit.ts:93` / `auditLog.ts:81` / `inviteLinks.ts:155`)は `extractClientIp` を呼ぶだけで変更不要のまま、Worker バンドルから `@hono/node-server` が消え、LC-5 の不在検査と整合する。 + +利用箇所(OQ-5 の確定、全 3 箇所 + テスト): `middleware/rateLimit.ts:93`、`services/auditLog.ts:81`(re-export `:54-55`)、`routes/inviteLinks.ts:155`。 + +## LC-4: エラー捕捉 DI + +現状 `errorHandler.ts:4` が `lib/sentry.ts`(@sentry/node)を静的 import — このままでは Worker バンドルに @sentry/node が入る。設計: `createApp(options)` に `captureException?: (err, status, ctx) => void`(+ `shouldCapture` 相当)を注入し、errorHandler はコンテキスト経由で受け取る。errorHandler が要求する DI サーフェスは `captureApiException(err,status,{method,routePath})` + `shouldCaptureApiException(status)` の 2 点(`errorHandler.ts:15-16,38-39` の現利用面)。 + +- **Node 側**(`index.ts`): 既存 `lib/sentry.ts` の実装をそのまま注入。`initSentry()`(`index.ts:5`)も現状維持。 +- **Workers 側**(`worker.ts`): **`withSentry` ラッパ方式を採用**(OQ-2 の解)。`@sentry/cloudflare` の `withSentry(env => ({ dsn: env.SENTRY_DSN_API, sendDefaultPii: false, denyUrls, beforeSend: scrubSentryEvent }), app)` で default export を包む。ラッパが SDK 初期化と `ctx.waitUntil` によるイベント flush を保証するため(素の captureException ではリクエスト終端で flush されず欠落し得る — RL-4 の捕捉保証)、Workers 側に `initSentry` は**作らない**。新規 `lib/sentryWorker.ts` は capture DI(`Sentry.captureException` + 共有 `shouldCaptureApiException`)と `withSentry` オプション生成のみを持つ。 +- スクラブ挙動(`sendDefaultPii:false`、`denyUrls`、`beforeSend`/`scrubSentryEvent`)は両ランタイム同一とする(FR-3.2)。`scrubSentryEvent`(`sentry.ts:99`)と `shouldCaptureApiException`(`:83`)は SDK 非依存の純関数なのでランタイム中立モジュールへ抽出して共有する。DSN 未設定時 no-op(FR-3.3)。 + +## LC-5: バンドル検査 + +新規スクリプト(`scripts/` 配下、`worker:bundle:check` npm script): `wrangler deploy --dry-run --outdir ` の出力を検査し、`@langchain` / `src/agents` 由来コードと `@hono/node-server` / `@sentry/node` の**不在**を fail-fast で確認。資格情報不要(dry-run)なので C-2 制約下でも CI 実行可能。 + +## LC-6: workerd テスト基盤 + +`@cloudflare/vitest-pool-workers` を別 config(例: `vitest.config.worker.ts` + `test:worker` script、include: `src/__tests__/worker/**`)で併存導入(OQ-3 の解: 既存 `vitest.config.ts` は不変のまま分離)。workerd テスト対象(FR-5.2 + SR-6 + TS-5 の全数): /api/health、CORS プリフライト、clientIp 分岐、ルート分割の登録有無、auth マウント、エラーマスキング、KvStore 3 用途(`incrWithTtl` レート制限カウンタ / `getdel` ワンタイムコード原子消費 / `get`+`setex` deny-list)、storage/R2 + `@aws-sdk` presign 生成(TS-5/FR-4 の検証の置き場はここ)。**互換リスク**: 現行 vitest は 4.1.10 で、pool-workers の対応 vitest レンジは導入時に最新ドキュメントで要確認。非対応の場合のフォールバック: worker テスト専用にサポート対象 vitest をローカル devDependency として分離するか、`wrangler dev` + fetch ベースの統合テストに切替(判断は code-generation で行い、採用した側を tech-stack-decisions に追記する)。 + +## Review + +**Verdict**: READY +**Reviewer**: aidlc-architecture-reviewer-agent +**Date**: 2026-07-18 + +### Findings + +(iteration 2 再レビュー。iteration 1 の 3 major / 3 minor は全て解消をリポジトリ実体との照合で確認。blocker / major なし。) + +- **[resolved: major] LC-3 conninfo バンドル除外機構。** `@hono/node-server/conninfo` の静的 import(`clientIp.ts:18`)を新規 `clientIpNode.ts` へ移し、socket フォールバックを注入 resolver 化、`clientIp.ts` をランタイム中立にして `index.ts` のみが配線する設計が明記された。LC-1 と同一のモジュールレベル境界原則で、共有利用 3 箇所(`rateLimit.ts:93` / `auditLog.ts:81` / `inviteLinks.ts:155`)は無変更のまま Worker バンドルから `@hono/node-server` が消える — FR-2.3 と LC-5 の不在検査に整合。 +- **[resolved: major] OQ-2 決定。** `withSentry(env => ({...}), app)` ラッパ採用が LC-2/LC-4 に明記され、Workers 側 `initSentry` の不作成、`ctx.waitUntil` による flush 保証(RL-4)、DO named export への非影響(`worker.ts:11` — withSentry は fetch handler のみラップ)まで記述。`scrubSentryEvent`(`sentry.ts:99`)/ `shouldCaptureApiException`(`:83`)の純関数抽出・共有も両者の実体(SDK 非依存ロジック)と一致。DI サーフェスの `errorHandler.ts:15-16,38-39` への絞り込みは現利用面と正確に一致(実線確認済み)。ゼロ質問根拠(OQ-2 を本ステージで解決)と成果物の不整合は解消。 +- **[resolved: major] SR-6 全 3 対象の収載。** security-design 新設 SR-6 節・scalability-design SC-2 節・LC-6 対象一覧の 3 箇所が同一の 3 対象を列挙し相互整合。挙げられた各用途はコード実体と一致: `incrWithTtl`(`rateLimit.ts:117`、invite/inviteLinks)、`getdel` ワンタイムコード原子消費(`mcpAuth.ts:94`、`extAuth.ts:60`)、`get`/`setex` MCP 失効 deny-list(`mcpAuth.ts:235`)。全 op は KvStore インターフェース(`lib/kv/types.ts:17,23,32`)と `DurableObjectKvStore`(`doKvStore.ts:23,28,34`)に実在。 +- **[resolved: minor] LC-1 の動的 import 代替案は削除され、モジュールレベル境界(`appAgents.ts` 分離 + `registerAgentRoutes` フック、`index.ts` のみが import)が唯一の機構となった。wrangler の esbuild 単一ファイルバンドルが動的 import も取り込むという根拠も正しく記載。 +- **[resolved: minor] TS-5/FR-4 の presign 検証は LC-6 の workerd テスト対象一覧に収載(「検証の置き場はここ」と明示)。FR-5.2 の全対象 + SR-6 + TS-5 で一覧は網羅的。 +- **[resolved: minor] 虚偽引用 `pdfSources.ts:14` は削除され、除外集合の根拠は grep 実結果(4 ファイルのみ)の記述に修正された — 再検証と一致。 +- **[verified] 新規の不整合なし。** performance-design / reliability-design は不変のまま LC-1〜6 改訂と矛盾しない(PR-3 の「モジュールレベル境界」参照・RL-4 の @sentry/cloudflare 捕捉は改訂後の withSentry 設計とそのまま整合)。iteration 1 で確認済みの file:line 主張(app.ts :47/:49-53/:69-73/:168/:192、clientIp.ts :18/:28/:81/:83-88、errorHandler.ts :4/:21、sentry.ts :50-52/:57-59/:83/:99、worker.ts :11/:13、vitest 4.1.10 + pool-workers 未導入、health.ts :22-27、deploy-api-worker-dev.yml :51-59)は改訂後も全て有効。SR-5 の明示的ランタイムシグナルによる `CF-Connecting-IP` 信頼ゲート(ヘッダ存在判定の禁止)も不変で健全。 +- **[verified] 要件トレーサビリティ充足。** PR-1〜6 / SR-1〜7 / SC-1〜4 / RL-1〜5 / TS-1〜9 の全てが設計要素への対応または理由付き deferral(SC-3/4、PR-2、RL-2 等)を持つ。OQ-1/OQ-2/OQ-3/OQ-5 は LC-1/LC-4/LC-6/LC-3 でコード事実により解決、OQ-4(資格情報復旧待ち)は C-2 整合の deferral として妥当。開発者が設計者への追加質問なしに実装可能な水準に達している。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/nfr-design-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/nfr-design-questions.md new file mode 100644 index 00000000..f9789a4b --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/nfr-design-questions.md @@ -0,0 +1,21 @@ +# NFR Design — Questions (unit: api-worker) + +質問なし(0 件)。 + +Construction 段階の質問は「真のギャップがある場合のみ」(stage-protocol §3 depth guidance)。本ステージの設計判断は上流で全て確定済み: + +- レジリエンス/信頼性方針 → reliability-requirements.md(RL-1〜5: SLA 不要、Railway 並行稼働が保険) +- スケーラビリティパターン → scalability-requirements.md(SC-1〜4: エッジ自動スケール + 論理キー毎 DO) +- パフォーマンス方針 → performance-requirements.md(PR-1〜6: パリティ、10MB/Paid) +- セキュリティ方針 → security-requirements.md(SR-1〜7)+ 既存実装踏襲 +- コンポーネント境界 → tech-stack-decisions.md(TS-1〜9)+ requirements.md FR-1〜6 + +残る未確定事項(OQ-1 ルート分割境界、OQ-2 Sentry 統合方式、OQ-3 vitest 併存構成、OQ-5 clientIp 利用箇所)は人間への質問ではなく、コード事実の調査で確定する設計作業として本ステージで解決する。 + +--- + +## G1. Approval gate (typed turn required) + +NFR Design 5成果物(READY)の承認ゲート。チャットに approve / request changes をタイプしてください。 + +[Answer]: approve (typed, 2026-07-18) diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/performance-design.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/performance-design.md new file mode 100644 index 00000000..06928716 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/performance-design.md @@ -0,0 +1,18 @@ +# Performance Design — api-worker + +前提: performance-requirements.md(PR-1〜6)を実現する設計。business-logic-model.md は infra スコープにより未作成。 + +## バンドル予算の達成設計(PR-3) + +- LangGraph 除外はモジュールレベルの境界(logical-components.md LC-1)で達成する。フラグによる実行時分岐ではなく、Worker エントリから agent 系モジュールへの静的到達経路を断つ。 +- 検査は LC-5 のバンドル検査スクリプトで自動化し、gzip 後サイズを 10MB 上限(Paid、PR-3)に対して報告する。 + +## キャッシュ・最適化(パリティ方針) + +- 新規キャッシュ層・クエリ最適化・接続プーリング変更は**行わない**(PR-1 パリティ方針。既存の KvStore キャッシュ用途は #1093 設計のまま)。 +- cold start への対策は本 Issue では講じない(PR-2: 記録のみ)。LangGraph 除外によるバンドル縮小が事実上の主要な cold start 改善になる。 + +## 検証設計(PR-5, PR-6) + +- CI 自動: `deploy-api-worker-dev.yml` の health-poll(HTTP 成功判定)を不変で通過させる。 +- 手動 dev 検証: /api/health の `runtime` フィールド確認、主要非 DB 経路の体感パリティ確認、CPU 超過エラー(exceeded CPU)の不在確認(PR-4)。チェックリストは build-and-test 段階で作成する(レビュー指摘の送り事項)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/reliability-design.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/reliability-design.md new file mode 100644 index 00000000..230a1e3a --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/reliability-design.md @@ -0,0 +1,23 @@ +# Reliability Design — api-worker + +前提: reliability-requirements.md(RL-1〜5)を実現する設計。business-logic-model.md は infra スコープにより未作成。 + +## フェイルセーフ構成(RL-1, RL-2) + +- サーキットブレーカ・リトライ・フェイルオーバは**追加しない**。Railway 並行稼働(`src/index.ts` 無変更)がそのままフォールバックであり、dev Worker の障害はユーザー影響を持たない。 +- ロールバック手順 = 不要(dev Worker は検証専用。prod 切替 PR で設計する)。 + +## 障害時挙動の整形(RL-3) + +- DB 依存ルートの失敗(#1090 まで不可避)は、`app.onError` → `errorHandler.ts` の経路で捕捉されマスク済み 5xx になることを workerd テストでアサートする。判定基準: Workers の 1101(uncaught exception)ではなく HTTP 500 + `{ error: "Internal server error" }` が返ること。 +- `getPool`/`getDb` の lazy 初期化(`db/client.ts`)により、DB 未接続でも import 時・起動時にはクラッシュしないことを worker 起動テストで確認する。 + +## エラー可観測性(RL-4) + +- `@sentry/cloudflare`(LC-4 DI)で dev Worker の例外を捕捉。DSN 未設定時は no-op(`SENTRY_DSN_API` 契約は現行 `sentry.ts:50` と同一)。 +- Cloudflare 側 observability(`wrangler tail` / MCP)は資格情報復旧後の手動検証手段として位置づける。 + +## デプロイ健全性(RL-5) + +- CI 自動: `deploy-api-worker-dev.yml` の `curl --fail` health-poll(HTTP 成功判定のみ)維持。 +- 手動 dev 検証: `runtime: "cloudflare-workers"` フィールドの確認(FR-6.2 帰属)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/scalability-design.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/scalability-design.md new file mode 100644 index 00000000..747003ab --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/scalability-design.md @@ -0,0 +1,18 @@ +# Scalability Design — api-worker + +前提: scalability-requirements.md(SC-1〜4)を実現する設計。business-logic-model.md は infra スコープにより未作成。 + +## ステートレス設計の維持(SC-1) + +- Worker インスタンスに状態を持たない現行構成(状態は DB / KvStore / R2 に外部化済み)を維持する。本 Issue で新たな in-memory 状態を導入しない。 +- ルート分割(LC-1)は登録の有無のみでスケーリング特性に影響しない。 + +## レート制限の分散設計(SC-2) + +- `DurableObjectKvStore`(`src/lib/kv/doKvStore.ts`、`idFromName(key)` で論理キー毎に 1 DO)を無変更で使用。IP/ユーザー単位のキー分割により単一 DO へのホットスポットを避ける既存設計を踏襲。 +- workerd テストで KvStore の 3 用途すべてが DO 経由で機能することを確認(SR-6 全数、FR-5.2): `incrWithTtl`(レート制限固定ウィンドウカウンタ)、`getdel`(ext/MCP ワンタイムコードの原子消費)、`get`/`setex`(MCP 失効 deny-list)。 + +## スコープ外の明示(SC-3, SC-4) + +- 負荷テスト・キャパシティ閾値・auto-scaling ルールは設定しない(Workers に該当概念がなく、prod 切替判断時に評価)。 +- データ層のシャーディング設計は #1090。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/security-design.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/security-design.md new file mode 100644 index 00000000..21505d81 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-design/security-design.md @@ -0,0 +1,28 @@ +# Security Design — api-worker + +前提: security-requirements.md(SR-1〜7)を実現する設計。business-logic-model.md は infra スコープにより未作成。 + +## 認証境界(SR-1, SR-2) + +- Better Auth のマウント(共有 `createApp()` 内)は無変更。Worker 化で構成差を作らない。 +- `BETTER_AUTH_SECRET` は `wrangler secret bulk`(`putWorkerSecrets.ts`)供給のみ。コード・設定ファイルに値を置かない。 +- workerd テストで `/api/auth/*` がランタイム例外なく整形済みレスポンス(DB 未接続時はマスク済み 5xx を含む)を返すことを確認(FR-5.2)。 + +## クライアント IP の信頼設計(SR-5) + +- `CF-Connecting-IP` は **Workers ランタイムと確定した場合のみ**信頼する(logical-components.md LC-3)。判定は明示的シグナル(binding / `RUNTIME` var)で行い、ヘッダ存在による判定はしない — Node/Railway 経路で攻撃者が `CF-Connecting-IP` を自称してレート制限キーを偽装する攻撃を排除する。 +- Node 側の TRUST_PROXY セマンティクス(`clientIp.ts:28`)は不変。 + +## エラーマスキング・Sentry(SR-3, SR-4) + +- `{ error: string }` 封筒(`errorHandler.ts:21`)と 5xx マスキングは両ランタイム共通の errorHandler をそのまま使う。 +- Sentry capture の DI 化(LC-4)でも スクラブ挙動(`scrubSentryEvent`、`sendDefaultPii: false`、`beforeSend`)を `@sentry/cloudflare` 側に同一実装する。PII をイベントに含めない基準は現行 `sentry.ts:99` を正とする。 +- バンドル検査(LC-5)で secret 値の混入がないことも grep 対象に含める。 + +## KvStore セキュリティ用途の検証(SR-6) + +- SR-6 の 3 対象 — レート制限カウンタ(`incrWithTtl`)、ext/MCP ワンタイムコード(`getdel` 原子消費)、MCP 失効 deny-list(`get`/`setex`)— を workerd テスト対象に全数含める(logical-components.md LC-6 の対象一覧に収載)。 + +## CORS(SR-7) + +- `src/lib/cors.ts` は runtime-agnostic のため無変更。workerd テストでプリフライトの許可オリジンが Node 実行時と同一であることをアサートする。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/nfr-requirements-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/nfr-requirements-questions.md new file mode 100644 index 00000000..d98c14e4 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/nfr-requirements-questions.md @@ -0,0 +1,32 @@ +# NFR Requirements — Questions (unit: api-worker) + +Construction 段階のため質問は真のギャップのみ(requirements.md で大半の NFR 方針は確定済み)。 + +## Q1. Cloudflare Workers のプラン + +NFR-1 のバンドル上限(有料 10MB / 無料 3MB)と CPU 時間予算(Issue は「有料プランで最大 5 分」と記載)の判定基準になります。zedi の Cloudflare アカウントはどちらですか? + +A. 有料(Workers Paid)— バンドル 10MB / CPU 上限拡張可 +B. 無料(Free)— バンドル 3MB / CPU 10ms +C. 現在無料だが移行までに有料化する予定 +X. Other (please specify) + +[Answer]: A. 有料(Workers Paid)— バンドル 10MB / CPU 上限拡張可 (2026-07-18, mode: guided) + +## Q2. パフォーマンス要件の立て方 + +本 Issue は dev 検証までがスコープです。パフォーマンス目標はどう扱いますか? + +A. Railway 現行との体感パリティで十分 — 新たな数値目標は設けない(cold start 等の Workers 特性は記録のみ) +B. 明示的な数値目標を設定したい(p95 レイテンシ等を指定してください) +X. Other (please specify) + +[Answer]: A. Railway 現行との体感パリティで十分 — 新たな数値目標は設けない(cold start 等の Workers 特性は記録のみ) (2026-07-18, mode: guided) + +--- + +## G1. Approval gate (typed turn required) + +NFR 5成果物(READY)の承認ゲート。チャットに approve / request changes をタイプしてください。 + +[Answer]: approve (typed, 2026-07-18) diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/performance-requirements.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/performance-requirements.md new file mode 100644 index 00000000..b3614046 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/performance-requirements.md @@ -0,0 +1,18 @@ +# Performance Requirements — api-worker + +前提: functional-design(business-logic-model.md / business-rules.md)は infra スコープにより未作成。本書は `inception/requirements-analysis/requirements.md`(FR-1〜FR-6、NFR-1〜4)と Q&A(Q1=Paid、Q2=パリティ)から導出する。 + +## 目標の立て方(パリティ方針) + +- PR-1: 新規の数値目標は設けない。dev Worker の応答は Railway 現行 API との**体感パリティ**を基準とする(Q2=A)。 +- PR-2: Workers 特有の特性(cold start、リージョンエッジ実行)は dev 検証時に記録のみ行い、改善タスク化は本 Issue の外とする。 + +## リソース予算 + +- PR-3: バンドルサイズ: gzip 後 10 MB 以内(Workers Paid プラン上限。Q1=A)。LangGraph 除外(FR-1)が主たる達成手段。 +- PR-4: CPU 時間: Paid プランの既定内で動作すること。長時間処理(LangGraph)はルート分割で Worker に載らない。残経路については dev 検証で CPU 超過エラー(exceeded CPU)が観測されないことを確認する。 +- PR-5: `deploy-api-worker-dev.yml` の health-poll(/api/health)が既存のタイムアウト設定内で成功すること(FR-6.1)。 + +## 検証方法 + +- PR-6: workerd 実行テスト(FR-5)はレイテンシ計測を目的としない。性能はパリティ確認(dev 実機での手動確認)のみ。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/reliability-requirements.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/reliability-requirements.md new file mode 100644 index 00000000..a0d660fc --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/reliability-requirements.md @@ -0,0 +1,17 @@ +# Reliability Requirements — api-worker + +前提: business-logic-model.md / business-rules.md は infra スコープにより未作成。requirements.md(A-3、FR-6、Out of Scope)から導出する。 + +## 可用性 + +- RL-1: SLA/SLO は設定しない。本番トラフィックは Railway が引き続き担い(A-3)、dev Worker は検証専用のため可用性保証は不要。 +- RL-2: ロールバック戦略 = 「何もしない」。Railway 並行稼働が生きている限り、dev Worker の障害はユーザー影響ゼロ。prod 切替時のロールバック設計は切替 PR(Out of Scope)で行う。 + +## 障害時挙動 + +- RL-3: DB 依存ルートは #1090 まで dev Worker 上で失敗するが、その失敗は SR-3 のマスク済み 5xx(`{ error: string }`)として整形されること — Worker ランタイム例外(unhandled rejection / 1101 エラー)にしないこと(FR-5.2 / FR-6.2 の判定基準)。 +- RL-4: Sentry(@sentry/cloudflare、FR-3)で dev Worker のエラーを捕捉し、DSN 未設定時は no-op で落ちないこと。 + +## デプロイ健全性 + +- RL-5: `deploy-api-worker-dev.yml` の health-poll(/api/health の HTTP 成功判定 = CI 自動)をデプロイ成功条件として維持する(FR-6.1)。レスポンス中の `runtime` フィールド確認は CI ではなく FR-6.2 の手動 dev 検証に属する。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/scalability-requirements.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/scalability-requirements.md new file mode 100644 index 00000000..e8195779 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/scalability-requirements.md @@ -0,0 +1,13 @@ +# Scalability Requirements — api-worker + +前提: business-logic-model.md / business-rules.md は infra スコープにより未作成。requirements.md と Workers プラットフォーム特性から導出する。 + +## スケーリングモデル + +- SC-1: HTTP 処理は Workers のエッジ自動スケールに委ねる。キャパシティプランニングは不要(サーバプロビジョニング概念が無いため)。 +- SC-2: レート制限は論理キーごとに 1 つの Durable Object(`DurableObjectKvStore`、#1093 設計)。DO は単一スレッドだが、キー分割(IP/ユーザー単位)により集中ボトルネックにならない設計を維持する。 + +## スコープ上の制限 + +- SC-3: 本 Issue は dev 検証まで(Q1=A/DoD)。本番トラフィック規模での負荷検証・growth projection は prod 切替判断時(Out of Scope)に実施する。 +- SC-4: DB 層のスケーラビリティは #1090(D1 + ノート単位 DO + ユーザーシャード)の設計事項であり、本書では扱わない。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/security-requirements.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/security-requirements.md new file mode 100644 index 00000000..d7594e1c --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/security-requirements.md @@ -0,0 +1,22 @@ +# Security Requirements — api-worker + +前提: business-logic-model.md / business-rules.md は infra スコープにより未作成。requirements.md の NFR-3・FR-2 と既存実装のセキュリティ態勢(practices-discovery evidence)から導出する。 + +## 認証・認可 + +- SR-1: Better Auth の Cookie ベース認証・CSRF 保護は共有 `createApp()` 経由で Worker でも同一構成でマウントされること(FR-5.2 のランタイム適合確認まで。DB 依存成功系は #1090 後)。 +- SR-2: `BETTER_AUTH_SECRET` は API/MCP 間で同値共有(既存制約)。Worker への供給は `wrangler secret bulk` のみ。 + +## データ保護・エラーマスキング + +- SR-3: 5xx はグローバル `onError` の `{ error: string }` 封筒で内部情報(DB エラー文字列等)を漏らさない(NFR-3、既存挙動維持)。 +- SR-4: secrets はリポジトリ非コミット(`.env.worker.*` は gitignore、`putWorkerSecrets.ts` で `wrangler secret bulk`)。dry-run バンドル検査(FR-1.2)の出力にも secret 値が含まれないこと。 + +## クライアント IP・レート制限 + +- SR-5: Workers では `cf-connecting-ip`(Cloudflare が付与し偽装不可)を第一候補とし、x-forwarded-for 偽装によるレート制限回避を防ぐ(FR-2)。 +- SR-6: レート制限カウンタ・ワンタイムコード・deny-list は KvStore 抽象(DurableObjectKvStore)で Worker 上も機能すること(A-4、workerd テスト対象)。 + +## CORS + +- SR-7: CORS 設定(`src/lib/cors.ts`)は両ランタイムで同一の許可オリジンを返すこと。Worker 化による許可先の拡張はしない。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/tech-stack-decisions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/tech-stack-decisions.md new file mode 100644 index 00000000..8b094ef4 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/api-worker/nfr-requirements/tech-stack-decisions.md @@ -0,0 +1,44 @@ +# Tech Stack Decisions — api-worker + +前提: business-logic-model.md / business-rules.md は infra スコープにより未作成。requirements.md の Q&A 確定事項を技術選定として固定する。 + +## 決定事項 + +| # | 領域 | 決定 | 根拠 | +|---|------|------|------| +| TS-1 | ランタイム | Cloudflare Workers(`nodejs_compat`)、Paid プラン | Q1=A。wrangler.jsonc が構成の正本(C-3) | +| TS-2 | フレームワーク | Hono(既存共有 `createApp()` を維持、`includeAgentRoutes` 相当のオプション追加のみ) | FR-1。Workers ネイティブ対応 | +| TS-3 | DB 接続 | **導入しない**(Hyperdrive/HTTP ドライバとも不採用)。D1 へ直行(#1090) | Q2=X(requirements)。二度手間の暫定実装を避ける | +| TS-4 | KV/レート制限 | KvStore 抽象 + `DurableObjectKvStore`(導入済み #1093/#1118) | A-4。追加選定不要 | +| TS-5 | ストレージ | R2 binding + `@aws-sdk` presign 維持(動作確認のみ) | Q6=A。変更最小 | +| TS-6 | エラー監視 | `@sentry/cloudflare`(Worker)+ `@sentry/node`(Railway、現状維持) | Q4=A。FR-3 | +| TS-7 | クライアント IP | `cf-connecting-ip` 第一候補の分岐を `src/lib/clientIp.ts` に追加 | Q5=A。FR-2 | +| TS-8 | テスト基盤 | `@cloudflare/vitest-pool-workers`(workerd 実行)を既存 vitest と併存構成で導入 | Q7=A。FR-5、OQ-3 | +| TS-9 | CI/CD | 既存 `deploy-api-worker-dev.yml` 維持。prod ワークフローは作らない | Q8=B | + +## 実装時の確定事項(2026-07-18 追記) + +- TS-8 確定: `@cloudflare/vitest-pool-workers@0.18.6` は vitest ^4.1.0 対応で互換リスク解消。フォールバック不要。vitest 4 向けの `cloudflareTest` プラグイン API(`vitest.config.worker.ts`)で導入。 +- 境界の追加確定(設計原則の適用拡張): `ioredis` は `createKvStoreNode.ts`、`jsdom` は遅延 import 化により Worker のモジュールロードから除外。`worker:bundle:check` の禁止マーカーは 5 件(@langchain / src/agents / @hono/node-server / @sentry/node / ioredis)。 + +## 却下した選択肢 + +- Hyperdrive + pg / neon serverless: D1 ビッグバン移行(#1090)で捨てる暫定層になるため不採用(TS-3)。 +- LangGraph の Worker 同梱(フラグ無効化・専用 Worker 化含む): バンドル上限・workerd 互換リスク・#1095 との重複のため不採用。ルート分割(FR-1)を採用。 +- `aws4fetch` への presign 置換: 動作すれば不要な書き換え。動作確認で問題が出た場合のみ再検討(TS-5)。 + +## Review + +**Verdict**: READY +**Reviewer**: aidlc-architecture-reviewer-agent +**Date**: 2026-07-18 + +### Findings + +(アドバーサリアル・レビュー。5 成果物 + Q&A + requirements.md を対象に、リポジトリ実体との照合を実施。blocker / major なし。) + +- **[minor] RL-5 が health-poll の検証内容を過大に記述。** `deploy-api-worker-dev.yml`(L51-59)の実体は `curl --fail` による HTTP 成功判定のみで、レスポンス中の `runtime` フィールド(ランタイム判定)はアサートしない。また同ステップは `vars.WORKER_API_BASE_URL` 未設定時にスキップされる(`if:` ガード)。RL-5 の「200 + ランタイム判定をデプロイ成功条件として維持」は、ランタイム判定部分を FR-6.2 の手動 dev 検証(PR-5 は正しく HTTP 通過のみと記述)と区別して読む必要がある。nfr-design で判定基準の帰属(CI 自動 vs 手動検証)を 1 行明確化すれば足りる。 +- **[minor] PR-1「体感パリティ」の判定手続きが未定義。** Q2=A(数値目標なし)への忠実な反映であり方針としては正当だが、dev 実機での「パリティ確認(手動)」(PR-6)は誰が・どの経路で判定するかが書かれておらず、C-2(資格情報ブロック)解消まで実施もできない。requirements.md 自体が DoD 実機確認の後日実施を認めているため blocker ではないが、検証チェックリスト化は build-and-test 段階で必要。 +- **[minor] PR-4「残る経路に CPU 制限の懸念はない」は無根拠の断定。** LangGraph 除外(FR-1)後の残経路(presign、検索等)の CPU プロファイルは未計測。Paid プラン既定値が十分広いため実害リスクは低いが、「懸念はない」ではなく「dev 検証で CPU 超過エラー(exceeded CPU)が観測されないことを確認する」と検証可能な形が望ましい。 +- **[verified] リポジトリ実体との照合はすべて一致。** `server/api/src/lib/clientIp.ts`(現状 Node 専用、TS-7 の「追加」記述と整合)、`server/api/src/lib/cors.ts`、`server/api/src/lib/kv/doKvStore.ts`(`idFromName(key)` による論理キー毎 DO — SC-2 の記述と一致)、`server/api/scripts/putWorkerSecrets.ts`、ルート `.gitignore` L27-28(`.env.worker.*` 非コミット — SR-4 と一致)、`server/api/src/middleware/errorHandler.ts` L21(`{ error: err.message }` 封筒 — SR-3/RL-3 と一致)、`server/api/src/routes/health.ts` の `runtime` フィールド、`.github/workflows/deploy-api-worker-dev.yml` を確認。 +- **[verified] Q&A 忠実性・スコープ整合。** Q1=Paid → PR-3 gzip 10MB / TS-1 と一致。Q2=パリティ → 全 5 成果物に数値レイテンシ目標なし、RL-1 の SLA/SLO 不設定・SC-3 の負荷検証送りとも矛盾なし。TS-3(DB 不導入)= A-1/Q2=X、TS-9(prod ワークフロー不作成)= Q8=B/Out of Scope と一致。スコープの拡大・縮小なし。全成果物が requirements / business-logic-model / business-rules への参照(infra スコープによる不在の明示含む)を持ち、H2 見出し 2 以上の必須構成を満たす。5 成果物間の予算・主張の矛盾なし。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/ci-config.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/ci-config.md new file mode 100644 index 00000000..9790127b --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/ci-config.md @@ -0,0 +1,27 @@ +# CI Config — api-worker (#1091) + +前提: infrastructure-design/cicd-pipeline.md の設計を具体的なワークフロー変更に落とす。code-generation の code-summary.md / build-and-test 成果物は infra スコープにより不在(expected)— 実装 PR 時にこの設計を適用する。 + +## 変更対象と内容 + +### `ci.yml`(品質ゲート — 追加のみ、既存ジョブ不変) + +- `api-typecheck` ジョブ(L265 付近)に 1 ステップ追加: `bun run worker:bundle:check`(LC-5 dry-run 検査。資格情報不要のため PR でも実行可能)。 +- `api-test` ジョブ(L289 付近)に 1 ステップ追加: `bun run test:worker`(workerd テスト。vitest-pool-workers 互換が取れない場合のフォールバックは code 実装時に確定 — LC-6)。 + +### `deploy-api-worker-dev.yml`(デプロイ — ステップ追加のみ) + +- `wrangler deploy --env dev` の**直前**に `bun run worker:bundle:check` ステップを追加(ci.yml と並列起動のため、デプロイ前実行の機械保証はここ。cicd-pipeline.md の設計どおり)。 +- health-poll・トリガ・paths フィルタは不変。 + +### `server/api/package.json`(scripts 追加) + +- `worker:bundle:check`: dry-run バンドル検査スクリプトの起動。 +- `test:worker`: workerd テスト実行(`vitest run -c vitest.config.worker.ts` 相当)。 + +## 変更しないもの + +- prod 用デプロイワークフロー(作らない — Q8=B)。 +- `mutation-light` / `nightly-mutation`(server/api は Stryker 対象外 — team-practices)。 +- drizzle 系チェック(DB スキーマ変更なし — C-4)。 +- Terraform 系ワークフロー(凍結 — C-3)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/ci-pipeline-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/ci-pipeline-questions.md new file mode 100644 index 00000000..dfef5f8e --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/ci-pipeline-questions.md @@ -0,0 +1,5 @@ +# CI Pipeline — Questions + +質問なし(0 件)。 + +CI 方針は上流で確定済み: 品質ゲートは GitHub Actions 維持・デプロイは wrangler(cloudflare-zedi スキルのハイブリッド方針)、追加する検査は infrastructure-design/cicd-pipeline.md(bundle check の二重配置、test:worker)で設計済み、prod ワークフロー不作成(Q8=B)。code-generation/code-summary.md と build-and-test 成果物は infra スコープにより不在(expected)— CI 設計は既存の `ci.yml` / `deploy-api-worker-dev.yml` 実体と cicd-pipeline.md を正とする。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/memory.md new file mode 100644 index 00000000..7575af00 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/memory.md @@ -0,0 +1,15 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + + +## Deviations + + +## Tradeoffs + + +## Open questions + +- 2026-07-18T08:10:00Z — the infra scope excludes code-generation and build-and-test, so this workflow ends with designs + CI config but no implementation of FR-1..6; the user should decide whether to run code-generation as a single-stage run (/aidlc --stage code-generation --single) afterwards or implement via a normal TDD dev flow. diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/quality-gates.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/quality-gates.md new file mode 100644 index 00000000..a5b99410 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/ci-pipeline/quality-gates.md @@ -0,0 +1,24 @@ +# Quality Gates — api-worker (#1091) + +前提: ci-config.md のワークフロー変更が守るべきゲート基準。code-generation / build-and-test 成果物は infra スコープにより不在(expected)。 + +## PR マージ要件(required status checks — GitHub 側構成済み、Q4=A) + +| ゲート | 判定 | 根拠 | +|--------|------|------| +| `api-typecheck` + **bundle check** | `tsc --noEmit` green + 禁止依存(`@langchain` / `src/agents` / `@hono/node-server` / `@sentry/node`)が Worker バンドルに不在 | FR-1.2 / FR-2.3 / LC-4 | +| `api-test` + **test:worker** | 既存 vitest green(NFR-2: Node 側パリティ)+ workerd テスト green(FR-5.2 の対象全数) | TDD 必守(team-practices) | +| 既存 lint / knip / e2e 等 | 不変 | — | + +## デプロイゲート(dev) + +| ゲート | 判定 | +|--------|------| +| deploy 直前 bundle check | ci.yml と同一スクリプト。fail で deploy 中止 | +| health-poll | `curl --fail`(HTTP 成功判定、CI 自動)。`runtime` フィールド確認は手動 dev 検証(RL-5 帰属) | + +## ゲートにしないもの(明示) + +- カバレッジ閾値の機械強制(Q5=A: 80% は目標値。Mutation は server/api 対象外)。 +- パフォーマンス数値(Q2=A: パリティ方針、PR-1)。 +- prod 昇格ゲート(Out of Scope)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/infrastructure-design/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/infrastructure-design/memory.md new file mode 100644 index 00000000..8efdb56f --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/infrastructure-design/memory.md @@ -0,0 +1,16 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + +- 2026-07-18T07:50:00Z — zero-question stage again: no new resources are provisioned (existing R2/DO/Workers reused), so no human decisions remained; wrangler.jsonc was read directly to fix real env facts (ENVIRONMENT var exists, RUNTIME var to be added as the runtime signal). +- 2026-07-18T07:50:00Z — chose a separate RUNTIME var over reusing ENVIRONMENT for the Workers runtime signal: ENVIRONMENT distinguishes dev/prod and exists on Railway too, so it cannot discriminate runtime. + +## Deviations + + +## Tradeoffs + + +## Open questions + diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/nfr-design/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/nfr-design/memory.md new file mode 100644 index 00000000..84e548f1 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/nfr-design/memory.md @@ -0,0 +1,17 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + +- 2026-07-18T06:45:00Z — Step 3-4 interview skipped with a zero-question file: every focus area is already pinned by nfr-requirements artifacts, and the remaining OQs (route boundary, Sentry method, vitest coexistence, clientIp call sites) are code-fact investigations, not human questions; resolved them via a read-only repo scan instead. + +## Deviations + + +## Tradeoffs + +- 2026-07-18T07:30:00Z — bundle exclusion designed as module-level boundaries (appAgents.ts / clientIpNode.ts / sentryWorker.ts + entry-side wiring) over runtime flags or dynamic imports; wrangler's esbuild single-file bundle pulls in dynamic imports too, so only module boundaries survive the LC-5 absence check. +- 2026-07-18T07:30:00Z — chose withSentry wrapper over plain captureException for the Worker entry: the wrapper guarantees init + ctx.waitUntil flush; the DI surface for errorHandler stays the same 2 functions on both runtimes. + +## Open questions + diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/construction/nfr-requirements/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/nfr-requirements/memory.md new file mode 100644 index 00000000..fda17deb --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/construction/nfr-requirements/memory.md @@ -0,0 +1,18 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + +- 2026-07-18T05:40:00Z — no compiled unit list exists (infra scope skips units-generation-driven bolt planning here), so the stage ran single-unit with {unit-name}=api-worker; artifacts live under construction/api-worker/nfr-requirements/. +- 2026-07-18T05:40:00Z — functional-design artifacts (business-logic-model.md, business-rules.md) absent by infra-scope design; NFR context derived from requirements.md per the stage file's documented fallback. +- 2026-07-18T05:40:00Z — performance stance set to parity-with-Railway (no numeric targets) per user answer; Workers plan confirmed Paid, fixing NFR-1 bundle budget at 10MB gzip. + +## Deviations + + +## Tradeoffs + + +## Open questions + +- 2026-07-18T06:05:00Z — reviewer minor: PR-1 "perceived parity" needs a concrete verification checklist at build-and-test; blocked until Cloudflare credentials (C-2) are restored. diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/discovered-rules.md b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/discovered-rules.md new file mode 100644 index 00000000..5fe96710 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/discovered-rules.md @@ -0,0 +1,22 @@ +# Discovered Rules — Zedi + +## Mandated + +- ALWAYS follow TDD Red → Green → Refactor: write a failing test before any implementation change. +- ALWAYS start Construction with a walking skeleton — the thinnest end-to-end slice proving every integration point — before building out the rest. +- ALWAYS pair `server/api/src/schema/**/*.ts` changes with a `server/api/drizzle/NNNN_*.sql` migration and a `drizzle/meta/_journal.json` entry in the same PR. +- ALWAYS use a merge commit (never squash) when syncing `main` → `develop`. +- ALWAYS update `packages/shared` and the corresponding `server/*` duplicated constants together; the drift test must stay green. +- ALWAYS treat each service's `wrangler.jsonc` as the source of truth for Cloudflare configuration (not Terraform). +- ALWAYS use Conventional Commits for PR titles. +- ALWAYS manage secrets via `wrangler secret bulk` (`server/api/scripts/putWorkerSecrets.ts`) or GitHub Actions encrypted secrets. + +## Forbidden + +- NEVER add colocated test files in `server/api` or `server/mcp` — tests mirror the source tree under `src/__tests__/`. +- NEVER write "写し絵テスト" (tests copied from implementation output) or rewrite expectations to match a failing implementation without justification. +- NEVER use kebab-case for domain directories (exception: `packages/ui/src/components/` shadcn files). +- NEVER place non-hook modules in `src/hooks/` or hooks in `src/pages/`. +- NEVER let 5xx responses leak raw internal/DB error messages — all errors flow through the global `onError` `{ error: string }` envelope. +- NEVER commit secret values (`.env*` real files stay gitignored; `.gitleaksignore` tracks false positives only). +- NEVER apply SQL migrations outside `server/api/drizzle/` — other SQL locations do not reach production. diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/evidence.md b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/evidence.md new file mode 100644 index 00000000..9291681f --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/evidence.md @@ -0,0 +1,37 @@ +# Evidence — Practices Discovery (2026-07-18) + +Brownfield scan: 4 parallel read-only agents. Reverse-engineering artifacts were not consumed (infra scope skips that stage); agents scanned the repo directly. Interview covered only the 5 gaps evidence could not resolve (`practices-discovery-questions.md`). + +## aidlc-pipeline-deploy-agent (lead) — branching / CI / deployment + +- **Scanned**: `git log` / `git branch -a`, `.github/workflows/*` (ci, deploy-dev, deploy-prod, sync-main-to-develop, release-please, nightly-mutation ほか), `server/*/wrangler.jsonc`, AGENTS.md. +- **Inferred**: GitFlow 系 `develop`+`main`。feature PR は squash、`main`↔`develop` 同期は merge commit 固定(`sync-main-to-develop.yml` が自動化)。`develop`→dev / `main`→prod の push 駆動デプロイ。フロント/admin は Cloudflare Pages、server/* は Railway 暫定稼働で Workers 移行中(#1088、dev Worker のみ先行、prod Worker デプロイ未整備、hocuspocus は wrangler.jsonc 無し)。 +- **Asked**: 本番デプロイの手動承認有無(Q3 → 完全自動)。 + +## aidlc-quality-agent — testing posture + +- **Scanned**: AGENTS.md TDD 節、テストファイル分布(src/ colocated 262 件、server/api `__tests__` ミラー約 146 件)、`stryker.config.mjs`(break:70)、`nightly-mutation.yml`(break:null 観測のみ)、`playwright.config.ts` + `e2e/`(バックエンドレスモック)、`.github/rulesets/main-develop-branch-protection.json`。 +- **Inferred**: TDD 必須ドクトリン、Mutation 第一指標、配置ルールはドキュメントと実態が一致。coverage 80% は機械強制なし。 +- **Asked**: required status checks の実態(Q4 → GitHub 側で構成済み)、coverage の位置づけ(Q5 → 目標値のまま)。 + +## aidlc-developer-agent — code patterns + +- **Scanned**: AGENTS.md 命名/配置規則、`src/hooks`・`src/components`・`server/api/src` ツリー、`middleware/errorHandler.ts`、`routes/lint.ts`、import パターン grep(`@zedi/*` 322 hits、HTTPException 系 450 hits)。 +- **Inferred**: 命名規則にドリフト無し。server/api は routes/services/lib/schema 4 層、例外ベース(HTTPException + グローバル onError、`{ error: string }` 封筒、5xx マスク)。`server/*` は workspace 外で `@zedi/shared` 直 import 不可 → 二重定義 + ドリフトテスト。 +- **Open**: ドリフトテストのカバー範囲の広さ(ドキュメント上の例は 1 件)— 影響軽微のため質問せず記録のみ。 + +## aidlc-devsecops-agent — lint / security / secrets + +- **Scanned**: `eslint.config.js`(flat、ts-eslint strict)、`.prettierrc`、ci.yml security ジョブ(gitleaks SHA ピン、npm-audit 非ブロッキング)、`.github/dependabot.yml`(週次、develop ターゲット)、`dependabot-bun-lock.yml`、`putWorkerSecrets.ts`(`wrangler secret bulk`)、`.env*` の gitignore 状態。 +- **Inferred**: SAST/DAST 無し(gitleaks + npm-audit のみ)。Actions は commit SHA ピン留め。secrets は GitHub encrypted secrets + wrangler secret で、実 env ファイルは未追跡。`BETTER_AUTH_SECRET` は API/MCP 間で値共有(パリティの自動強制なし)。 +- **Open**: dependabot.yml に server/mcp と admin が無いのは意図的か — 影響軽微のため質問せず記録のみ。 + +## Interview (Q1-Q5 summary) + +| Q | Gap | Answer | +|---|-----|--------| +| Q1 | チーム構成 | ソロ + AI エージェント(自己承認 + AI レビュー) | +| Q2 | Walking skeleton | 常に実施 | +| Q3 | 本番承認ゲート | 完全自動 | +| Q4 | required checks | GitHub 側で構成済み | +| Q5 | coverage 80% | 目標値のまま(Mutation 第一指標) | diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/memory.md new file mode 100644 index 00000000..409749ce --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/memory.md @@ -0,0 +1,17 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + +- 2026-07-18T02:02:21Z — infra scope skips reverse-engineering, so brownfield evidence scan runs directly against the repo (AGENTS.md + configs + git history) instead of consuming reverse-engineering artifacts; consumes list was empty as expected. + +## Deviations + +- 2026-07-18T02:10:00Z — aidlc-log.ts answer refused to record ("a real human has not acted at this checkpoint this turn") because answers arrived via the harness AskUserQuestion tool, not a typed message; the questions file retains the full answers as the source of truth, so the QUESTION_ANSWERED audit rows are missing for this stage's interview. +- 2026-07-18T03:45:00Z — Claude Code harness lacked the UserPromptSubmit presence hook, so the typed approve minted no HUMAN_TURN and the gate refused; wired .claude/settings.json to run .codex/hooks/aidlc-mint-presence.ts and manually minted one presence event for the already-typed approve turn before committing the gate. + +## Tradeoffs + + +## Open questions + diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/practices-discovery-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/practices-discovery-questions.md new file mode 100644 index 00000000..69873016 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/practices-discovery-questions.md @@ -0,0 +1,83 @@ +# Practices Discovery — Interview Questions + +Brownfield evidence scan (4 agents: pipeline-deploy, quality, developer, devsecops) resolved most practices from AGENTS.md, CI workflows, and configs. The questions below cover only the gaps evidence could not determine. + +--- + +## Q1. Way of Working — チーム構成とレビュー運用 + +エビデンス: ブランチ保護ルールセットは「PR承認1件」のみ要求。コミット履歴はほぼ otomatty 単独 + Dependabot/Claude/Cursor の自動ブランチ。 + +現在の開発体制に最も近いのはどれですか? + +A. ソロ開発者 + AIエージェント(レビューは自己承認、AIレビュー活用) +B. ソロ開発者だが、重要変更は外部レビュアーに依頼 +C. 小規模チーム(2-5人)で相互レビュー +D. チーム開発(5人以上) +E. その他の体制 +X. Other (please specify) + +[Answer]: A. ソロ開発者 + AIエージェント(レビューは自己承認、AIレビュー活用) (2026-07-18, mode: guided) + +## Q2. Walking Skeleton の方針 + +エビデンス: コードからは判断不可。Construction フェーズの最初の Bolt をウォーキングスケルトン(最小のE2Eスライスで統合点を先に通す)として必ずゲート付きで実行するかを決めます。 + +新規機能・移行作業でウォーキングスケルトンをどう扱いますか? + +A. 常に実施 — 最初に最薄のE2Eスライスを通してから残りを作る +B. 実施しない — 通常のユニット単位で最初から作り込む +C. スコープ依存 — インフラ移行や大きい機能では実施、小さい修正では省略 +D. 今回の移行(#1091)に限っては必ず実施したい +X. Other (please specify) + +[Answer]: A. 常に実施 — 最初に最薄のE2Eスライスを通してから残りを作る (2026-07-18, mode: guided) + +## Q3. Deployment — 本番デプロイの承認ゲート + +エビデンス: `deploy-prod.yml` は `main` push で自動起動。GitHub Environments(production)は参照されているが、Environment protection rules(手動承認)の有無はリポジトリからは見えない。 + +`main` への push 後の本番デプロイはどう運用していますか? + +A. 完全自動 — main にマージされたら承認なしで本番へ +B. Environment protection で手動承認を挟んでいる +C. 現在は自動だが、Workers 移行後は手動承認を入れたい +D. わからない / 確認する +X. Other (please specify) + +[Answer]: A. 完全自動 — main にマージされたら承認なしで本番へ (2026-07-18, mode: guided) + +## Q4. CI Gates — required status checks の実態 + +エビデンス: コミット済みルールセット(`.github/rulesets/main-develop-branch-protection.json`)には required_status_checks が無い。GitHub 側の設定でテストジョブがマージをブロックしているかは不明。 + +CI のテストジョブ(test / api-test / e2e 等)はマージ要件になっていますか? + +A. なっている — GitHub 側設定で required checks を構成済み +B. なっていない — CI は走るが赤でもマージ可能(運用でカバー) +C. なっていない — required checks にすべき(今後設定したい) +D. わからない / 確認する +X. Other (please specify) + +[Answer]: A. なっている — GitHub 側設定で required checks を構成済み (2026-07-18, mode: guided) + +## Q5. Coverage 80% の位置づけ + +エビデンス: AGENTS.md は「行カバレッジ 80% 以上を目標」と記すが、vitest 設定に thresholds は無く機械強制されていない(レポートのみ)。Mutation score が第一指標。 + +80% カバレッジ目標の扱いはどうしますか? + +A. 現状維持 — 目標値であり機械強制はしない(Mutation が第一指標) +B. vitest thresholds で機械強制したい +C. サービスごとに決める(server/api のみ強制 等) +X. Other (please specify) + +[Answer]: A. 現状維持 — 目標値であり機械強制はしない(Mutation が第一指標) (2026-07-18, mode: guided) + +--- + +## G1. Affirmation gate acknowledgment (typed turn required by gate guard) + +AskUserQuestion での承認は記録済みですが、ゲートガードがタイプされたメッセージを要求しています。チャットに approve(または修正指示)を入力してください。 + +[Answer]: approve (typed, 2026-07-18) diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/practices-discovery-timestamp.md b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/practices-discovery-timestamp.md new file mode 100644 index 00000000..1bffe310 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/practices-discovery-timestamp.md @@ -0,0 +1 @@ +Discovered: 2026-07-18T03:30:29Z at commit dc5f9e00f80130ea0549e06fdf076c9bdf7fabe7 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/team-practices.md b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/team-practices.md new file mode 100644 index 00000000..2e7f78a7 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/practices-discovery/team-practices.md @@ -0,0 +1,21 @@ +# Team Practices — Zedi + +## Way of Working + +ソロ開発者(otomatty)+ AI エージェント(Claude Code / Cursor / Codex / Dependabot)の体制。PR ベースで開発し、レビューは自己承認 + AI レビューの組み合わせ(ブランチ保護は PR 承認 1 件)。ブランチは `feature/…`・`fix/…`・`hotfix/…`・`chore/…`(Issue 番号命名可)、PR タイトルは Conventional Commits で release-please の semver を駆動する。feature PR は squash で `develop` に取り込み、`main` ↔ `develop` の同期のみ必ず merge commit(squash 禁止)。 + +## Walking Skeleton + +常に実施する。新規機能・インフラ移行のいずれでも、最初に最薄のエンドツーエンドスライスを通して統合点を先に証明してから残りを作り込む。 + +## Testing Posture + +TDD(Red → Green → Refactor)が必須で、実装後のテスト後付けと「写し絵テスト」は禁止。第一品質指標は Mutation score(Stryker、PR は `mutation-light` break:70、golden list 目標 85%+)で、行カバレッジ 80% は目標値であり機械強制しない。テスト配置は `src/` 等が同居(colocated)、`server/api`・`server/mcp` は `src/__tests__/` ミラー(同居禁止)。E2E は Playwright(バックエンドレス、モック駆動)。CI のテストジョブは GitHub 側の required status checks としてマージ要件になっている。 + +## Deployment + +`develop` → dev 環境、`main` → prod 環境の push 駆動・完全自動デプロイ(手動承認ゲートなし)。フロント/admin は Cloudflare Pages、`server/*` は暫定 Railway で、Cloudflare Workers への移行が進行中(#1088。dev 用 Worker デプロイのみ先行、構成の正本は各 `wrangler.jsonc`、Terraform は廃止予定)。DB マイグレーションは deploy ワークフロー内の `drizzle-kit migrate` のみが本番に効く。本番フロント切替は `/api/health` の commit SHA 一致で gate。 + +## Code Style + +ESLint flat config(typescript-eslint strict)+ Prettier(幅 100、ダブルクォート)。命名はドメインディレクトリ camelCase(kebab-case 禁止、例外は `packages/ui` の shadcn)、コンポーネント PascalCase、フックは `use*` で `src/hooks/` のみ。`server/api` は routes / services / lib / schema の 4 層分離で、エラーは HTTPException + グローバル `onError` の `{ error: string }` 封筒に統一(5xx は内部情報を漏らさない)。`server/*` は workspace 外のため `@zedi/shared` の値をサーバ内に二重定義し、ドリフトテストで文字列一致を検証する。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/inception/requirements-analysis/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/requirements-analysis/memory.md new file mode 100644 index 00000000..e04b2b1c --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/requirements-analysis/memory.md @@ -0,0 +1,17 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + +- 2026-07-18T04:05:00Z — user's Q2 answer "D1 直行のため Postgres 対応不要" contradicted Q1's "dev で主要エンドポイントが通る"; resolved via follow-up: #1091 = Worker runtime適合 + 非DB経路のdev検証、DB依存経路(auth含む)の検証は #1090 後と DoD を精緻化した。 +- 2026-07-18T04:05:00Z — the issue body's scope items for pg/Hyperdrive・LangGraph 本体・prod 切替は、回答により明示的に out-of-scope へ移した(issue 記載と requirements の差分は requirements.md が正)。 + +## Deviations + + +## Tradeoffs + + +## Open questions + +- 2026-07-18T04:05:00Z — Cloudflare 資格情報(正アカウント wrangler / CI トークン)の復旧タイミングが dev 実機検証(FR-6)の前提; 復旧前は wrangler dev + workerd テストで代替する。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/inception/requirements-analysis/requirements-analysis-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/requirements-analysis/requirements-analysis-questions.md new file mode 100644 index 00000000..0633eecd --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/requirements-analysis/requirements-analysis-questions.md @@ -0,0 +1,99 @@ +# Requirements Analysis — Clarifying Questions (Issue #1091) + +前提(コード突合済み): Phase 2a 実装済み — `wrangler.jsonc`(nodejs_compat, R2, DO)、`src/worker.ts` エントリ、KvStore 抽象(#1093/#1118)、storage 抽象(#1089)、`putWorkerSecrets.ts`、`/api/health` ランタイム判定、`deploy-api-worker-dev.yml`。 +未着手/部分: pg ドライバ(Hyperdrive 無し)、LangGraph 未分離、Sentry は @sentry/node のみ、clientIp が Node 依存、aws-sdk presign 残存、Worker ランタイムテスト無し、prod デプロイワークフロー無し。 + +--- + +## Q1. 本 Issue の完了ライン(Definition of Done) + +A. dev Worker で主要エンドポイント + 認証フローが通る(本番 DNS 切替は Phase 2b 後続に委譲) +B. 本番切替まで含む(prod Worker デプロイ + DNS カットオーバー) +C. コード実装 + ローカル `wrangler dev` 検証まで(クラウド側の検証は資格情報復旧後に別途) +D. Issue の検証チェックボックス全達成(dev での E2E + レート制限 + テスト green) +X. Other (please specify) + +[Answer]: A. dev Worker で主要エンドポイント + 認証フローが通る — ただし Q2 追問により精緻化: DB 依存経路の dev 検証は #1090(D1)完了後。#1091 は Worker ランタイム適合 + 非 DB 経路の dev 検証まで (2026-07-18, mode: guided) + +## Q2. Worker からの Postgres 接続方式 + +現状 `src/db/client.ts` は `pg.Pool` + `node-postgres` 固定。Workers では TCP 直結不可のため対応が必要。D1 移行(#1090)は Phase 4。 + +A. Hyperdrive binding を導入し、既存 `pg` を Hyperdrive 経由で使う(Workers 推奨構成) +B. HTTP ベースドライバ(neon serverless 等)へ切替 +C. dev のみ Hyperdrive で検証し、本番 DB 接続の最終形は #1090(D1)に委ねる +D. DB 接続対応はこの Issue のスコープ外に切り出す(別 Issue 化) +X. Other (please specify) + +[Answer]: X. Other — Postgres への Workers 対応(Hyperdrive 等)は不要。D1(#1090)へビッグバン移行するため、この Issue では DB 接続層に触れない。追問回答: DB 依存エンドポイントの検証は #1090 後に実施 (2026-07-18, mode: guided) + +## Q3. LangGraph エージェント実行経路の分離方式(→ #1095 委譲) + +現状 `createApp()` が agent graphs / compose ルートを全て登録し、worker.ts にも含まれる。 + +A. ルート分割 — worker エントリでは compose/agent 系ルートを登録しない(Railway 側で継続稼働、#1095 で Workflows/Queues 化) +B. Worker にも載せるが環境フラグで無効化(501 応答) +C. 分離せず Worker に含める(CPU 上限 5 分で一旦許容) +D. 専用 Worker に切り出す +X. Other (please specify) + +[Answer]: A. ルート分割 — worker エントリでは compose/agent 系ルートを登録しない(議論の上、バンドル除外の確実性と #1095 との整合で A を選択) (2026-07-18, mode: guided) + +## Q4. Sentry の Workers 対応 + +A. `@sentry/cloudflare` を導入し worker.ts で初期化(Node 側は @sentry/node 維持) +B. Workers では Sentry 無しで進め、observability は Cloudflare 側ツールで代替 +C. スコープ外(別 Issue) +X. Other (please specify) + +[Answer]: A. @sentry/cloudflare を導入し worker.ts で初期化(Node 側は @sentry/node 維持) (2026-07-18, mode: guided) + +## Q5. clientIp / TRUST_PROXY の Workers 調整 + +現状 `src/lib/clientIp.ts` のフォールバックが `@hono/node-server/conninfo`(Node 専用)。 + +A. Workers では `cf-connecting-ip` ヘッダを第一候補にする分岐を追加 +B. 既存の x-forwarded-for(TRUST_PROXY)で十分、変更不要 +C. スコープ外 +X. Other (please specify) + +[Answer]: A. Workers では cf-connecting-ip ヘッダを第一候補にする分岐を追加 (2026-07-18, mode: guided) + +## Q6. presigned URL 経路(@aws-sdk 残存) + +R2 binding は導入済みだが、presign は `@aws-sdk/s3-request-presigner` のまま(S3 互換エンドポイント利用)。 + +A. nodejs_compat での @aws-sdk presign 動作確認をして維持(変更最小) +B. `aws4fetch` 等の Workers 軽量実装へ置換(バンドル削減) +C. presign 廃止方向(R2 binding 直接配信へ寄せる。設計は #1089/#1090 と整合確認) +X. Other (please specify) + +[Answer]: A. nodejs_compat での @aws-sdk presign 動作確認をして維持(変更最小) (2026-07-18, mode: guided) + +## Q7. テスト戦略(TDD 必守 / Walking Skeleton 常時実施の前提で) + +A. Worker エントリを対象にしたテストを追加(`@cloudflare/vitest-pool-workers` 等で主要エンドポイント + 認証を workerd 実行) +B. 共有 `createApp()` の既存テストで担保し、Workers 固有部(KV DO / storage / clientIp 分岐)のユニットテストのみ追加 +C. 既存テスト green のみで可(Worker ランタイムテストは追わない) +X. Other (please specify) + +[Answer]: A. Worker エントリを対象にしたテストを追加(@cloudflare/vitest-pool-workers 等で workerd 実行。DB 依存経路はスタブ境界で扱う) (2026-07-18, mode: guided) + +## Q8. prod 向けデプロイワークフローの扱い + +dev 用 `deploy-api-worker-dev.yml` は存在。`worker:deploy:production` スクリプトと wrangler `production` env は定義済み。 + +A. この Issue で prod 用ワークフローも作成する(ただし DNS 切替はしない) +B. dev のみ(prod ワークフローは切替判断時の別 PR) +C. prod 切替まで一気にやる +X. Other (please specify) + +[Answer]: B. dev のみ(prod ワークフローは切替判断時の別 PR) (2026-07-18, mode: guided) + +--- + +## G1. Approval gate (typed turn required) + +requirements.md(READY)の承認ゲート。チャットに approve / request changes / add user stories のいずれかをタイプしてください。 + +[Answer]: approve (typed, 2026-07-18) diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/inception/requirements-analysis/requirements.md b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/requirements-analysis/requirements.md new file mode 100644 index 00000000..a9162d3a --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/inception/requirements-analysis/requirements.md @@ -0,0 +1,98 @@ +# Requirements — Issue #1091: REST API (server/api, Hono) → Cloudflare Workers + +## Intent Analysis + +Railway → Cloudflare 全面移行(Epic #1088、Phase 2)の一環として、`server/api`(Hono REST API)を Cloudflare Workers で実行可能にする。Phase 2a で Worker 骨格(`src/worker.ts`、`wrangler.jsonc`、R2/DO binding、dev deploy CI)は導入済みであり、本 Issue の目的は**残る Node 依存を解消し、Worker ランタイムに適合した API を dev 環境で検証可能にすること**。ゴールは「本番切替」ではなく「切替可能な状態」——DB 層は #1090(D1 ビッグバン移行)、LangGraph は #1095(Workflows/Queues 化)にそれぞれ委譲し、本 Issue はその境界を明確に切る。 + +回答セット(`requirements-analysis-questions.md`、Q1-Q8)と practices-discovery の `team-practices.md`(TDD 必守、Walking Skeleton 常時実施、Mutation 第一指標、develop→dev 自動デプロイ)を前提とする。 + +## Functional Requirements + +### FR-1. LangGraph ルート分割(Q3=A) + +- FR-1.1: `createApp()` にオプション(例: `{ includeAgentRoutes: boolean }`)を導入し、compose/agent 系ルート(compose-sessions、`/api/ingest` 等の LangGraph 依存経路)と agent graph 登録を条件化する。 +- FR-1.2: `src/worker.ts` は agent ルート**無効**で app を生成し、`@langchain/langgraph` および `src/agents/**` が Worker バンドルに含まれないことを保証する。検証手段: `wrangler deploy --dry-run --outdir` のバンドル出力に対する内容検査(`@langchain/langgraph` / `src/agents` 由来コードの不在チェック)を再実行可能なスクリプトまたはテストとして残す。 +- FR-1.3: `src/index.ts`(Node/Railway)は現行どおり全ルートを登録し、挙動が変わらないこと。 +- FR-1.4: Worker 側で分割対象パスへのリクエストは 404(未登録)となる。501 スタブは作らない。 + +### FR-2. clientIp の Workers 対応(Q5=A) + +- FR-2.1: Workers ランタイムでは `cf-connecting-ip` ヘッダを第一候補として clientIp を解決する。 +- FR-2.2: Node ランタイムでは既存の TRUST_PROXY / x-forwarded-for / conninfo フォールバック挙動を維持する。 +- FR-2.3: Node 専用 import(`@hono/node-server/conninfo`)が Worker バンドル・実行経路に入らないこと。 +- FR-2.4: clientIp の全利用箇所が両ランタイムで正しい IP を受け取る。利用箇所の全数列挙(レート制限ミドルウェア等)は Functional Design の成果物として確定する(OQ-5)。 + +### FR-3. Sentry の Workers 対応(Q4=A) + +- FR-3.1: `@sentry/cloudflare` を導入し、worker エントリでエラー捕捉を初期化する。 +- FR-3.2: Node 側は `@sentry/node` を現状維持。`captureApiException` 等の共有呼び出し面は両 SDK で動作する抽象を保つ。 +- FR-3.3: Sentry DSN 未設定時は両ランタイムで no-op(現行挙動踏襲)。 + +### FR-4. presigned URL 経路の Workers 動作確認(Q6=A) + +- FR-4.1: `@aws-sdk/client-s3` + `s3-request-presigner` による presign 生成が `nodejs_compat` 下の workerd で動作することをテストで確認する(コード変更は不具合時のみ最小限)。 + +### FR-5. Worker ランタイムテスト(Q7=A) + +- FR-5.1: `@cloudflare/vitest-pool-workers`(または同等の workerd 実行基盤)を server/api に導入する。 +- FR-5.2: Worker エントリを対象に、非 DB 経路(/api/health、CORS、clientIp、ルート分割の登録有無、KV DO レート制限、storage/R2、presign、**auth ハンドラのマウント・起動**)の workerd 実行テストを TDD で作成する。auth については `/api/auth/*` ルートが workerd 上で Worker ランタイム例外を出さずに整形済みレスポンスを返すことまでを確認し、DB 到達を要する成功系はスタブ境界(FR-5.3)で扱う。 +- FR-5.3: DB 依存経路はスタブ境界(drizzle クライアント境界等)で扱い、DB 実接続を要するアサーションは #1090 後の検証に委ねる。 +- FR-5.4: テスト配置は `server/api/src/__tests__/` ミラー規則に従う(team-practices.md の配置規則)。 + +### FR-6. dev デプロイでの検証(Q1=A / Q8=B) + +- FR-6.1: 既存 `deploy-api-worker-dev.yml`(develop push → `wrangler deploy --env dev`)が本変更後も health-poll を通過する。 +- FR-6.2: dev Worker 上で非 DB 経路(/api/health のランタイム判定、CORS プリフライト、レート制限の KV DO 動作、auth エンドポイントが Worker ランタイムエラーなく応答すること)が確認できる。auth の DB 依存成功系(サインイン/セッション確立)の dev 検証は #1090 後。 + +## Non-Functional Requirements + +- NFR-1 **バンドル**: Worker バンドルは gzip 後 10 MB(Workers 有料プラン上限。無料プランは 3 MB)以内に収まり、LangGraph 系依存を含まない。上限値は設計時に最新の Cloudflare ドキュメントで再確認する。 +- NFR-2 **互換性**: Node(Railway)側の全既存テストが green のまま(並行稼働の維持)。 +- NFR-3 **セキュリティ**: 5xx マスキング(`{ error: string }` 封筒)、secrets の `wrangler secret bulk` 管理、`.env.worker.*` 非コミットを維持。clientIp の偽装耐性は Workers では CF 保証ヘッダで担保。 +- NFR-4 **テスト品質**: TDD(Red→Green→Refactor)。server/api は Stryker 対象外のため、coverage + アサーション強度で担保(team-practices.md の Testing Posture)。 + +## Constraints + +- C-1: `server/api` は root workspace 外(サービス単位 `bun.lock`)。依存追加は `server/api` 内で完結させる。 +- C-2: ローカル wrangler は現在**別アカウント**にログイン中、CI の Cloudflare トークンも要修復。資格情報が必要な操作(実 dev デプロイ検証)はブロックされ得るため、DoD の実機確認は資格情報復旧後に実施可能な形にする(ローカルは `wrangler dev` / workerd テストで代替)。 +- C-3: 構成の正本は `server/api/wrangler.jsonc`。Terraform には触れない(凍結中)。 +- C-4: DB スキーマ変更は行わない(drizzle 移行ルールの対象外であること)。 + +## Assumptions + +- A-1: D1 移行(#1090)がビッグバンで DB 層を置換するため、Worker からの Postgres 接続(Hyperdrive 等)は実装しない(Q2=X)。本 Issue 完了時点で dev Worker の DB 依存経路は未検証のままとなる。 +- A-2: 認証(Better Auth)は DB 依存のため、認証フローの dev E2E 検証も #1090 後。 本 Issue では auth ハンドラが workerd で例外なくマウント・起動できること(ランタイム適合)までを確認する。 +- A-3: Railway 稼働(`src/index.ts`)は並行継続し、本 Issue で挙動変更しない。 +- A-4: レート制限は KvStore 抽象(#1093/#1118 済)の `DurableObjectKvStore` で Workers 動作する前提。workerd テストで検証する。 + +## Out of Scope + +- 本番切替(prod Worker デプロイ、DNS カットオーバー、prod デプロイワークフロー作成)— 切替判断時の別 PR(Q8=B)。 +- Postgres/Hyperdrive 対応、D1 移行 — #1090。 +- LangGraph の Workflows/Queues 化 — #1095(本 Issue はルート分割による境界確定まで)。 +- `server/mcp` / `server/hocuspocus` の Workers 化 — #1092 / #1094。 +- 既存オブジェクト・データの移送。 + +## Open Questions + +- OQ-1: compose/agent 系ルートの正確な一覧(分割境界の確定)は Functional Design で `createApp()` のルート登録を精査して確定する。 +- OQ-2: `@sentry/cloudflare` と Hono の統合方式(withSentry ラッパ vs ミドルウェア)は設計時に最新ドキュメントで確認する。 +- OQ-3: vitest-pool-workers と既存 vitest 設定(`server/api/vitest.config.ts`)の共存構成(プロジェクト分割 or 別 config)。 +- OQ-4: dev 実機検証のタイミング — Cloudflare 資格情報(正アカウントの wrangler ログイン / CI トークン)の復旧に依存。 +- OQ-5: clientIp 利用箇所の全数列挙(FR-2.4)— Functional Design で確定する。 + +## Review + +**Verdict**: READY +**Reviewer**: aidlc-product-lead-agent +**Date**: 2026-07-18 + +### Findings + +(iteration 2 再レビュー。iteration 1 の指摘 1 major / 3 minor は全て解消を確認。blocker / major なし。) + +- **[resolved: major] 認証のランタイム適合確認の FR 昇格。** FR-5.2 に workerd テスト対象として auth ハンドラのマウント・起動と `/api/auth/*` の整形済みレスポンス確認(DB 到達成功系は FR-5.3 スタブ境界)、FR-6.2 に dev 検証項目として「auth エンドポイントが Worker ランタイムエラーなく応答」+ DB 依存成功系の #1090 送りが明記された。A-2 と矛盾なし、精緻化 Q1 回答(ランタイム適合 + 非 DB 経路 dev 検証)と一致。dev Worker 上で DB 到達時に NFR-3 の `{ error: string }` 封筒 5xx が返るケースも「ランタイム例外なく応答」の判定基準として整合。 +- **[resolved: minor] FR-1.2 バンドル検証手段。** `wrangler deploy --dry-run --outdir` 出力の内容検査を再実行可能なスクリプト/テストとして残すと明記。dry-run は Cloudflare 資格情報を要さないため C-2(資格情報ブロック)とも矛盾しない。 +- **[resolved: minor] NFR-1 サイズ上限。** gzip 後 10 MB(有料プラン。無料 3 MB)と明記、設計時のドキュメント再確認注記付き。数値は現行 Cloudflare 制限と一致。 +- **[resolved: minor] FR-2.4 利用箇所列挙。** OQ-5 新設で Functional Design の成果物として明示。 +- **[note] 新規不整合なし。** 改訂は FR-1.2 / FR-2.4 / FR-5.2 / FR-6.2 / NFR-1 / OQ-5 に限定され、Q1〜Q8 回答との忠実性・Out of Scope 境界・team-practices.md(TDD / ミラー配置 / Stryker 対象外)との整合は iteration 1 検証時から不変。Step 10 必須 7 セクション全て存在。参照リポジトリ事実(worker.ts / app.ts / clientIp.ts / wrangler.jsonc / deploy-api-worker-dev.yml / DurableObjectKvStore / captureApiException / `src/__tests__/` ミラー / `db/client.ts` lazy 初期化)は iteration 1 で実在確認済み。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/deployment-execution-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/deployment-execution-questions.md new file mode 100644 index 00000000..dc4bbcd4 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/deployment-execution-questions.md @@ -0,0 +1,5 @@ +# Deployment Execution — Questions + +質問なし(0 件)。 + +実行判断は確定済み: 本 Issue の実装コード(FR-1〜6)は infra スコープの後段作業(code-generation 単独実行 or 通常 TDD フロー)でまだ存在しないため、**新規デプロイの実行は実装 PR 到達時に延期**する。本ステージは既存 dev デプロイの実測記録(deployment-log / health-check)に充てる。deployment-strategy.md(dev recreate、成功 4 条件)と environment-inventory.md のアクセス経路を前提とする。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/deployment-log.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/deployment-log.md new file mode 100644 index 00000000..ed8e7ecd --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/deployment-log.md @@ -0,0 +1,22 @@ +# Deployment Log — api-worker (#1091) 2026-07-18 + +前提: deployment-pipeline/cd-config.md・deployment-strategy.md の手順に対する実行記録。environment-provisioning/environment-inventory.md のアクセス経路で実測。 + +## 本ステージでの新規デプロイ: 実行せず(意図的) + +#1091 の実装コード(ルート分割・clientIp・withSentry 等)は未着手のため、いまデプロイしても Phase 2a 骨格の再デプロイにしかならない。**実装 PR がマージされる develop push が本番の実行タイミング**であり、その時点で cd-config.md のパイプライン(bundle check → deploy → health-poll)が適用される。 + +## 既存デプロイの実測記録(読み取りのみ) + +| 項目 | 値 | +|------|-----| +| Worker | `zedi-api-dev`(account `175c04a4…`) | +| 最新 deployment | 2026-07-18T00:56:22Z Upload(version `28fefd1d…`、100%)+ 00:56:23Z Secret Change(version `82574367…`) | +| 実行者 | saedgewell@gmail.com(ローカル wrangler、手動) | +| CI 経由デプロイ | ❌ 直近 3 run 全て failure(`CLOUDFLARE_API_TOKEN` 未修復 — validation-report.md) | + +## 実装 PR 時の実行手順(再掲・確定) + +1. `bun run worker:bundle:check`(ローカル/CI とも。禁止依存の不在) +2. `wrangler deploy --env dev`(CI トークン修復前はローカル実行で代替可 — 資格情報は検証済み) +3. health-poll + 手動 dev 検証(deployment-strategy.md の成功 4 条件) diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/health-check-report.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/health-check-report.md new file mode 100644 index 00000000..29bb7800 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/health-check-report.md @@ -0,0 +1,21 @@ +# Health Check Report — api-worker (#1091) 2026-07-18 + +前提: deployment-log.md の既存デプロイに対するヘルス確認の実測結果。cd-config.md の health-poll 設計と突き合わせる。 + +## 公開 URL 経由のヘルスチェック: 実行不可(記録) + +- リポジトリ変数 `WORKER_API_BASE_URL` は**未設定**(`gh variable list` 空)。CI の health-poll ステップも `if:` ガードによりスキップされている状態。 +- workers.dev サブドメイン URL はリポジトリからは確定できず、公開経路の疎通確認は未実施。 +- 含意: dev Worker はデプロイされているが、**HTTP 経由の外形監視はまだ誰も行っていない**。実装 PR の dev 検証時に (a) workers.dev URL を確認して `WORKER_API_BASE_URL` を設定する、または (b) `wrangler tail` + 直接リクエストで確認する。 + +## 代替確認(実施済み) + +| 確認 | 結果 | +|------|------| +| deployment が active(100%)| ✅ `wrangler deployments list --env dev` | +| アカウント・リソース整合 | ✅ R2 バケット・Worker とも正アカウントに存在 | + +## 残作業(ヘルス面) + +1. workers.dev URL(または将来の custom domain)確定 → `WORKER_API_BASE_URL` リポジトリ変数設定 → CI health-poll 有効化。 +2. 実装 PR 時: `/api/health` の `runtime: "cloudflare-workers"` を手動確認(FR-6.2 / RL-5 帰属どおり)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/memory.md new file mode 100644 index 00000000..139c7513 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/memory.md @@ -0,0 +1,16 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + + +## Deviations + +- 2026-07-18T08:45:00Z — no new deployment executed: the implementation (FR-1..6) does not exist yet because the infra scope excludes code-generation, so this stage recorded the existing Phase 2a deployment state and the deferred execution plan instead of redeploying an unchanged skeleton. +- 2026-07-18T08:45:00Z — discovered WORKER_API_BASE_URL repo variable is unset, meaning CI health-poll has been silently skipping; recorded as a remaining environment task. + +## Tradeoffs + + +## Open questions + diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/smoke-test-results.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/smoke-test-results.md new file mode 100644 index 00000000..1dae0e08 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-execution/smoke-test-results.md @@ -0,0 +1,16 @@ +# Smoke Test Results — api-worker (#1091) 2026-07-18 + +前提: deployment-strategy.md の成功 4 条件に対するスモークテストの状況。deployment-log.md / health-check-report.md の実測と整合。 + +## 結果: 未実施(実装待ち — 意図的延期) + +| 成功条件 | 状態 | 理由 | +|---------|------|------| +| 1. bundle check green | ⏳ 未実施 | 検査スクリプト自体が実装 PR の成果物(LC-5) | +| 2. `wrangler deploy --env dev` 成功 | ✅(既存 Phase 2a 骨格、本日 00:56) | 実装版は未デプロイ | +| 3. health-poll HTTP 成功 | ⏳ 実行不可 | `WORKER_API_BASE_URL` 未設定(health-check-report.md) | +| 4. 手動 dev 検証(runtime / 非 DB 経路 / auth / CPU) | ⏳ 未実施 | 実装コード未着手のため対象が存在しない | + +## スモークテストの実行計画(実装 PR 時) + +workerd テスト(FR-5.2 の対象全数: health / CORS / clientIp / ルート分割 / auth マウント / KvStore 3 用途 / presign / エラーマスキング)が CI で green になった後、dev デプロイに対して同項目を手動スモークとして流す。DB 依存成功系は #1090 後(A-1/A-2)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/cd-config.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/cd-config.md new file mode 100644 index 00000000..bba2a36d --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/cd-config.md @@ -0,0 +1,26 @@ +# CD Config — api-worker (#1091) + +前提: construction/ci-pipeline/ci-config.md・quality-gates.md と infrastructure-design の deployment-architecture.md・cicd-pipeline.md(unit: api-worker)を CD 面から確定する。 + +## デプロイパイプライン(dev のみ) + +``` +develop push (paths: server/api/**) + -> deploy-api-worker-dev.yml + 1. bun install --frozen-lockfile + 2. bun run worker:bundle:check <- 追加(デプロイ前の機械保証) + 3. wrangler deploy --env dev <- 既存(GIT_COMMIT_SHA var 付与) + 4. curl --fail health-poll <- 既存(WORKER_API_BASE_URL 設定時のみ) +``` + + + +## 環境と資格情報 + +- 対象環境: `zedi-api-dev` のみ。production env は定義済みだがデプロイ経路を作らない(Q8=B)。 +- CI 資格情報(`CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID`)は**要修復(C-2)**。修復までは workflow は起動しても deploy ステップで失敗し得る — bundle check / テストは資格情報不要のため PR 品質ゲートは機能し続ける。 + +## 変更しないもの + +- Railway デプロイ(`src/index.ts`、本番トラフィック)— 不変(A-3)。 +- `deploy-dev.yml` / `deploy-prod.yml`(フロント・DB migrate)— 本 Issue の対象外。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/deployment-pipeline-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/deployment-pipeline-questions.md new file mode 100644 index 00000000..db565d62 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/deployment-pipeline-questions.md @@ -0,0 +1,5 @@ +# Deployment Pipeline — Questions + +質問なし(0 件)。 + +CD 判断は全て確定済み: dev のみデプロイ(Q8=B)、既存 `deploy-api-worker-dev.yml` 維持 + デプロイ直前 bundle check 追加(ci-config.md / cicd-pipeline.md で設計済み)、ロールバック不要(RL-2: Railway 並行稼働が保険)、prod 切替・段階デプロイは Out of Scope。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/deployment-strategy.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/deployment-strategy.md new file mode 100644 index 00000000..70aa145a --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/deployment-strategy.md @@ -0,0 +1,15 @@ +# Deployment Strategy — api-worker (#1091) + +前提: cd-config.md のパイプラインが従う戦略。deployment-architecture.md(unit: api-worker)の並行稼働トポロジを踏襲。 + +## 戦略: 並行稼働 + 全量置換(dev) + +- dev Worker は **recreate(全量置換)**: `wrangler deploy --env dev` は新バージョンへ即時切替。dev は検証専用でユーザートラフィックが無いため、canary / gradual rollout は不要。 +- 本番トラフィックは Railway が担い続ける(A-3)。本 Issue に本番デプロイ戦略は存在しない — prod 切替 PR(Out of Scope)で blue-green / gradual(`wrangler versions deploy`)を設計する。 + +## デプロイ成功の定義 + +1. bundle check green(禁止依存の不在)。 +2. `wrangler deploy --env dev` 成功。 +3. health-poll HTTP 成功(CI 自動)。 +4. 手動 dev 検証(資格情報復旧後): `runtime: "cloudflare-workers"`、非 DB 経路の応答、auth エンドポイントのランタイムエラー無し、CPU 超過エラー無し(FR-6.2 / PR-4)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/memory.md new file mode 100644 index 00000000..5adb2af6 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/memory.md @@ -0,0 +1,14 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + + +## Deviations + + +## Tradeoffs + + +## Open questions + diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/rollback-runbook.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/rollback-runbook.md new file mode 100644 index 00000000..cfac2264 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/deployment-pipeline/rollback-runbook.md @@ -0,0 +1,21 @@ +# Rollback Runbook — api-worker (#1091) + +前提: cd-config.md / deployment-strategy.md の dev 専用パイプラインに対するロールバック手順。quality-gates.md のゲートを通過したデプロイのみが対象。 + +## 原則: ロールバック不要(RL-2) + +dev Worker は検証専用でユーザー影響がゼロのため、失敗時の standing 手順は「放置して次の push で直す」。本番トラフィックは Railway(無変更)が担っており、dev Worker の障害は本番に波及しない。 + +## それでも戻したい場合(手動、資格情報必要) + +1. `bunx wrangler rollback --env dev` — 直前バージョンへ戻す(Workers はバージョン保持済み)。 +2. または直前の green コミットで `bun run worker:deploy:dev` 相当を手動実行。 +3. 確認: `/api/health` の `git_commit_sha` が意図したコミットに一致すること。 + +## エスカレーション条件 + +- dev Worker の障害が R2 バケット(zedi-storage-dev)や DO データの破壊を伴う場合のみ、#1089/#1093 の担当文脈(cloudflare-zedi スキル)で個別対応する。Worker コード自体の再デプロイでは R2 / DO のデータは失われない。 + +## 本番ロールバック + +存在しない(本番は Railway のまま — 本 Issue のスコープ外。prod 切替 PR で設計する)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/environment-inventory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/environment-inventory.md new file mode 100644 index 00000000..385d84f6 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/environment-inventory.md @@ -0,0 +1,30 @@ +# Environment Inventory — api-worker (#1091) + +前提: infrastructure-design/deployment-architecture.md・infrastructure-services.md(unit: api-worker)と deployment-pipeline/cd-config.md の環境要素を棚卸しする。構成の正本は `server/api/wrangler.jsonc`(実体確認済み)。 + +## dev 環境(本 Issue の検証対象) + +| 要素 | 値 | 状態 | 出典 | +|------|-----|------|------| +| Worker | `zedi-api-dev` | デプロイ実績あり(deploy-api-worker-dev.yml) | wrangler.jsonc env.dev | +| R2 | `zedi-storage-dev`(binding `STORAGE_BUCKET`) | 作成済み(#1089) | wrangler.jsonc / migration-plan Phase 1 | +| DO | `KV_DO` → `KvDurableObject`(SQLite classes, migration v1) | 定義済み(#1093/#1118) | wrangler.jsonc | +| vars | `ENVIRONMENT=development`(既存)+ `RUNTIME=cloudflare-workers`(実装時追加) | 一部未追加 | deployment-architecture.md | +| secrets | `BETTER_AUTH_SECRET`, `STORAGE_*`(presign)+ `SENTRY_DSN_API`(実装時追加) | bulk 投入機構あり(putWorkerSecrets.ts) | infrastructure-services.md | + +## production 環境(定義のみ — 本 Issue ではデプロイしない) + +| 要素 | 値 | 状態 | +|------|-----|------| +| Worker | `zedi-api` | env 定義のみ。デプロイ経路なし(Q8=B) | +| R2 | `zedi-storage-prod` | 作成済み。Railway API が S3 互換エンドポイント経由で使用中 | + +## アクセス経路と資格情報 + +| 経路 | 状態 | +|------|------| +| ローカル wrangler | **別アカウントにログイン中(要切替)** — C-2 | +| CI トークン(CLOUDFLARE_API_TOKEN) | **要修復** — C-2 | +| Cloudflare MCP(claude.ai connector) | 本セッション未認証 | + +新規プロビジョニングは不要(作成すべき未作成リソースなし)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/environment-provisioning-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/environment-provisioning-questions.md new file mode 100644 index 00000000..0eedc8bb --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/environment-provisioning-questions.md @@ -0,0 +1,5 @@ +# Environment Provisioning — Questions + +質問なし(0 件)。 + +プロビジョニング対象は存在しない: 必要リソース(Worker `zedi-api-dev`、R2 `zedi-storage-dev/prod`、DO `KvDurableObject`)は全て作成・定義済み(infrastructure-services.md のインベントリどおり、#1089/#1093/#1106 で導入)。本ステージの実体は「新規作成」ではなく「既存環境の棚卸しと検証」であり、判断事項はない。実環境への検証アクセスは資格情報復旧(C-2)待ちで、これは既知の制約(requirements.md OQ-4)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/memory.md new file mode 100644 index 00000000..5010c77f --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/memory.md @@ -0,0 +1,15 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + +- 2026-07-18T08:30:00Z — stage ran as inventory+validation (no provisioning): all resources pre-exist from #1089/#1093/#1106. Live checks (read-only wrangler + gh) found C-2 half-resolved: local wrangler is now on the correct zedi account with today's deployments, while the CI token still fails (latest run failure 2026-07-18T01:44). Persistent memory updated accordingly. + +## Deviations + + +## Tradeoffs + + +## Open questions + diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/validation-report.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/validation-report.md new file mode 100644 index 00000000..498ac6d3 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/environment-provisioning/validation-report.md @@ -0,0 +1,24 @@ +# Validation Report — api-worker 環境検証 (2026-07-18) + +前提: environment-inventory.md の棚卸しと deployment-pipeline/cd-config.md のアクセス経路を実測で検証した結果。infrastructure-design/deployment-architecture.md・infrastructure-services.md の宣言と突き合わせる。 + +## 実測結果(本日実施、読み取り操作のみ) + +| 検証 | 方法 | 結果 | +|------|------|------| +| ローカル wrangler 認証 | `wrangler whoami` | ✅ `saedgewell@gmail.com`(account `175c04a4…`)OAuth | +| アカウントが zedi アカウントであること | `wrangler r2 bucket list` に既知リソース | ✅ `zedi-storage-dev` / `zedi-storage-prod` 存在(2026-06-29 作成) | +| dev Worker 存在・稼働 | `wrangler deployments list --env dev` | ✅ `zedi-api-dev` に本日 2026-07-18T00:56 の deployment(Upload + Secret Change、author saedgewell@gmail.com) | +| CI デプロイ経路 | `gh run list`(deploy-api-worker-dev.yml 直近 3 件) | ❌ **全て failure**(最新 2026-07-18T01:44、develop)— `CLOUDFLARE_API_TOKEN` 要修復のまま | +| Cloudflare MCP | セッション接続状態 | ❌ 未認証(claude.ai connector 側で要認可) | + +## 判定 + +- **ローカル経路の資格情報ブロッカー(C-2 の前半)は解消済み。** 実装後の dev デプロイ・手動検証(FR-6.2、`wrangler tail`)はローカル wrangler で実施可能。 +- **CI 経路(C-2 の後半)は未解消。** `deploy-api-worker-dev.yml` は起動するがデプロイ失敗が継続。GitHub secret `CLOUDFLARE_API_TOKEN` のローテーション(token-scopes.md のスコープで再発行)が必要 — 本ワークフローのコード実装とは独立に対応可能。 +- 新規プロビジョニングは不要(インベントリどおり全リソース既存)。`RUNTIME` var と `SENTRY_DSN_API` secret は実装 PR / secret 投入時に追加する(環境作成ではなく構成追記)。 + +## 残作業(環境面) + +1. GitHub secret `CLOUDFLARE_API_TOKEN` の再発行・更新(アカウント `175c04a4…` に対して)。 +2. 更新後、develop push で deploy-api-worker-dev.yml が green になることの確認。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/alarms.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/alarms.md new file mode 100644 index 00000000..bd9745c6 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/alarms.md @@ -0,0 +1,12 @@ +# Alarms — api-worker (#1091) + +前提: monitoring-design.md「本 Issue で追加しないもの」と reliability-design.md(RL-1: SLA なし)の確定記録。 + +## 設定するアラート + +なし(意図的)。dev Worker は検証専用でユーザー影響がなく、障害対応は「次の push で直す」(rollback-runbook.md の原則)。 + +## prod 切替時に設計するもの + +- エラー率・CPU 超過・health 失敗の通知(Cloudflare notifications / Sentry alert rules)。 +- ロールバックトリガと連動した閾値(deployment-strategy.md の prod 設計とセット)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/anomaly-config.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/anomaly-config.md new file mode 100644 index 00000000..e0f480db --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/anomaly-config.md @@ -0,0 +1,11 @@ +# Anomaly Config — api-worker (#1091) + +前提: alarms.md(アラートなし)・slo-config.md(SLO なし)と同じ dev スコープ判断の確定記録。 + +## 異常検知 + +設定しない(意図的)。検知対象となる定常トラフィックが dev Worker には存在せず、ベースライン学習が成立しない。 + +## prod 切替時の再評価 + +- Cloudflare の標準メトリクス(エラー率・レイテンシ)に対する閾値検知から開始し、必要なら Sentry の issue alert / spike detection を重ねる。異常検知は SLO(slo-config.md)確定後に意味を持つため、順序もそれに従う。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/dashboards.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/dashboards.md new file mode 100644 index 00000000..370c4f50 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/dashboards.md @@ -0,0 +1,12 @@ +# Dashboards — api-worker (#1091) + +前提: monitoring-design.md・infrastructure-services.md(unit: api-worker)の可観測性構成を確定記録する。 + +## いま有効なもの + +- Cloudflare ダッシュボード標準ビュー(Workers & Pages → `zedi-api-dev`): リクエスト数・エラー率・CPU 時間・invocation logs。`wrangler.jsonc` の `observability.enabled: true` により追加設定なしで利用可能。 +- Sentry プロジェクト(`SENTRY_DSN_API` 投入後): エラーイベントビュー。 + +## 作らないもの(prod 切替時に設計) + +- カスタムダッシュボード・集約ビュー(dev 検証専用のため不要 — reliability-design.md RL-1)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/log-queries.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/log-queries.md new file mode 100644 index 00000000..f9141396 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/log-queries.md @@ -0,0 +1,22 @@ +# Log Queries — api-worker (#1091) + +前提: monitoring-design.md の手動確認手段を実行可能なコマンドとして確定する。ローカル wrangler は正アカウントで検証済み(validation-report.md)。 + +## 実装 PR の dev 検証で使うクエリ + +```bash +# ライブテイル(dev Worker の全リクエスト+例外) +cd server/api && bunx wrangler tail --env dev + +# エラーのみ +bunx wrangler tail --env dev --status error + +# health 確認(URL 確定後) +curl -s $WORKER_API_BASE_URL/api/health | jq .runtime # 期待: "cloudflare-workers" +``` + +## 確認観点(smoke-test-results.md の計画と対応) + +- 1101(uncaught exception)が出ないこと — マスク済み 5xx への整形(RL-3)の実地確認。 +- `exceeded CPU` が出ないこと(PR-4)。 +- Sentry 側にイベントが届くこと(DSN 投入後、withSentry の flush 確認)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/memory.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/memory.md new file mode 100644 index 00000000..9c61b570 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/memory.md @@ -0,0 +1,15 @@ + +> This file is maintained by the orchestrator during stage execution. Add observations at the gate ritual, not by editing here directly. + +## Interpretations + +- 2026-07-18T09:00:00Z — final stage ran as a decide-and-record pass: for a dev-only scope, alarms/SLO/anomaly detection are deliberately "none" with the prod-cutover re-evaluation path written down, rather than inventing dev-grade monitoring nobody would read. + +## Deviations + + +## Tradeoffs + + +## Open questions + diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/observability-setup-questions.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/observability-setup-questions.md new file mode 100644 index 00000000..e3694aa3 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/observability-setup-questions.md @@ -0,0 +1,5 @@ +# Observability Setup — Questions + +質問なし(0 件)。 + +可観測性の方針は monitoring-design.md(unit: api-worker)で確定済み: dev 検証専用のためアラート・ダッシュボード・SLO は設定しない(RL-1)、エラー監視は withSentry + `observability.enabled: true`、手動確認は `wrangler tail` / MCP。本ステージは各成果物で「いま有効なもの」と「prod 切替時に設計するもの」を確定記録するのみで、判断事項はない。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/slo-config.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/slo-config.md new file mode 100644 index 00000000..b375a943 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/slo-config.md @@ -0,0 +1,11 @@ +# SLO Config — api-worker (#1091) + +前提: performance-design.md(PR-1: パリティ方針、数値目標なし)・reliability-design.md(RL-1: SLA/SLO 不設定)の確定記録。 + +## SLO + +設定しない(意図的)。性能はパリティ基準(数値目標なし — Q2=A)、可用性は Railway 並行稼働が本番を担うため dev Worker に SLO は定義できない・しない。 + +## prod 切替時の入力 + +- 切替判断時に Railway 現行の実測(レイテンシ分布・エラー率)をベースラインとして採取し、SLO 初期値の根拠にする(performance-requirements.md PR-2 の「記録のみ」がその布石)。 diff --git a/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/tracing-config.md b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/tracing-config.md new file mode 100644 index 00000000..a91f5d88 --- /dev/null +++ b/aidlc/spaces/default/intents/260718-api-workers-migration/operation/observability-setup/tracing-config.md @@ -0,0 +1,11 @@ +# Tracing Config — api-worker (#1091) + +前提: monitoring-design.md・security-design.md(Sentry スクラブ方針)の確定記録。 + +## 設定するもの + +- Sentry エラートレース(withSentry ラッパ経由、LC-4): エラーイベントに `httpStatus` タグ + `method`/`routePath` extra(現行 `captureApiException` の文脈情報を両ランタイムで維持)。PII は `scrubSentryEvent` + `sendDefaultPii: false` で除外。 + +## 設定しないもの + +- 分散トレーシング(OpenTelemetry / Sentry Performance): 単一 Worker + 外部依存最小の dev 検証に対して過剰。prod 切替後、D1/DO/Workflows が絡む構成(#1090/#1095)になった時点で必要性を再評価。 diff --git a/aidlc/spaces/default/intents/intents.json b/aidlc/spaces/default/intents/intents.json new file mode 100644 index 00000000..d3aeb497 --- /dev/null +++ b/aidlc/spaces/default/intents/intents.json @@ -0,0 +1,9 @@ +[ + { + "uuid": "019f72f4-6db1-7d66-b28b-21e662155d0b", + "slug": "api-workers-migration", + "dirName": "260718-api-workers-migration", + "scope": "infra", + "status": "complete" + } +] diff --git a/aidlc/spaces/default/memory/project.md b/aidlc/spaces/default/memory/project.md index 2c1263bf..990b3610 100644 --- a/aidlc/spaces/default/memory/project.md +++ b/aidlc/spaces/default/memory/project.md @@ -57,13 +57,43 @@ +- NEVER add colocated test files in `server/api` or `server/mcp` — tests mirror the source tree under `src/__tests__/`. (affirmed 2026-07-18) +- NEVER write "写し絵テスト" (tests copied from implementation output) or rewrite expectations to match a failing implementation without justification. (affirmed 2026-07-18) +- NEVER use kebab-case for domain directories (exception: `packages/ui/src/components/` shadcn files). (affirmed 2026-07-18) +- NEVER place non-hook modules in `src/hooks/` or hooks in `src/pages/`. (affirmed 2026-07-18) +- NEVER let 5xx responses leak raw internal/DB error messages — all errors flow through the global `onError` `{ error: string }` envelope. (affirmed 2026-07-18) +- NEVER commit secret values (`.env*` real files stay gitignored; `.gitleaksignore` tracks false positives only). (affirmed 2026-07-18) +- NEVER apply SQL migrations outside `server/api/drizzle/` — other SQL locations do not reach production. (affirmed 2026-07-18) ## Mandated +- ALWAYS follow TDD Red → Green → Refactor: write a failing test before any implementation change. (affirmed 2026-07-18) +- ALWAYS start Construction with a walking skeleton — the thinnest end-to-end slice proving every integration point — before building out the rest. (affirmed 2026-07-18) +- ALWAYS pair `server/api/src/schema/**/*.ts` changes with a `server/api/drizzle/NNNN_*.sql` migration and a `drizzle/meta/_journal.json` entry in the same PR. (affirmed 2026-07-18) +- ALWAYS use a merge commit (never squash) when syncing `main` → `develop`. (affirmed 2026-07-18) +- ALWAYS update `packages/shared` and the corresponding `server/*` duplicated constants together; the drift test must stay green. (affirmed 2026-07-18) +- ALWAYS treat each service's `wrangler.jsonc` as the source of truth for Cloudflare configuration (not Terraform). (affirmed 2026-07-18) +- ALWAYS use Conventional Commits for PR titles. (affirmed 2026-07-18) +- ALWAYS manage secrets via `wrangler secret bulk` (`server/api/scripts/putWorkerSecrets.ts`) or GitHub Actions encrypted secrets. (affirmed 2026-07-18) ## Corrections + +## AI-DLC Execution Notes +- infra scope skips reverse-engineering, so practices-discovery evidence scans run directly against the repo (AGENTS.md, CI configs, git history); do not expect reverse-engineering artifacts as inputs. (learned 2026-07-18) +- aidlc-log.ts answer's human-turn guard rejects answers collected via the harness AskUserQuestion tool; the stage questions file is the authoritative answer record, and QUESTION_ANSWERED audit rows may be absent for interactively answered stages. (learned 2026-07-18) + +- This intent has a single unit named api-worker: all per-unit Construction stages (nfr-design, infrastructure-design, code-generation) write artifacts under construction/api-worker// using that same unit name. (learned 2026-07-18) +- Construction stages that provision no new resources and whose decisions all trace to approved upstream artifacts may run with a zero-question file stating that rationale; verify real config (e.g. wrangler.jsonc) directly instead of asking. (learned 2026-07-18) +## Issue 1091 Scope Decisions +- #1091 DoD is refined to Worker runtime compatibility + dev verification of non-DB routes only; DB-dependent routes (auth success flows included) are verified after #1090 (D1 big-bang). No Hyperdrive/Postgres-from-Workers work. (learned 2026-07-18) +- Where the #1091 issue body and inception/requirements-analysis/requirements.md diverge (pg/Hyperdrive, LangGraph body, prod cutover moved out of scope), requirements.md is authoritative. (learned 2026-07-18) + +## Cloudflare Workers Bundle Boundaries +- Worker bundle exclusion must use module-level boundaries (separate module + entry-side wiring, e.g. appAgents.ts / clientIpNode.ts / sentryWorker.ts), never runtime flags or dynamic imports — wrangler's esbuild single-file bundle pulls dynamic imports in too. Applies equally to #1092 (server/mcp Workers migration). (learned 2026-07-18) +- Workers-side Sentry uses the @sentry/cloudflare withSentry wrapper on the default export (guarantees init + ctx.waitUntil event flush); plain captureException without the wrapper can lose events at request end. (learned 2026-07-18) +- Runtime discrimination (Workers vs Node) uses a dedicated RUNTIME var in wrangler.jsonc, never ENVIRONMENT — ENVIRONMENT distinguishes dev/prod and also exists on Railway, so it cannot tell runtimes apart. Security-sensitive branches (CF-Connecting-IP trust) must gate on this explicit signal. (learned 2026-07-18) diff --git a/aidlc/spaces/default/memory/team.md b/aidlc/spaces/default/memory/team.md index af8e3d00..0b77c123 100644 --- a/aidlc/spaces/default/memory/team.md +++ b/aidlc/spaces/default/memory/team.md @@ -5,33 +5,19 @@ > not directly. ## Way of Working - - - - +ソロ開発者(otomatty)+ AI エージェント(Claude Code / Cursor / Codex / Dependabot)の体制。PR ベースで開発し、レビューは自己承認 + AI レビューの組み合わせ(ブランチ保護は PR 承認 1 件)。ブランチは `feature/…`・`fix/…`・`hotfix/…`・`chore/…`(Issue 番号命名可)、PR タイトルは Conventional Commits で release-please の semver を駆動する。feature PR は squash で `develop` に取り込み、`main` ↔ `develop` の同期のみ必ず merge commit(squash 禁止)。 ## Walking Skeleton - - - - +常に実施する。新規機能・インフラ移行のいずれでも、最初に最薄のエンドツーエンドスライスを通して統合点を先に証明してから残りを作り込む。 ## Testing Posture - - - - +TDD(Red → Green → Refactor)が必須で、実装後のテスト後付けと「写し絵テスト」は禁止。第一品質指標は Mutation score(Stryker、PR は `mutation-light` break:70、golden list 目標 85%+)で、行カバレッジ 80% は目標値であり機械強制しない。テスト配置は `src/` 等が同居(colocated)、`server/api`・`server/mcp` は `src/__tests__/` ミラー(同居禁止)。E2E は Playwright(バックエンドレス、モック駆動)。CI のテストジョブは GitHub 側の required status checks としてマージ要件になっている。 ## Deployment - - +`develop` → dev 環境、`main` → prod 環境の push 駆動・完全自動デプロイ(手動承認ゲートなし)。フロント/admin は Cloudflare Pages、`server/*` は暫定 Railway で、Cloudflare Workers への移行が進行中(#1088。dev 用 Worker デプロイのみ先行、構成の正本は各 `wrangler.jsonc`、Terraform は廃止予定)。DB マイグレーションは deploy ワークフロー内の `drizzle-kit migrate` のみが本番に効く。本番フロント切替は `/api/health` の commit SHA 一致で gate。 ## Code Style - - - - - +ESLint flat config(typescript-eslint strict)+ Prettier(幅 100、ダブルクォート)。命名はドメインディレクトリ camelCase(kebab-case 禁止、例外は `packages/ui` の shadcn)、コンポーネント PascalCase、フックは `use*` で `src/hooks/` のみ。`server/api` は routes / services / lib / schema の 4 層分離で、エラーは HTTPException + グローバル `onError` の `{ error: string }` 封筒に統一(5xx は内部情報を漏らさない)。`server/*` は workspace 外のため `@zedi/shared` の値をサーバ内に二重定義し、ドリフトテストで文字列一致を検証する。 ## Forbidden diff --git a/knip.json b/knip.json index c07f4dfe..0c684c7b 100644 --- a/knip.json +++ b/knip.json @@ -33,8 +33,9 @@ "project": ["src/**/*.ts"] }, "server/api": { - "entry": ["scripts/**/*.ts", "src/agents/index.ts"], - "project": ["src/**/*.{ts,tsx}", "scripts/**/*.ts"] + "entry": ["scripts/**/*.ts", "src/agents/index.ts", "src/__tests__/**/*.test.ts"], + "project": ["src/**/*.{ts,tsx}", "scripts/**/*.ts"], + "vitest": false }, "server/hocuspocus": { "project": ["src/**/*.ts"] @@ -106,7 +107,8 @@ "@langchain/openai", "@langchain/anthropic", "@langchain/google-genai", - "@vitest/coverage-v8" + "@vitest/coverage-v8", + "cloudflare" ], "ignoreExportsUsedInFile": { "interface": true, diff --git a/server/api/.env.worker.dev.example b/server/api/.env.worker.dev.example index d3918c94..6bce396c 100644 --- a/server/api/.env.worker.dev.example +++ b/server/api/.env.worker.dev.example @@ -1,16 +1,64 @@ -# Copy to `.env.worker.dev` (gitignored) and fill values, then: -# bun run worker:secrets:put -- --env dev +# ───────────────────────────────────────────────────────────────────────────── +# Cloudflare Workers (dev) secrets for `zedi-api-dev` (#1091) # -# R2 binding (STORAGE_BUCKET) is in wrangler.jsonc — not a secret. -# These STORAGE_* values are for S3-compatible presigned URLs only (#1089). +# `.env.worker.dev`(gitignore 済み)にコピーして値を埋め、次で一括投入する: +# bun run worker:secrets:put -- --env dev # 投入 +# bun run worker:secrets:put -- --env dev --dry-run # 中身の確認だけ +# +# ⚠️ 投入の順序(Cloudflare 仕様): +# `wrangler deploy` はデプロイ時に Worker を起動して検証する(startup +# validation, error 10021)。src/worker.ts → app.ts → auth.ts が **モジュール +# ロード時**に下記「必須」の値を読むため、secrets 未投入だと deploy が +# `... must be set` で失敗する。**先に `worker:secrets:put` を実行してから +# deploy する**こと。secrets は Worker に永続化され、以降の deploy に引き継がれる。 +# +# ⚠️ ここに書かないもの(wrangler.jsonc が供給する。secrets ではない): +# - vars: ENVIRONMENT=development / RUNTIME=cloudflare-workers +# - bindings: STORAGE_BUCKET(R2 zedi-storage-dev) / KV_DO(Durable Object) +# - deploy 時: GIT_COMMIT_SHA は CI が `--var` で渡す +# ───────────────────────────────────────────────────────────────────────────── + +# ── 必須 / Required at boot(未設定だと deploy validation が失敗する)────────── + +# Better Auth。BETTER_AUTH_SECRET は API / MCP / hocuspocus で同一値を共有する。 +BETTER_AUTH_URL=https://zedi-api-dev.otomatty.workers.dev +BETTER_AUTH_SECRET= +# CSRF / trustedOrigins 用の許可オリジン(カンマ区切り)。 +# 例: https://zedi-dev.pages.dev,https://zedi-admin-dev.pages.dev +CORS_ORIGIN= + +# Google OAuth(socialProviders)。auth.ts がモジュールロード時に読む。 +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# DB 接続文字列。auth.ts の drizzle アダプタがモジュールロード時に pg.Pool を +# 構築するため **起動時に必須**。ただし接続は DB 依存ルートを叩くまで発生しない +# (遅延接続)。D1 移行(#1090)までは到達しないプレースホルダで良い。 +DATABASE_URL=postgresql://placeholder:unused@127.0.0.1:5432/unused +# ── 任意 / Optional(未設定なら no-op またはランタイム時のみ必要)──────────── + +# Sentry(@sentry/cloudflare, withSentry)。空なら SDK は no-op。 +SENTRY_DSN_API= + +# R2 の S3 互換 presigned URL 用(読み書きは STORAGE_BUCKET binding が処理する。 +# presign 生成時のみ必要)。トークン作成時に発行された R2 アクセスキーを使う。 STORAGE_ENDPOINT=https://.r2.cloudflarestorage.com STORAGE_ACCESS_KEY= STORAGE_SECRET_KEY= STORAGE_BUCKET_NAME=zedi-storage-dev -# Add other Worker secrets as needed before cutover (see secrets-template.md): -# DATABASE_URL= -# BETTER_AUTH_SECRET= -# BETTER_AUTH_URL= -# CORS_ORIGIN= +# メール送信(招待マジックリンク等)。未設定ならメール送信は無効。 +# RESEND_API_KEY= +# RESEND_FROM_EMAIL=noreply@zedi-note.app + +# MCP 認可コードフロー(server/api /api/mcp)。外部 MCP を使う場合のみ。 +# MCP_REDIRECT_URI_ALLOW=http://127.0.0.1:,https://zedi-note.app +# MCP_JWT_EXP_DAYS=30 + +# その他の任意機能キー。 +# OPENROUTER_API_KEY= +# YOUTUBE_DATA_API_KEY= + +# 注: TRUST_PROXY は Workers では設定しない。ランタイム判定(RUNTIME var)で +# CF-Connecting-IP を第一候補にするため(#1091 clientIp.ts)。 diff --git a/server/api/.env.worker.production.example b/server/api/.env.worker.production.example index fdeaca23..b548d27a 100644 --- a/server/api/.env.worker.production.example +++ b/server/api/.env.worker.production.example @@ -1,16 +1,66 @@ -# Copy to `.env.worker.production` (gitignored) and fill values, then: +# ───────────────────────────────────────────────────────────────────────────── +# Cloudflare Workers (production) secrets for `zedi-api` (#1091) +# +# `.env.worker.production`(gitignore 済み)にコピーして値を埋め、次で一括投入: # bun run worker:secrets:put -- --env production +# bun run worker:secrets:put -- --env production --dry-run +# +# ⚠️ 投入の順序(Cloudflare 仕様): +# `wrangler deploy` はデプロイ時に Worker を起動して検証する(startup +# validation, error 10021)。src/worker.ts → app.ts → auth.ts が **モジュール +# ロード時**に下記「必須」の値を読むため、secrets 未投入だと deploy が +# `... must be set` で失敗する。**先に `worker:secrets:put` を実行してから +# deploy する**こと。secrets は Worker に永続化され、以降の deploy に引き継がれる。 +# +# ※ 本番 Worker(zedi-api)へのデプロイ経路は #1091 のスコープ外(切替判断時の +# 別 PR)。本ファイルは切替時に使う控え。 # -# R2 binding (STORAGE_BUCKET) is in wrangler.jsonc — not a secret. -# These STORAGE_* values are for S3-compatible presigned URLs only (#1089). +# ⚠️ ここに書かないもの(wrangler.jsonc が供給する。secrets ではない): +# - vars: ENVIRONMENT=production / RUNTIME=cloudflare-workers +# - bindings: STORAGE_BUCKET(R2 zedi-storage-prod) / KV_DO(Durable Object) +# - deploy 時: GIT_COMMIT_SHA は CI が `--var` で渡す +# ───────────────────────────────────────────────────────────────────────────── + +# ── 必須 / Required at boot(未設定だと deploy validation が失敗する)────────── + +# Better Auth。BETTER_AUTH_SECRET は API / MCP / hocuspocus で同一値を共有する。 +BETTER_AUTH_URL=https://api.zedi-note.app +BETTER_AUTH_SECRET= +# CSRF / trustedOrigins 用の許可オリジン(カンマ区切り)。 +# 例: https://zedi-note.app,https://admin.zedi-note.app +CORS_ORIGIN= + +# Google OAuth(socialProviders)。auth.ts がモジュールロード時に読む。 +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +# DB 接続文字列。auth.ts の drizzle アダプタがモジュールロード時に pg.Pool を +# 構築するため **起動時に必須**。D1 移行(#1090)までは到達しないプレースホルダで良い。 +DATABASE_URL=postgresql://placeholder:unused@127.0.0.1:5432/unused + +# ── 任意 / Optional(未設定なら no-op またはランタイム時のみ必要)──────────── + +# Sentry(@sentry/cloudflare, withSentry)。空なら SDK は no-op。 +SENTRY_DSN_API= + +# R2 の S3 互換 presigned URL 用(読み書きは STORAGE_BUCKET binding が処理する。 +# presign 生成時のみ必要)。 STORAGE_ENDPOINT=https://.r2.cloudflarestorage.com STORAGE_ACCESS_KEY= STORAGE_SECRET_KEY= STORAGE_BUCKET_NAME=zedi-storage-prod -# Add other Worker secrets as needed before cutover (see secrets-template.md): -# DATABASE_URL= -# BETTER_AUTH_SECRET= -# BETTER_AUTH_URL= -# CORS_ORIGIN= +# メール送信(招待マジックリンク等)。未設定ならメール送信は無効。 +# RESEND_API_KEY= +# RESEND_FROM_EMAIL=noreply@zedi-note.app + +# MCP 認可コードフロー(server/api /api/mcp)。外部 MCP を使う場合のみ。 +# MCP_REDIRECT_URI_ALLOW=http://127.0.0.1:,https://zedi-note.app +# MCP_JWT_EXP_DAYS=30 + +# その他の任意機能キー。 +# OPENROUTER_API_KEY= +# YOUTUBE_DATA_API_KEY= + +# 注: TRUST_PROXY は Workers では設定しない。ランタイム判定(RUNTIME var)で +# CF-Connecting-IP を第一候補にするため(#1091 clientIp.ts)。 diff --git a/server/api/bun.lock b/server/api/bun.lock index 54225273..9f2cb995 100644 --- a/server/api/bun.lock +++ b/server/api/bun.lock @@ -17,6 +17,7 @@ "@mozilla/readability": "^0.6.0", "@polar-sh/sdk": "^0.48.0", "@react-email/components": "^1.0.11", + "@sentry/cloudflare": "^10.51.0", "@sentry/node": "^10.51.0", "@tiptap/core": "^3.20.1", "@tiptap/extension-code-block-lowlight": "^3.20.1", @@ -42,6 +43,7 @@ "zod": "^4.4.3", }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.18.6", "@cloudflare/workers-types": "^5.20260706.1", "@types/jsdom": "^28.0.0", "@types/node": "^26.0.1", @@ -61,6 +63,12 @@ "packages": { "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.95.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ=="], + "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.18.0", "", { "dependencies": { "@types/estree": "^1.0.8", "astring": "^1.9.0", "esquery": "^1.7.0", "meriyah": "^6.1.4", "semifies": "^1.0.0", "source-map": "^0.6.0" }, "bin": { "code-transformer": "cli.js" } }, "sha512-aN3Oq8r1J3gPJtCwErP664gM0+HhM1I1lujPr9TMTCcEl/joQQbpGpeMdts9B1+W2wHMsvioDMv5F4PvMWE6gw=="], + + "@apm-js-collab/code-transformer-bundler-plugins": ["@apm-js-collab/code-transformer-bundler-plugins@0.6.2", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.18.0", "es-module-lexer": "^2.1.0", "magic-string": "^0.30.21", "module-details-from-path": "^1.0.4" } }, "sha512-5vBrtIEL+UVbO0YWWoyYG4QMgR+ZfnIL3xlteIkAmU7YaAPhc28k3md/NM14tnkfXKjKOn9yUzEA9AYUmNpvJg=="], + + "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.13.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.18.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.6" } }, "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw=="], "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.3", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7" } }, "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA=="], @@ -195,6 +203,8 @@ "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + "@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.18.6", "", { "dependencies": { "cjs-module-lexer": "1.2.3", "esbuild": "0.28.1", "miniflare": "4.20260714.0", "wrangler": "4.112.0", "zod": "3.25.76" }, "peerDependencies": { "@vitest/runner": "^4.1.0", "@vitest/snapshot": "^4.1.0", "vitest": "^4.1.0" } }, "sha512-6JGqaQsQRZIVq/6jEC4ouJnShZriPIJ2X0yGndwMm+SiPP93pJi5Dp30dYAoztNrNJC7wWK7ec5slLfMBMZ8jA=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260625.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A=="], "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260625.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw=="], @@ -235,57 +245,57 @@ "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="], @@ -431,6 +441,8 @@ "@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + "@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw=="], + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], @@ -565,7 +577,11 @@ "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], - "@sentry/core": ["@sentry/core@10.51.0", "", {}, "sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w=="], + "@sentry/cloudflare": ["@sentry/cloudflare@10.66.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/core": "10.66.0", "@sentry/node": "10.66.0", "@sentry/server-utils": "10.66.0" }, "peerDependencies": { "@cloudflare/workers-types": "^4.x || ^5.x" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-QYEZQsR09OO/0nfyZP4J56JG8zNO5PCPon7TM96fjjbKIbshpbUcr73XmUhHh3h1Kv0Ic/M5LWBm9eV6H5Cmmw=="], + + "@sentry/conventions": ["@sentry/conventions@0.16.0", "", {}, "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ=="], + + "@sentry/core": ["@sentry/core@10.66.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-9UbgSvds7bMJsP561eWmeyMLcfOmnwxtnx2QuW3yLobzP2Ob7CyJCOzP4tGzlTAGDrzShkFEZhiyuBUKiEK2oQ=="], "@sentry/node": ["@sentry/node@10.51.0", "", { "dependencies": { "@fastify/otel": "0.18.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-amqplib": "0.61.0", "@opentelemetry/instrumentation-connect": "0.57.0", "@opentelemetry/instrumentation-dataloader": "0.31.0", "@opentelemetry/instrumentation-fs": "0.33.0", "@opentelemetry/instrumentation-generic-pool": "0.57.0", "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", "@opentelemetry/instrumentation-ioredis": "0.62.0", "@opentelemetry/instrumentation-kafkajs": "0.23.0", "@opentelemetry/instrumentation-knex": "0.58.0", "@opentelemetry/instrumentation-koa": "0.62.0", "@opentelemetry/instrumentation-lru-memoizer": "0.58.0", "@opentelemetry/instrumentation-mongodb": "0.67.0", "@opentelemetry/instrumentation-mongoose": "0.60.0", "@opentelemetry/instrumentation-mysql": "0.60.0", "@opentelemetry/instrumentation-mysql2": "0.60.0", "@opentelemetry/instrumentation-pg": "0.66.0", "@opentelemetry/instrumentation-redis": "0.62.0", "@opentelemetry/instrumentation-tedious": "0.33.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@prisma/instrumentation": "7.6.0", "@sentry/core": "10.51.0", "@sentry/node-core": "10.51.0", "@sentry/opentelemetry": "10.51.0", "import-in-the-middle": "^3.0.0" } }, "sha512-2yZLRZwS1dKG8/4eOTpGSo/gO/EgmT9aPj6lAzUkRa7bZCTTdW4BraaHU0leX5T94909Qfhbr3W5AVTfDOCKiQ=="], @@ -573,6 +589,8 @@ "@sentry/opentelemetry": ["@sentry/opentelemetry@10.51.0", "", { "dependencies": { "@sentry/core": "10.51.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-Qc7AlCE4uhB+SvHLqah4RgR1WdY7wmmr/hx9g/prDP9R1ocshmUEMrZK9qjuwaklW7/fmkFCXI8ETxo5L1bHIA=="], + "@sentry/server-utils": ["@sentry/server-utils@10.66.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.18.0", "@apm-js-collab/code-transformer-bundler-plugins": "^0.6.1", "@apm-js-collab/tracing-hooks": "^0.13.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.66.0" } }, "sha512-h9EM9Wz9Mc6w2Vn7Fyh6ozjy4JC2UbUvWf9Ra1HYA4KMeYqjFA5/o0oB4pg4w/TF+rb+9OF/HNqxjuczxpudpA=="], + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], "@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], @@ -849,6 +867,8 @@ "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], @@ -883,7 +903,7 @@ "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], - "cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "cjs-module-lexer": ["cjs-module-lexer@1.2.3", "", {}, "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="], "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], @@ -949,10 +969,14 @@ "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="], - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], @@ -1089,7 +1113,7 @@ "meriyah": ["meriyah@6.1.4", "", {}, "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ=="], - "miniflare": ["miniflare@4.20260625.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260625.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA=="], + "miniflare": ["miniflare@4.20260714.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260714.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-MYlTCLdWCPqvrYY2uLwOjXwmglXuiHE3TGGkbOW4BwjUPa1r07E0iuHwrNDIs/sxK21r+o90Jx58AV2KeNdJZw=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -1267,6 +1291,8 @@ "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], + "semifies": ["semifies@1.0.0", "", {}, "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw=="], + "semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], @@ -1369,7 +1395,7 @@ "wrangler": ["wrangler@4.105.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260625.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260625.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260625.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g=="], - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], @@ -1463,6 +1489,10 @@ "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@cloudflare/vitest-pool-workers/wrangler": ["wrangler@4.112.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260714.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260714.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260714.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-5H+XUD0TySCv1LuktFHDIEOkboH2nTfQs+35L+USt3MtntjDTMVIJprLgQcL2WBjulOyjxpd1vyTiSTJVW5MjQ=="], + + "@cloudflare/vitest-pool-workers/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -1479,6 +1509,10 @@ "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], + "@opentelemetry/sdk-trace/@opentelemetry/core": ["@opentelemetry/core@2.9.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw=="], + + "@opentelemetry/sdk-trace/@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "@prisma/dev/@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], @@ -1495,6 +1529,14 @@ "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="], + "@sentry/cloudflare/@sentry/node": ["@sentry/node@10.66.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/instrumentation": "^0.220.0", "@opentelemetry/sdk-trace-base": "^2.9.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.66.0", "@sentry/node-core": "10.66.0", "@sentry/opentelemetry": "10.66.0", "@sentry/server-utils": "10.66.0", "import-in-the-middle": "^3.0.0" } }, "sha512-5Ow7iQiRjaSaEOmqEIkYV368hFFzShIZCXPoj+wX3JtOmKFrsNu6lr8/VJlQs7cyjFS6tzE50PrLrD8iAPS/8w=="], + + "@sentry/node/@sentry/core": ["@sentry/core@10.51.0", "", {}, "sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w=="], + + "@sentry/node-core/@sentry/core": ["@sentry/core@10.51.0", "", {}, "sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w=="], + + "@sentry/opentelemetry/@sentry/core": ["@sentry/core@10.51.0", "", {}, "sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w=="], + "@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], "@smithy/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], @@ -1587,21 +1629,27 @@ "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "happy-dom/@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="], "happy-dom/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + "happy-dom/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], + "jsdom/parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], "markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], - "miniflare/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "miniflare/workerd": ["workerd@1.20260714.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260714.1", "@cloudflare/workerd-darwin-arm64": "1.20260714.1", "@cloudflare/workerd-linux-64": "1.20260714.1", "@cloudflare/workerd-linux-arm64": "1.20260714.1", "@cloudflare/workerd-windows-64": "1.20260714.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oIbQzfdyl9UQUnG6XLegcSq0Mgt/7WKDbFOoqGgOWCS+/fhyGB460uKEgdAQQ9RHCO/ttcNCX/KiMIQzdoeu3Q=="], "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], @@ -1613,7 +1661,7 @@ "vite/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], - "wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + "wrangler/miniflare": ["miniflare@4.20260625.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260625.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -1637,6 +1685,8 @@ "@aws-sdk/nested-clients/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], + "@cloudflare/vitest-pool-workers/wrangler/workerd": ["workerd@1.20260714.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260714.1", "@cloudflare/workerd-darwin-arm64": "1.20260714.1", "@cloudflare/workerd-linux-64": "1.20260714.1", "@cloudflare/workerd-linux-arm64": "1.20260714.1", "@cloudflare/workerd-windows-64": "1.20260714.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oIbQzfdyl9UQUnG6XLegcSq0Mgt/7WKDbFOoqGgOWCS+/fhyGB460uKEgdAQQ9RHCO/ttcNCX/KiMIQzdoeu3Q=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1697,6 +1747,14 @@ "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="], + "@sentry/cloudflare/@sentry/node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw=="], + + "@sentry/cloudflare/@sentry/node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-trace": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag=="], + + "@sentry/cloudflare/@sentry/node/@sentry/node-core": ["@sentry/node-core@10.66.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.66.0", "@sentry/opentelemetry": "10.66.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base"] }, "sha512-SUnXHROqSdSetKgZC1goDEKCuMz3OmQ1h4rxzWeexLyqan+pensZXfLouP6jzZXlA8e/HP7uQg78LnfqYDcZlQ=="], + + "@sentry/cloudflare/@sentry/node/@sentry/opentelemetry": ["@sentry/opentelemetry@10.66.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.66.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" } }, "sha512-K5Y9IettN9yIOnpqCs40HRLGqaGUoaQ50+ZsLqX2kPCk3TzJVpKZ9icKwaJ4Nxm72QgGVT5Ovfr/9FXbgd3b/Q=="], + "@smithy/core/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], "@smithy/core/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], @@ -1731,8 +1789,70 @@ "bun-types/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "happy-dom/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260714.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZWXqAN8G7Cx9hMRQuk+59ziJhR3j1F4iO+Qs8aHdfKZ3Dq5Yi/57xvkJTgCGBnW1YU/L78r8f6HEy51bwbTpNw=="], + + "miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260714.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tueWxWC3wyCbMG6zRAxsMXX0YLgrRWbiAPYFQ2uJ7dUH8G+5E7UTWaQS9B1HdJ0bpKFW1NWxhs1o2noKVFSUYg=="], + + "miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260714.1", "", { "os": "linux", "cpu": "x64" }, "sha512-1VChTZRb0l0F7R4e1G5RtLKV4oFi6x+rQgxh2+yu887j3l/3TLgatuv1L8/5zhc9gKEhATTxOh0e52Rtd9dDWQ=="], + + "miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260714.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-rMm3G+NirG2UdgHIRDdF1asNC6FqgIzZzkRG+VDhhDGcVxAQwvrMT1E38BivEvHr3G04MB4AfhcOczX0+GtRkQ=="], + + "miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260714.1", "", { "os": "win32", "cpu": "x64" }, "sha512-cGqnU3Hg2YZS/k3SAqrMp1DjpdsyFde72tWltdl6ZT9+SFz/Zrk/8gyTU1TcxC4YApXeNVH5TyU5cOGPgUJ0pg=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -1837,64 +1957,34 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], - "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - - "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - - "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - - "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + "wrangler/miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], - "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - - "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - - "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - - "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - - "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - - "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - - "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - - "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - - "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - - "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - - "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - - "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - - "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260714.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZWXqAN8G7Cx9hMRQuk+59ziJhR3j1F4iO+Qs8aHdfKZ3Dq5Yi/57xvkJTgCGBnW1YU/L78r8f6HEy51bwbTpNw=="], - "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260714.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tueWxWC3wyCbMG6zRAxsMXX0YLgrRWbiAPYFQ2uJ7dUH8G+5E7UTWaQS9B1HdJ0bpKFW1NWxhs1o2noKVFSUYg=="], - "wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260714.1", "", { "os": "linux", "cpu": "x64" }, "sha512-1VChTZRb0l0F7R4e1G5RtLKV4oFi6x+rQgxh2+yu887j3l/3TLgatuv1L8/5zhc9gKEhATTxOh0e52Rtd9dDWQ=="], - "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260714.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-rMm3G+NirG2UdgHIRDdF1asNC6FqgIzZzkRG+VDhhDGcVxAQwvrMT1E38BivEvHr3G04MB4AfhcOczX0+GtRkQ=="], - "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260714.1", "", { "os": "win32", "cpu": "x64" }, "sha512-cGqnU3Hg2YZS/k3SAqrMp1DjpdsyFde72tWltdl6ZT9+SFz/Zrk/8gyTU1TcxC4YApXeNVH5TyU5cOGPgUJ0pg=="], - "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + "@fastify/otel/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], - "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle/cjs-module-lexer": ["cjs-module-lexer@2.2.0", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@sentry/cloudflare/@sentry/node/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w=="], - "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@sentry/cloudflare/@sentry/node/@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.9.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw=="], - "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "@sentry/cloudflare/@sentry/node/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], } } diff --git a/server/api/package.json b/server/api/package.json index 44332b36..9e437174 100644 --- a/server/api/package.json +++ b/server/api/package.json @@ -20,7 +20,9 @@ "worker:deploy:dev": "wrangler deploy --env dev", "worker:deploy:production": "wrangler deploy --env production", "worker:types": "wrangler types", - "worker:secrets:put": "tsx scripts/putWorkerSecrets.ts" + "worker:secrets:put": "tsx scripts/putWorkerSecrets.ts", + "worker:bundle:check": "tsx scripts/checkWorkerBundle.ts", + "test:worker": "vitest run -c vitest.config.worker.ts" }, "dependencies": { "@aws-sdk/client-s3": "^3.1002.0", @@ -35,6 +37,7 @@ "@mozilla/readability": "^0.6.0", "@polar-sh/sdk": "^0.48.0", "@react-email/components": "^1.0.11", + "@sentry/cloudflare": "^10.51.0", "@sentry/node": "^10.51.0", "@tiptap/core": "^3.20.1", "@tiptap/extension-code-block-lowlight": "^3.20.1", @@ -60,6 +63,7 @@ "zod": "^4.4.3" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.18.6", "@cloudflare/workers-types": "^5.20260706.1", "@types/jsdom": "^28.0.0", "@types/node": "^26.0.1", diff --git a/server/api/scripts/checkWorkerBundle.lib.test.ts b/server/api/scripts/checkWorkerBundle.lib.test.ts new file mode 100644 index 00000000..9da61b99 --- /dev/null +++ b/server/api/scripts/checkWorkerBundle.lib.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from "vitest"; +import { FORBIDDEN_MARKERS, findForbiddenMarkers } from "./checkWorkerBundle.lib.js"; + +describe("findForbiddenMarkers — Worker bundle guard (FR-1.2 / FR-2.3 / LC-5)", () => { + it("flags LangGraph package code (esbuild node_modules path comment)", () => { + const hits = findForbiddenMarkers( + "// node_modules/@langchain/langgraph/dist/index.js\nvar x=1;", + ); + expect(hits).toContain("node_modules/@langchain/"); + }); + + it("flags agent source modules", () => { + const hits = findForbiddenMarkers("// src/agents/runner/graphRunner.ts\nvar y=2;"); + expect(hits).toContain("src/agents/"); + }); + + it("flags @hono/node-server and @sentry/node", () => { + expect(findForbiddenMarkers("// node_modules/@hono/node-server/dist/conninfo.js")).toContain( + "node_modules/@hono/node-server/", + ); + expect(findForbiddenMarkers("// node_modules/@sentry/node/build/index.js")).toContain( + "node_modules/@sentry/node/", + ); + }); + + it("flags ioredis (Node-only KV adapter must stay out of the Worker)", () => { + expect(findForbiddenMarkers("// node_modules/ioredis/built/Redis.js")).toContain( + "node_modules/ioredis/", + ); + }); + + it("does not flag @sentry/cloudflare or clean worker code", () => { + const clean = [ + "// node_modules/@sentry/cloudflare/build/index.js", + "// node_modules/hono/dist/index.js", + "// src/lib/clientIp.ts", + 'var app = "zedi";', + ].join("\n"); + expect(findForbiddenMarkers(clean)).toEqual([]); + }); + + it("returns each marker at most once", () => { + const doubled = + "// node_modules/@langchain/core/a.js\n// node_modules/@langchain/langgraph/b.js"; + const hits = findForbiddenMarkers(doubled); + expect(hits.filter((h) => h === "node_modules/@langchain/")).toHaveLength(1); + }); + + it("exposes exactly the five designed markers", () => { + expect([...FORBIDDEN_MARKERS].sort()).toEqual( + [ + "node_modules/@hono/node-server/", + "node_modules/@langchain/", + "node_modules/@sentry/node/", + "node_modules/ioredis/", + "src/agents/", + ].sort(), + ); + }); +}); diff --git a/server/api/scripts/checkWorkerBundle.lib.ts b/server/api/scripts/checkWorkerBundle.lib.ts new file mode 100644 index 00000000..e7246a70 --- /dev/null +++ b/server/api/scripts/checkWorkerBundle.lib.ts @@ -0,0 +1,33 @@ +/** + * Worker バンドル検査の純ロジック(#1091 LC-5)。 + * + * `wrangler deploy --dry-run --outdir` の出力(esbuild は取り込んだモジュールを + * `// node_modules/...` / `// src/...` のパスコメントとして残す)に、Worker に + * 入ってはならない依存が含まれていないかを検査する。 + * + * Pure logic for the Worker bundle guard. Scans the dry-run bundle output for + * forbidden module markers (esbuild leaves per-module path comments). + */ + +/** + * Worker バンドルに存在してはならないマーカー(FR-1.2 / FR-2.3 / LC-4)。 + * - `@langchain/*` と `src/agents/**` — ルート分割で除外(appAgents.ts は Node 専用) + * - `@hono/node-server` — clientIpNode.ts 経由の Node 専用フォールバック + * - `@sentry/node` — Worker は @sentry/cloudflare を使う + * - `ioredis` — Worker は DurableObjectKvStore を使う(createKvStoreNode.ts が Node 専用) + */ +export const FORBIDDEN_MARKERS: readonly string[] = [ + "node_modules/@langchain/", + "src/agents/", + "node_modules/@hono/node-server/", + "node_modules/@sentry/node/", + "node_modules/ioredis/", +]; + +/** + * バンドル内容から検出された禁止マーカーを返す(各マーカー高々 1 回)。 + * Return the forbidden markers found in the bundle content (each at most once). + */ +export function findForbiddenMarkers(content: string): string[] { + return FORBIDDEN_MARKERS.filter((marker) => content.includes(marker)); +} diff --git a/server/api/scripts/checkWorkerBundle.ts b/server/api/scripts/checkWorkerBundle.ts new file mode 100644 index 00000000..e7b523f7 --- /dev/null +++ b/server/api/scripts/checkWorkerBundle.ts @@ -0,0 +1,76 @@ +/** + * Worker バンドル検査 CLI(#1091 LC-5、`bun run worker:bundle:check`)。 + * + * `wrangler deploy --dry-run --outdir --env dev` でバンドルを生成し、 + * 禁止依存(checkWorkerBundle.lib.ts の FORBIDDEN_MARKERS)の不在と gzip 後 + * サイズ(NFR-1: 10MB / Workers Paid)を検査する。資格情報不要(dry-run)の + * ため PR CI・deploy 直前の両方で実行できる。 + * + * Bundle guard CLI. Builds the Worker via a credential-free wrangler dry-run, + * fails on forbidden dependencies, and reports the gzipped bundle size. + */ +import { execSync } from "node:child_process"; +import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { gzipSync } from "node:zlib"; +import { findForbiddenMarkers } from "./checkWorkerBundle.lib.js"; + +/** NFR-1: Workers Paid プランの gzip 後バンドル上限。 */ +const MAX_GZIP_BYTES = 10 * 1024 * 1024; + +function listFilesRecursive(dir: string): string[] { + return readdirSync(dir).flatMap((name) => { + const full = join(dir, name); + return statSync(full).isDirectory() ? listFilesRecursive(full) : [full]; + }); +} + +function main(): void { + const outdir = mkdtempSync(join(tmpdir(), "zedi-worker-bundle-")); + try { + execSync(`bunx wrangler deploy --dry-run --outdir "${outdir}" --env dev`, { + stdio: ["ignore", "inherit", "inherit"], + cwd: join(import.meta.dirname, ".."), + }); + + const jsFiles = listFilesRecursive(outdir).filter((f) => f.endsWith(".js")); + if (jsFiles.length === 0) { + console.error(`[bundle-check] no .js output found in ${outdir}`); + process.exit(1); + } + + let totalGzip = 0; + const violations: { file: string; markers: string[] }[] = []; + for (const file of jsFiles) { + const content = readFileSync(file, "utf8"); + totalGzip += gzipSync(content).byteLength; + const markers = findForbiddenMarkers(content); + if (markers.length > 0) violations.push({ file, markers }); + } + + const mb = (totalGzip / (1024 * 1024)).toFixed(2); + console.log(`[bundle-check] gzip total: ${mb} MB (limit ${MAX_GZIP_BYTES / 1024 / 1024} MB)`); + + if (violations.length > 0) { + for (const v of violations) { + console.error(`[bundle-check] FORBIDDEN in ${v.file}: ${v.markers.join(", ")}`); + } + console.error( + "[bundle-check] Worker bundle must not contain LangGraph/agents, @hono/node-server, or @sentry/node (#1091 FR-1.2 / FR-2.3).", + ); + process.exit(1); + } + if (totalGzip > MAX_GZIP_BYTES) { + console.error( + `[bundle-check] bundle exceeds the ${MAX_GZIP_BYTES / 1024 / 1024} MB gzip limit`, + ); + process.exit(1); + } + console.log("[bundle-check] OK — no forbidden dependencies in the Worker bundle"); + } finally { + rmSync(outdir, { recursive: true, force: true }); + } +} + +main(); diff --git a/server/api/src/__tests__/app.test.ts b/server/api/src/__tests__/app.test.ts new file mode 100644 index 00000000..4594ca4a --- /dev/null +++ b/server/api/src/__tests__/app.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, vi } from "vitest"; + +// auth.js は module-load 時に Better Auth を初期化するため、ルーティング +// テーブルの検証には不要な env 依存を避けてスタブする。 +vi.mock("../auth.js", () => ({ + auth: { handler: vi.fn(async () => new Response(null, { status: 200 })) }, +})); + +import { createApp } from "../app.js"; + +/** app.routes からパス一覧を取り出す(メソッド不問・重複許容) */ +function registeredPaths(app: { routes: { path: string }[] }): string[] { + return app.routes.map((r) => r.path); +} + +describe("createApp — agent route split (FR-1)", () => { + it("registers no agent routes by default (worker entry shape)", () => { + const app = createApp(); + const paths = registeredPaths(app); + expect(paths.some((p) => p.startsWith("/api/ingest"))).toBe(false); + expect(paths.some((p) => p.includes("compose-sessions"))).toBe(false); + }); + + it("invokes registerAgentRoutes hook exactly once with the app", () => { + const hook = vi.fn(); + const app = createApp({ registerAgentRoutes: hook }); + expect(hook).toHaveBeenCalledTimes(1); + expect(hook).toHaveBeenCalledWith(app); + }); + + it("keeps non-agent routes registered regardless of the hook", () => { + const app = createApp(); + const paths = registeredPaths(app); + expect(paths.some((p) => p.startsWith("/api/pages"))).toBe(true); + expect(paths.some((p) => p.startsWith("/api/media"))).toBe(true); + }); +}); diff --git a/server/api/src/__tests__/lib/clientIp.test.ts b/server/api/src/__tests__/lib/clientIp.test.ts new file mode 100644 index 00000000..1a33d516 --- /dev/null +++ b/server/api/src/__tests__/lib/clientIp.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, afterEach, vi } from "vitest"; +import { Hono } from "hono"; +import type { AppEnv } from "../../types/index.js"; +import { extractClientIp, setSocketIpResolver } from "../../lib/clientIp.js"; + +function buildApp() { + const app = new Hono(); + app.get("/ip", (c) => c.json({ ip: extractClientIp(c) })); + return app; +} + +async function requestIp( + headers: Record, + env?: Record, +): Promise { + const res = await buildApp().request("/ip", { headers }, env); + const body = (await res.json()) as { ip: string | null }; + return body.ip; +} + +afterEach(() => { + vi.unstubAllEnvs(); + setSocketIpResolver(null); +}); + +describe("extractClientIp — Workers runtime (FR-2 / SR-5)", () => { + it("prefers CF-Connecting-IP over spoofed x-forwarded-for without TRUST_PROXY", async () => { + const ip = await requestIp( + { "cf-connecting-ip": "203.0.113.7", "x-forwarded-for": "198.51.100.99" }, + { RUNTIME: "cloudflare-workers" }, + ); + expect(ip).toBe("203.0.113.7"); + }); + + it("falls back to trusted proxy headers when CF header is absent", async () => { + vi.stubEnv("TRUST_PROXY", "true"); + const ip = await requestIp( + { "x-forwarded-for": "203.0.113.1, 10.0.0.1" }, + { RUNTIME: "cloudflare-workers" }, + ); + expect(ip).toBe("203.0.113.1"); + }); +}); + +describe("extractClientIp — Node runtime", () => { + it("ignores CF-Connecting-IP when RUNTIME is not cloudflare-workers (spoofing guard)", async () => { + const ip = await requestIp({ "cf-connecting-ip": "203.0.113.7" }); + expect(ip).toBeNull(); + }); + + it("keeps x-forwarded-for-first behavior under TRUST_PROXY=true", async () => { + vi.stubEnv("TRUST_PROXY", "true"); + const ip = await requestIp({ "x-forwarded-for": "203.0.113.1, 10.0.0.1" }); + expect(ip).toBe("203.0.113.1"); + }); + + it("uses the injected socket resolver as the fallback", async () => { + setSocketIpResolver(() => "10.0.0.5"); + const ip = await requestIp({}); + expect(ip).toBe("10.0.0.5"); + }); + + it("returns null when no resolver is injected and proxy is untrusted", async () => { + const ip = await requestIp({ "x-forwarded-for": "198.51.100.99" }); + expect(ip).toBeNull(); + }); +}); diff --git a/server/api/src/__tests__/lib/kv/createKvStore.test.ts b/server/api/src/__tests__/lib/kv/createKvStore.test.ts new file mode 100644 index 00000000..41031801 --- /dev/null +++ b/server/api/src/__tests__/lib/kv/createKvStore.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { createKvStore, setNodeKvStoreFactory } from "../../../lib/kv/createKvStore.js"; +import { DurableObjectKvStore } from "../../../lib/kv/doKvStore.js"; +import type { KvStore } from "../../../lib/kv/types.js"; + +afterEach(() => { + setNodeKvStoreFactory(null); +}); + +const fakeKv = {} as KvStore; + +describe("createKvStore — Node adapter injection (FR-5 / module boundary)", () => { + it("returns null when no KV_DO binding and no Node factory installed (Workers default)", () => { + expect(createKvStore(undefined)).toBeNull(); + expect(createKvStore({})).toBeNull(); + }); + + it("uses the injected Node factory when no KV_DO binding exists", () => { + setNodeKvStoreFactory(() => fakeKv); + expect(createKvStore(undefined)).toBe(fakeKv); + }); + + it("prefers the KV_DO Durable Object binding over the Node factory", () => { + setNodeKvStoreFactory(() => fakeKv); + const kvDoNamespace = { + idFromName: () => ({}), + get: () => ({}), + } as unknown as NonNullable[0]>["KV_DO"]; + const store = createKvStore({ KV_DO: kvDoNamespace }); + expect(store).toBeInstanceOf(DurableObjectKvStore); + }); +}); diff --git a/server/api/src/__tests__/lib/sentryWorkerOptions.test.ts b/server/api/src/__tests__/lib/sentryWorkerOptions.test.ts new file mode 100644 index 00000000..137d2b64 --- /dev/null +++ b/server/api/src/__tests__/lib/sentryWorkerOptions.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from "vitest"; +import { buildSentryOptions } from "../../lib/sentryWorkerOptions.js"; + +describe("buildSentryOptions — Worker Sentry config (FR-3)", () => { + it("uses the trimmed SENTRY_DSN_API and the ENVIRONMENT var", () => { + const opts = buildSentryOptions({ + SENTRY_DSN_API: " https://key@o0.ingest.sentry.io/1 ", + ENVIRONMENT: "development", + }); + expect(opts.dsn).toBe("https://key@o0.ingest.sentry.io/1"); + expect(opts.environment).toBe("development"); + }); + + it("no-ops via undefined dsn when SENTRY_DSN_API is unset or blank (FR-3.3)", () => { + expect(buildSentryOptions({}).dsn).toBeUndefined(); + expect(buildSentryOptions({ SENTRY_DSN_API: " " }).dsn).toBeUndefined(); + }); + + it("never sends default PII and keeps the health-check denyUrls", () => { + const opts = buildSentryOptions({}); + expect(opts.sendDefaultPii).toBe(false); + expect(opts.denyUrls?.some((re) => re.test("/api/health"))).toBe(true); + }); + + it("scrubs PII via beforeSend (same scrub as the Node runtime)", () => { + const opts = buildSentryOptions({}); + const scrubbed = opts.beforeSend?.({ + user: { email: "user@example.com" }, + }); + expect(scrubbed?.user?.email).toBe("[Filtered]"); + }); +}); diff --git a/server/api/src/__tests__/middleware/errorHandler.test.ts b/server/api/src/__tests__/middleware/errorHandler.test.ts index 280b94d3..0e12b3dc 100644 --- a/server/api/src/__tests__/middleware/errorHandler.test.ts +++ b/server/api/src/__tests__/middleware/errorHandler.test.ts @@ -5,31 +5,22 @@ * - サービス層が throw する `new Error("UNAUTHORIZED")` などの * "magic message" は statusMap に従って HTTP ステータスへ写像される。 * - 未知のエラーは 500 を返し、message は必ず「Internal server error」に sanitize する。 - * - Sentry capture は実装側の `shouldCaptureApiException` の判定に従い、 - * `captureApiException` だけを差し替える partial mock で検証する。 + * - Sentry capture は DI(`createErrorHandler({ captureApiException })`)で注入し、 + * 実装側の `shouldCaptureApiException` の判定に従うことを検証する(#1091 LC-4: + * Worker バンドルに @sentry/node を入れないため、errorHandler は SDK を + * 静的 import せず capture 関数を注入で受け取る)。 * * Unit tests for the global Hono error handler. Covers HTTPException pass-through, * the magic-message → status mapping, the unknown-error 500 default, and the - * Sentry capture policy by partially mocking only `captureApiException`. + * Sentry capture policy via the injected capture function (DI — no SDK import). */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { Hono } from "hono"; import { HTTPException } from "hono/http-exception"; import type { AppEnv } from "../../types/index.js"; +import { createErrorHandler } from "../../middleware/errorHandler.js"; -const sentryMock = vi.hoisted(() => ({ - captureApiException: vi.fn(), -})); - -vi.mock("../../lib/sentry.js", async () => { - const actual = await vi.importActual("../../lib/sentry.js"); - return { - ...actual, - captureApiException: sentryMock.captureApiException, - }; -}); - -import { errorHandler } from "../../middleware/errorHandler.js"; +const captureSpy = vi.fn(); /** * Build an app whose `/throw` route throws the supplied error. @@ -37,7 +28,7 @@ import { errorHandler } from "../../middleware/errorHandler.js"; */ function appThrowing(err: unknown) { const app = new Hono(); - app.onError(errorHandler); + app.onError(createErrorHandler({ captureApiException: captureSpy })); app.get("/throw", () => { throw err; }); @@ -55,7 +46,7 @@ describe("errorHandler", () => { afterEach(() => { errorSpy.mockRestore(); - sentryMock.captureApiException.mockReset(); + captureSpy.mockReset(); }); describe("HTTPException pass-through", () => { @@ -85,7 +76,7 @@ describe("errorHandler", () => { async (status) => { await appThrowing(new HTTPException(status, { message: "expected" })).request("/throw"); - expect(sentryMock.captureApiException).not.toHaveBeenCalled(); + expect(captureSpy).not.toHaveBeenCalled(); }, ); @@ -94,7 +85,7 @@ describe("errorHandler", () => { await appThrowing(err).request("/throw"); - expect(sentryMock.captureApiException).toHaveBeenCalledWith( + expect(captureSpy).toHaveBeenCalledWith( err, status, expect.objectContaining({ @@ -134,7 +125,7 @@ describe("errorHandler", () => { ] as const)("does not capture expected Error('%s') as %d", async (message, _expectedStatus) => { await appThrowing(new Error(message)).request("/throw"); - expect(sentryMock.captureApiException).not.toHaveBeenCalled(); + expect(captureSpy).not.toHaveBeenCalled(); }); it.each([ @@ -148,7 +139,7 @@ describe("errorHandler", () => { await appThrowing(err).request("/throw"); - expect(sentryMock.captureApiException).toHaveBeenCalledWith( + expect(captureSpy).toHaveBeenCalledWith( err, expectedStatus, expect.objectContaining({ @@ -163,14 +154,14 @@ describe("errorHandler", () => { // Ensures we forward the route pattern (e.g., `/invite/:token`) so that // request-time capability tokens never reach Sentry through `extra`. const app = new Hono(); - app.onError(errorHandler); + app.onError(createErrorHandler({ captureApiException: captureSpy })); app.get("/invite/:token", () => { throw new Error("kapow"); }); await app.request("/invite/secret-capability-token"); - expect(sentryMock.captureApiException).toHaveBeenCalledWith( + expect(captureSpy).toHaveBeenCalledWith( expect.any(Error), 500, expect.objectContaining({ method: "GET", routePath: "/invite/:token" }), @@ -178,6 +169,21 @@ describe("errorHandler", () => { }); }); + describe("default handler (no capture injected — worker-safe no-op)", () => { + it("still shapes errors without any capture function", async () => { + const app = new Hono(); + app.onError(createErrorHandler()); + app.get("/throw", () => { + throw new Error('relation "pages" does not exist'); + }); + + const res = await app.request("/throw"); + expect(res.status).toBe(500); + const body = (await res.json()) as { error: string }; + expect(body.error).toBe("Internal server error"); + }); + }); + describe("unknown errors", () => { it("returns 500 for an Error with an unmapped message and hides the raw message", async () => { // 5xx では DB/内部の生メッセージを露出させず、固定文言に差し替える。 diff --git a/server/api/src/__tests__/services/articleExtractor.test.ts b/server/api/src/__tests__/services/articleExtractor.test.ts index 34bdaf25..cfc9fd1d 100644 --- a/server/api/src/__tests__/services/articleExtractor.test.ts +++ b/server/api/src/__tests__/services/articleExtractor.test.ts @@ -148,38 +148,44 @@ describe("extractArticleFromUrl", () => { expect(mockFetchHtml).not.toHaveBeenCalled(); }); - it("通常記事を取得して Tiptap JSON・プレビュー・ハッシュを返す / fetches a normal article and returns Tiptap JSON, preview and hash", async () => { - mockFetchHtml.mockResolvedValue({ - html: ARTICLE_HTML, - finalUrl: "https://blog.example.com/post", - contentType: "text/html", - }); - - const result = await extractArticleFromUrl({ - url: "https://blog.example.com/post", - previewLength: 30, - }); - - expect(result.finalUrl).toBe("https://blog.example.com/post"); - expect(result.title).toBe("Sample Article Title"); - // og:image の相対 URL は finalUrl を基準に絶対化される。 - // The relative og:image is resolved against finalUrl. - expect(result.thumbnailUrl).toBe("https://blog.example.com/cover.png"); - // 先頭に OGP 画像ノードが差し込まれる。 - // The OGP image node is prepended to the document content. - expect(result.tiptapJson.type).toBe("doc"); - expect(result.tiptapJson.content?.[0]).toEqual({ - type: "image", - attrs: { src: "https://blog.example.com/cover.png", alt: "Sample Article Title" }, - }); - // contentText は previewLength で切り詰められる。 - // contentText is truncated to previewLength characters. - expect(result.contentText).toHaveLength(30); - // contentHash は本文の SHA-256(64 桁の 16 進)。 - // contentHash is a SHA-256 hex digest (64 chars). - expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/); - expect(result.aiUsage).toBeUndefined(); - }); + // jsdom は遅延 import (#1091) のため、最初に踏むテストはモジュールロード分の + // 時間を要する。timeout を引き上げる(jsdom lazy-load pays its cost here)。 + it( + "通常記事を取得して Tiptap JSON・プレビュー・ハッシュを返す / fetches a normal article and returns Tiptap JSON, preview and hash", + { timeout: 30_000 }, + async () => { + mockFetchHtml.mockResolvedValue({ + html: ARTICLE_HTML, + finalUrl: "https://blog.example.com/post", + contentType: "text/html", + }); + + const result = await extractArticleFromUrl({ + url: "https://blog.example.com/post", + previewLength: 30, + }); + + expect(result.finalUrl).toBe("https://blog.example.com/post"); + expect(result.title).toBe("Sample Article Title"); + // og:image の相対 URL は finalUrl を基準に絶対化される。 + // The relative og:image is resolved against finalUrl. + expect(result.thumbnailUrl).toBe("https://blog.example.com/cover.png"); + // 先頭に OGP 画像ノードが差し込まれる。 + // The OGP image node is prepended to the document content. + expect(result.tiptapJson.type).toBe("doc"); + expect(result.tiptapJson.content?.[0]).toEqual({ + type: "image", + attrs: { src: "https://blog.example.com/cover.png", alt: "Sample Article Title" }, + }); + // contentText は previewLength で切り詰められる。 + // contentText is truncated to previewLength characters. + expect(result.contentText).toHaveLength(30); + // contentHash は本文の SHA-256(64 桁の 16 進)。 + // contentHash is a SHA-256 hex digest (64 chars). + expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/); + expect(result.aiUsage).toBeUndefined(); + }, + ); it("og:image が無ければ thumbnailUrl は null で画像ノードを差し込まない / no thumbnail node when og:image is absent", async () => { const noImageHtml = ARTICLE_HTML.replace(/]*>/, ""); diff --git a/server/api/src/__tests__/worker/kvDurableObject.test.ts b/server/api/src/__tests__/worker/kvDurableObject.test.ts new file mode 100644 index 00000000..5e5fa64c --- /dev/null +++ b/server/api/src/__tests__/worker/kvDurableObject.test.ts @@ -0,0 +1,49 @@ +/** + * DurableObjectKvStore の workerd 実行テスト(#1091 SR-6 全 3 用途)。 + * + * - `incrWithTtl` — レート制限固定ウィンドウカウンタ + * - `getdel` — ext/MCP ワンタイムコードの原子消費 + * - `setex` + `get` — MCP 失効 deny-list + * + * 実 DO binding(wrangler.jsonc の KV_DO → KvDurableObject)に対して実行する。 + * Exercises all three security-relevant KvStore usages against the real + * Durable Object binding under workerd. + */ +/// +import { describe, it, expect } from "vitest"; +import { env } from "cloudflare:test"; +import { DurableObjectKvStore } from "../../lib/kv/doKvStore.js"; + +function store() { + const kvDo = (env as unknown as Record).KV_DO; + return new DurableObjectKvStore(kvDo as ConstructorParameters[0]); +} + +describe("DurableObjectKvStore on workerd (SR-6)", () => { + it("incrWithTtl counts within a fixed window (rate-limit counter)", async () => { + const kv = store(); + const key = `test:rate:${crypto.randomUUID()}`; + const first = await kv.incrWithTtl(key, 60); + const second = await kv.incrWithTtl(key, 60); + expect(first).toBe(1); + expect(second).toBe(2); + }); + + it("getdel consumes a one-time code atomically", async () => { + const kv = store(); + const key = `test:code:${crypto.randomUUID()}`; + await kv.setex(key, 60, "one-time-value"); + const first = await kv.getdel(key); + const second = await kv.getdel(key); + expect(first).toBe("one-time-value"); + expect(second).toBeNull(); + }); + + it("setex + get serve the deny-list lookup", async () => { + const kv = store(); + const key = `test:deny:${crypto.randomUUID()}`; + expect(await kv.get(key)).toBeNull(); + await kv.setex(key, 60, "revoked"); + expect(await kv.get(key)).toBe("revoked"); + }); +}); diff --git a/server/api/src/__tests__/worker/presign.test.ts b/server/api/src/__tests__/worker/presign.test.ts new file mode 100644 index 00000000..5533c437 --- /dev/null +++ b/server/api/src/__tests__/worker/presign.test.ts @@ -0,0 +1,29 @@ +/** + * `@aws-sdk` presign の workerd 動作確認(#1091 FR-4.1 / TS-5)。 + * + * presign は純粋な署名計算(ネットワーク不要)のため、ダミー資格情報で + * `nodejs_compat` 下の workerd 上で URL が生成できることを確認する。 + * Verifies S3-compatible presigned-URL generation works under workerd's + * nodejs_compat with dummy credentials (pure signing, no network). + */ +import { describe, it, expect } from "vitest"; +import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +describe("@aws-sdk presign on workerd (FR-4.1)", () => { + it("generates a signed URL with the R2 S3-compatible endpoint shape", async () => { + const client = new S3Client({ + region: "auto", + endpoint: "https://0123456789abcdef.r2.cloudflarestorage.com", + credentials: { accessKeyId: "test-access-key", secretAccessKey: "test-secret-key" }, + }); + const url = await getSignedUrl( + client, + new GetObjectCommand({ Bucket: "zedi-storage-dev", Key: "media/example.png" }), + { expiresIn: 60 }, + ); + expect(url).toContain("zedi-storage-dev"); + expect(url).toContain("X-Amz-Signature="); + expect(url).toContain("X-Amz-Expires=60"); + }); +}); diff --git a/server/api/src/__tests__/worker/worker.test.ts b/server/api/src/__tests__/worker/worker.test.ts new file mode 100644 index 00000000..8c9c9afb --- /dev/null +++ b/server/api/src/__tests__/worker/worker.test.ts @@ -0,0 +1,91 @@ +/** + * Worker エントリ(src/worker.ts)の workerd 実行テスト(#1091 FR-5.2)。 + * + * 対象: /api/health のランタイム判定、CORS プリフライト、ルート分割の登録有無、 + * auth マウントのランタイム適合(例外なく整形済みレスポンス)、エラーマスキング。 + * DB 依存の成功系は #1090(D1)後 — ここでは「クラッシュせずマスクされる」まで。 + * + * workerd-runtime tests for the Worker entry: health runtime field, CORS + * preflight, agent-route exclusion, auth mount runtime compatibility, and + * masked error shaping. DB-backed success paths are deferred to #1090. + */ +/// +import { describe, it, expect } from "vitest"; +import { env, createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; +import worker from "../../worker.js"; + +async function dispatch(path: string, init?: RequestInit): Promise { + const ctx = createExecutionContext(); + const request = new Request(`https://zedi-api-dev.test${path}`, init); + const handler = worker as unknown as { + fetch: (req: unknown, env: unknown, ctx: unknown) => Promise; + }; + const res = await handler.fetch(request, env, ctx); + await waitOnExecutionContext(ctx); + return res as Response; +} + +describe("worker entry — health & runtime (FR-6.2)", () => { + it("serves /api/health with runtime=cloudflare-workers", async () => { + const res = await dispatch("/api/health"); + expect(res.status).toBe(200); + const body = (await res.json()) as { runtime?: string }; + expect(body.runtime).toBe("cloudflare-workers"); + }); +}); + +describe("worker entry — CORS preflight (SR-7)", () => { + it("answers OPTIONS preflight without a runtime error", async () => { + const res = await dispatch("/api/health", { + method: "OPTIONS", + headers: { + Origin: "https://zedi-note.app", + "Access-Control-Request-Method": "GET", + }, + }); + expect([200, 204]).toContain(res.status); + expect(res.headers.get("access-control-allow-methods")).toBeTruthy(); + }); +}); + +describe("worker entry — agent route split (FR-1.4)", () => { + it("returns 404 for /api/ingest (excluded from the Worker)", async () => { + // CSRF ミドルウェアの 403 で遮られないよう許可オリジンを付け、 + // ルーティングの不在(404 fallback)そのものを検証する。 + const res = await dispatch("/api/ingest", { + method: "POST", + headers: { Origin: "https://zedi-note.app", "Content-Type": "application/json" }, + body: "{}", + }); + expect(res.status).toBe(404); + }); + + it("returns 404 for compose-sessions routes (excluded from the Worker)", async () => { + const res = await dispatch("/api/pages/00000000-0000-0000-0000-000000000000/compose-sessions"); + expect(res.status).toBe(404); + }); +}); + +describe("worker entry — auth mount runtime compatibility (FR-5.2 / A-2)", () => { + it("responds to /api/auth/get-session as well-formed JSON (no workerd crash)", async () => { + const res = await dispatch("/api/auth/get-session"); + // DB 未接続のため成功は要求しない。Worker ランタイム例外(fetch reject)に + // ならず、HTTP レスポンスとして整形されていることが合格条件。 + expect(res.status).toBeGreaterThanOrEqual(200); + expect(res.status).toBeLessThan(600); + }); +}); + +describe("worker entry — masked error shaping (RL-3 / SR-3)", () => { + it("shapes DB-dependent route failures as JSON, never a 1101 crash", async () => { + // /api/users/me は認証→DB 到達で失敗し得るが、onError で必ず整形される。 + const res = await dispatch("/api/users/me"); + expect(res.status).toBeGreaterThanOrEqual(400); + const body = (await res.json()) as { error?: string }; + expect(typeof body.error).toBe("string"); + // 5xx の場合は内部メッセージが漏れていないこと。 + if (res.status >= 500) { + expect(body.error).toBe("Internal server error"); + } + }); +}); diff --git a/server/api/src/app.ts b/server/api/src/app.ts index 070ba636..ce4ca085 100644 --- a/server/api/src/app.ts +++ b/server/api/src/app.ts @@ -1,7 +1,8 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; import { getAllowedOrigins, isWildcardCors } from "./lib/cors.js"; -import { errorHandler } from "./middleware/errorHandler.js"; +import { createErrorHandler } from "./middleware/errorHandler.js"; +import type { ApiExceptionCapture } from "./lib/sentryShared.js"; import { csrfOriginCheck } from "./middleware/csrfOrigin.js"; import { dbMiddleware } from "./middleware/db.js"; import { kvMiddleware } from "./middleware/kv.js"; @@ -18,7 +19,6 @@ import noteRoutes from "./routes/notes/index.js"; import searchRoutes from "./routes/search.js"; import mediaRoutes from "./routes/media.js"; import clipRoutes from "./routes/clip.js"; -import ingestRoutes from "./routes/ingest.js"; import pdfSourcesRoutes from "./routes/pdfSources.js"; import wikiSchemaRoutes from "./routes/wikiSchema.js"; import extRoutes from "./routes/ext.js"; @@ -44,34 +44,32 @@ import lintRoutes from "./routes/lint.js"; import activityRoutes from "./routes/activity.js"; import onboardingRoutes from "./routes/onboarding.js"; import internalRoutes from "./routes/internal.js"; -import composeSessionRoutes from "./routes/composeSessions.js"; import userAiCredentialRoutes from "./routes/userAiCredentials.js"; -import { registerStubGraph } from "./agents/registry/stubGraph.js"; -import { registerResearchLoopGraph } from "./agents/subgraphs/research/index.js"; -import { registerWikiComposeGraph } from "./agents/graphs/wikiCompose/index.js"; -import { registerIngestPlannerGraph } from "./agents/graphs/ingest/index.js"; -import { registerWikiMaintenanceGraph } from "./agents/graphs/wikiMaintenance/index.js"; + +/** + * createApp のオプション。 + * Options for {@link createApp}. + */ +export interface CreateAppOptions { + /** + * LangGraph 依存ルート(compose-sessions / ingest)と graph 登録を行うフック。 + * Node エントリ(index.ts)だけが appAgents.ts の実装を渡す。Worker エントリは + * 渡さないことで `@langchain/*` をバンドルから物理的に除外する(#1091 FR-1)。 + */ + registerAgentRoutes?: (app: Hono) => void; + + /** + * errorHandler に注入する Sentry capture 関数。index.ts は @sentry/node、 + * worker.ts は @sentry/cloudflare の実装を渡す。未指定なら no-op。 + */ + captureApiException?: ApiExceptionCapture; +} /** * Creates and configures the Hono API app (routes, CORS, etc.). * Hono APIアプリを作成・設定する(ルート・CORS等)。 */ -export function createApp(): Hono { - // Wiki Compose graphs を registry に登録する。いずれも idempotent。 - // - `wiki-compose-stub` — P0 smoke test (#948) - // - `wiki-compose-research` — P1 自律調査ループ (#949) - // - `wiki-compose` — P2 全体オーケストレータ (#950) - // - `ingest-planner` — P4 ingest + shared research loop (#952) - // - `wiki-maintenance` — P5 broken links + stub scan (#953) - // - // Register all Wiki Compose graphs. Calls are idempotent across hot - // reloads (registry uses `Map#set` so the latest registration wins). - registerStubGraph(); - registerResearchLoopGraph(); - registerWikiComposeGraph(); - registerIngestPlannerGraph(); - registerWikiMaintenanceGraph(); - +export function createApp(options: CreateAppOptions = {}): Hono { const app = new Hono(); const wildcard = isWildcardCors(); const allowedOrigins = getAllowedOrigins(); @@ -94,7 +92,7 @@ export function createApp(): Hono { app.use("*", dbMiddleware); app.use("*", kvMiddleware); app.use("*", storageMiddleware); - app.onError(errorHandler); + app.onError(createErrorHandler({ captureApiException: options.captureApiException })); // Better Auth の magicLink プラグインは `POST /api/auth/sign-in/magic-link` を // 自動で登録するが、これを公開すると招待トークン検証やレート制限を経由せずに @@ -163,10 +161,6 @@ export function createApp(): Hono { // Page Snapshots (version history) app.route("/api/pages", pageSnapshotRoutes); - // Wiki Compose sessions (LangGraph runs) — issue #948. - // `/api/pages/:pageId/compose-sessions[/:id[/run|/resume]]` - app.route("/api/pages", composeSessionRoutes); - // Sync app.route("/api/sync/pages", syncPageRoutes); @@ -188,9 +182,6 @@ export function createApp(): Hono { // Clip app.route("/api/clip", clipRoutes); - // Ingest (LLM Wiki pattern, P1) - app.route("/api/ingest", ingestRoutes); - // Local PDF sources + highlights (issue otomatty/zedi#389). // PDF binaries never reach the server — only hashes / page counts / highlights. // ローカル PDF のソース行とハイライト (issue otomatty/zedi#389)。 @@ -230,6 +221,10 @@ export function createApp(): Hono { app.route("/api/thumbnail/commit", thumbCommitRoutes); app.route("/api/thumbnail/serve", thumbServeRoutes); + // LangGraph 依存ルート(compose-sessions / ingest)はフック経由でのみ登録される。 + // パスが他ルートと競合しないため、404 フォールバック直前の一括登録で足りる。 + options.registerAgentRoutes?.(app); + // 404 fallback(要求パスを返してデバッグしやすくする) app.all("*", (c) => c.json({ error: "Not found", path: c.req.path, method: c.req.method }, 404)); diff --git a/server/api/src/appAgents.ts b/server/api/src/appAgents.ts new file mode 100644 index 00000000..e0124ab3 --- /dev/null +++ b/server/api/src/appAgents.ts @@ -0,0 +1,40 @@ +import type { Hono } from "hono"; +import type { AppEnv } from "./types/index.js"; + +import composeSessionRoutes from "./routes/composeSessions.js"; +import ingestRoutes from "./routes/ingest.js"; +import { registerStubGraph } from "./agents/registry/stubGraph.js"; +import { registerResearchLoopGraph } from "./agents/subgraphs/research/index.js"; +import { registerWikiComposeGraph } from "./agents/graphs/wikiCompose/index.js"; +import { registerIngestPlannerGraph } from "./agents/graphs/ingest/index.js"; +import { registerWikiMaintenanceGraph } from "./agents/graphs/wikiMaintenance/index.js"; + +/** + * LangGraph 依存のルート・graph 登録をまとめて行う(Node エントリ専用)。 + * Worker バンドルから `@langchain/*` / `src/agents/**` を除外するため、 + * このモジュールは `index.ts` だけが import する(worker.ts は import 禁止 — + * `worker:bundle:check` が不在を検査する)。#1091 FR-1。 + */ +export function registerAgentRoutes(app: Hono): void { + // Wiki Compose graphs を registry に登録する。いずれも idempotent。 + // - `wiki-compose-stub` — P0 smoke test (#948) + // - `wiki-compose-research` — P1 自律調査ループ (#949) + // - `wiki-compose` — P2 全体オーケストレータ (#950) + // - `ingest-planner` — P4 ingest + shared research loop (#952) + // - `wiki-maintenance` — P5 broken links + stub scan (#953) + // + // Register all Wiki Compose graphs. Calls are idempotent across hot + // reloads (registry uses `Map#set` so the latest registration wins). + registerStubGraph(); + registerResearchLoopGraph(); + registerWikiComposeGraph(); + registerIngestPlannerGraph(); + registerWikiMaintenanceGraph(); + + // Wiki Compose sessions (LangGraph runs) — issue #948. + // `/api/pages/:pageId/compose-sessions[/:id[/run|/resume]]` + app.route("/api/pages", composeSessionRoutes); + + // Ingest (LLM Wiki pattern, P1) + app.route("/api/ingest", ingestRoutes); +} diff --git a/server/api/src/auth.ts b/server/api/src/auth.ts index 3e1ae78c..a3525a95 100644 --- a/server/api/src/auth.ts +++ b/server/api/src/auth.ts @@ -46,10 +46,6 @@ export const auth = betterAuth({ clientId: getEnv("GOOGLE_CLIENT_ID"), clientSecret: getEnv("GOOGLE_CLIENT_SECRET"), }, - github: { - clientId: getEnv("GITHUB_CLIENT_ID"), - clientSecret: getEnv("GITHUB_CLIENT_SECRET"), - }, }, session: { diff --git a/server/api/src/index.ts b/server/api/src/index.ts index d9354f35..f17e4f82 100644 --- a/server/api/src/index.ts +++ b/server/api/src/index.ts @@ -1,9 +1,14 @@ import { serve } from "@hono/node-server"; import { createApp } from "./app.js"; -import { initSentry } from "./lib/sentry.js"; +import { registerAgentRoutes } from "./appAgents.js"; +import { captureApiException, initSentry } from "./lib/sentry.js"; +import { installNodeSocketIpResolver } from "./lib/clientIpNode.js"; +import { installNodeKvStoreFactory } from "./lib/kv/createKvStoreNode.js"; initSentry(); -const app = createApp(); +installNodeSocketIpResolver(); +installNodeKvStoreFactory(); +const app = createApp({ registerAgentRoutes, captureApiException }); const port = parseInt(process.env.PORT || "3000", 10); serve({ fetch: app.fetch, port }, (info) => { diff --git a/server/api/src/lib/clientIp.ts b/server/api/src/lib/clientIp.ts index 76977cd3..e4685b85 100644 --- a/server/api/src/lib/clientIp.ts +++ b/server/api/src/lib/clientIp.ts @@ -5,19 +5,44 @@ * 信頼してよいかは「アプリの直前にプロキシ(Railway, Cloudflare, nginx, ...)が * 必ず存在し、かつそれが正しく XFF を上書きする構成か」に依存する。 * 環境変数 `TRUST_PROXY=true` のときだけプロキシヘッダを採用し、それ以外では - * ソケット由来の peer IP(`@hono/node-server` の conninfo)を使う。これにより、 - * 直接公開されたサーバや、信頼できないプロキシ経由のリクエストで XFF を偽装した - * クライアントが per-IP レートリミットを回避できないようにする。 + * ソケット由来の peer IP を使う。これにより、直接公開されたサーバや、信頼できない + * プロキシ経由のリクエストで XFF を偽装したクライアントが per-IP レートリミットを + * 回避できないようにする。 + * + * Workers ランタイム(`RUNTIME=cloudflare-workers` — wrangler.jsonc の vars で + * 明示宣言)では、Cloudflare がエッジで必ず上書きする `CF-Connecting-IP` を + * 第一候補にする。ヘッダの存在ではなく RUNTIME シグナルでゲートするのは、 + * Node/Railway 経路で攻撃者が同名ヘッダを自称する偽装を排除するため(#1091 SR-5)。 + * + * ソケットフォールバックは Node 専用(`@hono/node-server`)のため、このモジュール + * には静的 import せず、`clientIpNode.ts` が resolver を注入する(Worker バンドル + * から `@hono/node-server` を除外する — #1091 FR-2.3)。 * * `x-forwarded-for` and `x-real-ip` are client-controllable. We only trust them - * when `TRUST_PROXY=true` so unauthenticated clients cannot rotate those headers - * to bypass per-IP throttling. When the proxy is not trusted (or no proxy is - * configured), fall back to the socket peer address. + * when `TRUST_PROXY=true`. On the Workers runtime (explicit `RUNTIME` var, never + * header sniffing) `CF-Connecting-IP` wins. The Node-only socket fallback is + * injected by `clientIpNode.ts` so the Worker bundle never pulls in + * `@hono/node-server`. */ import type { Context } from "hono"; -import { getConnInfo } from "@hono/node-server/conninfo"; import type { AppEnv } from "../types/index.js"; +/** + * ソケット接続元 IP の resolver。Node エントリだけが `clientIpNode.ts` 経由で + * 注入する。未注入(= Workers)ではソケットフォールバックは常に null。 + */ +export type SocketIpResolver = (c: Context) => string | null; + +let socketIpResolver: SocketIpResolver | null = null; + +/** + * ソケット IP resolver を注入する(null でリセット)。 + * Inject the Node-only socket peer resolver (pass null to reset). + */ +export function setSocketIpResolver(resolver: SocketIpResolver | null): void { + socketIpResolver = resolver; +} + /** * `TRUST_PROXY` 環境変数が真と評価されるかを返す。 * `"true" | "1" | "yes"` を真として扱い、それ以外は偽。 @@ -30,6 +55,18 @@ export function isProxyTrusted(): boolean { return raw === "true" || raw === "1" || raw === "yes"; } +/** + * Workers ランタイムか。`c.env.RUNTIME`(wrangler.jsonc vars)で判定する。 + * ヘッダ存在による判定は偽装可能なため行わない(#1091 SR-5)。 + * + * Whether we are on the Workers runtime, decided by the explicit `RUNTIME` + * binding var — never by header presence, which clients can spoof. + */ +function isWorkersRuntime(c: Context): boolean { + const env = c.env as Record | undefined; + return env?.RUNTIME === "cloudflare-workers"; +} + /** * `x-forwarded-for` の最左の非空 IP を返す。先頭にカンマが並ぶ不正値も許容する。 * Return the leftmost non-empty IP from `x-forwarded-for`; tolerates @@ -55,14 +92,13 @@ function readRealIp(c: Context): string | null { } /** - * ソケット接続元 IP を返す。取得できなければ null。 - * Return the underlying socket peer address, or null when not available. + * 注入済み resolver 経由でソケット接続元 IP を返す。未注入なら null。 + * Return the socket peer address via the injected resolver, or null. */ function readSocketIp(c: Context): string | null { + if (!socketIpResolver) return null; try { - const info = getConnInfo(c); - const addr = info.remote.address?.trim(); - return addr ? addr : null; + return socketIpResolver(c); } catch { return null; } @@ -71,14 +107,19 @@ function readSocketIp(c: Context): string | null { /** * クライアント IP を抽出する。 * + * - Workers ランタイム: `CF-Connecting-IP` を第一候補に採用。 * - `TRUST_PROXY=true` のとき: `x-forwarded-for` → `x-real-ip` → ソケット の順で採用。 * - それ以外: ソケット IP のみを採用(プロキシヘッダは無視)。 * - * Extract the best-effort client IP. Trust proxy headers only when - * `TRUST_PROXY=true`; otherwise rely on the socket peer address so spoofed - * `x-forwarded-for` / `x-real-ip` values cannot influence callers. + * Extract the best-effort client IP. On Workers, `CF-Connecting-IP` (set by + * Cloudflare at the edge) wins. Otherwise trust proxy headers only when + * `TRUST_PROXY=true`, falling back to the injected socket resolver. */ export function extractClientIp(c: Context): string | null { + if (isWorkersRuntime(c)) { + const cfIp = c.req.header("cf-connecting-ip")?.trim(); + if (cfIp) return cfIp; + } if (isProxyTrusted()) { const fromXff = readForwardedFor(c); if (fromXff) return fromXff; diff --git a/server/api/src/lib/clientIpNode.ts b/server/api/src/lib/clientIpNode.ts new file mode 100644 index 00000000..8fa08886 --- /dev/null +++ b/server/api/src/lib/clientIpNode.ts @@ -0,0 +1,34 @@ +/** + * Node 専用のソケット IP resolver 配線。`@hono/node-server/conninfo` への + * 静的 import はこのモジュールだけが持ち、`index.ts` だけが import する。 + * worker.ts が import すると `worker:bundle:check` が fail する(#1091 FR-2.3)。 + * + * Node-only socket resolver wiring. This is the single module allowed to + * import `@hono/node-server/conninfo`; only the Node entry imports it. + */ +import type { Context } from "hono"; +import { getConnInfo } from "@hono/node-server/conninfo"; +import type { AppEnv } from "../types/index.js"; +import { setSocketIpResolver } from "./clientIp.js"; + +/** + * ソケット接続元 IP を返す。取得できなければ null。 + * Return the underlying socket peer address, or null when not available. + */ +function readNodeSocketIp(c: Context): string | null { + try { + const info = getConnInfo(c); + const addr = info.remote.address?.trim(); + return addr ? addr : null; + } catch { + return null; + } +} + +/** + * Node エントリ起動時に呼び、ソケットフォールバックを有効化する。 + * Install the Node socket fallback into the runtime-neutral clientIp module. + */ +export function installNodeSocketIpResolver(): void { + setSocketIpResolver(readNodeSocketIp); +} diff --git a/server/api/src/lib/kv/createKvStore.ts b/server/api/src/lib/kv/createKvStore.ts index 575dae17..97a89985 100644 --- a/server/api/src/lib/kv/createKvStore.ts +++ b/server/api/src/lib/kv/createKvStore.ts @@ -1,31 +1,34 @@ /** - * createKvStore — ランタイムに応じた KvStore の生成 (#1093) + * createKvStore — ランタイムに応じた KvStore の生成 (#1093 / #1091) * - * Workers では `KV_DO` binding(Durable Object)、Node/Railway では `REDIS_URL` - * から生成する。どちらも無い場合は null(レート制限等は graceful degradation)。 + * Workers では `KV_DO` binding(Durable Object)、Node/Railway では + * `createKvStoreNode.ts` が注入する Redis ファクトリから生成する。どちらも + * 無い場合は null(レート制限等は graceful degradation)。 + * + * Node アダプタ(ioredis)はこのモジュールから静的 import しない — Worker + * バンドル・workerd 実行から ioredis を物理的に除外するためのモジュール境界 + * (#1091 FR-5。`worker:bundle:check` が不在を検査する)。 * * Creates a KvStore for the current runtime: the `KV_DO` Durable Object - * binding on Workers, an ioredis client from `REDIS_URL` on Node/Railway, - * or null when neither is configured (callers degrade gracefully). + * binding on Workers, or the injected Node factory (ioredis-backed, wired by + * `createKvStoreNode.ts` from the Node entry only). ioredis is never imported + * statically here so it stays out of the Worker bundle. */ -import { Redis } from "ioredis"; import type { CloudflareBindings } from "../../types/cloudflare.js"; import { DurableObjectKvStore } from "./doKvStore.js"; -import { RedisKvStore } from "./redisKvStore.js"; import type { KvStore } from "./types.js"; -let _redis: Redis | null = null; +/** Node エントリが注入する KvStore ファクトリ(未注入 = Workers or 未設定)。 */ +export type NodeKvStoreFactory = () => KvStore | null; + +let nodeKvStoreFactory: NodeKvStoreFactory | null = null; -function getRedis(): Redis | null { - if (_redis) return _redis; - const url = process.env.REDIS_URL; - if (!url) return null; - _redis = new Redis(url, { maxRetriesPerRequest: 3, lazyConnect: true }); - _redis.connect().catch((err) => { - console.error("[Redis] Connection failed:", err); - _redis = null; - }); - return _redis; +/** + * Node 側 KvStore ファクトリを注入する(null でリセット)。 + * Inject the Node-only KvStore factory (pass null to reset). + */ +export function setNodeKvStoreFactory(factory: NodeKvStoreFactory | null): void { + nodeKvStoreFactory = factory; } /** @@ -36,6 +39,5 @@ export function createKvStore(bindings?: Partial): KvStore | if (bindings?.KV_DO) { return new DurableObjectKvStore(bindings.KV_DO); } - const redis = getRedis(); - return redis ? new RedisKvStore(redis) : null; + return nodeKvStoreFactory?.() ?? null; } diff --git a/server/api/src/lib/kv/createKvStoreNode.ts b/server/api/src/lib/kv/createKvStoreNode.ts new file mode 100644 index 00000000..4ca0ed8d --- /dev/null +++ b/server/api/src/lib/kv/createKvStoreNode.ts @@ -0,0 +1,40 @@ +/** + * Node 専用の KvStore ファクトリ配線(#1091 FR-5 のモジュール境界)。 + * + * `ioredis` への静的 import はこのモジュールだけが持ち、`index.ts` だけが + * import する。worker.ts が import すると `worker:bundle:check` が fail する。 + * + * Node-only KvStore factory wiring. This is the single module allowed to + * import `ioredis`; only the Node entry imports it. + */ +import { Redis } from "ioredis"; +import { RedisKvStore } from "./redisKvStore.js"; +import { setNodeKvStoreFactory } from "./createKvStore.js"; +import type { KvStore } from "./types.js"; + +let _redis: Redis | null = null; + +function getRedis(): Redis | null { + if (_redis) return _redis; + const url = process.env.REDIS_URL; + if (!url) return null; + _redis = new Redis(url, { maxRetriesPerRequest: 3, lazyConnect: true }); + _redis.connect().catch((err) => { + console.error("[Redis] Connection failed:", err); + _redis = null; + }); + return _redis; +} + +function createNodeKvStore(): KvStore | null { + const redis = getRedis(); + return redis ? new RedisKvStore(redis) : null; +} + +/** + * Node エントリ起動時に呼び、Redis ベースの KvStore ファクトリを有効化する。 + * Install the ioredis-backed KvStore factory into the runtime-neutral module. + */ +export function installNodeKvStoreFactory(): void { + setNodeKvStoreFactory(createNodeKvStore); +} diff --git a/server/api/src/lib/sentry.ts b/server/api/src/lib/sentry.ts index fdb29f0d..7468a6b4 100644 --- a/server/api/src/lib/sentry.ts +++ b/server/api/src/lib/sentry.ts @@ -1,47 +1,21 @@ -import * as Sentry from "@sentry/node"; -import type { ErrorEvent } from "@sentry/node"; - -const FILTERED = "[Filtered]"; -const CIRCULAR = "[Circular]"; - -/** - * PII を含み得るキー名(小文字)。 - * Lowercased keys whose values are scrubbed regardless of where they appear. - */ -const SENSITIVE_KEYS = new Set([ - "authorization", - "cookie", - "email", - "password", - "token", - "access_token", - "refresh_token", - "secret", - "set-cookie", -]); - /** - * URL クエリ文字列に含まれる機微な値(`?token=...` 等)を redact する正規表現。 - * Redacts sensitive URL query parameters (e.g., `?token=...`) embedded in string values. + * Sentry の Node ランタイム実装(Railway / `src/index.ts` 専用)。 + * + * スクラブ・capture 判定の純関数は `sentryShared.ts` に分離し、Worker 側 + * (`sentryWorker.ts`, `@sentry/cloudflare`)と共有する。このモジュールを + * worker.ts から import してはならない(`worker:bundle:check` が + * `@sentry/node` の不在を検査する — #1091 LC-4)。 + * + * Node-runtime Sentry implementation. Pure helpers live in `sentryShared.ts` + * and are shared with the Workers entry; this module must never be imported + * from worker.ts so `@sentry/node` stays out of the Worker bundle. */ -const SENSITIVE_QUERY_PARAM_RE = - /([?&](?:authorization|cookie|email|password|token|access_token|refresh_token|secret)=)[^&\s]*/gi; +import * as Sentry from "@sentry/node"; +import { scrubSentryEvent, shouldCaptureApiException } from "./sentryShared.js"; +import type { ApiErrorContext } from "./sentryShared.js"; -/** - * Sentry に送る API エラーの補助コンテキスト。 - * Supplemental route context attached to API errors sent to Sentry. - */ -export interface ApiErrorContext { - method: string; - /** - * Hono の matched route pattern(例: `/api/invite/:token`)。 - * 生のリクエストパスを送ると capability token 等が漏れるため、必ずパターン側を渡す。 - * - * Hono's matched route pattern (e.g., `/api/invite/:token`). The raw request - * path must not be passed because it may contain capability tokens. - */ - routePath: string; -} +export { scrubSentryEvent, shouldCaptureApiException }; +export type { ApiErrorContext }; /** * Sentry Node SDK を初期化する。`SENTRY_DSN_API` が未設定の場合は no-op。 @@ -75,157 +49,3 @@ export function captureApiException(err: unknown, status: number, context: ApiEr }, }); } - -/** - * Sentry に送るべき API エラーかどうかを判定する。 - * Returns whether an API error should be sent to Sentry. - */ -export function shouldCaptureApiException(status: number): boolean { - return status >= 500 || (status >= 400 && status < 500 && ![401, 403, 404].includes(status)); -} - -/** - * Sentry イベントから PII を含み得るフィールドだけを安全にスクラブする。 - * - * イベント全体を再帰的に再構築すると、`Date` や `RegExp` 等の非 plain object が - * `{}` に変質したり、循環参照でスタックオーバーフローする恐れがあるため、 - * ここでは PII を含み得る `request` / `user` / `extra` / `tags` / `contexts` / - * `breadcrumbs[].data` のみを対象にする。 - * - * Scrubs only the fields of a Sentry event that may contain PII. The full event - * is left intact to preserve `Date`/`RegExp`/SDK-managed shapes and to avoid - * stack overflows on circular references. - */ -export function scrubSentryEvent(event: ErrorEvent): ErrorEvent { - if (event.request) { - event.request = { - ...event.request, - url: scrubRequestUrl(event.request.url), - headers: scrubShallowRecord(event.request.headers), - data: scrubDeep(event.request.data, new WeakSet()), - query_string: scrubQueryStringField(event.request.query_string), - cookies: scrubCookies(event.request.cookies), - }; - } - if (event.user) { - event.user = scrubShallowRecord(event.user) ?? event.user; - } - if (event.extra) { - event.extra = scrubDeep(event.extra, new WeakSet()) as typeof event.extra; - } - if (event.contexts) { - event.contexts = scrubShallowRecord(event.contexts) ?? event.contexts; - } - if (event.tags) { - event.tags = scrubShallowRecord(event.tags) ?? event.tags; - } - if (event.breadcrumbs) { - event.breadcrumbs = event.breadcrumbs.map((breadcrumb) => ({ - ...breadcrumb, - data: breadcrumb.data - ? (scrubDeep(breadcrumb.data, new WeakSet()) as typeof breadcrumb.data) - : breadcrumb.data, - })); - } - return event; -} - -/** - * `new URL(relative)` が失敗する場合のみ使う合成オリジン。 - * Synthetic origin used only when `new URL(absolute)` fails for SDK-recorded paths. - */ -const RELATIVE_REQUEST_URL_BASE = "https://sentry-request-url-scrub.invalid"; - -/** - * SDK が記録するリクエスト URL からクエリと不透明パス成分を落とす。 - * matched route は `routePath` など別チャネルに載せる前提。 - * - * Strip query strings and redact opaque path segments from the raw URL SDKs - * record; structured routing context belongs in `routePath` / extras instead. - * - * 相対パス(例: `/api/pages/...`)もパースできるよう合成ベースで解決する。 - * Relative paths (e.g. `/api/pages/...`) are resolved against a synthetic base so - * scrubbing still applies instead of collapsing everything to `[Filtered]`. - */ -function scrubRequestUrl(url: string | undefined): string | undefined { - if (url === undefined) return undefined; - if (typeof url !== "string") return undefined; - if (url.length === 0) return ""; - - let u: URL; - try { - u = new URL(url); - } catch { - try { - u = new URL(url, `${RELATIVE_REQUEST_URL_BASE}/`); - } catch { - return FILTERED; - } - } - - u.search = ""; - const segments = u.pathname.split("/").map((seg) => { - if (seg.length === 0) return seg; - if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) { - return "[uuid]"; - } - if (/^[A-Za-z0-9._~-]{24,}$/.test(seg)) { - return FILTERED; - } - return seg; - }); - u.pathname = segments.join("/") || "/"; - return u.toString(); -} - -function scrubShallowRecord | undefined>(input: T): T; -function scrubShallowRecord( - input: Record | undefined, -): Record | undefined { - if (!input || typeof input !== "object") return input; - const out: Record = {}; - for (const [key, value] of Object.entries(input)) { - if (SENSITIVE_KEYS.has(key.toLowerCase())) { - out[key] = FILTERED; - } else if (typeof value === "string") { - out[key] = scrubQueryStringField(value); - } else { - out[key] = value; - } - } - return out; -} - -function scrubCookies( - input: Record | undefined, -): Record | undefined { - if (!input || typeof input !== "object") return input; - return Object.fromEntries(Object.keys(input).map((key) => [key, FILTERED])); -} - -function scrubDeep(value: unknown, seen: WeakSet, key?: string): unknown { - if (key && SENSITIVE_KEYS.has(key.toLowerCase())) return FILTERED; - if (typeof value === "string") return scrubQueryStringField(value); - if (value === null || typeof value !== "object") return value; - if (seen.has(value as object)) return CIRCULAR; - seen.add(value as object); - if (Array.isArray(value)) return value.map((item) => scrubDeep(item, seen)); - if (!isPlainObject(value)) return value; - return Object.fromEntries( - Object.entries(value as Record).map(([entryKey, entryValue]) => [ - entryKey, - scrubDeep(entryValue, seen, entryKey), - ]), - ); -} - -function scrubQueryStringField(value: T): T; -function scrubQueryStringField(value: unknown): unknown { - if (typeof value !== "string") return value; - return value.replace(SENSITIVE_QUERY_PARAM_RE, `$1${FILTERED}`); -} - -function isPlainObject(value: object): boolean { - const proto = Object.getPrototypeOf(value); - return proto === Object.prototype || proto === null; -} diff --git a/server/api/src/lib/sentryShared.ts b/server/api/src/lib/sentryShared.ts new file mode 100644 index 00000000..2c5ad786 --- /dev/null +++ b/server/api/src/lib/sentryShared.ts @@ -0,0 +1,233 @@ +/** + * Sentry まわりのランタイム中立な純関数(SDK 非依存)。 + * + * `@sentry/node`(Node/Railway)と `@sentry/cloudflare`(Worker)の両方から + * 使うため、SDK を import しない。Worker バンドルに `@sentry/node` を入れない + * ためのモジュール境界(#1091 LC-4)。 + * + * Runtime-neutral Sentry helpers shared by the Node and Workers entries. + * Imports no SDK so the Worker bundle stays free of `@sentry/node`. + */ + +const FILTERED = "[Filtered]"; +const CIRCULAR = "[Circular]"; + +/** + * PII を含み得るキー名(小文字)。 + * Lowercased keys whose values are scrubbed regardless of where they appear. + */ +const SENSITIVE_KEYS = new Set([ + "authorization", + "cookie", + "email", + "password", + "token", + "access_token", + "refresh_token", + "secret", + "set-cookie", +]); + +/** + * URL クエリ文字列に含まれる機微な値(`?token=...` 等)を redact する正規表現。 + * Redacts sensitive URL query parameters (e.g., `?token=...`) embedded in string values. + */ +const SENSITIVE_QUERY_PARAM_RE = + /([?&](?:authorization|cookie|email|password|token|access_token|refresh_token|secret)=)[^&\s]*/gi; + +/** + * Sentry に送る API エラーの補助コンテキスト。 + * Supplemental route context attached to API errors sent to Sentry. + */ +export interface ApiErrorContext { + method: string; + /** + * Hono の matched route pattern(例: `/api/invite/:token`)。 + * 生のリクエストパスを送ると capability token 等が漏れるため、必ずパターン側を渡す。 + * + * Hono's matched route pattern (e.g., `/api/invite/:token`). The raw request + * path must not be passed because it may contain capability tokens. + */ + routePath: string; +} + +/** + * `captureApiException` と同型の capture 関数。エントリポイント(index.ts / + * worker.ts)が各ランタイムの SDK 実装を注入する。 + * Capture function shape injected by the runtime entrypoints. + */ +export type ApiExceptionCapture = (err: unknown, status: number, context: ApiErrorContext) => void; + +/** + * `scrubSentryEvent` が触るフィールドだけの構造型。両 SDK の `ErrorEvent` が + * 構造的に満たすため、SDK の型 import を不要にする。 + * Structural subset of both SDKs' `ErrorEvent`; keeps this module SDK-free. + */ +export interface ScrubbableSentryEvent { + request?: { + url?: string; + headers?: Record; + data?: unknown; + query_string?: unknown; + cookies?: Record; + }; + user?: Record; + extra?: Record; + contexts?: Record; + tags?: Record; + breadcrumbs?: { data?: Record }[]; +} + +/** + * Sentry に送るべき API エラーかどうかを判定する。 + * Returns whether an API error should be sent to Sentry. + */ +export function shouldCaptureApiException(status: number): boolean { + return status >= 500 || (status >= 400 && status < 500 && ![401, 403, 404].includes(status)); +} + +/** + * Sentry イベントから PII を含み得るフィールドだけを安全にスクラブする。 + * + * イベント全体を再帰的に再構築すると、`Date` や `RegExp` 等の非 plain object が + * `{}` に変質したり、循環参照でスタックオーバーフローする恐れがあるため、 + * ここでは PII を含み得る `request` / `user` / `extra` / `tags` / `contexts` / + * `breadcrumbs[].data` のみを対象にする。 + * + * Scrubs only the fields of a Sentry event that may contain PII. The full event + * is left intact to preserve `Date`/`RegExp`/SDK-managed shapes and to avoid + * stack overflows on circular references. + */ +export function scrubSentryEvent(event: T): T { + if (event.request) { + event.request = { + ...event.request, + url: scrubRequestUrl(event.request.url), + headers: scrubShallowRecord(event.request.headers), + data: scrubDeep(event.request.data, new WeakSet()), + query_string: scrubQueryStringField(event.request.query_string), + cookies: scrubCookies(event.request.cookies), + }; + } + if (event.user) { + event.user = scrubShallowRecord(event.user) ?? event.user; + } + if (event.extra) { + event.extra = scrubDeep(event.extra, new WeakSet()) as typeof event.extra; + } + if (event.contexts) { + event.contexts = scrubShallowRecord(event.contexts) ?? event.contexts; + } + if (event.tags) { + event.tags = scrubShallowRecord(event.tags) ?? event.tags; + } + if (event.breadcrumbs) { + event.breadcrumbs = event.breadcrumbs.map((breadcrumb) => ({ + ...breadcrumb, + data: breadcrumb.data + ? (scrubDeep(breadcrumb.data, new WeakSet()) as typeof breadcrumb.data) + : breadcrumb.data, + })); + } + return event; +} + +/** + * `new URL(relative)` が失敗する場合のみ使う合成オリジン。 + * Synthetic origin used only when `new URL(absolute)` fails for SDK-recorded paths. + */ +const RELATIVE_REQUEST_URL_BASE = "https://sentry-request-url-scrub.invalid"; + +/** + * SDK が記録するリクエスト URL からクエリと不透明パス成分を落とす。 + * matched route は `routePath` など別チャネルに載せる前提。 + * + * Strip query strings and redact opaque path segments from the raw URL SDKs + * record; structured routing context belongs in `routePath` / extras instead. + * + * 相対パス(例: `/api/pages/...`)もパースできるよう合成ベースで解決する。 + * Relative paths (e.g. `/api/pages/...`) are resolved against a synthetic base so + * scrubbing still applies instead of collapsing everything to `[Filtered]`. + */ +function scrubRequestUrl(url: string | undefined): string | undefined { + if (url === undefined) return undefined; + if (typeof url !== "string") return undefined; + if (url.length === 0) return ""; + + let u: URL; + try { + u = new URL(url); + } catch { + try { + u = new URL(url, `${RELATIVE_REQUEST_URL_BASE}/`); + } catch { + return FILTERED; + } + } + + u.search = ""; + const segments = u.pathname.split("/").map((seg) => { + if (seg.length === 0) return seg; + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) { + return "[uuid]"; + } + if (/^[A-Za-z0-9._~-]{24,}$/.test(seg)) { + return FILTERED; + } + return seg; + }); + u.pathname = segments.join("/") || "/"; + return u.toString(); +} + +function scrubShallowRecord | undefined>(input: T): T; +function scrubShallowRecord( + input: Record | undefined, +): Record | undefined { + if (!input || typeof input !== "object") return input; + const out: Record = {}; + for (const [key, value] of Object.entries(input)) { + if (SENSITIVE_KEYS.has(key.toLowerCase())) { + out[key] = FILTERED; + } else if (typeof value === "string") { + out[key] = scrubQueryStringField(value); + } else { + out[key] = value; + } + } + return out; +} + +function scrubCookies( + input: Record | undefined, +): Record | undefined { + if (!input || typeof input !== "object") return input; + return Object.fromEntries(Object.keys(input).map((key) => [key, FILTERED])); +} + +function scrubDeep(value: unknown, seen: WeakSet, key?: string): unknown { + if (key && SENSITIVE_KEYS.has(key.toLowerCase())) return FILTERED; + if (typeof value === "string") return scrubQueryStringField(value); + if (value === null || typeof value !== "object") return value; + if (seen.has(value as object)) return CIRCULAR; + seen.add(value as object); + if (Array.isArray(value)) return value.map((item) => scrubDeep(item, seen)); + if (!isPlainObject(value)) return value; + return Object.fromEntries( + Object.entries(value as Record).map(([entryKey, entryValue]) => [ + entryKey, + scrubDeep(entryValue, seen, entryKey), + ]), + ); +} + +function scrubQueryStringField(value: T): T; +function scrubQueryStringField(value: unknown): unknown { + if (typeof value !== "string") return value; + return value.replace(SENSITIVE_QUERY_PARAM_RE, `$1${FILTERED}`); +} + +function isPlainObject(value: object): boolean { + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} diff --git a/server/api/src/lib/sentryWorker.ts b/server/api/src/lib/sentryWorker.ts new file mode 100644 index 00000000..15b6df56 --- /dev/null +++ b/server/api/src/lib/sentryWorker.ts @@ -0,0 +1,28 @@ +/** + * Sentry の Workers ランタイム実装(`src/worker.ts` 専用)。 + * + * `withSentry` ラッパが SDK 初期化と `ctx.waitUntil` によるイベント flush を + * 保証するため、Workers 側に `initSentry` は存在しない(#1091 LC-4 / OQ-2)。 + * このモジュールを index.ts から import してはならない(Node バンドルに + * `@sentry/cloudflare` を混ぜない)。 + * + * Workers-runtime Sentry implementation. The `withSentry` wrapper owns SDK + * init and waitUntil-based event flushing, so there is no `initSentry` here. + */ +import * as Sentry from "@sentry/cloudflare"; +import type { ApiErrorContext } from "./sentryShared.js"; + +/** + * API errorHandler から渡された例外を HTTP 情報付きで Sentry に送信する。 + * Node 側 `lib/sentry.ts` の `captureApiException` と同一のタグ/extra 形状。 + * Captures an API exception with the same tag/extra shape as the Node runtime. + */ +export function captureApiException(err: unknown, status: number, context: ApiErrorContext): void { + Sentry.captureException(err, { + tags: { httpStatus: String(status) }, + extra: { + method: context.method, + routePath: context.routePath, + }, + }); +} diff --git a/server/api/src/lib/sentryWorkerOptions.ts b/server/api/src/lib/sentryWorkerOptions.ts new file mode 100644 index 00000000..eb18b04f --- /dev/null +++ b/server/api/src/lib/sentryWorkerOptions.ts @@ -0,0 +1,38 @@ +/** + * Worker 向け Sentry 設定のビルダー(SDK 非依存 — 単体テスト可能)。 + * + * `withSentry` ラッパ(`sentryWorker.ts`)に渡すオプションを生成する。 + * スクラブ挙動(`sendDefaultPii: false` / `denyUrls` / `beforeSend`)は + * Node 側 `initSentry()` と同一に保つ(#1091 FR-3.2)。 + * + * SDK-free builder for the Workers Sentry options passed to `withSentry`. + * Keeps scrub behavior identical to the Node runtime's `initSentry()`. + */ +import { scrubSentryEvent } from "./sentryShared.js"; +import type { ScrubbableSentryEvent } from "./sentryShared.js"; + +/** `withSentry` に渡すオプションの最小型(SDK の型に構造的に適合する)。 */ +export interface WorkerSentryOptions { + dsn?: string; + environment: string; + sendDefaultPii: boolean; + denyUrls: RegExp[]; + beforeSend: (event: T) => T; +} + +/** + * Worker env(bindings + vars + secrets)から Sentry オプションを組み立てる。 + * `SENTRY_DSN_API` が未設定/空なら dsn を undefined にして SDK を no-op にする + * (FR-3.3 — Node 側の DSN 未設定 no-op と同じ契約)。 + */ +export function buildSentryOptions(env: Record): WorkerSentryOptions { + const rawDsn = typeof env.SENTRY_DSN_API === "string" ? env.SENTRY_DSN_API.trim() : ""; + const environment = typeof env.ENVIRONMENT === "string" ? env.ENVIRONMENT : "development"; + return { + dsn: rawDsn || undefined, + environment, + sendDefaultPii: false, + denyUrls: [/\/api\/health(?:$|\?)/], + beforeSend: (event) => scrubSentryEvent(event), + }; +} diff --git a/server/api/src/middleware/errorHandler.ts b/server/api/src/middleware/errorHandler.ts index f80bb634..b4e01b70 100644 --- a/server/api/src/middleware/errorHandler.ts +++ b/server/api/src/middleware/errorHandler.ts @@ -1,49 +1,78 @@ import type { ErrorHandler } from "hono"; import type { ContentfulStatusCode } from "hono/utils/http-status"; import { HTTPException } from "hono/http-exception"; -import { captureApiException, shouldCaptureApiException } from "../lib/sentry.js"; +import { shouldCaptureApiException } from "../lib/sentryShared.js"; +import type { ApiExceptionCapture } from "../lib/sentryShared.js"; import type { AppEnv } from "../types/index.js"; /** - * Hono のグローバルエラーハンドラ。HTTP 応答へ変換し、対象エラーのみ Sentry に送る。 - * Global Hono error handler. Converts thrown errors to HTTP responses and captures eligible errors in Sentry. + * {@link createErrorHandler} のオプション。 + * Options for {@link createErrorHandler}. */ -export const errorHandler: ErrorHandler = (err, c) => { - if (err instanceof HTTPException) { - const status = err.status; - console.error(`[api] ${c.req.method} ${c.req.path} → ${status}`, err.message); +export interface ErrorHandlerOptions { + /** + * 捕捉対象エラーを送信する capture 関数。エントリポイントがランタイムごとの + * Sentry 実装を注入する(index.ts → @sentry/node、worker.ts → + * @sentry/cloudflare)。未注入なら no-op(#1091 LC-4 の DI 境界)。 + */ + captureApiException?: ApiExceptionCapture; +} + +/** + * Hono のグローバルエラーハンドラを生成する。HTTP 応答へ変換し、対象エラーのみ + * 注入された capture へ渡す。SDK を静的 import しないことで Worker バンドルに + * `@sentry/node` が入らないようにする。 + * + * Build the global Hono error handler. Converts thrown errors to HTTP + * responses and forwards eligible errors to the injected capture function. + * No SDK is imported statically so the Worker bundle stays SDK-free. + */ +/** + * capture なしの既定ハンドラ。ルート単体テストや capture 不要のマウントで使う。 + * Default handler without capture — for per-route tests and capture-free mounts. + */ +export const errorHandler: ErrorHandler = createErrorHandler(); + +export function createErrorHandler(options: ErrorHandlerOptions = {}): ErrorHandler { + const capture: ApiExceptionCapture = options.captureApiException ?? (() => {}); + + return (err, c) => { + if (err instanceof HTTPException) { + const status = err.status; + console.error(`[api] ${c.req.method} ${c.req.path} → ${status}`, err.message); + if (shouldCaptureApiException(status)) { + capture(err, status, { + method: c.req.method, + routePath: c.req.routePath, + }); + } + return c.json({ error: err.message }, status); + } + + const message = err instanceof Error ? err.message : "Internal server error"; + const statusMap: Record = { + UNAUTHORIZED: 401, + FORBIDDEN: 403, + RATE_LIMIT_EXCEEDED: 429, + STORAGE_QUOTA_EXCEEDED: 403, + NOT_FOUND: 404, + BAD_REQUEST: 400, + CONFLICT: 409, + VALIDATION_FAILED: 422, + }; + const status = statusMap[message] ?? 500; + + console.error(`[api] ${c.req.method} ${c.req.path} → ${status}`, err); if (shouldCaptureApiException(status)) { - captureApiException(err, status, { + capture(err, status, { method: c.req.method, routePath: c.req.routePath, }); } - return c.json({ error: err.message }, status); - } - - const message = err instanceof Error ? err.message : "Internal server error"; - const statusMap: Record = { - UNAUTHORIZED: 401, - FORBIDDEN: 403, - RATE_LIMIT_EXCEEDED: 429, - STORAGE_QUOTA_EXCEEDED: 403, - NOT_FOUND: 404, - BAD_REQUEST: 400, - CONFLICT: 409, - VALIDATION_FAILED: 422, + // 5xx では DB/内部の生メッセージ(スキーマ名・接続先など)をクライアントへ露出しない。 + // statusMap で明示された 4xx コードのみ、そのままレスポンスに載せる。 + // Never leak raw internal/DB error messages on 5xx; only the mapped 4xx codes are safe to return. + const responseMessage = status >= 500 ? "Internal server error" : message; + return c.json({ error: responseMessage }, status as ContentfulStatusCode); }; - const status = statusMap[message] ?? 500; - - console.error(`[api] ${c.req.method} ${c.req.path} → ${status}`, err); - if (shouldCaptureApiException(status)) { - captureApiException(err, status, { - method: c.req.method, - routePath: c.req.routePath, - }); - } - // 5xx では DB/内部の生メッセージ(スキーマ名・接続先など)をクライアントへ露出しない。 - // statusMap で明示された 4xx コードのみ、そのままレスポンスに載せる。 - // Never leak raw internal/DB error messages on 5xx; only the mapped 4xx codes are safe to return. - const responseMessage = status >= 500 ? "Internal server error" : message; - return c.json({ error: responseMessage }, status as ContentfulStatusCode); -}; +} diff --git a/server/api/src/services/articleExtractor.ts b/server/api/src/services/articleExtractor.ts index 6a2c9673..cf6a4405 100644 --- a/server/api/src/services/articleExtractor.ts +++ b/server/api/src/services/articleExtractor.ts @@ -9,7 +9,16 @@ * Ingest プランナー(otomatty/zedi#595)が Page を保存せずに同じ抽出を行うために共有する。 */ import { Mutex } from "async-mutex"; -import { JSDOM } from "jsdom"; +// jsdom は Node 専用(workerd では実行不可)のため静的 import しない。 +// Worker のモジュールロードを通し、clip 実行時のみ評価する(呼ばれた場合の +// 失敗は onError でマスク済み 5xx になる — #1091 RL-3)。 +// jsdom is Node-only; load it lazily so the Worker module graph still boots. +type JsdomModule = typeof import("jsdom"); +let _jsdom: JsdomModule | null = null; +async function loadJsdom(): Promise { + if (!_jsdom) _jsdom = await import("jsdom"); + return _jsdom; +} import { Readability } from "@mozilla/readability"; import { generateJSON } from "@tiptap/html"; import { getSchema } from "@tiptap/core"; @@ -275,6 +284,7 @@ export async function extractArticleFromUrl(input: ExtractArticleInput): Promise clearTimeout(timer); } + const { JSDOM } = await loadJsdom(); const dom = new JSDOM(html, { url: finalUrl }); const document = dom.window.document; diff --git a/server/api/src/worker.ts b/server/api/src/worker.ts index 3018ed1b..d2c6b937 100644 --- a/server/api/src/worker.ts +++ b/server/api/src/worker.ts @@ -1,15 +1,26 @@ /** - * Cloudflare Workers entry for the Zedi API (#1091 / Phase 2a). - * Hono default export — see Hono skill "Adapters". + * Cloudflare Workers entry for the Zedi API (#1091). * * Zedi API の Cloudflare Workers エントリポイント。 + * + * - agent 系ルート(compose-sessions / ingest)は登録しない — `appAgents.ts` は + * Node エントリ専用で、`@langchain/*` / `src/agents/**` を Worker バンドルから + * 物理的に除外する(FR-1。`worker:bundle:check` が不在を検査する)。 + * - Sentry は `withSentry` ラッパで default export を包む。ラッパが SDK 初期化と + * `ctx.waitUntil` によるイベント flush を保証する(LC-4 / OQ-2)。DSN 未設定時は + * no-op(FR-3.3)。 + * - `KvDurableObject` の named export は `withSentry` の影響を受けない + * (wrangler.jsonc `durable_objects` が参照)。 */ +import { withSentry } from "@sentry/cloudflare"; import { createApp } from "./app.js"; +import { captureApiException } from "./lib/sentryWorker.js"; +import { buildSentryOptions } from "./lib/sentryWorkerOptions.js"; // Durable Object classes must be exported from the Worker entry so the // runtime can instantiate them (see wrangler.jsonc `durable_objects`). export { KvDurableObject } from "./lib/kv/kvDurableObject.js"; -const app = createApp(); +const app = createApp({ captureApiException }); -export default app; +export default withSentry((env: Record) => buildSentryOptions(env), app); diff --git a/server/api/vitest.config.ts b/server/api/vitest.config.ts index 85caffd7..1e9a9576 100644 --- a/server/api/vitest.config.ts +++ b/server/api/vitest.config.ts @@ -6,6 +6,9 @@ export default defineConfig({ globals: true, environment: "node", include: ["src/**/*.{test,spec}.{ts,tsx}", "scripts/**/*.{test,spec}.{ts,tsx}"], + // workerd 実行テストは vitest.config.worker.ts(test:worker)専用。 + // Worker-runtime tests run only under vitest.config.worker.ts. + exclude: ["**/node_modules/**", "src/__tests__/worker/**"], coverage: createCoverageConfig({ include: ["src/**/*.ts", "scripts/**/*.ts"], exclude: ["src/types/**", "src/index.ts"], diff --git a/server/api/vitest.config.worker.ts b/server/api/vitest.config.worker.ts new file mode 100644 index 00000000..9b84e692 --- /dev/null +++ b/server/api/vitest.config.worker.ts @@ -0,0 +1,42 @@ +/** + * workerd 実行テスト用の vitest 設定(#1091 LC-6、`bun run test:worker`)。 + * + * 既存の Node 実行テスト(vitest.config.ts)とは完全分離し、 + * `src/__tests__/worker/**` だけを @cloudflare/vitest-pool-workers(0.18+ の + * vitest 4 向け `cloudflareTest` プラグイン API)で workerd 上で実行する。 + * バインディング(R2 / KV_DO / vars)は wrangler.jsonc の dev env から + * miniflare が構成する。 + * + * Worker-runtime test config. Runs only `src/__tests__/worker/**` on workerd + * via the vitest-pool-workers `cloudflareTest` plugin (vitest 4 API), with + * bindings simulated from wrangler.jsonc. + */ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.jsonc", environment: "dev" }, + miniflare: { + bindings: { + // 実 dev では wrangler.jsonc vars / secrets が供給する値のテスト代替。 + RUNTIME: "cloudflare-workers", + BETTER_AUTH_URL: "https://zedi-api-dev.test", + BETTER_AUTH_SECRET: "workerd-test-secret-0000000000000000", + // auth.ts はモジュールロード時に OAuth/CORS env も要求する(実 dev では + // wrangler secrets)。ここではダミー値でランタイム適合のみ検証する。 + GOOGLE_CLIENT_ID: "workerd-test-google-id", + GOOGLE_CLIENT_SECRET: "workerd-test-google-secret", + CORS_ORIGIN: "https://zedi-note.app", + // DB へは到達しない(lazy 接続)。到達した場合はマスク済み 5xx を検証する。 + DATABASE_URL: "postgres://workerd-test:unused@127.0.0.1:5/unused", + }, + }, + }), + ], + test: { + globals: true, + include: ["src/__tests__/worker/**/*.test.ts"], + }, +}); diff --git a/server/api/wrangler.jsonc b/server/api/wrangler.jsonc index 11aa4044..8b2f9d83 100644 --- a/server/api/wrangler.jsonc +++ b/server/api/wrangler.jsonc @@ -7,6 +7,11 @@ "observability": { "enabled": true, }, + // RUNTIME はランタイム判定の明示シグナル(#1091 SR-5 / clientIp.ts)。 + // ENVIRONMENT は dev/prod の区別であり Railway 側にも存在するため流用しない。 + "vars": { + "RUNTIME": "cloudflare-workers", + }, "r2_buckets": [ { "binding": "STORAGE_BUCKET", @@ -48,6 +53,7 @@ }, "vars": { "ENVIRONMENT": "development", + "RUNTIME": "cloudflare-workers", }, }, "production": { @@ -68,6 +74,7 @@ }, "vars": { "ENVIRONMENT": "production", + "RUNTIME": "cloudflare-workers", }, }, }, diff --git a/src/i18n/locales/en/auth.json b/src/i18n/locales/en/auth.json index 813bd8f3..98508195 100644 --- a/src/i18n/locales/en/auth.json +++ b/src/i18n/locales/en/auth.json @@ -5,7 +5,6 @@ "title": "Sign in", "subtitle": "Sign in to your account to continue", "google": "Sign in with Google", - "github": "Sign in with GitHub", "error": "Sign-in failed. Please try again." }, "callbackTimeout": "Could not complete sign-in. Please try again.", diff --git a/src/i18n/locales/en/invite.json b/src/i18n/locales/en/invite.json index a968b2e4..dd64ea87 100644 --- a/src/i18n/locales/en/invite.json +++ b/src/i18n/locales/en/invite.json @@ -9,7 +9,6 @@ "joining": "Joining...", "signInToAccept": "Sign in to accept this invitation", "signInWithGoogle": "Sign in with Google", - "signInWithGitHub": "Sign in with GitHub", "emailMismatch": "This account ({{userEmail}}) differs from the invited address ({{email}}).", "emailMismatchHelp": "You can accept the invitation in one of the following ways:", "sendMagicLinkCta": "Send a sign-in link to {{email}}", diff --git a/src/i18n/locales/ja/auth.json b/src/i18n/locales/ja/auth.json index db832cbc..e0217058 100644 --- a/src/i18n/locales/ja/auth.json +++ b/src/i18n/locales/ja/auth.json @@ -5,7 +5,6 @@ "title": "サインイン", "subtitle": "アカウントにサインインして続行", "google": "Google でサインイン", - "github": "GitHub でサインイン", "error": "サインインに失敗しました。再度お試しください。" }, "callbackTimeout": "サインインの完了を確認できませんでした。再度お試しください。", diff --git a/src/i18n/locales/ja/invite.json b/src/i18n/locales/ja/invite.json index 748b7da5..b35513f2 100644 --- a/src/i18n/locales/ja/invite.json +++ b/src/i18n/locales/ja/invite.json @@ -9,7 +9,6 @@ "joining": "参加中...", "signInToAccept": "招待を承認するにはサインインしてください", "signInWithGoogle": "Google でサインイン", - "signInWithGitHub": "GitHub でサインイン", "emailMismatch": "このアカウント ({{userEmail}}) は招待先 ({{email}}) と異なります。", "emailMismatchHelp": "以下のいずれかの方法で招待を受諾できます。", "sendMagicLinkCta": "{{email}} 宛にサインインリンクを送る", diff --git a/src/pages/ExtensionAuth.tsx b/src/pages/ExtensionAuth.tsx index 34b61637..e6a86667 100644 --- a/src/pages/ExtensionAuth.tsx +++ b/src/pages/ExtensionAuth.tsx @@ -2,7 +2,7 @@ * Chrome 拡張用認可開始ページ * * クエリ: redirect_uri, code_challenge, state - * Google/GitHub でサインインし、callbackURL(ExtensionAuthCallback)へ遷移する。 + * Google でサインインし、callbackURL(ExtensionAuthCallback)へ遷移する。 */ import React, { useState } from "react"; import { Link, useSearchParams } from "react-router-dom"; @@ -29,10 +29,10 @@ const ExtensionAuth: React.FC = () => { if (state) params.set("state", state); const callbackURL = params.toString() ? `${baseCallback}?${params.toString()}` : baseCallback; - const handleSocialSignIn = (provider: "google" | "github") => async () => { + const handleGoogle = async () => { setError(null); try { - await signIn.social({ provider, callbackURL }); + await signIn.social({ provider: "google", callbackURL }); } catch (err) { if (err instanceof Error) console.warn("Social sign-in failed:", err.message); setError(t("auth.signIn.error")); @@ -81,7 +81,7 @@ const ExtensionAuth: React.FC = () => {
-
)} diff --git a/src/pages/InviteLinkPage.test.tsx b/src/pages/InviteLinkPage.test.tsx index 915cf2ec..cd542826 100644 --- a/src/pages/InviteLinkPage.test.tsx +++ b/src/pages/InviteLinkPage.test.tsx @@ -234,11 +234,13 @@ describe("InviteLinkPage", () => { ).toBeInTheDocument(); }); - it("renders Google and GitHub sign-in buttons", () => { + it("renders Google sign-in only", () => { renderAt("/invite-links/share-token"); expect(screen.getByRole("button", { name: "invite.signInWithGoogle" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "invite.signInWithGitHub" })).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "invite.signInWithGitHub" }), + ).not.toBeInTheDocument(); }); it("starts Google social sign-in with returnTo callback URL", async () => { @@ -252,17 +254,6 @@ describe("InviteLinkPage", () => { }); }); - it("starts GitHub social sign-in with returnTo callback URL", async () => { - renderAt("/invite-links/share-token"); - fireEvent.click(screen.getByRole("button", { name: "invite.signInWithGitHub" })); - - await waitFor(() => expect(mockSignInSocial).toHaveBeenCalledTimes(1)); - expect(mockSignInSocial).toHaveBeenCalledWith({ - provider: "github", - callbackURL: `${window.location.origin}/auth/callback?returnTo=%2Finvite-links%2Fshare-token`, - }); - }); - it("shows destructive toast when social sign-in fails", async () => { mockSignInSocial.mockRejectedValueOnce(new Error("OAuth failed")); renderAt("/invite-links/share-token"); diff --git a/src/pages/InviteLinkPage.tsx b/src/pages/InviteLinkPage.tsx index d4c6930a..be02f706 100644 --- a/src/pages/InviteLinkPage.tsx +++ b/src/pages/InviteLinkPage.tsx @@ -51,23 +51,16 @@ const GoogleIcon: React.FC = () => ( ); -const GitHubIcon: React.FC = () => ( - - - -); - /** - * Social sign-in button wrapper for the share-link layout. - * 共有リンクページ用のソーシャルサインインボタン。 + * Google sign-in button for the share-link layout. + * 共有リンクページ用の Google サインインボタン。 */ -const SocialButton: React.FC<{ - provider: "google" | "github"; +const GoogleSignInButton: React.FC<{ label: string; onClick: () => void; -}> = ({ provider, label, onClick }) => ( +}> = ({ label, onClick }) => ( ); @@ -150,10 +143,10 @@ const SignInPane: React.FC<{ const { t } = useTranslation(); const { toast } = useToast(); const redirectTarget = `/invite-links/${encodeURIComponent(token)}`; - const handle = (provider: "google" | "github") => async () => { + const handleGoogle = async () => { const callbackURL = `${window.location.origin}/auth/callback?returnTo=${encodeURIComponent(redirectTarget)}`; try { - await signIn.social({ provider, callbackURL }); + await signIn.social({ provider: "google", callbackURL }); } catch { toast({ variant: "destructive", description: t("auth.signIn.error") }); } @@ -173,16 +166,7 @@ const SignInPane: React.FC<{

{t("invite.linkSignInRequired")}

- - +
diff --git a/src/pages/InvitePage.tsx b/src/pages/InvitePage.tsx index f4bcd6ff..8efc47e9 100644 --- a/src/pages/InvitePage.tsx +++ b/src/pages/InvitePage.tsx @@ -66,26 +66,15 @@ const GoogleIcon: React.FC = () => ( ); /** - * GitHub icon SVG. - * GitHub アイコン SVG。 + * Google sign-in button using the shared UI Button. + * Google サインインボタン(共有 UI Button を使用)。 */ -const GitHubIcon: React.FC = () => ( - - - -); - -/** - * Social sign-in button (Google / GitHub) using the shared UI Button. - * ソーシャルサインインボタン(共有 UI Button を使用)。 - */ -const SocialButton: React.FC<{ - provider: "google" | "github"; +const GoogleSignInButton: React.FC<{ label: string; onClick: () => void; -}> = ({ provider, label, onClick }) => ( +}> = ({ label, onClick }) => ( ); @@ -284,7 +273,7 @@ const InviteContent: React.FC<{ onAccept: () => void; acceptPending: boolean; acceptError: Error | null; - onSocialSignIn: (provider: "google" | "github") => () => void; + onGoogleSignIn: () => void; }> = ({ invitation, token, @@ -294,7 +283,7 @@ const InviteContent: React.FC<{ onAccept, acceptPending, acceptError, - onSocialSignIn, + onGoogleSignIn, }) => { const { t } = useTranslation(); const roleName = invitation.role === "editor" ? t("invite.roleEditor") : t("invite.roleViewer"); @@ -335,16 +324,7 @@ const InviteContent: React.FC<{

{t("invite.signInToAccept")}

- - +
@@ -411,18 +391,15 @@ const InvitePage: React.FC = () => { const currentPagePath = `/invite?token=${encodeURIComponent(token)}`; - const handleSocialSignIn = useCallback( - (provider: "google" | "github") => async () => { - const baseCallback = `${window.location.origin}/auth/callback`; - const callbackURL = `${baseCallback}?returnTo=${encodeURIComponent(currentPagePath)}`; - try { - await signIn.social({ provider, callbackURL }); - } catch { - toast({ variant: "destructive", description: t("auth.signIn.error") }); - } - }, - [currentPagePath, toast, t], - ); + const handleGoogleSignIn = useCallback(async () => { + const baseCallback = `${window.location.origin}/auth/callback`; + const callbackURL = `${baseCallback}?returnTo=${encodeURIComponent(currentPagePath)}`; + try { + await signIn.social({ provider: "google", callbackURL }); + } catch { + toast({ variant: "destructive", description: t("auth.signIn.error") }); + } + }, [currentPagePath, toast, t]); const handleAccept = useCallback(async () => { if (!token) return; @@ -492,7 +469,7 @@ const InvitePage: React.FC = () => { onAccept={handleAccept} acceptPending={acceptMutation.isPending} acceptError={acceptMutation.error} - onSocialSignIn={handleSocialSignIn} + onGoogleSignIn={handleGoogleSignIn} /> ); diff --git a/src/pages/SignIn.test.tsx b/src/pages/SignIn.test.tsx index 7a53fb77..2e1d52e1 100644 --- a/src/pages/SignIn.test.tsx +++ b/src/pages/SignIn.test.tsx @@ -60,14 +60,19 @@ describe("SignIn returnTo forwarding", () => { it("uses the bare callback URL when no returnTo is present", async () => { renderAt("/sign-in"); - fireEvent.click(screen.getByText("auth.signIn.github")); + fireEvent.click(screen.getByText("auth.signIn.google")); await waitFor(() => expect(signInSocial).toHaveBeenCalledTimes(1)); const arg = signInSocial.mock.calls[0][0] as { provider: string; callbackURL: string }; - expect(arg.provider).toBe("github"); + expect(arg.provider).toBe("google"); expect(arg.callbackURL).toBe(`${window.location.origin}/auth/callback`); }); + it("does not offer GitHub sign-in", () => { + renderAt("/sign-in"); + expect(screen.queryByText("auth.signIn.github")).not.toBeInTheDocument(); + }); + it("drops an unsafe returnTo (protocol-relative) to prevent open redirect", async () => { renderAt(`/sign-in?returnTo=${encodeURIComponent("//evil.example/")}`); diff --git a/src/pages/SignIn.tsx b/src/pages/SignIn.tsx index 3be07ccb..ae71e23d 100644 --- a/src/pages/SignIn.tsx +++ b/src/pages/SignIn.tsx @@ -11,8 +11,8 @@ function getSafeReturnTo(returnTo: string | null): string | null { } /** - * Sign-in page. Social login and returnTo redirect. - * サインインページ。ソーシャルログインとreturnToリダイレクト。 + * Sign-in page. Google social login and returnTo redirect. + * サインインページ。Google ソーシャルログインと returnTo リダイレクト。 */ const SignIn: React.FC = () => { const { t } = useTranslation(); @@ -24,17 +24,16 @@ const SignIn: React.FC = () => { const callbackURL = returnTo ? `${baseCallback}?returnTo=${encodeURIComponent(returnTo)}` : baseCallback; - const handleSocialSignIn = (provider: "google" | "github") => async () => { + + const handleGoogle = async () => { setSocialError(null); try { - await signIn.social({ provider, callbackURL }); + await signIn.social({ provider: "google", callbackURL }); } catch (err) { if (err instanceof Error) console.warn("Social sign-in failed:", err.message); setSocialError(t("auth.signIn.error")); } }; - const handleGoogle = handleSocialSignIn("google"); - const handleGitHub = handleSocialSignIn("github"); return (
@@ -86,16 +85,6 @@ const SignIn: React.FC = () => { {t("auth.signIn.google")} -