Skip to content

Sui wallet connect + zkLogin sign-in to replace the Ed25519 delegate-key signup wall #17

Open

Description

@harrymove-ctrl

Context

There is no real signup today: the account surrogate is pasting a 64-char hex Ed25519 MemWal delegate seed via Auth1 → importDelegate (apps/web/src/main.tsx:1043), stored in localStorage. The README says "no wallet connect," and apps/web/package.json has no Sui/wallet deps (only three); the root has @mysten/sui@^2.9.1. All /app/* content is hard-gated behind hasMemWalDelegate (renderShell, main.tsx:1510), so a first-timer cannot proceed without a raw private key.

Goal / user story

As a new user, I sign in with my Sui wallet (or keyless zkLogin via Google) instead of pasting a private key, my Sui address becomes my identity, and the app unlocks — with the delegate-paste flow demoted to an advanced "bring-your-own MemWal" option.

Acceptance criteria

  • A "Connect wallet" entry point (built on @mysten/dapp-kit ConnectButton) lists installed Sui wallets and connects.
  • On connect, the wallet address becomes the hosted identity (replaces the 'anonymous' owner_id default) and unlocks /app/* without a pasted delegate.
  • A zkLogin / Google keyless option is available for users without a wallet extension.
  • The Worker verifies a signed nonce (useSignPersonalMessage) before issuing a session token — address ownership is proven, not asserted.
  • The Ed25519 delegate import (Auth1/importDelegate) is moved behind an "Advanced / BYO MemWal" affordance, not the default gate.
  • Disconnect clears the session and returns the user to the public/landing state.

Implementation notes

  • Add to apps/web/package.json: @mysten/dapp-kit, @mysten/sui (root is already ^2.9.1), @tanstack/react-query; wrap the app in SuiClientProvider + WalletProvider + QueryClientProvider. Use useCurrentAccount, useSignPersonalMessage.
  • Backend: add POST /api/auth/wallet in apps/api/src/worker.ts — issue a nonce, verify the signed message, map address → owner_id, return a session token in the existing me/session shape (mirror importDelegate's response handling at main.tsx:1100).
  • zkLogin: use Enoki (@mysten/enoki EnokiFlow) + Google OAuth; needs an ENOKI_API_KEY worker secret. Enoki sponsorship means no SUI balance is required.
  • A wallet/zkLogin user still needs a MemWal delegate to remember/recall; decide whether the Worker mints/holds a managed delegate keyed to the address, or keeps BYO for now (this is the open account-model decision). Keep BYO working as a fallback this milestone.
  • Gotcha: dapp-kit adds bundle weight — lazy-load the wallet provider on /app routes so the landing/share pages don't ship it.

Sui Overflow angle

This is the headline Sui-native onboarding for the hackathon: real Sui wallet + keyless zkLogin sign-in, with the Sui address as on-chain identity. It also sets up address-owned namespaces and attribution receipts (the address becomes the owner/producer in the Move registry/receipt). Judges expect wallet connect; today's private-key paste is a non-starter for a Sui demo.

Dependencies

Account-model decision (delegate-as-identity vs real accounts + managed delegate). Pairs with the first-run onboarding issue. Downstream on-chain registry/receipt work uses this address as owner.

Part of the ContextMEM roadmap (#4) • Sui Overflow build.

Activity

  1. added this to the Sui Overflow milestone on Jun 16, 2026
  2. added
    P0Demo-blocking: required for a working Sui Overflow demo
    featureUser- or agent-facing capability
    suiSui chain: tx signing, objects, wallet, zkLogin, explorer
    onboardingUser and developer onboarding / first-run experience
    on Jun 16, 2026
  3. harrymove-ctrl commented on Jun 19, 2026

    @harrymove-ctrl
    OwnerAuthor

    Implementation Plan

    Approach
    Add a real Sui identity layer in front of the existing delegate flow. Wrap the React app in @mysten/dapp-kit providers, register Enoki zkLogin wallets so Google keyless and browser-extension wallets share one ConnectButton. On connect, the client signs a server-issued nonce with useSignPersonalMessage; a new POST /api/auth/wallet in the Worker verifies the signature with @mysten/sui/verify, derives the Sui address, and returns a stateless HMAC session token that the Worker accepts on subsequent requests as the owner identity (owner_id = <0x address>). The hard hasMemWalDelegate gate (main.tsx:1510) is split into an identity gate (wallet/zkLogin OR delegate unlocks /app/*) and a delegate-only action gate (remember/recall to Walrus Memory stays BYO this milestone). The Ed25519 delegate-paste form is demoted to an "Advanced / BYO MemWal" disclosure. Key design decision: introduce a Worker session token (HMAC, no new D1 table/KV) because hosted auth today is header-only and has no session concept — this is the new, load-bearing piece.

    Prerequisites & dependencies

    • Open roadmap decision Namespace→Space/Bucket/Seal mapping + Harbor service-key management in Worker secrets #7 (account model): this issue implements "wallet/zkLogin identity + keep BYO managed delegate as fallback." Confirm we are NOT minting a managed server-side delegate this milestone (the issue says keep BYO working; managed-delegate is the paired onboarding issue). Remember/recall therefore still require a delegate.
    • Network decision (roadmap docs: add product roadmap (Harbor storage + Talus on-chain direction) #4): pick the Sui network for sign-in. Recommend testnet to align with the Harbor/on-chain alpha and the Sui Overflow demo; the dapp-kit SuiClientProvider network and the Worker's verification SuiClient must match. zkLogin signature verification is epoch/JWK-sensitive, so client and server must point at the same network.
    • New deps in apps/web/package.json: @mysten/dapp-kit, @mysten/sui (pin to root ^2.9.1), @mysten/enoki, @tanstack/react-query. (vite.config.ts:23-28 already has manualChunks for @mysten/ and @tanstack/ — chunking is pre-wired.)
    • Worker secrets/vars (add to WorkerEnv at worker.ts:32-55 + apps/api/cloudflare/wrangler.jsonc):
      • CONTEXTMEM_SESSION_SECRET (secret) — HMAC key for nonce + session tokens.
      • SUI_NETWORK (var, e.g. "testnet") and optional SUI_RPC_URL for the verification client.
      • ENOKI_API_KEY (Enoki public key is used client-side; if a server-side Enoki call is added later it's a secret — not needed for this milestone).
    • Client build vars (Vite VITE_*): VITE_ENOKI_API_KEY, VITE_GOOGLE_CLIENT_ID, VITE_SUI_NETWORK. Google OAuth client ID must be created in Google Cloud console with the app origin + redirect URL allow-listed; the Enoki dashboard must register that Google client.
    • No Vectorize/Harbor/Sui-wallet-with-balance needed (Enoki sponsors zkLogin; sign-in is gas-free).

    Step-by-step

    1. Add deps + a lazy provider wrapper (client). Add the four deps to apps/web/package.json (bun install). Create apps/web/src/lib/sui-providers.tsx exporting a SuiAppProviders component that composes QueryClientProvider → SuiClientProvider (networks via createNetworkConfig, default from VITE_SUI_NETWORK) → WalletProvider autoConnect. In a module-init effect call registerEnokiWallets({ apiKey: VITE_ENOKI_API_KEY, providers: { google: { clientId: VITE_GOOGLE_CLIENT_ID, redirectUrl: location.origin } }, client: suiClient, network }) so the Google zkLogin wallet appears in ConnectButton alongside extensions (satisfies the zkLogin AC with the same sign flow). Import @mysten/dapp-kit/dist/index.css.

    2. Lazy-load providers only on /app/* (bundle hygiene). In main.tsx, keep App() (main.tsx:815-821) as BrowserRouter only. Wrap the /app/* routes — not landing/share/showcase — in <React.lazy>-loaded SuiAppProviders. Concretely, factor the /app/* <Route>s (main.tsx:1548-1603) into an AppRoutes element rendered inside a Suspense + lazy SuiAppProviders. Landing (/), /share/:shareId, /showcase stay outside so they never ship the Sui/dapp-kit chunk (addresses the implementation-note gotcha).

    3. Build the Worker auth endpoints. In worker.ts route dispatcher (add next to main.tsx's /api/me at worker.ts:592 and the /api/memwal/* routes ~worker.ts:637):

      • GET /api/auth/nonce → returns { nonce, expiresAt }. Implement stateless: nonce = base64url(randomId|exp) + "." + base64url(HMAC-SHA256(CONTEXTMEM_SESSION_SECRET, randomId|exp)), TTL ~2 min. Uses WebCrypto crypto.subtle (available in Worker, no nodejs_compat needed).
      • POST /api/auth/wallet → body { address, signature, nonce }. Validate the nonce HMAC + expiry; reconstruct the exact signed message string (e.g. ContextMEM sign-in\naddress: <addr>\nnonce: <nonce>), then await verifyPersonalMessageSignature(new TextEncoder().encode(message), signature, { address, client }) from @mysten/sui/verify. Compare recoveredPublicKey.toSuiAddress() (or the {address} assertion the verifier enforces) to the claimed address. On success issue a session token (next step) and return { token, me } mirroring importDelegate's response handling at main.tsx:1100-1106 ("token" in result).
      • For zkLogin signatures, verifyPersonalMessageSignature needs the network client (epoch/JWK) — pass the same SuiClient/network as the client used. Flag in code that network must match.
    4. Stateless session token + Worker identity resolution. Add helpers in worker.ts near readHostedRunAuth (worker.ts:1461):

      • issueWalletSession(address) → HMAC token base64url({sub:address, owner:address, iat, exp(~7d)}) + "." + HMAC.
      • Refactor readHostedRunAuth (worker.ts:1461-1475) into a unified resolveHostedAuth(request, env) that returns { ownerId, accountId, kind: 'wallet'|'delegate', authorization? }. Source order: (a) Authorization: Bearer <session-token> verified by HMAC → { ownerId: address, accountId: address, kind:'wallet' }; (b) existing x-memwal-account-id + x-memwal-authorization delegate headers → { ownerId: hostedOwnerId(accountId), kind:'delegate', authorization }. hostedOwnerId stays for delegate users (worker.ts:1512); wallet owner_id is the raw 0x address (the future on-chain owner).
      • Update getHostedMe (worker.ts:1192-1207) and hostedMe (worker.ts:1490-1510) to report provider: 'wallet' | 'zklogin' | 'unknown', ownerAddress: address, and a new hasDelegateKey that is true only for delegate sessions (wallet sessions get hasDelegateKey:false, authenticated:true).
    5. Relax write paths that only need an owner. createHostedRun (worker.ts:1229-1230), listHostedRuns (worker.ts:1209-1210), namespace CRUD, and run-read use resolveHostedAuth and accept a wallet session for owner_id (so building into an address-owned private namespace works without a delegate). Keep a stricter requireDelegate(request) guard ONLY on the MemWal remember/recall sync path (the x-memwal-authorization-dependent calls), returning the existing HOSTED_DELEGATE_REQUIRED error with copy pointing to the Advanced/BYO affordance. This satisfies "unlocks /app/* without a pasted delegate" while keeping remember BYO.

    6. Client: connect → nonce → sign → session. In main.tsx add async function connectWallet() near importDelegate (main.tsx:1043): read useCurrentAccount() address, GET /api/auth/nonce, build the message, signPersonalMessage({ message: bytes }) via useSignPersonalMessage, POST /api/auth/wallet, then on {token} do safeLocalSet("contextmem.session", token) + setSessionToken(token) + setMe(result.me) (reuse the exact pattern at main.tsx:1102-1106). Because dapp-kit hooks must run inside the providers, place the connect UI inside the lazily-wrapped /app subtree (or a small WalletConnectGate component rendered there).

    7. Send the session token to the Worker. Today hosted requests use hostedDelegateHeaders() (main.tsx:7971-7979) and the local path uses authHeaders(token). Make hosted requests send Authorization: Bearer <sessionToken> when a wallet session exists, falling back to delegate headers. Update loadMe (main.tsx:969-993): in hosted mode, if sessionToken is set, call GET /api/me with the Bearer token (currently hosted loadMe short-circuits to localStorage at main.tsx:970-977 — add a token branch first); else fall back to readHostedDelegate()/hostedBrowserMe.

    8. Split the gate + demote the delegate form (client). Replace hasMemWalDelegate (main.tsx:864) usage at the unlock points with a new hasIdentity = me.authenticated (wallet/zkLogin OR delegate) for: renderShell gate (main.tsx:1510), AppShell/landing locked styling, /app/settings access. Keep a separate hasMemWalDelegate strictly for remember/recall action buttons and the startRun MemWal preconditions (main.tsx:1153-1157) where a delegate is genuinely required — or relax startRun to allow build-only and gate just the remember step. In LockedPreview/HostedCredentialGate (main.tsx:4576, 4522) and SettingsAppPage (main.tsx:1584), make ConnectButton + "Sign in with Google (zkLogin)" the primary affordance and move the existing Auth1/delegate paste fields (delegateAccountId/delegateKey/onImport) behind a collapsed <details> "Advanced — bring your own MemWal delegate".

    9. Disconnect clears session. Extend logout() (main.tsx:995-1004) to also call dapp-kit useDisconnectWallet().mutate() and clear contextmem.session; update sessionSlot (main.tsx:1433-1438) to show the connected address and a Disconnect action returning to the public/landing state.

    New / changed files

    • New: apps/web/src/lib/sui-providers.tsx (providers + Enoki registration).
    • New (optional): apps/web/src/components/WalletConnectGate.tsx (ConnectButton + zkLogin + nonce-sign flow).
    • Edit: apps/web/package.json (4 deps).
    • Edit: apps/web/src/main.tsx (lazy provider wrap on /app/*; connectWallet; loadMe/logout/sessionSlot; gate split hasIdentity vs hasMemWalDelegate; demote delegate form in LockedPreview/HostedCredentialGate/SettingsAppPage).
    • Edit: apps/api/src/worker.ts (WorkerEnv fields; GET /api/auth/nonce; POST /api/auth/wallet; issueWalletSession/resolveHostedAuth refactor of readHostedRunAuth; getHostedMe/hostedMe provider fields; relax owner-only write paths + requireDelegate for remember).
    • Edit: apps/api/cloudflare/wrangler.jsonc (+ wrangler.example.jsonc): SUI_NETWORK var; document CONTEXTMEM_SESSION_SECRET secret.
    • Edit: .env.example (VITE_ENOKI_API_KEY, VITE_GOOGLE_CLIENT_ID, VITE_SUI_NETWORK, CONTEXTMEM_SESSION_SECRET).

    Test plan

    • Unit (Worker, bun:test under apps/api): nonce issue/verify round-trip (valid, expired, tampered-HMAC); session token issue→resolveHostedAuth round-trip; POST /api/auth/wallet rejects a signature for a different address. Mock verifyPersonalMessageSignature for the happy path and add one real-signature fixture (sign with Ed25519Keypair from @mysten/sui, verify end-to-end) — this is the assert that ownership is proven.
    • Integration/manual (testnet, the proof it works):
      1. Connect a real Sui wallet (e.g. Slush) → ConnectButton lists it → sign nonce → /app/* unlocks with no delegate; GET /api/me returns authenticated:true, provider:'wallet', ownerAddress = your 0x....
      2. Google zkLogin via Enoki → same unlock, gas-free, address derived (record the address as evidence).
      3. Build a private hosted run → confirm owner_id in D1 equals the wallet address (run a wrangler d1 execute ... "SELECT owner_id FROM contextmem_extraction_jobs ORDER BY created_at DESC LIMIT 1").
      4. Disconnect → session cleared, back to public/landing, /app/* re-locked.
      5. Confirm landing/share bundle does NOT include the sui/query chunks (inspect vite build output / network tab).
    • Negative: remember/recall with wallet-only session returns HOSTED_DELEGATE_REQUIRED and the Advanced/BYO form still works (paste delegate → remember succeeds).

    Risks & gotchas

    • No server session today — the HMAC session token is net-new and load-bearing; get the verify path right or all hosted auth breaks. Stateless nonce is not truly single-use (replayable within its 2-min TTL); acceptable for alpha, but offer a D1 contextmem_auth_nonces table as the hardened follow-up. Document this in SECURITY tradeoffs.
    • zkLogin verification is network/epoch-sensitive — client SuiClientProvider network, Enoki-registered network, and the Worker verification SuiClient must all match (SUI_NETWORK). A mainnet/testnet split silently fails verification. zkLogin sigs may need the current epoch JWKs via the client arg of verifyPersonalMessageSignature.
    • Worker CPU/bundle: @mysten/sui/verify (BLS/zk) runs under nodejs_compat (already set, wrangler.jsonc:17) but watch the compressed bundle cap; if zkLogin verify is too heavy, fall back to verifying via the network RPC. (Unrelated to the separate @mysten/seal Worker risk in issue Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5.)
    • dapp-kit hooks must be inside providers — useCurrentAccount/useSignPersonalMessage will throw if the connect UI renders outside the lazily-wrapped /app subtree; keep connect UI inside it.
    • owner_id scheme change — wallet users get owner_id = 0xaddress (no hosted:/demo: prefix). Existing hosted:/demo: rows are untouched (no migration), but a delegate user and a wallet user are distinct identities even for the same person this milestone (acceptable; managed-delegate linking is the follow-up issue).
    • CSS/SSR: dapp-kit ships its own CSS and assumes window; fine for this SPA (no SSR) but the import must be client-only (it is, inside the lazy chunk).

    Acceptance-criteria mapping

    • "Connect wallet entry point with dapp-kit ConnectButton" → Steps 1, 8.
    • "Wallet address becomes hosted identity (replaces 'anonymous') and unlocks /app/* without a delegate" → Steps 4, 5, 7, 8.
    • "zkLogin / Google keyless option" → Step 1 (registerEnokiWallets), surfaced in Step 8.
    • "Worker verifies a signed nonce before issuing a session token (ownership proven)" → Steps 3, 4, 6.
    • "Delegate import moved behind Advanced / BYO MemWal" → Step 8.
    • "Disconnect clears session and returns to public/landing" → Step 9.

    Effort
    L — ~4-6 person-days. ~1.5d Worker (nonce/session/verify + auth-source refactor across owner-gated paths), ~2.5d web (providers + lazy split + gate refactor across a 8.2k-line main.tsx + form demotion), ~1d Enoki/Google OAuth + testnet wiring and end-to-end testing.


    Implementation plan generated for execution (planning phase). Part of the roadmap (#4).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Demo-blocking: required for a working Sui Overflow demofeatureUser- or agent-facing capabilityonboardingUser and developer onboarding / first-run experiencesuiSui chain: tx signing, objects, wallet, zkLogin, explorer

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions