@stackframe/js 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 (28) 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/interfaces/admin-app.js.map +1 -1
  13. package/dist/esm/lib/stack-app/apps/interfaces/client-app.js.map +1 -1
  14. package/dist/index.d.mts +10 -1
  15. package/dist/index.d.ts +10 -1
  16. package/dist/integrations/convex.js +6 -0
  17. package/dist/integrations/convex.js.map +1 -1
  18. package/dist/lib/cookie.js +38 -7
  19. package/dist/lib/cookie.js.map +1 -1
  20. package/dist/lib/stack-app/apps/implementations/admin-app-impl.js +10 -0
  21. package/dist/lib/stack-app/apps/implementations/admin-app-impl.js.map +1 -1
  22. package/dist/lib/stack-app/apps/implementations/client-app-impl.js +220 -25
  23. package/dist/lib/stack-app/apps/implementations/client-app-impl.js.map +1 -1
  24. package/dist/lib/stack-app/apps/implementations/common.js +1 -1
  25. package/dist/lib/stack-app/apps/implementations/common.js.map +1 -1
  26. package/dist/lib/stack-app/apps/interfaces/admin-app.js.map +1 -1
  27. package/dist/lib/stack-app/apps/interfaces/client-app.js.map +1 -1
  28. package/package.json +2 -2
@@ -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");
@@ -57,6 +58,7 @@ var import_projects = require("../../projects/index.js");
57
58
  var import_teams = require("../../teams/index.js");
58
59
  var import_users = require("../../users/index.js");
59
60
  var import_common2 = require("./common.js");
61
+ var import_json = require("@stackframe/stack-shared/dist/utils/json");
60
62
  var isReactServer = false;
61
63
  var process = globalThis.process ?? { env: {} };
62
64
  var allClientApps = /* @__PURE__ */ new Map();
@@ -205,11 +207,15 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
205
207
  this._convexPartialUserCache = (0, import_common2.createCache)(
206
208
  async ([ctx]) => await this._getPartialUserFromConvex(ctx)
207
209
  );
210
+ this._trustedParentDomainCache = (0, import_common2.createCache)(
211
+ async ([domain]) => await this._getTrustedParentDomain(domain)
212
+ );
208
213
  this._anonymousSignUpInProgress = null;
209
214
  this._memoryTokenStore = (0, import_common2.createEmptyTokenStore)();
210
215
  this._nextServerCookiesTokenStores = /* @__PURE__ */ new WeakMap();
211
216
  this._requestTokenStores = /* @__PURE__ */ new WeakMap();
212
217
  this._storedBrowserCookieTokenStore = null;
