@stackframe/react 2.8.48 → 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.
@@ -36,6 +36,7 @@ module.exports = __toCommonJS(client_app_impl_exports);
36
36
  var import_browser = require("@simplewebauthn/browser");
37
37
  var import_stack_shared = require("@stackframe/stack-shared");
38
38
  var import_sessions = require("@stackframe/stack-shared/dist/sessions");
39
+ var import_bytes = require("@stackframe/stack-shared/dist/utils/bytes");
39
40
  var import_env = require("@stackframe/stack-shared/dist/utils/env");
40
41
  var import_errors = require("@stackframe/stack-shared/dist/utils/errors");
41
42
  var import_maps = require("@stackframe/stack-shared/dist/utils/maps");
@@ -59,6 +60,7 @@ var import_projects = require("../../projects/index.js");
59
60
  var import_teams = require("../../teams/index.js");
60
61
  var import_users = require("../../users/index.js");
61
62
  var import_common2 = require("./common.js");
63
+ var import_json = require("@stackframe/stack-shared/dist/utils/json");
62
64
  var import_common3 = require("./common.js");
63
65
  var isReactServer = false;
64
66
  var process = globalThis.process ?? { env: {} };
@@ -209,11 +211,15 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
209
211
  this._convexPartialUserCache = (0, import_common2.createCache)(
210
212
  async ([ctx]) => await this._getPartialUserFromConvex(ctx)
211
213
  );
214
+ this._trustedParentDomainCache = (0, import_common2.createCache)(
215
+ async ([domain]) => await this._getTrustedParentDomain(domain)
216
+ );
212
217
  this._anonymousSignUpInProgress = null;
213
218
  this._memoryTokenStore = (0, import_common2.createEmptyTokenStore)();
214
219
  this._nextServerCookiesTokenStores = /* @__PURE__ */ new WeakMap();
215
220
  this._requestTokenStores = /* @__PURE__ */ new WeakMap();
216
221
  this._storedBrowserCookieTokenStore = null;
