Skip to content

feat: Steward auth integration — Phase 1 (parallel with Privy)#446

Merged
lalalune merged 7 commits intodevfrom
feat/steward-auth-phase1
Apr 12, 2026
Merged

feat: Steward auth integration — Phase 1 (parallel with Privy)#446
lalalune merged 7 commits intodevfrom
feat/steward-auth-phase1

Conversation

@0xSolace
Copy link
Copy Markdown
Collaborator

Summary

Add Steward as an alternative auth provider alongside Privy. Feature-flagged, non-breaking. Existing Privy auth untouched.

Type

  • Feature
  • Infra / ops

Changes

New files

  • packages/lib/providers/StewardProvider.tsx — React provider wrapping @stwd/react, auto-syncs JWT via CustomEvent
  • packages/lib/auth/steward-client.ts — JWT verification with 3-layer cache (in-memory LRU → Redis → local verify), mirrors privy-client.ts
  • packages/lib/steward-sync.ts — User sync from Steward JWT to eliza-cloud user+org, mirrors privy-sync.ts (645 lines)
  • packages/scripts/provision-waifu-tenant.ts — One-time script to create waifu.fun steward tenant

Modified files

  • app/layout.tsx — MaybeStewardProvider wrapper (gated on NEXT_PUBLIC_STEWARD_AUTH_ENABLED)
  • app/login/page.tsx — Steward login section (passkey + email) above Privy options, feature-flagged
  • packages/lib/auth.ts — Bearer auth chain: Privy → Steward → API key fallback
  • packages/lib/cache/keys.ts — Added steward session cache keys + TTL
  • package.json — Added @stwd/react, @stwd/sdk, @simplewebauthn/browser

How it works

  1. Set NEXT_PUBLIC_STEWARD_AUTH_ENABLED=true to show Steward login
  2. Users sign in via passkey or email magic link
  3. Steward JWT verified server-side (cached, same pattern as Privy)
  4. User auto-created/linked in eliza-cloud DB via steward-sync
  5. Privy users unaffected — both auth systems coexist

Env vars needed

NEXT_PUBLIC_STEWARD_AUTH_ENABLED=true
NEXT_PUBLIC_STEWARD_API_URL=http://steward:3200
STEWARD_SESSION_SECRET=<must match steward service>

Breaking Changes

  • None — additive only, feature-flagged

Co-authored-by: wakesync shadow@shad0w.xyz

0xSolace and others added 7 commits April 12, 2026 04:41
- Import StewardLogin and StewardProvider from @stwd/react
- Feature-flagged via NEXT_PUBLIC_STEWARD_AUTH_ENABLED env var
- Shows passkey + email magic link when enabled
- Self-contained: StewardProvider wraps only the login widget
- Existing Privy auth flow completely unchanged when flag is off
- On success, redirects to dashboard (same as Privy flow)

Co-authored-by: wakesync <shadow@shad0w.xyz>
Mirrors privy-sync.ts pattern for Steward JWT-based authentication.

Flow:
1. Look up existing user by steward_user_id
2. Check pending invites by email
3. Link to existing account if email matches
4. Create new user + organization

Key differences from privy-sync:
- Uses steward_user_id column instead of privy_user_id
- JWT contains email/userId/walletAddress directly (no third-party API)
- No anonymous user upgrade path (Steward has no anonymous users)

Co-authored-by: wakesync <shadow@shad0w.xyz>
Mirrors privy-client.ts pattern for Steward-issued JWTs:
- steward-client.ts: JWT verify via jose + in-memory LRU + Redis cache
- auth.ts: Bearer JWT chain now tries Privy -> Steward -> API key
- cache/keys.ts: add session.steward cache key + TTL (5min, same as Privy)

Steward JIT sync is stubbed (TODO) pending steward-sync worker.
invalidateSessionCaches now clears both Privy + Steward caches.

Co-authored-by: wakesync <shadow@shad0w.xyz>
- Create packages/lib/providers/StewardProvider.tsx mirroring PrivyProvider pattern
- StewardProvider wraps @stwd/react StewardProvider with auth config
- AuthTokenSync dispatches 'steward-token-sync' events for non-React consumers
- Graceful fallback: renders children without provider if URL unconfigured
- Conditional activation via NEXT_PUBLIC_STEWARD_AUTH_ENABLED env var
- Both auth providers coexist, managing separate auth state
- Add @stwd/react@0.6.4, @stwd/sdk@0.7.2, @simplewebauthn/browser deps

Co-authored-by: wakesync <shadow@shad0w.xyz>
One-time script to create a steward tenant for waifu.fun.
Run after steward service is up.

Co-authored-by: wakesync <shadow@shad0w.xyz>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 12, 2026

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

Project Deployment Actions Updated (UTC)
eliza-cloud-v2 Error Error Apr 12, 2026 5:01am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 90821214-7c92-4a5c-87d8-decbe2c50c61

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/steward-auth-phase1

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

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

@claude
Copy link
Copy Markdown

claude bot commented Apr 12, 2026

Code Review — feat: Steward auth integration (Phase 1)

Overall the structure is solid — mirrors Privy cleanly, feature-flagged, non-breaking. A few issues to address before merging.


🔴 Critical

1. JIT sync is commented out but steward-sync.ts is in this very PR

packages/lib/auth.ts has:

// TODO: Import syncUserFromSteward once steward-sync module is created
// import { syncUserFromSteward } from "./steward-sync";

…and the JIT path is fully commented out. But packages/lib/steward-sync.ts is in this PR. As written, any Steward JWT that verifies successfully but has no pre-existing user row returns AuthenticationError("User not found"). New Steward signups will fail auth silently. The sync needs to be wired up or the PR is incomplete.

2. Account linking silently overwrites steward_user_id by email match (security)

In steward-sync.ts, step 3 (and again in the duplicate-key recovery in step 4):

if (existingByEmail && existingByEmail.steward_user_id !== stewardUserId) {
  // Linking Steward account...
  await usersService.update(existingByEmail.id, { steward_user_id: stewardUserId, ... });

This allows anyone who registers in Steward with email victim@example.com to be silently linked to the existing eliza-cloud account for that email, with no confirmation from the original user. If the two auth systems have different email-verification guarantees, this is an account-takeover vector. At minimum, require the incoming Steward JWT to have email_verified: true before allowing the link.


🟡 Medium

3. Double StewardProvider — login page creates a nested provider inside the layout's provider

app/layout.tsx already wraps children in StewardAuthProvider<StewardProvider client={...}>. Then app/login/page.tsx renders another <StewardProvider auth={{ baseUrl: ... }}> inline around <StewardLogin>. This creates two nested provider instances with potentially different configs. The login page should consume the context from the outer provider, not create its own.

4. Null dereference in slug uniqueness retry loop

// steward-sync.ts — inside the while(await organizationsService.getBySlug(orgSlug)) loop
orgSlug = email ? generateSlugFromEmail(email) : generateSlugFromWallet(walletAddress!);

If the code reaches this point via the name-only branch (no email, no wallet), walletAddress is undefined and walletAddress! will call generateSlugFromWallet(undefined) at runtime. Add a guard:

if (email) orgSlug = generateSlugFromEmail(email);
else if (walletAddress) orgSlug = generateSlugFromWallet(walletAddress);
else { orgSlug = `user-${stewardUserId.substring(0,8)}-${Date.now().toString(36)}`; }

🟠 Minor

5. Duplicate import from @stwd/react in login page

import { StewardLogin } from "@stwd/react";
import { StewardProvider } from "@stwd/react";

Merge into one import statement.

6. privyUserId forwarded in Steward signup Discord log

discordService.logUserSignup({
  ...
  privyUserId: userWithOrg.privy_user_id || "",  // always "" for Steward users

This will always be an empty string for Steward signups and may mislead whoever reads those logs. Consider either omitting the field or using a different key name for the Steward path.

7. invalidateSessionCaches now fans out to both providers unconditionally

Calling invalidateStewardTokenCache on every logout (including Privy-only sessions) adds a redundant Redis DEL call. Not a blocker, but consider passing the auth method as a hint so only the relevant cache is invalidated.


✅ Looks good

  • 3-layer cache (in-memory LRU → Redis → local verify) mirrors Privy pattern correctly
  • hashToken / never storing raw tokens in cache keys is the right approach
  • effectiveTtl = Math.min(CacheTTL.session.steward, tokenRemainingSeconds) correctly avoids caching near-expired tokens past their expiry
  • Lazy-init getJwtSecret() prevents import-time throws when env vars are absent
  • MaybeStewardProvider gating in layout is clean
  • Error recovery paths (recoverCanonicalStewardUser, rollback helpers) are well-structured
  • Feature flag discipline throughout — existing Privy flow is untouched

@lalalune lalalune merged commit 32e0489 into dev Apr 12, 2026
9 of 12 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.

2 participants