Skip to content

feat: auth-only mode for identity-only connect flow#63

Open
volod-vana wants to merge 1 commit intomainfrom
feat/auth-only-mode
Open

feat: auth-only mode for identity-only connect flow#63
volod-vana wants to merge 1 commit intomainfrom
feat/auth-only-mode

Conversation

@volod-vana
Copy link
Copy Markdown
Member

Summary

  • Adds mode: "auth" option to ConnectConfig that authenticates users and returns their wallet address without requiring DataConnect or the full data grant flow
  • After Privy auth on the Account Portal, the session is claimed and approved directly on the Session Relay, bypassing the DataConnect deep link entirely
  • Zero changes to Session Relay — reuses existing /claim and /approve endpoints with a synthetic identity.auth scope

Changes

SDK (src/):

  • ConnectConfig.mode?: "auth" | "connect" — new optional field
  • ConnectConfig.scopes — now optional (defaults to ["identity.auth"] for auth mode)
  • connect() passes mode and relayUrl to the Account Portal via connectUrl params

Account Portal (connect/):

  • handoff-contract.ts — threads mode + relayUrl through the handoff context (survives auth redirects)
  • use-connect-page.ts — auth-only flow: after Privy auth + wallet ready, calls /claim then /approve directly, skips master key signing
  • page.tsx — shows "Connected" confirmation instead of DataConnect deep link for auth-only mode

Usage

const session = await connect({
  privateKey: process.env.VANA_PRIVATE_KEY as `0x${string}`,
  mode: "auth",
});
// Redirect user to session.connectUrl
// User signs in via Privy → Account Portal auto-approves
const result = await pollUntilComplete(session.sessionId);
// result.grant.userAddress = "0x..."

Backward compatibility

  • Existing connect({ scopes: [...] }) flow is completely untouched
  • All 69 SDK tests pass
  • All 20 Account Portal tests pass
  • No Session Relay changes

Test plan

  • Verify existing connect flow still works (scopes + DataConnect)
  • Test auth-only flow: mode: "auth" → Privy login → address returned
  • Verify handoff context survives auth redirects with mode + relayUrl
  • Test error handling (relay down, claim fails, approve fails)

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 18, 2026

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

Project Deployment Actions Updated (UTC)
connect Ready Ready Preview Mar 18, 2026 11:47pm

Request Review

Adds `mode: "auth"` option to ConnectConfig that lets builders
authenticate users (get their wallet address) without requiring
DataConnect or the full data grant flow. After Privy auth, the
Account Portal claims and approves the session directly on the
relay, returning the user's embedded wallet address to the SDK.

Zero changes to Session Relay — uses existing claim/approve
endpoints with a synthetic `identity.auth` scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@tnunamak tnunamak left a comment

Choose a reason for hiding this comment

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

Review via Claude Code's next-prompt — an agent that picks the most valuable next action from connected personal data.


Overall this is a clean, well-scoped addition. The SDK-side changes follow existing patterns well and the portal changes are reasonable. A few issues worth addressing before merge:

Security / correctness concerns

1. relayUrl in the connect URL leaks the Session Relay origin to the browser

connect() puts relayUrl into the connectUrl query string, and the Account Portal then uses it to make direct fetch() calls from the browser to the Session Relay. This means:

  • The Session Relay URL is exposed in the browser's address bar and network tab (minor — it's likely already discoverable).
  • More importantly, the portal makes unauthenticated POST /v1/session/claim and POST /v1/session/${sessionId}/approve calls directly from the browser. The normal connect flow has the desktop app (DataConnect) call these endpoints. Are these relay endpoints designed to accept browser CORS requests? If not, this will fail in production. If they are, is there any rate-limiting or origin validation on them? A malicious page that intercepts the sessionId + secret could claim and approve the session, impersonating the user.

The secret is the only proof-of-possession here. Since auth-only mode skips master key signing, the security model relies entirely on the secret remaining confidential. This is probably acceptable for the use case but worth documenting.

Suggestion: Consider using the portal's own backend (e.g., an API route) to proxy the claim/approve calls instead of having the browser call the relay directly. This would keep the relay URL server-side and allow signed requests (which the existing createRequestSigner pattern supports). Alternatively, document why direct browser calls are acceptable here.

2. mode on ConnectHandoffContext is typed string | null but should be a union

In handoff-contract.ts, mode is string | null. The SDK types already constrain it to "auth" | "connect". The portal side should at least validate the value before acting on it — right now any string value for mode would trigger isAuthOnly = handoffContext?.mode === "auth", which is fine defensively, but the type should be "auth" | "connect" | null for self-documentation and to match the SDK.

API design

3. scopes becoming optional on ConnectConfig is a breaking behavioral change for connect mode

Making scopes optional is fine for auth mode, but for mode: "connect" (the default), calling connect({ privateKey }) without scopes will now silently send an empty scopes array to the relay instead of getting a TypeScript error. This could create confusing sessions that request no data.

Suggestion: Either validate at runtime that scopes is non-empty when mode !== "auth" (throw a ConnectError with CONFIG_INVALID), or use a discriminated union type:

export type ConnectConfig =
  | { mode?: "connect"; privateKey: `0x${string}`; scopes: string[]; /* ...rest */ }
  | { mode: "auth"; privateKey: `0x${string}`; scopes?: string[]; /* ...rest */ };

This would preserve the existing compile-time safety for connect mode while making scopes optional only for auth mode.

4. The synthetic grantId: "auth-only" may break downstream consumers

The approve call sends grantId: "auth-only". When the SDK consumer polls and receives the grant via useVanaConnect or pollUntilComplete, isValidGrant() will validate it (it checks grantId.length > 0, which passes). But if a consumer then passes this grant to getData(), it will attempt to fetch data with grantId: "auth-only", which will presumably fail at the Personal Server with an opaque error.

Suggestion: Document clearly in the TSDoc for mode: "auth" that the returned grant's grantId is synthetic and should NOT be passed to getData(). Even better, consider adding a grant.mode field or a helper like isAuthOnlyGrant(grant) so consumers can programmatically distinguish.

Testing gaps

5. No SDK-side tests for auth mode

The connect.test.ts file has no tests for:

  • connect({ privateKey, mode: "auth" }) — verifying that mode and relayUrl appear in the connectUrl
  • connect({ privateKey, mode: "auth" }) — verifying that scopes default to ["identity.auth"]
  • connect({ privateKey, mode: "connect" }) — verifying backward compatibility with explicit mode

These are straightforward to add given the existing test patterns.

6. No portal-side tests for the auth-only effect

The handoff-contract.test.ts test file adds mode: null and relayUrl: null to the createContext helper (good), but there are no tests for:

  • parseFromSearchParams with mode=auth&relayUrl=https://relay.example.com in the query string
  • Verifying mode and relayUrl survive serialization/deserialization round-trips
  • Verifying createHandoffQueryParams includes mode and relayUrl when present

Minor

  • The "Connected" success UI in connect-page-client.tsx uses a raw HTML entity (&#10003;) for the checkmark. Consider using the existing design system components/icons if available for consistency.
  • The isAuthOnly variable is derived in use-connect-page.ts but isAuthOnly is not part of ConnectPageView or documented in the hook's return type — it's just a boolean. Adding a brief TSDoc comment on the return value would help.

Summary

The core design — reusing existing relay endpoints with a synthetic scope to avoid DataConnect — is pragmatic and avoids unnecessary backend changes. The main risks are around the browser making direct relay calls (CORS / security surface), the lack of compile-time safety for scopes in connect mode, and missing test coverage for the new code paths. I'd want items 1, 3, and 5 addressed before merging.

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