Skip to content

ENG-1783: Hosted Connect Walrus Memory flow for web apps#193

Open
harrymove-ctrl wants to merge 9 commits into
devfrom
feature/eng-1783-hosted-connect-memwal-flow-for-web-apps
Open

ENG-1783: Hosted Connect Walrus Memory flow for web apps#193
harrymove-ctrl wants to merge 9 commits into
devfrom
feature/eng-1783-hosted-connect-memwal-flow-for-web-apps

Conversation

@harrymove-ctrl
Copy link
Copy Markdown
Collaborator

@harrymove-ctrl harrymove-ctrl commented May 25, 2026

Summary

  • Add hosted /connect/app flow for third-party backend apps, separate from /connect/mcp.
  • Add env-backed app auth clients, dev-only localhost wildcard client, strict redirect/fallback validation, one-time app auth codes, and server-side token exchange.
  • Add a backend-served app-auth demo with localhost and deployed APP_BASE_URL modes.
  • Merged latest dev, including the user-facing rename to Walrus Memory. Existing code/env identifiers such as MEMWAL_* remain where the current codebase still uses them.

Linear

https://linear.app/commandoss/issue/ENG-1783/hosted-connect-memwal-flow-for-web-apps

How a Deployed dApp Uses This

For this low-lift V1 there is no self-serve dApp registration UI yet. A deployed dApp registers by sending the Walrus Memory team/operator its exact HTTPS callback and fallback URLs. The operator adds one client entry to APP_AUTH_CLIENTS_JSON in Railway/env.

Example client config:

{
  "client_id": "my_dapp",
  "client_secret_sha256": "<sha256-secret>",
  "display_name": "My Dapp",
  "allowed_redirect_uris": [
    "https://my-dapp.com/api/memwal/callback"
  ],
  "fallback_uri": "https://my-dapp.com/memwal/error",
  "allowed_fallback_uris": [
    "https://my-dapp.com/memwal/error"
  ]
}

Generate the secret hash:

printf '%s' 'my_real_secret' | shasum -a 256

The raw client_secret stays only in the dApp backend env. It is never embedded in the frontend.

dApp Frontend Embed

The dApp frontend only needs a button/link to its own backend route:

export function ConnectWalrusMemoryButton() {
  return (
    <a href="/connect/memwal">
      Connect Walrus Memory
    </a>
  );
}

dApp Backend Redirect Route

The dApp backend creates state, stores it in an HTTP-only cookie/session, then redirects the user to Walrus Memory:

app.get("/connect/memwal", (req, res) => {
  const state = crypto.randomBytes(24).toString("base64url");

  res.cookie("memwal_state", state, {
    httpOnly: true,
    sameSite: "lax",
    secure: process.env.NODE_ENV === "production",
  });

  const callback = `${process.env.APP_BASE_URL}/api/memwal/callback`;
  const fallback = `${process.env.APP_BASE_URL}/memwal/error`;

  const url = new URL("/connect/app", process.env.MEMWAL_WEB_URL);
  url.searchParams.set("client_id", process.env.MEMWAL_CLIENT_ID);
  url.searchParams.set("redirect_uri", callback);
  url.searchParams.set("state", state);
  url.searchParams.set("label", "My Dapp");
  url.searchParams.set("intent", "sdk_delegate");
  url.searchParams.set("fallback_uri", fallback);

  res.redirect(url.toString());
});

dApp Backend Callback + Token Exchange

Walrus Memory redirects the browser back with only code and state. The dApp backend validates state and exchanges the one-time code server-side with HTTP Basic auth:

