Skip to content

feat(ai-engine): ADR-0011 PR-E2 — OAuthClient + LoopbackCallbackServer#30

Merged
shomatan merged 2 commits into
mainfrom
feat/oauth-client-pr-e2
May 2, 2026
Merged

feat(ai-engine): ADR-0011 PR-E2 — OAuthClient + LoopbackCallbackServer#30
shomatan merged 2 commits into
mainfrom
feat/oauth-client-pr-e2

Conversation

@shomatan
Copy link
Copy Markdown
Contributor

@shomatan shomatan commented May 2, 2026

Summary

ADR-0011 (Tally 側で OAuth 2.1 フローを管理する) の実装段階 2。issue #28 への対応として、外部 MCP server に対する OAuth 2.1 (Authorization Code + PKCE) クライアントを実装する。

PR-E1 (#29 merged) で作った schema / token store の上に乗る形。本 PR でも OAuth フロー全体は接続されない (PR-E3 で API + UI、PR-E4 で SDK 統合)。

設計判断

  • Node 標準のみ (依存追加なし、oauth4webapi 等は使わない)
  • PKCE は S256 のみ (RFC 7636 推奨)、plain は実装しない
  • state の verify は呼び出し側 (PR-E3 の Orchestrator) の責務
  • LoopbackCallbackServer は port=0 で OS 採番、host は 127.0.0.1 固定 (localhost だと IPv4/IPv6 衝突)
  • token endpoint は application/x-www-form-urlencoded (RFC 6749 §4.1.3 準拠)
  • public client (PKCE 経由) なので client_secret は使わない

変更ファイル

packages/ai-engine/src/oauth/ (新規)

  • oauth-client.ts: PKCE pair / state / authorization URL / token 交換 / refresh
  • loopback-callback-server.ts: 一時 HTTP server (127.0.0.1, port=0, callback path 限定、HTML escape、timeout、冪等 close、pending reject 発火)
  • index.ts: 公開 export
  • 関連 test 22 件

packages/core (拡張)

  • OAuthProviderConfig.prompt?: string: Atlassian 用 'consent' を provider 設定で制御(ハードコード回避)
  • OAUTH_REGISTRYas const satisfies Readonly<...> に変更(リテラル型維持 + contract check)
  • OAuthProviderConfig 型を core から公開 export

codex セカンドオピニオン対応 (push 直前 review)

重要度 対応
MEDIUM 2 close() で pending awaitCallback を reject 発火 / promptOAuthProviderConfig
LOW 2 access_tokentypeof + 空文字列 で明示チェック / close 中 pending reject の test

Test plan

  • pnpm typecheck (4/4 PASS)
  • pnpm test (core 99 + storage 97 + ai-engine 235 + frontend 273 = 704 PASS)
  • pnpm lint (exit 0)
  • PR-E3 で API endpoint と UI を繋ぐ際に LoopbackCallbackServer が想定通り動くか結合確認 (本 PR 範囲外)

Related

Summary by CodeRabbit

リリースノート

  • 新機能
    • OAuth 2.1認証フロー(PKCE対応)を追加しました。
    • 認可コードからアクセストークンへの交換機能を実装しました。
    • トークン更新機能を追加しました。
    • ローカルコールバック サーバーを実装し、OAuth認証の受け取りに対応しました。
    • Atlassian OAuth統合を改善しました。

ADR-0011 の実装段階 2。issue #28 への対応として、外部 MCP server に対する
OAuth 2.1 (Authorization Code + PKCE) クライアントを実装する。本 PR でも
OAuth フロー全体は接続されない (PR-E3 で API + UI、PR-E4 で SDK 統合)。

## 設計判断

- Node 標準のみ (依存追加なし、oauth4webapi 等は使わない)
- PKCE は S256 のみ (RFC 7636 推奨)
- state の verify は呼び出し側 (PR-E3 の Orchestrator) の責務
- LoopbackCallbackServer は port=0 で OS 採番、host は 127.0.0.1 固定
- token endpoint は application/x-www-form-urlencoded で叩く

## 新規ファイル

- packages/ai-engine/src/oauth/oauth-client.ts: PKCE / state / authorization
  URL / token 交換 / refresh
- packages/ai-engine/src/oauth/oauth-client.test.ts: 12 件
- packages/ai-engine/src/oauth/loopback-callback-server.ts: 一時 HTTP server
  (127.0.0.1, port=0, callback path 限定、HTML escape、timeout、冪等 close)
- packages/ai-engine/src/oauth/loopback-callback-server.test.ts: 10 件
- packages/ai-engine/src/oauth/index.ts: 公開 export

## packages/core 拡張

- OAuthProviderConfig.prompt?: string を追加 (Atlassian 用に 'consent')。
  ハードコードを避けて将来の prompt 不要 provider を許容する。
- OAUTH_REGISTRY を as const satisfies Readonly<...> に変更
- OAuthProviderConfig 型を core から公開 export

## codex セカンドオピニオン対応

[MEDIUM] close() で pending awaitCallback を reject 発火 (永久 pending リーク防止)
[MEDIUM] prompt=consent を OAuthProviderConfig に移して provider 制御化
[LOW] access_token を typeof + 空文字列で明示チェック
[LOW] close 中の pending awaitCallback の reject test を追加

pnpm typecheck 4/4 PASS / pnpm test core 99 + storage 97 + ai-engine 235 +
frontend 273 = 704 PASS / pnpm lint exit 0。
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

Warning

Rate limit exceeded

@shomatan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 36 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 35d3f83e-d07d-4d01-a983-e3ae85ad91db

📥 Commits

Reviewing files that changed from the base of the PR and between 7a45430 and d9ecb2e.

📒 Files selected for processing (2)
  • packages/ai-engine/src/oauth/loopback-callback-server.test.ts
  • packages/ai-engine/src/oauth/loopback-callback-server.ts
📝 Walkthrough

Walkthrough

このPRはOAuth 2.1クライアント実装を追加します。OSが割り当てたポートで127.0.0.1にバインドするループバックHTTPコールバックサーバー、PKCE、state生成、認可URL構築、トークン交換・更新機能、およびAtlassianプロバイダー構成の更新を含みます。

Changes

OAuth 2.1クライアント実装

Layer / File(s) Summary
型定義
packages/ai-engine/src/oauth/oauth-client.ts(11-87行目)、packages/ai-engine/src/oauth/loopback-callback-server.ts(14-36行目)
PkcePairTokenExchangeResultBuildAuthorizationUrlInputExchangeCodeInputRefreshTokenInputLoopbackCallbackLoopbackCallbackHandleStartLoopbackCallbackServerOptionsなどの新しい型インターフェースを定義します。
OAuth クライアント実装
packages/ai-engine/src/oauth/oauth-client.ts(11-157行目)
PKCE生成(SHA-256ベースのcodeChallenge)、OAuth state生成、認可エンドポイントURL構築、codeをトークンに交換、refresh tokenを使用したアクセストークン更新を実装します。
ループバックコールバックサーバー実装
packages/ai-engine/src/oauth/loopback-callback-server.ts(38-157行目)
127.0.0.1上のHTTPサーバーをバインドし、redirectUriを公開し、code/stateパラメータを解析し、pending callbackを解決・却下し、タイムアウト処理とidempotent close()を提供します。
プロバイダー設定更新
packages/core/src/oauth/atlassian.ts(19-47行目)
OAuthProviderConfigprompt?: stringフィールドを追加し、ATLASSIAN_CLOUD_OAUTHprompt: 'consent'を設定し、OAUTH_REGISTRYをリテラル型保持のためas const satisfiesで再定義します。
パッケージ再エクスポート
packages/ai-engine/src/oauth/index.tspackages/core/src/oauth/index.tspackages/core/src/index.ts
新しいOAuthクライアントAPIとループバックサーバーAPIを再エクスポートし、OAuthProviderConfig型を公開APIに含めます。
テスト
packages/ai-engine/src/oauth/oauth-client.test.ts(1-218行目)、packages/ai-engine/src/oauth/loopback-callback-server.test.ts(1-117行目)
PKCE/state生成、認可URL構築、トークン交換・更新フロー、ループバックサーバーのredirectUri、コールバック成功/エラー、HTTP 404、タイムアウト、close()idempotency、port 0動作、カスタムパスをテストします。

Sequence Diagram

sequenceDiagram
    participant User as ユーザー
    participant Client as クライアント
    participant LoopbackServer as ループバック<br/>サーバー
    participant AuthServer as 認可<br/>サーバー
    participant TokenServer as トークン<br/>エンドポイント

    User->>Client: OAuth開始をリクエスト
    Client->>Client: generatePkcePair()でPKCE生成
    Client->>Client: generateOAuthState()でstate生成
    Client->>LoopbackServer: startLoopbackCallbackServer()で開始
    LoopbackServer-->>Client: redirectUri(ポート付き)を返す
    Client->>Client: buildAuthorizationUrl()で認可URLを構築
    Client->>User: 認可URLをブラウザで開く
    User->>AuthServer: 認可URLにアクセス
    AuthServer->>User: ログイン・同意画面を表示
    User->>AuthServer: ログイン・同意
    AuthServer->>LoopbackServer: redirectUri + code + stateでリダイレクト
    LoopbackServer->>LoopbackServer: awaitCallback()でcode/stateを解決
    LoopbackServer-->>AuthServer: HTML応答(200)
    AuthServer-->>User: ブラウザでタブを閉じるよう指示
    LoopbackServer-->>Client: code/stateを返す
    Client->>TokenServer: exchangeCodeForToken()でPOST<br/>(code+codeVerifier)
    TokenServer-->>Client: accessToken+refreshToken
    Client->>LoopbackServer: close()でサーバーを停止
    Client-->>User: 認可完了
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed プルリクエストのタイトルは、OAuth 2.1 クライアント実装と LoopbackCallbackServer という主要な変更を明確に要約しており、PR の主要な目的と完全に一致しています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oauth-client-pr-e2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 54 minutes and 36 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/ai-engine/src/oauth/oauth-client.test.ts (1)

1-3: ⚡ Quick win

import 順序を規約順に揃えてください

vitest(external)を先にし、@tally/core(internal)を次に置く順にしてください。

修正例
-import { ATLASSIAN_CLOUD_OAUTH } from '@tally/core';
 import { afterEach, describe, expect, it, vi } from 'vitest';
 
+import { ATLASSIAN_CLOUD_OAUTH } from '@tally/core';
+
 import {
   buildAuthorizationUrl,
   exchangeCodeForToken,

As per coding guidelines, "Import order should be: external libraries → internal packages → relative paths, with blank lines between groups".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-engine/src/oauth/oauth-client.test.ts` around lines 1 - 3, The
import block is not in the mandated order; move the vitest imports (afterEach,
describe, expect, it, vi) to be the first import group (external libraries),
then place the internal import (ATLASSIAN_CLOUD_OAUTH from '@tally/core') after
a blank line so imports follow "external → internal → relative" ordering and
include a separating blank line between groups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ai-engine/src/oauth/loopback-callback-server.ts`:
- Around line 119-130: awaitCallback currently overwrites
resolveCallback/rejectCallback and timeoutHandle, leaking previously created
Promises and allowing calls after close to hang; modify awaitCallback to (1)
track a single active Promise (e.g. storedActivePromise) and if called while one
exists either return that same Promise or first reject/cleanup the previous one
before creating a new Promise, (2) check a closed flag set by close() and
immediately return a rejected Promise when closed, and (3) ensure cleanup()
clears resolveCallback, rejectCallback, timeoutHandle and the
storedActivePromise; update references to
resolveCallback/rejectCallback/timeoutHandle/cleanup()/close() in the same file
accordingly so no Promise can be left unresolved on multiple awaitCallback calls
or after close().

---

Nitpick comments:
In `@packages/ai-engine/src/oauth/oauth-client.test.ts`:
- Around line 1-3: The import block is not in the mandated order; move the
vitest imports (afterEach, describe, expect, it, vi) to be the first import
group (external libraries), then place the internal import
(ATLASSIAN_CLOUD_OAUTH from '@tally/core') after a blank line so imports follow
"external → internal → relative" ordering and include a separating blank line
between groups.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a91827ad-b2b0-4e83-a038-f6dbdca72c19

📥 Commits

Reviewing files that changed from the base of the PR and between 43c2f7d and 7a45430.

📒 Files selected for processing (8)
  • packages/ai-engine/src/oauth/index.ts
  • packages/ai-engine/src/oauth/loopback-callback-server.test.ts
  • packages/ai-engine/src/oauth/loopback-callback-server.ts
  • packages/ai-engine/src/oauth/oauth-client.test.ts
  • packages/ai-engine/src/oauth/oauth-client.ts
  • packages/core/src/index.ts
  • packages/core/src/oauth/atlassian.ts
  • packages/core/src/oauth/index.ts

Comment thread packages/ai-engine/src/oauth/loopback-callback-server.ts
CodeRabbit (PR #30) Major 1 件対応。

awaitCallback() は内部で resolveCallback / rejectCallback を上書きする実装
だったため、同 handle で 2 回以上呼ぶと先行 Promise が未解決のまま残って
リークしていた。close() 後の呼び出しも server 閉鎖済みで callback は届かず
永久 pending になっていた。

`awaitStarted` フラグと既存の `closed` フラグを冒頭で確認し、いずれかに
該当すれば即時 throw する。1 ハンドル 1 回呼出 + close 後不可の規約を
明示的に強制する。

test 2 件追加: 多重呼び出しで throw / close 後呼び出しで throw。

pnpm typecheck 4/4 PASS / pnpm test ai-engine 237 PASS / pnpm lint exit 0。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant