@stackframe/react 2.8.47 → 2.8.49

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/esm/integrations/convex.js +6 -0
  3. package/dist/esm/integrations/convex.js.map +1 -1
  4. package/dist/esm/lib/cookie.js +36 -7
  5. package/dist/esm/lib/cookie.js.map +1 -1
  6. package/dist/esm/lib/stack-app/apps/implementations/admin-app-impl.js +10 -0
  7. package/dist/esm/lib/stack-app/apps/implementations/admin-app-impl.js.map +1 -1
  8. package/dist/esm/lib/stack-app/apps/implementations/client-app-impl.js +221 -26
  9. package/dist/esm/lib/stack-app/apps/implementations/client-app-impl.js.map +1 -1
  10. package/dist/esm/lib/stack-app/apps/implementations/common.js +1 -1
  11. package/dist/esm/lib/stack-app/apps/implementations/common.js.map +1 -1
  12. package/dist/esm/lib/stack-app/apps/implementations/server-app-impl.js +1 -1
  13. package/dist/esm/lib/stack-app/apps/implementations/server-app-impl.js.map +1 -1
  14. package/dist/esm/lib/stack-app/apps/interfaces/admin-app.js.map +1 -1
  15. package/dist/esm/lib/stack-app/apps/interfaces/client-app.js.map +1 -1
  16. package/dist/index.d.mts +10 -1
  17. package/dist/index.d.ts +10 -1
  18. package/dist/integrations/convex.js +6 -0
  19. package/dist/integrations/convex.js.map +1 -1
  20. package/dist/lib/cookie.js +38 -7
  21. package/dist/lib/cookie.js.map +1 -1
  22. package/dist/lib/stack-app/apps/implementations/admin-app-impl.js +10 -0
  23. package/dist/lib/stack-app/apps/implementations/admin-app-impl.js.map +1 -1
  24. package/dist/lib/stack-app/apps/implementations/client-app-impl.js +220 -25
  25. package/dist/lib/stack-app/apps/implementations/client-app-impl.js.map +1 -1
  26. package/dist/lib/stack-app/apps/implementations/common.js +1 -1
  27. package/dist/lib/stack-app/apps/implementations/common.js.map +1 -1
  28. package/dist/lib/stack-app/apps/implementations/server-app-impl.js +1 -1
  29. package/dist/lib/stack-app/apps/implementations/server-app-impl.js.map +1 -1
  30. package/dist/lib/stack-app/apps/interfaces/admin-app.js.map +1 -1
  31. package/dist/lib/stack-app/apps/interfaces/client-app.js.map +1 -1
  32. package/package.json +3 -3
@@ -2,6 +2,7 @@
2
2
  import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
3
3
  import { KnownErrors, StackClientInterface } from "@stackframe/stack-shared";
4
4
  import { InternalSession } from "@stackframe/stack-shared/dist/sessions";
5
+ import { encodeBase32 } from "@stackframe/stack-shared/dist/utils/bytes";
5
6
  import { isBrowserLike } from "@stackframe/stack-shared/dist/utils/env";
