@hexclave/next 1.0.75 → 1.0.77

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 (231) hide show
  1. package/dist/{admin-app-impl-sTQD2d-3.d.ts → admin-app-impl-CxDpE89P.d.ts} +12 -10
  2. package/dist/admin-app-impl-CxDpE89P.d.ts.map +1 -0
  3. package/dist/clickmap/clickmap-core.d.ts +1 -1
  4. package/dist/clickmap/index.d.ts +1 -1
  5. package/dist/{client-app-BCT5p_70.d.ts → client-app-wFodsYml.d.ts} +4 -1
  6. package/dist/client-app-wFodsYml.d.ts.map +1 -0
  7. package/dist/common-BK_sydxF.d.ts.map +1 -1
  8. package/dist/components/elements/sidebar-layout.d.ts.map +1 -1
  9. package/dist/components/elements/sidebar-layout.js +12 -2
  10. package/dist/components/elements/sidebar-layout.js.map +1 -1
  11. package/dist/components/selected-team-switcher.d.ts +1 -1
  12. package/dist/components/team-icon.d.ts +1 -1
  13. package/dist/components/team-switcher.d.ts +1 -1
  14. package/dist/components-page/account-settings/payments/payments-page.d.ts +1 -1
  15. package/dist/components-page/account-settings/teams/leave-team-section.d.ts +1 -1
  16. package/dist/components-page/account-settings/teams/team-api-keys-section.d.ts +1 -1
  17. package/dist/components-page/account-settings/teams/team-display-name-section.d.ts +1 -1
  18. package/dist/components-page/account-settings/teams/team-member-invitation-section.d.ts +1 -1
  19. package/dist/components-page/account-settings/teams/team-member-list-section.d.ts +1 -1
  20. package/dist/components-page/account-settings/teams/team-page.d.ts +1 -1
  21. package/dist/components-page/account-settings/teams/team-profile-image-section.d.ts +1 -1
  22. package/dist/components-page/account-settings/teams/team-profile-user-section.d.ts +1 -1
  23. package/dist/components-page/hexclave-handler-client.d.ts +1 -1
  24. package/dist/components-page/hexclave-handler.d.ts +1 -1
  25. package/dist/components-page/team-invitation-cache.d.ts +5 -0
  26. package/dist/components-page/team-invitation-cache.d.ts.map +1 -0
  27. package/dist/components-page/team-invitation-cache.js +12 -0
  28. package/dist/components-page/team-invitation-cache.js.map +1 -0
  29. package/dist/components-page/team-invitation-cache.test.d.ts +1 -0
  30. package/dist/components-page/team-invitation-cache.test.js +23 -0
  31. package/dist/components-page/team-invitation-cache.test.js.map +1 -0
  32. package/dist/components-page/team-invitation.js +8 -7
  33. package/dist/components-page/team-invitation.js.map +1 -1
  34. package/dist/dev-tool/dev-tool-core.d.ts +1 -1
  35. package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
  36. package/dist/dev-tool/dev-tool-core.js +2 -2
  37. package/dist/dev-tool/dev-tool-core.js.map +1 -1
  38. package/dist/dev-tool/index.d.ts +1 -1
  39. package/dist/dev-tool/index.d.ts.map +1 -1
  40. package/dist/dev-tool/index.js +2 -8
  41. package/dist/dev-tool/index.js.map +1 -1
  42. package/dist/esm/components/elements/sidebar-layout.d.ts.map +1 -1
  43. package/dist/esm/components/elements/sidebar-layout.js +12 -2
  44. package/dist/esm/components/elements/sidebar-layout.js.map +1 -1
  45. package/dist/esm/components-page/team-invitation-cache.d.ts +5 -0
  46. package/dist/esm/components-page/team-invitation-cache.d.ts.map +1 -0
  47. package/dist/esm/components-page/team-invitation-cache.js +11 -0
  48. package/dist/esm/components-page/team-invitation-cache.js.map +1 -0
  49. package/dist/esm/components-page/team-invitation-cache.test.d.ts +1 -0
  50. package/dist/esm/components-page/team-invitation-cache.test.js +24 -0
  51. package/dist/esm/components-page/team-invitation-cache.test.js.map +1 -0
  52. package/dist/esm/components-page/team-invitation.d.ts.map +1 -1
  53. package/dist/esm/components-page/team-invitation.js +8 -7
  54. package/dist/esm/components-page/team-invitation.js.map +1 -1
  55. package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
  56. package/dist/esm/dev-tool/dev-tool-core.js +1 -1
  57. package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
  58. package/dist/esm/dev-tool/index.d.ts.map +1 -1
  59. package/dist/esm/dev-tool/index.js +2 -8
  60. package/dist/esm/dev-tool/index.js.map +1 -1
  61. package/dist/esm/generated/quetzal-translations.d.ts +2 -2
  62. package/dist/esm/in-page-ui/dev-environment.d.ts +5 -0
  63. package/dist/esm/in-page-ui/dev-environment.d.ts.map +1 -0
  64. package/dist/esm/in-page-ui/dev-environment.js +18 -0
  65. package/dist/esm/in-page-ui/dev-environment.js.map +1 -0
  66. package/dist/esm/in-page-ui/issue-card.d.ts +52 -0
  67. package/dist/esm/in-page-ui/issue-card.d.ts.map +1 -0
  68. package/dist/esm/in-page-ui/issue-card.js +392 -0
  69. package/dist/esm/in-page-ui/issue-card.js.map +1 -0
  70. package/dist/esm/in-page-ui/issue-card.test.d.ts +1 -0
  71. package/dist/esm/in-page-ui/issue-card.test.js +60 -0
  72. package/dist/esm/in-page-ui/issue-card.test.js.map +1 -0
  73. package/dist/esm/in-page-ui/logo.d.ts +5 -0
  74. package/dist/esm/in-page-ui/logo.d.ts.map +1 -0
  75. package/dist/esm/in-page-ui/logo.js +6 -0
  76. package/dist/esm/in-page-ui/logo.js.map +1 -0
  77. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +4 -4
  78. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  79. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +30 -1
  80. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -1
  81. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +140 -15
  82. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  83. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +5 -3
  84. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  85. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +184 -28
  86. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  87. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.d.ts +1 -0
  88. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js +83 -0
  89. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js.map +1 -0
  90. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.d.ts +1 -0
  91. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js +120 -0
  92. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js.map +1 -0
  93. package/dist/esm/lib/hexclave-app/apps/implementations/common.d.ts.map +1 -1
  94. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +4 -3
  95. package/dist/esm/lib/hexclave-app/apps/implementations/common.js.map +1 -1
  96. package/dist/esm/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
  97. package/dist/esm/lib/hexclave-app/apps/implementations/index.js +1 -1
  98. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +1 -0
  99. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -1
  100. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +2 -2
  101. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
  102. package/dist/esm/lib/hexclave-app/teams/index.d.ts +3 -0
  103. package/dist/esm/lib/hexclave-app/teams/index.d.ts.map +1 -1
  104. package/dist/esm/lib/hexclave-app/teams/index.js.map +1 -1
  105. package/dist/esm/pushed-config-error-overlay/index.d.ts.map +1 -1
  106. package/dist/esm/pushed-config-error-overlay/index.js +42 -343
  107. package/dist/esm/pushed-config-error-overlay/index.js.map +1 -1
  108. package/dist/esm/setup-error-overlay/index.d.ts +16 -0
  109. package/dist/esm/setup-error-overlay/index.d.ts.map +1 -0
  110. package/dist/esm/setup-error-overlay/index.js +106 -0
  111. package/dist/esm/setup-error-overlay/index.js.map +1 -0
  112. package/dist/esm/setup-error-overlay/index.test.d.ts +1 -0
  113. package/dist/esm/setup-error-overlay/index.test.js +125 -0
  114. package/dist/esm/setup-error-overlay/index.test.js.map +1 -0
  115. package/dist/esm/utils/monkey-patch.d.ts +7 -0
  116. package/dist/esm/utils/monkey-patch.d.ts.map +1 -0
  117. package/dist/esm/utils/monkey-patch.js +20 -0
  118. package/dist/esm/utils/monkey-patch.js.map +1 -0
  119. package/dist/esm/utils/react.d.ts +17 -0
  120. package/dist/esm/utils/react.d.ts.map +1 -0
  121. package/dist/esm/utils/react.js +60 -0
  122. package/dist/esm/utils/react.js.map +1 -0
  123. package/dist/generated/quetzal-translations.d.ts +1 -1
  124. package/dist/{hexclave-provider-BKxi3Vy-.d.ts → hexclave-provider-CMz2mt4I.d.ts} +3 -3
  125. package/dist/{hexclave-provider-BKxi3Vy-.d.ts.map → hexclave-provider-CMz2mt4I.d.ts.map} +1 -1
  126. package/dist/{hooks-CUe5bc3s.d.ts → hooks-CUHt4Y9C.d.ts} +2 -2
  127. package/dist/{hooks-CUe5bc3s.d.ts.map → hooks-CUHt4Y9C.d.ts.map} +1 -1
  128. package/dist/in-page-ui/dev-environment.d.ts +5 -0
  129. package/dist/in-page-ui/dev-environment.d.ts.map +1 -0
  130. package/dist/in-page-ui/dev-environment.js +19 -0
  131. package/dist/in-page-ui/dev-environment.js.map +1 -0
  132. package/dist/in-page-ui/issue-card.d.ts +52 -0
  133. package/dist/in-page-ui/issue-card.d.ts.map +1 -0
  134. package/dist/in-page-ui/issue-card.js +397 -0
  135. package/dist/in-page-ui/issue-card.js.map +1 -0
  136. package/dist/in-page-ui/issue-card.test.d.ts +1 -0
  137. package/dist/in-page-ui/issue-card.test.js +59 -0
  138. package/dist/in-page-ui/issue-card.test.js.map +1 -0
  139. package/dist/in-page-ui/logo.d.ts +5 -0
  140. package/dist/in-page-ui/logo.d.ts.map +1 -0
  141. package/dist/in-page-ui/logo.js +7 -0
  142. package/dist/in-page-ui/logo.js.map +1 -0
  143. package/dist/{index-CvB7XacB.d.ts → index-fR0LkUK7.d.ts} +2 -2
  144. package/dist/{index-CvB7XacB.d.ts.map → index-fR0LkUK7.d.ts.map} +1 -1
  145. package/dist/index.d.ts +4 -4
  146. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
  147. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +29 -0
  148. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -1
  149. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +140 -15
  150. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  151. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +2 -2
  152. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +182 -25
  153. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  154. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.d.ts +1 -0
  155. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js +82 -0
  156. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js.map +1 -0
  157. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.d.ts +1 -0
  158. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js +119 -0
  159. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js.map +1 -0
  160. package/dist/lib/hexclave-app/apps/implementations/common.js +4 -3
  161. package/dist/lib/hexclave-app/apps/implementations/common.js.map +1 -1
  162. package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
  163. package/dist/lib/hexclave-app/apps/implementations/index.js +1 -1
  164. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +1 -0
  165. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -1
  166. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  167. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
  168. package/dist/lib/hexclave-app/apps/index.d.ts +1 -1
  169. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -1
  170. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -1
  171. package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
  172. package/dist/lib/hexclave-app/index.d.ts +1 -1
  173. package/dist/lib/hexclave-app/projects/index.d.ts +1 -1
  174. package/dist/lib/hexclave-app/teams/index.d.ts +1 -1
  175. package/dist/lib/hexclave-app/teams/index.js.map +1 -1
  176. package/dist/lib/hexclave-app/users/index.d.ts +1 -1
  177. package/dist/lib/hooks.d.ts +1 -1
  178. package/dist/providers/hexclave-context.d.ts +1 -1
  179. package/dist/providers/hexclave-provider-client.d.ts +1 -1
  180. package/dist/providers/hexclave-provider.d.ts +1 -1
  181. package/dist/providers/translation-provider.d.ts +1 -1
  182. package/dist/pushed-config-error-overlay/index.d.ts +1 -1
  183. package/dist/pushed-config-error-overlay/index.d.ts.map +1 -1
  184. package/dist/pushed-config-error-overlay/index.js +41 -342
  185. package/dist/pushed-config-error-overlay/index.js.map +1 -1
  186. package/dist/{quetzal-translations-XeH3zQ9l.d.ts → quetzal-translations-B2jT2SWm.d.ts} +3 -3
  187. package/dist/{quetzal-translations-XeH3zQ9l.d.ts.map → quetzal-translations-B2jT2SWm.d.ts.map} +1 -1
  188. package/dist/setup-error-overlay/index.d.ts +16 -0
  189. package/dist/setup-error-overlay/index.d.ts.map +1 -0
  190. package/dist/setup-error-overlay/index.js +108 -0
  191. package/dist/setup-error-overlay/index.js.map +1 -0
  192. package/dist/setup-error-overlay/index.test.d.ts +1 -0
  193. package/dist/setup-error-overlay/index.test.js +124 -0
  194. package/dist/setup-error-overlay/index.test.js.map +1 -0
  195. package/dist/team-invitation-B5Ol7vdd.d.ts.map +1 -1
  196. package/dist/{translation-provider-WOaZXqAz.d.ts → translation-provider-6505O9P2.d.ts} +2 -2
  197. package/dist/{translation-provider-WOaZXqAz.d.ts.map → translation-provider-6505O9P2.d.ts.map} +1 -1
  198. package/dist/utils/monkey-patch.d.ts +7 -0
  199. package/dist/utils/monkey-patch.d.ts.map +1 -0
  200. package/dist/utils/monkey-patch.js +23 -0
  201. package/dist/utils/monkey-patch.js.map +1 -0
  202. package/dist/utils/react.d.ts +17 -0
  203. package/dist/utils/react.d.ts.map +1 -0
  204. package/dist/utils/react.js +63 -0
  205. package/dist/utils/react.js.map +1 -0
  206. package/package.json +4 -4
  207. package/src/components/elements/sidebar-layout.tsx +16 -1
  208. package/src/components-page/team-invitation-cache.test.ts +25 -0
  209. package/src/components-page/team-invitation-cache.ts +13 -0
  210. package/src/components-page/team-invitation.tsx +16 -7
  211. package/src/dev-tool/dev-tool-core.ts +1 -4
  212. package/src/dev-tool/index.ts +4 -15
  213. package/src/in-page-ui/dev-environment.ts +33 -0
  214. package/src/in-page-ui/issue-card.test.ts +67 -0
  215. package/src/in-page-ui/issue-card.ts +500 -0
  216. package/src/in-page-ui/logo.ts +8 -0
  217. package/src/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.ts +38 -1
  218. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +157 -0
  219. package/src/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.ts +96 -0
  220. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +255 -37
  221. package/src/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.ts +146 -0
  222. package/src/lib/hexclave-app/apps/implementations/common.ts +2 -1
  223. package/src/lib/hexclave-app/apps/implementations/redirect-page-urls.test.ts +1 -0
  224. package/src/lib/hexclave-app/teams/index.ts +3 -0
  225. package/src/pushed-config-error-overlay/index.ts +45 -386
  226. package/src/setup-error-overlay/index.test.ts +133 -0
  227. package/src/setup-error-overlay/index.ts +150 -0
  228. package/src/utils/monkey-patch.tsx +31 -0
  229. package/src/utils/react.tsx +93 -0
  230. package/dist/admin-app-impl-sTQD2d-3.d.ts.map +0 -1
  231. package/dist/client-app-BCT5p_70.d.ts.map +0 -1
@@ -2,9 +2,10 @@
2
2
  //===========================================
3
3
  // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
4
  //===========================================
5
- import { describe, expect, it } from "vitest";
5
+ import { describe, expect, it, vi } from "vitest";
6
6
  import { hexclaveAppInternalsSymbol } from "../../common";
7
7
  import { HexclaveClientApp, HexclaveClientAppConstructorOptions, HexclaveClientAppJson } from "../interfaces/client-app";
8
+ import { stripBrowserActionQueryParam } from "./client-app-impl";
8
9
 
9
10
  const baseOptions = {
10
11
  baseUrl: "http://localhost:12345",
@@ -123,4 +124,40 @@ describe("HexclaveClientApp automatic side effects", () => {
123
124
 
124
125
  expect(second).toBe(first);
125
126
  });
127
+
128
+ it("keeps stripping the browser action parameter after a router restores it", () => {
129
+ vi.useFakeTimers();
130
+ const previousWindow = Reflect.get(globalThis, "window");
131
+ const hadPreviousWindow = Reflect.has(globalThis, "window");
132
+ let currentHref = "http://localhost:12345/?hexclave_action_id=action-id";
133
+ const browserGlobal = {
134
+ location: {
135
+ get href() {
136
+ return currentHref;
137
+ },
138
+ },
139
+ history: {
140
+ state: null,
141
+ replaceState: (_state: null, _title: string, url: string | URL) => {
142
+ currentHref = String(url);
143
+ },
144
+ },
145
+ setTimeout,
146
+ };
147
+
148
+ Reflect.set(globalThis, "window", browserGlobal);
149
+ try {
150
+ stripBrowserActionQueryParam();
151
+ currentHref = "http://localhost:12345/?hexclave_action_id=action-id";
152
+ vi.runAllTimers();
153
+ expect(currentHref).toBe("http://localhost:12345/");
154
+ } finally {
155
+ vi.useRealTimers();
156
+ if (hadPreviousWindow) {
157
+ Reflect.set(globalThis, "window", previousWindow);
158
+ } else {
159
+ Reflect.deleteProperty(globalThis, "window");
160
+ }
161
+ }
162
+ });
126
163
  });
