@tenxyte/core 0.9.3 → 0.9.4

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
@@ -88,6 +88,10 @@ const tx = new TenxyteClient({
88
88
 
89
89
  // Optional — override auto-detected device info
90
90
  deviceInfoOverride: { app_name: 'MyApp', app_version: '2.0.0' },
91
+
92
+ // Optional — cookie-based refresh token transport (default: false)
93
+ // Enable when backend has TENXYTE_REFRESH_TOKEN_COOKIE_ENABLED=True
94
+ cookieMode: false,
91
95
  });
92
96
  ```
93
97
 
@@ -128,9 +132,16 @@ const tokens = await tx.auth.verifyMagicLink(urlToken);
128
132
 
129
133
  // Social OAuth2
130
134
  const tokens = await tx.auth.loginWithSocial('google', { id_token: 'jwt...' });
131
- const tokens = await tx.auth.handleSocialCallback('github', 'auth_code', 'https://myapp.com/cb');
132
135
 
133
- // Session management
136
+ // Social OAuth2 with PKCE (RFC 7636)
137
+ const tokens = await tx.auth.loginWithSocial('google', {
138
+ code: 'auth_code',
139
+ redirect_uri: 'https://myapp.com/cb',
140
+ code_verifier: 'pkce_verifier_string',
141
+ });
142
+ const tokens = await tx.auth.handleSocialCallback('github', 'auth_code', 'https://myapp.com/cb', 'pkce_verifier');
143
+
144
+ // Session management (refreshToken param is optional in cookie mode)
134
145
  await tx.auth.logout('refresh_token_value');
135
146
  await tx.auth.logoutAll();
136
147
  await tx.auth.refreshToken('refresh_token_value');
@@ -384,6 +395,39 @@ const state = await tx.getState();
384
395
 
385
396
  ---
386
397
 
398
+ ## Migration Guide: v0.9 → v0.10
399
+
400
+ ### New Features in v0.10
401
+
402
+ - **Cookie-based refresh tokens** — New `cookieMode` config option. When enabled, the SDK uses `credentials: 'include'` for refresh/logout requests and does not require a stored refresh token for silent refresh.
403
+ - **PKCE support** — `code_verifier` parameter added to `SocialLoginRequest` and `handleSocialCallback()` for RFC 7636 compliance.
404
+ - **Expanded error codes** — `TenxyteErrorCode` now includes all backend error codes: `MISSING_REFRESH_TOKEN`, `INVALID_REDIRECT_URI`, `PASSWORD_BREACHED`, `PASSWORD_REUSED`, `WEBAUTHN_*`, `LINK_EXPIRED`, `2FA_ALREADY_ENABLED`, and more.
405
+ - **Optional refresh token in responses** — `TokenPair.refresh_token` is now optional (absent when cookie mode is enabled on the backend).
406
+
407
+ ### Breaking Changes
408
+
409
+ 1. **`TokenPair.refresh_token` is now optional** — If you access `tokens.refresh_token` without a null check, add one:
410
+ ```typescript
411
+ if (tokens.refresh_token) {
412
+ // Store or use the refresh token
413
+ }
414
+ ```
415
+
416
+ 2. **`logout()` and `refreshToken()` parameters are now optional** — In cookie mode, you can call them without arguments:
417
+ ```typescript
418
+ // Cookie mode (refresh token is in HttpOnly cookie)
419
+ await tx.auth.logout();
420
+ await tx.auth.refreshToken();
421
+
422
+ // Classic mode (still works)
423
+ await tx.auth.logout('refresh_token_value');
424
+ await tx.auth.refreshToken('refresh_token_value');
425
+ ```
426
+
427
+ 3. **`handleSocialCallback()` now accepts an optional 4th parameter** (`codeVerifier`).
428
+
429
+ ---
430
+
387
431
  ## Migration Guide: v0.8 → v0.9
388
432
 
389
433
  ### Breaking Changes
@@ -436,6 +480,7 @@ const state = await tx.getState();
436
480
  - High-level helpers (`isAuthenticated`, `getCurrentUser`, `isTokenExpired`)
437
481
  - `getState()` for framework wrapper integration
438
482
  - EventEmitter for reactive state (`session:expired`, `token:refreshed`, etc.)
483
+ - WebAuthn / Passkeys (FIDO2) support
439
484
 
440
485
  ---
441
486
 
package/dist/index.cjs CHANGED
@@ -174,7 +174,8 @@ function resolveConfig(config) {
174
174
  logger: config.logger ?? NOOP_LOGGER,
175
175
  logLevel: config.logLevel ?? "silent",
176
176
  deviceInfoOverride: config.deviceInfoOverride,
177
- retryConfig: config.retryConfig
177
+ retryConfig: config.retryConfig,
178
+ cookieMode: config.cookieMode ?? false
178
179
  };
179
180
  }
180
181
 
@@ -396,7 +397,7 @@ function createAuthInterceptor(storage, context) {
396
397
  return { ...request, headers };
397
398
  };
398
399
  }
399
- function createRefreshInterceptor(client, storage, onSessionExpired, onTokenRefreshed) {
400
+ function createRefreshInterceptor(client, storage, onSessionExpired, onTokenRefreshed, cookieMode = false) {
400
401
  let isRefreshing = false;
401
402
  let refreshQueue = [];
402
403
  const processQueue = (error, token = null) => {
@@ -406,7 +407,7 @@ function createRefreshInterceptor(client, storage, onSessionExpired, onTokenRefr
406
407
  return async (response, request) => {
407
408
  if (response.status === 401 && !request.url.includes("/auth/refresh") && !request.url.includes("/auth/login")) {
408
409
  const refreshToken = await storage.getItem("tx_refresh");
409
- if (!refreshToken) {
410
+ if (!refreshToken && !cookieMode) {
410
411
  onSessionExpired();
411
412
  return response;
412
413
  }
@@ -424,10 +425,15 @@ function createRefreshInterceptor(client, storage, onSessionExpired, onTokenRefr
424
425
  }
425
426
  isRefreshing = true;
426
427
  try {
428
+ const refreshBody = {};
429
+ if (refreshToken) {
430
+ refreshBody.refresh_token = refreshToken;
431
+ }
427
432
  const refreshResponse = await fetch(`${client["baseUrl"]}/auth/refresh/`, {
428
433
  method: "POST",
429
434
  headers: { "Content-Type": "application/json" },
430
- body: JSON.stringify({ refresh_token: refreshToken })
435
+ body: JSON.stringify(refreshBody),
436
+ ...cookieMode ? { credentials: "include" } : {}
431
437
  });
432
438
  if (!refreshResponse.ok) {
433
439
  throw new Error("Refresh failed");
@@ -585,10 +591,16 @@ var AuthModule = class {
585
591
  /**
586
592
  * Logout from the current session.
587
593
  * Informs the backend to immediately revoke the specified refresh token.
588
- * @param refreshToken - The refresh token to revoke.
594
+ * When cookie mode is enabled, the refresh token parameter is optional —
595
+ * the server reads it from the HttpOnly cookie and clears it via Set-Cookie.
596
+ * @param refreshToken - The refresh token to revoke (optional in cookie mode).
589
597
  */
590
598
  async logout(refreshToken) {
591
- await this.client.post("/api/v1/auth/logout/", { refresh_token: refreshToken });
599
+ const body = {};
600
+ if (refreshToken) {
601
+ body.refresh_token = refreshToken;
602
+ }
603
+ await this.client.post("/api/v1/auth/logout/", body);
592
604
  await this.clearTokens();
593
605
  }
594
606
  /**
@@ -602,11 +614,17 @@ var AuthModule = class {
602
614
  /**
603
615
  * Manually refresh the access token using a valid refresh token.
604
616
  * The refresh token is automatically rotated for improved security.
605
- * @param refreshToken - The current refresh token.
617
+ * When cookie mode is enabled, the refresh token parameter is optional —
618
+ * the server reads it from the HttpOnly cookie.
619
+ * @param refreshToken - The current refresh token (optional in cookie mode).
606
620
  * @returns A new token pair (access + rotated refresh).
607
621
  */
608
622
  async refreshToken(refreshToken) {
609
- const tokens = await this.client.post("/api/v1/auth/refresh/", { refresh_token: refreshToken });
623
+ const body = {};
624
+ if (refreshToken) {
625
+ body.refresh_token = refreshToken;
626
+ }
627
+ const tokens = await this.client.post("/api/v1/auth/refresh/", body);
610
628
  return this.persistTokens(tokens);
611
629
  }
612
630
  /**
@@ -641,11 +659,16 @@ var AuthModule = class {
641
659
  * @param provider - The OAuth provider ('google', 'github', etc.)
642
660
  * @param code - The authorization code retrieved from the query string parameters.
643
661
  * @param redirectUri - The original redirect URI that was requested.
662
+ * @param codeVerifier - Optional PKCE code verifier (RFC 7636).
644
663
  * @returns An active session token pair after successful code exchange.
645
664
  */
646
- async handleSocialCallback(provider, code, redirectUri) {
665
+ async handleSocialCallback(provider, code, redirectUri, codeVerifier) {
666
+ const params = { code, redirect_uri: redirectUri };
667
+ if (codeVerifier) {
668
+ params.code_verifier = codeVerifier;
669
+ }
647
670
  const tokens = await this.client.get(`/api/v1/auth/social/${provider}/callback/`, {
648
- params: { code, redirect_uri: redirectUri }
671
+ params
649
672
  });
650
673
  return this.persistTokens(tokens);
651
674
  }
@@ -1748,7 +1771,8 @@ var TenxyteClient = class {
1748
1771
  this.rbac.setToken(accessToken);
1749
1772
  this.emit("token:refreshed", { accessToken });
1750
1773
  this.emit("token:stored", { accessToken, refreshToken });
1751
- }
1774
+ },
1775
+ this.config.cookieMode
1752
1776
  )
1753
1777
  );
1754
1778
  }