@hfunlabs/hypurr-connect 0.1.21 → 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,10 +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
+ clientId: "your-client-id",
47
48
  isTestnet: false,
48
49
  grpcUrl: "https://grpc.hypurr.fun",
49
50
  telegram: {
50
51
  authHubUrl: "https://auth.hypurr.fun/login",
52
+ // tokenUrl defaults to https://auth.hypurr.fun/token
51
53
  scope: [
52
54
  "telegram:user:read",
53
55
  "telegram:wallet:read",
@@ -118,12 +120,14 @@ function AppShell() {
118
120
 
119
121
  ```typescript
120
122
  interface HypurrConnectConfig {
123
+ clientId: string; // Auth hub client identifier
121
124
  grpcUrl?: string; // gRPC-web base URL (default: https://grpc.hypurr.fun)
122
125
  mediaUrl?: string; // Media base URL (default: https://media.hypurr.fun)
123
126
  grpcTimeout?: number; // Request timeout in ms (default: 15000)
124
127
  isTestnet?: boolean; // Use testnet endpoints (default: false)
125
128
  telegram: {
126
129
  authHubUrl?: string; // Auth hub URL (default: https://auth.hypurr.fun/login)
130
+ tokenUrl?: string; // Token exchange URL (default: auth hub /token)
127
131
  returnTo?: string | (() => string); // Callback URL (default: current page)
128
132
  scope?: string | string[]; // Requested JWT scopes
129
133
  };
@@ -132,7 +136,8 @@ interface HypurrConnectConfig {
132
136
 
133
137
  The SDK no longer renders Telegram's login widget or opens `oauth.telegram.org`
134
138
  directly. Telegram login is delegated to the Hypurr auth hub in a popup, and the
135
- 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.
136
141
 
137
142
  ### Dependencies
138
143
 
@@ -144,17 +149,20 @@ for generated protobuf service clients.
144
149
  ### Telegram Login
145
150
 
146
151
  1. User clicks "Telegram" in the `LoginModal`.
147
- 2. The SDK opens the configured auth hub in a popup with `return_to`, `state`,
148
- and requested `scope`.
152
+ 2. The SDK opens the configured auth hub in a popup with `client_id`,
153
+ `return_to`, `state`, requested `scope`, `code_challenge`, and
154
+ `code_challenge_method=S256`.
149
155
  3. The auth hub performs Telegram login and redirects back to `return_to` with
150
- a scoped JWT.
151
- 4. The popup callback page posts `{ token, state }` to the opener with
152
- `postMessage` and closes.
153
- 5. The opener validates `state`, stores the JWT, and calls the Hypurr gRPC
154
- backend with `Authorization: Bearer <jwt>` metadata.
155
- 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
156
164
  actions are still signed server-side by the Hypurr backend.
157
- 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`).
158
166
 
159
167
  If the popup is blocked, the SDK falls back to a full-page redirect. The
160
168
  default `returnTo` is the current page with auth query params removed; custom
@@ -579,8 +587,8 @@ Creates a gRPC-Web client for the Telegram service.
579
587
 
580
588
  Creates a gRPC-Web client for the Static service.
581
589
 
582
- Both use `GrpcWebFetchTransport` with `config.grpcUrl` as `baseUrl` and
583
- `config.grpcTimeout` as `timeout`.
590
+ Both use `GrpcWebFetchTransport` with `config.grpcUrl` or the default gRPC URL
591
+ as `baseUrl`, and `config.grpcTimeout` as `timeout`.
584
592
 
585
593
  ## localStorage Keys
586
594
 
package/dist/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export { HyperliquidWallet } from 'hypurr-grpc/ts/hypurr/wallet';
14
14
  import { AbstractViemLocalAccount } from '@hfunlabs/hyperliquid/signing';
15
15
 
16
16
  interface HypurrConnectConfig {
17
+ /** Auth hub client identifier. Sent as `client_id` during Telegram login. */
18
+ clientId: string;
17
19
  /** gRPC-web base URL. Defaults to https://grpc.hypurr.fun. */
18
20
  grpcUrl?: string;
19
21
  /** Media base URL for user-uploaded assets. Defaults to https://media.hypurr.fun. */
@@ -25,6 +27,8 @@ interface HypurrConnectConfig {
25
27
  telegram?: {
26
28
  /** Auth hub login URL. Defaults to https://auth.hypurr.fun/login. */
27
29
  authHubUrl?: string;
30
+ /** Auth hub token exchange URL. Defaults to the auth hub login URL with `/login` replaced by `/token`. */
31
+ tokenUrl?: string;
28
32
  /** Optional callback URL. Defaults to the current page without auth query params. */
29
33
  returnTo?: string | (() => string);
30
34
  /** Requested hub scopes. Defaults to the scopes required by this SDK. */
package/dist/index.js CHANGED
@@ -140,9 +140,12 @@ import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
140
140
  import { StaticClient } from "hypurr-grpc/ts/hypurr/static/static_service.client";
141
141
  import { TelegramClient } from "hypurr-grpc/ts/hypurr/telegram/telegram_service.client";
142
142
  var DEFAULT_GRPC_URL = "https://grpc.hypurr.fun";
143
+ function resolveGrpcUrl(config) {
144
+ return config.grpcUrl?.trim() || DEFAULT_GRPC_URL;
145
+ }
143
146
  function createTransport(config) {
144
147
  return new GrpcWebFetchTransport({
145
- baseUrl: config.grpcUrl ?? DEFAULT_GRPC_URL,
148
+ baseUrl: resolveGrpcUrl(config),
146
149
  timeout: config.grpcTimeout ?? 15e3
147
150
  });
148
151
  }
@@ -292,6 +295,8 @@ import { jsx } from "react/jsx-runtime";
292
295
  var TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-jwt";
293
296
  var LEGACY_TELEGRAM_STORAGE_KEY = "hypurr-connect-tg-user";
294
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:";
295
300
  var TELEGRAM_AUTH_MESSAGE = "hypurr-connect:telegram-auth";
296
301
  var DEFAULT_AUTH_HUB_URL = "https://auth.hypurr.fun/login";
297
302
  var DEFAULT_MEDIA_URL = "https://media.hypurr.fun";
@@ -427,6 +432,9 @@ function withExpectedFrom(transaction, address) {
427
432
  function currentReturnTo() {
428
433
  const url = new URL(window.location.href);
429
434
  for (const param of [
435
+ "code",
436
+ "error",
437
+ "error_description",
430
438
  "token",
431
439
  "token_type",
432
440
  "token_source",
@@ -437,6 +445,29 @@ function currentReturnTo() {
437
445
  }
438
446
  return url.toString();
439
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
+ }
440
471
  function randomState() {
441
472
  const bytes = new Uint8Array(16);
442
473
  crypto.getRandomValues(bytes);
@@ -444,12 +475,148 @@ function randomState() {
444
475
  ""
445
476
  );
446
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
+ }
447
495
  function normalizeScopes(scope) {
448
496
  if (Array.isArray(scope)) return scope.join(" ");
449
497
  return scope?.trim() || DEFAULT_TELEGRAM_SCOPES.join(" ");
450
498
  }
499
+ function normalizeClientId(clientId) {
500
+ const normalized = typeof clientId === "string" ? clientId.trim() : "";
501
+ if (!normalized) {
502
+ throw new Error("[HypurrConnect] config.clientId is required.");
503
+ }
504
+ return normalized;
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
+ }
451
612
  function isTelegramAuthMessage(data) {
452
- 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);
453
620
  }
454
621
  var HypurrConnectContext = createContext(null);
455
622
  function useHypurrConnect() {
@@ -507,6 +674,49 @@ function HypurrConnectProvider({
507
674
  localStorage.setItem(TELEGRAM_STORAGE_KEY, token);
508
675
  localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
509
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
+ );
510
720
  useEffect(() => {
511
721
  if (typeof document === "undefined" || !document.fonts) return;
512
722
  for (const face of [
@@ -522,57 +732,37 @@ function HypurrConnectProvider({
522
732
  useEffect(() => {
523
733
  const params = new URLSearchParams(window.location.search);
524
734
  const token = params.get("token");
525
- 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) {
526
738
  localStorage.removeItem(LEGACY_TELEGRAM_STORAGE_KEY);
527
739
  return;
528
740
  }
529
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
+ };
530
749
  if (window.opener && window.opener !== window) {
531
- window.opener.postMessage(
532
- {
533
- type: TELEGRAM_AUTH_MESSAGE,
534
- token,
535
- state: callbackState
536
- },
537
- window.location.origin
538
- );
750
+ window.opener.postMessage(callback, window.location.origin);
539
751
  window.close();
540
752
  return;
541
753
  }
542
- const expectedState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
543
- sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
544
- if (!expectedState || callbackState !== expectedState) {
545
- setTgError("Invalid auth callback state.");
546
- return;
547
- }
548
- acceptTelegramToken(token);
549
- const cleanUrl = new URL(window.location.href);
550
- for (const param of [
551
- "token",
552
- "token_type",
553
- "token_source",
554
- "state",
555
- "scope"
556
- ]) {
557
- cleanUrl.searchParams.delete(param);
558
- }
559
- window.history.replaceState({}, document.title, cleanUrl.toString());
560
- }, [acceptTelegramToken]);
754
+ cleanAuthCallbackUrl();
755
+ handleTelegramAuthCallback(callback);
756
+ }, [handleTelegramAuthCallback]);
561
757
  useEffect(() => {
562
758
  function onMessage(event) {
563
759
  if (event.origin !== window.location.origin) return;
564
760
  if (!isTelegramAuthMessage(event.data)) return;
565
- const expectedState = sessionStorage.getItem(TELEGRAM_AUTH_STATE_KEY);
566
- sessionStorage.removeItem(TELEGRAM_AUTH_STATE_KEY);
567
- if (!expectedState || event.data.state !== expectedState) {
568
- setTgError("Invalid auth callback state.");
569
- return;
570
- }
571
- acceptTelegramToken(event.data.token);
761
+ handleTelegramAuthCallback(event.data);
572
762
  }
573
763
  window.addEventListener("message", onMessage);
574
764
  return () => window.removeEventListener("message", onMessage);
575
- }, [acceptTelegramToken]);
765
+ }, [handleTelegramAuthCallback]);
576
766
  useEffect(() => {
577
767
  if (!tgAuthToken || !telegramRpcOptions) return;
578
768
  let cancelled = false;
@@ -1307,21 +1497,16 @@ function HypurrConnectProvider({
1307
1497
  const closeLoginModal = useCallback(() => setLoginModalOpen(false), []);
1308
1498
  const loginTelegram = useCallback(() => {
1309
1499
  const state = randomState();
1310
- sessionStorage.setItem(TELEGRAM_AUTH_STATE_KEY, state);
1500
+ const codeVerifier = randomCodeVerifier();
1311
1501
  const configuredReturnTo = config.telegram?.returnTo;
1312
1502
  const returnTo = typeof configuredReturnTo === "function" ? configuredReturnTo() : configuredReturnTo || currentReturnTo();
1313
- const authUrl = new URL(
1314
- config.telegram?.authHubUrl || DEFAULT_AUTH_HUB_URL
1315
- );
1316
- authUrl.searchParams.set("return_to", returnTo);
1317
- authUrl.searchParams.set("state", state);
1318
- authUrl.searchParams.set("scope", normalizeScopes(config.telegram?.scope));
1503
+ storeTelegramAuthSession(state, codeVerifier, returnTo);
1319
1504
  const width = 520;
1320
1505
  const height = 720;
1321
1506
  const left = window.screenX + Math.max(0, (window.outerWidth - width) / 2);
1322
1507
  const top = window.screenY + Math.max(0, (window.outerHeight - height) / 2);
1323
1508
  const popup = window.open(
1324
- authUrl.toString(),
1509
+ "about:blank",
1325
1510
  "hypurr_telegram_auth",
1326
1511
  [
1327
1512
  `width=${width}`,
@@ -1332,12 +1517,40 @@ function HypurrConnectProvider({
1332
1517
  "scrollbars=yes"
1333
1518
  ].join(",")
1334
1519
  );
1335
- if (popup) {
1336
- popup.focus();
1337
- return;
1338
- }
1339
- 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
+ })();
1340
1552
  }, [
1553
+ config.clientId,
1341
1554
  config.telegram?.authHubUrl,
1342
1555
  config.telegram?.returnTo,
1343
1556
  config.telegram?.scope