@hexclave/next 1.0.73 → 1.0.74
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-D867jXHk.d.ts → admin-app-impl-FZdANO62.d.ts} +10 -4
- package/dist/admin-app-impl-FZdANO62.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-B931IBrx.d.ts → client-app-BCT5p_70.d.ts} +11 -3
- package/dist/{client-app-B931IBrx.d.ts.map → client-app-BCT5p_70.d.ts.map} +1 -1
- package/dist/components/elements/sidebar-layout.js +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/oauth-callback.js +3 -18
- package/dist/components-page/oauth-callback.js.map +1 -1
- package/dist/dev-tool/dev-tool-core.d.ts +1 -1
- package/dist/dev-tool/index.d.ts +1 -1
- package/dist/esm/components/elements/sidebar-layout.js +1 -1
- package/dist/esm/components-page/oauth-callback.d.ts.map +1 -1
- package/dist/esm/components-page/oauth-callback.js +4 -19
- package/dist/esm/components-page/oauth-callback.js.map +1 -1
- package/dist/esm/lib/auth.d.ts.map +1 -1
- package/dist/esm/lib/auth.js +1 -1
- package/dist/esm/lib/auth.js.map +1 -1
- package/dist/esm/lib/auth.test.js +2 -2
- package/dist/esm/lib/auth.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +3 -0
- 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.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +102 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +57 -1
- 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 -2
- 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 +44 -12
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts +13 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.js +15 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.js +25 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.js +67 -12
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.d.ts +1 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +87 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -0
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +9 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
- package/dist/esm/utils/url.js +10 -2
- package/dist/esm/utils/url.js.map +1 -1
- package/dist/esm/utils/url.test.d.ts +1 -0
- package/dist/esm/utils/url.test.js +28 -0
- package/dist/esm/utils/url.test.js.map +1 -0
- package/dist/{hexclave-provider-DkFUT3Fa.d.ts → hexclave-provider-BKxi3Vy-.d.ts} +2 -2
- package/dist/{hexclave-provider-DkFUT3Fa.d.ts.map → hexclave-provider-BKxi3Vy-.d.ts.map} +1 -1
- package/dist/{hooks-4PMrTtJr.d.ts → hooks-CUe5bc3s.d.ts} +2 -2
- package/dist/{hooks-4PMrTtJr.d.ts.map → hooks-CUe5bc3s.d.ts.map} +1 -1
- package/dist/{index-C9bAeWRr.d.ts → index-CvB7XacB.d.ts} +2 -2
- package/dist/{index-C9bAeWRr.d.ts.map → index-CvB7XacB.d.ts.map} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +1 -1
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/auth.test.js +2 -2
- package/dist/lib/auth.test.js.map +1 -1
- 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.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +101 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +57 -1
- 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 +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +43 -11
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
- package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts +13 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.js +15 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.js +24 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.js +67 -12
- package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.d.ts +1 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +86 -0
- package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -0
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +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/client-app.js.map +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/users/index.d.ts +1 -1
- package/dist/lib/hooks.d.ts +1 -1
- package/dist/oauth-callback-tCVTRbF2.d.ts.map +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/pushed-config-error-overlay/index.d.ts +1 -1
- package/dist/{redirect-page-urls-NUHUchgR.d.ts → redirect-page-urls-BEWG0AtP.d.ts} +2 -1
- package/dist/redirect-page-urls-BEWG0AtP.d.ts.map +1 -0
- package/dist/utils/url.js +10 -2
- package/dist/utils/url.js.map +1 -1
- package/dist/utils/url.test.d.ts +1 -0
- package/dist/utils/url.test.js +27 -0
- package/dist/utils/url.test.js.map +1 -0
- package/package.json +4 -4
- package/src/components-page/oauth-callback.tsx +0 -16
- package/src/lib/auth.test.ts +2 -2
- package/src/lib/auth.ts +3 -1
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.ts +126 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +75 -0
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +73 -16
- package/src/lib/hexclave-app/apps/implementations/redirect-back-state.test.ts +29 -1
- package/src/lib/hexclave-app/apps/implementations/redirect-back-state.ts +17 -0
- package/src/lib/hexclave-app/apps/implementations/redirect-page-urls.test.ts +119 -0
- package/src/lib/hexclave-app/apps/implementations/redirect-page-urls.ts +104 -12
- package/src/lib/hexclave-app/apps/interfaces/client-app.ts +12 -2
- package/src/utils/url.test.ts +54 -0
- package/src/utils/url.ts +12 -3
- package/dist/admin-app-impl-D867jXHk.d.ts.map +0 -1
- package/dist/redirect-page-urls-NUHUchgR.d.ts.map +0 -1
|
@@ -59,7 +59,7 @@ import { TeamPermission } from "../../permissions";
|
|
|
59
59
|
import { AdminOwnedProject, AdminProjectUpdateOptions, Project, adminProjectCreateOptionsToCrud } from "../../projects";
|
|
60
60
|
import { EditableTeamMemberProfile, ReceivedTeamInvitation, SentTeamInvitation, Team, TeamCreateOptions, TeamUpdateOptions, TeamUser, teamCreateOptionsToCrud, teamUpdateOptionsToCrud } from "../../teams";
|
|
61
61
|
import { buildCliAuthConfirmUrl, getHostedHandlerUrl, isHostedHandlerUrlForProject, resolveHandlerUrls } from "../../url-targets";
|
|
62
|
-
import { augmentUrlWithPersistedRedirectBackState, saveRedirectBackStateFromUrl } from "./redirect-back-state";
|
|
62
|
+
import { augmentUrlWithPersistedRedirectBackState, getRawAfterAuthReturnTo, saveRedirectBackStateFromUrl } from "./redirect-back-state";
|
|
63
63
|
import { recordRedirectAndThrowIfLoopDetected } from "./redirect-loop-breaker";
|
|
64
64
|
import { scopePasskeyAuthenticationToHostname, scopePasskeyRegistrationToHostname } from "../../passkey-rp-id";
|
|
65
65
|
import { ActiveSession, Auth, BaseUser, CurrentUser, InternalUserExtra, OAuthProvider, ProjectCurrentUser, SyncedPartialUser, TokenPartialUser, UserExtra, UserUpdateOptions, userUpdateOptionsToCrud, withUserDestructureGuard } from "../../users";
|
|
@@ -86,6 +86,7 @@ isReactServer = sc.isReactServer;
|
|
|
86
86
|
const NextNavigation = scrambleDuringCompileTime(NextNavigationUnscrambled);
|
|
87
87
|
|
|
88
88
|
const prefetchedCrossDomainHandoffTtlMs = 55 * 60 * 1000;
|
|
89
|
+
const oauthAfterCallbackRedirectUrlQueryParam = "after_callback_redirect_url";
|
|
89
90
|
|
|
90
91
|
const nestedCrossDomainAuthQueryParams = {
|
|
91
92
|
refreshTokenId: "stack_nested_cross_domain_auth_refresh_token_id",
|
|
@@ -94,7 +95,7 @@ const nestedCrossDomainAuthQueryParams = {
|
|
|
94
95
|
state: "state",
|
|
95
96
|
codeChallenge: "code_challenge",
|
|
96
97
|
codeChallengeMethod: "code_challenge_method",
|
|
97
|
-
afterCallbackRedirectUrl:
|
|
98
|
+
afterCallbackRedirectUrl: oauthAfterCallbackRedirectUrlQueryParam,
|
|
98
99
|
} as const;
|
|
99
100
|
|
|
100
101
|
function getRedirectHelperInstruction(handlerName: string): string {
|
|
@@ -670,18 +671,32 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
670
671
|
this._redirectMethod = resolvedOptions.redirectMethod || "nextjs"; // THIS_LINE_PLATFORM next
|
|
671
672
|
this._urlOptions = resolvedOptions.urls ?? {};
|
|
672
673
|
this._oauthScopesOnSignIn = resolvedOptions.oauthScopesOnSignIn ?? {};
|
|
674
|
+
this._analyticsOptions = resolvedOptions.analytics;
|
|
675
|
+
|
|
676
|
+
if (extraOptions?.uniqueIdentifier !== undefined) {
|
|
677
|
+
this._uniqueIdentifier = extraOptions.uniqueIdentifier;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Custom dashboards can disable automatic initialization so URL parameters, browser storage, analytics, and
|
|
681
|
+
// development overlays cannot affect the containing page. Explicit SDK calls remain functional and retain their
|
|
682
|
+
// documented side effects.
|
|
683
|
+
if (resolvedOptions.automaticSideEffects === false) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
this._initializeAutomaticSideEffects(resolvedOptions);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private _initializeAutomaticSideEffects(resolvedOptions: HexclaveClientAppImplConstructorOptionsResolved<HasTokenStore, ProjectId>) {
|
|
673
691
|
if (isBrowserLike() && (resolvedOptions.tokenStore === "cookie" || resolvedOptions.tokenStore === "nextjs-cookie")) {
|
|
674
692
|
runAsynchronously(this._trustedParentDomainCache.getOrWait([window.location.hostname], "write-only"));
|
|
675
693
|
this._ensureCrossSubdomainCookieExists();
|
|
676
694
|
}
|
|
677
695
|
|
|
678
|
-
if (
|
|
679
|
-
this._uniqueIdentifier = extraOptions.uniqueIdentifier;
|
|
696
|
+
if (this._uniqueIdentifier !== undefined) {
|
|
680
697
|
this._initUniqueIdentifier();
|
|
681
698
|
}
|
|
682
699
|
|
|
683
|
-
this._analyticsOptions = resolvedOptions.analytics;
|
|
684
|
-
|
|
685
700
|
const getAnalyticsSession = async (): Promise<InternalSession> => {
|
|
686
701
|
this._ensurePersistentTokenStore();
|
|
687
702
|
const partialUser = await this.getPartialUser({ from: 'token', or: 'anonymous-if-exists' });
|
|
@@ -887,9 +902,31 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
887
902
|
|
|
888
903
|
protected async _redirectToOAuthCallbackError(error: KnownError): Promise<void> {
|
|
889
904
|
const errorUrl = new URL(this._getUrls().error, window.location.href);
|
|
905
|
+
const callbackUrl = augmentUrlWithPersistedRedirectBackState({
|
|
906
|
+
currentUrl: new URL(window.location.href),
|
|
907
|
+
projectId: this.projectId,
|
|
908
|
+
});
|
|
890
909
|
errorUrl.searchParams.set("errorCode", error.errorCode);
|
|
891
910
|
errorUrl.searchParams.set("message", error.message);
|
|
892
911
|
errorUrl.searchParams.set("details", JSON.stringify(error.details ?? {}));
|
|
912
|
+
for (const paramName of [
|
|
913
|
+
"after_auth_return_to",
|
|
914
|
+
crossDomainAuthQueryParams.state,
|
|
915
|
+
crossDomainAuthQueryParams.codeChallenge,
|
|
916
|
+
crossDomainAuthQueryParams.afterCallbackRedirectUrl,
|
|
917
|
+
]) {
|
|
918
|
+
const value = callbackUrl.searchParams.get(paramName);
|
|
919
|
+
if (value != null) {
|
|
920
|
+
errorUrl.searchParams.set(paramName, value);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const afterCallbackRedirectUrl = callbackUrl.searchParams.get(oauthAfterCallbackRedirectUrlQueryParam);
|
|
924
|
+
if (afterCallbackRedirectUrl != null) {
|
|
925
|
+
// Account-link callbacks need to finish consuming the OAuth response before returning to
|
|
926
|
+
// their invoking page. Carry the already server-validated continuation through the
|
|
927
|
+
// configured error handler, which validates it again before navigation.
|
|
928
|
+
errorUrl.searchParams.set(oauthAfterCallbackRedirectUrlQueryParam, afterCallbackRedirectUrl);
|
|
929
|
+
}
|
|
893
930
|
await this._redirectIfTrusted(errorUrl.toString(), { replace: true });
|
|
894
931
|
}
|
|
895
932
|
|
|
@@ -2673,6 +2710,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
2673
2710
|
tokenStore: null,
|
|
2674
2711
|
projectOwnerSession: session,
|
|
2675
2712
|
noAutomaticPrefetch: true,
|
|
2713
|
+
automaticSideEffects: false,
|
|
2676
2714
|
}));
|
|
2677
2715
|
}
|
|
2678
2716
|
return this._ownedAdminApps.get([session, forProjectId])!;
|
|
@@ -3014,16 +3052,18 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3014
3052
|
let currentUrl = isReactServer || typeof window === "undefined"
|
|
3015
3053
|
? null
|
|
3016
3054
|
: new URL(window.location.href);
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3055
|
+
const shouldRestorePersistedRedirectBackState = (
|
|
3056
|
+
(options?.noRedirectBack !== true && (handlerName === "afterSignIn" || handlerName === "afterSignUp"))
|
|
3057
|
+
|| handlerName === "forgotPassword"
|
|
3058
|
+
|| handlerName === "passwordReset"
|
|
3059
|
+
|| (handlerName === "signIn" && options?.noRedirectBack === true)
|
|
3060
|
+
);
|
|
3061
|
+
if (currentUrl != null && shouldRestorePersistedRedirectBackState) {
|
|
3022
3062
|
// If intermediate hops dropped the redirect-back query params, restore them from the
|
|
3023
|
-
// sessionStorage mirror
|
|
3024
|
-
//
|
|
3025
|
-
//
|
|
3026
|
-
//
|
|
3063
|
+
// sessionStorage mirror. Password-flow continuation pages only carry this inherited state;
|
|
3064
|
+
// redirect planning never turns forgot/reset pages themselves into return targets. The
|
|
3065
|
+
// restored URL still goes through the same _isTrusted / cross-domain-authorize validation
|
|
3066
|
+
// below as one read from query params.
|
|
3027
3067
|
currentUrl = augmentUrlWithPersistedRedirectBackState({ currentUrl, projectId: this.projectId });
|
|
3028
3068
|
}
|
|
3029
3069
|
const plan = await planRedirectToHandler({
|
|
@@ -3032,6 +3072,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
3032
3072
|
noRedirectBack: options?.noRedirectBack === true,
|
|
3033
3073
|
currentUrl,
|
|
3034
3074
|
localOAuthCallbackUrl: this._getLocalOAuthCallbackHandlerUrl(),
|
|
3075
|
+
rawHomeUrl: rawUrls.home,
|
|
3035
3076
|
getCrossDomainHandoffParams: async (href) => await this._getCrossDomainHandoffParamsForRedirect(href),
|
|
3036
3077
|
});
|
|
3037
3078
|
|
|
@@ -4048,13 +4089,19 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
4048
4089
|
}
|
|
4049
4090
|
|
|
4050
4091
|
const { analytics, ...restJson } = omit(json, ["uniqueIdentifier"]);
|
|
4051
|
-
|
|
4092
|
+
const app = new _HexclaveClientAppImplIncomplete<HasTokenStore, ProjectId>({
|
|
4052
4093
|
...restJson as any,
|
|
4053
4094
|
analytics: analyticsOptionsFromJson(analytics),
|
|
4054
4095
|
}, {
|
|
4055
4096
|
uniqueIdentifier: json.uniqueIdentifier,
|
|
4056
4097
|
checkString: providedCheckString,
|
|
4057
4098
|
});
|
|
4099
|
+
// Inert construction does not register itself, so register only after it has completed to preserve stable
|
|
4100
|
+
// identity across repeated client-side deserialization.
|
|
4101
|
+
if (json.automaticSideEffects === false) {
|
|
4102
|
+
app._initUniqueIdentifier();
|
|
4103
|
+
}
|
|
4104
|
+
return app;
|
|
4058
4105
|
}
|
|
4059
4106
|
};
|
|
4060
4107
|
}
|
|
@@ -4081,6 +4128,7 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
4081
4128
|
redirectMethod: this._redirectMethod,
|
|
4082
4129
|
extraRequestHeaders: this._options.extraRequestHeaders,
|
|
4083
4130
|
devTool: this._options.devTool,
|
|
4131
|
+
automaticSideEffects: this._options.automaticSideEffects,
|
|
4084
4132
|
analytics: analyticsOptionsToJson(this._analyticsOptions),
|
|
4085
4133
|
};
|
|
4086
4134
|
},
|
|
@@ -4117,6 +4165,15 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
|
|
|
4117
4165
|
redirectToHandler: async (handlerName: keyof HandlerUrls, options?: RedirectToOptions) => {
|
|
4118
4166
|
await this._redirectToHandler(handlerName, options);
|
|
4119
4167
|
},
|
|
4168
|
+
getRawAfterAuthReturnTo: () => {
|
|
4169
|
+
if (isReactServer || typeof window === "undefined") {
|
|
4170
|
+
return null;
|
|
4171
|
+
}
|
|
4172
|
+
return getRawAfterAuthReturnTo({
|
|
4173
|
+
currentUrl: new URL(window.location.href),
|
|
4174
|
+
projectId: this.projectId,
|
|
4175
|
+
});
|
|
4176
|
+
},
|
|
4120
4177
|
refreshOwnedProjects: async () => {
|
|
4121
4178
|
await this._refreshOwnedProjects(await this._getSession());
|
|
4122
4179
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
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
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import { augmentUrlWithPersistedRedirectBackState, readRedirectBackState, saveRedirectBackStateFromUrl } from "./redirect-back-state";
|
|
6
|
+
import { augmentUrlWithPersistedRedirectBackState, getRawAfterAuthReturnTo, readRedirectBackState, saveRedirectBackStateFromUrl } from "./redirect-back-state";
|
|
7
7
|
|
|
8
8
|
function createMockSessionStorage() {
|
|
9
9
|
const map = new Map<string, string>();
|
|
@@ -114,6 +114,34 @@ describe("redirect-back state mirror", () => {
|
|
|
114
114
|
expect(unchanged.toString()).toBe(explicitUrl.toString());
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it("reads the explicit return target before persisted state", () => {
|
|
118
|
+
saveRedirectBackStateFromUrl({
|
|
119
|
+
url: new URL("https://hosted.example.test/handler/sign-in?after_auth_return_to=/persisted"),
|
|
120
|
+
projectId,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(getRawAfterAuthReturnTo({
|
|
124
|
+
currentUrl: new URL("https://hosted.example.test/handler/mfa?after_auth_return_to=/explicit"),
|
|
125
|
+
projectId,
|
|
126
|
+
})).toBe("/explicit");
|
|
127
|
+
expect(getRawAfterAuthReturnTo({
|
|
128
|
+
currentUrl: new URL("https://hosted.example.test/handler/mfa"),
|
|
129
|
+
projectId,
|
|
130
|
+
})).toBe("/persisted");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("treats an explicitly empty return target as missing instead of reviving persisted state", () => {
|
|
134
|
+
saveRedirectBackStateFromUrl({
|
|
135
|
+
url: new URL("https://hosted.example.test/handler/sign-in?after_auth_return_to=/persisted"),
|
|
136
|
+
projectId,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(getRawAfterAuthReturnTo({
|
|
140
|
+
currentUrl: new URL("https://hosted.example.test/handler/mfa?after_auth_return_to=%20%20"),
|
|
141
|
+
projectId,
|
|
142
|
+
})).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
117
145
|
it("leaves the URL unchanged when there is no mirrored state", () => {
|
|
118
146
|
const url = new URL("https://hosted.example.test/handler/sign-in");
|
|
119
147
|
const augmented = augmentUrlWithPersistedRedirectBackState({ currentUrl: url, projectId });
|
|
@@ -111,6 +111,23 @@ export function readRedirectBackState(options: { projectId: string }): Persisted
|
|
|
111
111
|
return parsed;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Returns the raw return target carried by the current auth flow. The value is intentionally not
|
|
116
|
+
* described as trusted: callers must still pass it through the normal redirect validation before
|
|
117
|
+
* navigating to it.
|
|
118
|
+
*
|
|
119
|
+
* An explicit query parameter wins even when it is empty. This prevents a malformed URL from
|
|
120
|
+
* silently reviving an older flow from sessionStorage.
|
|
121
|
+
*/
|
|
122
|
+
export function getRawAfterAuthReturnTo(options: { currentUrl: URL, projectId: string }): string | null {
|
|
123
|
+
const explicitReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
124
|
+
if (explicitReturnTo != null) {
|
|
125
|
+
return explicitReturnTo.trim().length > 0 ? explicitReturnTo : null;
|
|
126
|
+
}
|
|
127
|
+
const persistedReturnTo = readRedirectBackState({ projectId: options.projectId })?.afterAuthReturnTo;
|
|
128
|
+
return persistedReturnTo != null && persistedReturnTo.trim().length > 0 ? persistedReturnTo : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
114
131
|
/**
|
|
115
132
|
* Returns a copy of `currentUrl` with any redirect-back query params that were dropped along the
|
|
116
133
|
* way restored from the sessionStorage mirror. If `currentUrl` already carries
|
|
@@ -0,0 +1,119 @@
|
|
|
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 { describe, expect, it } from "vitest";
|
|
6
|
+
import { crossDomainAuthQueryParams, planRedirectToHandler } from "./redirect-page-urls";
|
|
7
|
+
|
|
8
|
+
const localOAuthCallbackUrl = "https://hosted.example.test/handler/oauth-callback";
|
|
9
|
+
|
|
10
|
+
async function plan(options: {
|
|
11
|
+
handlerName: "signIn" | "forgotPassword" | "passwordReset" | "afterSignIn" | "afterSignUp",
|
|
12
|
+
rawHandlerUrl: string,
|
|
13
|
+
currentUrl: URL,
|
|
14
|
+
noRedirectBack?: boolean,
|
|
15
|
+
}) {
|
|
16
|
+
return await planRedirectToHandler({
|
|
17
|
+
...options,
|
|
18
|
+
noRedirectBack: options.noRedirectBack === true,
|
|
19
|
+
localOAuthCallbackUrl,
|
|
20
|
+
getCrossDomainHandoffParams: async () => {
|
|
21
|
+
throw new Error("Continuation-only redirects must not mint a new cross-domain handoff");
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function expectRedirectUrl(planResult: Awaited<ReturnType<typeof plan>>, baseUrl: URL): URL {
|
|
27
|
+
if (planResult.type !== "redirect") {
|
|
28
|
+
throw new Error("Expected a direct redirect plan");
|
|
29
|
+
}
|
|
30
|
+
return new URL(planResult.url, baseUrl);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function expectContinuation(url: URL, expectedReturnTo: string): void {
|
|
34
|
+
expect(url.searchParams.get("after_auth_return_to")).toBe(expectedReturnTo);
|
|
35
|
+
expect(url.searchParams.get(crossDomainAuthQueryParams.state)).toBe("outer-state");
|
|
36
|
+
expect(url.searchParams.get(crossDomainAuthQueryParams.codeChallenge)).toBe("outer-challenge");
|
|
37
|
+
expect(url.searchParams.get(crossDomainAuthQueryParams.afterCallbackRedirectUrl)).toBe("https://customer.example.test/dashboard");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("password-flow redirect continuation", () => {
|
|
41
|
+
it("carries the complete customer return state through forgot-password and a reset-email new tab", async () => {
|
|
42
|
+
const customerReturnTo = "https://customer.example.test/handler/oauth-callback?hexclave_cross_domain_auth=1";
|
|
43
|
+
const signInUrl = new URL("https://hosted.example.test/handler/sign-in");
|
|
44
|
+
signInUrl.searchParams.set("after_auth_return_to", customerReturnTo);
|
|
45
|
+
signInUrl.searchParams.set(crossDomainAuthQueryParams.state, "outer-state");
|
|
46
|
+
signInUrl.searchParams.set(crossDomainAuthQueryParams.codeChallenge, "outer-challenge");
|
|
47
|
+
signInUrl.searchParams.set(crossDomainAuthQueryParams.afterCallbackRedirectUrl, "https://customer.example.test/dashboard");
|
|
48
|
+
|
|
49
|
+
const forgotPlan = await plan({
|
|
50
|
+
handlerName: "forgotPassword",
|
|
51
|
+
rawHandlerUrl: "/handler/forgot-password",
|
|
52
|
+
currentUrl: signInUrl,
|
|
53
|
+
});
|
|
54
|
+
const forgotUrl = expectRedirectUrl(forgotPlan, signInUrl);
|
|
55
|
+
expectContinuation(forgotUrl, customerReturnTo);
|
|
56
|
+
|
|
57
|
+
const callbackPlan = await plan({
|
|
58
|
+
handlerName: "passwordReset",
|
|
59
|
+
rawHandlerUrl: "/handler/password-reset",
|
|
60
|
+
currentUrl: forgotUrl,
|
|
61
|
+
});
|
|
62
|
+
const resetEmailCallbackUrl = expectRedirectUrl(callbackPlan, forgotUrl);
|
|
63
|
+
expectContinuation(resetEmailCallbackUrl, customerReturnTo);
|
|
64
|
+
|
|
65
|
+
// Model opening the callback in a new tab: only URL state is available.
|
|
66
|
+
resetEmailCallbackUrl.searchParams.set("code", "reset-code");
|
|
67
|
+
const signInPlan = await plan({
|
|
68
|
+
handlerName: "signIn",
|
|
69
|
+
rawHandlerUrl: "/handler/sign-in",
|
|
70
|
+
currentUrl: resetEmailCallbackUrl,
|
|
71
|
+
noRedirectBack: true,
|
|
72
|
+
});
|
|
73
|
+
const finalSignInUrl = expectRedirectUrl(signInPlan, resetEmailCallbackUrl);
|
|
74
|
+
expectContinuation(finalSignInUrl, customerReturnTo);
|
|
75
|
+
expect(finalSignInUrl.searchParams.has("code")).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it.each([
|
|
79
|
+
["forgotPassword", "/handler/forgot-password"],
|
|
80
|
+
["passwordReset", "/handler/password-reset"],
|
|
81
|
+
] as const)("never synthesizes the current %s page as a return target", async (handlerName, rawHandlerUrl) => {
|
|
82
|
+
const currentUrl = new URL(`https://hosted.example.test${rawHandlerUrl}?code=reset-code`);
|
|
83
|
+
const result = expectRedirectUrl(await plan({
|
|
84
|
+
handlerName,
|
|
85
|
+
rawHandlerUrl,
|
|
86
|
+
currentUrl,
|
|
87
|
+
}), currentUrl);
|
|
88
|
+
|
|
89
|
+
expect(result.searchParams.has("after_auth_return_to")).toBe(false);
|
|
90
|
+
expect(result.searchParams.has("code")).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("makes sign-in noRedirectBack suppress capture while preserving only inherited state", async () => {
|
|
94
|
+
const resetUrlWithoutContinuation = new URL("https://hosted.example.test/handler/password-reset?code=reset-code");
|
|
95
|
+
const result = expectRedirectUrl(await plan({
|
|
96
|
+
handlerName: "signIn",
|
|
97
|
+
rawHandlerUrl: "/handler/sign-in",
|
|
98
|
+
currentUrl: resetUrlWithoutContinuation,
|
|
99
|
+
noRedirectBack: true,
|
|
100
|
+
}), resetUrlWithoutContinuation);
|
|
101
|
+
|
|
102
|
+
expect(result.toString()).toBe("https://hosted.example.test/handler/sign-in");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it.each(["afterSignIn", "afterSignUp"] as const)(
|
|
106
|
+
"keeps existing %s noRedirectBack semantics",
|
|
107
|
+
async (handlerName) => {
|
|
108
|
+
const currentUrl = new URL("https://hosted.example.test/handler/sign-in?after_auth_return_to=%2Fcustomer");
|
|
109
|
+
const result = expectRedirectUrl(await plan({
|
|
110
|
+
handlerName,
|
|
111
|
+
rawHandlerUrl: "/default-after-auth",
|
|
112
|
+
currentUrl,
|
|
113
|
+
noRedirectBack: true,
|
|
114
|
+
}), currentUrl);
|
|
115
|
+
|
|
116
|
+
expect(result.toString()).toBe("https://hosted.example.test/default-after-auth");
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
});
|
|
@@ -42,7 +42,8 @@ export function getCrossDomainHandoffParamsFromCurrentUrl(currentUrl: URL): Cros
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
type RedirectBackAwareHandlerName = "signIn" | "signUp" | "onboarding" | "signOut";
|
|
45
|
-
type
|
|
45
|
+
type ContinuationAwareHandlerName = "forgotPassword" | "passwordReset";
|
|
46
|
+
type HandlerRedirectPolicy = "none" | "redirect-back-aware" | "continuation-aware" | "after-auth-return";
|
|
46
47
|
|
|
47
48
|
type CrossDomainHandoffParamsMaybeMissing = {
|
|
48
49
|
state: string | null,
|
|
@@ -57,6 +58,10 @@ function isRedirectBackAwareHandlerName(handlerName: keyof HandlerUrls): handler
|
|
|
57
58
|
|| handlerName === "signOut";
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
function isContinuationAwareHandlerName(handlerName: keyof HandlerUrls): handlerName is ContinuationAwareHandlerName {
|
|
62
|
+
return handlerName === "forgotPassword" || handlerName === "passwordReset";
|
|
63
|
+
}
|
|
64
|
+
|
|
60
65
|
function hasCrossDomainHandoffParams(url: URL): boolean {
|
|
61
66
|
return (
|
|
62
67
|
hasCrossDomainParam(url.searchParams, "state")
|
|
@@ -98,14 +103,19 @@ function buildRedirectBackAwareHandlerUrl(options: {
|
|
|
98
103
|
currentUrl: URL,
|
|
99
104
|
crossDomainHandoffParams: CrossDomainHandoffParams | null,
|
|
100
105
|
localOAuthCallbackUrl: string,
|
|
106
|
+
noRedirectBack: boolean,
|
|
107
|
+
afterCallbackRedirectUrlOverride: string | null,
|
|
101
108
|
}): string {
|
|
102
109
|
const nextUrl = new URL(options.rawHandlerUrl, options.currentUrl);
|
|
103
110
|
// Preserve after_auth_return_to verbatim (not a rebranded param).
|
|
104
111
|
const currentAfterAuthReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
105
|
-
if (currentAfterAuthReturnTo != null && !nextUrl.searchParams.has("after_auth_return_to")) {
|
|
112
|
+
if (!options.noRedirectBack && currentAfterAuthReturnTo != null && !nextUrl.searchParams.has("after_auth_return_to")) {
|
|
106
113
|
nextUrl.searchParams.set("after_auth_return_to", currentAfterAuthReturnTo);
|
|
107
114
|
}
|
|
108
115
|
for (const preservedParam of ["state", "codeChallenge", "afterCallbackRedirectUrl"] as const) {
|
|
116
|
+
if (options.noRedirectBack && preservedParam === "afterCallbackRedirectUrl") {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
109
119
|
const currentValue = getCrossDomainParam(options.currentUrl.searchParams, preservedParam);
|
|
110
120
|
if (currentValue != null && !hasCrossDomainParam(nextUrl.searchParams, preservedParam)) {
|
|
111
121
|
setCrossDomainParam(nextUrl.searchParams, preservedParam, currentValue);
|
|
@@ -127,31 +137,58 @@ function buildRedirectBackAwareHandlerUrl(options: {
|
|
|
127
137
|
if (isCrossDomainHandlerRedirect) {
|
|
128
138
|
if (!hasCrossDomainHandoffParams(nextUrl)) {
|
|
129
139
|
const inheritedAfterAuthReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
130
|
-
const afterCallbackRedirectUrl =
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
const afterCallbackRedirectUrl = options.afterCallbackRedirectUrlOverride ?? (
|
|
141
|
+
inheritedAfterAuthReturnTo
|
|
142
|
+
? new URL(inheritedAfterAuthReturnTo, options.currentUrl).toString()
|
|
143
|
+
: options.currentUrl.toString()
|
|
144
|
+
);
|
|
145
|
+
const handoffParams = options.crossDomainHandoffParams
|
|
146
|
+
?? getCrossDomainHandoffParamsFromCurrentUrl(options.currentUrl);
|
|
133
147
|
const callbackUrl = buildCrossDomainAuthCallbackUrl({
|
|
134
148
|
currentUrl: options.currentUrl,
|
|
135
149
|
localOAuthCallbackUrl: options.localOAuthCallbackUrl,
|
|
136
|
-
state:
|
|
137
|
-
codeChallenge:
|
|
150
|
+
state: handoffParams?.state,
|
|
151
|
+
codeChallenge: handoffParams?.codeChallenge,
|
|
138
152
|
afterCallbackRedirectUrl,
|
|
139
153
|
});
|
|
140
154
|
|
|
141
155
|
nextUrl.searchParams.set("after_auth_return_to", callbackUrl.toString());
|
|
142
156
|
setCrossDomainParam(nextUrl.searchParams, "afterCallbackRedirectUrl", afterCallbackRedirectUrl);
|
|
143
|
-
if (
|
|
144
|
-
setCrossDomainParam(nextUrl.searchParams, "state",
|
|
145
|
-
setCrossDomainParam(nextUrl.searchParams, "codeChallenge",
|
|
157
|
+
if (handoffParams != null) {
|
|
158
|
+
setCrossDomainParam(nextUrl.searchParams, "state", handoffParams.state);
|
|
159
|
+
setCrossDomainParam(nextUrl.searchParams, "codeChallenge", handoffParams.codeChallenge);
|
|
146
160
|
}
|
|
147
161
|
}
|
|
148
|
-
} else if (options.currentUrl.protocol === nextUrl.protocol && options.currentUrl.host === nextUrl.host && !nextUrl.searchParams.has("after_auth_return_to")) {
|
|
162
|
+
} else if (!options.noRedirectBack && options.currentUrl.protocol === nextUrl.protocol && options.currentUrl.host === nextUrl.host && !nextUrl.searchParams.has("after_auth_return_to")) {
|
|
149
163
|
nextUrl.searchParams.set("after_auth_return_to", getRelativePart(options.currentUrl));
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
return nextUrl.origin === options.currentUrl.origin ? getRelativePart(nextUrl) : nextUrl.toString();
|
|
153
167
|
}
|
|
154
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Carries an auth flow's existing return state without ever making the current page the return
|
|
171
|
+
* target. This distinction is important for password flows: forgot-password and password-reset
|
|
172
|
+
* are continuation pages, not destinations customers should return to after signing in.
|
|
173
|
+
*/
|
|
174
|
+
function buildContinuationAwareHandlerUrl(options: {
|
|
175
|
+
rawHandlerUrl: string,
|
|
176
|
+
currentUrl: URL,
|
|
177
|
+
}): string {
|
|
178
|
+
const nextUrl = new URL(options.rawHandlerUrl, options.currentUrl);
|
|
179
|
+
const inheritedAfterAuthReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
180
|
+
if (inheritedAfterAuthReturnTo != null && !nextUrl.searchParams.has("after_auth_return_to")) {
|
|
181
|
+
nextUrl.searchParams.set("after_auth_return_to", inheritedAfterAuthReturnTo);
|
|
182
|
+
}
|
|
183
|
+
for (const preservedParam of ["state", "codeChallenge", "afterCallbackRedirectUrl"] as const) {
|
|
184
|
+
const currentValue = getCrossDomainParam(options.currentUrl.searchParams, preservedParam);
|
|
185
|
+
if (currentValue != null && !hasCrossDomainParam(nextUrl.searchParams, preservedParam)) {
|
|
186
|
+
setCrossDomainParam(nextUrl.searchParams, preservedParam, currentValue);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return nextUrl.origin === options.currentUrl.origin ? getRelativePart(nextUrl) : nextUrl.toString();
|
|
190
|
+
}
|
|
191
|
+
|
|
155
192
|
function getHandlerRedirectPolicy(handlerName: keyof HandlerUrls): HandlerRedirectPolicy {
|
|
156
193
|
if (handlerName === "afterSignIn" || handlerName === "afterSignUp") {
|
|
157
194
|
return "after-auth-return";
|
|
@@ -159,6 +196,9 @@ function getHandlerRedirectPolicy(handlerName: keyof HandlerUrls): HandlerRedire
|
|
|
159
196
|
if (isRedirectBackAwareHandlerName(handlerName)) {
|
|
160
197
|
return "redirect-back-aware";
|
|
161
198
|
}
|
|
199
|
+
if (isContinuationAwareHandlerName(handlerName)) {
|
|
200
|
+
return "continuation-aware";
|
|
201
|
+
}
|
|
162
202
|
return "none";
|
|
163
203
|
}
|
|
164
204
|
|
|
@@ -179,6 +219,8 @@ async function resolveRedirectBackAwareHandlerUrlForRedirect(options: {
|
|
|
179
219
|
currentUrl: URL,
|
|
180
220
|
localOAuthCallbackUrl: string,
|
|
181
221
|
getCrossDomainHandoffParams: (currentUrl: URL) => Promise<CrossDomainHandoffParams>,
|
|
222
|
+
noRedirectBack: boolean,
|
|
223
|
+
afterCallbackRedirectUrlOverride: string | null,
|
|
182
224
|
}): Promise<string> {
|
|
183
225
|
const initial = buildRedirectBackAwareHandlerUrl({
|
|
184
226
|
handlerName: options.handlerName,
|
|
@@ -186,6 +228,8 @@ async function resolveRedirectBackAwareHandlerUrlForRedirect(options: {
|
|
|
186
228
|
currentUrl: options.currentUrl,
|
|
187
229
|
crossDomainHandoffParams: null,
|
|
188
230
|
localOAuthCallbackUrl: options.localOAuthCallbackUrl,
|
|
231
|
+
noRedirectBack: options.noRedirectBack,
|
|
232
|
+
afterCallbackRedirectUrlOverride: options.afterCallbackRedirectUrlOverride,
|
|
189
233
|
});
|
|
190
234
|
if (options.handlerName === "signOut") {
|
|
191
235
|
return initial;
|
|
@@ -204,6 +248,8 @@ async function resolveRedirectBackAwareHandlerUrlForRedirect(options: {
|
|
|
204
248
|
currentUrl: options.currentUrl,
|
|
205
249
|
crossDomainHandoffParams,
|
|
206
250
|
localOAuthCallbackUrl: options.localOAuthCallbackUrl,
|
|
251
|
+
noRedirectBack: options.noRedirectBack,
|
|
252
|
+
afterCallbackRedirectUrlOverride: options.afterCallbackRedirectUrlOverride,
|
|
207
253
|
});
|
|
208
254
|
}
|
|
209
255
|
|
|
@@ -213,18 +259,56 @@ export async function planRedirectToHandler(options: {
|
|
|
213
259
|
noRedirectBack: boolean,
|
|
214
260
|
currentUrl: URL | null,
|
|
215
261
|
localOAuthCallbackUrl: string,
|
|
262
|
+
rawHomeUrl: string,
|
|
216
263
|
getCrossDomainHandoffParams: (currentUrl: URL) => Promise<CrossDomainHandoffParams>,
|
|
217
264
|
}): Promise<RedirectToHandlerPlan> {
|
|
218
|
-
if (options.
|
|
265
|
+
if (options.currentUrl == null) {
|
|
219
266
|
return { type: "redirect", url: options.rawHandlerUrl };
|
|
220
267
|
}
|
|
221
268
|
|
|
222
269
|
const policy = getHandlerRedirectPolicy(options.handlerName);
|
|
270
|
+
if (options.noRedirectBack) {
|
|
271
|
+
// Sign-in uses noRedirectBack from password-reset pages to avoid capturing the reset page.
|
|
272
|
+
// Same-domain sign-in must still carry an already-inherited continuation. Cross-domain
|
|
273
|
+
// sign-in falls through to the redirect-back-aware planner below because it needs to return
|
|
274
|
+
// through the source app's callback before continuing to home.
|
|
275
|
+
if (options.handlerName === "signIn") {
|
|
276
|
+
const signInTarget = new URL(options.rawHandlerUrl, options.currentUrl);
|
|
277
|
+
if (signInTarget.origin === options.currentUrl.origin) {
|
|
278
|
+
return {
|
|
279
|
+
type: "redirect",
|
|
280
|
+
url: buildContinuationAwareHandlerUrl({
|
|
281
|
+
rawHandlerUrl: options.rawHandlerUrl,
|
|
282
|
+
currentUrl: options.currentUrl,
|
|
283
|
+
}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
// Other handlers retain the historical noRedirectBack behavior, including
|
|
288
|
+
// afterSignIn/afterSignUp ignoring all return state.
|
|
289
|
+
return {
|
|
290
|
+
type: "redirect",
|
|
291
|
+
url: options.rawHandlerUrl,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
223
295
|
if (policy === "none") {
|
|
224
296
|
return { type: "redirect", url: options.rawHandlerUrl };
|
|
225
297
|
}
|
|
298
|
+
if (policy === "continuation-aware") {
|
|
299
|
+
return {
|
|
300
|
+
type: "redirect",
|
|
301
|
+
url: buildContinuationAwareHandlerUrl({
|
|
302
|
+
rawHandlerUrl: options.rawHandlerUrl,
|
|
303
|
+
currentUrl: options.currentUrl,
|
|
304
|
+
}),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
226
307
|
|
|
227
308
|
if (policy === "after-auth-return") {
|
|
309
|
+
if (options.noRedirectBack) {
|
|
310
|
+
return { type: "redirect", url: options.rawHandlerUrl };
|
|
311
|
+
}
|
|
228
312
|
const redirectBackUrl = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
229
313
|
if (redirectBackUrl == null) {
|
|
230
314
|
return { type: "redirect", url: options.rawHandlerUrl };
|
|
@@ -267,6 +351,12 @@ export async function planRedirectToHandler(options: {
|
|
|
267
351
|
});
|
|
268
352
|
}
|
|
269
353
|
|
|
354
|
+
// noRedirectBack suppresses the initiating deep link, not the return journey. Cross-domain auth
|
|
355
|
+
// still needs to return through the source app's callback, but its final destination is home.
|
|
356
|
+
const afterCallbackRedirectUrlOverride = options.noRedirectBack
|
|
357
|
+
? new URL(options.rawHomeUrl, options.currentUrl).toString()
|
|
358
|
+
: null;
|
|
359
|
+
|
|
270
360
|
return {
|
|
271
361
|
type: "redirect",
|
|
272
362
|
url: await resolveRedirectBackAwareHandlerUrlForRedirect({
|
|
@@ -275,6 +365,8 @@ export async function planRedirectToHandler(options: {
|
|
|
275
365
|
currentUrl: options.currentUrl,
|
|
276
366
|
localOAuthCallbackUrl: options.localOAuthCallbackUrl,
|
|
277
367
|
getCrossDomainHandoffParams: options.getCrossDomainHandoffParams,
|
|
368
|
+
noRedirectBack: options.noRedirectBack,
|
|
369
|
+
afterCallbackRedirectUrlOverride,
|
|
278
370
|
}),
|
|
279
371
|
};
|
|
280
372
|
}
|
|
@@ -3,10 +3,10 @@
|
|
|
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
5
|
import { KnownErrors } from "@hexclave/shared";
|
|
6
|
+
import type { RequestListener } from "@hexclave/shared/dist/interface/client-interface";
|
|
6
7
|
import { CurrentUserCrud } from "@hexclave/shared/dist/interface/crud/current-user";
|
|
7
8
|
import { Result } from "@hexclave/shared/dist/utils/results";
|
|
8
|
-
import { AsyncStoreProperty, AuthLike, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrlOptions, HandlerUrls, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, ResolvedHandlerUrls,
|
|
9
|
-
import type { RequestListener } from "@hexclave/shared/dist/interface/client-interface";
|
|
9
|
+
import { AsyncStoreProperty, AuthLike, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrlOptions, HandlerUrls, hexclaveAppInternalsSymbol, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, ResolvedHandlerUrls, TokenStoreInit } from "../../common";
|
|
10
10
|
import { CustomerInvoicesList, CustomerInvoicesRequestOptions, CustomerProductsList, CustomerProductsRequestOptions, Item } from "../../customers";
|
|
11
11
|
import { Project } from "../../projects";
|
|
12
12
|
import { ProjectCurrentUser, SyncedPartialUser, TokenPartialUser } from "../../users";
|
|
@@ -41,6 +41,14 @@ export type StackClientAppConstructorOptions<HasTokenStore extends boolean, Proj
|
|
|
41
41
|
*/
|
|
42
42
|
noAutomaticPrefetch?: boolean,
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Whether the constructor starts browser integrations and other automatic side effects. Defaults to `true`.
|
|
46
|
+
*
|
|
47
|
+
* Set this to `false` when the app is instantiated inside a custom dashboard. Explicit method calls remain
|
|
48
|
+
* available and can still perform their documented side effects.
|
|
49
|
+
*/
|
|
50
|
+
automaticSideEffects?: boolean,
|
|
51
|
+
|
|
44
52
|
/**
|
|
45
53
|
* Options for analytics and session recording. Replays are enabled by default;
|
|
46
54
|
* set `{ replays: { enabled: false } }` to opt out.
|
|
@@ -136,6 +144,8 @@ export type StackClientApp<HasTokenStore extends boolean = boolean, ProjectId ex
|
|
|
136
144
|
redirectToUrl(url: string | URL, options?: { replace?: boolean }): Promise<void>,
|
|
137
145
|
getRedirectToHandlerUrl(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise<string>,
|
|
138
146
|
redirectToHandler(handlerName: keyof HandlerUrls, options?: RedirectToOptions): Promise<void>,
|
|
147
|
+
/** Raw flow metadata only. Never navigate to this value without normal redirect validation. */
|
|
148
|
+
getRawAfterAuthReturnTo(): string | null,
|
|
139
149
|
signInWithTokens(tokens: { accessToken: string, refreshToken: string }): Promise<void>,
|
|
140
150
|
awaitPendingAuthResolutions(): Promise<void>,
|
|
141
151
|
isTrustedRedirectUrl(url: string): Promise<boolean>,
|