Skip to content

Extract apps/ai/src/auth.rs into crates/api-auth#3707

Merged
yujonglee merged 2 commits intomainfrom
devin/1770470323-extract-api-auth
Feb 7, 2026
Merged

Extract apps/ai/src/auth.rs into crates/api-auth#3707
yujonglee merged 2 commits intomainfrom
devin/1770470323-extract-api-auth

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Feb 7, 2026

Summary

Extracts the core auth middleware from apps/ai/src/auth.rs into a new standalone crate crates/api-auth. The new crate is focused purely on JWT validation and entitlement checking:

  • AuthState (takes a configurable required_entitlement instead of hardcoding "hyprnote_pro")
  • AuthError with axum IntoResponse
  • require_auth middleware (validates JWT, checks entitlement, inserts Claims into request extensions)

The crate depends only on hypr-supabase-auth + axum — no sentry, no analytics, no device fingerprint logic.

apps/ai/src/auth.rs becomes a thin adapter that layers a second middleware (sentry_and_analytics) which:

  • Reads Claims from request extensions (set by require_auth)
  • Reads the x-device-fingerprint header directly from request headers
  • Sets up sentry user context
  • Inserts hypr_analytics::DeviceFingerprint and hypr_analytics::AuthenticatedUserId into extensions for downstream handlers

Also adds Clone derive to Claims and SubscriptionStatus in supabase-auth (required by http::Extensions::insert).

Updates since last revision

  • Removed DeviceFingerprint/UserId wrapper types from api-auth — the crate now only inserts Claims into extensions. Device fingerprint extraction is an app-level concern and stays in apps/ai/src/auth.rs.
  • Removed DEVICE_FINGERPRINT_HEADER constant from api-auth — defined locally where needed (apps/ai/src/auth.rs and apps/ai/src/main.rs).
  • Removed Cargo.lock stub packages (tauri-plugin-cli2, tauri-plugin-db, tauri-plugin-export) that were artifacts from the dev machine.

Review & Testing Checklist for Human

  • Middleware ordering correctness: The monolithic require_pro is now split into two route_layer calls. Verify that request flow is require_authsentry_and_analytics → handler (last route_layer is outermost in axum). This is the highest-risk behavioral change.
  • AuthenticatedUserId insertion is now conditional: sentry_and_analytics only inserts AuthenticatedUserId if Claims is found in extensions. Since require_auth runs first and rejects unauthenticated requests, this should always be present — but confirm no edge case where sentry_and_analytics runs without require_auth having succeeded.
  • Adding Clone to Claims/SubscriptionStatus: Minor cross-cutting change to supabase-auth. Confirm no downstream issues with these types now being Clone.

Recommended test plan: Deploy to staging and verify that:

  1. Protected routes still require valid JWT with hyprnote_pro entitlement
  2. Sentry captures user context (user ID, email, entitlements) on requests
  3. Analytics receives DeviceFingerprint and AuthenticatedUserId as before

Notes

  • 6 unit tests added for error status code mapping
  • No integration tests for the middleware itself (token validation flow)

Link to Devin run: https://app.devin.ai/sessions/ddba5085a8b84ca3a404085c58cec62a
Requested by: @yujonglee


Open with Devin

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@netlify
Copy link

netlify bot commented Feb 7, 2026

Deploy Preview for hyprnote canceled.

Name Link
🔨 Latest commit 98a7768
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/69874468d65b950008e6f8ea

@netlify
Copy link

netlify bot commented Feb 7, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit 98a7768
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/6987446843e2bb0008dc64f1

@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@devin-ai-integration
Copy link
Contributor Author

Review

1. Correctness

Middleware ordering is correct. In axum, the last route_layer call is outermost, so request flow is require_auth -> sentry_and_analytics -> handler. require_auth inserts Claims/DeviceFingerprint/UserId into extensions, and sentry_and_analytics reads them. If require_auth rejects, sentry_and_analytics never runs. This matches the original single-middleware behavior.

The extension type bridging works. require_auth inserts hypr_api_auth::{Claims, DeviceFingerprint, UserId}, then sentry_and_analytics converts those to hypr_analytics::{DeviceFingerprint, AuthenticatedUserId} which is what the downstream handlers (llm-proxy, transcribe-proxy) extract via their AnalyticsContext extractors. No regression.

One cleanup needed: Cargo.lock has 3 unrelated stub packages (tauri-plugin-cli2, tauri-plugin-db, tauri-plugin-export) that are artifacts from the dev machine. These should be removed before merge.

Adding Clone to Claims/SubscriptionStatus is required by http::Extensions::insert in http 1.x. No downstream issues since Clone is strictly additive.

No functional regression overall - the auth logic, error codes, sentry context, and analytics extension insertion are all preserved.

2. API design - can we do better?

The current api-auth crate does 3 things: (a) validate JWT + check entitlement, (b) extract the device fingerprint header, (c) wrap everything in new DeviceFingerprint/UserId types.

Problem is (b) and (c) create unnecessary indirection. The crate introduces its own DeviceFingerprint/UserId wrapper types, but no downstream consumer ever reads those directly - they only exist so sentry_and_analytics can immediately unwrap them and re-wrap into hypr_analytics types. That's a roundabout path.

Suggested simpler API: api-auth should only do (a) - validate JWT, check entitlement, insert Claims into extensions. Device fingerprint extraction is an app-level concern (it has nothing to do with auth). Remove DeviceFingerprint/UserId from the crate entirely.

Then sentry_and_analytics in apps/ai would:

  1. Read Claims from extensions (set by require_auth)
  2. Read the fingerprint header directly from request headers (just like the original code did)
  3. Insert hypr_analytics::DeviceFingerprint and hypr_analytics::AuthenticatedUserId into extensions

This makes api-auth a focused, reusable auth crate with a single responsibility, and keeps the sentry/analytics/fingerprint wiring where it belongs (in the app layer).

Concretely, crates/api-auth/src/lib.rs would shrink to just:

pub use hypr_supabase_auth::Claims;

pub const DEVICE_FINGERPRINT_HEADER: &str = "x-device-fingerprint";

#[derive(Clone)]
pub struct AuthState {
    inner: SupabaseAuth,
    required_entitlement: String,
}

pub async fn require_auth(
    State(state): State<AuthState>,
    mut request: Request,
    next: Next,
) -> Result<Response, AuthError> {
    // validate JWT, check entitlement, insert Claims
    let claims = /* ... */;
    request.extensions_mut().insert(claims);
    Ok(next.run(request).await)
}

And DEVICE_FINGERPRINT_HEADER could even stay in the app or move to a shared constants crate - it's not really an auth concept.

…s; remove Cargo.lock stubs

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@devin-ai-integration
Copy link
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

5 similar comments
@devin-ai-integration
Copy link
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

@devin-ai-integration
Copy link
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

@devin-ai-integration
Copy link
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

@devin-ai-integration
Copy link
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

@devin-ai-integration
Copy link
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

@yujonglee yujonglee merged commit a184701 into main Feb 7, 2026
21 checks passed
@yujonglee yujonglee deleted the devin/1770470323-extract-api-auth branch February 7, 2026 14:03
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