@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.
Files changed (167) hide show
  1. package/dist/{admin-app-impl-BGNL1KyZ.d.ts → admin-app-impl-Bga0o6cL.d.ts} +11 -8
  2. package/dist/{admin-app-impl-BGNL1KyZ.d.ts.map → admin-app-impl-Bga0o6cL.d.ts.map} +1 -1
  3. package/dist/clickmap/clickmap-core.d.ts +1 -1
  4. package/dist/clickmap/index.d.ts +1 -1
  5. package/dist/{client-app-Majjw8Xp.d.ts → client-app-B0kBC67Z.d.ts} +2 -1
  6. package/dist/{client-app-Majjw8Xp.d.ts.map → client-app-B0kBC67Z.d.ts.map} +1 -1
  7. package/dist/common-BK_sydxF.d.ts.map +1 -1
  8. package/dist/components/api-key-dialogs.d.ts +1 -1
  9. package/dist/components/selected-team-switcher.d.ts +1 -1
  10. package/dist/components/team-icon.d.ts +1 -1
  11. package/dist/components/team-switcher.d.ts +1 -1
  12. package/dist/components-page/account-settings/payments/payments-page.d.ts +1 -1
  13. package/dist/components-page/account-settings/teams/leave-team-section.d.ts +1 -1
  14. package/dist/components-page/account-settings/teams/team-api-keys-section.d.ts +1 -1
  15. package/dist/components-page/account-settings/teams/team-display-name-section.d.ts +1 -1
  16. package/dist/components-page/account-settings/teams/team-member-invitation-section.d.ts +1 -1
  17. package/dist/components-page/account-settings/teams/team-member-list-section.d.ts +1 -1
  18. package/dist/components-page/account-settings/teams/team-page.d.ts +1 -1
  19. package/dist/components-page/account-settings/teams/team-profile-image-section.d.ts +1 -1
  20. package/dist/components-page/account-settings/teams/team-profile-user-section.d.ts +1 -1
  21. package/dist/components-page/hexclave-handler-client.d.ts +1 -1
  22. package/dist/components-page/hexclave-handler.d.ts +1 -1
  23. package/dist/components-page/sign-out.js +11 -2
  24. package/dist/components-page/sign-out.js.map +1 -1
  25. package/dist/components-page/sign-out.test.d.ts +1 -0
  26. package/dist/components-page/sign-out.test.js +157 -0
  27. package/dist/components-page/sign-out.test.js.map +1 -0
  28. package/dist/dev-tool/dev-tool-core.d.ts +1 -1
  29. package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
  30. package/dist/dev-tool/dev-tool-core.js +58 -28
  31. package/dist/dev-tool/dev-tool-core.js.map +1 -1
  32. package/dist/dev-tool/dev-tool-styles.js +35 -0
  33. package/dist/dev-tool/dev-tool-styles.js.map +1 -1
  34. package/dist/dev-tool/index.d.ts +1 -1
  35. package/dist/esm/components/api-key-dialogs.d.ts +1 -1
  36. package/dist/esm/components-page/sign-out.d.ts.map +1 -1
  37. package/dist/esm/components-page/sign-out.js +12 -3
  38. package/dist/esm/components-page/sign-out.js.map +1 -1
  39. package/dist/esm/components-page/sign-out.test.d.ts +1 -0
  40. package/dist/esm/components-page/sign-out.test.js +156 -0
  41. package/dist/esm/components-page/sign-out.test.js.map +1 -0
  42. package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
  43. package/dist/esm/dev-tool/dev-tool-core.js +58 -28
  44. package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
  45. package/dist/esm/dev-tool/dev-tool-styles.js +35 -0
  46. package/dist/esm/dev-tool/dev-tool-styles.js.map +1 -1
  47. package/dist/esm/generated/global-css.d.ts +1 -1
  48. package/dist/esm/generated/global-css.js +1 -1
  49. package/dist/esm/generated/global-css.js.map +1 -1
  50. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  51. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +5 -4
  52. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  53. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +97 -0
  54. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  55. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +3 -2
  56. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  57. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +26 -1
  58. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  59. package/dist/esm/lib/hexclave-app/apps/implementations/common.d.ts.map +1 -1
  60. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +21 -4
  61. package/dist/esm/lib/hexclave-app/apps/implementations/common.js.map +1 -1
  62. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts +37 -0
  63. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts.map +1 -0
  64. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.js +89 -0
  65. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.js.map +1 -0
  66. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.d.ts +1 -0
  67. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.js +134 -0
  68. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.js.map +1 -0
  69. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.d.ts +8 -0
  70. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.d.ts.map +1 -0
  71. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.js +73 -0
  72. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.js.map +1 -0
  73. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.d.ts +1 -0
  74. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.js +92 -0
  75. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.js.map +1 -0
  76. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -0
  77. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts.map +1 -1
  78. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +27 -6
  79. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
  80. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -0
  81. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  82. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  83. package/dist/esm/pushed-config-error-overlay/index.js +1 -1
  84. package/dist/esm/pushed-config-error-overlay/index.js.map +1 -1
  85. package/dist/esm/utils/session-storage.d.ts +12 -0
  86. package/dist/esm/utils/session-storage.d.ts.map +1 -0
  87. package/dist/esm/utils/session-storage.js +30 -0
  88. package/dist/esm/utils/session-storage.js.map +1 -0
  89. package/dist/generated/global-css.d.ts +1 -1
  90. package/dist/generated/global-css.js +1 -1
  91. package/dist/generated/global-css.js.map +1 -1
  92. package/dist/generated/quetzal-translations.d.ts +1 -1
  93. package/dist/{hexclave-provider-IAd7nlbi.d.ts → hexclave-provider-SQbs9rmZ.d.ts} +3 -3
  94. package/dist/{hexclave-provider-IAd7nlbi.d.ts.map → hexclave-provider-SQbs9rmZ.d.ts.map} +1 -1
  95. package/dist/{hooks-BKXOF6jh.d.ts → hooks-ykQ7Wsco.d.ts} +2 -2
  96. package/dist/{hooks-BKXOF6jh.d.ts.map → hooks-ykQ7Wsco.d.ts.map} +1 -1
  97. package/dist/{index-Ck1qKoin.d.ts → index-DwE1cNLH.d.ts} +2 -2
  98. package/dist/{index-Ck1qKoin.d.ts.map → index-DwE1cNLH.d.ts.map} +1 -1
  99. package/dist/index.d.ts +4 -4
  100. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
  101. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +97 -0
  102. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  103. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
  104. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +26 -1
  105. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  106. package/dist/lib/hexclave-app/apps/implementations/common.js +21 -4
  107. package/dist/lib/hexclave-app/apps/implementations/common.js.map +1 -1
  108. package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
  109. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts +37 -0
  110. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts.map +1 -0
  111. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.js +92 -0
  112. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.js.map +1 -0
  113. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.d.ts +1 -0
  114. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.js +133 -0
  115. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.js.map +1 -0
  116. package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.d.ts +8 -0
  117. package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.d.ts.map +1 -0
  118. package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.js +74 -0
  119. package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.js.map +1 -0
  120. package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.d.ts +1 -0
  121. package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.js +91 -0
  122. package/dist/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.js.map +1 -0
  123. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  124. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +27 -6
  125. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
  126. package/dist/lib/hexclave-app/apps/index.d.ts +1 -1
  127. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -1
  128. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -1
  129. package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  130. package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
  131. package/dist/lib/hexclave-app/index.d.ts +1 -1
  132. package/dist/lib/hexclave-app/projects/index.d.ts +1 -1
  133. package/dist/lib/hexclave-app/teams/index.d.ts +1 -1
  134. package/dist/lib/hexclave-app/users/index.d.ts +1 -1
  135. package/dist/lib/hooks.d.ts +1 -1
  136. package/dist/providers/hexclave-context.d.ts +1 -1
  137. package/dist/providers/hexclave-provider-client.d.ts +1 -1
  138. package/dist/providers/hexclave-provider.d.ts +1 -1
  139. package/dist/providers/translation-provider.d.ts +1 -1
  140. package/dist/pushed-config-error-overlay/index.d.ts +1 -1
  141. package/dist/pushed-config-error-overlay/index.js +1 -1
  142. package/dist/pushed-config-error-overlay/index.js.map +1 -1
  143. package/dist/{quetzal-translations-XeH3zQ9l.d.ts → quetzal-translations-CK45_4Dm.d.ts} +3 -3
  144. package/dist/{quetzal-translations-XeH3zQ9l.d.ts.map → quetzal-translations-CK45_4Dm.d.ts.map} +1 -1
  145. package/dist/sign-out-DBnSkfDz.d.ts.map +1 -1
  146. package/dist/{translation-provider-WOaZXqAz.d.ts → translation-provider-DQ3BJepJ.d.ts} +2 -2
  147. package/dist/{translation-provider-WOaZXqAz.d.ts.map → translation-provider-DQ3BJepJ.d.ts.map} +1 -1
  148. package/dist/utils/session-storage.d.ts +12 -0
  149. package/dist/utils/session-storage.d.ts.map +1 -0
  150. package/dist/utils/session-storage.js +33 -0
  151. package/dist/utils/session-storage.js.map +1 -0
  152. package/package.json +4 -4
  153. package/src/components-page/sign-out.test.tsx +190 -0
  154. package/src/components-page/sign-out.tsx +22 -3
  155. package/src/dev-tool/dev-tool-core.ts +100 -35
  156. package/src/dev-tool/dev-tool-styles.ts +35 -0
  157. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +127 -0
  158. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +34 -1
  159. package/src/lib/hexclave-app/apps/implementations/common.ts +18 -1
  160. package/src/lib/hexclave-app/apps/implementations/redirect-back-state.test.ts +134 -0
  161. package/src/lib/hexclave-app/apps/implementations/redirect-back-state.ts +140 -0
  162. package/src/lib/hexclave-app/apps/implementations/redirect-loop-breaker.test.ts +127 -0
  163. package/src/lib/hexclave-app/apps/implementations/redirect-loop-breaker.ts +117 -0
  164. package/src/lib/hexclave-app/apps/implementations/server-app-impl.ts +31 -5
  165. package/src/lib/hexclave-app/apps/interfaces/client-app.ts +1 -0
  166. package/src/pushed-config-error-overlay/index.ts +1 -1
  167. 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
- const teams = await this.listTeams();
1532
- return teams.find((t) => t.id === teamId) ?? null;
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
- const teams = this.useTeams();
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 teams.find((t) => t.id === teamId) ?? null;
1546
- }, [teams, teamId]);
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
+ }