@hfunlabs/hypurr-connect 0.1.22 → 0.1.23

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,11 +44,12 @@ 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: {
51
51
  authHubUrl: "https://auth.hypurr.fun/login",
52
+ // tokenUrl defaults to https://auth.hypurr.fun/token
52
53
  scope: [
53
54
  "telegram:user:read",
54
55
  "telegram:wallet:read",
@@ -119,13 +120,14 @@ function AppShell() {
119
120
 
120
121
  ```typescript
121
122
  interface HypurrConnectConfig {
122
- client_id: string; // Auth hub client identifier
123
+ clientId: string; // Auth hub client identifier
123
124
  grpcUrl?: string; // gRPC-web base URL (default: https://grpc.hypurr.fun)
124
125
  mediaUrl?: string; // Media base URL (default: https://media.hypurr.fun)
125
126
  grpcTimeout?: number; // Request timeout in ms (default: 15000)
126
127
  isTestnet?: boolean; // Use testnet endpoints (default: false)
127
128
  telegram: {
128
129
  authHubUrl?: string; // Auth hub URL (default: https://auth.hypurr.fun/login)
130
+ tokenUrl?: string; // Token exchange URL (default: auth hub /token)
129
131
  returnTo?: string | (() => string); // Callback URL (default: current page)
130
132
  scope?: string | string[]; // Requested JWT scopes
131
133
  };
@@ -134,7 +136,8 @@ interface HypurrConnectConfig {
134
136
 
135
137
  The SDK no longer renders Telegram's login widget or opens `oauth.telegram.org`
136
138
  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.
139
+ popup posts either a legacy scoped JWT or an authorization code back to the
140
+ original page.
138
141
 
139
142
  ### Dependencies
140
143
 
@@ -147,16 +150,19 @@ for generated protobuf service clients.
147
150
 
148
151
  1. User clicks "Telegram" in the `LoginModal`.
149
152
  2. The SDK opens the configured auth hub in a popup with `client_id`,
150
- `return_to`, `state`, and requested `scope`.
153
+ `return_to`, `state`, requested `scope`, `code_challenge`, and
154
+ `code_challenge_method=S256`.
151
155
  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
156
+ either a legacy scoped JWT or an authorization code.
157
+ 4. The popup callback page posts `{ token, state }` or `{ code, state }` to the
158
+ opener with `postMessage` and closes.
159
+ 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.
161
+ 6. The SDK calls the Hypurr gRPC backend with `Authorization: Bearer <jwt>`
162
+ metadata.
163
+ 7. An `ExchangeClient` is created with `GrpcExchangeTransport`; exchange
158
164
  actions are still signed server-side by the Hypurr backend.
159
- 7. The JWT session is persisted in localStorage (`hypurr-connect-tg-jwt`).
165
+ 8. The JWT session is persisted in localStorage (`hypurr-connect-tg-jwt`).
160
166
 
161
167
  If the popup is blocked, the SDK falls back to a full-page redirect. The
162
168
  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. */
@@ -27,6 +27,8 @@ 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;
30
32
  /** Optional callback URL. Defaults to the current page without auth query params. */
31
33
  returnTo?: string | (() => string);
32
34
  /** Requested hub scopes. Defaults to the scopes required by this SDK. */
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,124 @@ 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 resolveAuthTokenUrl(authHubUrl, tokenUrl) {
546
+ const configuredTokenUrl = tokenUrl?.trim();
547
+ if (configuredTokenUrl) return configuredTokenUrl;
548
+ 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`;
552
+ url.search = "";
553
+ url.hash = "";
554
+ return url.toString();
555
+ }
556
+ function getTokenFromExchangeResponse(data) {
557
+ if (typeof data === "string") {
558
+ const token = data.trim();
559
+ return token || null;
560
+ }
561
+ if (typeof data !== "object" || data === null) return null;
562
+ const response = data;
563
+ for (const token of [response.token, response.access_token, response.jwt]) {
564
+ if (typeof token === "string" && token.trim()) return token.trim();
565
+ }
566
+ return null;
567
+ }
568
+ async function exchangeTelegramAuthCode({
569
+ authHubUrl,
570
+ clientId,
571
+ code,
572
+ codeVerifier,
573
+ returnTo,
574
+ tokenUrl
575
+ }) {
576
+ const body = new URLSearchParams({
577
+ client_id: clientId,
578
+ code,
579
+ code_verifier: codeVerifier,
580
+ grant_type: "authorization_code",
581
+ return_to: returnTo
582
+ });
583
+ const response = await fetch(resolveAuthTokenUrl(authHubUrl, tokenUrl), {
584
+ method: "POST",
585
+ headers: {
586
+ accept: "application/json",
587
+ "content-type": "application/x-www-form-urlencoded"
588
+ },
589
+ body
590
+ });
591
+ const responseText = await response.text();
592
+ if (!response.ok) {
593
+ const detail = responseText.trim();
594
+ throw new Error(
595
+ detail ? `[HypurrConnect] Auth code exchange failed: ${detail}` : `[HypurrConnect] Auth code exchange failed with HTTP ${response.status}.`
596
+ );
597
+ }
598
+ let responseData = responseText;
599
+ if (responseText) {
600
+ try {
601
+ responseData = JSON.parse(responseText);
602
+ } catch {
603
+ responseData = responseText;
604
+ }
605
+ }
606
+ const token = getTokenFromExchangeResponse(responseData);
607
+ if (!token) {
608
+ throw new Error("[HypurrConnect] Auth code exchange did not return a JWT.");
609
+ }
610
+ return token;
611
+ }
461
612
  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";
613
+ if (typeof data !== "object" || data === null) return false;
614
+ if (!("type" in data) || !("state" in data)) return false;
615
+ const message = data;
616
+ const hasToken = typeof message.token === "string";
617
+ const hasCode = typeof message.code === "string";
618
+ const hasError = typeof message.error === "string";
619
+ return message.type === TELEGRAM_AUTH_MESSAGE && typeof message.state === "string" && (hasToken || hasCode || hasError);
463
620
  }
464
621
  var HypurrConnectContext = createContext(null);
465
622
  function useHypurrConnect() {
@@ -517,6 +674,49 @@ function HypurrConnectProvider({
517
674
  localStorage.setItem(TELEGRAM_STORAGE_KEY, token);
518
675
  localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
519
676
  }, []);
677
+ const handleTelegramAuthCallback = useCallback(
678
+ (callback) => {
679
+ const authSession = takeTelegramAuthSession(callback.state);
680
+ if (!authSession) {
681
+ setTgError("Invalid auth callback state.");
682
+ return;
683
+ }
684
+ if (callback.error) {
685
+ setTgError(callback.error);
686
+ return;
687
+ }
688
+ if (callback.code) {
689
+ if (!authSession.codeVerifier) {
690
+ setTgError("Missing auth code verifier.");
691
+ return;
692
+ }
693
+ setTgLoading(true);
694
+ setTgError(null);
695
+ void exchangeTelegramAuthCode({
696
+ authHubUrl: config.telegram?.authHubUrl,
697
+ clientId: normalizeClientId(config.clientId),
698
+ code: callback.code,
699
+ codeVerifier: authSession.codeVerifier,
700
+ returnTo: authSession.returnTo || currentReturnTo(),
701
+ tokenUrl: config.telegram?.tokenUrl
702
+ }).then(acceptTelegramToken).catch(
703
+ (err) => setTgError(err instanceof Error ? err.message : String(err))
704
+ ).finally(() => setTgLoading(false));
705
+ return;
706
+ }
707
+ if (callback.token) {
708
+ acceptTelegramToken(callback.token);
709
+ return;
710
+ }
711
+ setTgError("Invalid auth callback.");
712
+ },
713
+ [
714
+ acceptTelegramToken,
715
+ config.clientId,
716
+ config.telegram?.authHubUrl,
717
+ config.telegram?.tokenUrl
718
+ ]
719
+ );
520
720
  useEffect(() => {
521
721
  if (typeof document === "undefined" || !document.fonts) return;
522
722
  for (const face of [
@@ -532,57 +732,37 @@ function HypurrConnectProvider({
532
732
  useEffect(() => {
533
733
  const params = new URLSearchParams(window.location.search);
534
734
  const token = params.get("token");
535
- if (!token) {
735
+ const code = params.get("code");
736
+ const error = params.get("error_description") || params.get("error") || void 0;
737
+ if (!token && !code && !error) {
536
738
  localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
537
739
  return;
538
740
  }
539
741
  const callbackState = params.get("state") ?? "";
742
+ const callback = {
743
+ code: code || void 0,
744
+ error,
745
+ state: callbackState,
746
+ token: token || void 0,
747
+ type: TELEGRAM_AUTH_MESSAGE
748
+ };
540
749
  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
- );
750
+ window.opener.postMessage(callback, window.location.origin);
549
751
  window.close();
550
752
  return;
551
753
  }
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]);
754
+ cleanAuthCallbackUrl();
755
+ handleTelegramAuthCallback(callback);
756
+ }, [handleTelegramAuthCallback]);
571
757
  useEffect(() => {
572
758
  function onMessage(event) {
573
759
  if (event.origin !== window.location.origin) return;
574
760
  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);
761
+ handleTelegramAuthCallback(event.data);
582
762
  }
583
763
  window.addEventListener("message", onMessage);
584
764
  return () => window.removeEventListener("message", onMessage);
585
- }, [acceptTelegramToken]);
765
+ }, [handleTelegramAuthCallback]);
586
766
  useEffect(() => {
587
767
  if (!tgAuthToken || !telegramRpcOptions) return;
588
768
  let cancelled = false;
@@ -1317,22 +1497,16 @@ function HypurrConnectProvider({
1317
1497
  const closeLoginModal = useCallback(() => setLoginModalOpen(false), []);
1318
1498
  const loginTelegram = useCallback(() => {
1319
1499
  const state = randomState();
1320
- sessionStorage.setItem(TELEGRAM_AUTH_STATE_KEY, state);
1500
+ const codeVerifier = randomCodeVerifier();
1321
1501
  const configuredReturnTo = config.telegram?.returnTo;
1322
1502
  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));
1503
+ storeTelegramAuthSession(state, codeVerifier, returnTo);
1330
1504
  const width = 520;
1331
1505
  const height = 720;
1332
1506
  const left = window.screenX + Math.max(0, (window.outerWidth - width) / 2);
1333
1507
  const top = window.screenY + Math.max(0, (window.outerHeight - height) / 2);
1334
1508
  const popup = window.open(
1335
- authUrl.toString(),
1509
+ "about:blank",
1336
1510
  "hypurr_telegram_auth",
1337
1511
  [
1338
1512
  `width=${width}`,
@@ -1343,13 +1517,40 @@ function HypurrConnectProvider({
1343
1517
  "scrollbars=yes"
1344
1518
  ].join(",")
1345
1519
  );
1346
- if (popup) {
1347
- popup.focus();
1348
- return;
1349
- }
1350
- window.location.assign(authUrl.toString());
1520
+ void (async () => {
1521
+ try {
1522
+ const authUrl = new URL(
1523
+ config.telegram?.authHubUrl || DEFAULT_AUTH_HUB_URL
1524
+ );
1525
+ authUrl.searchParams.set(
1526
+ "client_id",
1527
+ normalizeClientId(config.clientId)
1528
+ );
1529
+ authUrl.searchParams.set("return_to", returnTo);
1530
+ authUrl.searchParams.set("state", state);
1531
+ authUrl.searchParams.set(
1532
+ "scope",
1533
+ normalizeScopes(config.telegram?.scope)
1534
+ );
1535
+ authUrl.searchParams.set(
1536
+ "code_challenge",
1537
+ await createCodeChallenge(codeVerifier)
1538
+ );
1539
+ authUrl.searchParams.set("code_challenge_method", "S256");
1540
+ if (popup) {
1541
+ popup.location.assign(authUrl.toString());
1542
+ popup.focus();
1543
+ return;
1544
+ }
1545
+ window.location.assign(authUrl.toString());
1546
+ } catch (err) {
1547
+ clearTelegramAuthSession(state);
1548
+ if (popup && !popup.closed) popup.close();
1549
+ setTgError(err instanceof Error ? err.message : String(err));
1550
+ }
1551
+ })();
1351
1552
  }, [
1352
- config.client_id,
1553
+ config.clientId,
1353
1554
  config.telegram?.authHubUrl,
1354
1555
  config.telegram?.returnTo,
1355
1556
  config.telegram?.scope