app.get("/api/memwal/callback", async (req, res) => {
  if (req.query.state !== req.cookies.memwal_state) {
    return res.status(400).send("Invalid state");
  }

  const basic = Buffer.from(
    `${process.env.MEMWAL_CLIENT_ID}:${process.env.MEMWAL_CLIENT_SECRET}`
  ).toString("base64");

  const tokenRes = await fetch(`${process.env.MEMWAL_API_URL}/api/app-auth/token`, {
    method: "POST",
    headers: {
      "Authorization": `Basic ${basic}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      grant_type: "authorization_code",
      code: req.query.code,
      redirect_uri: `${process.env.APP_BASE_URL}/api/memwal/callback`,
      state: req.query.state,
    }),
  });

  const data = await tokenRes.json();

  // Save account_id / owner_address / delegate.ref in the dApp backend DB/session.
  res.json(data);
});

dApp Deployed Env

APP_BASE_URL=https://my-dapp.com
MEMWAL_WEB_URL=https://dev.memwal.ai
MEMWAL_API_URL=https://api-dev.memwal.ai
MEMWAL_CLIENT_ID=my_dapp
MEMWAL_CLIENT_SECRET=my_real_secret

Security Notes

  • Frontend only embeds a Connect Walrus Memory button/link. It never sees client_secret.
  • Browser callback contains only code and state on success, or error and state on failure.
  • client_secret, token exchange, account data, and delegate.ref stay server-side.
  • Deployed redirect/fallback URLs must be exact HTTPS allowlist matches.
  • Google Console only needs Walrus Memory origins/callbacks, not every third-party dApp callback, because Google auth happens on Walrus Memory.

Test plan

  • cargo test --bins
  • pnpm --filter @memwal/app build
  • pnpm --filter @memwal/app-auth-demo build
  • Curl smoke check for deployed APP_BASE_URL=https://demo.example.com callback/fallback generation
  • Browser smoke check for demo UI at http://localhost:3000 showing local mode/callback/fallback config

@harrymove-ctrl harrymove-ctrl changed the title ENG-1783: Hosted Connect MemWal flow for web apps ENG-1783: Hosted Connect Walrus Memory flow for web apps May 26, 2026
@railway-app railway-app Bot temporarily deployed to MemWal / dev May 26, 2026 10:13 Inactive
@railway-app railway-app Bot temporarily deployed to MemWal / dev May 26, 2026 14:51 Inactive
@railway-app railway-app Bot temporarily deployed to MemWal / dev May 26, 2026 15:46 Inactive
@railway-app railway-app Bot temporarily deployed to MemWal / dev May 26, 2026 15:55 Inactive
@railway-app railway-app Bot temporarily deployed to MemWal / dev May 27, 2026 02:57 Inactive
@harrymove-ctrl
Copy link
Copy Markdown
Collaborator Author

Security review update pushed in f3655df.

What changed:

  • Added canonical self-serve DCR endpoint: POST /api/app-auth/clients; kept /api/app-auth/register as a backward-compatible alias for existing demos.
  • No email flow: registered clients are active immediately, unless an operator blocks them.
  • Added active | blocked client status in Postgres and filtered token/start lookup to active clients only.
  • Added operator kill switch: POST /api/admin/app-auth/clients/{client_id}/block with Authorization: Bearer $APP_AUTH_ADMIN_TOKEN or x-admin-token.
  • Added tighter public registration abuse guard: 5 registrations/hour/IP, Redis-backed with in-memory fallback.
  • Public DCR now rejects localhost, memwal.ai, *.memwal.ai, and display names that use reserved Walrus Memory/MemWal branding.
  • Demo app now registers through /api/app-auth/clients and defaults/sanitizes its label to Demo App so it does not trip the reserved-brand guard.

Dapp registration shape after deploy:

curl -X POST "https://relayer.dev.memwal.ai/api/app-auth/clients" \
  -H 'content-type: application/json' \
  --data '{
    "display_name": "Demo App",
    "redirect_uris": ["https://app-auth-demo-dev.up.railway.app/api/memwal/callback"],
    "fallback_uris": ["https://app-auth-demo-dev.up.railway.app/memwal/error"]
  }'

Use the returned client_id and one-time client_secret only in the dapp backend env, then redirect users to /connect/app and exchange the callback code + state server-side at /api/app-auth/token.

Demo link: https://app-auth-demo-dev.up.railway.app/

Test plan:

  • cargo test --manifest-path services/server/Cargo.toml app_auth --bins
  • cargo test --manifest-path services/server/Cargo.toml rate_limit --bins
  • cargo test --manifest-path services/server/Cargo.toml --bins
  • pnpm --filter @memwal/app-auth-demo build
  • git diff --check
  • docker build -f apps/app-auth-demo/Dockerfile -t memwal-app-auth-demo-pr193 .

@ducnmm
Copy link
Copy Markdown
Collaborator

ducnmm commented May 27, 2026

Hi @harrymove-ctrl
Do you really think UX like this will be put on production?
A simple case is that anyone can put that link, then, edit, then delete?

Another point is that it seems that you have not used the case with 20 delegate keys

@railway-app railway-app Bot temporarily deployed to MemWal / dev May 27, 2026 03:24 Inactive
@harrymove-ctrl
Copy link
Copy Markdown
Collaborator Author

Updated in 3a90763 to address this review directly.

You are right: the previous wording made the hosted registration look like production self-serve UX. That is not the right production model because without developer identity / app ownership, public create/edit/delete would be unsafe.

Current model after this update:

  1. End users do not create app clients

    • End users only click Connect Walrus Memory from a dapp.
    • They never see client_secret and never configure APP_AUTH_CLIENTS_JSON.
    • client_id belongs to the dapp backend, not the end user.
  2. Staging/dev scale

    • APP_AUTH_PUBLIC_CLIENT_REGISTRATION_ENABLED=true can be set on a dev/staging relayer.
    • Demo apps can call POST /api/app-auth/clients and receive client_id + one-time client_secret automatically.
    • This is only for hosted demo validation and third-party integration testing.
  3. Production scale

    • Public registration is disabled by default.
    • POST /api/app-auth/clients now requires Authorization: Bearer $APP_AUTH_ADMIN_TOKEN unless the staging flag above is explicitly enabled.
    • Operators create credentials for approved dapp developers and send them the client_id + one-time client_secret for backend env usage.
    • No public edit/delete endpoint is exposed in this PR. We should not add public edit/delete until there is a real developer portal / ownership model / domain verification / secret rotation UX.
    • Bad clients can still be killed with the existing admin block endpoint: POST /api/admin/app-auth/clients/{client_id}/block.
  4. How dapps get credentials now

    • Staging/demo: dapp backend calls /api/app-auth/clients directly when the staging flag is enabled.
    • Production: Walrus Memory operator calls the same endpoint with admin auth, then gives the returned client_id and one-time client_secret to the dapp developer. The dapp stores those in backend env and uses them for /api/app-auth/token exchange.
  5. 20 delegate key case

    • The on-chain account module caps delegate keys at 20.
    • ConnectApp now handles the same max-key abort case as ConnectMcp: if add_delegate_key returns abort code 2, the UI shows a friendly message telling the user the account already has 20 delegate keys and they need to revoke an unused key before retrying.
    • I also added a friendly owner-mismatch mapping for abort code 0 in the same add-delegate path.

Validation:

  • cargo test --manifest-path services/server/Cargo.toml app_auth --bins
  • cargo test --manifest-path services/server/Cargo.toml --bins
  • pnpm --filter @mysten-incubation/memwal build
  • pnpm --filter @memwal/app build
  • pnpm --filter @memwal/app-auth-demo build
  • git diff --check
  • docker build -f apps/app-auth-demo/Dockerfile -t memwal-app-auth-demo-pr193 .

@railway-app railway-app Bot temporarily deployed to MemWal / dev May 27, 2026 03:58 Inactive
@railway-app railway-app Bot temporarily deployed to MemWal / dev May 27, 2026 10:02 Inactive
@railway-app railway-app Bot temporarily deployed to Walrus Memory / dev June 8, 2026 02:55 Inactive
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.

3 participants