@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
@@ -1 +1 @@
1
- {"version":3,"file":"quetzal-translations-XeH3zQ9l.d.ts","names":[],"sources":["../src/generated/quetzal-translations.ts"],"mappings":";cAUa,WAAA,EAAW,GAAA;AAAA,cAqTX,cAAA,EAAc,GAAA,mIAAA,GAAA,66qBAAA,GAAA,usnBAAA,GAAA,iyrBAAA,GAAA,g+qBAAA,GAAA,8srBAAA,GAAA,2trBAAA,GAAA,41pBAAA,GAAA,m3eAAA,GAAA,oreAAA,GAAA,swpBAAA,GAAA,+0qBAAA,GAAA,8wbAAA,GAAA"}
1
+ {"version":3,"file":"quetzal-translations-CK45_4Dm.d.ts","names":[],"sources":["../src/generated/quetzal-translations.ts"],"mappings":";cAUa,WAAA,EAAW,GAAA;AAAA,cAqTX,cAAA,EAAc,GAAA,mIAAA,GAAA,66qBAAA,GAAA,usnBAAA,GAAA,iyrBAAA,GAAA,g+qBAAA,GAAA,8srBAAA,GAAA,2trBAAA,GAAA,41pBAAA,GAAA,m3eAAA,GAAA,oreAAA,GAAA,swpBAAA,GAAA,+0qBAAA,GAAA,8wbAAA,GAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"sign-out-DBnSkfDz.d.ts","names":[],"sources":["../src/components-page/sign-out.tsx"],"mappings":";iBAyBgB,OAAA,CAAQ,KAAA;EAAS,QAAA;EAAoB,YAAA,GAAe,MAAM;AAAA,gCAAkB,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"sign-out-DBnSkfDz.d.ts","names":[],"sources":["../src/components-page/sign-out.tsx"],"mappings":";iBA0CgB,OAAA,CAAQ,KAAA;EAAS,QAAA;EAAoB,YAAA,GAAe,MAAM;AAAA,gCAAkB,GAAA,CAAA,OAAA"}
@@ -1,4 +1,4 @@
1
- import { n as quetzalLocales } from "./quetzal-translations-XeH3zQ9l.js";
1
+ import { n as quetzalLocales } from "./quetzal-translations-CK45_4Dm.js";
2
2
 
3
3
  //#region src/providers/translation-provider.d.ts
4
4
  declare function TranslationProvider({
@@ -12,4 +12,4 @@ declare function TranslationProvider({
12
12
  }): import("react/jsx-runtime").JSX.Element;
13
13
  //#endregion
14
14
  export { TranslationProvider as t };
15
- //# sourceMappingURL=translation-provider-WOaZXqAz.d.ts.map
15
+ //# sourceMappingURL=translation-provider-DQ3BJepJ.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"translation-provider-WOaZXqAz.d.ts","names":[],"sources":["../src/providers/translation-provider.tsx"],"mappings":";;;iBAQgB,mBAAA;EAAsB,IAAA;EAAM,oBAAA;EAAsB;AAAA;EAChE,IAAA,EAAM,UAAA,QAAkB,cAAA,CAAe,GAAA;EACvC,oBAAA,GAAuB,MAAA;EACvB,QAAA,EAAU,KAAA,CAAM,SAAA;AAAA,gCACjB,GAAA,CAAA,OAAA"}
1
+ {"version":3,"file":"translation-provider-DQ3BJepJ.d.ts","names":[],"sources":["../src/providers/translation-provider.tsx"],"mappings":";;;iBAQgB,mBAAA;EAAsB,IAAA;EAAM,oBAAA;EAAsB;AAAA;EAChE,IAAA,EAAM,UAAA,QAAkB,cAAA,CAAe,GAAA;EACvC,oBAAA,GAAuB,MAAA;EACvB,QAAA,EAAU,KAAA,CAAM,SAAA;AAAA,gCACjB,GAAA,CAAA,OAAA"}
@@ -0,0 +1,12 @@
1
+ //#region src/utils/session-storage.d.ts
2
+ /**
3
+ * Guarded sessionStorage access for resilience layers (redirect loop breaking, redirect-back state
4
+ * mirroring). These layers must degrade to "no stored value" when storage is unavailable, instead
5
+ * of breaking the auth flow they are trying to protect.
6
+ */
7
+ declare function readSessionStorageItem(key: string): string | null;
8
+ declare function writeSessionStorageItem(key: string, value: string): void;
9
+ declare function removeSessionStorageItem(key: string): void;
10
+ //#endregion
11
+ export { readSessionStorageItem, removeSessionStorageItem, writeSessionStorageItem };
12
+ //# sourceMappingURL=session-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-storage.d.ts","names":[],"sources":["../../src/utils/session-storage.ts"],"mappings":";;AAUA;;;;iBAAgB,sBAAA,CAAuB,GAAW;AAAA,iBAalC,uBAAA,CAAwB,GAAA,UAAa,KAAa;AAAA,iBAYlD,wBAAA,CAAyB,GAAW"}
@@ -0,0 +1,33 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/utils/session-storage.ts
3
+ /**
4
+ * Guarded sessionStorage access for resilience layers (redirect loop breaking, redirect-back state
5
+ * mirroring). These layers must degrade to "no stored value" when storage is unavailable, instead
6
+ * of breaking the auth flow they are trying to protect.
7
+ */
8
+ function readSessionStorageItem(key) {
9
+ if (typeof window === "undefined") return null;
10
+ try {
11
+ return window.sessionStorage.getItem(key);
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ function writeSessionStorageItem(key, value) {
17
+ if (typeof window === "undefined") return;
18
+ try {
19
+ window.sessionStorage.setItem(key, value);
20
+ } catch {}
21
+ }
22
+ function removeSessionStorageItem(key) {
23
+ if (typeof window === "undefined") return;
24
+ try {
25
+ window.sessionStorage.removeItem(key);
26
+ } catch {}
27
+ }
28
+ //#endregion
29
+ exports.readSessionStorageItem = readSessionStorageItem;
30
+ exports.removeSessionStorageItem = removeSessionStorageItem;
31
+ exports.writeSessionStorageItem = writeSessionStorageItem;
32
+
33
+ //# sourceMappingURL=session-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-storage.js","names":[],"sources":["../../src/utils/session-storage.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\n/**\n * Guarded sessionStorage access for resilience layers (redirect loop breaking, redirect-back state\n * mirroring). These layers must degrade to \"no stored value\" when storage is unavailable, instead\n * of breaking the auth flow they are trying to protect.\n */\n\nexport function readSessionStorageItem(key: string): string | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n try {\n return window.sessionStorage.getItem(key);\n } catch {\n // Storage can be blocked entirely (private browsing, sandboxed iframes, disabled cookies), in\n // which case accessing window.sessionStorage throws. Treat that as \"nothing stored\".\n return null;\n }\n}\n\nexport function writeSessionStorageItem(key: string, value: string): void {\n if (typeof window === \"undefined\") {\n return;\n }\n try {\n window.sessionStorage.setItem(key, value);\n } catch {\n // Storage can be blocked or full; see readSessionStorageItem. Values written here are only\n // used as a fallback, so failing to persist must not fail the caller.\n }\n}\n\nexport function removeSessionStorageItem(key: string): void {\n if (typeof window === \"undefined\") {\n return;\n }\n try {\n window.sessionStorage.removeItem(key);\n } catch {\n // See readSessionStorageItem.\n }\n}\n"],"mappings":";;;;;;;AAUA,SAAgB,uBAAuB,KAA4B;CACjE,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,IAAI;EACF,OAAO,OAAO,eAAe,QAAQ,GAAG;CAC1C,QAAQ;EAGN,OAAO;CACT;AACF;AAEA,SAAgB,wBAAwB,KAAa,OAAqB;CACxE,IAAI,OAAO,WAAW,aACpB;CAEF,IAAI;EACF,OAAO,eAAe,QAAQ,KAAK,KAAK;CAC1C,QAAQ,CAGR;AACF;AAEA,SAAgB,yBAAyB,KAAmB;CAC1D,IAAI,OAAO,WAAW,aACpB;CAEF,IAAI;EACF,OAAO,eAAe,WAAW,GAAG;CACtC,QAAQ,CAER;AACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
3
3
  "name": "@hexclave/next",
4
- "version": "1.0.51",
4
+ "version": "1.0.53",
5
5
  "repository": "https://github.com/hexclave/hexclave",
6
6
  "sideEffects": false,
7
7
  "main": "./dist/index.js",
@@ -74,9 +74,9 @@
74
74
  "tailwindcss-animate": "^1.0.7",
75
75
  "rrweb": "^1.1.3",
76
76
  "yup": "^1.7.1",
77
- "@hexclave/sc": "1.0.51",
78
- "@hexclave/shared": "1.0.51",
79
- "@hexclave/ui": "1.0.51"
77
+ "@hexclave/sc": "1.0.53",
78
+ "@hexclave/shared": "1.0.53",
79
+ "@hexclave/ui": "1.0.53"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "@types/react": ">=18.0.0",
@@ -0,0 +1,190 @@
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
+ // @vitest-environment jsdom
6
+
7
+ import React, { act } from "react";
8
+ import { createRoot, type Root } from "react-dom/client";
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
+ import type { CurrentUser } from "../lib/hexclave-app";
11
+ import type { StackClientApp } from "../lib/hexclave-app/apps/interfaces/client-app";
12
+ import { hexclaveAppInternalsSymbol } from "../lib/hexclave-app/common";
13
+ import { TranslationProviderClient } from "../providers/translation-provider-client";
14
+ import { SignOut } from "./sign-out";
15
+
16
+ const appMockState = vi.hoisted(() => ({
17
+ app: null as unknown,
18
+ user: null as unknown,
19
+ }));
20
+
21
+ vi.mock("..", () => ({
22
+ useStackApp: () => {
23
+ if (appMockState.app == null) {
24
+ throw new Error("Expected test app to be set before rendering.");
25
+ }
26
+ return appMockState.app;
27
+ },
28
+ useUser: () => appMockState.user,
29
+ }));
30
+
31
+ // The real `use` delegates to React.use, which breaks in this test setup because the shared
32
+ // package and react-dom resolve different React instances. Replace it with an equivalent
33
+ // Suspense-compatible implementation that doesn't depend on the React dispatcher.
34
+ vi.mock("@hexclave/shared/dist/utils/react", async (importOriginal) => {
35
+ const original = await importOriginal<typeof import("@hexclave/shared/dist/utils/react")>();
36
+ type PromiseState = { status: "pending" } | { status: "ok", value: unknown } | { status: "error", error: unknown };
37
+ const promiseStates = new WeakMap<Promise<unknown>, PromiseState>();
38
+ return {
39
+ ...original,
40
+ use: <T,>(promise: Promise<T>): T => {
41
+ const state = promiseStates.get(promise);
42
+ if (state == null) {
43
+ promiseStates.set(promise, { status: "pending" });
44
+ promise.then(
45
+ (value) => promiseStates.set(promise, { status: "ok", value }),
46
+ (error) => promiseStates.set(promise, { status: "error", error }),
47
+ );
48
+ throw promise;
49
+ }
50
+ switch (state.status) {
51
+ case "pending": {
52
+ throw promise;
53
+ }
54
+ case "error": {
55
+ throw state.error;
56
+ }
57
+ case "ok": {
58
+ // This cast is safe because the value was stored from this exact promise's resolution;
59
+ // the WeakMap just can't express the per-key type relationship.
60
+ return state.value as T;
61
+ }
62
+ }
63
+ },
64
+ };
65
+ });
66
+
67
+ vi.mock("@hexclave/ui", async () => {
68
+ const React = await import("react");
69
+ return {
70
+ Button: (props: { children: React.ReactNode, onClick?: () => void }) => (
71
+ <button type="button" onClick={props.onClick}>{props.children}</button>
72
+ ),
73
+ Typography: (props: { children: React.ReactNode }) => <div>{props.children}</div>,
74
+ cn: (...classes: (string | false | null | undefined)[]) => classes.filter(Boolean).join(" "),
75
+ };
76
+ });
77
+
78
+ const previousActEnvironment = Reflect.get(globalThis, "IS_REACT_ACT_ENVIRONMENT");
79
+
80
+ function createAppTestDouble(options: {
81
+ trustedUrls: string[],
82
+ }) {
83
+ const app = {
84
+ redirectToSignIn: vi.fn(async () => {}),
85
+ [hexclaveAppInternalsSymbol]: {
86
+ isTrustedRedirectUrl: vi.fn(async (url: string) => options.trustedUrls.includes(url)),
87
+ },
88
+ };
89
+
90
+ // This test double intentionally implements only the StackClientApp surface that SignOut and
91
+ // the rendered signed-out card touch.
92
+ return app as unknown as StackClientApp<true>;
93
+ }
94
+
95
+ function createUserTestDouble() {
96
+ const user = {
97
+ signOut: vi.fn(async () => {}),
98
+ };
99
+ // Same as the app test double: only the surface SignOut touches.
100
+ return user as unknown as CurrentUser & { signOut: ReturnType<typeof vi.fn> };
101
+ }
102
+
103
+ // jsdom's window.location is not configurable, so it can't be spied on; replace it wholesale.
104
+ const originalLocation = window.location;
105
+ let locationReplaceMock = vi.fn();
106
+
107
+ let root: Root | null = null;
108
+ let container: HTMLDivElement | null = null;
109
+
110
+ async function renderSignOut(options: {
111
+ app: StackClientApp<true>,
112
+ user: unknown,
113
+ searchParams?: Record<string, string>,
114
+ }) {
115
+ appMockState.app = options.app;
116
+ appMockState.user = options.user;
117
+ container = document.createElement("div");
118
+ document.body.append(container);
119
+ root = createRoot(container);
120
+ await act(async () => {
121
+ root?.render(
122
+ <TranslationProviderClient quetzalKeys={new Map()} quetzalLocale={new Map()}>
123
+ <React.Suspense fallback={<div data-testid="suspense-fallback" />}>
124
+ <SignOut searchParams={options.searchParams} />
125
+ </React.Suspense>
126
+ </TranslationProviderClient>
127
+ );
128
+ });
129
+ await act(async () => {
130
+ await new Promise((resolve) => setTimeout(resolve, 0));
131
+ });
132
+ }
133
+
134
+ describe("SignOut", () => {
135
+ beforeEach(() => {
136
+ Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
137
+ locationReplaceMock = vi.fn();
138
+ Reflect.deleteProperty(window, "location");
139
+ Reflect.set(window, "location", { href: "https://demo.example.test/handler/sign-out", replace: locationReplaceMock });
140
+ });
141
+
142
+ afterEach(() => {
143
+ act(() => {
144
+ root?.unmount();
145
+ });
146
+ container?.remove();
147
+ root = null;
148
+ container = null;
149
+ appMockState.app = null;
150
+ appMockState.user = null;
151
+ Reflect.set(window, "location", originalLocation);
152
+ vi.restoreAllMocks();
153
+ Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", previousActEnvironment);
154
+ });
155
+
156
+ it("signs out with a trusted after_auth_return_to redirect URL", async () => {
157
+ const app = createAppTestDouble({ trustedUrls: ["/dashboard"] });
158
+ const user = createUserTestDouble();
159
+
160
+ await renderSignOut({ app, user, searchParams: { after_auth_return_to: "/dashboard" } });
161
+
162
+ expect(user.signOut).toHaveBeenCalledWith({ redirectUrl: "/dashboard" });
163
+ });
164
+
165
+ it("ignores an untrusted after_auth_return_to and falls back to the default sign-out redirect", async () => {
166
+ const app = createAppTestDouble({ trustedUrls: [] });
167
+ const user = createUserTestDouble();
168
+
169
+ await renderSignOut({ app, user, searchParams: { after_auth_return_to: "https://evil.example.test/phishing" } });
170
+
171
+ // The untrusted URL is dropped entirely; sign-out proceeds with the default destination.
172
+ expect(user.signOut).toHaveBeenCalledWith({ redirectUrl: undefined });
173
+ });
174
+
175
+ it("does not follow an untrusted after_auth_return_to when already signed out", async () => {
176
+ const app = createAppTestDouble({ trustedUrls: [] });
177
+
178
+ await renderSignOut({ app, user: null, searchParams: { after_auth_return_to: "https://evil.example.test/phishing-signed-out" } });
179
+
180
+ expect(locationReplaceMock).not.toHaveBeenCalled();
181
+ });
182
+
183
+ it("follows a trusted after_auth_return_to when already signed out", async () => {
184
+ const app = createAppTestDouble({ trustedUrls: ["/trusted-dashboard"] });
185
+
186
+ await renderSignOut({ app, user: null, searchParams: { after_auth_return_to: "/trusted-dashboard" } });
187
+
188
+ expect(locationReplaceMock).toHaveBeenCalledWith("/trusted-dashboard");
189
+ });
190
+ });
@@ -6,9 +6,26 @@
6
6
  //===========================================
7
7
 
8
8
  import { cacheFunction } from "@hexclave/shared/dist/utils/caches";
9
+ import { captureError } from "@hexclave/shared/dist/utils/errors";
9
10
  import { use } from "@hexclave/shared/dist/utils/react";
10
- import { CurrentUser, useUser } from "..";
11
+ import { CurrentUser, useStackApp, useUser } from "..";
11
12
  import { PredefinedMessageCard } from "../components/message-cards/predefined-message-card";
13
+ import { StackClientApp, hexclaveAppInternalsSymbol } from "../lib/hexclave-app";
14
+
15
+ // The sign-out page's `after_auth_return_to` comes straight from the query string, so it is
16
+ // attacker-craftable (open-redirect shaped). Only follow it if it passes the same trust validation
17
+ // as every other SDK redirect; otherwise ignore it and fall back to the default after-sign-out
18
+ // destination.
19
+ const cacheGetTrustedRedirectUrl = cacheFunction(async (app: StackClientApp<true, string>, redirectUrl: string | undefined): Promise<string | undefined> => {
20
+ if (redirectUrl == null) {
21
+ return undefined;
22
+ }
23
+ if (await app[hexclaveAppInternalsSymbol].isTrustedRedirectUrl(redirectUrl)) {
24
+ return redirectUrl;
25
+ }
26
+ captureError("sign-out-untrusted-redirect-url", new Error(`Ignoring untrusted after_auth_return_to query parameter on the sign-out page: ${redirectUrl}`));
27
+ return undefined;
28
+ });
12
29
 
13
30
  const cacheSignOut = cacheFunction(async (user: CurrentUser, redirectUrl: string | undefined) => {
14
31
  return await user.signOut({ redirectUrl });
@@ -24,13 +41,15 @@ const cacheRedirectIfAlreadySignedOut = cacheFunction(async (redirectUrl: string
24
41
  });
25
42
 
26
43
  export function SignOut(props: { fullPage?: boolean, searchParams?: Record<string, string> }) {
44
+ const app = useStackApp();
27
45
  const user = useUser({ or: "return-null" });
28
46
  const redirectUrl = props.searchParams?.after_auth_return_to;
47
+ const trustedRedirectUrl = use(cacheGetTrustedRedirectUrl(app, redirectUrl));
29
48
 
30
49
  if (user) {
31
- use(cacheSignOut(user, redirectUrl));
50
+ use(cacheSignOut(user, trustedRedirectUrl));
32
51
  } else {
33
- use(cacheRedirectIfAlreadySignedOut(redirectUrl));
52
+ use(cacheRedirectIfAlreadySignedOut(trustedRedirectUrl));
34
53
  }
35
54
 
36
55
  return <PredefinedMessageCard type='signedOut' fullPage={props.fullPage} />;
@@ -8,7 +8,6 @@ import { DEV_TOOL_ROOT_ID } from "@hexclave/shared/dist/utils/dev-tool";
8
8
  import { runAsynchronously, runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
9
9
  import { isLocalhost } from "@hexclave/shared/dist/utils/urls";
10
10
  import type { StackClientApp } from "../lib/hexclave-app";
11
- import { envVars } from "../generated/env";
12
11
  import { getGlobalUiInstance, h, hasAppendChild, setGlobalUiInstance, setHtml, type UiGlobalInstance } from "../in-page-ui/dom";
13
12
  import { getBaseUrl } from "../lib/hexclave-app/apps/implementations/common";
14
13
  import type { HandlerUrlOptions, HandlerUrls, HandlerUrlTarget } from "../lib/hexclave-app/common";
@@ -63,6 +62,11 @@ const MAX_LOG_ENTRIES = 500;
63
62
  const CONSOLE_LOG_BATCH_SIZE = 100;
64
63
  const DRAG_THRESHOLD = 5;
65
64
  const DOCS_URL = 'https://docs.hexclave.com';
65
+ // The local development-environment dashboard always listens on this loopback
66
+ // port (see the CLI's DEFAULT_DASHBOARD_PORT). The dev tool runs inside the
67
+ // customer's own app, so it can't read the CLI's env vars — the port is fixed.
68
+ const LOCAL_DASHBOARD_ORIGIN = 'http://localhost:26700';
69
+ const HOSTED_DASHBOARD_URL = 'https://app.hexclave.com';
66
70
 
67
71
  const TABS: { id: TabId; label: string; icon: string }[] = [
68
72
  { id: 'overview', label: 'Overview', icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>' },
@@ -193,42 +197,60 @@ function resolveApiBaseUrl(app: StackClientApp<true>): string {
193
197
  return getBaseUrl(opts.baseUrl);
194
198
  }
195
199
 
196
- function shouldShowDashboardTab(app: StackClientApp<true>): boolean {
197
- return envVars.HEXCLAVE_IS_LOCAL_EMULATOR === "true" && isLocalhost(resolveApiBaseUrl(app));
200
+ function localDashboardProjectUrl(app: StackClientApp<true>): string {
201
+ return `${LOCAL_DASHBOARD_ORIGIN}/projects/${encodeURIComponent(app.projectId)}`;
198
202
  }
199
203
 
200
- function getTabsForApp(app: StackClientApp<true>): { id: TabId; label: string; icon: string }[] {
201
- if (shouldShowDashboardTab(app)) {
202
- return TABS;
203
- }
204
- return TABS.filter((tab) => tab.id !== 'dashboard');
204
+ function parseProjectAvailabilityResponse(body: unknown): boolean {
205
+ if (typeof body !== 'object' || body === null) return false;
206
+ return 'project_available' in body && body.project_available === true;
207
+ }
208
+
209
+ // How long to wait for the availability probe before giving up. A refused
210
+ // connection rejects almost immediately, but a host that accepts the socket yet
211
+ // never responds would otherwise leave the tab stuck on "Checking…" forever.
212
+ const LOCAL_DASHBOARD_PROBE_TIMEOUT_MS = 3000;
213
+
214
+ // Whether the current page can even talk to the HTTP-only local dashboard.
215
+ // LOCAL_DASHBOARD_ORIGIN is `http://localhost:26700`, so the page itself must be
216
+ // an http: localhost origin: an https: page (e.g. an HTTPS dev server) would
217
+ // have both the probe fetch and the iframe embed blocked as mixed content, so
218
+ // we treat that as "not available" and fall back to the hosted dashboard.
219
+ function canReachLocalDashboard(): boolean {
220
+ return isLocalhost(window.location.href) && window.location.protocol === 'http:';
205
221
  }
206
222
 
207
- function deriveDashboardBaseUrl(apiBaseUrl: string): string {
223
+ // Decides whether the Dashboard tab can embed the locally-running
224
+ // development-environment dashboard. Rather than inferring from build-time env
225
+ // vars (which don't reflect whether the dev environment is actually up right
226
+ // now), we ask the running development environment directly:
227
+ // 1. We must be viewing the app from an http: localhost origin — the HTTP-only
228
+ // dashboard on :26700 is only reachable from, and only relevant to, a local
229
+ // browser (and an https: page can't reach it without mixed-content errors).
230
+ // 2. The development environment must be running AND already own this exact
231
+ // project (see the project-availability route it answers).
232
+ // Any failure — dev environment not running (connection refused), probe timeout,
233
+ // project unknown, or a malformed response — resolves to `false`, which shows
234
+ // the hosted-dashboard hint instead.
235
+ async function isLocalDashboardProjectAvailable(app: StackClientApp<true>): Promise<boolean> {
236
+ if (!canReachLocalDashboard()) return false;
237
+ const url = `${LOCAL_DASHBOARD_ORIGIN}/api/development-environment/project-availability?project_id=${encodeURIComponent(app.projectId)}`;
238
+ const controller = new AbortController();
239
+ const timeout = setTimeout(() => controller.abort(), LOCAL_DASHBOARD_PROBE_TIMEOUT_MS);
208
240
  try {
209
- const url = new URL(apiBaseUrl);
210
- if (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]') {
211
- const port = url.port;
212
- if (port && port.endsWith('02')) {
213
- url.port = port.slice(0, -2) + '01';
214
- }
215
- return url.origin;
216
- }
217
- if (url.hostname.startsWith('api.')) {
218
- url.hostname = 'app.' + url.hostname.slice(4);
219
- return url.origin;
220
- }
221
- return url.origin;
241
+ const response = await fetch(url, { method: 'GET', signal: controller.signal });
242
+ if (!response.ok) return false;
243
+ return parseProjectAvailabilityResponse(await response.json());
222
244
  } catch {
223
- return 'https://app.hexclave.com';
245
+ // Reachability probe: when the local development environment isn't running,
246
+ // the fetch rejects (connection refused); when it hangs, the AbortController
247
+ // aborts it. Either way that simply means "not available".
248
+ return false;
249
+ } finally {
250
+ clearTimeout(timeout);
224
251
  }
225
252
  }
226
253
 
227
- function resolveDashboardUrl(app: StackClientApp<true>): string {
228
- const base = deriveDashboardBaseUrl(resolveApiBaseUrl(app));
229
- return `${base}/projects/${encodeURIComponent(app.projectId)}`;
230
- }
231
-
232
254
  function formatTimestamp(ts: number): string {
233
255
  return new Date(ts).toLocaleTimeString([], {
234
256
  hour: '2-digit',
@@ -563,8 +585,8 @@ function createTabBar(
563
585
  // Iframe helper
564
586
  // ---------------------------------------------------------------------------
565
587
 
566
- function createIframeTab(src: string, title: string, loadingMsg = 'Loading\u2026', errorMsg = 'Unable to load content', errorDetail?: string, openExternallyLabel?: string): HTMLElement {
567
- const container = h('div', { className: 'sdt-iframe-container' });
588
+ function populateIframeTab(container: HTMLElement, src: string, title: string, loadingMsg = 'Loading\u2026', errorMsg = 'Unable to load content', errorDetail?: string, openExternallyLabel?: string): void {
589
+ container.innerHTML = '';
568
590
  if (openExternallyLabel != null) {
569
591
  container.appendChild(h('div', { className: 'sdt-iframe-toolbar' },
570
592
  h('a', { href: src, target: '_blank', rel: 'noopener noreferrer', className: 'sdt-iframe-open-link' }, openExternallyLabel),
@@ -594,7 +616,7 @@ function createIframeTab(src: string, title: string, loadingMsg = 'Loading\u2026
594
616
  }
595
617
  const retryBtn = h('button', { className: 'sdt-iframe-error-btn' }, 'Retry');
596
618
  retryBtn.addEventListener('click', () => {
597
- container.replaceWith(createIframeTab(src, title, loadingMsg, errorMsg, errorDetail, openExternallyLabel));
619
+ populateIframeTab(container, src, title, loadingMsg, errorMsg, errorDetail, openExternallyLabel);
598
620
  });
599
621
  errDiv.appendChild(retryBtn);
600
622
  const link = h('a', { href: src, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--sdt-accent)', fontSize: '12px', textDecoration: 'none' } }, 'Open in new tab');
@@ -603,7 +625,6 @@ function createIframeTab(src: string, title: string, loadingMsg = 'Loading\u2026
603
625
  });
604
626
 
605
627
  container.appendChild(iframe);
606
- return container;
607
628
  }
608
629
 
609
630
  // ===========================================================================================
@@ -1681,8 +1702,52 @@ function createAITab(app: StackClientApp<true>): HTMLElement {
1681
1702
  // ---------------------------------------------------------------------------
1682
1703
 
1683
1704
  function createDashboardTab(app: StackClientApp<true>): HTMLElement {
1684
- const dashboardUrl = resolveDashboardUrl(app);
1685
- return createIframeTab(dashboardUrl, 'Hexclave Dashboard', 'Loading dashboard\u2026', 'Unable to load dashboard', 'The dashboard may require authentication or block framing', 'Open in New Tab');
1705
+ const container = h('div', { className: 'sdt-iframe-container' });
1706
+
1707
+ function showChecking() {
1708
+ container.innerHTML = '';
1709
+ container.appendChild(h('div', { className: 'sdt-iframe-loading' }, 'Checking development environment\u2026'));
1710
+ }
1711
+
1712
+ function showUnavailable() {
1713
+ container.innerHTML = '';
1714
+ const message = h('div', { className: 'sdt-dashboard-unavailable' });
1715
+ const text = h('div', { className: 'sdt-dashboard-unavailable-text' });
1716
+ text.appendChild(document.createTextNode('Navigate to '));
1717
+ text.appendChild(h('a', {
1718
+ className: 'sdt-dashboard-unavailable-link',
1719
+ href: HOSTED_DASHBOARD_URL,
1720
+ target: '_blank',
1721
+ rel: 'noopener noreferrer',
1722
+ }, HOSTED_DASHBOARD_URL));
1723
+ text.appendChild(document.createTextNode(" to view this project's dashboard"));
1724
+ message.appendChild(text);
1725
+ container.appendChild(message);
1726
+ }
1727
+
1728
+ async function check() {
1729
+ showChecking();
1730
+ const available = await isLocalDashboardProjectAvailable(app);
1731
+ // The dev tool may have been torn down while we were probing; only mutate
1732
+ // the DOM if this container is still mounted.
1733
+ if (!container.isConnected) return;
1734
+ if (available) {
1735
+ populateIframeTab(
1736
+ container,
1737
+ localDashboardProjectUrl(app),
1738
+ 'Hexclave Dashboard',
1739
+ 'Loading dashboard\u2026',
1740
+ 'Unable to load dashboard',
1741
+ 'The dashboard may require authentication or block framing',
1742
+ 'Open in New Tab',
1743
+ );
1744
+ } else {
1745
+ showUnavailable();
1746
+ }
1747
+ }
1748
+
1749
+ runAsynchronously(check);
1750
+ return container;
1686
1751
  }
1687
1752
 
1688
1753
  // ---------------------------------------------------------------------------
@@ -2112,7 +2177,7 @@ function createPanel(
2112
2177
  panel.style.height = state.get().panelHeight + 'px';
2113
2178
  }
2114
2179
 
2115
- const tabs = getTabsForApp(app);
2180
+ const tabs = TABS;
2116
2181
  const storedActiveTab = state.get().activeTab;
2117
2182
  const activeTab = tabs.some((tab) => tab.id === storedActiveTab) ? storedActiveTab : DEFAULT_STATE.activeTab;
2118
2183
 
@@ -328,6 +328,14 @@ export const devToolCSS = getInPageUiBaseCSS('.hexclave-devtool') + `
328
328
  z-index: 1;
329
329
  }
330
330
 
331
+ /* Iframe panes lay their content out as a flex column so the embedded
332
+ dashboard (or the fallback message) fills the pane height. Uses two
333
+ classes to win over the plain .sdt-tab-pane-active { display: block }. */
334
+ .hexclave-devtool .sdt-tab-pane-iframe.sdt-tab-pane-active {
335
+ display: flex;
336
+ flex-direction: column;
337
+ }
338
+
331
339
  /* ===== Overview tab — single column ===== */
332
340
 
333
341
  .hexclave-devtool .sdt-ov {
@@ -1235,6 +1243,33 @@ export const devToolCSS = getInPageUiBaseCSS('.hexclave-devtool') + `
1235
1243
  background: var(--sdt-accent-hover);
1236
1244
  }
1237
1245
 
1246
+ .hexclave-devtool .sdt-dashboard-unavailable {
1247
+ flex: 1;
1248
+ display: flex;
1249
+ flex-direction: column;
1250
+ align-items: center;
1251
+ justify-content: center;
1252
+ gap: 12px;
1253
+ padding: 24px;
1254
+ text-align: center;
1255
+ color: var(--sdt-text-secondary);
1256
+ font-size: 14px;
1257
+ }
1258
+
1259
+ .hexclave-devtool .sdt-dashboard-unavailable-text {
1260
+ max-width: 420px;
1261
+ line-height: 1.5;
1262
+ }
1263
+
1264
+ .hexclave-devtool .sdt-dashboard-unavailable-link {
1265
+ color: var(--sdt-accent);
1266
+ text-decoration: none;
1267
+ }
1268
+
1269
+ .hexclave-devtool .sdt-dashboard-unavailable-link:hover {
1270
+ text-decoration: underline;
1271
+ }
1272
+
1238
1273
  /* Shared content fade animation */
1239
1274
  .hexclave-devtool .sdt-tab-content-fade {
1240
1275
  animation: sdt-tab-fade-in 0.15s ease-out;