@@ -4,11 +4,15 @@
4
4
  //===========================================
5
5
  import { describe, expect, it, vi } from "vitest";
6
6
  import { AccessToken } from "@hexclave/shared/dist/sessions";
7
+ import { HexclaveSetupError } from "@hexclave/shared/dist/utils/errors";
7
8
  import { Store } from "@hexclave/shared/dist/utils/stores";
8
9
  import { hexclaveAppInternalsSymbol } from "../../common";
9
10
  import { StackClientApp } from "../interfaces/client-app";
10
11
  import { planRedirectToHandler } from "./redirect-page-urls";
11
12
 
13
+ // Every app in this file is constructed with `devTool: false`. The tests install a mock window and document, which makes
14
+ // the SDK believe it is in a browser; mounting the dev tool would then start background requests against a backend that
15
+ // does not exist here and leak that work across test cases.
12
16
  function createAccessTokenString(refreshTokenId: string): string {
13
17
  const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
14
18
  const nowSeconds = Math.floor(Date.now() / 1000);
@@ -54,6 +58,28 @@ function createMockDocument(): Document {
54
58
  } as any;
55
59
  }
56
60
 
61
+ /**
62
+ * Installs a stand-in for `window` with only the parts the code under test touches. `globalThis.window` is typed as a
63
+ * full `Window`, which a hand-written stub can never be, so the assignment goes through `Reflect.set` — the stub's own
64
+ * shape stays checked, unlike with a cast on the object literal.
65
+ */
66
+ function setMockWindow(mockWindow: {
67
+ location: { href: string, replace?: (url: string) => void },
68
+ }): void {
69
+ Reflect.set(globalThis, "window", mockWindow);
70
+ }
71
+
72
+ /**
73
+ * Calls one of the app's `protected` methods, which are part of the flows under test but not of the public SDK surface.
74
+ */
75
+ function callProtectedMethod(app: StackClientApp<true>, methodName: string, ...args: unknown[]): Promise<unknown> {
76
+ const method = Reflect.get(app, methodName);
77
+ if (typeof method !== "function") {
78
+ throw new Error(`Expected StackClientApp to have a ${methodName} method in tests.`);
79
+ }
80
+ return method.apply(app, args);
81
+ }
82
+
57
83
  describe("StackClientApp cross-domain auth", () => {
58
84
  it("keeps noRedirectBack same-domain redirects free of redirect-back state", async () => {
59
85
  const redirectPlan = await planRedirectToHandler({
@@ -145,6 +171,7 @@ describe("StackClientApp cross-domain auth", () => {
145
171
  signIn: "/handler/sign-in",
146
172
  },
147
173
  noAutomaticPrefetch: true,
174
+ devTool: false,
148
175
  });
149
176
 
150
177
  const redirectUrl = await clientApp[hexclaveAppInternalsSymbol].getRedirectToHandlerUrl("signIn");
@@ -173,6 +200,7 @@ describe("StackClientApp cross-domain auth", () => {
173
200
  },
174
201
  redirectMethod: "none",
175
202
  noAutomaticPrefetch: true,
203
+ devTool: false,
176
204
  });
177
205
 
178
206
  const clientInterface = Reflect.get(clientApp, "_interface");
@@ -253,6 +281,7 @@ describe("StackClientApp cross-domain auth", () => {
253
281
  default: { type: "hosted" },
254
282
  },
255
283
  noAutomaticPrefetch: true,
284
+ devTool: false,
256
285
  });
257
286
  const outerState = "outer-cross-domain-state";
258
287
  const outerCodeChallenge = "abcdefghijklmnopqrstuvwxyzABCDEFG_0123456789-._~";
@@ -314,6 +343,7 @@ describe("StackClientApp cross-domain auth", () => {
314
343
  default: { type: "hosted" },
315
344
  },
316
345
  noAutomaticPrefetch: true,
346
+ devTool: false,
317
347
  });
318
348
  const tokenStore = Reflect.get(clientApp, "_memoryTokenStore");
319
349
  if (!(tokenStore instanceof Store)) {
@@ -392,6 +422,7 @@ describe("StackClientApp cross-domain auth", () => {
392
422
  tokenStore: "cookie",
393
423
  redirectMethod: "none",
394
424
  noAutomaticPrefetch: true,
425
+ devTool: false,
395
426
  });
396
427
  const clientInterface = Reflect.get(clientApp, "_interface");
397
428
  const originalFetchNewAccessToken = Reflect.get(clientInterface, "fetchNewAccessToken");
@@ -466,6 +497,7 @@ describe("StackClientApp cross-domain auth", () => {
466
497
  tokenStore: "memory",
467
498
  redirectMethod: "window",
468
499
  noAutomaticPrefetch: true,
500
+ devTool: false,
469
501
  });
470
502
 
471
503
  globalThis.document = createMockDocument();
@@ -499,6 +531,122 @@ describe("StackClientApp cross-domain auth", () => {
499
531
  }
500
532
  });
501
533
 
534
+ it("reports malformed nested cross-domain auth URLs as setup errors", async () => {
535
+ const projectId = "00000000-0000-4000-8000-000000000012";
536
+ const previousWindow = globalThis.window;
537
+ const previousDocument = globalThis.document;
538
+ globalThis.document = createMockDocument();
539
+
540
+ // A URL the flow cannot even parse is as fatal as an untrusted one, so it has to reach the developer the same way
541
+ // instead of escaping as a bare TypeError from `new URL()`, which the overlay only shows during development.
542
+ // The two branches of the handler want opposite session states: on the provider side (a.com) a query-derived failure
543
+ // only becomes a setup error when the handoff belongs to the current session, while the requesting side (b.com) only
544
+ // bounces when the current session is *not* the one that was asked for.
545
+ const malformedCases = [
546
+ {
547
+ description: "redirect URI",
548
+ currentRefreshTokenId: "source-refresh-token-id",
549
+ params: { redirect_uri: "not-a-url", state: "nested-state", code_challenge: "nested-code-challenge" },
550
+ },
551
+ {
552
+ description: "after-callback redirect URL",
553
+ currentRefreshTokenId: "source-refresh-token-id",
554
+ params: {
555
+ redirect_uri: "https://source.example.test/handler/account-settings",
556
+ state: "nested-state",
557
+ code_challenge: "nested-code-challenge",
558
+ after_callback_redirect_url: "http://[",
559
+ },
560
+ },
561
+ {
562
+ description: "callback URL",
563
+ currentRefreshTokenId: null,
564
+ params: { stack_nested_cross_domain_auth_callback_url: "http://[" },
565
+ },
566
+ ];
567
+
568
+ try {
569
+ for (const malformedCase of malformedCases) {
570
+ const currentUrl = new URL("https://target.example.test/nested-provider");
571
+ currentUrl.searchParams.set("stack_nested_cross_domain_auth_refresh_token_id", "source-refresh-token-id");
572
+ for (const [key, value] of Object.entries(malformedCase.params)) {
573
+ currentUrl.searchParams.set(key, value);
574
+ }
575
+ // Each app is constructed with the mock window uninstalled: the constructor's automatic side effects expect a
576
+ // real window, and a construction-time nested-auth URL would also schedule a second, unawaited run of the
577
+ // handler that keeps failing in the background and leaks into whichever test runs next.
578
+ globalThis.window = previousWindow;
579
+ const clientApp = new StackClientApp({
580
+ baseUrl: "http://localhost:12345",
581
+ projectId,
582
+ publishableClientKey: "stack-pk-test",
583
+ tokenStore: "memory",
584
+ redirectMethod: "window",
585
+ noAutomaticPrefetch: true,
586
+ devTool: false,
587
+ });
588
+ setMockWindow({ location: { href: currentUrl.toString() } });
589
+ Reflect.set(clientApp, "_fetchCurrentRefreshTokenIdIfSignedIn", async () => malformedCase.currentRefreshTokenId);
590
+ Reflect.set(clientApp, "_isTrusted", async () => true);
591
+
592
+ const nestedAuthPromise = callProtectedMethod(clientApp, "_maybeHandleNestedCrossDomainAuth");
593
+ await expect(nestedAuthPromise).rejects.toThrowError(new RegExp(`${malformedCase.description} .* is not a valid absolute URL`));
594
+ await expect(nestedAuthPromise).rejects.toSatisfy((error: unknown) => HexclaveSetupError.isSetupError(error));
595
+ }
596
+ } finally {
597
+ globalThis.window = previousWindow;
598
+ globalThis.document = previousDocument;
599
+ }
600
+ });
601
+
602
+ it("keeps nested cross-domain auth failures off the page for a handoff that does not own the session", async () => {
603
+ const projectId = "00000000-0000-4000-8000-000000000013";
604
+ const previousWindow = globalThis.window;
605
+ const previousDocument = globalThis.document;
606
+ globalThis.document = createMockDocument();
607
+
608
+ // Setup errors are shown in production, so a link anybody can craft must not be able to put a card on a healthy
609
+ // app's page: with a refresh-token ID that is not the current session's, every failure below stays an ordinary error
610
+ // that only captureError sees, even though the same URL from the right session would render a card.
611
+ const craftedRedirectUris = [
612
+ "https://evil.example.test/handler/account-settings", // untrusted
613
+ "not-a-url", // malformed
614
+ ];
615
+
616
+ try {
617
+ for (const craftedRedirectUri of craftedRedirectUris) {
618
+ const currentUrl = new URL("https://target.example.test/nested-provider");
619
+ currentUrl.searchParams.set("stack_nested_cross_domain_auth_refresh_token_id", "attacker-chosen-refresh-token-id");
620
+ currentUrl.searchParams.set("redirect_uri", craftedRedirectUri);
621
+ currentUrl.searchParams.set("state", "nested-state");
622
+ currentUrl.searchParams.set("code_challenge", "nested-code-challenge");
623
+
624
+ globalThis.window = previousWindow;
625
+ const clientApp = new StackClientApp({
626
+ baseUrl: "http://localhost:12345",
627
+ projectId,
628
+ publishableClientKey: "stack-pk-test",
629
+ tokenStore: "memory",
630
+ redirectMethod: "window",
631
+ noAutomaticPrefetch: true,
632
+ devTool: false,
633
+ });
634
+ setMockWindow({ location: { href: currentUrl.toString() } });
635
+ Reflect.set(clientApp, "_fetchCurrentRefreshTokenIdIfSignedIn", async () => "this-apps-refresh-token-id");
636
+ Reflect.set(clientApp, "_isTrusted", async () => false);
637
+
638
+ const nestedAuthPromise = callProtectedMethod(clientApp, "_maybeHandleNestedCrossDomainAuth");
639
+ await expect(nestedAuthPromise).rejects.toThrowError(/does not match the requested refresh token ID/);
640
+ await expect(nestedAuthPromise).rejects.toSatisfy(
641
+ (error: unknown) => !HexclaveSetupError.isSetupError(error),
642
+ );
643
+ }
644
+ } finally {
645
+ globalThis.window = previousWindow;
646
+ globalThis.document = previousDocument;
647
+ }
648
+ });
649
+
502
650
  it("passes the construction-time URL to the nested cross-domain auth handler", async () => {
503
651
  const projectId = "00000000-0000-4000-8000-000000000009";
504
652
  const previousWindow = globalThis.window;
@@ -529,6 +677,7 @@ describe("StackClientApp cross-domain auth", () => {
529
677
  tokenStore: "memory",
530
678
  redirectMethod: "window",
531
679
  noAutomaticPrefetch: true,
680
+ devTool: false,
532
681
  });
533
682
 
534
683
  // Simulate consumeOAuthCallbackQueryParams stripping code+state before microtasks run.
@@ -597,6 +746,7 @@ describe("StackClientApp cross-domain auth", () => {
597
746
  default: { type: "hosted" },
598
747
  },
599
748
  noAutomaticPrefetch: true,
749
+ devTool: false,
600
750
  });
601
751
 
602
752
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -629,6 +779,7 @@ describe("StackClientApp cross-domain auth", () => {
629
779
  signOut: { type: "hosted" },
630
780
  },
631
781
  noAutomaticPrefetch: true,
782
+ devTool: false,
632
783
  });
633
784
  const signOutSpy = vi.spyOn(clientApp, "signOut").mockRejectedValue(new Error("INTENTIONAL_TEST_ABORT"));
634
785
 
@@ -651,6 +802,7 @@ describe("StackClientApp cross-domain auth", () => {
651
802
  default: { type: "hosted" },
652
803
  },
653
804
  noAutomaticPrefetch: true,
805
+ devTool: false,
654
806
  });
655
807
 
656
808
  expect(() => clientApp.urls.signIn).toThrowError(/app\.urls\.signIn cannot be used when this app is configured to use hosted components.*Use app\.redirectToSignIn\(\) instead/s);
@@ -669,6 +821,7 @@ describe("StackClientApp cross-domain auth", () => {
669
821
  handler: "/custom-handler",
670
822
  },
671
823
  noAutomaticPrefetch: true,
824
+ devTool: false,
672
825
  });
