@hfunlabs/hypurr-connect 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hfunlabs/hypurr-connect",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@10.10.0",
6
6
  "main": "./dist/index.js",
@@ -55,14 +55,16 @@ import type {
55
55
  /** @internal context value — extends the public type with fields used only by library internals */
56
56
  interface InternalConnectState extends HypurrConnectState {
57
57
  loginTelegram: () => void;
58
+ /** Connected EOA owner address (EOA auth only); null otherwise. */
59
+ eoaAddress: `0x${string}` | null;
58
60
  }
59
61
 
60
62
  const TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-jwt";
61
63
  const LEGACY_TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-user";
62
64
  const TELEGRAM_AUTH_STATE_KEY = "hypurr-connect-auth-state";
63
- const TELEGRAM_AUTH_CODE_VERIFIER_PREFIX =
64
- "hypurr-connect-auth-code-verifier:";
65
- const TELEGRAM_AUTH_RETURN_TO_PREFIX = "hypurr-connect-auth-return-to:";
65
+ const TELEGRAM_AUTH_CODE_VERIFIER_PREFIX = "hypurr-connect-auth-code-verifier:";
66
+ const TELEGRAM_AUTH_REDIRECT_URI_PREFIX =
67
+ "hypurr-connect-auth-redirect-uri:";
66
68
  const TELEGRAM_AUTH_MESSAGE = "hypurr-connect:telegram-auth";
67
69
  const DEFAULT_AUTH_HUB_URL = "https://auth.hypurr.fun/login";
68
70
  const DEFAULT_MEDIA_URL = "https://media.hypurr.fun";
@@ -239,7 +241,7 @@ function withExpectedFrom(
239
241
  return transaction.from ? transaction : { ...transaction, from: address };
240
242
  }
241
243
 
242
- function currentReturnTo(): string {
244
+ function currentRedirectUri(): string {
243
245
  const url = new URL(window.location.href);
244
246
  for (const param of [
245
247
  "code",
@@ -325,54 +327,52 @@ function codeVerifierStorageKey(state: string): string {
325
327
  return `${TELEGRAM_AUTH_CODE_VERIFIER_PREFIX}${state}`;
326
328
  }
327
329
 
328
- function returnToStorageKey(state: string): string {
329
- return `${TELEGRAM_AUTH_RETURN_TO_PREFIX}${state}`;
330
+ function redirectUriStorageKey(state: string): string {
331
+ return `${TELEGRAM_AUTH_REDIRECT_URI_PREFIX}${state}`;
330
332
  }
331
333
 
332
334
  function storeTelegramAuthSession(
333
335
  state: string,
334
336
  codeVerifier: string,
335
- returnTo: string,
337
+ redirectUri: string,
336
338
  ): void {
337
339
  const previousState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
338
340
  if (previousState) {
339
341
  sessionStorage.removeItem(codeVerifierStorageKey(previousState));
340
- sessionStorage.removeItem(returnToStorageKey(previousState));
342
+ sessionStorage.removeItem(redirectUriStorageKey(previousState));
341
343
  }
342
344
  sessionStorage.setItem(TELEGRAM_AUTH_STATE_KEY, state);
343
345
  sessionStorage.setItem(codeVerifierStorageKey(state), codeVerifier);
344
- sessionStorage.setItem(returnToStorageKey(state), returnTo);
346
+ sessionStorage.setItem(redirectUriStorageKey(state), redirectUri);
345
347
  }
346
348
 
347
349
  function clearTelegramAuthSession(state: string): void {
348
350
  sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
349
351
  sessionStorage.removeItem(codeVerifierStorageKey(state));
350
- sessionStorage.removeItem(returnToStorageKey(state));
352
+ sessionStorage.removeItem(redirectUriStorageKey(state));
351
353
  }
352
354
 
353
- function takeTelegramAuthSession(state: string):
354
- | {
355
- codeVerifier: string | null;
356
- returnTo: string | null;
357
- }
358
- | null {
355
+ function takeTelegramAuthSession(state: string): {
356
+ codeVerifier: string | null;
357
+ redirectUri: string | null;
358
+ } | null {
359
359
  const expectedState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
360
360
  sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
361
361
  if (!expectedState || state !== expectedState) {
362
362
  if (expectedState) {
363
363
  sessionStorage.removeItem(codeVerifierStorageKey(expectedState));
364
- sessionStorage.removeItem(returnToStorageKey(expectedState));
364
+ sessionStorage.removeItem(redirectUriStorageKey(expectedState));
365
365
  }
366
366
  return null;
367
367
  }
368
368
 
369
369
  const codeVerifierKey = codeVerifierStorageKey(state);
370
- const returnToKey = returnToStorageKey(state);
370
+ const redirectUriKey = redirectUriStorageKey(state);
371
371
  const codeVerifier = sessionStorage.getItem(codeVerifierKey);
372
- const returnTo = sessionStorage.getItem(returnToKey);
372
+ const redirectUri = sessionStorage.getItem(redirectUriKey);
373
373
  sessionStorage.removeItem(codeVerifierKey);
374
- sessionStorage.removeItem(returnToKey);
375
- return { codeVerifier, returnTo };
374
+ sessionStorage.removeItem(redirectUriKey);
375
+ return { codeVerifier, redirectUri };
376
376
  }
377
377
 
378
378
  function fallbackAuthTokenUrl(authHubUrl?: string): string {
@@ -455,20 +455,20 @@ async function exchangeTelegramAuthCode({
455
455
  clientId,
456
456
  code,
457
457
  codeVerifier,
458
- returnTo,
458
+ redirectUri,
459
459
  }: {
460
460
  authHubUrl?: string;
461
461
  clientId: string;
462
462
  code: string;
463
463
  codeVerifier: string;
464
- returnTo: string;
464
+ redirectUri: string;
465
465
  }): Promise<string> {
466
466
  const body = new URLSearchParams({
467
467
  client_id: clientId,
468
468
  code,
469
469
  code_verifier: codeVerifier,
470
470
  grant_type: "authorization_code",
471
- return_to: returnTo,
471
+ redirect_uri: redirectUri,
472
472
  });
473
473
 
474
474
  const response = await fetch(await resolveAuthTokenUrl(authHubUrl), {
@@ -640,7 +640,7 @@ export function HypurrConnectProvider({
640
640
  clientId: normalizeClientId(config.clientId),
641
641
  code: callback.code,
642
642
  codeVerifier: authSession.codeVerifier,
643
- returnTo: authSession.returnTo || currentReturnTo(),
643
+ redirectUri: authSession.redirectUri || currentRedirectUri(),
644
644
  })
645
645
  .then(acceptTelegramToken)
646
646
  .catch((err) =>
@@ -1643,12 +1643,12 @@ export function HypurrConnectProvider({
1643
1643
  const state = randomState();
1644
1644
  const codeVerifier = randomCodeVerifier();
1645
1645
 
1646
- const configuredReturnTo = config.telegram?.returnTo;
1647
- const returnTo =
1648
- typeof configuredReturnTo === "function"
1649
- ? configuredReturnTo()
1650
- : configuredReturnTo || currentReturnTo();
1651
- storeTelegramAuthSession(state, codeVerifier, returnTo);
1646
+ const configuredRedirectUri = config.telegram?.redirectUri;
1647
+ const redirectUri =
1648
+ typeof configuredRedirectUri === "function"
1649
+ ? configuredRedirectUri()
1650
+ : configuredRedirectUri || currentRedirectUri();
1651
+ storeTelegramAuthSession(state, codeVerifier, redirectUri);
1652
1652
 
1653
1653
  const width = 520;
1654
1654
  const height = 720;
@@ -1676,7 +1676,8 @@ export function HypurrConnectProvider({
1676
1676
  "client_id",
1677
1677
  normalizeClientId(config.clientId),
1678
1678
  );
1679
- authUrl.searchParams.set("return_to", returnTo);
1679
+ authUrl.searchParams.set("redirect_uri", redirectUri);
1680
+ authUrl.searchParams.set("return_to", redirectUri);
1680
1681
  authUrl.searchParams.set("state", state);
1681
1682
  authUrl.searchParams.set(
1682
1683
  "scope",
@@ -1704,7 +1705,7 @@ export function HypurrConnectProvider({
1704
1705
  }, [
1705
1706
  config.clientId,
1706
1707
  config.telegram?.authHubUrl,
1707
- config.telegram?.returnTo,
1708
+ config.telegram?.redirectUri,
1708
1709
  config.telegram?.scope,
1709
1710
  ]);
1710
1711
 
@@ -1898,6 +1899,7 @@ export function HypurrConnectProvider({
1898
1899
  agent,
1899
1900
  agentReady,
1900
1901
  clearAgent: handleClearAgent,
1902
+ eoaAddress,
1901
1903
 
1902
1904
  authDataMap,
1903
1905
  authToken: tgAuthToken,
@@ -1942,6 +1944,7 @@ export function HypurrConnectProvider({
1942
1944
  agent,
1943
1945
  agentReady,
1944
1946
  handleClearAgent,
1947
+ eoaAddress,
1945
1948
  authDataMap,
1946
1949
  tgAuthToken,
1947
1950
  telegramRpcOptions,
@@ -2,6 +2,8 @@ import { AnimatePresence, motion } from "framer-motion";
2
2
  import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
3
3
  import {
4
4
  useCallback,
5
+ useEffect,
6
+ useMemo,
5
7
  useState,
6
8
  type CSSProperties,
7
9
  type ReactNode,
@@ -13,7 +15,12 @@ import {
13
15
  import { DeleteWalletModal } from "./DeleteWalletModal";
14
16
  import { useHypurrConnectInternal } from "./HypurrConnectProvider";
15
17
  import { RenameWalletModal } from "./RenameWalletModal";
16
- import { getAgentExpiryTitle } from "./agentWallet";
18
+ import {
19
+ clearAgent as clearStoredAgent,
20
+ loadAllAgents,
21
+ type LocalApprovedAgent,
22
+ } from "./agent";
23
+ import { formatAgentExpiry, getAgentExpiryTitle } from "./agentWallet";
17
24
  import {
18
25
  Bot,
19
26
  Copy,
@@ -314,6 +321,9 @@ export function UserProfileModal({
314
321
  deleteWallet,
315
322
  renameWallet,
316
323
  authMethod,
324
+ agent,
325
+ eoaAddress,
326
+ clearAgent: clearContextAgent,
317
327
  } = useHypurrConnectInternal();
318
328
 
319
329
  const [settingsTab, setSettingsTab] = useState<SettingsTab>("ui");
@@ -321,6 +331,41 @@ export function UserProfileModal({
321
331
  useState<HyperliquidWallet | null>(null);
322
332
  const [walletToRename, setWalletToRename] =
323
333
  useState<HyperliquidWallet | null>(null);
334
+ const [storedAgents, setStoredAgents] = useState<LocalApprovedAgent[]>([]);
335
+
336
+ // Local approved agents live in localStorage; refresh whenever the modal opens.
337
+ useEffect(() => {
338
+ if (isOpen) setStoredAgents(loadAllAgents());
339
+ }, [isOpen]);
340
+
341
+ // Merge the localStorage scan with the reactive context agent (the currently
342
+ // connected EOA's approved agent), deduped by owner address.
343
+ const localAgents = useMemo(() => {
344
+ const byOwner = new Map<string, LocalApprovedAgent>();
345
+ for (const a of storedAgents) byOwner.set(a.masterAddress.toLowerCase(), a);
346
+ if (agent && eoaAddress && !byOwner.has(eoaAddress.toLowerCase())) {
347
+ byOwner.set(eoaAddress.toLowerCase(), {
348
+ ...agent,
349
+ masterAddress: eoaAddress,
350
+ });
351
+ }
352
+ return [...byOwner.values()].sort((a, b) => b.approvedAt - a.approvedAt);
353
+ }, [storedAgents, agent, eoaAddress]);
354
+
355
+ const handleRemoveLocalAgent = useCallback(
356
+ (masterAddress: string) => {
357
+ if (
358
+ eoaAddress &&
359
+ masterAddress.toLowerCase() === eoaAddress.toLowerCase()
360
+ ) {
361
+ clearContextAgent();
362
+ } else {
363
+ clearStoredAgent(masterAddress);
364
+ }
365
+ setStoredAgents(loadAllAgents());
366
+ },
367
+ [eoaAddress, clearContextAgent],
368
+ );
324
369
 
325
370
  const profilePic = user?.photoUrl || "";
326
371
  const displayName = user?.displayName || "";
@@ -347,10 +392,12 @@ export function UserProfileModal({
347
392
  );
348
393
 
349
394
  const handleCopyAddress = useCallback(() => {
350
- if (!displayName) return;
351
- navigator.clipboard.writeText(displayName);
395
+ // Copy the full address, not the truncated display name.
396
+ const address = user?.address || eoaAddress || displayName;
397
+ if (!address) return;
398
+ navigator.clipboard.writeText(address);
352
399
  onNotify?.({ type: "success", message: "Address copied" });
353
- }, [displayName, onNotify]);
400
+ }, [user?.address, eoaAddress, displayName, onNotify]);
354
401
 
355
402
  return (
356
403
  <div className="hypurr-connect" style={{ display: "contents" }}>
@@ -798,6 +845,48 @@ export function UserProfileModal({
798
845
  {wallets.length} wallet
799
846
  {wallets.length !== 1 ? "s" : ""} linked
800
847
  </p>
848
+
849
+ {localAgents.length > 0 && (
850
+ <div style={{ marginTop: 8 }}>
851
+ <p
852
+ style={{
853
+ margin: "0 0 8px",
854
+ color: profileColors.muted,
855
+ ...upperLabelStyle,
856
+ }}
857
+ >
858
+ Agent wallets
859
+ </p>
860
+ <div
861
+ style={{
862
+ display: "flex",
863
+ flexDirection: "column",
864
+ gap: 6,
865
+ }}
866
+ >
867
+ {localAgents.map((agent) => (
868
+ <LocalAgentRow
869
+ key={agent.masterAddress}
870
+ agent={agent}
871
+ onRemove={() =>
872
+ handleRemoveLocalAgent(agent.masterAddress)
873
+ }
874
+ />
875
+ ))}
876
+ </div>
877
+ <p
878
+ style={{
879
+ margin: "8px 0 0",
880
+ fontSize: 12.5,
881
+ lineHeight: "1rem",
882
+ color: profileColors.subdued,
883
+ textAlign: "center",
884
+ }}
885
+ >
886
+ Approved on this device
887
+ </p>
888
+ </div>
889
+ )}
801
890
  </div>
802
891
  )}
803
892
  </div>
@@ -1039,6 +1128,118 @@ function WalletRow({
1039
1128
  );
1040
1129
  }
1041
1130
 
1131
+ function shortAddress(address: string): string {
1132
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
1133
+ }
1134
+
1135
+ // Older builds stored validUntil scaled ×1000 (µs). Coerce those back to ms.
1136
+ function normalizeExpiryMs(validUntil: number): number {
1137
+ return validUntil > 1e14 ? Math.round(validUntil / 1000) : validUntil;
1138
+ }
1139
+
1140
+ function LocalAgentRow({
1141
+ agent,
1142
+ onRemove,
1143
+ }: {
1144
+ agent: LocalApprovedAgent;
1145
+ onRemove: () => void;
1146
+ }) {
1147
+ const [hovered, setHovered] = useState(false);
1148
+ const expiresAtMs = normalizeExpiryMs(agent.validUntil);
1149
+ const isExpired = expiresAtMs <= Date.now();
1150
+ const agentTypeColor = "rgb(var(--blue-500))";
1151
+ return (
1152
+ <div
1153
+ style={{
1154
+ ...walletRowStyle,
1155
+ background: hovered
1156
+ ? profileColors.surfaceBtnHover
1157
+ : profileColors.surfaceBtn,
1158
+ borderColor: hovered
1159
+ ? profileColors.surfaceBdHover
1160
+ : profileColors.surfaceBd,
1161
+ }}
1162
+ onMouseEnter={() => setHovered(true)}
1163
+ onMouseLeave={() => setHovered(false)}
1164
+ >
1165
+ <div
1166
+ style={{
1167
+ width: 32,
1168
+ height: 32,
1169
+ borderRadius: 6,
1170
+ background: "rgb(var(--blue-500) / 0.16)",
1171
+ border: "1px solid rgb(var(--blue-500) / 0.34)",
1172
+ display: "flex",
1173
+ alignItems: "center",
1174
+ justifyContent: "center",
1175
+ flexShrink: 0,
1176
+ color: agentTypeColor,
1177
+ }}
1178
+ >
1179
+ <Bot size={15} />
1180
+ </div>
1181
+ <div style={{ flex: 1, minWidth: 0 }}>
1182
+ <p
1183
+ style={{
1184
+ margin: 0,
1185
+ fontSize: 12.5,
1186
+ lineHeight: "1rem",
1187
+ fontWeight: 500,
1188
+ color: profileColors.text,
1189
+ fontFamily: fontFamily.mono,
1190
+ overflow: "hidden",
1191
+ textOverflow: "ellipsis",
1192
+ whiteSpace: "nowrap",
1193
+ }}
1194
+ >
1195
+ {shortAddress(agent.address)}
1196
+ </p>
1197
+ <p
1198
+ style={{
1199
+ margin: "2px 0 0",
1200
+ fontSize: 12.5,
1201
+ lineHeight: "1rem",
1202
+ color: isExpired ? EXPIRED_AGENT_COLOR : profileColors.muted,
1203
+ }}
1204
+ >
1205
+ {isExpired ? "Expired" : "Valid until"}{" "}
1206
+ {formatAgentExpiry(expiresAtMs)}
1207
+ </p>
1208
+ <p
1209
+ style={{
1210
+ margin: "2px 0 0",
1211
+ fontSize: 12.5,
1212
+ lineHeight: "1rem",
1213
+ color: profileColors.subdued,
1214
+ fontFamily: fontFamily.mono,
1215
+ }}
1216
+ >
1217
+ Owner {shortAddress(agent.masterAddress)}
1218
+ </p>
1219
+ </div>
1220
+ <div
1221
+ style={{
1222
+ display: "flex",
1223
+ alignItems: "center",
1224
+ flexShrink: 0,
1225
+ opacity: hovered ? 1 : 0,
1226
+ transition: "opacity 120ms",
1227
+ }}
1228
+ >
1229
+ <IconBtn
1230
+ color="#ef4444"
1231
+ hoverBackgroundColor="rgba(239,68,68,0.1)"
1232
+ title="Remove local agent"
1233
+ ariaLabel={`Remove local agent for ${shortAddress(agent.masterAddress)}`}
1234
+ onClick={onRemove}
1235
+ >
1236
+ <Trash2 size={13} />
1237
+ </IconBtn>
1238
+ </div>
1239
+ </div>
1240
+ );
1241
+ }
1242
+
1042
1243
  function IconBtn({
1043
1244
  children,
1044
1245
  color,
package/src/agent.ts CHANGED
@@ -26,6 +26,39 @@ export function clearAgent(masterAddress: string): void {
26
26
  localStorage.removeItem(storageKey(masterAddress));
27
27
  }
28
28
 
29
+ /** A locally-stored agent alongside the master address it was approved for. */
30
+ export interface LocalApprovedAgent extends StoredAgent {
31
+ masterAddress: string;
32
+ }
33
+
34
+ /**
35
+ * Enumerate every agent approved and stored locally on this device by scanning
36
+ * localStorage for the agent storage prefix. Newest approvals first.
37
+ */
38
+ export function loadAllAgents(): LocalApprovedAgent[] {
39
+ const agents: LocalApprovedAgent[] = [];
40
+ try {
41
+ for (let i = 0; i < localStorage.length; i++) {
42
+ const key = localStorage.key(i);
43
+ if (!key || !key.startsWith(`${AGENT_STORAGE_PREFIX}:`)) continue;
44
+ const raw = localStorage.getItem(key);
45
+ if (!raw) continue;
46
+ try {
47
+ const agent = JSON.parse(raw) as StoredAgent;
48
+ agents.push({
49
+ ...agent,
50
+ masterAddress: key.slice(key.indexOf(":") + 1),
51
+ });
52
+ } catch {
53
+ // Skip malformed entries.
54
+ }
55
+ }
56
+ } catch {
57
+ return [];
58
+ }
59
+ return agents.sort((a, b) => b.approvedAt - a.approvedAt);
60
+ }
61
+
29
62
  /**
30
63
  * Generate a random 32-byte private key and derive its address using the
31
64
  * local PrivateKeySigner compatibility wrapper.
@@ -73,10 +106,8 @@ async function fetchExtraAgents(
73
106
  const agents: unknown = await res.json();
74
107
  if (!Array.isArray(agents)) return [];
75
108
 
76
- return (agents as ExtraAgent[]).map((agent) => ({
77
- ...agent,
78
- validUntil: agent.validUntil * 1000,
79
- }));
109
+ // The extraAgents API already returns validUntil in epoch ms.
110
+ return agents as ExtraAgent[];
80
111
  }
81
112
 
82
113
  /**
package/src/types.ts CHANGED
@@ -33,8 +33,8 @@ export interface HypurrConnectConfig {
33
33
  telegram?: {
34
34
  /** Auth hub login URL. Defaults to https://auth.hypurr.fun/login. */
35
35
  authHubUrl?: string;
36
- /** Optional callback URL. Defaults to the current page without auth query params. */
37
- returnTo?: string | (() => string);
36
+ /** Optional OAuth redirect URI. Defaults to the current page without auth query params. */
37
+ redirectUri?: string | (() => string);
38
38
  /** Requested hub scopes. Defaults to the scopes required by this SDK. */
39
39
  scope?: string | string[];
40
40
  /** @deprecated Telegram login is handled by the auth hub; this option is ignored. */