@hfunlabs/hypurr-connect 0.1.22 → 0.1.24

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
@@ -44,7 +44,7 @@ pnpm add @hfunlabs/hyperliquid @protobuf-ts/grpcweb-transport @protobuf-ts/runti
44
44
  import { HypurrConnectProvider } from "@hfunlabs/hypurr-connect";
45
45
 
46
46
  const config = {
47
- client_id: "your-client-id",
47
+ clientId: "your-client-id",
48
48
  isTestnet: false,
49
49
  grpcUrl: "https://grpc.hypurr.fun",
50
50
  telegram: {
@@ -119,7 +119,7 @@ function AppShell() {
119
119
 
120
120
  ```typescript
121
121
  interface HypurrConnectConfig {
122
- client_id: string; // Auth hub client identifier
122
+ clientId: string; // Auth hub client identifier
123
123
  grpcUrl?: string; // gRPC-web base URL (default: https://grpc.hypurr.fun)
124
124
  mediaUrl?: string; // Media base URL (default: https://media.hypurr.fun)
125
125
  grpcTimeout?: number; // Request timeout in ms (default: 15000)
@@ -134,7 +134,8 @@ interface HypurrConnectConfig {
134
134
 
135
135
  The SDK no longer renders Telegram's login widget or opens `oauth.telegram.org`
136
136
  directly. Telegram login is delegated to the Hypurr auth hub in a popup, and the
137
- popup posts the scoped JWT back to the original page.
137
+ popup posts either a legacy scoped JWT or an authorization code back to the
138
+ original page.
138
139
 
139
140
  ### Dependencies
140
141
 
@@ -147,16 +148,20 @@ for generated protobuf service clients.
147
148
 
148
149
  1. User clicks "Telegram" in the `LoginModal`.
149
150
  2. The SDK opens the configured auth hub in a popup with `client_id`,
150
- `return_to`, `state`, and requested `scope`.
151
+ `return_to`, `state`, requested `scope`, `code_challenge`, and
152
+ `code_challenge_method=S256`.
151
153
  3. The auth hub performs Telegram login and redirects back to `return_to` with
152
- a scoped JWT.
153
- 4. The popup callback page posts `{ token, state }` to the opener with
154
- `postMessage` and closes.
155
- 5. The opener validates `state`, stores the JWT, and calls the Hypurr gRPC
156
- backend with `Authorization: Bearer <jwt>` metadata.
157
- 6. An `ExchangeClient` is created with `GrpcExchangeTransport`; exchange
154
+ either a legacy scoped JWT or an authorization code.
155
+ 4. The popup callback page posts `{ token, state }` or `{ code, state }` to the
156
+ opener with `postMessage` and closes.
157
+ 5. The opener validates `state`. Legacy tokens are stored directly; auth codes
158
+ are exchanged at the OAuth metadata `token_endpoint` with the stored PKCE
159
+ verifier.
160
+ 6. The SDK calls the Hypurr gRPC backend with `Authorization: Bearer <jwt>`
161
+ metadata.
162
+ 7. An `ExchangeClient` is created with `GrpcExchangeTransport`; exchange
158
163
  actions are still signed server-side by the Hypurr backend.
159
- 7. The JWT session is persisted in localStorage (`hypurr-connect-tg-jwt`).
164
+ 8. The JWT session is persisted in localStorage (`hypurr-connect-tg-jwt`).
160
165
 
161
166
  If the popup is blocked, the SDK falls back to a full-page redirect. The
162
167
  default `returnTo` is the current page with auth query params removed; custom
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ import { AbstractViemLocalAccount } from '@hfunlabs/hyperliquid/signing';
15
15
 
16
16
  interface HypurrConnectConfig {
17
17
  /** Auth hub client identifier. Sent as `client_id` during Telegram login. */
18
- client_id: string;
18
+ clientId: string;
19
19
  /** gRPC-web base URL. Defaults to https://grpc.hypurr.fun. */
20
20
  grpcUrl?: string;
21
21
  /** Media base URL for user-uploaded assets. Defaults to https://media.hypurr.fun. */
package/dist/index.js CHANGED
@@ -295,6 +295,8 @@ import { jsx } from "react/jsx-runtime";
295
295
  var TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-jwt";
296
296
  var LEGACY_TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-user";
297
297
  var TELEGRAM_AUTH_STATE_KEY = "hypurr-connect-auth-state";
298
+ var TELEGRAM_AUTH_CODE_VERIFIER_PREFIX = "hypurr-connect-auth-code-verifier:";
299
+ var TELEGRAM_AUTH_RETURN_TO_PREFIX = "hypurr-connect-auth-return-to:";
298
300
  var TELEGRAM_AUTH_MESSAGE = "hypurr-connect:telegram-auth";
299
301
  var DEFAULT_AUTH_HUB_URL = "https://auth.hypurr.fun/login";
300
302
  var DEFAULT_MEDIA_URL = "https://media.hypurr.fun";
@@ -430,6 +432,9 @@ function withExpectedFrom(transaction, address) {
430
432
  function currentReturnTo() {
431
433
  const url = new URL(window.location.href);
432
434
  for (const param of [
435
+ "code",
436
+ "error",
437
+ "error_description",
433
438
  "token",
434
439
  "token_type",
435
440
  "token_source",
@@ -440,6 +445,29 @@ function currentReturnTo() {
440
445
  }
441
446
  return url.toString();
442
447
  }
448
+ function cleanAuthCallbackUrl() {
449
+ const cleanUrl = new URL(window.location.href);
450
+ for (const param of [
451
+ "code",
452
+ "error",
453
+ "error_description",
454
+ "token",
455
+ "token_type",
456
+ "token_source",
457
+ "state",
458
+ "scope"
459
+ ]) {
460
+ cleanUrl.searchParams.delete(param);
461
+ }
462
+ window.history.replaceState({}, document.title, cleanUrl.toString());
463
+ }
464
+ function base64UrlEncode(bytes) {
465
+ let value = "";
466
+ for (const byte of bytes) {
467
+ value += String.fromCharCode(byte);
468
+ }
469
+ return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
470
+ }
443
471
  function randomState() {
444
472
  const bytes = new Uint8Array(16);
445
473
  crypto.getRandomValues(bytes);
@@ -447,6 +475,23 @@ function randomState() {
447
475
  ""
448
476
  );
449
477
  }
478
+ function randomCodeVerifier() {
479
+ const bytes = new Uint8Array(64);
480
+ crypto.getRandomValues(bytes);
481
+ return base64UrlEncode(bytes);
482
+ }
483
+ async function createCodeChallenge(codeVerifier) {
484
+ if (!crypto.subtle) {
485
+ throw new Error(
486
+ "[HypurrConnect] Web Crypto API is required for Telegram auth."
487
+ );
488
+ }
489
+ const digest = await crypto.subtle.digest(
490
+ "SHA-256",
491
+ new TextEncoder().encode(codeVerifier)
492
+ );
493
+ return base64UrlEncode(new Uint8Array(digest));
494
+ }
450
495
  function normalizeScopes(scope) {
451
496
  if (Array.isArray(scope)) return scope.join(" ");
452
497
  return scope?.trim() || DEFAULT_TELEGRAM_SCOPES.join(" ");
@@ -454,12 +499,157 @@ function normalizeScopes(scope) {
454
499
  function normalizeClientId(clientId) {
455
500
  const normalized = typeof clientId === "string" ? clientId.trim() : "";
456
501
  if (!normalized) {
457
- throw new Error("[HypurrConnect] config.client_id is required.");
502
+ throw new Error("[HypurrConnect] config.clientId is required.");
458
503
  }
459
504
  return normalized;
460
505
  }
506
+ function codeVerifierStorageKey(state) {
507
+ return `${TELEGRAM_AUTH_CODE_VERIFIER_PREFIX}${state}`;
508
+ }
509
+ function returnToStorageKey(state) {
510
+ return `${TELEGRAM_AUTH_RETURN_TO_PREFIX}${state}`;
511
+ }
512
+ function storeTelegramAuthSession(state, codeVerifier, returnTo) {
513
+ const previousState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
514
+ if (previousState) {
515
+ sessionStorage.removeItem(codeVerifierStorageKey(previousState));
516
+ sessionStorage.removeItem(returnToStorageKey(previousState));
517
+ }
518
+ sessionStorage.setItem(TELEGRAM_AUTH_STATE_KEY, state);
519
+ sessionStorage.setItem(codeVerifierStorageKey(state), codeVerifier);
520
+ sessionStorage.setItem(returnToStorageKey(state), returnTo);
521
+ }
522
+ function clearTelegramAuthSession(state) {
523
+ sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
524
+ sessionStorage.removeItem(codeVerifierStorageKey(state));
525
+ sessionStorage.removeItem(returnToStorageKey(state));
526
+ }
527
+ function takeTelegramAuthSession(state) {
528
+ const expectedState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
529
+ sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
530
+ if (!expectedState || state !== expectedState) {
531
+ if (expectedState) {
532
+ sessionStorage.removeItem(codeVerifierStorageKey(expectedState));
533
+ sessionStorage.removeItem(returnToStorageKey(expectedState));
534
+ }
535
+ return null;
536
+ }
537
+ const codeVerifierKey = codeVerifierStorageKey(state);
538
+ const returnToKey = returnToStorageKey(state);
539
+ const codeVerifier = sessionStorage.getItem(codeVerifierKey);
540
+ const returnTo = sessionStorage.getItem(returnToKey);
541
+ sessionStorage.removeItem(codeVerifierKey);
542
+ sessionStorage.removeItem(returnToKey);
543
+ return { codeVerifier, returnTo };
544
+ }
545
+ function fallbackAuthTokenUrl(authHubUrl) {
546
+ const url = new URL(authHubUrl || DEFAULT_AUTH_HUB_URL);
547
+ url.pathname = "/oauth/token";
548
+ url.search = "";
549
+ url.hash = "";
550
+ return url.toString();
551
+ }
552
+ function authMetadataUrls(authHubUrl) {
553
+ const authUrl = new URL(authHubUrl || DEFAULT_AUTH_HUB_URL);
554
+ const urls = [
555
+ new URL("/.well-known/oauth-authorization-server", authUrl).toString(),
556
+ new URL("/.well-known/openid-configuration", authUrl).toString()
557
+ ];
558
+ const authPath = authUrl.pathname.replace(/\/+$/, "");
559
+ const basePath = authPath.replace(/\/[^/]*$/, "");
560
+ if (basePath) {
561
+ urls.push(
562
+ new URL(
563
+ `/.well-known/oauth-authorization-server${basePath}`,
564
+ authUrl
565
+ ).toString()
566
+ );
567
+ }
568
+ return Array.from(new Set(urls));
569
+ }
570
+ async function tokenUrlFromMetadata(authHubUrl) {
571
+ for (const metadataUrl of authMetadataUrls(authHubUrl)) {
572
+ try {
573
+ const response = await fetch(metadataUrl, {
574
+ headers: { accept: "application/json" }
575
+ });
576
+ if (!response.ok) continue;
577
+ const metadata = await response.json();
578
+ const tokenEndpoint = metadata.token_endpoint;
579
+ if (typeof tokenEndpoint === "string" && tokenEndpoint.trim()) {
580
+ return tokenEndpoint.trim();
581
+ }
582
+ } catch {
583
+ }
584
+ }
585
+ return void 0;
586
+ }
587
+ async function resolveAuthTokenUrl(authHubUrl) {
588
+ return await tokenUrlFromMetadata(authHubUrl) || fallbackAuthTokenUrl(authHubUrl);
589
+ }
590
+ function getTokenFromExchangeResponse(data) {
591
+ if (typeof data === "string") {
592
+ const token = data.trim();
593
+ return token || null;
594
+ }
595
+ if (typeof data !== "object" || data === null) return null;
596
+ const response = data;
597
+ for (const token of [response.token, response.access_token, response.jwt]) {
598
+ if (typeof token === "string" && token.trim()) return token.trim();
599
+ }
600
+ return null;
601
+ }
602
+ async function exchangeTelegramAuthCode({
603
+ authHubUrl,
604
+ clientId,
605
+ code,
606
+ codeVerifier,
607
+ returnTo
608
+ }) {
609
+ const body = new URLSearchParams({
610
+ client_id: clientId,
611
+ code,
612
+ code_verifier: codeVerifier,
613
+ grant_type: "authorization_code",
614
+ return_to: returnTo
615
+ });
616
+ const response = await fetch(await resolveAuthTokenUrl(authHubUrl), {
617
+ method: "POST",
618
+ headers: {
619
+ accept: "application/json",
620
+ "content-type": "application/x-www-form-urlencoded"
621
+ },
622
+ body
623
+ });
624
+ const responseText = await response.text();
625
+ if (!response.ok) {
626
+ const detail = responseText.trim();
627
+ throw new Error(
628
+ detail ? `[HypurrConnect] Auth code exchange failed: ${detail}` : `[HypurrConnect] Auth code exchange failed with HTTP ${response.status}.`
629
+ );
630
+ }
631
+ let responseData = responseText;
632
+ if (responseText) {
633
+ try {
634
+ responseData = JSON.parse(responseText);
635
+ } catch {
636
+ responseData = responseText;
637
+ }
638
+ }
639
+ const token = getTokenFromExchangeResponse(responseData);
640
+ if (!token) {
641
+ throw new Error("[HypurrConnect] Auth code exchange did not return a JWT.");
642
+ }
643
+ return token;
644
+ }
461
645
  function isTelegramAuthMessage(data) {
462
- return typeof data === "object" && data !== null && "type" in data && "token" in data && "state" in data && data.type === TELEGRAM_AUTH_MESSAGE && typeof data.token === "string" && typeof data.state === "string";
646
+ if (typeof data !== "object" || data === null) return false;
647
+ if (!("type" in data) || !("state" in data)) return false;
648
+ const message = data;
649
+ const hasToken = typeof message.token === "string";
650
+ const hasCode = typeof message.code === "string";
651
+ const hasError = typeof message.error === "string";
652
+ return message.type === TELEGRAM_AUTH_MESSAGE && typeof message.state === "string" && (hasToken || hasCode || hasError);
463
653
  }
464
654
  var HypurrConnectContext = createContext(null);
465
655
  function useHypurrConnect() {
@@ -517,6 +707,47 @@ function HypurrConnectProvider({
517
707
  localStorage.setItem(TELEGRAM_STORAGE_KEY, token);
518
708
  localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
519
709
  }, []);
710
+ const handleTelegramAuthCallback = useCallback(
711
+ (callback) => {
712
+ const authSession = takeTelegramAuthSession(callback.state);
713
+ if (!authSession) {
714
+ setTgError("Invalid auth callback state.");
715
+ return;
716
+ }
717
+ if (callback.error) {
718
+ setTgError(callback.error);
719
+ return;
720
+ }
721
+ if (callback.code) {
722
+ if (!authSession.codeVerifier) {
723
+ setTgError("Missing auth code verifier.");
724
+ return;
725
+ }
726
+ setTgLoading(true);
727
+ setTgError(null);
728
+ void exchangeTelegramAuthCode({
729
+ authHubUrl: config.telegram?.authHubUrl,
730
+ clientId: normalizeClientId(config.clientId),
731
+ code: callback.code,
732
+ codeVerifier: authSession.codeVerifier,
733
+ returnTo: authSession.returnTo || currentReturnTo()
734
+ }).then(acceptTelegramToken).catch(
735
+ (err) => setTgError(err instanceof Error ? err.message : String(err))
736
+ ).finally(() => setTgLoading(false));
737
+ return;
738
+ }
739
+ if (callback.token) {
740
+ acceptTelegramToken(callback.token);
741
+ return;
742
+ }
743
+ setTgError("Invalid auth callback.");
744
+ },
745
+ [
746
+ acceptTelegramToken,
747
+ config.clientId,
748
+ config.telegram?.authHubUrl
749
+ ]
750
+ );
520
751
  useEffect(() => {
521
752
  if (typeof document === "undefined" || !document.fonts) return;
522
753
  for (const face of [
@@ -532,57 +763,37 @@ function HypurrConnectProvider({
532
763
  useEffect(() => {
533
764
  const params = new URLSearchParams(window.location.search);
534
765
  const token = params.get("token");
535
- if (!token) {
766
+ const code = params.get("code");
767
+ const error = params.get("error_description") || params.get("error") || void 0;
768
+ if (!token && !code && !error) {
536
769
  localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
537
770
  return;
538
771
  }
539
772
  const callbackState = params.get("state") ?? "";
773
+ const callback = {
774
+ code: code || void 0,
775
+ error,
776
+ state: callbackState,
777
+ token: token || void 0,
778
+ type: TELEGRAM_AUTH_MESSAGE
779
+ };
540
780
  if (window.opener && window.opener !== window) {
541
- window.opener.postMessage(
542
- {
543
- type: TELEGRAM_AUTH_MESSAGE,
544
- token,
545
- state: callbackState
546
- },
547
- window.location.origin
548
- );
781
+ window.opener.postMessage(callback, window.location.origin);
549
782
  window.close();
550
783
  return;
551
784
  }
552
- const expectedState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
553
- sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
554
- if (!expectedState || callbackState !== expectedState) {
555
- setTgError("Invalid auth callback state.");
556
- return;
557
- }
558
- acceptTelegramToken(token);
559
- const cleanUrl = new URL(window.location.href);
560
- for (const param of [
561
- "token",
562
- "token_type",
563
- "token_source",
564
- "state",
565
- "scope"
566
- ]) {
567
- cleanUrl.searchParams.delete(param);
568
- }
569
- window.history.replaceState({}, document.title, cleanUrl.toString());
570
- }, [acceptTelegramToken]);
785
+ cleanAuthCallbackUrl();
786
+ handleTelegramAuthCallback(callback);
787
+ }, [handleTelegramAuthCallback]);
571
788
  useEffect(() => {
572
789
  function onMessage(event) {
573
790
  if (event.origin !== window.location.origin) return;
574
791
  if (!isTelegramAuthMessage(event.data)) return;
575
- const expectedState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
576
- sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
577
- if (!expectedState || event.data.state !== expectedState) {
578
- setTgError("Invalid auth callback state.");
579
- return;
580
- }
581
- acceptTelegramToken(event.data.token);
792
+ handleTelegramAuthCallback(event.data);
582
793
  }
583
794
  window.addEventListener("message", onMessage);
584
795
  return () => window.removeEventListener("message", onMessage);
585
- }, [acceptTelegramToken]);
796
+ }, [handleTelegramAuthCallback]);
586
797
  useEffect(() => {
587
798
  if (!tgAuthToken || !telegramRpcOptions) return;
588
799
  let cancelled = false;
@@ -1317,22 +1528,16 @@ function HypurrConnectProvider({
1317
1528
  const closeLoginModal = useCallback(() => setLoginModalOpen(false), []);
1318
1529
  const loginTelegram = useCallback(() => {
1319
1530
  const state = randomState();
1320
- sessionStorage.setItem(TELEGRAM_AUTH_STATE_KEY, state);
1531
+ const codeVerifier = randomCodeVerifier();
1321
1532
  const configuredReturnTo = config.telegram?.returnTo;
1322
1533
  const returnTo = typeof configuredReturnTo === "function" ? configuredReturnTo() : configuredReturnTo || currentReturnTo();
1323
- const authUrl = new URL(
1324
- config.telegram?.authHubUrl || DEFAULT_AUTH_HUB_URL
1325
- );
1326
- authUrl.searchParams.set("client_id", normalizeClientId(config.client_id));
1327
- authUrl.searchParams.set("return_to", returnTo);
1328
- authUrl.searchParams.set("state", state);
1329
- authUrl.searchParams.set("scope", normalizeScopes(config.telegram?.scope));
1534
+ storeTelegramAuthSession(state, codeVerifier, returnTo);
1330
1535
  const width = 520;
1331
1536
  const height = 720;
1332
1537
  const left = window.screenX + Math.max(0, (window.outerWidth - width) / 2);
1333
1538
  const top = window.screenY + Math.max(0, (window.outerHeight - height) / 2);
1334
1539
  const popup = window.open(
1335
- authUrl.toString(),
1540
+ "about:blank",
1336
1541
  "hypurr_telegram_auth",
1337
1542
  [
1338
1543
  `width=${width}`,
@@ -1343,13 +1548,40 @@ function HypurrConnectProvider({
1343
1548
  "scrollbars=yes"
1344
1549
  ].join(",")
1345
1550
  );
1346
- if (popup) {
1347
- popup.focus();
1348
- return;
1349
- }
1350
- window.location.assign(authUrl.toString());
1551
+ void (async () => {
1552
+ try {
1553
+ const authUrl = new URL(
1554
+ config.telegram?.authHubUrl || DEFAULT_AUTH_HUB_URL
1555
+ );
1556
+ authUrl.searchParams.set(
1557
+ "client_id",
1558
+ normalizeClientId(config.clientId)
1559
+ );
1560
+ authUrl.searchParams.set("return_to", returnTo);
1561
+ authUrl.searchParams.set("state", state);
1562
+ authUrl.searchParams.set(
1563
+ "scope",
1564
+ normalizeScopes(config.telegram?.scope)
1565
+ );
1566
+ authUrl.searchParams.set(
1567
+ "code_challenge",
1568
+ await createCodeChallenge(codeVerifier)
1569
+ );
1570
+ authUrl.searchParams.set("code_challenge_method", "S256");
1571
+ if (popup) {
1572
+ popup.location.assign(authUrl.toString());
1573
+ popup.focus();
1574
+ return;
1575
+ }
1576
+ window.location.assign(authUrl.toString());
1577
+ } catch (err) {
1578
+ clearTelegramAuthSession(state);
1579
+ if (popup && !popup.closed) popup.close();
1580
+ setTgError(err instanceof Error ? err.message : String(err));
1581
+ }
1582
+ })();
1351
1583
  }, [
1352
- config.client_id,
1584
+ config.clientId,
1353
1585
  config.telegram?.authHubUrl,
1354
1586
  config.telegram?.returnTo,
1355
1587
  config.telegram?.scope