npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@animocabrands/minds-connect

v0.1.2

Published

Browser OAuth (PKCE) and React bindings for Minds Connect

Readme

@animocabrands/minds-connect

Browser library for HelloMinds partner login (OAuth 2.1 authorization code + PKCE).

It sends the user to HelloMinds, returns tokens to your app, refreshes them, and clears them on partner logout. Consent UI lives on HelloMinds; this package does not call consent APIs.

Builder API calls go through oauth.client (from @animocabrands/minds-client-lib). Connect auto-refreshes the access token before each API call. It does not pass a Builder API key.

This package is browser-only (window, sessionStorage, localStorage). React bindings are "use client" — wrap them in a client boundary on Next.js App Router.

Install

npm install @animocabrands/minds-connect

| Import | Use when | | ------ | -------- | | @animocabrands/minds-connect | Vanilla JS / any framework | | @animocabrands/minds-connect/react | React (MindsConnect provider). Needs react / react-dom ≥ 18 (optional peer). |

Register an OAuth client in the Build console (redirect_uri and origin must match exactly). client_id is public.

Default Auth API: https://api.oauth.hellominds.ai. Override with authUrl for staging or local.

Stay on one host (localhost vs 127.0.0.1 are different origins). Complete the redirect in the same tab — PKCE lives in sessionStorage.

Quickstart (React)

  1. Wrap the router with MindsConnect so / and /callback share one client. opts are read once on mount — remount (hard-reload) to change them; Vite HMR will not rebuild the client.
  2. Put LoginCallback on the same path as redirectUri.
  3. Start login with signIn(). After the user returns, call the Builder API through oauth.client.
import { BrowserRouter, Route, Routes, useNavigate } from "react-router-dom";
import {
  LoginCallback,
  MindsConnect,
  MindsScope,
  useMindsConnect,
  type MindsOAuthOptions,
} from "@animocabrands/minds-connect/react";

const opts: MindsOAuthOptions = {
  clientId: "YOUR_CLIENT_ID",
  redirectUri: `${window.location.origin}/callback`,
  scopes: [MindsScope.MindsList],
};

export function Root() {
  return (
    <BrowserRouter>
      <MindsConnect opts={opts}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/callback" element={<Callback />} />
        </Routes>
      </MindsConnect>
    </BrowserRouter>
  );
}

function Home() {
  const { isInitialized, isAuthenticated, signIn, signOut, oauth } =
    useMindsConnect();

  if (!isInitialized || !oauth) return <p>Loading…</p>;
  if (!isAuthenticated) {
    return (
      <button type="button" onClick={() => signIn()}>
        Connect to HelloMinds
      </button>
    );
  }

  async function listMinds() {
    try {
      const minds = await oauth.client.listMinds();
      console.log(minds);
    } catch (err) {
      console.error(err);
    }
  }

  return (
    <>
      <button type="button" onClick={() => listMinds()}>
        List minds
      </button>
      <button type="button" onClick={() => signOut()}>
        Log out
      </button>
    </>
  );
}

function Callback() {
  const navigate = useNavigate();
  return (
    <LoginCallback
      onSuccess={() => navigate("/", { replace: true })}
      onError={(err) => console.error(err)}
    />
  );
}

Omit LoginCallback children to use the built-in status copy. If you pass children, they always render — use onSuccess / onError, or leave the page after onSuccess.

useMindsConnect() also exposes: hasScopes, tokens, getAccessToken, oauth. Incremental consent is signIn({ scopes }) after hasScopes is false.

Quickstart (Vanilla)

Two pages, one shared opts module. The callback page is a new load — oauth.ts runs again with the same clientId / redirectUri / authUrl. This needs a bundler (Vite, webpack) or native ESM.

Wire #connect, #list, and #logout on the home page. Load the callback module on the exact redirectUri path you registered.

onTokensChanged runs when Connect writes the session in this tab: signOut(), a refresh, or a failed refresh that clears the store (including inside oauth.client).

// oauth.ts
import { MindsOAuth, MindsScope, type MindsOAuthOptions } from "@animocabrands/minds-connect";

export const opts: MindsOAuthOptions = {
  clientId: "YOUR_CLIENT_ID",
  redirectUri: `${window.location.origin}/callback`,
  scopes: [MindsScope.MindsList],
};

export const oauth = new MindsOAuth(opts);
// main.ts — home page
import { oauth } from "./oauth";

const connectBtn = document.getElementById("connect");
const listBtn = document.getElementById("list");
const logoutBtn = document.getElementById("logout");

oauth.onTokensChanged((tokens) => {
  const signedIn = Boolean(tokens);
  if (connectBtn) connectBtn.hidden = signedIn;
  if (listBtn) listBtn.hidden = !signedIn;
  if (logoutBtn) logoutBtn.hidden = !signedIn;
});

connectBtn?.addEventListener("click", async () => {
  try {
    await oauth.signIn();
  } catch (err) {
    console.error(err);
  }
});