218
+ this._mostRecentQueuedCookieRefreshIndex = 0;
213
219
  /**
214
220
  * A map from token stores and session keys to sessions.
215
221
  *
@@ -325,13 +331,90 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
325
331
  (0, import_promises.runAsynchronously)(this._checkFeatureSupport(name, options));
326
332
  throw new import_errors.StackAssertionError(`${name} is not currently supported. Please reach out to Stack support for more information.`);
327
333
  }
334
+ get _legacyRefreshTokenCookieName() {
335
+ return `stack-refresh-${this.projectId}`;
336
+ }
328
337
  get _refreshTokenCookieName() {
329
338
  return `stack-refresh-${this.projectId}`;
330
339
  }
340
+ _getRefreshTokenDefaultCookieNameForSecure(secure) {
341
+ return `${secure ? "__Host-" : ""}${this._refreshTokenCookieName}--default`;
342
+ }
343
+ _getCustomRefreshCookieName(domain) {
344
+ const encoded = (0, import_bytes.encodeBase32)(new TextEncoder().encode(domain.toLowerCase()));
345
+ return `${this._refreshTokenCookieName}--custom-${encoded}`;
346
+ }
347
+ _formatRefreshCookieValue(refreshToken, updatedAt) {
348
+ return JSON.stringify({
349
+ refresh_token: refreshToken,
350
+ updated_at_millis: updatedAt
351
+ });
352
+ }
353
+ _formatAccessCookieValue(refreshToken, accessToken) {
354
+ return refreshToken && accessToken ? JSON.stringify([refreshToken, accessToken]) : null;
355
+ }
356
+ _parseStructuredRefreshCookie(value) {
357
+ if (!value) {
358
+ return null;
359
+ }
360
+ const parsed = (0, import_json.parseJson)(value);
361
+ if (parsed.status !== "ok" || typeof parsed.data !== "object" || parsed.data === null) {
362
+ console.warn("Failed to parse structured refresh cookie");
363
+ return null;
364
+ }
365
+ const data = parsed.data;
366
+ const refreshToken = "refresh_token" in data && typeof data.refresh_token === "string" ? data.refresh_token : null;
367
+ const updatedAt = "updated_at_millis" in data && typeof data.updated_at_millis === "number" ? data.updated_at_millis : null;
368
+ if (!refreshToken) {
369
+ console.warn("Refresh token not found in structured refresh cookie");
370
+ return null;
371
+ }
372
+ return {
373
+ refreshToken,
374
+ updatedAt
375
+ };
376
+ }
377
+ _extractRefreshTokenFromCookieMap(cookies) {
378
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
379
+ for (const name of legacyNames) {
380
+ const value = cookies[name];
381
+ if (value) {
382
+ return { refreshToken: value, updatedAt: null };
383
+ }
384
+ }
385
+ let selected = null;
386
+ for (const [name, value] of Object.entries(cookies)) {
387
+ if (!structuredPrefixes.some((prefix) => name.startsWith(prefix))) continue;
388
+ const parsed = this._parseStructuredRefreshCookie(value);
389
+ if (!parsed) continue;
390
+ const candidateUpdatedAt = parsed.updatedAt ?? Number.NEGATIVE_INFINITY;
391
+ const selectedUpdatedAt = selected?.updatedAt ?? Number.NEGATIVE_INFINITY;
392
+ if (!selected || candidateUpdatedAt > selectedUpdatedAt) {
393
+ selected = parsed;
394
+ }
395
+ }
396
+ if (!selected) {
397
+ return { refreshToken: null, updatedAt: null };
398
+ }
399
+ return {
400
+ refreshToken: selected.refreshToken,
401
+ updatedAt: selected.updatedAt ?? null
402
+ };
403
+ }
331
404
  _getTokensFromCookies(cookies) {
332
- const refreshToken = cookies.refreshTokenCookie;
333
- const accessTokenObject = cookies.accessTokenCookie?.startsWith('["') ? JSON.parse(cookies.accessTokenCookie) : null;
334
- const accessToken = accessTokenObject && refreshToken === accessTokenObject[0] ? accessTokenObject[1] : null;
405
+ const { refreshToken } = this._extractRefreshTokenFromCookieMap(cookies);
406
+ const accessTokenCookie = cookies[this._accessTokenCookieName] ?? null;
407
+ let accessToken = null;
408
+ if (accessTokenCookie && accessTokenCookie.startsWith('["')) {
409
+ const parsed = (0, import_json.parseJson)(accessTokenCookie);
410
+ 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") {
411
+ if (parsed.data[0] === refreshToken) {
412
+ accessToken = parsed.data[1];
413
+ }
414
+ } else {
415
+ console.warn("Access token cookie has invalid format");
416
+ }
417
+ }
335
418
  return {
336
419
  refreshToken,
337
420
  accessToken
@@ -340,17 +423,97 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
340
423
  get _accessTokenCookieName() {
341
424
  return `stack-access`;
342
425
  }
426
+ _getAllBrowserCookies() {
427
+ if (!(0, import_env.isBrowserLike)()) {
428
+ throw new import_errors.StackAssertionError("Cannot get browser cookies on the server!");
429
+ }
430
+ return cookie.parse(document.cookie || "");
431
+ }
432
+ _getRefreshTokenCookieNamePatterns() {
433
+ return {
434
+ legacyNames: [this._legacyRefreshTokenCookieName, "stack-refresh"],
435
+ structuredPrefixes: [
436
+ `${this._refreshTokenCookieName}--`,
437
+ `__Host-${this._refreshTokenCookieName}--`
438
+ ]
439
+ };
440
+ }
441
+ _collectRefreshTokenCookieNames(cookies) {
442
+ const { legacyNames, structuredPrefixes } = this._getRefreshTokenCookieNamePatterns();
443
+ const names = /* @__PURE__ */ new Set();
444
+ for (const name of legacyNames) {
445
+ if (cookies[name]) {
446
+ names.add(name);
447
+ }
448
+ }
449
+ for (const name of Object.keys(cookies)) {
450
+ if (structuredPrefixes.some((prefix) => name.startsWith(prefix))) {
451
+ names.add(name);
452
+ }
453
+ }
454
+ return names;
455
+ }
456
+ _prepareRefreshCookieUpdate(existingCookies, refreshToken, accessToken, defaultCookieName) {
457
+ const cookieNames = this._collectRefreshTokenCookieNames(existingCookies);
458
+ cookieNames.delete(defaultCookieName);
459
+ const updatedAt = refreshToken ? Date.now() : null;
460
+ const refreshCookieValue = refreshToken && updatedAt !== null ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
461
+ const accessTokenPayload = this._formatAccessCookieValue(refreshToken, accessToken);
462
+ return {
463
+ updatedAt,
464
+ refreshCookieValue,
465
+ accessTokenPayload,
466
+ cookieNamesToDelete: [...cookieNames]
467
+ };
468
+ }
469
+ _queueCustomRefreshCookieUpdate(refreshToken, updatedAt, context) {
470
+ (0, import_promises.runAsynchronously)(async () => {
471
+ this._mostRecentQueuedCookieRefreshIndex++;
472
+ const updateIndex = this._mostRecentQueuedCookieRefreshIndex;
473
+ let hostname;
474
+ if ((0, import_env.isBrowserLike)()) {
475
+ hostname = window.location.hostname;
476
+ }
477
+ if (!hostname) {
478
+ return;
479
+ }
480
+ const domain = await this._trustedParentDomainCache.getOrWait([hostname], "read-write");
481
+ const setCookie = async (targetDomain, value2) => {
482
+ const name = this._getCustomRefreshCookieName(targetDomain);
483
+ const options = { maxAge: 60 * 60 * 24 * 365, domain: targetDomain, noOpIfServerComponent: true };
484
+ if (context === "browser") {
485
+ (0, import_cookie.setOrDeleteCookieClient)(name, value2, options);
486
+ } else {
487
+ await (0, import_cookie.setOrDeleteCookie)(name, value2, options);
488
+ }
489
+ };
490
+ if (domain.status === "error" || !domain.data || updateIndex !== this._mostRecentQueuedCookieRefreshIndex) {
491
+ return;
492
+ }
493
+ const value = refreshToken && updatedAt ? this._formatRefreshCookieValue(refreshToken, updatedAt) : null;
494
+ await setCookie(domain.data, value);
495
+ });
496
+ }
497
+ async _getTrustedParentDomain(currentDomain) {
498
+ const project = import_results.Result.orThrow(await this._interface.getClientProject());
499
+ const domains = project.config.domains.map((d) => d.domain.trim().replace(/^https?:\/\//, "").split("/")[0]?.toLowerCase());
500
+ const trustedWildcards = domains.filter((d) => d.startsWith("**."));
501
+ const parts = currentDomain.split(".");
502
+ for (let i = parts.length - 2; i >= 0; i--) {
503
+ const parentDomain = parts.slice(i).join(".");
504
+ if (domains.includes(parentDomain) && trustedWildcards.includes("**." + parentDomain)) {
505
+ return parentDomain;
506
+ }
507
+ }
508
+ return null;
509
+ }
343
510
  _getBrowserCookieTokenStore() {
344
511
  if (!(0, import_env.isBrowserLike)()) {
345
512
  throw new Error("Cannot use cookie token store on the server!");
346
513
  }
347
514
  if (this._storedBrowserCookieTokenStore === null) {
348
515
  const getCurrentValue = (old) => {
349
- const tokens = this._getTokensFromCookies({
350
- refreshTokenCookie: (0, import_cookie.getCookieClient)(this._refreshTokenCookieName) ?? (0, import_cookie.getCookieClient)("stack-refresh"),
351
- // keep old cookie name for backwards-compatibility
352
- accessTokenCookie: (0, import_cookie.getCookieClient)(this._accessTokenCookieName)
353
- });
516
+ const tokens = this._getTokensFromCookies(this._getAllBrowserCookies());
354
517
  return {
355
518
  refreshToken: tokens.refreshToken,
356
519
  accessToken: tokens.accessToken ?? (old?.refreshToken === tokens.refreshToken ? old.accessToken : null)
@@ -369,9 +532,19 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
369
532
  }, 100);
370
533
  this._storedBrowserCookieTokenStore.onChange((value) => {
371
534
  try {
372
- (0, import_cookie.setOrDeleteCookieClient)(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365 });
373
- (0, import_cookie.setOrDeleteCookieClient)(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24 });
374
- (0, import_cookie.deleteCookieClient)("stack-refresh");
535
+ const refreshToken = value.refreshToken;
536
+ const secure = window.location.protocol === "https:";
537
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
538
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
539
+ this._getAllBrowserCookies(),
540
+ refreshToken,
541
+ value.accessToken ?? null,
542
+ defaultName
543
+ );
544
+ (0, import_cookie.setOrDeleteCookieClient)(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, secure });
545
+ (0, import_cookie.setOrDeleteCookieClient)(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24 });
546
+ cookieNamesToDelete.forEach((name) => (0, import_cookie.deleteCookieClient)(name));
547
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "browser");
375
548
  hasSucceededInWriting = true;
376
549
  } catch (e) {
377
550
  if (!(0, import_env.isBrowserLike)()) {
@@ -394,18 +567,31 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
394
567
  if ((0, import_env.isBrowserLike)()) {
395
568
  return this._getBrowserCookieTokenStore();
396
569
  } else {
397
- const tokens = this._getTokensFromCookies({
398
- refreshTokenCookie: cookieHelper.get(this._refreshTokenCookieName) ?? cookieHelper.get("stack-refresh"),
399
- // keep old cookie name for backwards-compatibility
400
- accessTokenCookie: cookieHelper.get(this._accessTokenCookieName)
401
- });
570
+ const tokens = this._getTokensFromCookies(cookieHelper.getAll());
402
571
  const store = new import_stores.Store(tokens);
403
572
  store.onChange((value) => {
404
573
  (0, import_promises.runAsynchronously)(async () => {
574
+ const refreshToken = value.refreshToken;
575
+ const secure = await (0, import_cookie.isSecure)();
576
+ const defaultName = this._getRefreshTokenDefaultCookieNameForSecure(secure);
577
+ const { updatedAt, refreshCookieValue, accessTokenPayload, cookieNamesToDelete } = this._prepareRefreshCookieUpdate(
578
+ cookieHelper.getAll(),
579
+ refreshToken,
580
+ value.accessToken ?? null,
581
+ defaultName
582
+ );
405
583
  await Promise.all([
406
- (0, import_cookie.setOrDeleteCookie)(this._refreshTokenCookieName, value.refreshToken, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
407
- (0, import_cookie.setOrDeleteCookie)(this._accessTokenCookieName, value.accessToken ? JSON.stringify([value.refreshToken, value.accessToken]) : null, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
584
+ (0, import_cookie.setOrDeleteCookie)(defaultName, refreshCookieValue, { maxAge: 60 * 60 * 24 * 365, noOpIfServerComponent: true }),
585
+ (0, import_cookie.setOrDeleteCookie)(this._accessTokenCookieName, accessTokenPayload, { maxAge: 60 * 60 * 24, noOpIfServerComponent: true })
408
586
  ]);
587
+ if (cookieNamesToDelete.length > 0) {
588
+ await Promise.all(
589
+ cookieNamesToDelete.map(
590
+ (name) => (0, import_cookie.setOrDeleteCookie)(name, null, { noOpIfServerComponent: true })
591
+ )
592
+ );
593
+ }
594
+ this._queueCustomRefreshCookieUpdate(refreshToken, updatedAt, "server");
409
595
  });
410
596
  });
411
597
  return store;
@@ -436,11 +622,7 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
436
622
  }
437
623
  const cookieHeader = tokenStoreInit.headers.get("cookie");
438
624
  const parsed = cookie.parse(cookieHeader || "");
439
- const res = new import_stores.Store({
440
- refreshToken: parsed[this._refreshTokenCookieName] || parsed["stack-refresh"] || null,
441
- // keep old cookie name for backwards-compatibility
442
- accessToken: parsed[this._accessTokenCookieName] || null
443
- });
625
+ const res = new import_stores.Store(this._getTokensFromCookies(parsed));
444
626
  this._requestTokenStores.set(tokenStoreInit, res);
445
627
  return res;
446
628
  } else if ("accessToken" in tokenStoreInit || "refreshToken" in tokenStoreInit) {
@@ -1384,15 +1566,28 @@ var __StackClientAppImplIncomplete = class __StackClientAppImplIncomplete {
1384
1566
  }
1385
1567
  }
1386
1568
  async signUpWithCredential(options) {
1569
+ if (options.noVerificationCallback && options.verificationCallbackUrl) {
1570
+ throw new import_errors.StackAssertionError("verificationCallbackUrl is not allowed when noVerificationCallback is true");
1571
+ }
1387
1572
  this._ensurePersistentTokenStore();
1388
1573
  const session = await this._getSession();
1389
- const emailVerificationRedirectUrl = options.verificationCallbackUrl ?? (0, import_url.constructRedirectUrl)(this.urls.emailVerification, "verificationCallbackUrl");
1390
- const result = await this._interface.signUpWithCredential(
1574
+ const emailVerificationRedirectUrl = options.noVerificationCallback ? void 0 : options.verificationCallbackUrl ?? (0, import_url.constructRedirectUrl)(this.urls.emailVerification, "verificationCallbackUrl");
1575
+ let result = await this._interface.signUpWithCredential(
1391
1576
  options.email,
1392
1577
  options.password,
1393
1578
  emailVerificationRedirectUrl,
1394
1579
  session
1395
1580
  );
1581
+ if (result.status === "error" && result.error instanceof import_stack_shared.KnownErrors.RedirectUrlNotWhitelisted && !options.noVerificationCallback && emailVerificationRedirectUrl !== void 0) {
1582
+ 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 });
1583
+ result = await this._interface.signUpWithCredential(
1584
+ options.email,
1585
+ options.password,
1586
+ void 0,
1587
+ // No email verification
1588
+ session
1589
+ );
1590
+ }
1396
1591
  if (result.status === "ok") {
1397
1592
  await this._signInToAccountWithTokens(result.data);
1398
1593
  if (!options.noRedirect) {