@stackframe/stack 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 +57 -14
  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 +222 -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 +59 -14
  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 +221 -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 +4 -4
@@ -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_compile_time = require("@stackframe/stack-shared/dist/utils/compile-time");
40
41
  var import_env = require("@stackframe/stack-shared/dist/utils/env");
41
42
  var import_errors = require("@stackframe/stack-shared/dist/utils/errors");
@@ -61,6 +62,7 @@ var import_projects = require("../../projects/index.js");
61
62
  var import_teams = require("../../teams/index.js");
62
63
  var import_users = require("../../users/index.js");
63
64
  var import_common2 = require("./common.js");
65
+ var import_json = require("@stackframe/stack-shared/dist/utils/json");
64
66
  var import_common3 = require("./common.js");
65
67
  var sc = __toESM(require("@stackframe/stack-sc"));
66
68
  var import_stack_sc = require("@stackframe/stack-sc");
@@ -214,11 +216,15 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
214
216
  this._convexPartialUserCache = (0, import_common2.createCache)(
215
217
  async ([ctx]) => await this._getPartialUserFromConvex(ctx)
216
218
  );
219
+ this._trustedParentDomainCache = (0, import_common2.createCache)(
220
+ async ([domain]) => await this._getTrustedParentDomain(domain)
221
+ );
217
222
  this._anonymousSignUpInProgress = null;
218
223
  this._memoryTokenStore = (0, import_common2.createEmptyTokenStore)();
219
224
  this._nextServerCookiesTokenStores = /* @__PURE__ */ new WeakMap();
220
225
  this._requestTokenStores = /* @__PURE__ */ new WeakMap();
221
226
  this._storedBrowserCookieTokenStore = null;
