@stackframe/js 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 { 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";
@@ -15,7 +16,7 @@ import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
15
16
  import * as cookie from "cookie";
16
17
  import { constructRedirectUrl } from "../../../../utils/url.js";
17
18
  import { addNewOAuthProviderOrScope, callOAuthCallback, signInWithOAuth } from "../../../auth.js";
18
- import { createCookieHelper, createPlaceholderCookieHelper, deleteCookieClient, getCookieClient, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie.js";
19
+ import { createCookieHelper, createPlaceholderCookieHelper, deleteCookieClient, isSecure as isSecureCookieContext, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie.js";
19
20
  import { apiKeyCreationOptionsToCrud } from "../../api-keys/index.js";
20
21
  import { stackAppInternalsSymbol } from "../../common.js";
21
22
  import { contactChannelCreateOptionsToCrud, contactChannelUpdateOptionsToCrud } from "../../contact-channels/index.js";
@@ -23,6 +24,7 @@ import { adminProjectCreateOptionsToCrud } from "../../projects/index.js";
23
24
  import { teamCreateOptionsToCrud, teamUpdateOptionsToCrud } from "../../teams/index.js";
24
25
  import { attachUserDestructureGuard, userUpdateOptionsToCrud } from "../../users/index.js";
25
26
  import { clientVersion, createCache, createCacheBySession, createEmptyTokenStore, getBaseUrl, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getUrls, resolveConstructorOptions } from "./common.js";
27
+ import { parseJson } from "@stackframe/stack-shared/dist/utils/json";
26
28
  var isReactServer = false;
27
29
  var process = globalThis.process ?? { env: {} };
28
30
  var allClientApps = /* @__PURE__ */ new Map();
@@ -171,11 +173,15 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
171
173
  this._convexPartialUserCache = createCache(
172
174
  async ([ctx]) => await this._getPartialUserFromConvex(ctx)
173
175
  );
176
+ this._trustedParentDomainCache = createCache(
177
+ async ([domain]) => await this._getTrustedParentDomain(domain)
178
+ );
174
179
  this._anonymousSignUpInProgress = null;
175
180
  this._memoryTokenStore = createEmptyTokenStore();
176
181
  this._nextServerCookiesTokenStores = /* @__PURE__ */ new WeakMap();
177
182
  this._requestTokenStores = /* @__PURE__ */ new WeakMap();
178
183
  this._storedBrowserCookieTokenStore = null;
184
+ this._mostRecentQueuedCookieRefreshIndex = 0;
179
185
  /**
180
186
  * A map from token stores and session keys to sessions.
181
187
  *
@@ -291,13 +297,90 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
291
297
  runAsynchronously(this._checkFeatureSupport(name, options));
292
298
  throw new StackAssertionError(`${name} is not currently supported. Please reach out to Stack support for more information.`);
293
299
  }
300
+ get _legacyRefreshTokenCookieName() {
301
+ return `stack-refresh-${this.projectId}`;
302
+ }
294
303
  get _refreshTokenCookieName() {
295
304
  return `stack-refresh-${this.projectId}`;
296
305
  }
306
+ _getRefreshTokenDefaultCookieNameForSecure(secure) {
307
+ return `${secure ? "__Host-" : ""}${this._refreshTokenCookieName}--default`;
308
+ }
309
+ _getCustomRefreshCookieName(domain) {
310
+ const encoded = encodeBase32(new TextEncoder().encode(domain.toLowerCase()));
311
+ return `${this._refreshTokenCookieName}--custom-${encoded}`;
312
+ }
313
+ _formatRefreshCookieValue(refreshToken, updatedAt) {
314
+ return JSON.stringify({
315
+ refresh_token: refreshToken,
316
+ updated_at_millis: updatedAt
317
+ });
318
+ }
319
+ _formatAccessCookieValue(refreshToken, accessToken) {
320
+ return refreshToken && accessToken ? JSON.stringify([refreshToken, accessToken]) : null;
321
+ }
322
+ _parseStructuredRefreshCookie(value) {
323
+ if (!value) {
324
+ return null;
325
+ }
326
+ const parsed = parseJson(value);
327
+ if (parsed.status !== "ok" || typeof parsed.data !== "object" || parsed.data === null) {
328
+ console.warn("Failed to parse structured refresh cookie");
329
+ return null;
330
+ }
331
+ const data = parsed.data;
332
+ const refreshToken = "refresh_token" in data && typeof data.refresh_token === "string" ? data.refresh_token : null;
333
+ const updatedAt = "updated_at_millis" in data && typeof data.updated_at_millis === "number" ? data.updated_at_millis : null;
334
+ if (!refreshToken) {
335
+ console.warn("Refresh token not found in structured refresh cookie");
336
+ return null;
337
+ }
338
+ return {
339
+ refreshToken,
340
+ updatedAt
341
+ };
342
+ }
343
+ _extractRefreshTokenFromCookieMap(cookies) {
344
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
345
+ for (const name of legacyNames) {
346
+ const value = cookies[name];
347
+ if (value) {
348
+ return { refreshToken: value, updatedAt: null };
349
+ }
350
+ }
351
+ let selected = null;
352
+ for (const [name, value] of Object.entries(cookies)) {
353
+ if (!structuredPrefixes.some((prefix) => name.startsWith(prefix))) continue;
354
+ const parsed = this._parseStructuredRefreshCookie(value);
355
+ if (!parsed) continue;
356
+ const candidateUpdatedAt = parsed.updatedAt ?? Number.NEGATIVE_INFINITY;
357
+ const selectedUpdatedAt = selected?.updatedAt ?? Number.NEGATIVE_INFINITY;
358
+ if (!selected || candidateUpdatedAt > selectedUpdatedAt) {
359
+ selected = parsed;
360
+ }
361
+ }
362
+ if (!selected) {
363
+ return { refreshToken: null, updatedAt: null };
364
+ }
365
+ return {
366
+ refreshToken: selected.refreshToken,
367
+ updatedAt: selected.updatedAt ?? null
368
+ };
369
+ }
297
370
  _getTokensFromCookies(cookies) {
298
- const refreshToken = cookies.refreshTokenCookie;
299
- const accessTokenObject = cookies.accessTokenCookie?.startsWith('["') ? JSON.parse(cookies.accessTokenCookie) : null;
300
- const accessToken = accessTokenObject && refreshToken === accessTokenObject[0] ? accessTokenObject[1] : null;
371
+ const { refreshToken } = this._extractRefreshTokenFromCookieMap(cookies);
372
+ const accessTokenCookie = cookies[this._accessTokenCookieName] ?? null;
373
+ let accessToken = null;
374
+ if (accessTokenCookie && accessTokenCookie.startsWith('["')) {
375
+ const parsed = parseJson(accessTokenCookie);
376
+ 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") {
377
+ if (parsed.data[0] === refreshToken) {
378
+ accessToken = parsed.data[1];
379
+ }
380
+ } else {
381
+ console.warn("Access token cookie has invalid format");
382
+ }
383
+ }
301
384
  return {
302
385
  refreshToken,
303
386
  accessToken
@@ -306,17 +389,97 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
306
389
  get _accessTokenCookieName() {
307
390
  return `stack-access`;
308
391
  }
392
+ _getAllBrowserCookies() {
393
+ if (!isBrowserLike()) {
394
+ throw new StackAssertionError("Cannot get browser cookies on the server!");
395
+ }
396
+ return cookie.parse(document.cookie || "");
397
+ }
398
+ _getRefreshTokenCookieNamePatterns() {
399
+ return {
400
+ legacyNames: [this._legacyRefreshTokenCookieName, "stack-refresh"],
401
+ structuredPrefixes: [
402
+ `${this._refreshTokenCookieName}--`,
403
+ `__Host-${this._refreshTokenCookieName}--`
404
+ ]
405
+ };
406
+ }
407
+ _collectRefreshTokenCookieNames(cookies) {
408
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
409
+ const names = /* @__PURE__ */ new Set();
410
+ for (const name of legacyNames) {
411
+ if (cookies[name]) {
412
+ names.add(name);
413
+ }
414
+ }
415
+ for (const name of Object.keys(cookies)) {
416
+ if (structuredPrefixes.some((prefix) => name.startsWith(prefix))) {
417
+ names.add(name);
418
+ }
419
+ }
420
+ return names;
421
+ }
422
+ _prepareRefreshCookieUpdate(existingCookies, refreshToken, accessToken, defaultCookieName) {
423
+ const cookieNames = this._collectRefreshTokenCookieNames(existingCookies);
424
+ cookieNames.delete(defaultCookieName);
425
+ const updatedAt = refreshToken ? Date.now() : null;
426
+ const refreshCookieValue = refreshToken && updatedAt !== null ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
427
+ const accessTokenPayload = this._formatAccessCookieValue(refreshToken, accessToken);
428
+ return {
429
+ updatedAt,
430
+ refreshCookieValue,
431
+ accessTokenPayload,
432
+ cookieNamesToDelete: [...cookieNames]
433
+ };
434
+ }
435
+ _queueCustomRefreshCookieUpdate(refreshToken, updatedAt, context) {
436
+ runAsynchronously(async () => {
437
+ this._mostRecentQueuedCookieRefreshIndex++;
438
+ const updateIndex = this._mostRecentQueuedCookieRefreshIndex;
439
+ let hostname;
440
+ if (isBrowserLike()) {
441
+ hostname = window.location.hostname;
442
+ }
443
+ if (!hostname) {
444
+ return;
445
+ }
446
+ const domain = await this._trustedParentDomainCache.getOrWait([hostname], "read-write");
447
+ const setCookie = async (targetDomain, value2) => {
448
+ const name = this._getCustomRefreshCookieName(targetDomain);
449
+ const options = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true };
450
+ if (context === "browser") {
451
+ setOrDeleteCookieClient(name, value2, options);
452
+ } else {
453
+ await setOrDeleteCookie(name, value2, options);
454
+ }
455
+ };
456
+ if (domain.status === "error" || !domain.data || updateIndex !== this._mostRecentQueuedCookieRefreshIndex) {
457
+ return;
458
+ }
459
+ const value = refreshToken && updatedAt ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
460
+ await setCookie(domain.data, value);
461
+ });
462
+ }
463
+ async _getTrustedParentDomain(currentDomain) {
464
+ const project = Result.orThrow(await this._interface.getClientProject());
465
+ const domains = project.config.domains.map((d) => d.domain.trim().replace(/^https?:\/\//, "").split("/")[0]?.toLowerCase());
466
+ const trustedWildcards = domains.filter((d) => d.startsWith("**."));
467
+ const parts = currentDomain.split(".");
468
+ for (let i = parts.length - 2; i >= 0; i--) {
469
+ const parentDomain = parts.slice(i).join(".");
470
+ if (domains.includes(parentDomain) && trustedWildcards.includes("**." + parentDomain)) {
471
+ return parentDomain;
472
+ }
473
+ }
474
+ return null;
475
+ }
309
476
  _getBrowserCookieTokenStore() {
310
477
  if (!isBrowserLike()) {
311
478
  throw new Error("Cannot use cookie token store on the server!");
312
479
  }
313
480
  if (this._storedBrowserCookieTokenStore === null) {
314
481
  const getCurrentValue = (old) => {
315
- const tokens = this._getTokensFromCookies({
316
- refreshTokenCookie: getCookieClient(this._refreshTokenCookieName) ?? getCookieClient("stack-refresh"),
317
- // keep old cookie name for backwards-compatibility
318
- accessTokenCookie: getCookieClient(this._accessTokenCookieName)
319
- });
482
+ const tokens = this._getTokensFromCookies(this._getAllBrowserCookies());
320
483
  return {
321
484
  refreshToken: tokens.refreshToken,
322
485
  accessToken: tokens.accessToken ?? (old?.refreshToken === tokens.refreshToken ? old.accessToken : null)
@@ -335,9 +498,19 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
335
498
  }, 100);
336
499
  this._storedBrowserCookieTokenStore.onChange((value) => {
337
500
  try {
338
- setOrDeleteCookieClient(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365 });
339
- setOrDeleteCookieClient(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24 });
340
- deleteCookieClient("stack-refresh");
501
+ const refreshToken = value.refreshToken;
502
+ const secure = window.location.protocol === "https:";
503
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
504
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
505
+ this._getAllBrowserCookies(),
506
+ refreshToken,
507
+ value.accessToken ?? null,
508
+ defaultName
509
+ );
510
+ setOrDeleteCookieClient(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, secure });
511
+ setOrDeleteCookieClient(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24 });
512
+ cookieNamesToDelete.forEach((name) => deleteCookieClient(name));
513
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "browser");
341
514
  hasSucceededInWriting = true;
342
515
  } catch (e) {
343
516
  if (!isBrowserLike()) {
@@ -360,18 +533,31 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
360
533
  if (isBrowserLike()) {
361
534
  return this._getBrowserCookieTokenStore();
362
535
  } else {
363
- const tokens = this._getTokensFromCookies({
364
- refreshTokenCookie: cookieHelper.get(this._refreshTokenCookieName) ?? cookieHelper.get("stack-refresh"),
365
- // keep old cookie name for backwards-compatibility
366
- accessTokenCookie: cookieHelper.get(this._accessTokenCookieName)
367
- });
536
+ const tokens = this._getTokensFromCookies(cookieHelper.getAll());
368
537
  const store = new Store(tokens);
369
538
  store.onChange((value) => {
370
539
  runAsynchronously(async () => {
540
+ const refreshToken = value.refreshToken;
541
+ const secure = await isSecureCookieContext();
542
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
543
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
544
+ cookieHelper.getAll(),
545
+ refreshToken,
546
+ value.accessToken ?? null,
547
+ defaultName
548
+ );
371
549
  await Promise.all([
372
- setOrDeleteCookie(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
373
- setOrDeleteCookie(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
550
+ setOrDeleteCookie(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
551
+ setOrDeleteCookie(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
374
552
  ]);
553
+ if (cookieNamesToDelete.length > 0) {
554
+ await Promise.all(
555
+ cookieNamesToDelete.map(
556
+ (name) => setOrDeleteCookie(name, null, { noOpIfServerComponent: true })
557
+ )
558
+ );
559
+ }
560
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "server");
375
561
  });
376
562
  });
377
563
  return store;
@@ -402,11 +588,7 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
402
588
  }
403
589
  const cookieHeader = tokenStoreInit.headers.get("cookie");
404
590
  const parsed = cookie.parse(cookieHeader || "");
405
- const res = new Store({
406
- refreshToken: parsed[this._refreshTokenCookieName] || parsed["stack-refresh"] || null,
407
- // keep old cookie name for backwards-compatibility
408
- accessToken: parsed[this._accessTokenCookieName] || null
409
- });
591
+ const res = new Store(this._getTokensFromCookies(parsed));
410
592
  this._requestTokenStores.set(tokenStoreInit, res);
411
593
  return res;
412
594
  } else if ("accessToken" in tokenStoreInit || "refreshToken" in tokenStoreInit) {