@hfunlabs/hypurr-connect 0.1.23 → 0.1.25

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/README.md CHANGED
@@ -49,7 +49,6 @@ const config = {
49
49
  grpcUrl: "https://grpc.hypurr.fun",
50
50
  telegram: {
51
51
  authHubUrl: "https://auth.hypurr.fun/login",
52
- // tokenUrl defaults to https://auth.hypurr.fun/token
53
52
  scope: [
54
53
  "telegram:user:read",
55
54
  "telegram:wallet:read",
@@ -127,7 +126,6 @@ interface HypurrConnectConfig {
127
126
  isTestnet?: boolean; // Use testnet endpoints (default: false)
128
127
  telegram: {
129
128
  authHubUrl?: string; // Auth hub URL (default: https://auth.hypurr.fun/login)
130
- tokenUrl?: string; // Token exchange URL (default: auth hub /token)
131
129
  returnTo?: string | (() => string); // Callback URL (default: current page)
132
130
  scope?: string | string[]; // Requested JWT scopes
133
131
  };
@@ -157,7 +155,8 @@ for generated protobuf service clients.
157
155
  4. The popup callback page posts `{ token, state }` or `{ code, state }` to the
158
156
  opener with `postMessage` and closes.
159
157
  5. The opener validates `state`. Legacy tokens are stored directly; auth codes
160
- are exchanged at the configured token URL with the stored PKCE verifier.
158
+ are exchanged at the OAuth metadata `token_endpoint` with the stored PKCE
159
+ verifier.
161
160
  6. The SDK calls the Hypurr gRPC backend with `Authorization: Bearer <jwt>`
162
161
  metadata.
163
162
  7. An `ExchangeClient` is created with `GrpcExchangeTransport`; exchange
package/dist/index.d.ts CHANGED
@@ -27,8 +27,6 @@ interface HypurrConnectConfig {
27
27
  telegram?: {
28
28
  /** Auth hub login URL. Defaults to https://auth.hypurr.fun/login. */
29
29
  authHubUrl?: string;
30
- /** Auth hub token exchange URL. Defaults to the auth hub login URL with `/login` replaced by `/token`. */
31
- tokenUrl?: string;
32
30
  /** Optional callback URL. Defaults to the current page without auth query params. */
33
31
  returnTo?: string | (() => string);
34
32
  /** Requested hub scopes. Defaults to the scopes required by this SDK. */
package/dist/index.js CHANGED
@@ -80,6 +80,28 @@ function saveAgent(masterAddress, agent) {
80
80
  function clearAgent(masterAddress) {
81
81
  localStorage.removeItem(storageKey(masterAddress));
82
82
  }
83
+ function loadAllAgents() {
84
+ const agents = [];
85
+ try {
86
+ for (let i = 0; i < localStorage.length; i++) {
87
+ const key = localStorage.key(i);
88
+ if (!key || !key.startsWith(`${AGENT_STORAGE_PREFIX}:`)) continue;
89
+ const raw = localStorage.getItem(key);
90
+ if (!raw) continue;
91
+ try {
92
+ const agent = JSON.parse(raw);
93
+ agents.push({
94
+ ...agent,
95
+ masterAddress: key.slice(key.indexOf(":") + 1)
96
+ });
97
+ } catch {
98
+ }
99
+ }
100
+ } catch {
101
+ return [];
102
+ }
103
+ return agents.sort((a, b) => b.approvedAt - a.approvedAt);
104
+ }
83
105
  async function generateAgentKey() {
84
106
  const bytes = crypto.getRandomValues(new Uint8Array(32));
85
107
  const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -100,10 +122,7 @@ async function fetchExtraAgents(userAddress, isTestnet) {
100
122
  if (!res.ok) return [];
101
123
  const agents = await res.json();
102
124
  if (!Array.isArray(agents)) return [];
103
- return agents.map((agent) => ({
104
- ...agent,
105
- validUntil: agent.validUntil * 1e3
106
- }));
125
+ return agents;
107
126
  }
108
127
  async function fetchActiveAgent(userAddress, isTestnet) {
109
128
  const nowMs = Date.now();
@@ -542,17 +561,51 @@ function takeTelegramAuthSession(state) {
542
561
  sessionStorage.removeItem(returnToKey);
543
562
  return { codeVerifier, returnTo };
544
563
  }
545
- function resolveAuthTokenUrl(authHubUrl, tokenUrl) {
546
- const configuredTokenUrl = tokenUrl?.trim();
547
- if (configuredTokenUrl) return configuredTokenUrl;
564
+ function fallbackAuthTokenUrl(authHubUrl) {
548
565
  const url = new URL(authHubUrl || DEFAULT_AUTH_HUB_URL);
549
- const pathWithoutTrailingSlash = url.pathname.replace(/\/+$/, "");
550
- const basePath = pathWithoutTrailingSlash.replace(/\/[^/]*$/, "");
551
- url.pathname = `${basePath}/token`;
566
+ url.pathname = "/oauth/token";
552
567
  url.search = "";
553
568
  url.hash = "";
554
569
  return url.toString();
555
570
  }
571
+ function authMetadataUrls(authHubUrl) {
572
+ const authUrl = new URL(authHubUrl || DEFAULT_AUTH_HUB_URL);
573
+ const urls = [
574
+ new URL("/.well-known/oauth-authorization-server", authUrl).toString(),
575
+ new URL("/.well-known/openid-configuration", authUrl).toString()
576
+ ];
577
+ const authPath = authUrl.pathname.replace(/\/+$/, "");
578
+ const basePath = authPath.replace(/\/[^/]*$/, "");
579
+ if (basePath) {
580
+ urls.push(
581
+ new URL(
582
+ `/.well-known/oauth-authorization-server${basePath}`,
583
+ authUrl
584
+ ).toString()
585
+ );
586
+ }
587
+ return Array.from(new Set(urls));
588
+ }
589
+ async function tokenUrlFromMetadata(authHubUrl) {
590
+ for (const metadataUrl of authMetadataUrls(authHubUrl)) {
591
+ try {
592
+ const response = await fetch(metadataUrl, {
593
+ headers: { accept: "application/json" }
594
+ });
595
+ if (!response.ok) continue;
596
+ const metadata = await response.json();
597
+ const tokenEndpoint = metadata.token_endpoint;
598
+ if (typeof tokenEndpoint === "string" && tokenEndpoint.trim()) {
599
+ return tokenEndpoint.trim();
600
+ }
601
+ } catch {
602
+ }
603
+ }
604
+ return void 0;
605
+ }
606
+ async function resolveAuthTokenUrl(authHubUrl) {
607
+ return await tokenUrlFromMetadata(authHubUrl) || fallbackAuthTokenUrl(authHubUrl);
608
+ }
556
609
  function getTokenFromExchangeResponse(data) {
557
610
  if (typeof data === "string") {
558
611
  const token = data.trim();
@@ -570,8 +623,7 @@ async function exchangeTelegramAuthCode({
570
623
  clientId,
571
624
  code,
572
625
  codeVerifier,
573
- returnTo,
574
- tokenUrl
626
+ returnTo
575
627
  }) {
576
628
  const body = new URLSearchParams({
577
629
  client_id: clientId,
@@ -580,7 +632,7 @@ async function exchangeTelegramAuthCode({
580
632
  grant_type: "authorization_code",
581
633
  return_to: returnTo
582
634
  });
583
- const response = await fetch(resolveAuthTokenUrl(authHubUrl, tokenUrl), {
635
+ const response = await fetch(await resolveAuthTokenUrl(authHubUrl), {
584
636
  method: "POST",
585
637
  headers: {
586
638
  accept: "application/json",
@@ -697,8 +749,7 @@ function HypurrConnectProvider({
697
749
  clientId: normalizeClientId(config.clientId),
698
750
  code: callback.code,
699
751
  codeVerifier: authSession.codeVerifier,
700
- returnTo: authSession.returnTo || currentReturnTo(),
701
- tokenUrl: config.telegram?.tokenUrl
752
+ returnTo: authSession.returnTo || currentReturnTo()
702
753
  }).then(acceptTelegramToken).catch(
703
754
  (err) => setTgError(err instanceof Error ? err.message : String(err))
704
755
  ).finally(() => setTgLoading(false));
@@ -713,8 +764,7 @@ function HypurrConnectProvider({
713
764
  [
714
765
  acceptTelegramToken,
715
766
  config.clientId,
716
- config.telegram?.authHubUrl,
717
- config.telegram?.tokenUrl
767
+ config.telegram?.authHubUrl
718
768
  ]
719
769
  );
720
770
  useEffect(() => {
@@ -1715,6 +1765,7 @@ function HypurrConnectProvider({
1715
1765
  agent,
1716
1766
  agentReady,
1717
1767
  clearAgent: handleClearAgent,
1768
+ eoaAddress,
1718
1769
  authDataMap,
1719
1770
  authToken: tgAuthToken,
1720
1771
  telegramRpcOptions,
@@ -1758,6 +1809,7 @@ function HypurrConnectProvider({
1758
1809
  agent,
1759
1810
  agentReady,
1760
1811
  handleClearAgent,
1812
+ eoaAddress,
1761
1813
  authDataMap,
1762
1814
  tgAuthToken,
1763
1815
  telegramRpcOptions,
@@ -3265,7 +3317,7 @@ import { AnimatePresence as AnimatePresence7, motion as motion7 } from "framer-m
3265
3317
  import {
3266
3318
  Fragment as Fragment8,
3267
3319
  useCallback as useCallback9,
3268
- useMemo as useMemo3,
3320
+ useMemo as useMemo4,
3269
3321
  useState as useState8
3270
3322
  } from "react";
3271
3323
 
@@ -3371,6 +3423,8 @@ function AgentExpiryWarningIcon({
3371
3423
  import { AnimatePresence as AnimatePresence6, motion as motion6 } from "framer-motion";
3372
3424
  import {
3373
3425
  useCallback as useCallback8,
3426
+ useEffect as useEffect3,
3427
+ useMemo as useMemo3,
3374
3428
  useState as useState7
3375
3429
  } from "react";
3376
3430
 
@@ -4329,11 +4383,40 @@ function UserProfileModal({
4329
4383
  wallets: userWallets,
4330
4384
  deleteWallet,
4331
4385
  renameWallet,
4332
- authMethod
4386
+ authMethod,
4387
+ agent,
4388
+ eoaAddress,
4389
+ clearAgent: clearContextAgent
4333
4390
  } = useHypurrConnectInternal();
4334
4391
  const [settingsTab, setSettingsTab] = useState7("ui");
4335
4392
  const [walletToDelete, setWalletToDelete] = useState7(null);
4336
4393
  const [walletToRename, setWalletToRename] = useState7(null);
4394
+ const [storedAgents, setStoredAgents] = useState7([]);
4395
+ useEffect3(() => {
4396
+ if (isOpen) setStoredAgents(loadAllAgents());
4397
+ }, [isOpen]);
4398
+ const localAgents = useMemo3(() => {
4399
+ const byOwner = /* @__PURE__ */ new Map();
4400
+ for (const a of storedAgents) byOwner.set(a.masterAddress.toLowerCase(), a);
4401
+ if (agent && eoaAddress && !byOwner.has(eoaAddress.toLowerCase())) {
4402
+ byOwner.set(eoaAddress.toLowerCase(), {
4403
+ ...agent,
4404
+ masterAddress: eoaAddress
4405
+ });
4406
+ }
4407
+ return [...byOwner.values()].sort((a, b) => b.approvedAt - a.approvedAt);
4408
+ }, [storedAgents, agent, eoaAddress]);
4409
+ const handleRemoveLocalAgent = useCallback8(
4410
+ (masterAddress) => {
4411
+ if (eoaAddress && masterAddress.toLowerCase() === eoaAddress.toLowerCase()) {
4412
+ clearContextAgent();
4413
+ } else {
4414
+ clearAgent(masterAddress);
4415
+ }
4416
+ setStoredAgents(loadAllAgents());
4417
+ },
4418
+ [eoaAddress, clearContextAgent]
4419
+ );
4337
4420
  const profilePic = user?.photoUrl || "";
4338
4421
  const displayName = user?.displayName || "";
4339
4422
  const hfunScore = user?.hfunScore ?? null;
@@ -4356,10 +4439,11 @@ function UserProfileModal({
4356
4439
  [renameWallet, onWalletRenamed]
4357
4440
  );
4358
4441
  const handleCopyAddress = useCallback8(() => {
4359
- if (!displayName) return;
4360
- navigator.clipboard.writeText(displayName);
4442
+ const address = user?.address || eoaAddress || displayName;
4443
+ if (!address) return;
4444
+ navigator.clipboard.writeText(address);
4361
4445
  onNotify?.({ type: "success", message: "Address copied" });
4362
- }, [displayName, onNotify]);
4446
+ }, [user?.address, eoaAddress, displayName, onNotify]);
4363
4447
  return /* @__PURE__ */ jsxs10("div", { className: "hypurr-connect", style: { display: "contents" }, children: [
4364
4448
  /* @__PURE__ */ jsx11(AnimatePresence6, { children: isOpen && /* @__PURE__ */ jsxs10(Fragment7, { children: [
4365
4449
  /* @__PURE__ */ jsx11(SpinKeyframes, {}),
@@ -4873,7 +4957,51 @@ function UserProfileModal({
4873
4957
  " linked"
4874
4958
  ]
4875
4959
  }
4876
- )
4960
+ ),
4961
+ localAgents.length > 0 && /* @__PURE__ */ jsxs10("div", { style: { marginTop: 8 }, children: [
4962
+ /* @__PURE__ */ jsx11(
4963
+ "p",
4964
+ {
4965
+ style: {
4966
+ margin: "0 0 8px",
4967
+ color: profileColors.muted,
4968
+ ...upperLabelStyle
4969
+ },
4970
+ children: "Agent wallets"
4971
+ }
4972
+ ),
4973
+ /* @__PURE__ */ jsx11(
4974
+ "div",
4975
+ {
4976
+ style: {
4977
+ display: "flex",
4978
+ flexDirection: "column",
4979
+ gap: 6
4980
+ },
4981
+ children: localAgents.map((agent2) => /* @__PURE__ */ jsx11(
4982
+ LocalAgentRow,
4983
+ {
4984
+ agent: agent2,
4985
+ onRemove: () => handleRemoveLocalAgent(agent2.masterAddress)
4986
+ },
4987
+ agent2.masterAddress
4988
+ ))
4989
+ }
4990
+ ),
4991
+ /* @__PURE__ */ jsx11(
4992
+ "p",
4993
+ {
4994
+ style: {
4995
+ margin: "8px 0 0",
4996
+ fontSize: 12.5,
4997
+ lineHeight: "1rem",
4998
+ color: profileColors.subdued,
4999
+ textAlign: "center"
5000
+ },
5001
+ children: "Approved on this device"
5002
+ }
5003
+ )
5004
+ ] })
4877
5005
  ]
4878
5006
  }
4879
5007
  )
@@ -5117,6 +5245,127 @@ function WalletRow({
5117
5245
  }
5118
5246
  );
5119
5247
  }
5248
+ function shortAddress(address) {
5249
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
5250
+ }
5251
+ function normalizeExpiryMs(validUntil) {
5252
+ return validUntil > 1e14 ? Math.round(validUntil / 1e3) : validUntil;
5253
+ }
5254
+ function LocalAgentRow({
5255
+ agent,
5256
+ onRemove
5257
+ }) {
5258
+ const [hovered, setHovered] = useState7(false);
5259
+ const expiresAtMs = normalizeExpiryMs(agent.validUntil);
5260
+ const isExpired = expiresAtMs <= Date.now();
5261
+ const agentTypeColor = "rgb(var(--blue-500))";
5262
+ return /* @__PURE__ */ jsxs10(
5263
+ "div",
5264
+ {
5265
+ style: {
5266
+ ...walletRowStyle,
5267
+ background: hovered ? profileColors.surfaceBtnHover : profileColors.surfaceBtn,
5268
+ borderColor: hovered ? profileColors.surfaceBdHover : profileColors.surfaceBd
5269
+ },
5270
+ onMouseEnter: () => setHovered(true),
5271
+ onMouseLeave: () => setHovered(false),
5272
+ children: [
5273
+ /* @__PURE__ */ jsx11(
5274
+ "div",
5275
+ {
5276
+ style: {
5277
+ width: 32,
5278
+ height: 32,
5279
+ borderRadius: 6,
5280
+ background: "rgb(var(--blue-500) / 0.16)",
5281
+ border: "1px solid rgb(var(--blue-500) / 0.34)",
5282
+ display: "flex",
5283
+ alignItems: "center",
5284
+ justifyContent: "center",
5285
+ flexShrink: 0,
5286
+ color: agentTypeColor
5287
+ },
5288
+ children: /* @__PURE__ */ jsx11(Bot, { size: 15 })
5289
+ }
5290
+ ),
5291
+ /* @__PURE__ */ jsxs10("div", { style: { flex: 1, minWidth: 0 }, children: [
5292
+ /* @__PURE__ */ jsx11(
5293
+ "p",
5294
+ {
5295
+ style: {
5296
+ margin: 0,
5297
+ fontSize: 12.5,
5298
+ lineHeight: "1rem",
5299
+ fontWeight: 500,
5300
+ color: profileColors.text,
5301
+ fontFamily: fontFamily.mono,
5302
+ overflow: "hidden",
5303
+ textOverflow: "ellipsis",
5304
+ whiteSpace: "nowrap"
5305
+ },
5306
+ children: shortAddress(agent.address)
5307
+ }
5308
+ ),
5309
+ /* @__PURE__ */ jsxs10(
5310
+ "p",
5311
+ {
5312
+ style: {
5313
+ margin: "2px 0 0",
5314
+ fontSize: 12.5,
5315
+ lineHeight: "1rem",
5316
+ color: isExpired ? EXPIRED_AGENT_COLOR : profileColors.muted
5317
+ },
5318
+ children: [
5319
+ isExpired ? "Expired" : "Valid until",
5320
+ " ",
5321
+ formatAgentExpiry(expiresAtMs)
5322
+ ]
5323
+ }
5324
+ ),
5325
+ /* @__PURE__ */ jsxs10(
5326
+ "p",
5327
+ {
5328
+ style: {
5329
+ margin: "2px 0 0",
5330
+ fontSize: 12.5,
5331
+ lineHeight: "1rem",
5332
+ color: profileColors.subdued,
5333
+ fontFamily: fontFamily.mono
5334
+ },
5335
+ children: [
5336
+ "Owner ",
5337
+ shortAddress(agent.masterAddress)
5338
+ ]
5339
+ }
5340
+ )
5341
+ ] }),
5342
+ /* @__PURE__ */ jsx11(
5343
+ "div",
5344
+ {
5345
+ style: {
5346
+ display: "flex",
5347
+ alignItems: "center",
5348
+ flexShrink: 0,
5349
+ opacity: hovered ? 1 : 0,
5350
+ transition: "opacity 120ms"
5351
+ },
5352
+ children: /* @__PURE__ */ jsx11(
5353
+ IconBtn,
5354
+ {
5355
+ color: "#ef4444",
5356
+ hoverBackgroundColor: "rgba(239,68,68,0.1)",
5357
+ title: "Remove local agent",
5358
+ ariaLabel: `Remove local agent for ${shortAddress(agent.masterAddress)}`,
5359
+ onClick: onRemove,
5360
+ children: /* @__PURE__ */ jsx11(Trash2, { size: 13 })
5361
+ }
5362
+ )
5363
+ }
5364
+ )
5365
+ ]
5366
+ }
5367
+ );
5368
+ }
5120
5369
  function IconBtn({
5121
5370
  children,
5122
5371
  color,
@@ -5291,7 +5540,7 @@ function WalletSelectorDropdown({
5291
5540
  setProfileOpen(true);
5292
5541
  onClose();
5293
5542
  }, [onClose]);
5294
- const walletListEntries = useMemo3(
5543
+ const walletListEntries = useMemo4(
5295
5544
  () => buildWalletListEntries(Array.isArray(wallets) ? wallets : void 0),
5296
5545
  [wallets]
5297
5546
  );