@hexclave/next 1.0.51 → 1.0.53
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/dist/{admin-app-impl-BGNL1KyZ.d.ts → admin-app-impl-Bga0o6cL.d.ts} +11 -8
- package/dist/{admin-app-impl-BGNL1KyZ.d.ts.map → admin-app-impl-Bga0o6cL.d.ts.map} +1 -1
- package/dist/clickmap/clickmap-core.d.ts +1 -1
- package/dist/clickmap/index.d.ts +1 -1
- package/dist/{client-app-Majjw8Xp.d.ts → client-app-B0kBC67Z.d.ts} +2 -1
- package/dist/{client-app-Majjw8Xp.d.ts.map → client-app-B0kBC67Z.d.ts.map} +1 -1
- package/dist/common-BK_sydxF.d.ts.map +1 -1
- package/dist/components/api-key-dialogs.d.ts +1 -1
- package/dist/components/selected-team-switcher.d.ts +1 -1
- package/dist/components/team-icon.d.ts +1 -1
- package/dist/components/team-switcher.d.ts +1 -1
- package/dist/components-page/account-settings/payments/payments-page.d.ts +1 -1
- package/dist/components-page/account-settings/teams/leave-team-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-api-keys-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-display-name-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-member-invitation-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-member-list-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-page.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-profile-image-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-profile-user-section.d.ts +1 -1
- package/dist/components-page/hexclave-handler-client.d.ts +1 -1
- package/dist/components-page/hexclave-handler.d.ts +1 -1
- package/dist/components-page/sign-out.js +11 -2
- package/dist/components-page/sign-out.js.map +1 -1
- package/dist/components-page/sign-out.test.d.ts +1 -0
- package/dist/components-page/sign-out.test.js +157 -0
- package/dist/components-page/sign-out.test.js.map +1 -0
- package/dist/dev-tool/dev-tool-core.d.ts +1 -1
- package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
- package/dist/dev-tool/dev-tool-core.js +58 -28
- package/dist/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/dev-tool/dev-tool-styles.js +35 -0
- package/dist/dev-tool/dev-tool-styles.js.map +1 -1
- package/dist/dev-tool/index.d.ts +1 -1
- package/dist/esm/components/api-key-dialogs.d.ts +1 -1
- package/dist/esm/components-page/sign-out.d.ts.map +1 -1
- package/dist/esm/components-page/sign-out.js +12 -3
- package/dist/esm/components-page/sign-out.js.map +1 -1
- package/dist/esm/components-page/sign-out.test.d.ts +1 -0
- package/dist/esm/components-page/sign-out.test.js +156 -0
- package/dist/esm/components-page/sign-out.test.js.map +1 -0
- package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
- package/dist/esm/dev-tool/dev-tool-core.js +58 -28
- package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/esm/dev-tool/dev-tool-styles.js +35 -0
- package/dist/esm/dev-tool/dev-tool-styles.js.map +1 -1
- package/dist/esm/generated/global-css.d.ts +1 -1
- package/dist/esm/generated/global-css.js +1 -1
- package/dist/esm/generated/global-css.js.map +1 -1
- package/dist/esm/generated/quetzal-translations.d.ts +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +5 -4
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +97 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +3 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +26 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +21 -4
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts +37 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.js +89 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.js +134 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.d.ts +8 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.d.ts.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.js +73 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.js +92 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +27 -6
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/esm/pushed-config-error-overlay/index.js +1 -1
- package/dist/esm/pushed-config-error-overlay/index.js.map +1 -1
- package/dist/esm/utils/session-storage.d.ts +12 -0
- package/dist/esm/utils/session-storage.d.ts.map +1 -0
- package/dist/esm/utils/session-storage.js +30 -0
- package/dist/esm/utils/session-storage.js.map +1 -0
- package/dist/generated/global-css.d.ts +1 -1
- package/dist/generated/global-css.js +1 -1
- package/dist/generated/global-css.js.map +1 -1
- package/dist/generated/quetzal-translations.d.ts +1 -1
- package/dist/{hexclave-provider-IAd7nlbi.d.ts → hexclave-provider-SQbs9rmZ.d.ts} +3 -3
- package/dist/{hexclave-provider-IAd7nlbi.d.ts.map → hexclave-provider-SQbs9rmZ.d.ts.map} +1 -1
- package/dist/{hooks-BKXOF6jh.d.ts → hooks-ykQ7Wsco.d.ts} +2 -2
- package/dist/{hooks-BKXOF6jh.d.ts.map → hooks-ykQ7Wsco.d.ts.map} +1 -1
- package/dist/{index-Ck1qKoin.d.ts → index-DwE1cNLH.d.ts} +2 -2
- package/dist/{index-Ck1qKoin.d.ts.map → index-DwE1cNLH.d.ts.map} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +97 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +26 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +21 -4
- package/dist/lib/hexclave-app/apps/implementations/common.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts +37 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.js +92 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.js +133 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.d.ts +8 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.d.ts.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.js +74 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.js +91 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +27 -6
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/index.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
- package/dist/lib/hexclave-app/index.d.ts +1 -1
- package/dist/lib/hexclave-app/projects/index.d.ts +1 -1
- package/dist/lib/hexclave-app/teams/index.d.ts +1 -1
- package/dist/lib/hexclave-app/users/index.d.ts +1 -1
- package/dist/lib/hooks.d.ts +1 -1
- package/dist/providers/hexclave-context.d.ts +1 -1
- package/dist/providers/hexclave-provider-client.d.ts +1 -1
- package/dist/providers/hexclave-provider.d.ts +1 -1
- package/dist/providers/translation-provider.d.ts +1 -1
- package/dist/pushed-config-error-overlay/index.d.ts +1 -1
- package/dist/pushed-config-error-overlay/index.js +1 -1
- package/dist/pushed-config-error-overlay/index.js.map +1 -1
- package/dist/{quetzal-translations-XeH3zQ9l.d.ts → quetzal-translations-CK45_4Dm.d.ts} +3 -3
- package/dist/{quetzal-translations-XeH3zQ9l.d.ts.map → quetzal-translations-CK45_4Dm.d.ts.map} +1 -1
- package/dist/sign-out-DBnSkfDz.d.ts.map +1 -1
- package/dist/{translation-provider-WOaZXqAz.d.ts → translation-provider-DQ3BJepJ.d.ts} +2 -2
- package/dist/{translation-provider-WOaZXqAz.d.ts.map → translation-provider-DQ3BJepJ.d.ts.map} +1 -1
- package/dist/utils/session-storage.d.ts +12 -0
- package/dist/utils/session-storage.d.ts.map +1 -0
- package/dist/utils/session-storage.js +33 -0
- package/dist/utils/session-storage.js.map +1 -0
- package/package.json +4 -4
- package/src/components-page/sign-out.test.tsx +190 -0
- package/src/components-page/sign-out.tsx +22 -3
- package/src/dev-tool/dev-tool-core.ts +100 -35
- package/src/dev-tool/dev-tool-styles.ts +35 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +127 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +34 -1
- package/src/lib/hexclave-app/apps/implementations/common.ts +18 -1
- package/src/lib/hexclave-app/apps/implementations/redirect-back-state.test.ts +134 -0
- package/src/lib/hexclave-app/apps/implementations/redirect-back-state.ts +140 -0
- package/src/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.ts +127 -0
- package/src/lib/hexclave-app/apps/implementations/redirect-loop-breaker.ts +117 -0
- package/src/lib/hexclave-app/apps/implementations/server-app-impl.ts +31 -5
- package/src/lib/hexclave-app/apps/interfaces/client-app.ts +1 -0
- package/src/pushed-config-error-overlay/index.ts +1 -1
- package/src/utils/session-storage.ts +45 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
|
|
2
|
+
//===========================================
|
|
3
|
+
// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
|
|
4
|
+
//===========================================
|
|
5
|
+
import { HexclaveAssertionError, captureError } from "@hexclave/shared/dist/utils/errors";
|
|
6
|
+
import { readSessionStorageItem, removeSessionStorageItem, writeSessionStorageItem } from "../../../../utils/session-storage";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Last line of defense against redirect loops (most importantly in hosted components flows, where
|
|
10
|
+
* redirects bounce between the app domain and the hosted domain and a bug anywhere in the chain
|
|
11
|
+
* can bounce the user forever).
|
|
12
|
+
*
|
|
13
|
+
* Every SDK-driven redirect records a breadcrumb, and when the exact same redirect keeps repeating
|
|
14
|
+
* within a short window, the redirect throws instead of navigating. The page that initiated the
|
|
15
|
+
* redirect then renders its error state (eg. the auth page's error card), which both stops the
|
|
16
|
+
* loop and surfaces the bug loudly.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// sessionStorage (not module state) because most redirect loops involve full page loads, so each
|
|
20
|
+
// loop iteration runs in a fresh JS context. sessionStorage is also per-tab, which is exactly the
|
|
21
|
+
// scope of a redirect loop.
|
|
22
|
+
const breadcrumbsStorageKey = "hexclave-redirect-loop-breadcrumbs";
|
|
23
|
+
|
|
24
|
+
// A redirect loop cycles within a few seconds per iteration even with full cross-domain page
|
|
25
|
+
// loads, so a real loop reaches 5 identical redirects well within 30 seconds. A human bouncing
|
|
26
|
+
// between pages (eg. toggling sign-in <-> sign-up) produces the same (from, to) pair far less
|
|
27
|
+
// often, which keeps false positives implausible.
|
|
28
|
+
const loopWindowMillis = 30_000;
|
|
29
|
+
const maxIdenticalRedirectsPerWindow = 5;
|
|
30
|
+
const maxStoredBreadcrumbs = 50;
|
|
31
|
+
|
|
32
|
+
type RedirectBreadcrumb = {
|
|
33
|
+
from: string,
|
|
34
|
+
to: string,
|
|
35
|
+
/**
|
|
36
|
+
* Wall-clock milliseconds (Date.now()). Although this is used to measure elapsed time (where we
|
|
37
|
+
* would normally use performance.now()), breadcrumbs must be comparable across full page loads,
|
|
38
|
+
* and performance.now() restarts at zero on every navigation.
|
|
39
|
+
*/
|
|
40
|
+
at: number,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function isRedirectBreadcrumb(value: unknown): value is RedirectBreadcrumb {
|
|
44
|
+
return (
|
|
45
|
+
typeof value === "object"
|
|
46
|
+
&& value !== null
|
|
47
|
+
&& "from" in value && typeof value.from === "string"
|
|
48
|
+
&& "to" in value && typeof value.to === "string"
|
|
49
|
+
&& "at" in value && typeof value.at === "number"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readBreadcrumbs(): RedirectBreadcrumb[] {
|
|
54
|
+
const raw = readSessionStorageItem(breadcrumbsStorageKey);
|
|
55
|
+
if (raw == null) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
let parsed: unknown;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(raw);
|
|
61
|
+
} catch {
|
|
62
|
+
// A corrupted value (eg. written by an older SDK version) just means we lose loop protection
|
|
63
|
+
// for this one redirect; start over with a fresh list.
|
|
64
|
+
removeSessionStorageItem(breadcrumbsStorageKey);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(parsed)) {
|
|
68
|
+
removeSessionStorageItem(breadcrumbsStorageKey);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
return parsed.filter(isRedirectBreadcrumb);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeBreadcrumbs(breadcrumbs: RedirectBreadcrumb[]): void {
|
|
75
|
+
writeSessionStorageItem(breadcrumbsStorageKey, JSON.stringify(breadcrumbs));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Loop iterations usually differ only in nonce-style query params (`code`, `state`,
|
|
80
|
+
* `after_auth_return_to`, ...), so redirects are compared by origin + pathname only.
|
|
81
|
+
*/
|
|
82
|
+
function getComparableRedirectLocation(url: URL): string {
|
|
83
|
+
return `${url.origin}${url.pathname}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function recordRedirectAndThrowIfLoopDetected(options: { currentUrl: URL, targetUrl: URL }): void {
|
|
87
|
+
const from = getComparableRedirectLocation(options.currentUrl);
|
|
88
|
+
const to = getComparableRedirectLocation(options.targetUrl);
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
|
|
91
|
+
// Clock jumps backwards (eg. NTP sync) would make breadcrumbs appear to be from the future;
|
|
92
|
+
// treat those as expired rather than counting them towards a loop.
|
|
93
|
+
const recentBreadcrumbs = readBreadcrumbs().filter((b) => now - b.at >= 0 && now - b.at < loopWindowMillis);
|
|
94
|
+
const identicalRedirectCount = recentBreadcrumbs.filter((b) => b.from === from && b.to === to).length + 1;
|
|
95
|
+
|
|
96
|
+
if (identicalRedirectCount >= maxIdenticalRedirectsPerWindow) {
|
|
97
|
+
// Drop the matching breadcrumbs so a manual retry (eg. the user reloading and clicking sign-in
|
|
98
|
+
// again) isn't instantly blocked by the history of the loop we just broke.
|
|
99
|
+
writeBreadcrumbs(recentBreadcrumbs.filter((b) => !(b.from === from && b.to === to)));
|
|
100
|
+
const error = new HexclaveAssertionError(
|
|
101
|
+
`Redirect loop detected: the redirect ${from} -> ${to} was attempted ${identicalRedirectCount} times within ${loopWindowMillis / 1000}s, so it was blocked. This is a bug in the auth flow; the recent redirect chain is included in the error details.`,
|
|
102
|
+
{
|
|
103
|
+
from,
|
|
104
|
+
to,
|
|
105
|
+
currentUrl: options.currentUrl.toString(),
|
|
106
|
+
targetUrl: options.targetUrl.toString(),
|
|
107
|
+
recentRedirects: recentBreadcrumbs.map((b) => `${b.from} -> ${b.to}`),
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
// Also captured (not just thrown) so the loop is reported even if a caller maps the thrown
|
|
111
|
+
// error to UI without reporting it.
|
|
112
|
+
captureError("redirect-loop-detected", error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
writeBreadcrumbs([...recentBreadcrumbs, { from, to, at: now }].slice(-maxStoredBreadcrumbs));
|
|
117
|
+
}
|
|
@@ -23,6 +23,7 @@ import { ProviderType } from "@hexclave/shared/dist/utils/oauth";
|
|
|
23
23
|
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
|
|
24
24
|
import { suspend } from "@hexclave/shared/dist/utils/react";
|
|
25
25
|
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
26
|
+
import { isUuid } from "@hexclave/shared/dist/utils/uuids";
|
|
26
27
|
import { WebAuthnError, startRegistration } from "@simplewebauthn/browser";
|
|
27
28
|
import { useMemo } from "react"; // THIS_LINE_PLATFORM react-like
|
|
28
29
|
import * as yup from "yup";
|
|
@@ -90,6 +91,20 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
90
91
|
], TeamsCrud['Server']['List']>(async ([userId, orderBy, desc, cursor, limit, query]) => {
|
|
91
92
|
return await this._interface.listServerTeamsPaginated({ userId, orderBy, desc, cursor, limit, query });
|
|
92
93
|
});
|
|
94
|
+
private readonly _serverTeamCache = createCache<string[], TeamsCrud['Server']['Read'] | null>(async ([teamId]) => {
|
|
95
|
+
// The previous list-and-find implementation treated unknown or malformed IDs as null; preserve that behavior without making an invalid request.
|
|
96
|
+
if (!isUuid(teamId)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return await this._interface.getServerTeam(teamId);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (KnownErrors.TeamNotFound.isInstance(error)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
93
108
|
|
|
94
109
|
protected async _refreshTeamMembership(teamId: string, userId: string) {
|
|
95
110
|
await Promise.all([
|
|
@@ -700,6 +715,10 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
700
715
|
throw new HexclaveAssertionError("useOrLinkConnectedAccount is not available for server users. OAuth flows must be initiated on the client side.");
|
|
701
716
|
},
|
|
702
717
|
selectedTeam: crud.selected_team ? app._serverTeamFromCrud(crud.selected_team) : null,
|
|
718
|
+
// Unlike the app-level getTeam/useTeam (which fetch any team by id),
|
|
719
|
+
// the user-scoped variants search the user's own team list on purpose:
|
|
720
|
+
// they must return null for teams the user is not a member of, and some
|
|
721
|
+
// callers rely on that as a membership check.
|
|
703
722
|
async getTeam(teamId: string) {
|
|
704
723
|
const teams = await this.listTeams();
|
|
705
724
|
return teams.find((t) => t.id === teamId) ?? null;
|
|
@@ -1008,6 +1027,7 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1008
1027
|
async update(update: Partial<ServerTeamUpdateOptions>) {
|
|
1009
1028
|
await app._interface.updateServerTeam(crud.id, serverTeamUpdateOptionsToCrud(update));
|
|
1010
1029
|
await Promise.all([
|
|
1030
|
+
app._serverTeamCache.refresh([crud.id]),
|
|
1011
1031
|
app._serverTeamsCache.refreshWhere(() => true),
|
|
1012
1032
|
app._serverUsersCache.refreshWhere(() => true),
|
|
1013
1033
|
]);
|
|
@@ -1015,6 +1035,7 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1015
1035
|
async delete() {
|
|
1016
1036
|
await app._interface.deleteServerTeam(crud.id);
|
|
1017
1037
|
await Promise.all([
|
|
1038
|
+
app._serverTeamCache.refresh([crud.id]),
|
|
1018
1039
|
app._serverTeamsCache.refreshWhere(() => true),
|
|
1019
1040
|
app._serverUsersCache.refreshWhere(() => true),
|
|
1020
1041
|
]);
|
|
@@ -1489,6 +1510,7 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1489
1510
|
|
|
1490
1511
|
async createTeam(data: ServerTeamCreateOptions): Promise<ServerTeam> {
|
|
1491
1512
|
const team = await this._interface.createServerTeam(serverTeamCreateOptionsToCrud(data));
|
|
1513
|
+
await this._serverTeamCache.refresh([team.id]);
|
|
1492
1514
|
await this._serverTeamsCache.refreshWhere(() => true);
|
|
1493
1515
|
return this._serverTeamFromCrud(team);
|
|
1494
1516
|
}
|
|
@@ -1528,8 +1550,11 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1528
1550
|
return await this._getTeamByApiKey(options.apiKey);
|
|
1529
1551
|
} else {
|
|
1530
1552
|
const teamId = options;
|
|
1531
|
-
|
|
1532
|
-
|
|
1553
|
+
if (teamId == null) {
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
const team = Result.orThrow(await this._serverTeamCache.getOrWait([teamId], "write-only"));
|
|
1557
|
+
return team == null ? null : this._serverTeamFromCrud(team);
|
|
1533
1558
|
}
|
|
1534
1559
|
}
|
|
1535
1560
|
|
|
@@ -1540,10 +1565,11 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1540
1565
|
return this._useTeamByApiKey(options.apiKey);
|
|
1541
1566
|
} else {
|
|
1542
1567
|
const teamId = options;
|
|
1543
|
-
|
|
1568
|
+
// "" is never a valid UUID, so the cache resolves a nullish id to null while keeping the hook call order stable.
|
|
1569
|
+
const team = useAsyncCache(this._serverTeamCache, [teamId ?? ""], "serverApp.useTeam()");
|
|
1544
1570
|
return useMemo(() => {
|
|
1545
|
-
return
|
|
1546
|
-
}, [
|
|
1571
|
+
return team == null ? null : this._serverTeamFromCrud(team);
|
|
1572
|
+
}, [team]);
|
|
1547
1573
|
}
|
|
1548
1574
|
}
|
|
1549
1575
|
|
|
@@ -138,6 +138,7 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
|
|
138
138
|
redirectToHandler(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise<void>,
|
|
139
139
|
signInWithTokens(tokens: { accessToken: string, refreshToken: string }): Promise<void>,
|
|
140
140
|
awaitPendingAuthResolutions(): Promise<void>,
|
|
141
|
+
isTrustedRedirectUrl(url: string): Promise<boolean>,
|
|
141
142
|
},
|
|
142
143
|
}
|
|
143
144
|
& AsyncStoreProperty<"project", [], Project, false>
|
|
@@ -372,7 +372,7 @@ export function mountPushedConfigErrorOverlay(app: StackClientApp<true>): () =>
|
|
|
372
372
|
return;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
const issueMessage = issue.messages.join("\n");
|
|
375
|
+
const issueMessage = "Hexclave config " + issue.kind + ": " + issue.messages.join("\n");
|
|
376
376
|
const issueKey = `${app.projectId}:${issue.kind}:${issueMessage}`;
|
|
377
377
|
const issueLabel = issue.kind === "error" ? "error" : "warning";
|
|
378
378
|
const issueTitle = issue.kind === "error"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
//===========================================
|
|
3
|
+
// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
|
|
4
|
+
//===========================================
|
|
5
|
+
/**
|
|
6
|
+
* Guarded sessionStorage access for resilience layers (redirect loop breaking, redirect-back state
|
|
7
|
+
* mirroring). These layers must degrade to "no stored value" when storage is unavailable, instead
|
|
8
|
+
* of breaking the auth flow they are trying to protect.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function readSessionStorageItem(key: string): string | null {
|
|
12
|
+
if (typeof window === "undefined") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return window.sessionStorage.getItem(key);
|
|
17
|
+
} catch {
|
|
18
|
+
// Storage can be blocked entirely (private browsing, sandboxed iframes, disabled cookies), in
|
|
19
|
+
// which case accessing window.sessionStorage throws. Treat that as "nothing stored".
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeSessionStorageItem(key: string, value: string): void {
|
|
25
|
+
if (typeof window === "undefined") {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
window.sessionStorage.setItem(key, value);
|
|
30
|
+
} catch {
|
|
31
|
+
// Storage can be blocked or full; see readSessionStorageItem. Values written here are only
|
|
32
|
+
// used as a fallback, so failing to persist must not fail the caller.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function removeSessionStorageItem(key: string): void {
|
|
37
|
+
if (typeof window === "undefined") {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
window.sessionStorage.removeItem(key);
|
|
42
|
+
} catch {
|
|
43
|
+
// See readSessionStorageItem.
|
|
44
|
+
}
|
|
45
|
+
}
|