26 KiB
OAuth2 模块化功能实施计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 实现基于 GitHub 的 OAuth2 登录模块,支持新用户 OAuth 登录和已注册用户账号绑定,采用配置驱动架构设计。
Architecture: 采用策略模式实现 OAuth Provider 接口,核心模块 OAuthManager 统一调度,StateStore 管理 CSRF 令牌防止攻击,集成到现有认证系统。
Tech Stack: Nuxt 4 / Nitro API / TypeScript / SQLite / Drizzle ORM
文件结构
packages/oauth/
├── src/
│ ├── index.ts # 模块入口
│ ├── core/
│ │ ├── oauth-manager.ts # 核心管理器
│ │ ├── oauth-state-store.ts # State 存储
│ │ └── oauth-error.ts # 错误类
│ ├── providers/
│ │ ├── base.ts # Provider 接口
│ │ ├── github.ts # GitHub Provider
│ │ └── index.ts # Provider 导出
│ ├── db/
│ │ ├── schema.ts # oauth_accounts 表定义
│ │ └── queries.ts # 数据库操作
│ └── api/
│ ├── authorize.get.ts # 授权接口
│ ├── callback.get.ts # 回调接口
│ ├── bind.post.ts # 绑定接口
│ ├── unbind.delete.ts # 解绑接口
│ ├── bindings.get.ts # 绑定列表接口
│ └── status.get.ts # 绑定状态接口
├── package.json
└── tsconfig.json
Task 1: 创建 oauth 包基础结构
Files:
-
Create:
packages/oauth/package.json -
Create:
packages/oauth/tsconfig.json -
Create:
packages/oauth/src/index.ts -
Step 1: 创建目录结构
mkdir -p packages/oauth/src/{core,providers,db,api}
- Step 2: 创建 package.json
{
"name": "oauth",
"version": "0.1.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"zod": "4.3.6"
},
"devDependencies": {
"@types/node": "20.0.0"
}
}
- Step 3: 创建 tsconfig.json
{
"extends": "tsconfig/tsconfig.json"
}
- Step 4: 创建 src/index.ts
export { OAuthManager } from './core/oauth-manager';
export { OAuthStateStore } from './core/oauth-state-store';
export { OAuthError, OAuthErrorCode } from './core/oauth-error';
export type { OAuthProvider, OAuthUserInfo } from './providers/base';
export type { OAuthBinding, OAuthCallbackResult } from './core/oauth-manager';
- Step 5: 在根目录 package.json 添加 workspace 依赖
在根目录 package.json 的 scripts.postinstall 之前添加:
"install:oauth": "bun add -w oauth",
然后运行 bun install 安装依赖。
- Step 6: Commit
git add packages/oauth/
git commit -m "feat(oauth): 创建 oauth 包基础结构"
Task 2: 实现 OAuth 错误类
Files:
-
Create:
packages/oauth/src/core/oauth-error.ts -
Step 1: 创建 oauth-error.ts
export const OAuthErrorCodes = {
PROVIDER_NOT_FOUND: 'OAUTH_PROVIDER_NOT_FOUND',
STATE_INVALID: 'OAUTH_STATE_INVALID',
STATE_EXPIRED: 'OAUTH_STATE_EXPIRED',
TOKEN_EXCHANGE_FAILED: 'OAUTH_TOKEN_EXCHANGE_FAILED',
USER_INFO_FAILED: 'OAUTH_USER_INFO_FAILED',
ALREADY_BIND: 'OAUTH_ALREADY_BIND',
BINDING_USER_MISMATCH: 'OAUTH_BINDING_USER_MISMATCH',
BINDING_TOKEN_INVALID: 'OAUTH_BINDING_TOKEN_INVALID',
LAST_BIND: 'OAUTH_LAST_BIND',
} as const;
export type OAuthErrorCode = typeof OAuthErrorCodes[keyof typeof OAuthErrorCodes];
export class OAuthError extends Error {
constructor(
public code: OAuthErrorCode,
message: string,
public statusCode: number = 400
) {
super(message);
this.name = 'OAuthError';
}
}
- Step 2: Commit
git add packages/oauth/src/core/oauth-error.ts
git commit -m "feat(oauth): 添加 OAuth 错误类"
Task 3: 实现 State 存储管理
Files:
-
Create:
packages/oauth/src/core/oauth-state-store.ts -
Step 1: 创建 oauth-state-store.ts
import { randomUUID } from 'crypto';
export type OAuthState = {
providerName: string;
redirectUri: string;
createdAt: number;
userId?: number;
isBinding: boolean;
};
const STATE_TTL_MS = 5 * 60 * 1000; // 5 minutes
export class OAuthStateStore {
private store = new Map<string, OAuthState>();
// 定期清理过期 state
private cleanupInterval: NodeJS.Timeout | null = null;
constructor() {
this.startCleanup();
}
generate(
providerName: string,
redirectUri: string,
userId?: number
): { state: string; oauthState: OAuthState } {
const state = randomUUID();
const oauthState: OAuthState = {
providerName,
redirectUri,
createdAt: Date.now(),
userId,
isBinding: userId !== undefined,
};
this.store.set(state, oauthState);
return { state, oauthState };
}
validate(state: string): OAuthState | null {
const oauthState = this.store.get(state);
if (!oauthState) {
return null;
}
if (Date.now() - oauthState.createdAt > STATE_TTL_MS) {
this.store.delete(state);
return null;
}
return oauthState;
}
consume(state: string): OAuthState | null {
const oauthState = this.validate(state);
if (oauthState) {
this.store.delete(state);
}
return oauthState;
}
private startCleanup() {
// 每分钟清理一次过期 state
this.cleanupInterval = setInterval(() => {
const now = Date.now();
for (const [state, data] of this.store.entries()) {
if (now - data.createdAt > STATE_TTL_MS) {
this.store.delete(state);
}
}
}, 60_000);
}
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
}
}
export const oauthStateStore = new OAuthStateStore();
- Step 2: Commit
git add packages/oauth/src/core/oauth-state-store.ts
git commit -m "feat(oauth): 实现 State 存储管理"
Task 4: 实现 GitHub Provider
Files:
-
Create:
packages/oauth/src/providers/base.ts -
Create:
packages/oauth/src/providers/github.ts -
Create:
packages/oauth/src/providers/index.ts -
Step 1: 创建 providers/base.ts
export interface OAuthProvider {
name: string;
icon: string;
clientId: string;
clientSecret: string;
authorizeUrl: string;
tokenUrl: string;
userInfoUrl: string;
scopes: string[];
getAuthorizationUrl(code: string, state: string): string;
mapUserInfo(raw: Record<string, unknown>): OAuthUserInfo;
}
export type OAuthUserInfo = {
provider: string;
providerUserId: string;
username?: string;
email?: string;
avatar?: string;
};
- Step 2: 创建 providers/github.ts
import type { OAuthProvider, OAuthUserInfo } from './base';
export class GitHubProvider implements OAuthProvider {
name = 'github';
icon = 'github';
clientId: string;
clientSecret: string;
authorizeUrl = 'https://github.com/login/oauth/authorize';
tokenUrl = 'https://github.com/login/oauth/access_token';
userInfoUrl = 'https://api.github.com/user';
scopes = ['read:user', 'user:email'];
constructor(config: { clientId: string; clientSecret: string }) {
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
}
getAuthorizationUrl(code: string, state: string): string {
const params = new URLSearchParams({
client_id: this.clientId,
redirect_uri: `${process.env.APP_URL}/api/auth/oauth/github/callback`,
scope: this.scopes.join(' '),
state,
});
return `${this.authorizeUrl}?${params.toString()}`;
}
mapUserInfo(raw: Record<string, unknown>): OAuthUserInfo {
return {
provider: this.name,
providerUserId: String(raw.id),
username: String(raw.login ?? ''),
email: String(raw.email ?? ''),
avatar: String(raw.avatar_url ?? ''),
};
}
}
- Step 3: 创建 providers/index.ts
import { GitHubProvider } from './github';
export type { OAuthProvider, OAuthUserInfo } from './base';
export const oauthProviders = {
github: new GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
};
export function getProvider(name: string): OAuthProvider | undefined {
return (oauthProviders as Record<string, OAuthProvider>)[name];
}
- Step 4: Commit
git add packages/oauth/src/providers/
git commit -m "feat(oauth): 实现 GitHub Provider"
Task 5: 实现数据库 Schema 和查询
Files:
-
Create:
packages/oauth/src/db/schema.ts -
Create:
packages/oauth/src/db/queries.ts -
Step 1: 创建 db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const oauthAccounts = sqliteTable('oauth_accounts', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull(),
provider: text('provider').notNull(),
providerUserId: text('provider_user_id').notNull(),
username: text('username'),
email: text('email'),
avatar: text('avatar'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export type OAuthAccount = typeof oauthAccounts.$inferSelect;
export type NewOAuthAccount = typeof oauthAccounts.$inferInsert;
注意: 需要在 drizzle-pkg 中添加 oauth_accounts 表的定义,因为 drizzle-pkg 管理数据库 schema。
- Step 2: 创建 db/queries.ts
import { dbGlobal } from 'drizzle-pkg/lib/db';
import { oauthAccounts } from './schema';
import { eq, and } from 'drizzle-orm';
import type { NewOAuthAccount } from './schema';
import type { OAuthUserInfo } from '../providers/base';
export async function createOAuthAccount(data: {
userId: number;
provider: string;
providerUserId: string;
username?: string;
email?: string;
avatar?: string;
}): Promise<OAuthAccount> {
const now = new Date();
const [account] = await dbGlobal
.insert(oauthAccounts)
.values({
userId: data.userId,
provider: data.provider,
providerUserId: data.providerUserId,
username: data.username,
email: data.email,
avatar: data.avatar,
createdAt: now,
updatedAt: now,
})
.returning();
return account;
}
export async function findOAuthAccount(
provider: string,
providerUserId: string
): Promise<OAuthAccount | null> {
const [account] = await dbGlobal
.select()
.from(oauthAccounts)
.where(and(
eq(oauthAccounts.provider, provider),
eq(oauthAccounts.providerUserId, providerUserId)
));
return account ?? null;
}
export async function findUserOAuthAccounts(userId: number): Promise<OAuthAccount[]> {
return dbGlobal
.select()
.from(oauthAccounts)
.where(eq(oauthAccounts.userId, userId));
}
export async function deleteOAuthAccount(
userId: number,
provider: string
): Promise<boolean> {
const result = await dbGlobal
.delete(oauthAccounts)
.where(and(
eq(oauthAccounts.userId, userId),
eq(oauthAccounts.provider, provider)
));
return result.rowCount > 0;
}
- Step 3: 在 drizzle-pkg 添加 oauth_accounts 表
需要修改 packages/drizzle-pkg/lib/schema/auth.ts 添加 oauth_accounts 表定义。
在 sessions 表定义后添加:
export const oauthAccounts = sqliteTable('oauth_accounts', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull(),
provider: text('provider').notNull(),
providerUserId: text('provider_user_id').notNull(),
username: text('username'),
email: text('email'),
avatar: text('avatar'),
createdAt: integer('created_at').default(sql`(cast((julianday('now') - 2440587.5)*86400000 as integer))`),
updatedAt: integer('updated_at').default(sql`(cast((julianday('now') - 2440587.5)*86400000 as integer))`),
});
然后生成数据库迁移 SQL。
- Step 4: Commit
git add packages/oauth/src/db/
git add packages/drizzle-pkg/lib/schema/auth.ts
git commit -m "feat(oauth): 添加 oauth_accounts 表和查询"
Task 6: 实现 OAuthManager 核心类
Files:
-
Create:
packages/oauth/src/core/oauth-manager.ts -
Step 1: 创建 core/oauth-manager.ts
import type { OAuthProvider, OAuthUserInfo } from '../providers/base';
import { getProvider } from '../providers';
import { OAuthStateStore, type OAuthState } from './oauth-state-store';
import { OAuthError, OAuthErrorCodes } from './oauth-error';
import {
createOAuthAccount,
findOAuthAccount,
findUserOAuthAccounts,
deleteOAuthAccount,
} from '../db/queries';
import { registerUser } from 'auth';
import { createSession } from 'auth';
import { MinimalUser } from 'auth';
export type OAuthBinding = {
id: number;
provider: string;
username: string | null;
email: string | null;
avatar: string | null;
boundAt: Date;
};
export type OAuthCallbackResult = {
success: boolean;
isNewUser: boolean;
user?: MinimalUser;
requiresBinding: boolean;
bindingToken?: string;
sessionId?: string;
expiresAt?: Date;
};
export class OAuthManager {
private stateStore: OAuthStateStore;
constructor() {
this.stateStore = new OAuthStateStore();
}
getAuthorizationUrl(providerName: string, userId?: number): string {
const provider = getProvider(providerName);
if (!provider) {
throw new OAuthError(
OAuthErrorCodes.PROVIDER_NOT_FOUND,
`Provider ${providerName} not found`
);
}
const redirectUri = userId
? `/profile?bind_success=1`
: `/auth/login?oauth_success=1`;
const { state } = this.stateStore.generate(providerName, redirectUri, userId);
return provider.getAuthorizationUrl('', state);
}
async handleCallback(
providerName: string,
code: string,
state: string
): Promise<OAuthCallbackResult> {
const provider = getProvider(providerName);
if (!provider) {
throw new OAuthError(
OAuthErrorCodes.PROVIDER_NOT_FOUND,
`Provider ${providerName} not found`
);
}
const oauthState = this.stateStore.consume(state);
if (!oauthState) {
throw new OAuthError(
OAuthErrorCodes.STATE_INVALID,
'Invalid or expired state'
);
}
if (oauthState.providerName !== providerName) {
throw new OAuthError(
OAuthErrorCodes.STATE_INVALID,
'State provider mismatch'
);
}
// 交换 token
const tokenResponse = await this.exchangeToken(provider, code);
if (!tokenResponse.access_token) {
throw new OAuthError(
OAuthErrorCodes.TOKEN_EXCHANGE_FAILED,
'Failed to exchange token'
);
}
// 获取用户信息
const userInfo = await this.getUserInfo(provider, tokenResponse.access_token);
if (!userInfo) {
throw new OAuthError(
OAuthErrorCodes.USER_INFO_FAILED,
'Failed to get user info'
);
}
// 查找是否已绑定
const existingAccount = await findOAuthAccount(providerName, userInfo.providerUserId);
if (oauthState.isBinding) {
// 绑定模式:关联到已登录用户
if (existingAccount && existingAccount.userId !== oauthState.userId) {
throw new OAuthError(
OAuthErrorCodes.ALREADY_BIND,
'This OAuth account is already bound to another user'
);
}
await createOAuthAccount({
userId: oauthState.userId!,
provider: providerName,
providerUserId: userInfo.providerUserId,
username: userInfo.username,
email: userInfo.email,
avatar: userInfo.avatar,
});
return {
success: true,
isNewUser: false,
requiresBinding: false,
};
}
// 登录模式
if (existingAccount) {
// 已存在用户,创建 session
const { sessionId, expiresAt } = await createSession(existingAccount.userId);
return {
success: true,
isNewUser: false,
requiresBinding: false,
sessionId,
expiresAt,
};
}
// 新用户:自动注册
const newUser = await registerUser({
username: userInfo.username || `oauth_${providerName}_${userInfo.providerUserId}`,
password: '', // OAuth 用户不需要密码
} as any);
await createOAuthAccount({
userId: newUser.id,
provider: providerName,
providerUserId: userInfo.providerUserId,
username: userInfo.username,
email: userInfo.email,
avatar: userInfo.avatar,
});
const { sessionId, expiresAt } = await createSession(newUser.id);
return {
success: true,
isNewUser: true,
user: newUser,
requiresBinding: false,
sessionId,
expiresAt,
};
}
async bindAccount(
userId: number,
providerName: string,
userInfo: OAuthUserInfo
): Promise<void> {
const existingAccount = await findOAuthAccount(providerName, userInfo.providerUserId);
if (existingAccount && existingAccount.userId !== userId) {
throw new OAuthError(
OAuthErrorCodes.ALREADY_BIND,
'This OAuth account is already bound to another user'
);
}
if (existingAccount) {
return; // 已绑定,跳过
}
await createOAuthAccount({
userId,
provider: providerName,
providerUserId: userInfo.providerUserId,
username: userInfo.username,
email: userInfo.email,
avatar: userInfo.avatar,
});
}
async unbindAccount(userId: number, providerName: string): Promise<void> {
const deleted = await deleteOAuthAccount(userId, providerName);
if (!deleted) {
throw new OAuthError(
OAuthErrorCodes.PROVIDER_NOT_FOUND,
'OAuth account not found'
);
}
}
async getUserBindings(userId: number): Promise<OAuthBinding[]> {
const accounts = await findUserOAuthAccounts(userId);
return accounts.map((account) => ({
id: account.id,
provider: account.provider,
username: account.username,
email: account.email,
avatar: account.avatar,
boundAt: account.createdAt,
}));
}
private async exchangeToken(
provider: OAuthProvider,
code: string
): Promise<{ access_token?: string; token_type?: string; scope?: string }> {
const response = await fetch(provider.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
client_id: provider.clientId,
client_secret: provider.clientSecret,
code,
redirect_uri: `${process.env.APP_URL}/api/auth/oauth/${provider.name}/callback`,
}),
});
if (!response.ok) {
throw new OAuthError(
OAuthErrorCodes.TOKEN_EXCHANGE_FAILED,
'Failed to exchange token'
);
}
return response.json() as Promise<{
access_token?: string;
token_type?: string;
scope?: string;
}>;
}
private async getUserInfo(
provider: OAuthProvider,
accessToken: string
): Promise<OAuthUserInfo | null> {
const response = await fetch(provider.userInfoUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
});
if (!response.ok) {
return null;
}
const raw = await response.json() as Record<string, unknown>;
return provider.mapUserInfo(raw);
}
}
export const oauthManager = new OAuthManager();
- Step 2: Commit
git add packages/oauth/src/core/oauth-manager.ts
git commit -m "feat(oauth): 实现 OAuthManager 核心类"
Task 7: 实现 API 接口
Files:
-
Create:
packages/oauth/src/api/authorize.get.ts -
Create:
packages/oauth/src/api/callback.get.ts -
Create:
packages/oauth/src/api/bind.post.ts -
Create:
packages/oauth/src/api/unbind.delete.ts -
Create:
packages/oauth/src/api/bindings.get.ts -
Create:
packages/oauth/src/api/status.get.ts -
Step 1: 创建 authorize.get.ts
import { oauthManager } from 'oauth';
import { createAuthContext } from '#server/service/auth/context';
export default defineWrappedResponseHandler(async (event) => {
const { getCurrent } = createAuthContext(event);
const user = await getCurrent();
const providerName = getRouterParam(event, 'provider');
if (!providerName) {
throw createError({
statusCode: 400,
statusMessage: 'Provider is required',
});
}
const userId = user?.id;
const authUrl = oauthManager.getAuthorizationUrl(providerName, userId);
return sendRedirect(event, authUrl);
});
- Step 2: 创建 callback.get.ts
import { oauthManager } from 'oauth';
import { setSessionCookie } from '#server/service/auth/cookie';
export default defineWrappedResponseHandler(async (event) => {
const providerName = getRouterParam(event, 'provider');
const query = getQuery(event);
const { code, state } = query as { code?: string; state?: string };
if (!code || !state) {
return sendRedirect(event, '/auth/login?oauth_error=missing_params');
}
try {
const result = await oauthManager.handleCallback(providerName!, code, state);
if (result.sessionId) {
setSessionCookie(event, result.sessionId);
}
const redirectUri = new URL(
result.isNewUser ? '/auth/login?oauth_success=1' : '/auth/login?oauth_success=1'
);
return sendRedirect(event, redirectUri.toString());
} catch (error) {
const errorCode = error instanceof OAuthError ? error.code : 'OAUTH_UNKNOWN';
return sendRedirect(event, `/auth/login?oauth_error=${errorCode}`);
}
});
- Step 3: 创建 bindings.get.ts
import { oauthManager } from 'oauth';
import { createAuthContext } from '#server/service/auth/context';
export default defineWrappedResponseHandler(async (event) => {
const { requireUser } = createAuthContext(event);
const user = await requireUser();
const bindings = await oauthManager.getUserBindings(user.id);
return R.success({ bindings });
});
- Step 4: 创建 status.get.ts
import { oauthManager } from 'oauth';
import { createAuthContext } from '#server/service/auth/context';
export default defineWrappedResponseHandler(async (event) => {
const { requireUser } = createAuthContext(event);
const user = await requireUser();
const providerName = getRouterParam(event, 'provider');
const bindings = await oauthManager.getUserBindings(user.id);
const binding = bindings.find((b) => b.provider === providerName);
return R.success({
bound: !!binding,
binding: binding
? {
id: binding.id,
username: binding.username,
}
: undefined,
});
});
- Step 5: 创建 unbind.delete.ts
import { oauthManager } from 'oauth';
import { createAuthContext } from '#server/service/auth/context';
export default defineWrappedResponseHandler(async (event) => {
const { requireUser } = createAuthContext(event);
const user = await requireUser();
const providerName = getRouterParam(event, 'provider');
await oauthManager.unbindAccount(user.id, providerName!);
return R.success({ success: true });
});
- Step 6: 创建 bind.post.ts
import { oauthManager } from 'oauth';
import { createAuthContext } from '#server/service/auth/context';
export default defineWrappedResponseHandler(async (event) => {
const { requireUser } = createAuthContext(event);
const user = await requireUser();
const body = await readBody(event);
const { bindingToken } = body;
// bindingToken 在当前设计中暂不使用,回调时已直接绑定
return R.success({
success: true,
binding: {
provider: 'github',
username: user.username,
},
});
});
- Step 7: Commit
git add packages/oauth/src/api/
git commit -m "feat(oauth): 实现 API 接口"
Task 8: 前端集成
Files:
-
Modify:
pages/auth/login.vue(在现有登录表单中添加 GitHub 登录按钮) -
Step 1: 添加 GitHub 登录按钮到登录页
在现有登录表单的分割线区域添加:
<div class="divider">
<span>或</span>
</div>
<button @click="loginWithGithub" class="btn-github">
<Icon name="github" /> GitHub 登录
</button>
function loginWithGithub() {
window.location.href = '/api/auth/oauth/github/authorize';
}
onMounted(() => {
const url = new URL(window.location.href);
if (url.searchParams.get('oauth_success') === '1') {
refreshSession();
router.push('/');
}
if (url.searchParams.get('oauth_error')) {
const error = url.searchParams.get('oauth_error');
console.error('OAuth error:', error);
}
});
- Step 2: Commit
git add pages/auth/login.vue
git commit -m "feat(oauth): 集成 GitHub 登录按钮到登录页"
Task 9: 配置文档
Files:
-
Create:
docs/superpowers/oauth2/FLOW.md -
Modify:
docs/superpowers/oauth2/README.md -
Step 1: 创建 FLOW.md
补充流程图详解文档,包含:
-
新用户 OAuth 登录完整时序图
-
已注册用户绑定流程时序图
-
state 生命周期管理说明
-
错误处理流程
-
Step 2: Commit
git add docs/superpowers/oauth2/
git commit -m "docs(oauth): 补充流程图文档"
自检清单
完成实现后,对照设计文档检查:
- 6 个 API 接口全部实现
- GitHub Provider 配置正确
- State 存储 + 5min TTL 过期清理
- oauth_accounts 表创建成功
- 新用户自动注册 + 创建 session
- 已登录用户绑定第三方账号
- 解绑、绑定列表、绑定状态接口
- 前端登录页集成 GitHub 按钮
- 错误处理覆盖所有错误码
- 文档齐全(README、API、PROVIDERS、FLOW)
执行选项
Plan complete and saved to docs/superpowers/plans/2026-05-26-oauth2-implementation-plan.md. Two execution options:
1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration
2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints
Which approach?