222
+ this._mostRecentQueuedCookieRefreshIndex = 0;
217
223
  /**
218
224
  * A map from token stores and session keys to sessions.
219
225
  *
@@ -336,13 +342,90 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
336
342
  (0, import_promises.runAsynchronously)(this._checkFeatureSupport(name, options));
337
343
  throw new import_errors.StackAssertionError(`${name} is not currently supported. Please reach out to Stack support for more information.`);
338
344
  }
345
+ get _legacyRefreshTokenCookieName() {
346
+ return `stack-refresh-${this.projectId}`;
347
+ }
339
348
  get _refreshTokenCookieName() {
340
349
  return `stack-refresh-${this.projectId}`;
341
350
  }
351
+ _getRefreshTokenDefaultCookieNameForSecure(secure) {
352
+ return `${secure ? "__Host-" : ""}${this._refreshTokenCookieName}--default`;
353
+ }
354
+ _getCustomRefreshCookieName(domain) {
355
+ const encoded = (0, import_bytes.encodeBase32)(new TextEncoder().encode(domain.toLowerCase()));
356
+ return `${this._refreshTokenCookieName}--custom-${encoded}`;
357
+ }
358
+ _formatRefreshCookieValue(refreshToken, updatedAt) {
359
+ return JSON.stringify({
360
+ refresh_token: refreshToken,
361
+ updated_at_millis: updatedAt
362
+ });
363
+ }
364
+ _formatAccessCookieValue(refreshToken, accessToken) {
365
+ return refreshToken && accessToken ? JSON.stringify([refreshToken, accessToken]) : null;
366
+ }
367
+ _parseStructuredRefreshCookie(value) {
368
+ if (!value) {
369
+ return null;
370
+ }
371
+ const parsed = (0, import_json.parseJson)(value);
372
+ if (parsed.status !== "ok" || typeof parsed.data !== "object" || parsed.data === null) {
373
+ console.warn("Failed to parse structured refresh cookie");
374
+ return null;
375
+ }
376
+ const data = parsed.data;
377
+ const refreshToken = "refresh_token" in data && typeof data.refresh_token === "string" ? data.refresh_token : null;
378
+ const updatedAt = "updated_at_millis" in data && typeof data.updated_at_millis === "number" ? data.updated_at_millis : null;
379
+ if (!refreshToken) {
380
+ console.warn("Refresh token not found in structured refresh cookie");
381
+ return null;
382
+ }
383
+ return {
384
+ refreshToken,
385
+ updatedAt
386
+ };
387
+ }
388
+ _extractRefreshTokenFromCookieMap(cookies) {
389
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
390
+ for (const name of legacyNames) {
391
+ const value = cookies[name];
392
+ if (value) {
393
+ return { refreshToken: value, updatedAt: null };
394
+ }
395
+ }
396
+ let selected = null;
397
+ for (const [name, value] of Object.entries(cookies)) {
398
+ if (!structuredPrefixes.some((prefix) => name.startsWith(prefix))) continue;
399
+ const parsed = this._parseStructuredRefreshCookie(value);
400
+ if (!parsed) continue;
401
+ const candidateUpdatedAt = parsed.updatedAt ?? Number.NEGATIVE_INFINITY;
402
+ const selectedUpdatedAt = selected?.updatedAt ?? Number.NEGATIVE_INFINITY;
403
+ if (!selected || candidateUpdatedAt > selectedUpdatedAt) {
404
+ selected = parsed;
405
+ }
406
+ }
407
+ if (!selected) {
408
+ return { refreshToken: null, updatedAt: null };
409
+ }
410
+ return {
411
+ refreshToken: selected.refreshToken,
412
+ updatedAt: selected.updatedAt ?? null
413
+ };
414
+ }
342
415
  _getTokensFromCookies(cookies) {
343
- const refreshToken = cookies.refreshTokenCookie;
344
- const accessTokenObject = cookies.accessTokenCookie?.startsWith('["') ? JSON.parse(cookies.accessTokenCookie) : null;
345
- const accessToken = accessTokenObject && refreshToken === accessTokenObject[0] ? accessTokenObject[1] : null;
416
+ const { refreshToken } = this._extractRefreshTokenFromCookieMap(cookies);
417
+ const accessTokenCookie = cookies[this._accessTokenCookieName] ?? null;
418
+ let accessToken = null;
419
+ if (accessTokenCookie && accessTokenCookie.startsWith('["')) {
420
+ const parsed = (0, import_json.parseJson)(accessTokenCookie);
421
+ 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") {
422
+ if (parsed.data[0] === refreshToken) {
423
+ accessToken = parsed.data[1];
424
+ }
425
+ } else {
426
+ console.warn("Access token cookie has invalid format");
427
+ }
428
+ }
346
429
  return {
347
430
  refreshToken,
348
431
  accessToken
@@ -351,17 +434,97 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
351
434
  get _accessTokenCookieName() {
352
435
  return `stack-access`;
353
436
  }
437
+ _getAllBrowserCookies() {
438
+ if (!(0, import_env.isBrowserLike)()) {
439
+ throw new import_errors.StackAssertionError("Cannot get browser cookies on the server!");
440
+ }
441
+ return cookie.parse(document.cookie || "");
442
+ }
443
+ _getRefreshTokenCookieNamePatterns() {
444
+ return {
445
+ legacyNames: [this._legacyRefreshTokenCookieName, "stack-refresh"],
446
+ structuredPrefixes: [
447
+ `${this._refreshTokenCookieName}--`,
448
+ `__Host-${this._refreshTokenCookieName}--`
449
+ ]
450
+ };
451
+ }
452
+ _collectRefreshTokenCookieNames(cookies) {
453
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
454
+ const names = /* @__PURE__ */ new Set();
455
+ for (const name of legacyNames) {
456
+ if (cookies[name]) {
457
+ names.add(name);
458
+ }
459
+ }
460
+ for (const name of Object.keys(cookies)) {
461
+ if (structuredPrefixes.some((prefix) => name.startsWith(prefix))) {
462
+ names.add(name);
463
+ }
464
+ }
465
+ return names;
466
+ }
467
+ _prepareRefreshCookieUpdate(existingCookies, refreshToken, accessToken, defaultCookieName) {
468
+ const cookieNames = this._collectRefreshTokenCookieNames(existingCookies);
469
+ cookieNames.delete(defaultCookieName);
470
+ const updatedAt = refreshToken ? Date.now() : null;
471
+ const refreshCookieValue = refreshToken && updatedAt !== null ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
472
+ const accessTokenPayload = this._formatAccessCookieValue(refreshToken, accessToken);
473
+ return {
474
+ updatedAt,
475
+ refreshCookieValue,
476
+ accessTokenPayload,
477
+ cookieNamesToDelete: [...cookieNames]
478
+ };
479
+ }
480
+ _queueCustomRefreshCookieUpdate(refreshToken, updatedAt, context) {
481
+ (0, import_promises.runAsynchronously)(async () => {
482
+ this._mostRecentQueuedCookieRefreshIndex++;
483
+ const updateIndex = this._mostRecentQueuedCookieRefreshIndex;
484
+ let hostname;
485
+ if ((0, import_env.isBrowserLike)()) {
486
+ hostname = window.location.hostname;
487
+ }
488
+ if (!hostname) {
489
+ return;
490
+ }
491
+ const domain = await this._trustedParentDomainCache.getOrWait([hostname], "read-write");
492
+ const setCookie = async (targetDomain, value2) => {
493
+ const name = this._getCustomRefreshCookieName(targetDomain);
494
+ const options = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true };
495
+ if (context === "browser") {
496
+ (0, import_cookie.setOrDeleteCookieClient)(name, value2, options);
497
+ } else {
498
+ await (0, import_cookie.setOrDeleteCookie)(name, value2, options);
499
+ }
500
+ };
501
+ if (domain.status === "error" || !domain.data || updateIndex !== this._mostRecentQueuedCookieRefreshIndex) {
502
+ return;
503
+ }
504
+ const value = refreshToken && updatedAt ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
505
+ await setCookie(domain.data, value);
506
+ });
507
+ }
508
+ async _getTrustedParentDomain(currentDomain) {
509
+ const project = import_results.Result.orThrow(await this._interface.getClientProject());
510
+ const domains = project.config.domains.map((d) => d.domain.trim().replace(/^https?:\/\//, "").split("/")[0]?.toLowerCase());
511
+ const trustedWildcards = domains.filter((d) => d.startsWith("**."));
512
+ const parts = currentDomain.split(".");
513
+ for (let i = parts.length - 2; i >= 0; i--) {
514
+ const parentDomain = parts.slice(i).join(".");
515
+ if (domains.includes(parentDomain) && trustedWildcards.includes("**." + parentDomain)) {
516
+ return parentDomain;
517
+ }
518
+ }
519
+ return null;
520
+ }
354
521
  _getBrowserCookieTokenStore() {
355
522
  if (!(0, import_env.isBrowserLike)()) {
356
523
  throw new Error("Cannot use cookie token store on the server!");
357
524
  }
358
525
  if (this._storedBrowserCookieTokenStore === null) {
359
526
  const getCurrentValue = (old) => {
360
- const tokens = this._getTokensFromCookies({
361
- refreshTokenCookie: (0, import_cookie.getCookieClient)(this._refreshTokenCookieName) ?? (0, import_cookie.getCookieClient)("stack-refresh"),
362
- // keep old cookie name for backwards-compatibility
363
- accessTokenCookie: (0, import_cookie.getCookieClient)(this._accessTokenCookieName)
364
- });
527
+ const tokens = this._getTokensFromCookies(this._getAllBrowserCookies());
365
528
  return {
366
529
  refreshToken: tokens.refreshToken,
367
530
  accessToken: tokens.accessToken ?? (old?.refreshToken === tokens.refreshToken ? old.accessToken : null)
@@ -380,9 +543,19 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
380
543
  }, 100);
381
544
  this._storedBrowserCookieTokenStore.onChange((value) => {
382
545
  try {
383
- (0, import_cookie.setOrDeleteCookieClient)(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365 });
384
- (0, import_cookie.setOrDeleteCookieClient)(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24 });
385
- (0, import_cookie.deleteCookieClient)("stack-refresh");
546
+ const refreshToken = value.refreshToken;
547
+ const secure = window.location.protocol === "https:";
548
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
549
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
550
+ this._getAllBrowserCookies(),
551
+ refreshToken,
552
+ value.accessToken ?? null,
553
+ defaultName
554
+ );
555
+ (0, import_cookie.setOrDeleteCookieClient)(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, secure });
556
+ (0, import_cookie.setOrDeleteCookieClient)(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24 });
557
+ cookieNamesToDelete.forEach((name) => (0, import_cookie.deleteCookieClient)(name));
558
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "browser");
386
559
  hasSucceededInWriting = true;
387
560
  } catch (e) {
388
561
  if (!(0, import_env.isBrowserLike)()) {
@@ -405,18 +578,31 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
405
578
  if ((0, import_env.isBrowserLike)()) {
406
579
  return this._getBrowserCookieTokenStore();
407
580
  } else {
408
- const tokens = this._getTokensFromCookies({
409
- refreshTokenCookie: cookieHelper.get(this._refreshTokenCookieName) ?? cookieHelper.get("stack-refresh"),
410
- // keep old cookie name for backwards-compatibility
411
- accessTokenCookie: cookieHelper.get(this._accessTokenCookieName)
412
- });
581
+ const tokens = this._getTokensFromCookies(cookieHelper.getAll());
413
582
  const store = new import_stores.Store(tokens);
414
583
  store.onChange((value) => {
415
584
  (0, import_promises.runAsynchronously)(async () => {
585
+ const refreshToken = value.refreshToken;
586
+ const secure = await (0, import_cookie.isSecure)();
587
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
588
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
589
+ cookieHelper.getAll(),
590
+ refreshToken,
591
+ value.accessToken ?? null,
592
+ defaultName
593
+ );
416
594
  await Promise.all([
417
- (0, import_cookie.setOrDeleteCookie)(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
418
- (0, import_cookie.setOrDeleteCookie)(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
595
+ (0, import_cookie.setOrDeleteCookie)(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
596
+ (0, import_cookie.setOrDeleteCookie)(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
419
597
  ]);
598
+ if (cookieNamesToDelete.length > 0) {
599
+ await Promise.all(
600
+ cookieNamesToDelete.map(
601
+ (name) => (0, import_cookie.setOrDeleteCookie)(name, null, { noOpIfServerComponent: true })
602
+ )
603
+ );
604
+ }
605
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "server");
420
606
  });
421
607
  });
422
608
  return store;
@@ -447,11 +633,7 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
447
633
  }
448
634
  const cookieHeader = tokenStoreInit.headers.get("cookie");
449
635
  const parsed = cookie.parse(cookieHeader || "");
450
- const res = new import_stores.Store({
451
- refreshToken: parsed[this._refreshTokenCookieName] || parsed["stack-refresh"] || null,
452
- // keep old cookie name for backwards-compatibility
453
- accessToken: parsed[this._accessTokenCookieName] || null
454
- });
636
+ const res = new import_stores.Store(this._getTokensFromCookies(parsed));
455
637
  this._requestTokenStores.set(tokenStoreInit, res);
456
638
  return res;
457
639
  } else if ("accessToken" in tokenStoreInit || "refreshToken" in tokenStoreInit) {