@stackframe/stack 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.
@@ -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 { scrambleDuringCompileTime } from "@stackframe/stack-shared/dist/utils/compile-time";
6
7
  import { isBrowserLike } from "@stackframe/stack-shared/dist/utils/env";
7
8
  import { StackAssertionError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
@@ -19,7 +20,7 @@ import * as NextNavigationUnscrambled from "next/navigation";
19
20
  import React, { useCallback, useMemo } from "react";
20
21
  import { constructRedirectUrl } from "../../../../utils/url.js";
21
22
  import { addNewOAuthProviderOrScope, callOAuthCallback, signInWithOAuth } from "../../../auth.js";
22
- import { createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookieClient, getCookieClient, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie.js";
23
+ import { createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookieClient, isSecure as isSecureCookieContext, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie.js";
23
24
  import { apiKeyCreationOptionsToCrud } from "../../api-keys/index.js";
24
25
  import { stackAppInternalsSymbol } from "../../common.js";
25
26
  import { contactChannelCreateOptionsToCrud, contactChannelUpdateOptionsToCrud } from "../../contact-channels/index.js";
@@ -27,6 +28,7 @@ import { adminProjectCreateOptionsToCrud } from "../../projects/index.js";
27
28
  import { teamCreateOptionsToCrud, teamUpdateOptionsToCrud } from "../../teams/index.js";
28
29
  import { attachUserDestructureGuard, userUpdateOptionsToCrud } from "../../users/index.js";
29
30
  import { clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, resolveConstructorOptions } from "./common.js";
31
+ import { parseJson } from "@stackframe/stack-shared/dist/utils/json";
30
32
  import { useAsyncCache } from "./common.js";
31
33
  import * as sc from "@stackframe/stack-sc";
32
34
  import { cookies } from "@stackframe/stack-sc";
@@ -180,11 +182,15 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
180
182
  this._convexPartialUserCache = createCache(
181
183
  async ([ctx]) => await this._getPartialUserFromConvex(ctx)
182
184
  );
185
+ this._trustedParentDomainCache = createCache(
186
+ async ([domain]) => await this._getTrustedParentDomain(domain)
187
+ );
183
188
  this._anonymousSignUpInProgress = null;
184
189
  this._memoryTokenStore = createEmptyTokenStore();
185
190
  this._nextServerCookiesTokenStores = /* @__PURE__ */ new WeakMap();
186
191
  this._requestTokenStores = /* @__PURE__ */ new WeakMap();
187
192
  this._storedBrowserCookieTokenStore = null;
193
+ this._mostRecentQueuedCookieRefreshIndex = 0;
188
194
  /**
189
195
  * A map from token stores and session keys to sessions.
190
196
  *
@@ -309,13 +315,90 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
309
315
  runAsynchronously(this._checkFeatureSupport(name, options));
310
316
  throw new StackAssertionError(`${name} is not currently supported. Please reach out to Stack support for more information.`);
311
317
  }
318
+ get _legacyRefreshTokenCookieName() {
319
+ return `stack-refresh-${this.projectId}`;
320
+ }
312
321
  get _refreshTokenCookieName() {
313
322
  return `stack-refresh-${this.projectId}`;
314
323
  }
324
+ _getRefreshTokenDefaultCookieNameForSecure(secure) {
325
+ return `${secure ? "__Host-" : ""}${this._refreshTokenCookieName}--default`;
326
+ }
327
+ _getCustomRefreshCookieName(domain) {
328
+ const encoded = encodeBase32(new TextEncoder().encode(domain.toLowerCase()));
329
+ return `${this._refreshTokenCookieName}--custom-${encoded}`;
330
+ }
331
+ _formatRefreshCookieValue(refreshToken, updatedAt) {
332
+ return JSON.stringify({
333
+ refresh_token: refreshToken,
334
+ updated_at_millis: updatedAt
335
+ });
336
+ }
337
+ _formatAccessCookieValue(refreshToken, accessToken) {
338
+ return refreshToken && accessToken ? JSON.stringify([refreshToken, accessToken]) : null;
339
+ }
340
+ _parseStructuredRefreshCookie(value) {
341
+ if (!value) {
342
+ return null;
343
+ }
344
+ const parsed = parseJson(value);
345
+ if (parsed.status !== "ok" || typeof parsed.data !== "object" || parsed.data === null) {
346
+ console.warn("Failed to parse structured refresh cookie");
347
+ return null;
348
+ }
349
+ const data = parsed.data;
350
+ const refreshToken = "refresh_token" in data && typeof data.refresh_token === "string" ? data.refresh_token : null;
351
+ const updatedAt = "updated_at_millis" in data && typeof data.updated_at_millis === "number" ? data.updated_at_millis : null;
352
+ if (!refreshToken) {
353
+ console.warn("Refresh token not found in structured refresh cookie");
354
+ return null;
355
+ }
356
+ return {
357
+ refreshToken,
358
+ updatedAt
359
+ };
360
+ }
361
+ _extractRefreshTokenFromCookieMap(cookies2) {
362
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
363
+ for (const name of legacyNames) {
364
+ const value = cookies2[name];
365
+ if (value) {
366
+ return { refreshToken: value, updatedAt: null };
367
+ }
368
+ }
369
+ let selected = null;
370
+ for (const [name, value] of Object.entries(cookies2)) {
371
+ if (!structuredPrefixes.some((prefix) => name.startsWith(prefix))) continue;
372
+ const parsed = this._parseStructuredRefreshCookie(value);
373
+ if (!parsed) continue;
374
+ const candidateUpdatedAt = parsed.updatedAt ?? Number.NEGATIVE_INFINITY;
375
+ const selectedUpdatedAt = selected?.updatedAt ?? Number.NEGATIVE_INFINITY;
376
+ if (!selected || candidateUpdatedAt > selectedUpdatedAt) {
377
+ selected = parsed;
378
+ }
379
+ }
380
+ if (!selected) {
381
+ return { refreshToken: null, updatedAt: null };
382
+ }
383
+ return {
384
+ refreshToken: selected.refreshToken,
385
+ updatedAt: selected.updatedAt ?? null
386
+ };
387
+ }
315
388
  _getTokensFromCookies(cookies2) {
316
- const refreshToken = cookies2.refreshTokenCookie;
317
- const accessTokenObject = cookies2.accessTokenCookie?.startsWith('["') ? JSON.parse(cookies2.accessTokenCookie) : null;
318
- const accessToken = accessTokenObject && refreshToken === accessTokenObject[0] ? accessTokenObject[1] : null;
389
+ const { refreshToken } = this._extractRefreshTokenFromCookieMap(cookies2);
390
+ const accessTokenCookie = cookies2[this._accessTokenCookieName] ?? null;
391
+ let accessToken = null;
392
+ if (accessTokenCookie && accessTokenCookie.startsWith('["')) {
393
+ const parsed = parseJson(accessTokenCookie);
394
+ 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") {
395
+ if (parsed.data[0] === refreshToken) {
396
+ accessToken = parsed.data[1];
397
+ }
398
+ } else {
399
+ console.warn("Access token cookie has invalid format");
400
+ }
401
+ }
319
402
  return {
320
403
  refreshToken,
321
404
  accessToken
@@ -324,17 +407,98 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
324
407
  get _accessTokenCookieName() {
325
408
  return `stack-access`;
326
409
  }
410
+ _getAllBrowserCookies() {
411
+ if (!isBrowserLike()) {
412
+ throw new StackAssertionError("Cannot get browser cookies on the server!");
413
+ }
414
+ return cookie.parse(document.cookie || "");
415
+ }
416
+ _getRefreshTokenCookieNamePatterns() {
417
+ return {
418
+ legacyNames: [this._legacyRefreshTokenCookieName, "stack-refresh"],
419
+ structuredPrefixes: [
420
+ `${this._refreshTokenCookieName}--`,
421
+ `__Host-${this._refreshTokenCookieName}--`
422
+ ]
423
+ };
424
+ }
425
+ _collectRefreshTokenCookieNames(cookies2) {
426
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
427
+ const names = /* @__PURE__ */ new Set();
428
+ for (const name of legacyNames) {
429
+ if (cookies2[name]) {
430
+ names.add(name);
431
+ }
432
+ }
433
+ for (const name of Object.keys(cookies2)) {
434
+ if (structuredPrefixes.some((prefix) => name.startsWith(prefix))) {
435
+ names.add(name);
436
+ }
437
+ }
438
+ return names;
439
+ }
440
+ _prepareRefreshCookieUpdate(existingCookies, refreshToken, accessToken, defaultCookieName) {
441
+ const cookieNames = this._collectRefreshTokenCookieNames(existingCookies);
442
+ cookieNames.delete(defaultCookieName);
443
+ const updatedAt = refreshToken ? Date.now() : null;
444
+ const refreshCookieValue = refreshToken && updatedAt !== null ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
445
+ const accessTokenPayload = this._formatAccessCookieValue(refreshToken, accessToken);
446
+ return {
447
+ updatedAt,
448
+ refreshCookieValue,
449
+ accessTokenPayload,
450
+ cookieNamesToDelete: [...cookieNames]
451
+ };
452
+ }
453
+ _queueCustomRefreshCookieUpdate(refreshToken, updatedAt, context) {
454
+ runAsynchronously(async () => {
455
+ this._mostRecentQueuedCookieRefreshIndex++;
456
+ const updateIndex = this._mostRecentQueuedCookieRefreshIndex;
457
+ let hostname;
458
+ if (isBrowserLike()) {
459
+ hostname = window.location.hostname;
460
+ }
461
+ hostname = (await sc.headers?.())?.get("host");
462
+ if (!hostname) {
463
+ return;
464
+ }
465
+ const domain = await this._trustedParentDomainCache.getOrWait([hostname], "read-write");
466
+ const setCookie = async (targetDomain, value2) => {
467
+ const name = this._getCustomRefreshCookieName(targetDomain);
468
+ const options = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true };
469
+ if (context === "browser") {
470
+ setOrDeleteCookieClient(name, value2, options);
471
+ } else {
472
+ await setOrDeleteCookie(name, value2, options);
473
+ }
474
+ };
475
+ if (domain.status === "error" || !domain.data || updateIndex !== this._mostRecentQueuedCookieRefreshIndex) {
476
+ return;
477
+ }
478
+ const value = refreshToken && updatedAt ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
479
+ await setCookie(domain.data, value);
480
+ });
481
+ }
482
+ async _getTrustedParentDomain(currentDomain) {
483
+ const project = Result.orThrow(await this._interface.getClientProject());
484
+ const domains = project.config.domains.map((d) => d.domain.trim().replace(/^https?:\/\//, "").split("/")[0]?.toLowerCase());
485
+ const trustedWildcards = domains.filter((d) => d.startsWith("**."));
486
+ const parts = currentDomain.split(".");
487
+ for (let i = parts.length - 2; i >= 0; i--) {
488
+ const parentDomain = parts.slice(i).join(".");
489
+ if (domains.includes(parentDomain) && trustedWildcards.includes("**." + parentDomain)) {
490
+ return parentDomain;
491
+ }
492
+ }
493
+ return null;
494
+ }
327
495
  _getBrowserCookieTokenStore() {
328
496
  if (!isBrowserLike()) {
329
497
  throw new Error("Cannot use cookie token store on the server!");
330
498
  }
331
499
  if (this._storedBrowserCookieTokenStore === null) {
332
500
  const getCurrentValue = (old) => {
333
- const tokens = this._getTokensFromCookies({
334
- refreshTokenCookie: getCookieClient(this._refreshTokenCookieName) ?? getCookieClient("stack-refresh"),
335
- // keep old cookie name for backwards-compatibility
336
- accessTokenCookie: getCookieClient(this._accessTokenCookieName)
337
- });
501
+ const tokens = this._getTokensFromCookies(this._getAllBrowserCookies());
338
502
  return {
339
503
  refreshToken: tokens.refreshToken,
340
504
  accessToken: tokens.accessToken ?? (old?.refreshToken === tokens.refreshToken ? old.accessToken : null)
@@ -353,9 +517,19 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
353
517
  }, 100);
354
518
  this._storedBrowserCookieTokenStore.onChange((value) => {
355
519
  try {
356
- setOrDeleteCookieClient(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365 });
357
- setOrDeleteCookieClient(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24 });
358
- deleteCookieClient("stack-refresh");
520
+ const refreshToken = value.refreshToken;
521
+ const secure = window.location.protocol === "https:";
522
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
523
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
524
+ this._getAllBrowserCookies(),
525
+ refreshToken,
526
+ value.accessToken ?? null,
527
+ defaultName
528
+ );
529
+ setOrDeleteCookieClient(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, secure });
530
+ setOrDeleteCookieClient(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24 });
531
+ cookieNamesToDelete.forEach((name) => deleteCookieClient(name));
532
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "browser");
359
533
  hasSucceededInWriting = true;
360
534
  } catch (e) {
361
535
  if (!isBrowserLike()) {
@@ -378,18 +552,31 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
378
552
  if (isBrowserLike()) {
379
553
  return this._getBrowserCookieTokenStore();
380
554
  } else {
381
- const tokens = this._getTokensFromCookies({
382
- refreshTokenCookie: cookieHelper.get(this._refreshTokenCookieName) ?? cookieHelper.get("stack-refresh"),
383
- // keep old cookie name for backwards-compatibility
384
- accessTokenCookie: cookieHelper.get(this._accessTokenCookieName)
385
- });
555
+ const tokens = this._getTokensFromCookies(cookieHelper.getAll());
386
556
  const store = new Store(tokens);
387
557
  store.onChange((value) => {
388
558
  runAsynchronously(async () => {
559
+ const refreshToken = value.refreshToken;
560
+ const secure = await isSecureCookieContext();
561
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
562
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
563
+ cookieHelper.getAll(),
564
+ refreshToken,
565
+ value.accessToken ?? null,
566
+ defaultName
567
+ );
389
568
  await Promise.all([
390
- setOrDeleteCookie(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
391
- setOrDeleteCookie(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
569
+ setOrDeleteCookie(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
570
+ setOrDeleteCookie(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
392
571
  ]);
572
+ if (cookieNamesToDelete.length > 0) {
573
+ await Promise.all(
574
+ cookieNamesToDelete.map(
575
+ (name) => setOrDeleteCookie(name, null, { noOpIfServerComponent: true })
576
+ )
577
+ );
578
+ }
579
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "server");
393
580
  });
394
581
  });
395
582
  return store;
@@ -420,11 +607,7 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
420
607
  }
421
608
  const cookieHeader = tokenStoreInit.headers.get("cookie");
422
609
  const parsed = cookie.parse(cookieHeader || "");
423
- const res = new Store({
424
- refreshToken: parsed[this._refreshTokenCookieName] || parsed["stack-refresh"] || null,
425
- // keep old cookie name for backwards-compatibility
426
- accessToken: parsed[this._accessTokenCookieName] || null
427
- });
610
+ const res = new Store(this._getTokensFromCookies(parsed));
428
611
  this._requestTokenStores.set(tokenStoreInit, res);
429
612
  return res;
430
613
  } else if ("accessToken" in tokenStoreInit || "refreshToken" in tokenStoreInit) {