6
7
  import { StackAssertionError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
7
8
  import { DependenciesMap } from "@stackframe/stack-shared/dist/utils/maps";
@@ -17,7 +18,7 @@ import * as cookie from "cookie";
17
18
  import React, { useCallback, useMemo } from "react";
18
19
  import { constructRedirectUrl } from "../../../../utils/url.js";
19
20
  import { addNewOAuthProviderOrScope, callOAuthCallback, signInWithOAuth } from "../../../auth.js";
20
- import { createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookieClient, getCookieClient, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie.js";
21
+ import { createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookieClient, isSecure as isSecureCookieContext, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie.js";
21
22
  import { apiKeyCreationOptionsToCrud } from "../../api-keys/index.js";
22
23
  import { stackAppInternalsSymbol } from "../../common.js";
23
24
  import { contactChannelCreateOptionsToCrud, contactChannelUpdateOptionsToCrud } from "../../contact-channels/index.js";
@@ -25,6 +26,7 @@ import { adminProjectCreateOptionsToCrud } from "../../projects/index.js";
25
26
  import { teamCreateOptionsToCrud, teamUpdateOptionsToCrud } from "../../teams/index.js";
26
27
  import { attachUserDestructureGuard, userUpdateOptionsToCrud } from "../../users/index.js";
27
28
  import { clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, resolveConstructorOptions } from "./common.js";
29
+ import { parseJson } from "@stackframe/stack-shared/dist/utils/json";
28
30
  import { useAsyncCache } from "./common.js";
29
31
  var isReactServer = false;
30
32
  var process = globalThis.process ?? { env: {} };
@@ -175,11 +177,15 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
175
177
  this._convexPartialUserCache = createCache(
176
178
  async ([ctx]) => await this._getPartialUserFromConvex(ctx)
177
179
  );
180
+ this._trustedParentDomainCache = createCache(
181
+ async ([domain]) => await this._getTrustedParentDomain(domain)
182
+ );
178
183
  this._anonymousSignUpInProgress = null;
179
184
  this._memoryTokenStore = createEmptyTokenStore();
180
185
  this._nextServerCookiesTokenStores = /* @__PURE__ */ new WeakMap();
181
186
  this._requestTokenStores = /* @__PURE__ */ new WeakMap();
182
187
  this._storedBrowserCookieTokenStore = null;
188
+ this._mostRecentQueuedCookieRefreshIndex = 0;
183
189
  /**
184
190
  * A map from token stores and session keys to sessions.
185
191
  *
@@ -302,13 +308,90 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
302
308
  runAsynchronously(this._checkFeatureSupport(name, options));
303
309
  throw new StackAssertionError(`${name} is not currently supported. Please reach out to Stack support for more information.`);
304
310
  }
311
+ get _legacyRefreshTokenCookieName() {
312
+ return `stack-refresh-${this.projectId}`;
313
+ }
305
314
  get _refreshTokenCookieName() {
306
315
  return `stack-refresh-${this.projectId}`;
307
316
  }
317
+ _getRefreshTokenDefaultCookieNameForSecure(secure) {
318
+ return `${secure ? "__Host-" : ""}${this._refreshTokenCookieName}--default`;
319
+ }
320
+ _getCustomRefreshCookieName(domain) {
321
+ const encoded = encodeBase32(new TextEncoder().encode(domain.toLowerCase()));
322
+ return `${this._refreshTokenCookieName}--custom-${encoded}`;
323
+ }
324
+ _formatRefreshCookieValue(refreshToken, updatedAt) {
325
+ return JSON.stringify({
326
+ refresh_token: refreshToken,
327
+ updated_at_millis: updatedAt
328
+ });
329
+ }
330
+ _formatAccessCookieValue(refreshToken, accessToken) {
331
+ return refreshToken && accessToken ? JSON.stringify([refreshToken, accessToken]) : null;
332
+ }
333
+ _parseStructuredRefreshCookie(value) {
334
+ if (!value) {
335
+ return null;
336
+ }
337
+ const parsed = parseJson(value);
338
+ if (parsed.status !== "ok" || typeof parsed.data !== "object" || parsed.data === null) {
339
+ console.warn("Failed to parse structured refresh cookie");
340
+ return null;
341
+ }
342
+ const data = parsed.data;
343
+ const refreshToken = "refresh_token" in data && typeof data.refresh_token === "string" ? data.refresh_token : null;
344
+ const updatedAt = "updated_at_millis" in data && typeof data.updated_at_millis === "number" ? data.updated_at_millis : null;
345
+ if (!refreshToken) {
346
+ console.warn("Refresh token not found in structured refresh cookie");
347
+ return null;
348
+ }
349
+ return {
350
+ refreshToken,
351
+ updatedAt
352
+ };
353
+ }
354
+ _extractRefreshTokenFromCookieMap(cookies) {
355
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
356
+ for (const name of legacyNames) {
357
+ const value = cookies[name];
358
+ if (value) {
359
+ return { refreshToken: value, updatedAt: null };
360
+ }
361
+ }
362
+ let selected = null;
363
+ for (const [name, value] of Object.entries(cookies)) {
364
+ if (!structuredPrefixes.some((prefix) => name.startsWith(prefix))) continue;
365
+ const parsed = this._parseStructuredRefreshCookie(value);
366
+ if (!parsed) continue;
367
+ const candidateUpdatedAt = parsed.updatedAt ?? Number.NEGATIVE_INFINITY;
368
+ const selectedUpdatedAt = selected?.updatedAt ?? Number.NEGATIVE_INFINITY;
369
+ if (!selected || candidateUpdatedAt > selectedUpdatedAt) {
370
+ selected = parsed;
371
+ }
372
+ }
373
+ if (!selected) {
374
+ return { refreshToken: null, updatedAt: null };
375
+ }
376
+ return {
377
+ refreshToken: selected.refreshToken,
378
+ updatedAt: selected.updatedAt ?? null
379
+ };
380
+ }
308
381
  _getTokensFromCookies(cookies) {
309
- const refreshToken = cookies.refreshTokenCookie;
310
- const accessTokenObject = cookies.accessTokenCookie?.startsWith('["') ? JSON.parse(cookies.accessTokenCookie) : null;
311
- const accessToken = accessTokenObject && refreshToken === accessTokenObject[0] ? accessTokenObject[1] : null;
382
+ const { refreshToken } = this._extractRefreshTokenFromCookieMap(cookies);
383
+ const accessTokenCookie = cookies[this._accessTokenCookieName] ?? null;
384
+ let accessToken = null;
385
+ if (accessTokenCookie && accessTokenCookie.startsWith('["')) {
386
+ const parsed = parseJson(accessTokenCookie);
387
+ if (parsed.status === "ok" && typeof parsed.data === "object" && parsed.data !== null && Array.isArray(parsed.data) && parsed.data.length === 2 && typeof parsed.data[0] === "string" && typeof parsed.data[1] === "string") {
388
+ if (parsed.data[0] === refreshToken) {
389
+ accessToken = parsed.data[1];
390
+ }
391
+ } else {
392
+ console.warn("Access token cookie has invalid format");
393
+ }
394
+ }
312
395
  return {
313
396
  refreshToken,
314
397
  accessToken
@@ -317,17 +400,97 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
317
400
  get _accessTokenCookieName() {
318
401
  return `stack-access`;
319
402
  }
403
+ _getAllBrowserCookies() {
404
+ if (!isBrowserLike()) {
405
+ throw new StackAssertionError("Cannot get browser cookies on the server!");
406
+ }
407
+ return cookie.parse(document.cookie || "");
408
+ }
409
+ _getRefreshTokenCookieNamePatterns() {
410
+ return {
411
+ legacyNames: [this._legacyRefreshTokenCookieName, "stack-refresh"],
412
+ structuredPrefixes: [
413
+ `${this._refreshTokenCookieName}--`,
414
+ `__Host-${this._refreshTokenCookieName}--`
415
+ ]
416
+ };
417
+ }
418
+ _collectRefreshTokenCookieNames(cookies) {
419
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
420
+ const names = /* @__PURE__ */ new Set();
421
+ for (const name of legacyNames) {
422
+ if (cookies[name]) {
423
+ names.add(name);
424
+ }
425
+ }
426
+ for (const name of Object.keys(cookies)) {
427
+ if (structuredPrefixes.some((prefix) => name.startsWith(prefix))) {
428
+ names.add(name);
429
+ }
430
+ }
431
+ return names;
432
+ }
433
+ _prepareRefreshCookieUpdate(existingCookies, refreshToken, accessToken, defaultCookieName) {
434
+ const cookieNames = this._collectRefreshTokenCookieNames(existingCookies);
435
+ cookieNames.delete(defaultCookieName);
436
+ const updatedAt = refreshToken ? Date.now() : null;
437
+ const refreshCookieValue = refreshToken && updatedAt !== null ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
438
+ const accessTokenPayload = this._formatAccessCookieValue(refreshToken, accessToken);
439
+ return {
440
+ updatedAt,
441
+ refreshCookieValue,
442
+ accessTokenPayload,
443
+ cookieNamesToDelete: [...cookieNames]
444
+ };
445
+ }
446
+ _queueCustomRefreshCookieUpdate(refreshToken, updatedAt, context) {
447
+ runAsynchronously(async () => {
448
+ this._mostRecentQueuedCookieRefreshIndex++;
449
+ const updateIndex = this._mostRecentQueuedCookieRefreshIndex;
450
+ let hostname;
451
+ if (isBrowserLike()) {
452
+ hostname = window.location.hostname;
453
+ }
454
+ if (!hostname) {
455
+ return;
456
+ }
457
+ const domain = await this._trustedParentDomainCache.getOrWait([hostname], "read-write");
458
+ const setCookie = async (targetDomain, value2) => {
459
+ const name = this._getCustomRefreshCookieName(targetDomain);
460
+ const options = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true };
461
+ if (context === "browser") {
462
+ setOrDeleteCookieClient(name, value2, options);
463
+ } else {
464
+ await setOrDeleteCookie(name, value2, options);
465
+ }
466
+ };
467
+ if (domain.status === "error" || !domain.data || updateIndex !== this._mostRecentQueuedCookieRefreshIndex) {
468
+ return;
469
+ }
470
+ const value = refreshToken && updatedAt ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
471
+ await setCookie(domain.data, value);
472
+ });
473
+ }
474
+ async _getTrustedParentDomain(currentDomain) {
475
+ const project = Result.orThrow(await this._interface.getClientProject());
476
+ const domains = project.config.domains.map((d) => d.domain.trim().replace(/^https?:\/\//, "").split("/")[0]?.toLowerCase());
477
+ const trustedWildcards = domains.filter((d) => d.startsWith("**."));
478
+ const parts = currentDomain.split(".");
479
+ for (let i = parts.length - 2; i >= 0; i--) {
480
+ const parentDomain = parts.slice(i).join(".");
481
+ if (domains.includes(parentDomain) && trustedWildcards.includes("**." + parentDomain)) {
482
+ return parentDomain;
483
+ }
484
+ }
485
+ return null;
486
+ }
320
487
  _getBrowserCookieTokenStore() {
321
488
  if (!isBrowserLike()) {
322
489
  throw new Error("Cannot use cookie token store on the server!");
323
490
  }
324
491
  if (this._storedBrowserCookieTokenStore === null) {
325
492
  const getCurrentValue = (old) => {
326
- const tokens = this._getTokensFromCookies({
327
- refreshTokenCookie: getCookieClient(this._refreshTokenCookieName) ?? getCookieClient("stack-refresh"),
328
- // keep old cookie name for backwards-compatibility
329
- accessTokenCookie: getCookieClient(this._accessTokenCookieName)
330
- });
493
+ const tokens = this._getTokensFromCookies(this._getAllBrowserCookies());
331
494
  return {
332
495
  refreshToken: tokens.refreshToken,
333
496
  accessToken: tokens.accessToken ?? (old?.refreshToken === tokens.refreshToken ? old.accessToken : null)
@@ -346,9 +509,19 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
346
509
  }, 100);
347
510
  this._storedBrowserCookieTokenStore.onChange((value) => {
348
511
  try {
349
- setOrDeleteCookieClient(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365 });
350
- setOrDeleteCookieClient(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24 });
351
- deleteCookieClient("stack-refresh");
512
+ const refreshToken = value.refreshToken;
513
+ const secure = window.location.protocol === "https:";
514
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
515
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
516
+ this._getAllBrowserCookies(),
517
+ refreshToken,
518
+ value.accessToken ?? null,
519
+ defaultName
520
+ );
521
+ setOrDeleteCookieClient(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, secure });
522
+ setOrDeleteCookieClient(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24 });
523
+ cookieNamesToDelete.forEach((name) => deleteCookieClient(name));
524
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "browser");
352
525
  hasSucceededInWriting = true;
353
526
  } catch (e) {
354
527
  if (!isBrowserLike()) {
@@ -371,18 +544,31 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
371
544
  if (isBrowserLike()) {
372
545
  return this._getBrowserCookieTokenStore();
373
546
  } else {
374
- const tokens = this._getTokensFromCookies({
375
- refreshTokenCookie: cookieHelper.get(this._refreshTokenCookieName) ?? cookieHelper.get("stack-refresh"),
376
- // keep old cookie name for backwards-compatibility
377
- accessTokenCookie: cookieHelper.get(this._accessTokenCookieName)
378
- });
547
+ const tokens = this._getTokensFromCookies(cookieHelper.getAll());
379
548
  const store = new Store(tokens);
380
549
  store.onChange((value) => {
381
550
  runAsynchronously(async () => {
551
+ const refreshToken = value.refreshToken;
552
+ const secure = await isSecureCookieContext();
553
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
554
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
555
+ cookieHelper.getAll(),
556
+ refreshToken,
557
+ value.accessToken ?? null,
558
+ defaultName
559
+ );
382
560
  await Promise.all([
383
- setOrDeleteCookie(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
384
- setOrDeleteCookie(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
561
+ setOrDeleteCookie(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
562
+ setOrDeleteCookie(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
385
563
  ]);
564
+ if (cookieNamesToDelete.length > 0) {
565
+ await Promise.all(
566
+ cookieNamesToDelete.map(
567
+ (name) => setOrDeleteCookie(name, null, { noOpIfServerComponent: true })
568
+ )
569
+ );
570
+ }
571
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "server");
386
572
  });
387
573
  });
388
574
  return store;
@@ -413,11 +599,7 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
413
599
  }
414
600
  const cookieHeader = tokenStoreInit.headers.get("cookie");
415
601
  const parsed = cookie.parse(cookieHeader || "");
416
- const res = new Store({
417
- refreshToken: parsed[this._refreshTokenCookieName] || parsed["stack-refresh"] || null,
418
- // keep old cookie name for backwards-compatibility
419
- accessToken: parsed[this._accessTokenCookieName] || null
420
- });
602
+ const res = new Store(this._getTokensFromCookies(parsed));
421
603
  this._requestTokenStores.set(tokenStoreInit, res);
422
604
  return res;
423
605
  } else if ("accessToken" in tokenStoreInit || "refreshToken" in tokenStoreInit) {
@@ -1540,15 +1722,28 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
1540
1722
  }
1541
1723
  }
1542
1724
  async signUpWithCredential(options) {
1725
+ if (options.noVerificationCallback && options.verificationCallbackUrl) {
1726
+ throw new StackAssertionError("verificationCallbackUrl is not allowed when noVerificationCallback is true");
1727
+ }
1543
1728
  this._ensurePersistentTokenStore();
1544
1729
  const session = await this._getSession();
1545
- const emailVerificationRedirectUrl = options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification, "verificationCallbackUrl");
1546
- const result = await this._interface.signUpWithCredential(
1730
+ const emailVerificationRedirectUrl = options.noVerificationCallback ? void 0 : options.verificationCallbackUrl ?? constructRedirectUrl(this.urls.emailVerification, "verificationCallbackUrl");
1731
+ let result = await this._interface.signUpWithCredential(
1547
1732
  options.email,
1548
1733
  options.password,
1549
1734
  emailVerificationRedirectUrl,
1550
1735
  session
1551
1736
  );
1737
+ if (result.status === "error" && result.error instanceof KnownErrors.RedirectUrlNotWhitelisted && !options.noVerificationCallback && emailVerificationRedirectUrl !== void 0) {
1738
+ console.error("Warning: The verification callback URL is not trusted. Proceeding with signup without email verification. Please add your domain to the trusted domains list in your Stack Auth dashboard.", { url: emailVerificationRedirectUrl });
1739
+ result = await this._interface.signUpWithCredential(
1740
+ options.email,
1741
+ options.password,
1742
+ void 0,
1743
+ // No email verification
1744
+ session
1745
+ );
1746
+ }
1552
1747
  if (result.status === "ok") {
1553
1748
  await this._signInToAccountWithTokens(result.data);
1554
1749
  if (!options.noRedirect) {