Skip to content

fix: unify bookmark data sources and repair OAuth token management#5

Merged
autism-ip merged 9 commits into
masterfrom
fix/bookmarks-auth
Jun 2, 2026
Merged

fix: unify bookmark data sources and repair OAuth token management#5
autism-ip merged 9 commits into
masterfrom
fix/bookmarks-auth

Conversation

@autism-ip

Copy link
Copy Markdown
Owner

Summary

  • 统一数据源: [slug]/page.jsraindrop.js(静态token)切换到 raindrop-with-auth.js(OAuth),消除分裂的数据获取路径
  • DRY 重构: 提取 getTokenManager()src/lib/auth/get-token-manager.js,消除6处重复的选择逻辑(~88行)
  • Crypto 现代化: createCipher/createDeciphercreateCipheriv/createDecipheriv,修复废弃 API
  • Cron 增强: token 刷新失败时返回结构化 { success, needs_reauth } 响应,区分 refresh_token 失效与临时错误
  • Admin 保护: /admin/raindrop-setup 添加 ADMIN_SECRET Basic Auth 验证(未设置时向后兼容)

Root Cause

OAuth token 自2026年1月29日过期后,每日 cron 刷新因 refresh_token 同样失效而持续失败。[slug]/page.js 使用旧的静态 token 路径进一步加剧了问题。

Test plan

  • npm run build 通过
  • npm run lint 通过
  • 部署后访问 /bookmarks 正常显示
  • 部署后访问 /bookmarks/[slug] 正常显示书签详情
  • 通过 /admin/raindrop-setup 重新完成 OAuth 授权
  • 验证 cron /api/cron/refresh-tokens 返回结构化响应

autism-ip added 3 commits June 2, 2026 11:51
Comprehensive fix plan for expired OAuth tokens, data source split
in bookmark pages, and architecture improvements to token management.
7 tasks covering centralized token manager, crypto API fix,
data source unification, DRY refactoring, cron enhancement,
admin protection, and build verification.
- Create centralized getTokenManager module (DRY: 6 files → 1)
- Switch [slug]/page.js from raindrop.js to raindrop-with-auth.js
- Fix deprecated crypto.createCipher → createCipheriv
- Enhance cron error handling with needs_reauth classification
- Add ADMIN_SECRET Basic Auth protection to admin page
- Add L2 CLAUDE.md for auth module

Resolves: bookmarks not displaying, expired OAuth tokens,
duplicated token manager selection logic.
@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
zen-dev-blog Ready Ready Preview, Comment Jun 2, 2026 8:45am

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef49dcb5f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/auth/crypto.js Outdated
const encrypted = encryptedData.slice((IV_LENGTH + TAG_LENGTH) * 2)

const decipher = crypto.createDecipher(ALGORITHM, key, iv)
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve legacy ciphertext during crypto migration

When this deploys over existing KV/Supabase/env tokens written by the old createCipher path, decrypt() now tries to authenticate them with createDecipheriv using the prepended IV, but the old API derived its own IV/key from the password and did not use that stored IV. Those existing encrypted access_token, refresh_token, and RAINDROP_ENCRYPTED_REFRESH_TOKEN values will fail GCM authentication, so token status, cron refresh, and bookmark fetches break until a full reauth/manual env replacement is done; the task also states the output format should stay compatible, so decrypt needs a fallback/migration path for the legacy format.

Useful? React with 👍 / 👎.