listBtn?.addEventListener("click", async () => {
  try {
    const minds = await oauth.client.listMinds();
    console.log(minds);
  } catch (err) {
    console.error(err);
  }
});

logoutBtn?.addEventListener("click", async () => {
  try {
    await oauth.signOut();
  } catch (err) {
    console.error(err);
  }
});
// callback.ts — same path as redirectUri
import { OAuthRedirectError } from "@animocabrands/minds-connect";
import { oauth } from "./oauth";

try {
  await oauth.handleRedirect();
  window.location.replace("/");
} catch (err) {
  if (err instanceof OAuthRedirectError) {
    console.error(err.error, err.errorDescription);
  } else {
    console.error(err);
  }
}

Options (MindsOAuth / MindsConnect opts)

Same object for new MindsOAuth(opts) and <MindsConnect opts={opts}>.

| Option | Required | Type | Default | Description | | ------ | -------- | ---- | ------- | ----------- | | clientId | yes | string | — | Public OAuth client id from the Build console. | | redirectUri | yes | string | — | Exact redirect URI registered for this client. Must match the callback route (including origin). | | scopes | yes | MindsScope[] (non-empty) | — | Default scopes for signIn(). Use MindsScope constants — the values are the OAuth wire ids. Joined to the OAuth wire scope param (space-delimited). Pass the full desired set, not only a delta. Auth rejects partner login with no scope. For coverage / incremental consent, use hasScopes / signIn({ scopes }) with that list. See Scopes. | | authUrl | no | string | https://api.oauth.hellominds.ai | Auth API base URL. | | storage | no | "localStorage" | TokenStore | "localStorage" | Where to persist the session after login / refresh. See Token storage. |

Methods

signIn(options?)

Starts the OAuth redirect (PKCE) to HelloMinds login / consent.

Params (all optional):

| Param | Required | Type | Description | | ----- | -------- | ---- | ----------- | | state | no | string | object | Opaque value Connect JSON-encodes on the authorize URL and returns from handleRedirect. Defaults to {}. | | scopes | no | MindsScope[] (non-empty) | Full desired scope set. Defaults to opts.scopes. Pass this for incremental consent. |

  • Returns: Promise<void> (navigates away)
  • Throws: construction / PKCE failures; empty scopes when provided
await oauth.signIn();

Incremental consent is the same call with a full scope list:

import { MindsScope } from "@animocabrands/minds-connect";

const scopes = [
  MindsScope.MindsList,
  MindsScope.ConversationsList,
  MindsScope.MessagingSend,
];

if (oauth.tokens && !oauth.hasScopes(scopes)) {
  await oauth.signIn({ scopes });
}

In React, gate with the hook (isAuthenticated, hasScopes, signIn) using the same list. Baseline “covers opts.scopes” is hasScopes(opts.scopes).

handleRedirect()

Completes the OAuth redirect on your callback page. Validates state, exchanges the code, persists tokens.

Connect reads the code and state query params Auth put on this page.

PKCE verifier is in sessionStorage (this tab only). A new tab, popup, or bookmarked callback URL fails the exchange.

  • Returns: Promise<{ tokens: TokenSuccess, state }>
  • Throws: OAuthRedirectError on denial, mismatch, or exchange failure
import { OAuthRedirectError } from "@animocabrands/minds-connect";

try {
  const { tokens, state } = await oauth.handleRedirect();
} catch (err) {
  if (err instanceof OAuthRedirectError) {
    // err.error — e.g. access_denied, state_mismatch, token_exchange_failed
    // err.errorDescription — human-readable detail
    // err.state — echoed app state when present
  }
}

signOut()

Partner logout: best-effort revoke of the refresh token, then clear the local store. Does not sign the user out of HelloMinds.

  • Returns: Promise<void>
await oauth.signOut();

getAccessToken()

Return a usable access token, refreshing when the access token is within 60 seconds of expiry. null if there is no session. Clears the store if refresh fails.

oauth.tokens is the sync store snapshot (the access token may be expired).

  • Returns: Promise<string | null> — null if there is no session
const accessToken = await oauth.getAccessToken();

tokens

  • Returns: TokenSuccess | null — current tokens in the store. expiresIn is seconds remaining, not the original lifetime. scope is a space-delimited string. Use hasScopes to check coverage. The access token may already be expired.

hasScopes(scopes)

| Param | Required | Type | Description | | ----- | -------- | ---- | ----------- | | scopes | yes | MindsScope[] | Full set to compare against the stored token. |

  • Returns: boolean — session exists and tokens.scope covers scopes. hasScopes([]) is true when a session exists.

onTokensChanged(listener)

Same-tab session changes (login, refresh, logout). Returns an unsubscribe function. Does not run on a new page load or when getAccessToken returns an already-usable session — read oauth.tokens to paint the page.

oauth.onTokensChanged((tokens) => {
  // update the signed-in UI
});

Property: oauth.client

MindsClient for Builder API calls. Auto-refreshes before each authed request. Method list: @animocabrands/minds-client-lib.