673
826
 
674
827
  expect(clientApp.urls.signIn).toBe("/custom-handler/sign-in");
@@ -685,6 +838,7 @@ describe("StackClientApp cross-domain auth", () => {
685
838
  default: { type: "hosted" },
686
839
  },
687
840
  noAutomaticPrefetch: true,
841
+ devTool: false,
688
842
  });
689
843
  const currentHref = "https://demo.stack-auth.com/settings?tab=profile";
690
844
 
@@ -754,6 +908,7 @@ describe("StackClientApp cross-domain auth", () => {
754
908
  tokenStore: "memory",
755
909
  redirectMethod: "window",
756
910
  noAutomaticPrefetch: true,
911
+ devTool: false,
757
912
  });
758
913
 
759
914
  // Intermediate hops (MFA, magic link, OAuth round-trips, ...) dropped the query params.
@@ -822,6 +977,7 @@ describe("StackClientApp cross-domain auth", () => {
822
977
  tokenStore: "memory",
823
978
  redirectMethod: "window",
824
979
  noAutomaticPrefetch: true,
980
+ devTool: false,
825
981
  });
826
982
  const crossDomainAuthorizeRedirect = "https://demo.example.test/handler/oauth-callback?code=minted-code&state=mirror-handoff-state";
827
983
  const createCrossDomainAuthRedirectUrlSpy = vi