Comment thread src/app/admin/raindrop-setup/page.js Outdated
Comment on lines +56 to +57
if (!verifyBasicAuth(authorization)) {
return unauthorizedResponse()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle admin authentication outside the page render

When ADMIN_SECRET is set and the request is unauthenticated, this page returns a Response object from a React Server Component. App Router pages render React nodes rather than route-handler responses, so the unauthenticated path will render an invalid object/throw instead of sending the intended 401 with WWW-Authenticate; put this check in middleware/a route handler or use a supported page-level flow.

Useful? React with 👍 / 👎.

P1: Add legacy createCipher fallback in decrypt() for existing
    encrypted tokens written by the old API.
P2: Replace Response return in Server Component with React node
    rendering for unauthorized state.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1477fc5ee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/auth/crypto.js Outdated
try {
const decipher = crypto.createDecipher(ALGORITHM, key)
let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
decrypted += decipher.final('utf8')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the legacy GCM auth tag

When existing tokens were encrypted by the old createCipher path, the stored value is still iv + tag + ciphertext because encrypt() prepended the random IV and GCM tag before this commit. This fallback now passes that whole blob as ciphertext and never calls setAuthTag() with the stored tag, so legacy access_token, refresh_token, and RAINDROP_ENCRYPTED_REFRESH_TOKEN values still fail authentication instead of decrypting; fresh evidence is that the new fallback at this line uses encryptedData directly rather than the parsed tag/ciphertext.

Useful? React with 👍 / 👎.

Comment on lines +31 to +35
export default async function RaindropSetupPage() {
const adminSecret = process.env.ADMIN_SECRET

// ADMIN_SECRET 未设置 → 开发环境向后兼容,直接放行
if (adminSecret) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Protect the OAuth token endpoints too

When ADMIN_SECRET is set, this guard only restricts rendering /admin/raindrop-setup; I checked the linked API routes (/api/auth/raindrop, /callback, /clear, and /status) and none of them reads ADMIN_SECRET or otherwise authenticates the caller. An unauthenticated internet user can still start the OAuth flow and replace the stored Raindrop tokens, or POST to /api/auth/raindrop/clear to delete them, so the new admin protection does not actually protect the token-management operations it exposes.

Useful? React with 👍 / 👎.

P1: Fix legacy crypto fallback to use only ciphertext portion
    (skip stored IV/tag which were meaningless under old API).
P2: Add ADMIN_SECRET auth guard to OAuth API endpoints
    (initiate, status, clear) via shared requireAdminAuth().

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2f1ab42e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +40 to +41
if (!authorization || !authorization.startsWith('Basic ')) {
return <UnauthorizedPage />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Issue a Basic challenge before rendering unauthorized

When ADMIN_SECRET is set and a normal browser visits /admin/raindrop-setup without cached credentials, this renders the unauthorized React page instead of sending an HTTP 401 with a WWW-Authenticate challenge. Because the browser never gets a Basic-auth challenge, it has no way to prompt for or cache the password, so the admin setup flow remains unreachable unless the caller manually injects an Authorization header; put this guard in middleware/a route handler or otherwise send the challenge before rendering.

Useful? React with 👍 / 👎.

Add middleware-level 401 + WWW-Authenticate: Basic for /admin routes
so browsers prompt native credential dialog instead of showing an
inert unauthorized page.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@autism-ip

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c4be7945d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/auth/crypto.js Outdated
Comment on lines +57 to +59
const decipher = crypto.createDecipher(ALGORITHM, key)
let decrypted = decipher.update(legacyCiphertext, 'hex', 'utf8')
decrypted += decipher.final('utf8')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the GCM tag in legacy decryption

For deployments that already have tokens encrypted before this change, the first createDecipheriv attempt fails because the legacy createCipher ignored the stored IV, and this fallback then tries to decrypt the legacy GCM ciphertext without calling setAuthTag(tag). AES-GCM still requires the saved auth tag, so final() throws and decrypt() returns null, making existing access/refresh tokens unusable until manual reauth.

Useful? React with 👍 / 👎.

Comment thread src/lib/auth/require-admin-auth.js Outdated
Comment on lines +21 to +22
if (!auth || !auth.startsWith('Basic ')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Challenge the API auth endpoints too

When ADMIN_SECRET is set, the setup UI authenticates on /admin/raindrop-setup but then fetches and links to /api/auth/raindrop/...; Basic auth credentials challenged for /admin/ are not reliably sent to the separate /api/ scope, and this 401 response does not include a WWW-Authenticate challenge. As a result, a valid admin can load the page but the status fetch, clear action, and OAuth start endpoint can all get stuck returning JSON 401 instead of prompting/succeeding.

Useful? React with 👍 / 👎.

P1: Remove legacy crypto fallback — old createCipher tokens cannot
    be decrypted; expired tokens will trigger reauth naturally.
P2: Move API endpoint protection to middleware with same Basic Auth
    realm so browser reuses cached credentials across /admin and /api.
    Remove redundant requireAdminAuth() guards from API routes.
    Delete unused require-admin-auth.js.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@autism-ip

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@autism-ip

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc93ff910a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/auth/crypto.js Outdated

const decipher = crypto.createDecipher(ALGORITHM, key, iv)
// --- 解密: createCipheriv (显式 IV + GCM auth tag) ---
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve decryption of existing tokens

When this deploys with tokens already stored by the previous encrypt() implementation, decrypt() now reads the random IV prefix and passes it to createDecipheriv, but the old createCipher/createDecipher path used password-based derivation and did not actually use that prefixed IV. As a result, every existing KV/Supabase/env access_token or refresh_token encrypted before this commit fails GCM authentication, so bookmark fetching and scheduled refreshes break until the owner manually reauthorizes; add a legacy-decrypt fallback/migration or explicitly clear and reauth those tokens.

Useful? React with 👍 / 👎.

Comment thread src/middleware.js
}
},
'/admin/:path*',
'/api/auth/raindrop/:path((?!callback$).*)'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Protect the OAuth start route too

With ADMIN_SECRET set, this matcher still does not run for the bare /api/auth/raindrop endpoint (I checked the current Next matcher behavior: it matches /api/auth/raindrop/status but not /api/auth/raindrop). That leaves the OAuth start URL unauthenticated, so an outside user can get redirected with this app's client id and complete the unprotected callback to overwrite the stored Raindrop tokens with their own account; include the base route in the matcher while still excluding only the callback.

Useful? React with 👍 / 👎.

getBookmarks() can return null on non-auth errors. The [slug] page
called .find() directly on the result, crashing Server Components
render. Add null/empty checks before array operations.
decipher.final() throws GCM auth tag mismatch for tokens encrypted
by the old createCipher API. Return null on failure so callers
gracefully degrade to reauth flow instead of crashing.
@autism-ip
autism-ip merged commit 0bfee7a into master Jun 2, 2026
4 checks passed
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