const minds = await oauth.client.listMinds();

React: LoginCallback

Place on the callback route, inside MindsConnect. Runs handleRedirect once on mount.

| Prop | Required | Type | Description | | ---- | -------- | ---- | ----------- | | onSuccess | no | (result: { tokens, state }) => void | After a successful exchange. Navigate home here. | | onError | no | (error: unknown) => void | OAuthRedirectError or unexpected throw. | | children | no | ReactNode | If set, always rendered (built-in status is skipped). |

Scopes

MindsScope is exported from both entry points. Put the full desired set on opts.scopes. Pass a full set to signIn({ scopes }) when expanding access.

| MindsScope | Scope | Description | | ------------ | ----- | ----------- | | MindsScope.Email | email | See the user's email | | MindsScope.MindsList | minds:list | See the user's minds | | MindsScope.MindsStatus | minds:status | See whether the user's minds are on | | MindsScope.MindsCognition | minds:cognition | See the user's minds' cognition | | MindsScope.MindsSkillsList | minds:skills:list | See the user's minds' skills | | MindsScope.MindsToolsList | minds:tools:list | See the user's minds' tools | | MindsScope.MindsAppsList | minds:apps:list | See the user's minds' apps | | MindsScope.MindsEmail | minds:email | See the user's minds' email | | MindsScope.MindsWallets | minds:wallets | See the user's minds' wallet addresses | | MindsScope.MindsAwaken | minds:awaken | Awaken minds for the user | | MindsScope.MindsEnable | minds:enable | Turn the user's minds on | | MindsScope.MindsDisable | minds:disable | Turn the user's minds off | | MindsScope.MindsSkillsEquip | minds:skills:equip | Equip the user's minds' skills | | MindsScope.MindsSkillsUnequip | minds:skills:unequip | Unequip the user's minds' skills | | MindsScope.MindsToolsEquip | minds:tools:equip | Equip the user's minds' tools | | MindsScope.MindsToolsUnequip | minds:tools:unequip | Unequip the user's minds' tools | | MindsScope.MindsAppsEquip | minds:apps:equip | Equip the user's minds' apps | | MindsScope.MindsAppsUnequip | minds:apps:unequip | Unequip the user's minds' apps | | MindsScope.ConversationsCreate | conversations:create | Start chats with the user's minds | | MindsScope.ConversationsList | conversations:list | See the user's chats | | MindsScope.ConversationsRead | conversations:read | See a chat with the user's minds | | MindsScope.MessagingSend | messaging:send | Send messages as the user | | MindsScope.MessagingBeacon | messaging:beacon | Nudge the user's minds | | MindsScope.MessagingHistory | messaging:history | See the user's chat history | | MindsScope.MessagingActivityStream | messaging:activity:stream | Watch activity for a user's mind | | MindsScope.MessagingStream | messaging:stream | Watch messaging events across the user's minds |

The OAuth client’s allowed scopes are configured in the Build console. Requesting a scope that is not allowed fails at Auth.

Tokens

TokenSuccess (from oauth.tokens, onTokensChanged, handleRedirect):

| Field | Type | Description | | ----- | ---- | ----------- | | accessToken | string | Bearer for Builder API calls. | | refreshToken | string | Rotated on refresh; store always keeps the latest. | | expiresIn | number | Seconds remaining when read from the store. | | scope | string | Space-delimited granted scopes. Use hasScopes to check coverage. |

Token storage

Omit storage to use localStorage, key minds_oauth_session:${clientId}. It survives reload and other tabs on this origin. Any script on the page can read it (XSS).

For cookies, a BFF, or native, extend TokenStore and implement get / set / clear the same way. The store holds an OAuthSession (expiresAt is epoch ms), not TokenSuccess (expiresIn is seconds remaining). When the backend is a JSON string, use this.parseSession. Return null from get() if the data is missing or corrupt. The cookie example uses js-cookie (npm install js-cookie); Connect does not depend on it.

import Cookies from "js-cookie";
import { MindsOAuth, TokenStore } from "@animocabrands/minds-connect";

class CookieTokenStore extends TokenStore {
  get() {
    const serialized = Cookies.get(this.sessionKey);
    if (!serialized) return null;
    return this.parseSession(serialized);
  }

  set(session) {
    Cookies.set(this.sessionKey, JSON.stringify(session));
  }

  clear() {
    Cookies.remove(this.sessionKey);
  }
}

new MindsOAuth({
  ...opts,
  storage: new CookieTokenStore(opts.clientId),
});

signOut() clears this store. It does not clear HelloMinds Auth cookies.

Backend Builder calls

Keep login/refresh in the browser. On your server, pass the access token:

import { createMindsClient } from "@animocabrands/minds-client-lib";

const accessToken = req.headers.authorization?.replace(/^Bearer\s+/i, "");
const minds = await createMindsClient({ accessToken }).listMinds();

See the client library README for the full method list. The Builder host is fixed in that package.

License

UNLICENSED. Published on npm as @animocabrands/minds-connect.