@@ -858,6 +1014,7 @@ describe("StackClientApp cross-domain auth", () => {
858
1014
  tokenStore: "memory",
859
1015
  redirectMethod: "none",
860
1016
  noAutomaticPrefetch: true,
1017
+ devTool: false,
861
1018
  });
862
1019
  const oldAccessToken = createAccessTokenString("old-refresh-token-id");
863
1020
  const refreshedOldAccessToken = createAccessTokenString("refreshed-old-refresh-token-id");
@@ -0,0 +1,96 @@
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 { AsyncStore } from "@hexclave/shared/dist/utils/stores";
6
+ import { describe, expect, it } from "vitest";
7
+ import { StackClientApp } from "../interfaces/client-app";
8
+
9
+ function createAccessTokenString(refreshTokenId: string): string {
10
+ const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
11
+ const nowSeconds = Math.floor(Date.now() / 1000);
12
+ return [
13
+ encode({ alg: "none", typ: "JWT" }),
14
+ encode({ sub: "user-id", exp: nowSeconds + 60, iat: nowSeconds, refresh_token_id: refreshTokenId }),
15
+ "",
16
+ ].join(".");
17
+ }
18
+
19
+ class Deferred {
20
+ readonly promise: Promise<void>;
21
+ private _resolve: () => void = () => {
22
+ throw new Error("Deferred promise was resolved before initialization");
23
+ };
24
+
25
+ constructor() {
26
+ this.promise = new Promise<void>((resolve) => {
27
+ this._resolve = resolve;
28
+ });
29
+ }
30
+
31
+ resolve(): void {
32
+ this._resolve();
33
+ }
34
+ }
35
+
36
+ describe("StackClientApp sign-out", () => {
37
+ it("delays current-user reads until redirect dispatch without blocking unrelated stores", async () => {
38
+ const clientApp = new StackClientApp({
39
+ baseUrl: "http://localhost:12345",
40
+ projectId: "00000000-0000-4000-8000-000000000000",
41
+ publishableClientKey: "stack-pk-test",
42
+ tokenStore: "memory",
43
+ redirectMethod: "none",
44
+ noAutomaticPrefetch: true,
45
+ devTool: false,
46
+ });
47
+ const clientInterface = Reflect.get(clientApp, "_interface");
48
+ Reflect.set(clientInterface, "getClientUserByToken", async () => ({
49
+ id: "user-id",
50
+ is_anonymous: false,
51
+ is_restricted: false,
52
+ }));
53
+
54
+ const signInToAccountWithTokens = Reflect.get(clientApp, "_signInToAccountWithTokens");
55
+ await signInToAccountWithTokens.call(clientApp, {
56
+ accessToken: createAccessTokenString("refresh-token-id"),
57
+ refreshToken: "refresh-token",
58
+ });
59
+
60
+ const createCookieHelper = Reflect.get(clientApp, "_createCookieHelper");
61
+ const getOrCreateTokenStore = Reflect.get(clientApp, "_getOrCreateTokenStore");
62
+ const getSessionFromTokenStore = Reflect.get(clientApp, "_getSessionFromTokenStore");
63
+ const tokenStore = getOrCreateTokenStore.call(clientApp, await createCookieHelper.call(clientApp));
64
+ const session = getSessionFromTokenStore.call(clientApp, tokenStore);
65
+
66
+ Reflect.set(clientInterface, "signOut", async () => {
67
+ session.markInvalid();
68
+ });
69
+
70
+ const redirectStarted = new Deferred();
71
+ const redirectFinished = new Deferred();
72
+ Reflect.set(clientApp, "_redirectToDefaultAfterSignOut", async () => {
73
+ redirectStarted.resolve();
74
+ await redirectFinished.promise;
75
+ });
76
+
77
+ const unrelatedStore = new AsyncStore<void>();
78
+ expect(unrelatedStore.setAsync(new Promise<void>(() => {}))).toBeInstanceOf(Promise);
79
+
80
+ const signOut = Reflect.get(clientApp, "_signOut");
81
+ const signOutPromise = signOut.call(clientApp, session);
82
+ await redirectStarted.promise;
83
+
84
+ const getUserDuringSignOut = clientApp.getUser();
85
+ const stateBeforeRedirectFinished = await Promise.race([
86
+ getUserDuringSignOut.then(() => "resolved"),
87
+ Promise.resolve("pending"),
88
+ ]);
89
+ expect(stateBeforeRedirectFinished).toBe("pending");
90
+
91
+ redirectFinished.resolve();
92
+ await signOutPromise;
93
+
94
+ await expect(getUserDuringSignOut).resolves.toBeNull();
95
+ });
96
+ });