@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.
- 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 +2 -2
|
@@ -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,
|
|
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
|
|
299
|
-
const
|
|
300
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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(
|
|
373
|
-
setOrDeleteCookie(this._accessTokenCookieName,
|
|
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) {
|