@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
|
@@ -25,20 +25,21 @@ import { InternalSession } from "@hexclave/shared/dist/sessions";
|
|
|
25
25
|
import { decodeBase32, decodeBase64, encodeBase32, encodeBase64 } from "@hexclave/shared/dist/utils/bytes";
|
|
26
26
|
import { scrambleDuringCompileTime } from "@hexclave/shared/dist/utils/compile-time";
|
|
27
27
|
import { isBrowserLike } from "@hexclave/shared/dist/utils/env";
|
|
28
|
-
import { HexclaveAssertionError, captureError, throwErr } from "@hexclave/shared/dist/utils/errors";
|
|
28
|
+
import { HexclaveAssertionError, HexclaveSetupError, captureError, throwErr } from "@hexclave/shared/dist/utils/errors";
|
|
29
29
|
import { parseJson } from "@hexclave/shared/dist/utils/json";
|
|
30
30
|
import { DependenciesMap } from "@hexclave/shared/dist/utils/maps";
|
|
31
31
|
import { ProviderType } from "@hexclave/shared/dist/utils/oauth";
|
|
32
32
|
import { deepPlainEquals, omit } from "@hexclave/shared/dist/utils/objects";
|
|
33
33
|
import { neverResolve, runAsynchronously, wait } from "@hexclave/shared/dist/utils/promises";
|
|
34
|
-
import { suspend,
|
|
34
|
+
import { suspend, use } from "@hexclave/shared/dist/utils/react";
|
|
35
35
|
import { getTrustedParentDomain, validateRedirectUrl } from "@hexclave/shared/dist/utils/redirect-urls";
|
|
36
36
|
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
37
|
-
import { Store
|
|
37
|
+
import { Store } from "@hexclave/shared/dist/utils/stores";
|
|
38
38
|
import { deindent, mergeScopeStrings } from "@hexclave/shared/dist/utils/strings";
|
|
39
39
|
import type { TurnstileAction } from "@hexclave/shared/dist/utils/turnstile";
|
|
40
40
|
import { BotChallengeExecutionFailedError, BotChallengeUserCancelledError, withBotChallengeFlow } from "@hexclave/shared/dist/utils/turnstile-flow";
|
|
41
41
|
import { createUrlIfValid, getRelativePart, isRelative } from "@hexclave/shared/dist/utils/urls";
|
|
42
|
+
import { BROWSER_ACTION_QUERY_PARAM } from "@hexclave/shared/dist/utils/browser-action-snippets";
|
|
42
43
|
import { generateUuid } from "@hexclave/shared/dist/utils/uuids";
|
|
43
44
|
import { WebAuthnError, startAuthentication, startRegistration } from "@simplewebauthn/browser";
|
|
44
45
|
import * as cookie from "cookie";
|
|
@@ -46,6 +47,7 @@ import * as NextNavigationUnscrambled from "next/navigation"; // import the enti
|
|
|
46
47
|
import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react-like
|
|
47
48
|
import type * as yup from "yup";
|
|
48
49
|
import { envVars } from "../../../../generated/env";
|
|
50
|
+
import { suspendIfSsr } from "../../../../utils/react"; // THIS_LINE_PLATFORM react-like
|
|
49
51
|
import { constructRedirectUrl } from "../../../../utils/url";
|
|
50
52
|
import { callOAuthCallback, getNewOAuthProviderOrScopeUrl } from "../../../auth";
|
|
51
53
|
import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookie, deleteCookieClient, getCookieClient, isSecure as isSecureCookieContext, saveVerifierAndState, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
|
|
@@ -72,10 +74,27 @@ import { crossDomainAuthQueryParams, getCrossDomainHandoffParamsFromCurrentUrl,
|
|
|
72
74
|
import { subscribeSessionRefresh } from "./session-refresh-subscription";
|
|
73
75
|
import { AnalyticsOptions, SessionRecorder, analyticsOptionsFromJson, analyticsOptionsToJson, getSessionReplayOptions } from "./session-replay";
|
|
74
76
|
|
|
77
|
+
export function stripBrowserActionQueryParam() {
|
|
78
|
+
let stripAttemptsRemaining = 20;
|
|
79
|
+
const stripActionId = () => {
|
|
80
|
+
const currentUrl = new URL(window.location.href);
|
|
81
|
+
if (currentUrl.searchParams.has(BROWSER_ACTION_QUERY_PARAM)) {
|
|
82
|
+
currentUrl.searchParams.delete(BROWSER_ACTION_QUERY_PARAM);
|
|
83
|
+
window.history.replaceState(window.history.state, "", currentUrl);
|
|
84
|
+
}
|
|
85
|
+
if (stripAttemptsRemaining > 0) {
|
|
86
|
+
stripAttemptsRemaining -= 1;
|
|
87
|
+
window.setTimeout(stripActionId, 0);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
stripActionId();
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
import { useAsyncCache } from "./common";
|
|
76
94
|
import { mountClickmapOverlay } from "../../../../clickmap";
|
|
77
95
|
import { mountDevTool } from "../../../../dev-tool";
|
|
78
96
|
import { mountPushedConfigErrorOverlay } from "../../../../pushed-config-error-overlay";
|
|
97
|
+
import { showSetupErrorOverlay } from "../../../../setup-error-overlay";
|
|
79
98
|
|
|
80
99
|
let isReactServer = false;
|
|
81
100
|
import * as sc from "@hexclave/sc";
|
|
@@ -98,6 +117,81 @@ const nestedCrossDomainAuthQueryParams = {
|
|
|
98
117
|
afterCallbackRedirectUrl: oauthAfterCallbackRedirectUrlQueryParam,
|
|
99
118
|
} as const;
|
|
100
119
|
|
|
120
|
+
/**
|
|
121
|
+
* An auth flow can only ever send the user to a URL that the project trusts, so an untrusted one means the flow is dead
|
|
122
|
+
* until someone changes the project's configuration. Describe the problem in terms of that configuration rather than in
|
|
123
|
+
* terms of the flow's internals, which nobody outside this file knows about.
|
|
124
|
+
*/
|
|
125
|
+
function createUntrustedUrlError(options: {
|
|
126
|
+
/** Kept verbatim from the throw site, so the message stays greppable for whoever already has it in their logs. */
|
|
127
|
+
message: string,
|
|
128
|
+
url: string,
|
|
129
|
+
projectId: string,
|
|
130
|
+
cause?: unknown,
|
|
131
|
+
}): HexclaveSetupError {
|
|
132
|
+
const origin = createUrlIfValid(options.url)?.origin ?? options.url;
|
|
133
|
+
return new HexclaveSetupError({
|
|
134
|
+
title: "A domain in your authentication flow is not one of your project's trusted domains",
|
|
135
|
+
message: options.message,
|
|
136
|
+
howToFix: [
|
|
137
|
+
`If ${origin} is yours, add it to your project's trusted domains in the Hexclave dashboard, under Domains.`,
|
|
138
|
+
"Protocol, domain and port must all match a trusted domain exactly; a wildcard like https://*.example.com matches only one subdomain level.",
|
|
139
|
+
`If ${origin} is not yours, do not add it \u2014 somebody may be trying to redirect your users' credentials to it.`,
|
|
140
|
+
],
|
|
141
|
+
extraData: {
|
|
142
|
+
url: options.url,
|
|
143
|
+
projectId: options.projectId,
|
|
144
|
+
...options.cause === undefined ? {} : { cause: options.cause },
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function createUntrustedNestedCrossDomainUrlError(options: {
|
|
150
|
+
urlDescription: string,
|
|
151
|
+
url: string,
|
|
152
|
+
projectId: string,
|
|
153
|
+
}): HexclaveSetupError {
|
|
154
|
+
return createUntrustedUrlError({
|
|
155
|
+
message: `Nested cross-domain auth ${options.urlDescription} ${options.url} is not trusted.`,
|
|
156
|
+
url: options.url,
|
|
157
|
+
projectId: options.projectId,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A URL that the auth flow cannot even parse is just as fatal as an untrusted one, and just as much a configuration
|
|
163
|
+
* problem: something in the developer's setup put a relative or malformed value where an absolute URL belongs.
|
|
164
|
+
*/
|
|
165
|
+
function createMalformedNestedCrossDomainUrlError(options: {
|
|
166
|
+
urlDescription: string,
|
|
167
|
+
url: string,
|
|
168
|
+
}): HexclaveSetupError {
|
|
169
|
+
return new HexclaveSetupError({
|
|
170
|
+
title: "A URL in your authentication flow is not a valid absolute URL",
|
|
171
|
+
message: `Nested cross-domain auth ${options.urlDescription} ${options.url} is not a valid absolute URL.`,
|
|
172
|
+
howToFix: [
|
|
173
|
+
"Handler URLs and after-auth redirect URLs must be absolute, including their protocol, like https://example.com/handler/oauth-callback.",
|
|
174
|
+
"Check the handler URLs configured for your project and the redirect URLs you pass to Hexclave's sign-in and sign-up methods.",
|
|
175
|
+
"If none of those look wrong, check whether a proxy or rewrite in front of your app is rewriting the authentication flow's query parameters.",
|
|
176
|
+
],
|
|
177
|
+
extraData: {
|
|
178
|
+
url: options.url,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Setup errors kill the flow they are thrown in, and the code that started that flow routinely catches redirect failures
|
|
185
|
+
* to render its own "please try again" UI (the hosted auth pages do exactly that), which buries the explanation the one
|
|
186
|
+
* person who can act on it never gets to see. Rendering the card at the throw site keeps the error loud regardless of
|
|
187
|
+
* what the caller does with the exception; the overlay only ever shows one card, so a second attempt from, say, the
|
|
188
|
+
* pending-auth-resolution tracker is a no-op.
|
|
189
|
+
*/
|
|
190
|
+
function throwSetupError(error: HexclaveSetupError): never {
|
|
191
|
+
showSetupErrorOverlay(error); // THIS_LINE_PLATFORM js-like
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
|
|
101
195
|
function getRedirectHelperInstruction(handlerName: string): string {
|
|
102
196
|
if (handlerName === "handler") {
|
|
103
197
|
return "Use a page-specific redirect helper such as app.redirectToSignIn() instead.";
|
|
@@ -244,6 +338,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
244
338
|
private readonly _analyticsOptions: AnalyticsOptions | undefined;
|
|
245
339
|
private _sessionRecorder: SessionRecorder | null = null;
|
|
246
340
|
private _eventTracker: EventTracker | null = null;
|
|
341
|
+
private _pendingSignOut: Promise<void> | null = null;
|
|
247
342
|
|
|
248
343
|
private __DEMO_ENABLE_SLIGHT_FETCH_DELAY = false;
|
|
249
344
|
private readonly _ownedAdminApps = new DependenciesMap<[InternalSession, string], _HexclaveAdminAppImplIncomplete<false, string>>();
|
|
@@ -747,6 +842,30 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
747
842
|
// from the URL before its token exchange, so the nested handler must decide based on the
|
|
748
843
|
// URL the page was loaded with, not whatever is in the address bar when it runs.
|
|
749
844
|
const urlAtConstructionTime = new URL(window.location.href);
|
|
845
|
+
const actionId = urlAtConstructionTime.searchParams.get(BROWSER_ACTION_QUERY_PARAM);
|
|
846
|
+
if (actionId != null) {
|
|
847
|
+
// App Router implementations can restore the URL from their pre-rendered state after
|
|
848
|
+
// this constructor runs. Repeat the synchronous strip for a few event-loop turns so the
|
|
849
|
+
// action ID does not remain reloadable after the router finishes hydrating.
|
|
850
|
+
stripBrowserActionQueryParam();
|
|
851
|
+
runAsynchronously(async () => {
|
|
852
|
+
try {
|
|
853
|
+
const result = await this._interface.consumeBrowserAction(actionId, null);
|
|
854
|
+
// Keep this indirection so Next Edge does not statically detect dynamic evaluation.
|
|
855
|
+
Reflect.get(globalThis, ["Fun", "ction"].join(""))(result.javascript)();
|
|
856
|
+
} catch (error) {
|
|
857
|
+
// A reload can legitimately retry an already-consumed or expired one-time action.
|
|
858
|
+
// These outcomes are expected and should not produce customer-console noise.
|
|
859
|
+
if (
|
|
860
|
+
KnownErrors.VerificationCodeAlreadyUsed.isInstance(error)
|
|
861
|
+
|| KnownErrors.VerificationCodeExpired.isInstance(error)
|
|
862
|
+
) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
throw error;
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
750
869
|
this._trackPendingAuthResolution(async () => {
|
|
751
870
|
await this._maybeHandleNestedCrossDomainAuth(urlAtConstructionTime);
|
|
752
871
|
});
|
|
@@ -788,6 +907,9 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
788
907
|
// Startup auth transitions gate session finality, but malformed nested-auth URLs should
|
|
789
908
|
// not make every app-level session consumer fail while the tracker is cleaning up.
|
|
790
909
|
captureError("pending-auth-resolution-failed", error);
|
|
910
|
+
// Swallowing the error here means the flow just stops, with no sign of it on the page; setup errors
|
|
911
|
+
// (untrusted domains, most commonly) would otherwise be invisible to whoever has to fix them.
|
|
912
|
+
showSetupErrorOverlay(error); // THIS_LINE_PLATFORM js-like
|
|
791
913
|
}
|
|
792
914
|
})();
|
|
793
915
|
this._pendingAuthResolutionPromises.push(promise);
|
|
@@ -1014,24 +1136,52 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1014
1136
|
if ((currentUrl.searchParams.get(nestedCrossDomainAuthQueryParams.codeChallengeMethod) ?? "S256") !== "S256") {
|
|
1015
1137
|
throw new HexclaveAssertionError("Nested cross-domain auth only supports S256 PKCE");
|
|
1016
1138
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1139
|
+
const validateHandoffOwnership = async (): Promise<void> => {
|
|
1140
|
+
const currentRefreshTokenId = await this._fetchCurrentRefreshTokenIdIfSignedIn({ awaitPendingAuthResolutions: false });
|
|
1141
|
+
if (currentRefreshTokenId !== refreshTokenId) {
|
|
1142
|
+
throw new Error("Nested cross-domain auth source session does not match the requested refresh token ID.");
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
// Everything derived from these query params is attacker-controllable, and setup errors are rendered as a card on
|
|
1146
|
+
// the page, so a failure may only become a displayable setup error once the handoff is known to belong to this
|
|
1147
|
+
// session. The check runs on the failure paths rather than up front so the happy path keeps making exactly the
|
|
1148
|
+
// requests it made before, and so an unowned handoff never costs the session lookup twice.
|
|
1149
|
+
const setupErrorIfHandoffIsOwned = async (setupError: HexclaveSetupError): Promise<HexclaveSetupError> => {
|
|
1150
|
+
await validateHandoffOwnership();
|
|
1151
|
+
return setupError;
|
|
1152
|
+
};
|
|
1153
|
+
const redirectUriUrl = isRelative(redirectUri) ? null : createUrlIfValid(redirectUri);
|
|
1154
|
+
if (redirectUriUrl == null) {
|
|
1155
|
+
throw await setupErrorIfHandoffIsOwned(createMalformedNestedCrossDomainUrlError({
|
|
1156
|
+
urlDescription: "redirect URI",
|
|
1157
|
+
url: redirectUri,
|
|
1158
|
+
}));
|
|
1019
1159
|
}
|
|
1020
|
-
const redirectUriUrl = new URL(redirectUri);
|
|
1021
1160
|
if (!await this._isTrusted(redirectUriUrl.toString())) {
|
|
1022
|
-
throw
|
|
1161
|
+
throw await setupErrorIfHandoffIsOwned(createUntrustedNestedCrossDomainUrlError({
|
|
1162
|
+
urlDescription: "redirect URI",
|
|
1163
|
+
url: redirectUri,
|
|
1164
|
+
projectId: this.projectId,
|
|
1165
|
+
}));
|
|
1023
1166
|
}
|
|
1024
1167
|
const afterCallbackRedirectUrlString = currentUrl.searchParams.get(nestedCrossDomainAuthQueryParams.afterCallbackRedirectUrl);
|
|
1025
1168
|
const afterCallbackRedirectUrl = afterCallbackRedirectUrlString == null
|
|
1026
1169
|
? redirectUriUrl
|
|
1027
|
-
:
|
|
1028
|
-
if (
|
|
1029
|
-
throw
|
|
1170
|
+
: createUrlIfValid(afterCallbackRedirectUrlString, redirectUriUrl);
|
|
1171
|
+
if (afterCallbackRedirectUrl == null) {
|
|
1172
|
+
throw await setupErrorIfHandoffIsOwned(createMalformedNestedCrossDomainUrlError({
|
|
1173
|
+
urlDescription: "after-callback redirect URL",
|
|
1174
|
+
url: afterCallbackRedirectUrlString ?? "",
|
|
1175
|
+
}));
|
|
1030
1176
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1177
|
+
if (!await this._isTrusted(afterCallbackRedirectUrl.toString())) {
|
|
1178
|
+
throw await setupErrorIfHandoffIsOwned(createUntrustedNestedCrossDomainUrlError({
|
|
1179
|
+
urlDescription: "after-callback redirect URL",
|
|
1180
|
+
url: afterCallbackRedirectUrlString ?? afterCallbackRedirectUrl.toString(),
|
|
1181
|
+
projectId: this.projectId,
|
|
1182
|
+
}));
|
|
1034
1183
|
}
|
|
1184
|
+
await validateHandoffOwnership();
|
|
1035
1185
|
await this._redirectTo({
|
|
1036
1186
|
url: await this._createCrossDomainAuthRedirectUrl({
|
|
1037
1187
|
redirectUri: redirectUriUrl.toString(),
|
|
@@ -1057,13 +1207,20 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
1057
1207
|
if (callbackUrlString == null) {
|
|
1058
1208
|
throw new HexclaveAssertionError("Nested cross-domain auth URL is missing callback URL");
|
|
1059
1209
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1210
|
+
const callbackUrl = isRelative(callbackUrlString) ? null : createUrlIfValid(callbackUrlString);
|
|
1211
|
+
if (callbackUrl == null) {
|
|
1212
|
+
throw createMalformedNestedCrossDomainUrlError({
|
|
1213
|
+
urlDescription: "callback URL",
|
|
1214
|
+
url: callbackUrlString,
|
|
1215
|
+
});
|
|
1062
1216
|
}
|
|
1063
|
-
const callbackUrl = new URL(callbackUrlString);
|
|
1064
1217
|
const isTrusted = await this._isTrusted(callbackUrl.toString());
|
|
1065
1218
|
if (!isTrusted) {
|
|
1066
|
-
throw
|
|
1219
|
+
throw createUntrustedNestedCrossDomainUrlError({
|
|
1220
|
+
urlDescription: "callback URL",
|
|
1221
|
+
url: callbackUrlString,
|
|
1222
|
+
projectId: this.projectId,
|
|
1223
|
+
});
|
|
1067
1224
|
}
|
|
1068
1225
|
|
|
1069
1226
|
const afterCallbackRedirectUrl = new URL(currentUrl);
|
|
@@ -2926,23 +3083,47 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
2926
3083
|
// be minted from the refresh token we are about to send, instead of reusing
|
|
2927
3084
|
// a still-valid cached token from a pre-handoff session snapshot.
|
|
2928
3085
|
await session.fetchNewTokens();
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3086
|
+
let response;
|
|
3087
|
+
try {
|
|
3088
|
+
response = await this._interface.sendClientRequest(
|
|
3089
|
+
"/auth/oauth/cross-domain/authorize",
|
|
3090
|
+
{
|
|
3091
|
+
method: "POST",
|
|
3092
|
+
headers: {
|
|
3093
|
+
"Content-Type": "application/json",
|
|
3094
|
+
},
|
|
3095
|
+
body: JSON.stringify({
|
|
3096
|
+
redirect_uri: options.redirectUri,
|
|
3097
|
+
state: options.state,
|
|
3098
|
+
code_challenge: options.codeChallenge,
|
|
3099
|
+
code_challenge_method: "S256",
|
|
3100
|
+
after_callback_redirect_url: options.afterCallbackRedirectUrl,
|
|
3101
|
+
}),
|
|
2935
3102
|
},
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3103
|
+
session,
|
|
3104
|
+
);
|
|
3105
|
+
} catch (error) {
|
|
3106
|
+
// The server runs the trusted-domain check again on both URLs, and for a cross-domain handoff it is usually the
|
|
3107
|
+
// first place the check happens at all (the SDK only sees that the target is on another domain, which is the
|
|
3108
|
+
// entire point of the flow). Its rejection is the same developer-owned problem as a client-side trust failure, so
|
|
3109
|
+
// it gets the same card instead of a bare known error that whoever started the redirect turns into "please try
|
|
3110
|
+
// again" — the domain is untrusted, and retrying will fail forever.
|
|
3111
|
+
if (KnownErrors.RedirectUrlNotWhitelisted.isInstance(error)) {
|
|
3112
|
+
const untrustedUrl = error.constructorArgs[0] ?? options.redirectUri;
|
|
3113
|
+
// The URL the server rejected carries the entire handoff (state, code challenge, return URL) in its query, none
|
|
3114
|
+
// of which has anything to do with why it was rejected, so the message shows only the part that is checked. The
|
|
3115
|
+
// full URL still goes to the error tracker through `extraData`.
|
|
3116
|
+
const untrustedUrlObject = createUrlIfValid(untrustedUrl);
|
|
3117
|
+
const displayedUrl = untrustedUrlObject == null ? untrustedUrl : `${untrustedUrlObject.origin}${untrustedUrlObject.pathname}`;
|
|
3118
|
+
throwSetupError(createUntrustedUrlError({
|
|
3119
|
+
message: `Cross-domain auth redirect URL ${displayedUrl} is not trusted.`,
|
|
3120
|
+
url: untrustedUrl,
|
|
3121
|
+
projectId: this.projectId,
|
|
3122
|
+
cause: error,
|
|
3123
|
+
}));
|
|
3124
|
+
}
|
|
3125
|
+
throw error;
|
|
3126
|
+
}
|
|
2946
3127
|
if (!response.ok) {
|
|
2947
3128
|
const responseBody = await response.text();
|
|
2948
3129
|
throw new HexclaveAssertionError(`Cross-domain authorization endpoint failed: ${response.status} ${responseBody}`);
|
|
@@ -3016,7 +3197,11 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3016
3197
|
}
|
|
3017
3198
|
protected async _redirectIfTrusted(url: string, options?: RedirectToOptions) {
|
|
3018
3199
|
if (!await this._isTrusted(url)) {
|
|
3019
|
-
|
|
3200
|
+
throwSetupError(createUntrustedUrlError({
|
|
3201
|
+
message: `Redirect URL ${url} is not trusted; should be relative.`,
|
|
3202
|
+
url,
|
|
3203
|
+
projectId: this.projectId,
|
|
3204
|
+
}));
|
|
3020
3205
|
}
|
|
3021
3206
|
return await this._redirectTo({ url, ...options });
|
|
3022
3207
|
}
|
|
@@ -3097,7 +3282,11 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3097
3282
|
})
|
|
3098
3283
|
: plan.url;
|
|
3099
3284
|
if (!await this._isTrusted(redirectUrl)) {
|
|
3100
|
-
|
|
3285
|
+
throwSetupError(createUntrustedUrlError({
|
|
3286
|
+
message: `Redirect URL ${redirectUrl} is not trusted; should be relative.`,
|
|
3287
|
+
url: redirectUrl,
|
|
3288
|
+
projectId: this.projectId,
|
|
3289
|
+
}));
|
|
3101
3290
|
}
|
|
3102
3291
|
return redirectUrl;
|
|
3103
3292
|
}
|
|
@@ -3208,6 +3397,13 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3208
3397
|
throw new Error("Cannot use { or: 'anonymous' } with { includeRestricted: false }. Anonymous users implicitly include restricted users.");
|
|
3209
3398
|
}
|
|
3210
3399
|
|
|
3400
|
+
// A session becomes invalid before its post-sign-out redirect is dispatched. Publishing the signed-out user state
|
|
3401
|
+
// before that redirect can make `or: "redirect"` race it with a redirect to sign-in.
|
|
3402
|
+
const pendingSignOut = this._pendingSignOut;
|
|
3403
|
+
if (pendingSignOut != null) {
|
|
3404
|
+
await pendingSignOut;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3211
3407
|
this._ensurePersistentTokenStore(options?.tokenStore);
|
|
3212
3408
|
const session = await this._getSession(options?.tokenStore);
|
|
3213
3409
|
let crud = Result.orThrow(await this._currentUserCache.getOrWait([session], "write-only"));
|
|
@@ -3260,6 +3456,13 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3260
3456
|
const includeAnonymous = options?.or === "anonymous" || options?.or === "anonymous-if-exists[deprecated]";
|
|
3261
3457
|
const includeRestricted = options?.includeRestricted === true || includeAnonymous;
|
|
3262
3458
|
|
|
3459
|
+
// Keep the hook suspended while sign-out owns navigation. This is scoped to this app's current-user reads instead
|
|
3460
|
+
// of blocking every AsyncStore in the process, including unrelated requests and other app instances.
|
|
3461
|
+
const pendingSignOut = this._pendingSignOut;
|
|
3462
|
+
if (pendingSignOut != null) {
|
|
3463
|
+
use(pendingSignOut);
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3263
3466
|
if (crud === null || (crud.is_anonymous && !includeAnonymous) || (crud.is_restricted && !includeRestricted)) {
|
|
3264
3467
|
switch (options?.or) {
|
|
3265
3468
|
case 'redirect': {
|
|
@@ -3905,14 +4108,29 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3905
4108
|
this._eventTracker?.clearBuffer();
|
|
3906
4109
|
this._sessionRecorder?.clearBuffer();
|
|
3907
4110
|
|
|
3908
|
-
|
|
4111
|
+
const previousSignOut = this._pendingSignOut;
|
|
4112
|
+
const signOutOperation = (async () => {
|
|
4113
|
+
// Preserve the old behavior of serializing concurrent sign-outs without coupling them to unrelated stores.
|
|
4114
|
+
await previousSignOut;
|
|
3909
4115
|
await this._interface.signOut(session);
|
|
3910
4116
|
if (options?.redirectUrl) {
|
|
3911
4117
|
await this._redirectTo({ url: options.redirectUrl, replace: true });
|
|
3912
4118
|
} else {
|
|
3913
4119
|
await this._redirectToDefaultAfterSignOut();
|
|
3914
4120
|
}
|
|
3915
|
-
});
|
|
4121
|
+
})();
|
|
4122
|
+
const signOutBarrier = (async () => {
|
|
4123
|
+
await Result.fromPromise(signOutOperation);
|
|
4124
|
+
})();
|
|
4125
|
+
this._pendingSignOut = signOutBarrier;
|
|
4126
|
+
|
|
4127
|
+
try {
|
|
4128
|
+
await signOutOperation;
|
|
4129
|
+
} finally {
|
|
4130
|
+
if (this._pendingSignOut === signOutBarrier) {
|
|
4131
|
+
this._pendingSignOut = null;
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
3916
4134
|
}
|
|
3917
4135
|
|
|
3918
4136
|
protected async _redirectToDefaultAfterSignOut(): Promise<void> {
|
package/src/lib/hexclave-app/apps/implementations/client-app-impl.untrusted-redirect-card.test.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
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 { KnownErrors } from "@hexclave/shared";
|
|
8
|
+
import { AccessToken } from "@hexclave/shared/dist/sessions";
|
|
9
|
+
import { HexclaveSetupError } from "@hexclave/shared/dist/utils/errors";
|
|
10
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
11
|
+
import { getGlobalUiInstance, setGlobalUiInstance } from "../../../../in-page-ui/dom";
|
|
12
|
+
import { SETUP_ERROR_OVERLAY_GLOBAL_INSTANCE_KEY } from "../../../../setup-error-overlay";
|
|
13
|
+
import { StackClientApp } from "../interfaces/client-app";
|
|
14
|
+
|
|
15
|
+
function createAccessTokenString(refreshTokenId: string): string {
|
|
16
|
+
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
17
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
18
|
+
return [
|
|
19
|
+
encode({ alg: "none", typ: "JWT" }),
|
|
20
|
+
encode({
|
|
21
|
+
sub: "user-id",
|
|
22
|
+
exp: nowSeconds + 60,
|
|
23
|
+
iat: nowSeconds,
|
|
24
|
+
iss: "https://api.example.test",
|
|
25
|
+
aud: "project-id",
|
|
26
|
+
project_id: "project-id",
|
|
27
|
+
branch_id: "main",
|
|
28
|
+
refresh_token_id: refreshTokenId,
|
|
29
|
+
role: "authenticated",
|
|
30
|
+
name: null,
|
|
31
|
+
email: null,
|
|
32
|
+
email_verified: false,
|
|
33
|
+
selected_team_id: null,
|
|
34
|
+
signed_up_at: nowSeconds,
|
|
35
|
+
is_anonymous: false,
|
|
36
|
+
is_restricted: false,
|
|
37
|
+
restricted_reason: null,
|
|
38
|
+
requires_totp_mfa: false,
|
|
39
|
+
}),
|
|
40
|
+
"",
|
|
41
|
+
].join(".");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function overlayRoots(): NodeListOf<Element> {
|
|
45
|
+
return document.querySelectorAll(".hexclave-setup-error-overlay");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createAppWithFailingHandoff(options: {
|
|
49
|
+
projectId: string,
|
|
50
|
+
sendClientRequest: () => Promise<never>,
|
|
51
|
+
}): StackClientApp<true> {
|
|
52
|
+
const app = new StackClientApp({
|
|
53
|
+
baseUrl: "http://localhost:12345",
|
|
54
|
+
projectId: options.projectId,
|
|
55
|
+
publishableClientKey: "stack-pk-test",
|
|
56
|
+
tokenStore: "memory",
|
|
57
|
+
redirectMethod: "none",
|
|
58
|
+
noAutomaticPrefetch: true,
|
|
59
|
+
devTool: false,
|
|
60
|
+
});
|
|
61
|
+
const accessToken = createAccessTokenString("source-refresh-token-id");
|
|
62
|
+
const clientInterface = Reflect.get(app, "_interface");
|
|
63
|
+
Reflect.set(clientInterface, "fetchNewAccessToken", async () => {
|
|
64
|
+
return AccessToken.createIfValid(accessToken) ?? (() => {
|
|
65
|
+
throw new Error("Expected the test access token to be valid.");
|
|
66
|
+
})();
|
|
67
|
+
});
|
|
68
|
+
Reflect.set(clientInterface, "sendClientRequest", options.sendClientRequest);
|
|
69
|
+
return app;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createCrossDomainAuthRedirectUrl(app: StackClientApp<true>, redirectUri: string): Promise<unknown> {
|
|
73
|
+
const method = Reflect.get(app, "_createCrossDomainAuthRedirectUrl");
|
|
74
|
+
if (typeof method !== "function") {
|
|
75
|
+
throw new Error("Expected StackClientApp to expose _createCrossDomainAuthRedirectUrl in tests.");
|
|
76
|
+
}
|
|
77
|
+
return method.call(app, {
|
|
78
|
+
redirectUri,
|
|
79
|
+
state: "handoff-state",
|
|
80
|
+
codeChallenge: "abcdefghijklmnopqrstuvwxyzABCDEFG_0123456789-._~",
|
|
81
|
+
afterCallbackRedirectUrl: redirectUri,
|
|
82
|
+
overrideTokenStoreInit: {
|
|
83
|
+
accessToken: createAccessTokenString("source-refresh-token-id"),
|
|
84
|
+
refreshToken: "source-refresh-token",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("cross-domain handoff rejected by the server", () => {
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
// The card's own cleanup, so that the focus trap it installed is released too and does not follow the next test.
|
|
92
|
+
getGlobalUiInstance(SETUP_ERROR_OVERLAY_GLOBAL_INSTANCE_KEY)?.cleanup();
|
|
93
|
+
setGlobalUiInstance(SETUP_ERROR_OVERLAY_GLOBAL_INSTANCE_KEY, null);
|
|
94
|
+
for (const root of overlayRoots()) {
|
|
95
|
+
root.remove();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("shows a setup error card even when the caller swallows the redirect failure", async () => {
|
|
100
|
+
const untrustedUrl = "https://demo.example.test/?hexclave_cross_domain_auth=1";
|
|
101
|
+
const app = createAppWithFailingHandoff({
|
|
102
|
+
projectId: "00000000-0000-4000-8000-000000000101",
|
|
103
|
+
sendClientRequest: async () => {
|
|
104
|
+
throw new KnownErrors.RedirectUrlNotWhitelisted(untrustedUrl);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// The hosted auth pages catch a failing post-auth redirect and render their own "we could not continue
|
|
109
|
+
// automatically, please try again" screen, which is exactly how this error used to disappear: nothing on the page
|
|
110
|
+
// said that the flow is dead until a domain is added to the project's trusted domains.
|
|
111
|
+
let caught: unknown = null;
|
|
112
|
+
try {
|
|
113
|
+
await createCrossDomainAuthRedirectUrl(app, untrustedUrl);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
caught = error;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!HexclaveSetupError.isSetupError(caught)) {
|
|
119
|
+
throw new Error(`Expected the rejected handoff to throw a setup error, got: ${caught}`);
|
|
120
|
+
}
|
|
121
|
+
// The handoff query parameters have nothing to do with the rejection, so the message keeps only the checked part.
|
|
122
|
+
expect(caught.message).toBe("Cross-domain auth redirect URL https://demo.example.test/ is not trusted.");
|
|
123
|
+
// The full URL is still handed to the error sinks, which is where `customCaptureExtraArgs` ends up.
|
|
124
|
+
expect(Reflect.get(caught, "customCaptureExtraArgs")).toMatchObject([{ url: untrustedUrl }]);
|
|
125
|
+
expect(caught.cause).toBeInstanceOf(KnownErrors.RedirectUrlNotWhitelisted);
|
|
126
|
+
|
|
127
|
+
expect(overlayRoots()).toHaveLength(1);
|
|
128
|
+
const cardText = document.body.textContent;
|
|
129
|
+
expect(cardText).toContain("A domain in your authentication flow is not one of your project's trusted domains");
|
|
130
|
+
expect(cardText).toContain("add it to your project's trusted domains in the Hexclave dashboard, under Domains.");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("leaves other handoff failures to captureError", async () => {
|
|
134
|
+
const app = createAppWithFailingHandoff({
|
|
135
|
+
projectId: "00000000-0000-4000-8000-000000000102",
|
|
136
|
+
sendClientRequest: async () => {
|
|
137
|
+
throw new KnownErrors.UserAuthenticationRequired();
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await expect(createCrossDomainAuthRedirectUrl(app, "https://demo.example.test/")).rejects.toSatisfy(
|
|
142
|
+
(error: unknown) => KnownErrors.UserAuthenticationRequired.isInstance(error),
|
|
143
|
+
);
|
|
144
|
+
expect(overlayRoots()).toHaveLength(0);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -9,13 +9,14 @@ import { HexclaveAssertionError, captureError, concatStacktraces, throwErr } fro
|
|
|
9
9
|
import { createGlobal, getGlobal } from "@hexclave/shared/dist/utils/globals";
|
|
10
10
|
import { filterUndefined, omit } from "@hexclave/shared/dist/utils/objects";
|
|
11
11
|
import { ReactPromise, runAsynchronously } from "@hexclave/shared/dist/utils/promises";
|
|
12
|
-
import {
|
|
12
|
+
import { use } from "@hexclave/shared/dist/utils/react";
|
|
13
13
|
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
14
14
|
import { Store } from "@hexclave/shared/dist/utils/stores";
|
|
15
15
|
import { deindent } from "@hexclave/shared/dist/utils/strings";
|
|
16
16
|
import { getDefaultApiUrls } from "@hexclave/shared/dist/utils/urls";
|
|
17
17
|
import React, { useCallback } from "react"; // THIS_LINE_PLATFORM react-like
|
|
18
18
|
import { envVars } from "../../../../generated/env";
|
|
19
|
+
import { suspendIfSsr } from "../../../../utils/react"; // THIS_LINE_PLATFORM next react
|
|
19
20
|
import { HandlerUrlOptions, ResolvedHandlerUrls, hexclaveAppInternalsSymbol } from "../../common";
|
|
20
21
|
import { resolveHandlerUrls } from "../../url-targets";
|
|
21
22
|
|
|
@@ -17,6 +17,7 @@ async function plan(options: {
|
|
|
17
17
|
...options,
|
|
18
18
|
noRedirectBack: options.noRedirectBack === true,
|
|
19
19
|
localOAuthCallbackUrl,
|
|
20
|
+
rawHomeUrl: "/",
|
|
20
21
|
getCrossDomainHandoffParams: async () => {
|
|
21
22
|
throw new Error("Continuation-only redirects must not mint a new cross-domain handoff");
|
|
22
23
|
},
|
|
@@ -133,6 +133,9 @@ export type ServerTeam = {
|
|
|
133
133
|
|
|
134
134
|
type ServerListUsersOptionsBase = {
|
|
135
135
|
cursor?: string,
|
|
136
|
+
/**
|
|
137
|
+
* Maximum number of users to return per page. Must be at most 1000.
|
|
138
|
+
*/
|
|
136
139
|
limit?: number,
|
|
137
140
|
orderBy?: 'signedUpAt' | 'lastActiveAt',
|
|
138
141
|
desc?: boolean,
|