@animocabrands/minds-connect
v0.1.2
Published
Browser OAuth (PKCE) and React bindings for Minds Connect
Keywords
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)
- Wrap the router with
MindsConnectso/and/callbackshare one client.optsare read once on mount — remount (hard-reload) to change them; Vite HMR will not rebuild the client. - Put
LoginCallbackon the same path asredirectUri. - Start login with
signIn(). After the user returns, call the Builder API throughoauth.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
scopeswhen 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:
OAuthRedirectErroron 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>—nullif there is no session
const accessToken = await oauth.getAccessToken();tokens
- Returns:
TokenSuccess | null— current tokens in the store.expiresInis seconds remaining, not the original lifetime.scopeis a space-delimited string. UsehasScopesto 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 andtokens.scopecoversscopes.hasScopes([])istruewhen 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.