227
+ this._mostRecentQueuedCookieRefreshIndex = 0;
222
228
  /**
223
229
  * A map from token stores and session keys to sessions.
224
230
  *
@@ -343,13 +349,90 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
343
349
  (0, import_promises.runAsynchronously)(this._checkFeatureSupport(name, options));
344
350
  throw new import_errors.StackAssertionError(`${name} is not currently supported. Please reach out to Stack support for more information.`);
345
351
  }
352
+ get _legacyRefreshTokenCookieName() {
353
+ return `stack-refresh-${this.projectId}`;
354
+ }
346
355
  get _refreshTokenCookieName() {
347
356
  return `stack-refresh-${this.projectId}`;
348
357
  }
358
+ _getRefreshTokenDefaultCookieNameForSecure(secure) {
359
+ return `${secure ? "__Host-" : ""}${this._refreshTokenCookieName}--default`;
360
+ }
361
+ _getCustomRefreshCookieName(domain) {
362
+ const encoded = (0, import_bytes.encodeBase32)(new TextEncoder().encode(domain.toLowerCase()));
363
+ return `${this._refreshTokenCookieName}--custom-${encoded}`;
364
+ }
365
+ _formatRefreshCookieValue(refreshToken, updatedAt) {
366
+ return JSON.stringify({
367
+ refresh_token: refreshToken,
368
+ updated_at_millis: updatedAt
369
+ });
370
+ }
371
+ _formatAccessCookieValue(refreshToken, accessToken) {
372
+ return refreshToken && accessToken ? JSON.stringify([refreshToken, accessToken]) : null;
373
+ }
374
+ _parseStructuredRefreshCookie(value) {
375
+ if (!value) {
376
+ return null;
377
+ }
378
+ const parsed = (0, import_json.parseJson)(value);
379
+ if (parsed.status !== "ok" || typeof parsed.data !== "object" || parsed.data === null) {
380
+ console.warn("Failed to parse structured refresh cookie");
381
+ return null;
382
+ }
383
+ const data = parsed.data;
384
+ const refreshToken = "refresh_token" in data && typeof data.refresh_token === "string" ? data.refresh_token : null;
385
+ const updatedAt = "updated_at_millis" in data && typeof data.updated_at_millis === "number" ? data.updated_at_millis : null;
386
+ if (!refreshToken) {
387
+ console.warn("Refresh token not found in structured refresh cookie");
388
+ return null;
389
+ }
390
+ return {
391
+ refreshToken,
392
+ updatedAt
393
+ };
394
+ }
395
+ _extractRefreshTokenFromCookieMap(cookies2) {
396
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
397
+ for (const name of legacyNames) {
398
+ const value = cookies2[name];
399
+ if (value) {
400
+ return { refreshToken: value, updatedAt: null };
401
+ }
402
+ }
403
+ let selected = null;
404
+ for (const [name, value] of Object.entries(cookies2)) {
405
+ if (!structuredPrefixes.some((prefix) => name.startsWith(prefix))) continue;
406
+ const parsed = this._parseStructuredRefreshCookie(value);
407
+ if (!parsed) continue;
408
+ const candidateUpdatedAt = parsed.updatedAt ?? Number.NEGATIVE_INFINITY;
409
+ const selectedUpdatedAt = selected?.updatedAt ?? Number.NEGATIVE_INFINITY;
410
+ if (!selected || candidateUpdatedAt > selectedUpdatedAt) {
411
+ selected = parsed;
412
+ }
413
+ }
414
+ if (!selected) {
415
+ return { refreshToken: null, updatedAt: null };
416
+ }
417
+ return {
418
+ refreshToken: selected.refreshToken,
419
+ updatedAt: selected.updatedAt ?? null
420
+ };
421
+ }
349
422
  _getTokensFromCookies(cookies2) {
350
- const refreshToken = cookies2.refreshTokenCookie;
351
- const accessTokenObject = cookies2.accessTokenCookie?.startsWith('["') ? JSON.parse(cookies2.accessTokenCookie) : null;
352
- const accessToken = accessTokenObject && refreshToken === accessTokenObject[0] ? accessTokenObject[1] : null;
423
+ const { refreshToken } = this._extractRefreshTokenFromCookieMap(cookies2);
424
+ const accessTokenCookie = cookies2[this._accessTokenCookieName] ?? null;
425
+ let accessToken = null;
426
+ if (accessTokenCookie && accessTokenCookie.startsWith('["')) {
427
+ const parsed = (0, import_json.parseJson)(accessTokenCookie);
428
+ 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") {
429
+ if (parsed.data[0] === refreshToken) {
430
+ accessToken = parsed.data[1];
431
+ }
432
+ } else {
433
+ console.warn("Access token cookie has invalid format");
434
+ }
435
+ }
353
436
  return {
354
437
  refreshToken,
355
438
  accessToken
@@ -358,17 +441,98 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
358
441
  get _accessTokenCookieName() {
359
442
  return `stack-access`;
360
443
  }
444
+ _getAllBrowserCookies() {
445
+ if (!(0, import_env.isBrowserLike)()) {
446
+ throw new import_errors.StackAssertionError("Cannot get browser cookies on the server!");
447
+ }
448
+ return cookie.parse(document.cookie || "");
449
+ }
450
+ _getRefreshTokenCookieNamePatterns() {
451
+ return {
452
+ legacyNames: [this._legacyRefreshTokenCookieName, "stack-refresh"],
453
+ structuredPrefixes: [
454
+ `${this._refreshTokenCookieName}--`,
455
+ `__Host-${this._refreshTokenCookieName}--`
456
+ ]
457
+ };
458
+ }
459
+ _collectRefreshTokenCookieNames(cookies2) {
460
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
461
+ const names = /* @__PURE__ */ new Set();
462
+ for (const name of legacyNames) {
463
+ if (cookies2[name]) {
464
+ names.add(name);
465
+ }
466
+ }
467
+ for (const name of Object.keys(cookies2)) {
468
+ if (structuredPrefixes.some((prefix) => name.startsWith(prefix))) {
469
+ names.add(name);
470
+ }
471
+ }
472
+ return names;
473
+ }
474
+ _prepareRefreshCookieUpdate(existingCookies, refreshToken, accessToken, defaultCookieName) {
475
+ const cookieNames = this._collectRefreshTokenCookieNames(existingCookies);
476
+ cookieNames.delete(defaultCookieName);
477
+ const updatedAt = refreshToken ? Date.now() : null;
478
+ const refreshCookieValue = refreshToken && updatedAt !== null ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
479
+ const accessTokenPayload = this._formatAccessCookieValue(refreshToken, accessToken);
480
+ return {
481
+ updatedAt,
482
+ refreshCookieValue,
483
+ accessTokenPayload,
484
+ cookieNamesToDelete: [...cookieNames]
485
+ };
486
+ }
487
+ _queueCustomRefreshCookieUpdate(refreshToken, updatedAt, context) {
488
+ (0, import_promises.runAsynchronously)(async () => {
489
+ this._mostRecentQueuedCookieRefreshIndex++;
490
+ const updateIndex = this._mostRecentQueuedCookieRefreshIndex;
491
+ let hostname;
492
+ if ((0, import_env.isBrowserLike)()) {
493
+ hostname = window.location.hostname;
494
+ }
495
+ hostname = (await sc.headers?.())?.get("host");
496
+ if (!hostname) {
497
+ return;
498
+ }
499
+ const domain = await this._trustedParentDomainCache.getOrWait([hostname], "read-write");
500
+ const setCookie = async (targetDomain, value2) => {
501
+ const name = this._getCustomRefreshCookieName(targetDomain);
502
+ const options = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true };
503
+ if (context === "browser") {
504
+ (0, import_cookie.setOrDeleteCookieClient)(name, value2, options);
505
+ } else {
506
+ await (0, import_cookie.setOrDeleteCookie)(name, value2, options);
507
+ }
508
+ };
509
+ if (domain.status === "error" || !domain.data || updateIndex !== this._mostRecentQueuedCookieRefreshIndex) {
510
+ return;
511
+ }
512
+ const value = refreshToken && updatedAt ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
513
+ await setCookie(domain.data, value);
514
+ });
515
+ }
516
+ async _getTrustedParentDomain(currentDomain) {
517
+ const project = import_results.Result.orThrow(await this._interface.getClientProject());
518
+ const domains = project.config.domains.map((d) => d.domain.trim().replace(/^https?:\/\//, "").split("/")[0]?.toLowerCase());
519
+ const trustedWildcards = domains.filter((d) => d.startsWith("**."));
520
+ const parts = currentDomain.split(".");
521
+ for (let i = parts.length - 2; i >= 0; i--) {
522
+ const parentDomain = parts.slice(i).join(".");
523
+ if (domains.includes(parentDomain) && trustedWildcards.includes("**." + parentDomain)) {
524
+ return parentDomain;
525
+ }
526
+ }
527
+ return null;
528
+ }
361
529
  _getBrowserCookieTokenStore() {
362
530
  if (!(0, import_env.isBrowserLike)()) {
363
531
  throw new Error("Cannot use cookie token store on the server!");
364
532
  }
365
533
  if (this._storedBrowserCookieTokenStore === null) {
366
534
  const getCurrentValue = (old) => {
367
- const tokens = this._getTokensFromCookies({
368
- refreshTokenCookie: (0, import_cookie.getCookieClient)(this._refreshTokenCookieName) ?? (0, import_cookie.getCookieClient)("stack-refresh"),
369
- // keep old cookie name for backwards-compatibility
370
- accessTokenCookie: (0, import_cookie.getCookieClient)(this._accessTokenCookieName)
371
- });
535
+ const tokens = this._getTokensFromCookies(this._getAllBrowserCookies());
372
536
  return {
373
537
  refreshToken: tokens.refreshToken,
374
538
  accessToken: tokens.accessToken ?? (old?.refreshToken === tokens.refreshToken ? old.accessToken : null)
@@ -387,9 +551,19 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
387
551
  }, 100);
388
552
  this._storedBrowserCookieTokenStore.onChange((value) => {
389
553
  try {
390
- (0, import_cookie.setOrDeleteCookieClient)(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365 });
391
- (0, import_cookie.setOrDeleteCookieClient)(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24 });
392
- (0, import_cookie.deleteCookieClient)("stack-refresh");
554
+ const refreshToken = value.refreshToken;
555
+ const secure = window.location.protocol === "https:";
556
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
557
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
558
+ this._getAllBrowserCookies(),
559
+ refreshToken,
560
+ value.accessToken ?? null,
561
+ defaultName
562
+ );
563
+ (0, import_cookie.setOrDeleteCookieClient)(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, secure });
564
+ (0, import_cookie.setOrDeleteCookieClient)(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24 });
565
+ cookieNamesToDelete.forEach((name) => (0, import_cookie.deleteCookieClient)(name));
566
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "browser");
393
567
  hasSucceededInWriting = true;
394
568
  } catch (e) {
395
569
  if (!(0, import_env.isBrowserLike)()) {
@@ -412,18 +586,31 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
412
586
  if ((0, import_env.isBrowserLike)()) {
413
587
  return this._getBrowserCookieTokenStore();
414
588
  } else {
415
- const tokens = this._getTokensFromCookies({
416
- refreshTokenCookie: cookieHelper.get(this._refreshTokenCookieName) ?? cookieHelper.get("stack-refresh"),
417
- // keep old cookie name for backwards-compatibility
418
- accessTokenCookie: cookieHelper.get(this._accessTokenCookieName)
419
- });
589
+ const tokens = this._getTokensFromCookies(cookieHelper.getAll());
420
590
  const store = new import_stores.Store(tokens);
421
591
  store.onChange((value) => {
422
592
  (0, import_promises.runAsynchronously)(async () => {
593
+ const refreshToken = value.refreshToken;
594
+ const secure = await (0, import_cookie.isSecure)();
595
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
596
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
597
+ cookieHelper.getAll(),
598
+ refreshToken,
599
+ value.accessToken ?? null,
600
+ defaultName
601
+ );
423
602
  await Promise.all([
424
- (0, import_cookie.setOrDeleteCookie)(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
425
- (0, import_cookie.setOrDeleteCookie)(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
603
+ (0, import_cookie.setOrDeleteCookie)(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
604
+ (0, import_cookie.setOrDeleteCookie)(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
426
605
  ]);
606
+ if (cookieNamesToDelete.length > 0) {
607
+ await Promise.all(
608
+ cookieNamesToDelete.map(
609
+ (name) => (0, import_cookie.setOrDeleteCookie)(name, null, { noOpIfServerComponent: true })
610
+ )
611
+ );
612
+ }
613
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "server");
427
614
  });
428
615
  });
429
616
  return store;
@@ -454,11 +641,7 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
454
641
  }
455
642
  const cookieHeader = tokenStoreInit.headers.get("cookie");
456
643
  const parsed = cookie.parse(cookieHeader || "");
457
- const res = new import_stores.Store({
458
- refreshToken: parsed[this._refreshTokenCookieName] || parsed["stack-refresh"] || null,
459
- // keep old cookie name for backwards-compatibility
460
- accessToken: parsed[this._accessTokenCookieName] || null
461
- });
644
+ const res = new import_stores.Store(this._getTokensFromCookies(parsed));
462
645
  this._requestTokenStores.set(tokenStoreInit, res);
463
646
  return res;
464
647
  } else if ("accessToken" in tokenStoreInit || "refreshToken" in tokenStoreInit) {
@@ -1586,15 +1769,28 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
1586
1769
  }
1587
1770
  }
1588
1771
  async signUpWithCredential(options) {
1772
+ if (options.noVerificationCallback && options.verificationCallbackUrl) {
1773
+ throw new import_errors.StackAssertionError("verificationCallbackUrl is not allowed when noVerificationCallback is true");
1774
+ }
1589
1775
  this._ensurePersistentTokenStore();
1590
1776
  const session = await this._getSession();
1591
- const emailVerificationRedirectUrl = options.verificationCallbackUrl ?? (0, import_url.constructRedirectUrl)(this.urls.emailVerification, "verificationCallbackUrl");
1592
- const result = await this._interface.signUpWithCredential(
1777
+ const emailVerificationRedirectUrl = options.noVerificationCallback ? void 0 : options.verificationCallbackUrl ?? (0, import_url.constructRedirectUrl)(this.urls.emailVerification, "verificationCallbackUrl");
1778
+ let result = await this._interface.signUpWithCredential(
1593
1779
  options.email,
1594
1780
  options.password,
1595
1781
  emailVerificationRedirectUrl,
1596
1782
  session
1597
1783
  );
1784
+ if (result.status === "error" && result.error instanceof import_stack_shared.KnownErrors.RedirectUrlNotWhitelisted && !options.noVerificationCallback && emailVerificationRedirectUrl !== void 0) {
1785
+ 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 });
1786
+ result = await this._interface.signUpWithCredential(
1787
+ options.email,
1788
+ options.password,
1789
+ void 0,
1790
+ // No email verification
1791
+ session
1792
+ );
1793
+ }
1598
1794
  if (result.status === "ok") {
1599
1795
  await this._signInToAccountWithTokens(result.data);
1600
1796
  if (!options.noRedirect) {