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