@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.
- package/dist/{admin-app-impl-sTQD2d-3.d.ts → admin-app-impl-CxDpE89P.d.ts} +12 -10
- package/dist/admin-app-impl-CxDpE89P.d.ts.map +1 -0
- package/dist/clickmap/clickmap-core.d.ts +1 -1
- package/dist/clickmap/index.d.ts +1 -1
- package/dist/{client-app-BCT5p_70.d.ts → client-app-wFodsYml.d.ts} +4 -1
- package/dist/client-app-wFodsYml.d.ts.map +1 -0
- package/dist/common-BK_sydxF.d.ts.map +1 -1
- package/dist/components/elements/sidebar-layout.d.ts.map +1 -1
- package/dist/components/elements/sidebar-layout.js +12 -2
- package/dist/components/elements/sidebar-layout.js.map +1 -1
- package/dist/components/selected-team-switcher.d.ts +1 -1
- package/dist/components/team-icon.d.ts +1 -1
- package/dist/components/team-switcher.d.ts +1 -1
- package/dist/components-page/account-settings/payments/payments-page.d.ts +1 -1
- package/dist/components-page/account-settings/teams/leave-team-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-api-keys-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-display-name-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-member-invitation-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-member-list-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-page.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-profile-image-section.d.ts +1 -1
- package/dist/components-page/account-settings/teams/team-profile-user-section.d.ts +1 -1
- package/dist/components-page/hexclave-handler-client.d.ts +1 -1
- package/dist/components-page/hexclave-handler.d.ts +1 -1
- package/dist/components-page/team-invitation-cache.d.ts +5 -0
- package/dist/components-page/team-invitation-cache.d.ts.map +1 -0
- package/dist/components-page/team-invitation-cache.js +12 -0
- package/dist/components-page/team-invitation-cache.js.map +1 -0
- package/dist/components-page/team-invitation-cache.test.d.ts +1 -0
- package/dist/components-page/team-invitation-cache.test.js +23 -0
- package/dist/components-page/team-invitation-cache.test.js.map +1 -0
- package/dist/components-page/team-invitation.js +8 -7
- package/dist/components-page/team-invitation.js.map +1 -1
- package/dist/dev-tool/dev-tool-core.d.ts +1 -1
- package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
- package/dist/dev-tool/dev-tool-core.js +2 -2
- package/dist/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/dev-tool/index.d.ts +1 -1
- package/dist/dev-tool/index.d.ts.map +1 -1
- package/dist/dev-tool/index.js +2 -8
- package/dist/dev-tool/index.js.map +1 -1
- package/dist/esm/components/elements/sidebar-layout.d.ts.map +1 -1
- package/dist/esm/components/elements/sidebar-layout.js +12 -2
- package/dist/esm/components/elements/sidebar-layout.js.map +1 -1
- package/dist/esm/components-page/team-invitation-cache.d.ts +5 -0
- package/dist/esm/components-page/team-invitation-cache.d.ts.map +1 -0
- package/dist/esm/components-page/team-invitation-cache.js +11 -0
- package/dist/esm/components-page/team-invitation-cache.js.map +1 -0
- package/dist/esm/components-page/team-invitation-cache.test.d.ts +1 -0
- package/dist/esm/components-page/team-invitation-cache.test.js +24 -0
- package/dist/esm/components-page/team-invitation-cache.test.js.map +1 -0
- package/dist/esm/components-page/team-invitation.d.ts.map +1 -1
- package/dist/esm/components-page/team-invitation.js +8 -7
- package/dist/esm/components-page/team-invitation.js.map +1 -1
- package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
- package/dist/esm/dev-tool/dev-tool-core.js +1 -1
- package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/esm/dev-tool/index.d.ts.map +1 -1
- package/dist/esm/dev-tool/index.js +2 -8
- package/dist/esm/dev-tool/index.js.map +1 -1
- package/dist/esm/generated/quetzal-translations.d.ts +2 -2
- package/dist/esm/in-page-ui/dev-environment.d.ts +5 -0
- package/dist/esm/in-page-ui/dev-environment.d.ts.map +1 -0
- package/dist/esm/in-page-ui/dev-environment.js +18 -0
- package/dist/esm/in-page-ui/dev-environment.js.map +1 -0
- package/dist/esm/in-page-ui/issue-card.d.ts +52 -0
- package/dist/esm/in-page-ui/issue-card.d.ts.map +1 -0
- package/dist/esm/in-page-ui/issue-card.js +392 -0
- package/dist/esm/in-page-ui/issue-card.js.map +1 -0
- package/dist/esm/in-page-ui/issue-card.test.d.ts +1 -0
- package/dist/esm/in-page-ui/issue-card.test.js +60 -0
- package/dist/esm/in-page-ui/issue-card.test.js.map +1 -0
- package/dist/esm/in-page-ui/logo.d.ts +5 -0
- package/dist/esm/in-page-ui/logo.d.ts.map +1 -0
- package/dist/esm/in-page-ui/logo.js +6 -0
- package/dist/esm/in-page-ui/logo.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +4 -4
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +30 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +140 -15
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +5 -3
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +184 -28
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js +83 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js +120 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/common.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +4 -3
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/index.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
- package/dist/esm/lib/hexclave-app/teams/index.d.ts +3 -0
- package/dist/esm/lib/hexclave-app/teams/index.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/teams/index.js.map +1 -1
- package/dist/esm/pushed-config-error-overlay/index.d.ts.map +1 -1
- package/dist/esm/pushed-config-error-overlay/index.js +42 -343
- package/dist/esm/pushed-config-error-overlay/index.js.map +1 -1
- package/dist/esm/setup-error-overlay/index.d.ts +16 -0
- package/dist/esm/setup-error-overlay/index.d.ts.map +1 -0
- package/dist/esm/setup-error-overlay/index.js +106 -0
- package/dist/esm/setup-error-overlay/index.js.map +1 -0
- package/dist/esm/setup-error-overlay/index.test.d.ts +1 -0
- package/dist/esm/setup-error-overlay/index.test.js +125 -0
- package/dist/esm/setup-error-overlay/index.test.js.map +1 -0
- package/dist/esm/utils/monkey-patch.d.ts +7 -0
- package/dist/esm/utils/monkey-patch.d.ts.map +1 -0
- package/dist/esm/utils/monkey-patch.js +20 -0
- package/dist/esm/utils/monkey-patch.js.map +1 -0
- package/dist/esm/utils/react.d.ts +17 -0
- package/dist/esm/utils/react.d.ts.map +1 -0
- package/dist/esm/utils/react.js +60 -0
- package/dist/esm/utils/react.js.map +1 -0
- package/dist/generated/quetzal-translations.d.ts +1 -1
- package/dist/{hexclave-provider-BKxi3Vy-.d.ts → hexclave-provider-CMz2mt4I.d.ts} +3 -3
- package/dist/{hexclave-provider-BKxi3Vy-.d.ts.map → hexclave-provider-CMz2mt4I.d.ts.map} +1 -1
- package/dist/{hooks-CUe5bc3s.d.ts → hooks-CUHt4Y9C.d.ts} +2 -2
- package/dist/{hooks-CUe5bc3s.d.ts.map → hooks-CUHt4Y9C.d.ts.map} +1 -1
- package/dist/in-page-ui/dev-environment.d.ts +5 -0
- package/dist/in-page-ui/dev-environment.d.ts.map +1 -0
- package/dist/in-page-ui/dev-environment.js +19 -0
- package/dist/in-page-ui/dev-environment.js.map +1 -0
- package/dist/in-page-ui/issue-card.d.ts +52 -0
- package/dist/in-page-ui/issue-card.d.ts.map +1 -0
- package/dist/in-page-ui/issue-card.js +397 -0
- package/dist/in-page-ui/issue-card.js.map +1 -0
- package/dist/in-page-ui/issue-card.test.d.ts +1 -0
- package/dist/in-page-ui/issue-card.test.js +59 -0
- package/dist/in-page-ui/issue-card.test.js.map +1 -0
- package/dist/in-page-ui/logo.d.ts +5 -0
- package/dist/in-page-ui/logo.d.ts.map +1 -0
- package/dist/in-page-ui/logo.js +7 -0
- package/dist/in-page-ui/logo.js.map +1 -0
- package/dist/{index-CvB7XacB.d.ts → index-fR0LkUK7.d.ts} +2 -2
- package/dist/{index-CvB7XacB.d.ts.map → index-fR0LkUK7.d.ts.map} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +29 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +140 -15
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +2 -2
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +182 -25
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js +82 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js +119 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/common.js +4 -3
- package/dist/lib/hexclave-app/apps/implementations/common.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/index.js +1 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +1 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
- package/dist/lib/hexclave-app/apps/index.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
- package/dist/lib/hexclave-app/index.d.ts +1 -1
- package/dist/lib/hexclave-app/projects/index.d.ts +1 -1
- package/dist/lib/hexclave-app/teams/index.d.ts +1 -1
- package/dist/lib/hexclave-app/teams/index.js.map +1 -1
- package/dist/lib/hexclave-app/users/index.d.ts +1 -1
- package/dist/lib/hooks.d.ts +1 -1
- package/dist/providers/hexclave-context.d.ts +1 -1
- package/dist/providers/hexclave-provider-client.d.ts +1 -1
- package/dist/providers/hexclave-provider.d.ts +1 -1
- package/dist/providers/translation-provider.d.ts +1 -1
- package/dist/pushed-config-error-overlay/index.d.ts +1 -1
- package/dist/pushed-config-error-overlay/index.d.ts.map +1 -1
- package/dist/pushed-config-error-overlay/index.js +41 -342
- package/dist/pushed-config-error-overlay/index.js.map +1 -1
- package/dist/{quetzal-translations-XeH3zQ9l.d.ts → quetzal-translations-B2jT2SWm.d.ts} +3 -3
- package/dist/{quetzal-translations-XeH3zQ9l.d.ts.map → quetzal-translations-B2jT2SWm.d.ts.map} +1 -1
- package/dist/setup-error-overlay/index.d.ts +16 -0
- package/dist/setup-error-overlay/index.d.ts.map +1 -0
- package/dist/setup-error-overlay/index.js +108 -0
- package/dist/setup-error-overlay/index.js.map +1 -0
- package/dist/setup-error-overlay/index.test.d.ts +1 -0
- package/dist/setup-error-overlay/index.test.js +124 -0
- package/dist/setup-error-overlay/index.test.js.map +1 -0
- package/dist/team-invitation-B5Ol7vdd.d.ts.map +1 -1
- package/dist/{translation-provider-WOaZXqAz.d.ts → translation-provider-6505O9P2.d.ts} +2 -2
- package/dist/{translation-provider-WOaZXqAz.d.ts.map → translation-provider-6505O9P2.d.ts.map} +1 -1
- package/dist/utils/monkey-patch.d.ts +7 -0
- package/dist/utils/monkey-patch.d.ts.map +1 -0
- package/dist/utils/monkey-patch.js +23 -0
- package/dist/utils/monkey-patch.js.map +1 -0
- package/dist/utils/react.d.ts +17 -0
- package/dist/utils/react.d.ts.map +1 -0
- package/dist/utils/react.js +63 -0
- package/dist/utils/react.js.map +1 -0
- package/package.json +4 -4
- package/src/components/elements/sidebar-layout.tsx +16 -1
- package/src/components-page/team-invitation-cache.test.ts +25 -0
- package/src/components-page/team-invitation-cache.ts +13 -0
- package/src/components-page/team-invitation.tsx +16 -7
- package/src/dev-tool/dev-tool-core.ts +1 -4
- package/src/dev-tool/index.ts +4 -15
- package/src/in-page-ui/dev-environment.ts +33 -0
- package/src/in-page-ui/issue-card.test.ts +67 -0
- package/src/in-page-ui/issue-card.ts +500 -0
- package/src/in-page-ui/logo.ts +8 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.ts +38 -1
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +157 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.sign-out.test.ts +96 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +255 -37
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.ts +146 -0
- package/src/lib/hexclave-app/apps/implementations/common.ts +2 -1
- package/src/lib/hexclave-app/apps/implementations/redirect-page-urls.test.ts +1 -0
- package/src/lib/hexclave-app/teams/index.ts +3 -0
- package/src/pushed-config-error-overlay/index.ts +45 -386
- package/src/setup-error-overlay/index.test.ts +133 -0
- package/src/setup-error-overlay/index.ts +150 -0
- package/src/utils/monkey-patch.tsx +31 -0
- package/src/utils/react.tsx +93 -0
- package/dist/admin-app-impl-sTQD2d-3.d.ts.map +0 -1
- package/dist/client-app-BCT5p_70.d.ts.map +0 -1
package/src/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.ts
CHANGED
|
@@ -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
|
+
});
|