@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
|
@@ -17,8 +17,8 @@ let _hexclave_shared_dist_utils_stores = require("@hexclave/shared/dist/utils/st
|
|
|
17
17
|
let ____________generated_env_js = require("../../../../generated/env.js");
|
|
18
18
|
let ______url_targets_js = require("../../url-targets.js");
|
|
19
19
|
//#region src/lib/hexclave-app/apps/implementations/common.ts
|
|
20
|
-
const clientVersion = "js @hexclave/next@1.0.
|
|
21
|
-
if ("js @hexclave/next@1.0.
|
|
20
|
+
const clientVersion = "js @hexclave/next@1.0.74";
|
|
21
|
+
if ("js @hexclave/next@1.0.74".startsWith("STACK_COMPILE_TIME"));
|
|
22
22
|
const replaceHexclavePortPrefix = (input) => {
|
|
23
23
|
if (!input) return input;
|
|
24
24
|
const prefix = ____________generated_env_js.envVars.HEXCLAVE_PORT_PREFIX;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as _HexclaveServerAppImplIncomplete, r as _HexclaveClientAppImplIncomplete, t as _HexclaveAdminAppImplIncomplete } from "../../../../admin-app-impl-
|
|
1
|
+
import { n as _HexclaveServerAppImplIncomplete, r as _HexclaveClientAppImplIncomplete, t as _HexclaveAdminAppImplIncomplete } from "../../../../admin-app-impl-FZdANO62.js";
|
|
2
2
|
|
|
3
3
|
//#region src/lib/hexclave-app/apps/implementations/index.d.ts
|
|
4
4
|
declare const _HexclaveAdminAppImpl: typeof _HexclaveAdminAppImplIncomplete, _HexclaveClientAppImpl: typeof _HexclaveClientAppImplIncomplete, _HexclaveServerAppImpl: typeof _HexclaveServerAppImplIncomplete;
|
|
@@ -23,6 +23,18 @@ declare function saveRedirectBackStateFromUrl(options: {
|
|
|
23
23
|
declare function readRedirectBackState(options: {
|
|
24
24
|
projectId: string;
|
|
25
25
|
}): PersistedRedirectBackState | null;
|
|
26
|
+
/**
|
|
27
|
+
* Returns the raw return target carried by the current auth flow. The value is intentionally not
|
|
28
|
+
* described as trusted: callers must still pass it through the normal redirect validation before
|
|
29
|
+
* navigating to it.
|
|
30
|
+
*
|
|
31
|
+
* An explicit query parameter wins even when it is empty. This prevents a malformed URL from
|
|
32
|
+
* silently reviving an older flow from sessionStorage.
|
|
33
|
+
*/
|
|
34
|
+
declare function getRawAfterAuthReturnTo(options: {
|
|
35
|
+
currentUrl: URL;
|
|
36
|
+
projectId: string;
|
|
37
|
+
}): string | null;
|
|
26
38
|
/**
|
|
27
39
|
* Returns a copy of `currentUrl` with any redirect-back query params that were dropped along the
|
|
28
40
|
* way restored from the sessionStorage mirror. If `currentUrl` already carries
|
|
@@ -33,5 +45,5 @@ declare function augmentUrlWithPersistedRedirectBackState(options: {
|
|
|
33
45
|
projectId: string;
|
|
34
46
|
}): URL;
|
|
35
47
|
//#endregion
|
|
36
|
-
export { PersistedRedirectBackState, augmentUrlWithPersistedRedirectBackState, readRedirectBackState, saveRedirectBackStateFromUrl };
|
|
48
|
+
export { PersistedRedirectBackState, augmentUrlWithPersistedRedirectBackState, getRawAfterAuthReturnTo, readRedirectBackState, saveRedirectBackStateFromUrl };
|
|
37
49
|
//# sourceMappingURL=redirect-back-state.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect-back-state.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/redirect-back-state.ts"],"mappings":";KAoCY,0BAAA;EAAA;;;;;EAMV,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,mCAAA,iBAEa;EAAb,aAAA;AAAA;;;;;;iBAwBc,4BAAA,CAA6B,OAAA;EAAW,GAAA,EAAK,GAAG;EAAE,SAAA;AAAA;AAAA,iBAelD,qBAAA,CAAsB,OAAA;EAAW,SAAA;AAAA,IAAsB,0BAA0B;;;;;;
|
|
1
|
+
{"version":3,"file":"redirect-back-state.d.ts","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/redirect-back-state.ts"],"mappings":";KAoCY,0BAAA;EAAA;;;;;EAMV,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,mCAAA,iBAEa;EAAb,aAAA;AAAA;;;;;;iBAwBc,4BAAA,CAA6B,OAAA;EAAW,GAAA,EAAK,GAAG;EAAE,SAAA;AAAA;AAAA,iBAelD,qBAAA,CAAsB,OAAA;EAAW,SAAA;AAAA,IAAsB,0BAA0B;;;;;;AAAA;AAmCjG;;iBAAgB,uBAAA,CAAwB,OAAA;EAAW,UAAA,EAAY,GAAG;EAAE,SAAA;AAAA;;;;AAAmB;AAcvF;iBAAgB,wCAAA,CAAyC,OAAA;EAAW,UAAA,EAAY,GAAA;EAAK,SAAA;AAAA,IAAsB,GAAG"}
|
|
@@ -66,6 +66,20 @@ function readRedirectBackState(options) {
|
|
|
66
66
|
return parsed;
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
+
* Returns the raw return target carried by the current auth flow. The value is intentionally not
|
|
70
|
+
* described as trusted: callers must still pass it through the normal redirect validation before
|
|
71
|
+
* navigating to it.
|
|
72
|
+
*
|
|
73
|
+
* An explicit query parameter wins even when it is empty. This prevents a malformed URL from
|
|
74
|
+
* silently reviving an older flow from sessionStorage.
|
|
75
|
+
*/
|
|
76
|
+
function getRawAfterAuthReturnTo(options) {
|
|
77
|
+
const explicitReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
78
|
+
if (explicitReturnTo != null) return explicitReturnTo.trim().length > 0 ? explicitReturnTo : null;
|
|
79
|
+
const persistedReturnTo = readRedirectBackState({ projectId: options.projectId })?.afterAuthReturnTo;
|
|
80
|
+
return persistedReturnTo != null && persistedReturnTo.trim().length > 0 ? persistedReturnTo : null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
69
83
|
* Returns a copy of `currentUrl` with any redirect-back query params that were dropped along the
|
|
70
84
|
* way restored from the sessionStorage mirror. If `currentUrl` already carries
|
|
71
85
|
* `after_auth_return_to`, it is returned unchanged — explicit query params always win.
|
|
@@ -86,6 +100,7 @@ function augmentUrlWithPersistedRedirectBackState(options) {
|
|
|
86
100
|
}
|
|
87
101
|
//#endregion
|
|
88
102
|
exports.augmentUrlWithPersistedRedirectBackState = augmentUrlWithPersistedRedirectBackState;
|
|
103
|
+
exports.getRawAfterAuthReturnTo = getRawAfterAuthReturnTo;
|
|
89
104
|
exports.readRedirectBackState = readRedirectBackState;
|
|
90
105
|
exports.saveRedirectBackStateFromUrl = saveRedirectBackStateFromUrl;
|
|
91
106
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect-back-state.js","names":["crossDomainAuthQueryParams"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/redirect-back-state.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { readSessionStorageItem, removeSessionStorageItem, writeSessionStorageItem } from \"../../../../utils/session-storage\";\nimport { crossDomainAuthQueryParams } from \"./redirect-page-urls\";\n\n/**\n * Resilience layer for the redirect-back (\"return to where you came from\") state of auth flows.\n *\n * The primary transport for this state is query params (`after_auth_return_to` +\n * `hexclave_cross_domain_*`), which must survive every hop of the auth flow (sign-in page, OAuth\n * provider round-trips, MFA, magic links, ...). Historically, any hop that dropped these params\n * stranded the user on the wrong page after auth — on hosted components, that meant the hosted\n * welcome page instead of the customer's app.\n *\n * This module mirrors the state into sessionStorage whenever it appears in a URL, so\n * `redirectToAfterSignIn`/`redirectToAfterSignUp` can restore it if the params were dropped along\n * the way. Query params always take precedence; the mirror is only a fallback. It is scoped\n * per-tab (sessionStorage), per-origin (sessionStorage again), and per-project, and it expires\n * after a TTL. Restored URLs still go through the exact same trust validation as URLs read from\n * query params (the `_isTrusted` gate client-side and the cross-domain authorize endpoint\n * server-side), so the mirror never widens the set of allowed destinations.\n */\n\n// Deliberately NOT consumed/cleared on use: React can invoke the after-sign-in redirect twice\n// (strict mode, remounts), and consuming on first read would make the second invocation fall back\n// to the default post-auth page and race the correct redirect. Staleness is bounded by the TTL\n// instead, and a stale entry merely returns the user to the last page that started an auth flow\n// in this tab — which is also what they'd expect.\n//\n// The TTL must stay below the 1h lifetime of the outer PKCE verifier cookie (see\n// saveVerifierAndState in cookie.ts): the mirrored cross-domain params are useless once their\n// verifier cookie on the app domain has expired.\nconst redirectBackStateTtlMillis = 30 * 60 * 1000;\n\nexport type PersistedRedirectBackState = {\n /**\n * The `after_auth_return_to` value exactly as it appeared in the URL (possibly relative).\n * sessionStorage is per-origin, so restoring it into a URL later always resolves it against the\n * same origin it was saved from.\n */\n afterAuthReturnTo: string,\n crossDomainState: string | null,\n crossDomainCodeChallenge: string | null,\n crossDomainAfterCallbackRedirectUrl: string | null,\n /** Wall-clock ms (Date.now()); must survive page loads, so performance.now() is not usable. */\n savedAtMillis: number,\n};\n\nfunction getStorageKey(projectId: string): string {\n return `hexclave-redirect-back-state-${projectId}`;\n}\n\nfunction isPersistedRedirectBackState(value: unknown): value is PersistedRedirectBackState {\n return (\n typeof value === \"object\"\n && value !== null\n && \"afterAuthReturnTo\" in value && typeof value.afterAuthReturnTo === \"string\"\n && \"crossDomainState\" in value && (value.crossDomainState === null || typeof value.crossDomainState === \"string\")\n && \"crossDomainCodeChallenge\" in value && (value.crossDomainCodeChallenge === null || typeof value.crossDomainCodeChallenge === \"string\")\n && \"crossDomainAfterCallbackRedirectUrl\" in value && (value.crossDomainAfterCallbackRedirectUrl === null || typeof value.crossDomainAfterCallbackRedirectUrl === \"string\")\n && \"savedAtMillis\" in value && typeof value.savedAtMillis === \"number\"\n );\n}\n\n/**\n * Mirrors the redirect-back state from the given URL into sessionStorage, if it has any. Called\n * with the page URL at app construction time (full page loads) and with the target URL of every\n * SDK-driven redirect (client-side navigations, which don't re-run the constructor).\n */\nexport function saveRedirectBackStateFromUrl(options: { url: URL, projectId: string }): void {\n const afterAuthReturnTo = options.url.searchParams.get(\"after_auth_return_to\");\n if (afterAuthReturnTo == null) {\n return;\n }\n const state: PersistedRedirectBackState = {\n afterAuthReturnTo,\n crossDomainState: options.url.searchParams.get(crossDomainAuthQueryParams.state),\n crossDomainCodeChallenge: options.url.searchParams.get(crossDomainAuthQueryParams.codeChallenge),\n crossDomainAfterCallbackRedirectUrl: options.url.searchParams.get(crossDomainAuthQueryParams.afterCallbackRedirectUrl),\n savedAtMillis: Date.now(),\n };\n writeSessionStorageItem(getStorageKey(options.projectId), JSON.stringify(state));\n}\n\nexport function readRedirectBackState(options: { projectId: string }): PersistedRedirectBackState | null {\n const storageKey = getStorageKey(options.projectId);\n const raw = readSessionStorageItem(storageKey);\n if (raw == null) {\n return null;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // Corrupted value (eg. written by a different SDK version); losing the fallback for this flow\n // is the pre-existing behavior, so just discard it.\n removeSessionStorageItem(storageKey);\n return null;\n }\n if (!isPersistedRedirectBackState(parsed)) {\n removeSessionStorageItem(storageKey);\n return null;\n }\n const ageMillis = Date.now() - parsed.savedAtMillis;\n if (ageMillis < 0 || ageMillis > redirectBackStateTtlMillis) {\n removeSessionStorageItem(storageKey);\n return null;\n }\n return parsed;\n}\n\n/**\n * Returns a copy of `currentUrl` with any redirect-back query params that were dropped along the\n * way restored from the sessionStorage mirror. If `currentUrl` already carries\n * `after_auth_return_to`, it is returned unchanged — explicit query params always win.\n */\nexport function augmentUrlWithPersistedRedirectBackState(options: { currentUrl: URL, projectId: string }): URL {\n if (options.currentUrl.searchParams.has(\"after_auth_return_to\")) {\n return options.currentUrl;\n }\n const persisted = readRedirectBackState({ projectId: options.projectId });\n if (persisted == null) {\n return options.currentUrl;\n }\n const augmented = new URL(options.currentUrl.toString());\n augmented.searchParams.set(\"after_auth_return_to\", persisted.afterAuthReturnTo);\n const crossDomainParamValues = [\n [crossDomainAuthQueryParams.state, persisted.crossDomainState],\n [crossDomainAuthQueryParams.codeChallenge, persisted.crossDomainCodeChallenge],\n [crossDomainAuthQueryParams.afterCallbackRedirectUrl, persisted.crossDomainAfterCallbackRedirectUrl],\n ] as const;\n for (const [paramName, persistedValue] of crossDomainParamValues) {\n if (persistedValue != null && !augmented.searchParams.has(paramName)) {\n augmented.searchParams.set(paramName, persistedValue);\n }\n }\n return augmented;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAkCA,MAAM,6BAA6B,OAAU;AAgB7C,SAAS,cAAc,WAA2B;CAChD,OAAO,gCAAgC;AACzC;AAEA,SAAS,6BAA6B,OAAqD;CACzF,OACE,OAAO,UAAU,YACd,UAAU,QACV,uBAAuB,SAAS,OAAO,MAAM,sBAAsB,YACnE,sBAAsB,UAAU,MAAM,qBAAqB,QAAQ,OAAO,MAAM,qBAAqB,aACrG,8BAA8B,UAAU,MAAM,6BAA6B,QAAQ,OAAO,MAAM,6BAA6B,aAC7H,yCAAyC,UAAU,MAAM,wCAAwC,QAAQ,OAAO,MAAM,wCAAwC,aAC9J,mBAAmB,SAAS,OAAO,MAAM,kBAAkB;AAElE;;;;;;AAOA,SAAgB,6BAA6B,SAAgD;CAC3F,MAAM,oBAAoB,QAAQ,IAAI,aAAa,IAAI,sBAAsB;CAC7E,IAAI,qBAAqB,MACvB;CAEF,MAAM,QAAoC;EACxC;EACA,kBAAkB,QAAQ,IAAI,aAAa,IAAIA,wBAAAA,2BAA2B,KAAK;EAC/E,0BAA0B,QAAQ,IAAI,aAAa,IAAIA,wBAAAA,2BAA2B,aAAa;EAC/F,qCAAqC,QAAQ,IAAI,aAAa,IAAIA,wBAAAA,2BAA2B,wBAAwB;EACrH,eAAe,KAAK,IAAI;CAC1B;CACA,CAAA,GAAA,qCAAA,wBAAA,CAAwB,cAAc,QAAQ,SAAS,GAAG,KAAK,UAAU,KAAK,CAAC;AACjF;AAEA,SAAgB,sBAAsB,SAAmE;CACvG,MAAM,aAAa,cAAc,QAAQ,SAAS;CAClD,MAAM,OAAA,GAAA,qCAAA,uBAAA,CAA6B,UAAU;CAC7C,IAAI,OAAO,MACT,OAAO;CAET,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,QAAQ;EAGN,CAAA,GAAA,qCAAA,yBAAA,CAAyB,UAAU;EACnC,OAAO;CACT;CACA,IAAI,CAAC,6BAA6B,MAAM,GAAG;EACzC,CAAA,GAAA,qCAAA,yBAAA,CAAyB,UAAU;EACnC,OAAO;CACT;CACA,MAAM,YAAY,KAAK,IAAI,IAAI,OAAO;CACtC,IAAI,YAAY,KAAK,YAAY,4BAA4B;EAC3D,CAAA,GAAA,qCAAA,yBAAA,CAAyB,UAAU;EACnC,OAAO;CACT;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,yCAAyC,SAAsD;CAC7G,IAAI,QAAQ,WAAW,aAAa,IAAI,sBAAsB,GAC5D,OAAO,QAAQ;CAEjB,MAAM,YAAY,sBAAsB,EAAE,WAAW,QAAQ,UAAU,CAAC;CACxE,IAAI,aAAa,MACf,OAAO,QAAQ;CAEjB,MAAM,YAAY,IAAI,IAAI,QAAQ,WAAW,SAAS,CAAC;CACvD,UAAU,aAAa,IAAI,wBAAwB,UAAU,iBAAiB;CAC9E,MAAM,yBAAyB;EAC7B,CAACA,wBAAAA,2BAA2B,OAAO,UAAU,gBAAgB;EAC7D,CAACA,wBAAAA,2BAA2B,eAAe,UAAU,wBAAwB;EAC7E,CAACA,wBAAAA,2BAA2B,0BAA0B,UAAU,mCAAmC;CACrG;CACA,KAAK,MAAM,CAAC,WAAW,mBAAmB,wBACxC,IAAI,kBAAkB,QAAQ,CAAC,UAAU,aAAa,IAAI,SAAS,GACjE,UAAU,aAAa,IAAI,WAAW,cAAc;CAGxD,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"redirect-back-state.js","names":["crossDomainAuthQueryParams"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/redirect-back-state.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { readSessionStorageItem, removeSessionStorageItem, writeSessionStorageItem } from \"../../../../utils/session-storage\";\nimport { crossDomainAuthQueryParams } from \"./redirect-page-urls\";\n\n/**\n * Resilience layer for the redirect-back (\"return to where you came from\") state of auth flows.\n *\n * The primary transport for this state is query params (`after_auth_return_to` +\n * `hexclave_cross_domain_*`), which must survive every hop of the auth flow (sign-in page, OAuth\n * provider round-trips, MFA, magic links, ...). Historically, any hop that dropped these params\n * stranded the user on the wrong page after auth — on hosted components, that meant the hosted\n * welcome page instead of the customer's app.\n *\n * This module mirrors the state into sessionStorage whenever it appears in a URL, so\n * `redirectToAfterSignIn`/`redirectToAfterSignUp` can restore it if the params were dropped along\n * the way. Query params always take precedence; the mirror is only a fallback. It is scoped\n * per-tab (sessionStorage), per-origin (sessionStorage again), and per-project, and it expires\n * after a TTL. Restored URLs still go through the exact same trust validation as URLs read from\n * query params (the `_isTrusted` gate client-side and the cross-domain authorize endpoint\n * server-side), so the mirror never widens the set of allowed destinations.\n */\n\n// Deliberately NOT consumed/cleared on use: React can invoke the after-sign-in redirect twice\n// (strict mode, remounts), and consuming on first read would make the second invocation fall back\n// to the default post-auth page and race the correct redirect. Staleness is bounded by the TTL\n// instead, and a stale entry merely returns the user to the last page that started an auth flow\n// in this tab — which is also what they'd expect.\n//\n// The TTL must stay below the 1h lifetime of the outer PKCE verifier cookie (see\n// saveVerifierAndState in cookie.ts): the mirrored cross-domain params are useless once their\n// verifier cookie on the app domain has expired.\nconst redirectBackStateTtlMillis = 30 * 60 * 1000;\n\nexport type PersistedRedirectBackState = {\n /**\n * The `after_auth_return_to` value exactly as it appeared in the URL (possibly relative).\n * sessionStorage is per-origin, so restoring it into a URL later always resolves it against the\n * same origin it was saved from.\n */\n afterAuthReturnTo: string,\n crossDomainState: string | null,\n crossDomainCodeChallenge: string | null,\n crossDomainAfterCallbackRedirectUrl: string | null,\n /** Wall-clock ms (Date.now()); must survive page loads, so performance.now() is not usable. */\n savedAtMillis: number,\n};\n\nfunction getStorageKey(projectId: string): string {\n return `hexclave-redirect-back-state-${projectId}`;\n}\n\nfunction isPersistedRedirectBackState(value: unknown): value is PersistedRedirectBackState {\n return (\n typeof value === \"object\"\n && value !== null\n && \"afterAuthReturnTo\" in value && typeof value.afterAuthReturnTo === \"string\"\n && \"crossDomainState\" in value && (value.crossDomainState === null || typeof value.crossDomainState === \"string\")\n && \"crossDomainCodeChallenge\" in value && (value.crossDomainCodeChallenge === null || typeof value.crossDomainCodeChallenge === \"string\")\n && \"crossDomainAfterCallbackRedirectUrl\" in value && (value.crossDomainAfterCallbackRedirectUrl === null || typeof value.crossDomainAfterCallbackRedirectUrl === \"string\")\n && \"savedAtMillis\" in value && typeof value.savedAtMillis === \"number\"\n );\n}\n\n/**\n * Mirrors the redirect-back state from the given URL into sessionStorage, if it has any. Called\n * with the page URL at app construction time (full page loads) and with the target URL of every\n * SDK-driven redirect (client-side navigations, which don't re-run the constructor).\n */\nexport function saveRedirectBackStateFromUrl(options: { url: URL, projectId: string }): void {\n const afterAuthReturnTo = options.url.searchParams.get(\"after_auth_return_to\");\n if (afterAuthReturnTo == null) {\n return;\n }\n const state: PersistedRedirectBackState = {\n afterAuthReturnTo,\n crossDomainState: options.url.searchParams.get(crossDomainAuthQueryParams.state),\n crossDomainCodeChallenge: options.url.searchParams.get(crossDomainAuthQueryParams.codeChallenge),\n crossDomainAfterCallbackRedirectUrl: options.url.searchParams.get(crossDomainAuthQueryParams.afterCallbackRedirectUrl),\n savedAtMillis: Date.now(),\n };\n writeSessionStorageItem(getStorageKey(options.projectId), JSON.stringify(state));\n}\n\nexport function readRedirectBackState(options: { projectId: string }): PersistedRedirectBackState | null {\n const storageKey = getStorageKey(options.projectId);\n const raw = readSessionStorageItem(storageKey);\n if (raw == null) {\n return null;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // Corrupted value (eg. written by a different SDK version); losing the fallback for this flow\n // is the pre-existing behavior, so just discard it.\n removeSessionStorageItem(storageKey);\n return null;\n }\n if (!isPersistedRedirectBackState(parsed)) {\n removeSessionStorageItem(storageKey);\n return null;\n }\n const ageMillis = Date.now() - parsed.savedAtMillis;\n if (ageMillis < 0 || ageMillis > redirectBackStateTtlMillis) {\n removeSessionStorageItem(storageKey);\n return null;\n }\n return parsed;\n}\n\n/**\n * Returns the raw return target carried by the current auth flow. The value is intentionally not\n * described as trusted: callers must still pass it through the normal redirect validation before\n * navigating to it.\n *\n * An explicit query parameter wins even when it is empty. This prevents a malformed URL from\n * silently reviving an older flow from sessionStorage.\n */\nexport function getRawAfterAuthReturnTo(options: { currentUrl: URL, projectId: string }): string | null {\n const explicitReturnTo = options.currentUrl.searchParams.get(\"after_auth_return_to\");\n if (explicitReturnTo != null) {\n return explicitReturnTo.trim().length > 0 ? explicitReturnTo : null;\n }\n const persistedReturnTo = readRedirectBackState({ projectId: options.projectId })?.afterAuthReturnTo;\n return persistedReturnTo != null && persistedReturnTo.trim().length > 0 ? persistedReturnTo : null;\n}\n\n/**\n * Returns a copy of `currentUrl` with any redirect-back query params that were dropped along the\n * way restored from the sessionStorage mirror. If `currentUrl` already carries\n * `after_auth_return_to`, it is returned unchanged — explicit query params always win.\n */\nexport function augmentUrlWithPersistedRedirectBackState(options: { currentUrl: URL, projectId: string }): URL {\n if (options.currentUrl.searchParams.has(\"after_auth_return_to\")) {\n return options.currentUrl;\n }\n const persisted = readRedirectBackState({ projectId: options.projectId });\n if (persisted == null) {\n return options.currentUrl;\n }\n const augmented = new URL(options.currentUrl.toString());\n augmented.searchParams.set(\"after_auth_return_to\", persisted.afterAuthReturnTo);\n const crossDomainParamValues = [\n [crossDomainAuthQueryParams.state, persisted.crossDomainState],\n [crossDomainAuthQueryParams.codeChallenge, persisted.crossDomainCodeChallenge],\n [crossDomainAuthQueryParams.afterCallbackRedirectUrl, persisted.crossDomainAfterCallbackRedirectUrl],\n ] as const;\n for (const [paramName, persistedValue] of crossDomainParamValues) {\n if (persistedValue != null && !augmented.searchParams.has(paramName)) {\n augmented.searchParams.set(paramName, persistedValue);\n }\n }\n return augmented;\n}\n"],"mappings":"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"}
|
|
@@ -105,6 +105,30 @@ const projectId = "00000000-0000-4000-8000-000000000000";
|
|
|
105
105
|
projectId
|
|
106
106
|
}).toString()).toBe(explicitUrl.toString());
|
|
107
107
|
});
|
|
108
|
+
(0, vitest.it)("reads the explicit return target before persisted state", () => {
|
|
109
|
+
(0, __redirect_back_state_js.saveRedirectBackStateFromUrl)({
|
|
110
|
+
url: new URL("https://hosted.example.test/handler/sign-in?after_auth_return_to=/persisted"),
|
|
111
|
+
projectId
|
|
112
|
+
});
|
|
113
|
+
(0, vitest.expect)((0, __redirect_back_state_js.getRawAfterAuthReturnTo)({
|
|
114
|
+
currentUrl: new URL("https://hosted.example.test/handler/mfa?after_auth_return_to=/explicit"),
|
|
115
|
+
projectId
|
|
116
|
+
})).toBe("/explicit");
|
|
117
|
+
(0, vitest.expect)((0, __redirect_back_state_js.getRawAfterAuthReturnTo)({
|
|
118
|
+
currentUrl: new URL("https://hosted.example.test/handler/mfa"),
|
|
119
|
+
projectId
|
|
120
|
+
})).toBe("/persisted");
|
|
121
|
+
});
|
|
122
|
+
(0, vitest.it)("treats an explicitly empty return target as missing instead of reviving persisted state", () => {
|
|
123
|
+
(0, __redirect_back_state_js.saveRedirectBackStateFromUrl)({
|
|
124
|
+
url: new URL("https://hosted.example.test/handler/sign-in?after_auth_return_to=/persisted"),
|
|
125
|
+
projectId
|
|
126
|
+
});
|
|
127
|
+
(0, vitest.expect)((0, __redirect_back_state_js.getRawAfterAuthReturnTo)({
|
|
128
|
+
currentUrl: new URL("https://hosted.example.test/handler/mfa?after_auth_return_to=%20%20"),
|
|
129
|
+
projectId
|
|
130
|
+
})).toBeNull();
|
|
131
|
+
});
|
|
108
132
|
(0, vitest.it)("leaves the URL unchanged when there is no mirrored state", () => {
|
|
109
133
|
const url = new URL("https://hosted.example.test/handler/sign-in");
|
|
110
134
|
(0, vitest.expect)((0, __redirect_back_state_js.augmentUrlWithPersistedRedirectBackState)({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect-back-state.test.js","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/redirect-back-state.test.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { augmentUrlWithPersistedRedirectBackState, readRedirectBackState, saveRedirectBackStateFromUrl } from \"./redirect-back-state\";\n\nfunction createMockSessionStorage() {\n const map = new Map<string, string>();\n return {\n getItem: (key: string) => map.get(key) ?? null,\n setItem: (key: string, value: string) => {\n map.set(key, value);\n },\n removeItem: (key: string) => {\n map.delete(key);\n },\n };\n}\n\nconst previousWindow = Reflect.get(globalThis, \"window\");\nconst hadPreviousWindow = Reflect.has(globalThis, \"window\");\nconst projectId = \"00000000-0000-4000-8000-000000000000\";\n\ndescribe(\"redirect-back state mirror\", () => {\n beforeEach(() => {\n Reflect.set(globalThis, \"window\", { sessionStorage: createMockSessionStorage() });\n vi.useFakeTimers();\n vi.setSystemTime(new Date(\"2026-01-01T00:00:00Z\"));\n });\n\n afterEach(() => {\n vi.useRealTimers();\n if (hadPreviousWindow) {\n Reflect.set(globalThis, \"window\", previousWindow);\n } else {\n Reflect.deleteProperty(globalThis, \"window\");\n }\n });\n\n it(\"round-trips redirect-back state including the cross-domain handoff params\", () => {\n const url = new URL(\"https://hosted.example.test/handler/sign-in\");\n url.searchParams.set(\"after_auth_return_to\", \"https://app.example.test/handler/oauth-callback?hexclave_cross_domain_auth=1\");\n url.searchParams.set(\"hexclave_cross_domain_state\", \"the-state\");\n url.searchParams.set(\"hexclave_cross_domain_code_challenge\", \"the-code-challenge\");\n url.searchParams.set(\"hexclave_cross_domain_after_callback_redirect_url\", \"https://app.example.test/dashboard\");\n\n saveRedirectBackStateFromUrl({ url, projectId });\n\n expect(readRedirectBackState({ projectId })).toEqual({\n afterAuthReturnTo: \"https://app.example.test/handler/oauth-callback?hexclave_cross_domain_auth=1\",\n crossDomainState: \"the-state\",\n crossDomainCodeChallenge: \"the-code-challenge\",\n crossDomainAfterCallbackRedirectUrl: \"https://app.example.test/dashboard\",\n savedAtMillis: Date.now(),\n });\n });\n\n it(\"returns null when nothing was saved and does not save URLs without after_auth_return_to\", () => {\n expect(readRedirectBackState({ projectId })).toBeNull();\n saveRedirectBackStateFromUrl({ url: new URL(\"https://hosted.example.test/handler/sign-in?foo=bar\"), projectId });\n expect(readRedirectBackState({ projectId })).toBeNull();\n });\n\n it(\"expires the state after the TTL\", () => {\n const url = new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/music\");\n saveRedirectBackStateFromUrl({ url, projectId });\n\n vi.advanceTimersByTime(29 * 60 * 1000);\n expect(readRedirectBackState({ projectId })).not.toBeNull();\n\n vi.advanceTimersByTime(2 * 60 * 1000);\n expect(readRedirectBackState({ projectId })).toBeNull();\n });\n\n it(\"scopes the state per project\", () => {\n const url = new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/music\");\n saveRedirectBackStateFromUrl({ url, projectId });\n expect(readRedirectBackState({ projectId: \"11111111-1111-4111-8111-111111111111\" })).toBeNull();\n expect(readRedirectBackState({ projectId })).not.toBeNull();\n });\n\n it(\"discards malformed stored values\", () => {\n const storage = createMockSessionStorage();\n storage.setItem(`hexclave-redirect-back-state-${projectId}`, \"{not json\");\n Reflect.set(globalThis, \"window\", { sessionStorage: storage });\n expect(readRedirectBackState({ projectId })).toBeNull();\n\n storage.setItem(`hexclave-redirect-back-state-${projectId}`, JSON.stringify({ some: \"other-shape\" }));\n expect(readRedirectBackState({ projectId })).toBeNull();\n });\n\n it(\"restores dropped params onto a URL, but never overrides an explicit after_auth_return_to\", () => {\n const arrivalUrl = new URL(\"https://hosted.example.test/handler/sign-in\");\n arrivalUrl.searchParams.set(\"after_auth_return_to\", \"https://app.example.test/handler/oauth-callback\");\n arrivalUrl.searchParams.set(\"hexclave_cross_domain_state\", \"the-state\");\n arrivalUrl.searchParams.set(\"hexclave_cross_domain_code_challenge\", \"the-code-challenge\");\n arrivalUrl.searchParams.set(\"hexclave_cross_domain_after_callback_redirect_url\", \"https://app.example.test/dashboard\");\n saveRedirectBackStateFromUrl({ url: arrivalUrl, projectId });\n\n // Params were dropped along the way: restore all of them.\n const strippedUrl = new URL(\"https://hosted.example.test/handler/mfa\");\n const augmented = augmentUrlWithPersistedRedirectBackState({ currentUrl: strippedUrl, projectId });\n expect(augmented.searchParams.get(\"after_auth_return_to\")).toBe(\"https://app.example.test/handler/oauth-callback\");\n expect(augmented.searchParams.get(\"hexclave_cross_domain_state\")).toBe(\"the-state\");\n expect(augmented.searchParams.get(\"hexclave_cross_domain_code_challenge\")).toBe(\"the-code-challenge\");\n expect(augmented.searchParams.get(\"hexclave_cross_domain_after_callback_redirect_url\")).toBe(\"https://app.example.test/dashboard\");\n // The input URL is not mutated.\n expect(strippedUrl.searchParams.has(\"after_auth_return_to\")).toBe(false);\n\n // Explicit query params always win over the mirror.\n const explicitUrl = new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/somewhere-else\");\n const unchanged = augmentUrlWithPersistedRedirectBackState({ currentUrl: explicitUrl, projectId });\n expect(unchanged.toString()).toBe(explicitUrl.toString());\n });\n\n it(\"leaves the URL unchanged when there is no mirrored state\", () => {\n const url = new URL(\"https://hosted.example.test/handler/sign-in\");\n const augmented = augmentUrlWithPersistedRedirectBackState({ currentUrl: url, projectId });\n expect(augmented.toString()).toBe(url.toString());\n });\n\n it(\"degrades to a no-op when sessionStorage is unavailable\", () => {\n Reflect.set(globalThis, \"window\", {\n get sessionStorage(): Storage {\n throw new Error(\"Storage is disabled\");\n },\n });\n const url = new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/music\");\n saveRedirectBackStateFromUrl({ url, projectId });\n expect(readRedirectBackState({ projectId })).toBeNull();\n expect(augmentUrlWithPersistedRedirectBackState({ currentUrl: url, projectId }).toString()).toBe(url.toString());\n });\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"redirect-back-state.test.js","names":[],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/redirect-back-state.test.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { augmentUrlWithPersistedRedirectBackState, getRawAfterAuthReturnTo, readRedirectBackState, saveRedirectBackStateFromUrl } from \"./redirect-back-state\";\n\nfunction createMockSessionStorage() {\n const map = new Map<string, string>();\n return {\n getItem: (key: string) => map.get(key) ?? null,\n setItem: (key: string, value: string) => {\n map.set(key, value);\n },\n removeItem: (key: string) => {\n map.delete(key);\n },\n };\n}\n\nconst previousWindow = Reflect.get(globalThis, \"window\");\nconst hadPreviousWindow = Reflect.has(globalThis, \"window\");\nconst projectId = \"00000000-0000-4000-8000-000000000000\";\n\ndescribe(\"redirect-back state mirror\", () => {\n beforeEach(() => {\n Reflect.set(globalThis, \"window\", { sessionStorage: createMockSessionStorage() });\n vi.useFakeTimers();\n vi.setSystemTime(new Date(\"2026-01-01T00:00:00Z\"));\n });\n\n afterEach(() => {\n vi.useRealTimers();\n if (hadPreviousWindow) {\n Reflect.set(globalThis, \"window\", previousWindow);\n } else {\n Reflect.deleteProperty(globalThis, \"window\");\n }\n });\n\n it(\"round-trips redirect-back state including the cross-domain handoff params\", () => {\n const url = new URL(\"https://hosted.example.test/handler/sign-in\");\n url.searchParams.set(\"after_auth_return_to\", \"https://app.example.test/handler/oauth-callback?hexclave_cross_domain_auth=1\");\n url.searchParams.set(\"hexclave_cross_domain_state\", \"the-state\");\n url.searchParams.set(\"hexclave_cross_domain_code_challenge\", \"the-code-challenge\");\n url.searchParams.set(\"hexclave_cross_domain_after_callback_redirect_url\", \"https://app.example.test/dashboard\");\n\n saveRedirectBackStateFromUrl({ url, projectId });\n\n expect(readRedirectBackState({ projectId })).toEqual({\n afterAuthReturnTo: \"https://app.example.test/handler/oauth-callback?hexclave_cross_domain_auth=1\",\n crossDomainState: \"the-state\",\n crossDomainCodeChallenge: \"the-code-challenge\",\n crossDomainAfterCallbackRedirectUrl: \"https://app.example.test/dashboard\",\n savedAtMillis: Date.now(),\n });\n });\n\n it(\"returns null when nothing was saved and does not save URLs without after_auth_return_to\", () => {\n expect(readRedirectBackState({ projectId })).toBeNull();\n saveRedirectBackStateFromUrl({ url: new URL(\"https://hosted.example.test/handler/sign-in?foo=bar\"), projectId });\n expect(readRedirectBackState({ projectId })).toBeNull();\n });\n\n it(\"expires the state after the TTL\", () => {\n const url = new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/music\");\n saveRedirectBackStateFromUrl({ url, projectId });\n\n vi.advanceTimersByTime(29 * 60 * 1000);\n expect(readRedirectBackState({ projectId })).not.toBeNull();\n\n vi.advanceTimersByTime(2 * 60 * 1000);\n expect(readRedirectBackState({ projectId })).toBeNull();\n });\n\n it(\"scopes the state per project\", () => {\n const url = new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/music\");\n saveRedirectBackStateFromUrl({ url, projectId });\n expect(readRedirectBackState({ projectId: \"11111111-1111-4111-8111-111111111111\" })).toBeNull();\n expect(readRedirectBackState({ projectId })).not.toBeNull();\n });\n\n it(\"discards malformed stored values\", () => {\n const storage = createMockSessionStorage();\n storage.setItem(`hexclave-redirect-back-state-${projectId}`, \"{not json\");\n Reflect.set(globalThis, \"window\", { sessionStorage: storage });\n expect(readRedirectBackState({ projectId })).toBeNull();\n\n storage.setItem(`hexclave-redirect-back-state-${projectId}`, JSON.stringify({ some: \"other-shape\" }));\n expect(readRedirectBackState({ projectId })).toBeNull();\n });\n\n it(\"restores dropped params onto a URL, but never overrides an explicit after_auth_return_to\", () => {\n const arrivalUrl = new URL(\"https://hosted.example.test/handler/sign-in\");\n arrivalUrl.searchParams.set(\"after_auth_return_to\", \"https://app.example.test/handler/oauth-callback\");\n arrivalUrl.searchParams.set(\"hexclave_cross_domain_state\", \"the-state\");\n arrivalUrl.searchParams.set(\"hexclave_cross_domain_code_challenge\", \"the-code-challenge\");\n arrivalUrl.searchParams.set(\"hexclave_cross_domain_after_callback_redirect_url\", \"https://app.example.test/dashboard\");\n saveRedirectBackStateFromUrl({ url: arrivalUrl, projectId });\n\n // Params were dropped along the way: restore all of them.\n const strippedUrl = new URL(\"https://hosted.example.test/handler/mfa\");\n const augmented = augmentUrlWithPersistedRedirectBackState({ currentUrl: strippedUrl, projectId });\n expect(augmented.searchParams.get(\"after_auth_return_to\")).toBe(\"https://app.example.test/handler/oauth-callback\");\n expect(augmented.searchParams.get(\"hexclave_cross_domain_state\")).toBe(\"the-state\");\n expect(augmented.searchParams.get(\"hexclave_cross_domain_code_challenge\")).toBe(\"the-code-challenge\");\n expect(augmented.searchParams.get(\"hexclave_cross_domain_after_callback_redirect_url\")).toBe(\"https://app.example.test/dashboard\");\n // The input URL is not mutated.\n expect(strippedUrl.searchParams.has(\"after_auth_return_to\")).toBe(false);\n\n // Explicit query params always win over the mirror.\n const explicitUrl = new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/somewhere-else\");\n const unchanged = augmentUrlWithPersistedRedirectBackState({ currentUrl: explicitUrl, projectId });\n expect(unchanged.toString()).toBe(explicitUrl.toString());\n });\n\n it(\"reads the explicit return target before persisted state\", () => {\n saveRedirectBackStateFromUrl({\n url: new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/persisted\"),\n projectId,\n });\n\n expect(getRawAfterAuthReturnTo({\n currentUrl: new URL(\"https://hosted.example.test/handler/mfa?after_auth_return_to=/explicit\"),\n projectId,\n })).toBe(\"/explicit\");\n expect(getRawAfterAuthReturnTo({\n currentUrl: new URL(\"https://hosted.example.test/handler/mfa\"),\n projectId,\n })).toBe(\"/persisted\");\n });\n\n it(\"treats an explicitly empty return target as missing instead of reviving persisted state\", () => {\n saveRedirectBackStateFromUrl({\n url: new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/persisted\"),\n projectId,\n });\n\n expect(getRawAfterAuthReturnTo({\n currentUrl: new URL(\"https://hosted.example.test/handler/mfa?after_auth_return_to=%20%20\"),\n projectId,\n })).toBeNull();\n });\n\n it(\"leaves the URL unchanged when there is no mirrored state\", () => {\n const url = new URL(\"https://hosted.example.test/handler/sign-in\");\n const augmented = augmentUrlWithPersistedRedirectBackState({ currentUrl: url, projectId });\n expect(augmented.toString()).toBe(url.toString());\n });\n\n it(\"degrades to a no-op when sessionStorage is unavailable\", () => {\n Reflect.set(globalThis, \"window\", {\n get sessionStorage(): Storage {\n throw new Error(\"Storage is disabled\");\n },\n });\n const url = new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=/music\");\n saveRedirectBackStateFromUrl({ url, projectId });\n expect(readRedirectBackState({ projectId })).toBeNull();\n expect(augmentUrlWithPersistedRedirectBackState({ currentUrl: url, projectId }).toString()).toBe(url.toString());\n });\n});\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as planRedirectToHandler, n as crossDomainAuthQueryParams, r as getCrossDomainHandoffParamsFromCurrentUrl, t as CrossDomainHandoffParams } from "../../../../redirect-page-urls-
|
|
1
|
+
import { i as planRedirectToHandler, n as crossDomainAuthQueryParams, r as getCrossDomainHandoffParamsFromCurrentUrl, t as CrossDomainHandoffParams } from "../../../../redirect-page-urls-BEWG0AtP.js";
|
|
2
2
|
export { CrossDomainHandoffParams, crossDomainAuthQueryParams, getCrossDomainHandoffParamsFromCurrentUrl, planRedirectToHandler };
|
|
@@ -29,6 +29,9 @@ function getCrossDomainHandoffParamsFromCurrentUrl(currentUrl) {
|
|
|
29
29
|
function isRedirectBackAwareHandlerName(handlerName) {
|
|
30
30
|
return handlerName === "signIn" || handlerName === "signUp" || handlerName === "onboarding" || handlerName === "signOut";
|
|
31
31
|
}
|
|
32
|
+
function isContinuationAwareHandlerName(handlerName) {
|
|
33
|
+
return handlerName === "forgotPassword" || handlerName === "passwordReset";
|
|
34
|
+
}
|
|
32
35
|
function hasCrossDomainHandoffParams(url) {
|
|
33
36
|
return hasCrossDomainParam(url.searchParams, "state") && hasCrossDomainParam(url.searchParams, "codeChallenge") && hasCrossDomainParam(url.searchParams, "afterCallbackRedirectUrl");
|
|
34
37
|
}
|
|
@@ -47,12 +50,13 @@ function buildCrossDomainAuthCallbackUrl(options) {
|
|
|
47
50
|
function buildRedirectBackAwareHandlerUrl(options) {
|
|
48
51
|
const nextUrl = new URL(options.rawHandlerUrl, options.currentUrl);
|
|
49
52
|
const currentAfterAuthReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
50
|
-
if (currentAfterAuthReturnTo != null && !nextUrl.searchParams.has("after_auth_return_to")) nextUrl.searchParams.set("after_auth_return_to", currentAfterAuthReturnTo);
|
|
53
|
+
if (!options.noRedirectBack && currentAfterAuthReturnTo != null && !nextUrl.searchParams.has("after_auth_return_to")) nextUrl.searchParams.set("after_auth_return_to", currentAfterAuthReturnTo);
|
|
51
54
|
for (const preservedParam of [
|
|
52
55
|
"state",
|
|
53
56
|
"codeChallenge",
|
|
54
57
|
"afterCallbackRedirectUrl"
|
|
55
58
|
]) {
|
|
59
|
+
if (options.noRedirectBack && preservedParam === "afterCallbackRedirectUrl") continue;
|
|
56
60
|
const currentValue = getCrossDomainParam(options.currentUrl.searchParams, preservedParam);
|
|
57
61
|
if (currentValue != null && !hasCrossDomainParam(nextUrl.searchParams, preservedParam)) setCrossDomainParam(nextUrl.searchParams, preservedParam, currentValue);
|
|
58
62
|
}
|
|
@@ -64,27 +68,48 @@ function buildRedirectBackAwareHandlerUrl(options) {
|
|
|
64
68
|
if (options.currentUrl.origin !== nextUrl.origin) {
|
|
65
69
|
if (!hasCrossDomainHandoffParams(nextUrl)) {
|
|
66
70
|
const inheritedAfterAuthReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
67
|
-
const afterCallbackRedirectUrl = inheritedAfterAuthReturnTo ? new URL(inheritedAfterAuthReturnTo, options.currentUrl).toString() : options.currentUrl.toString();
|
|
71
|
+
const afterCallbackRedirectUrl = options.afterCallbackRedirectUrlOverride ?? (inheritedAfterAuthReturnTo ? new URL(inheritedAfterAuthReturnTo, options.currentUrl).toString() : options.currentUrl.toString());
|
|
72
|
+
const handoffParams = options.crossDomainHandoffParams ?? getCrossDomainHandoffParamsFromCurrentUrl(options.currentUrl);
|
|
68
73
|
const callbackUrl = buildCrossDomainAuthCallbackUrl({
|
|
69
74
|
currentUrl: options.currentUrl,
|
|
70
75
|
localOAuthCallbackUrl: options.localOAuthCallbackUrl,
|
|
71
|
-
state:
|
|
72
|
-
codeChallenge:
|
|
76
|
+
state: handoffParams?.state,
|
|
77
|
+
codeChallenge: handoffParams?.codeChallenge,
|
|
73
78
|
afterCallbackRedirectUrl
|
|
74
79
|
});
|
|
75
80
|
nextUrl.searchParams.set("after_auth_return_to", callbackUrl.toString());
|
|
76
81
|
setCrossDomainParam(nextUrl.searchParams, "afterCallbackRedirectUrl", afterCallbackRedirectUrl);
|
|
77
|
-
if (
|
|
78
|
-
setCrossDomainParam(nextUrl.searchParams, "state",
|
|
79
|
-
setCrossDomainParam(nextUrl.searchParams, "codeChallenge",
|
|
82
|
+
if (handoffParams != null) {
|
|
83
|
+
setCrossDomainParam(nextUrl.searchParams, "state", handoffParams.state);
|
|
84
|
+
setCrossDomainParam(nextUrl.searchParams, "codeChallenge", handoffParams.codeChallenge);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
} else if (options.currentUrl.protocol === nextUrl.protocol && options.currentUrl.host === nextUrl.host && !nextUrl.searchParams.has("after_auth_return_to")) nextUrl.searchParams.set("after_auth_return_to", (0, _hexclave_shared_dist_utils_urls.getRelativePart)(options.currentUrl));
|
|
87
|
+
} else if (!options.noRedirectBack && options.currentUrl.protocol === nextUrl.protocol && options.currentUrl.host === nextUrl.host && !nextUrl.searchParams.has("after_auth_return_to")) nextUrl.searchParams.set("after_auth_return_to", (0, _hexclave_shared_dist_utils_urls.getRelativePart)(options.currentUrl));
|
|
88
|
+
return nextUrl.origin === options.currentUrl.origin ? (0, _hexclave_shared_dist_utils_urls.getRelativePart)(nextUrl) : nextUrl.toString();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Carries an auth flow's existing return state without ever making the current page the return
|
|
92
|
+
* target. This distinction is important for password flows: forgot-password and password-reset
|
|
93
|
+
* are continuation pages, not destinations customers should return to after signing in.
|
|
94
|
+
*/
|
|
95
|
+
function buildContinuationAwareHandlerUrl(options) {
|
|
96
|
+
const nextUrl = new URL(options.rawHandlerUrl, options.currentUrl);
|
|
97
|
+
const inheritedAfterAuthReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
98
|
+
if (inheritedAfterAuthReturnTo != null && !nextUrl.searchParams.has("after_auth_return_to")) nextUrl.searchParams.set("after_auth_return_to", inheritedAfterAuthReturnTo);
|
|
99
|
+
for (const preservedParam of [
|
|
100
|
+
"state",
|
|
101
|
+
"codeChallenge",
|
|
102
|
+
"afterCallbackRedirectUrl"
|
|
103
|
+
]) {
|
|
104
|
+
const currentValue = getCrossDomainParam(options.currentUrl.searchParams, preservedParam);
|
|
105
|
+
if (currentValue != null && !hasCrossDomainParam(nextUrl.searchParams, preservedParam)) setCrossDomainParam(nextUrl.searchParams, preservedParam, currentValue);
|
|
106
|
+
}
|
|
83
107
|
return nextUrl.origin === options.currentUrl.origin ? (0, _hexclave_shared_dist_utils_urls.getRelativePart)(nextUrl) : nextUrl.toString();
|
|
84
108
|
}
|
|
85
109
|
function getHandlerRedirectPolicy(handlerName) {
|
|
86
110
|
if (handlerName === "afterSignIn" || handlerName === "afterSignUp") return "after-auth-return";
|
|
87
111
|
if (isRedirectBackAwareHandlerName(handlerName)) return "redirect-back-aware";
|
|
112
|
+
if (isContinuationAwareHandlerName(handlerName)) return "continuation-aware";
|
|
88
113
|
return "none";
|
|
89
114
|
}
|
|
90
115
|
async function resolveRedirectBackAwareHandlerUrlForRedirect(options) {
|
|
@@ -93,7 +118,9 @@ async function resolveRedirectBackAwareHandlerUrlForRedirect(options) {
|
|
|
93
118
|
rawHandlerUrl: options.rawHandlerUrl,
|
|
94
119
|
currentUrl: options.currentUrl,
|
|
95
120
|
crossDomainHandoffParams: null,
|
|
96
|
-
localOAuthCallbackUrl: options.localOAuthCallbackUrl
|
|
121
|
+
localOAuthCallbackUrl: options.localOAuthCallbackUrl,
|
|
122
|
+
noRedirectBack: options.noRedirectBack,
|
|
123
|
+
afterCallbackRedirectUrlOverride: options.afterCallbackRedirectUrlOverride
|
|
97
124
|
});
|
|
98
125
|
if (options.handlerName === "signOut") return initial;
|
|
99
126
|
const initialTarget = new URL(initial, options.currentUrl);
|
|
@@ -104,20 +131,45 @@ async function resolveRedirectBackAwareHandlerUrlForRedirect(options) {
|
|
|
104
131
|
rawHandlerUrl: options.rawHandlerUrl,
|
|
105
132
|
currentUrl: options.currentUrl,
|
|
106
133
|
crossDomainHandoffParams,
|
|
107
|
-
localOAuthCallbackUrl: options.localOAuthCallbackUrl
|
|
134
|
+
localOAuthCallbackUrl: options.localOAuthCallbackUrl,
|
|
135
|
+
noRedirectBack: options.noRedirectBack,
|
|
136
|
+
afterCallbackRedirectUrlOverride: options.afterCallbackRedirectUrlOverride
|
|
108
137
|
});
|
|
109
138
|
}
|
|
110
139
|
async function planRedirectToHandler(options) {
|
|
111
|
-
if (options.
|
|
140
|
+
if (options.currentUrl == null) return {
|
|
112
141
|
type: "redirect",
|
|
113
142
|
url: options.rawHandlerUrl
|
|
114
143
|
};
|
|
115
144
|
const policy = getHandlerRedirectPolicy(options.handlerName);
|
|
145
|
+
if (options.noRedirectBack) if (options.handlerName === "signIn") {
|
|
146
|
+
if (new URL(options.rawHandlerUrl, options.currentUrl).origin === options.currentUrl.origin) return {
|
|
147
|
+
type: "redirect",
|
|
148
|
+
url: buildContinuationAwareHandlerUrl({
|
|
149
|
+
rawHandlerUrl: options.rawHandlerUrl,
|
|
150
|
+
currentUrl: options.currentUrl
|
|
151
|
+
})
|
|
152
|
+
};
|
|
153
|
+
} else return {
|
|
154
|
+
type: "redirect",
|
|
155
|
+
url: options.rawHandlerUrl
|
|
156
|
+
};
|
|
116
157
|
if (policy === "none") return {
|
|
117
158
|
type: "redirect",
|
|
118
159
|
url: options.rawHandlerUrl
|
|
119
160
|
};
|
|
161
|
+
if (policy === "continuation-aware") return {
|
|
162
|
+
type: "redirect",
|
|
163
|
+
url: buildContinuationAwareHandlerUrl({
|
|
164
|
+
rawHandlerUrl: options.rawHandlerUrl,
|
|
165
|
+
currentUrl: options.currentUrl
|
|
166
|
+
})
|
|
167
|
+
};
|
|
120
168
|
if (policy === "after-auth-return") {
|
|
169
|
+
if (options.noRedirectBack) return {
|
|
170
|
+
type: "redirect",
|
|
171
|
+
url: options.rawHandlerUrl
|
|
172
|
+
};
|
|
121
173
|
const redirectBackUrl = options.currentUrl.searchParams.get("after_auth_return_to");
|
|
122
174
|
if (redirectBackUrl == null) return {
|
|
123
175
|
type: "redirect",
|
|
@@ -153,6 +205,7 @@ async function planRedirectToHandler(options) {
|
|
|
153
205
|
handlerName: options.handlerName,
|
|
154
206
|
policy
|
|
155
207
|
});
|
|
208
|
+
const afterCallbackRedirectUrlOverride = options.noRedirectBack ? new URL(options.rawHomeUrl, options.currentUrl).toString() : null;
|
|
156
209
|
return {
|
|
157
210
|
type: "redirect",
|
|
158
211
|
url: await resolveRedirectBackAwareHandlerUrlForRedirect({
|
|
@@ -160,7 +213,9 @@ async function planRedirectToHandler(options) {
|
|
|
160
213
|
rawHandlerUrl: options.rawHandlerUrl,
|
|
161
214
|
currentUrl: options.currentUrl,
|
|
162
215
|
localOAuthCallbackUrl: options.localOAuthCallbackUrl,
|
|
163
|
-
getCrossDomainHandoffParams: options.getCrossDomainHandoffParams
|
|
216
|
+
getCrossDomainHandoffParams: options.getCrossDomainHandoffParams,
|
|
217
|
+
noRedirectBack: options.noRedirectBack,
|
|
218
|
+
afterCallbackRedirectUrlOverride
|
|
164
219
|
})
|
|
165
220
|
};
|
|
166
221
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect-page-urls.js","names":["HexclaveAssertionError"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/redirect-page-urls.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { HexclaveAssertionError } from \"@hexclave/shared/dist/utils/errors\";\nimport { getRelativePart } from \"@hexclave/shared/dist/utils/urls\";\nimport { HandlerUrls } from \"../../common\";\n\nexport const crossDomainAuthQueryParams = {\n marker: \"hexclave_cross_domain_auth\",\n state: \"hexclave_cross_domain_state\",\n codeChallenge: \"hexclave_cross_domain_code_challenge\",\n afterCallbackRedirectUrl: \"hexclave_cross_domain_after_callback_redirect_url\",\n} as const;\n\ntype CrossDomainAuthQueryParamKey = keyof typeof crossDomainAuthQueryParams;\n\nfunction getCrossDomainParam(params: URLSearchParams, key: CrossDomainAuthQueryParamKey): string | null {\n return params.get(crossDomainAuthQueryParams[key]);\n}\n\nfunction hasCrossDomainParam(params: URLSearchParams, key: CrossDomainAuthQueryParamKey): boolean {\n return params.has(crossDomainAuthQueryParams[key]);\n}\n\nfunction setCrossDomainParam(params: URLSearchParams, key: CrossDomainAuthQueryParamKey, value: string): void {\n params.set(crossDomainAuthQueryParams[key], value);\n}\n\nexport type CrossDomainHandoffParams = {\n state: string,\n codeChallenge: string,\n};\n\nexport function getCrossDomainHandoffParamsFromCurrentUrl(currentUrl: URL): CrossDomainHandoffParams | null {\n const state = getCrossDomainParam(currentUrl.searchParams, \"state\");\n const codeChallenge = getCrossDomainParam(currentUrl.searchParams, \"codeChallenge\");\n if (state == null || codeChallenge == null) {\n return null;\n }\n return { state, codeChallenge };\n}\n\ntype RedirectBackAwareHandlerName = \"signIn\" | \"signUp\" | \"onboarding\" | \"signOut\";\ntype HandlerRedirectPolicy = \"none\" | \"redirect-back-aware\" | \"after-auth-return\";\n\ntype CrossDomainHandoffParamsMaybeMissing = {\n state: string | null,\n codeChallenge: string | null,\n afterCallbackRedirectUrl: string | null,\n};\n\nfunction isRedirectBackAwareHandlerName(handlerName: keyof HandlerUrls): handlerName is RedirectBackAwareHandlerName {\n return handlerName === \"signIn\"\n || handlerName === \"signUp\"\n || handlerName === \"onboarding\"\n || handlerName === \"signOut\";\n}\n\nfunction hasCrossDomainHandoffParams(url: URL): boolean {\n return (\n hasCrossDomainParam(url.searchParams, \"state\")\n && hasCrossDomainParam(url.searchParams, \"codeChallenge\")\n && hasCrossDomainParam(url.searchParams, \"afterCallbackRedirectUrl\")\n );\n}\n\nfunction buildCrossDomainAuthCallbackUrl(options: {\n currentUrl: URL,\n localOAuthCallbackUrl: string,\n state?: string,\n codeChallenge?: string,\n afterCallbackRedirectUrl?: string,\n}): URL {\n const localOAuthCallbackUrl = new URL(options.localOAuthCallbackUrl, options.currentUrl);\n if (localOAuthCallbackUrl.origin !== options.currentUrl.origin) {\n throw new HexclaveAssertionError(\"Cross-domain auth callback URL must stay on the current origin\", {\n localOAuthCallbackUrl: localOAuthCallbackUrl.toString(),\n currentUrl: options.currentUrl.toString(),\n });\n }\n setCrossDomainParam(localOAuthCallbackUrl.searchParams, \"marker\", \"1\");\n if (options.state != null) {\n setCrossDomainParam(localOAuthCallbackUrl.searchParams, \"state\", options.state);\n }\n if (options.codeChallenge != null) {\n setCrossDomainParam(localOAuthCallbackUrl.searchParams, \"codeChallenge\", options.codeChallenge);\n }\n if (options.afterCallbackRedirectUrl != null) {\n setCrossDomainParam(localOAuthCallbackUrl.searchParams, \"afterCallbackRedirectUrl\", options.afterCallbackRedirectUrl);\n }\n return localOAuthCallbackUrl;\n}\n\nfunction buildRedirectBackAwareHandlerUrl(options: {\n handlerName: RedirectBackAwareHandlerName,\n rawHandlerUrl: string,\n currentUrl: URL,\n crossDomainHandoffParams: CrossDomainHandoffParams | null,\n localOAuthCallbackUrl: string,\n}): string {\n const nextUrl = new URL(options.rawHandlerUrl, options.currentUrl);\n // Preserve after_auth_return_to verbatim (not a rebranded param).\n const currentAfterAuthReturnTo = options.currentUrl.searchParams.get(\"after_auth_return_to\");\n if (currentAfterAuthReturnTo != null && !nextUrl.searchParams.has(\"after_auth_return_to\")) {\n nextUrl.searchParams.set(\"after_auth_return_to\", currentAfterAuthReturnTo);\n }\n for (const preservedParam of [\"state\", \"codeChallenge\", \"afterCallbackRedirectUrl\"] as const) {\n const currentValue = getCrossDomainParam(options.currentUrl.searchParams, preservedParam);\n if (currentValue != null && !hasCrossDomainParam(nextUrl.searchParams, preservedParam)) {\n setCrossDomainParam(nextUrl.searchParams, preservedParam, currentValue);\n }\n }\n\n if (options.handlerName === \"signOut\") {\n if (!nextUrl.searchParams.has(\"after_auth_return_to\")) {\n if (options.currentUrl.protocol === nextUrl.protocol && options.currentUrl.host === nextUrl.host) {\n nextUrl.searchParams.set(\"after_auth_return_to\", getRelativePart(options.currentUrl));\n } else {\n nextUrl.searchParams.set(\"after_auth_return_to\", options.currentUrl.toString());\n }\n }\n return nextUrl.origin === options.currentUrl.origin ? getRelativePart(nextUrl) : nextUrl.toString();\n }\n\n const isCrossDomainHandlerRedirect = options.currentUrl.origin !== nextUrl.origin;\n if (isCrossDomainHandlerRedirect) {\n if (!hasCrossDomainHandoffParams(nextUrl)) {\n const inheritedAfterAuthReturnTo = options.currentUrl.searchParams.get(\"after_auth_return_to\");\n const afterCallbackRedirectUrl = inheritedAfterAuthReturnTo\n ? new URL(inheritedAfterAuthReturnTo, options.currentUrl).toString()\n : options.currentUrl.toString();\n const callbackUrl = buildCrossDomainAuthCallbackUrl({\n currentUrl: options.currentUrl,\n localOAuthCallbackUrl: options.localOAuthCallbackUrl,\n state: options.crossDomainHandoffParams?.state,\n codeChallenge: options.crossDomainHandoffParams?.codeChallenge,\n afterCallbackRedirectUrl,\n });\n\n nextUrl.searchParams.set(\"after_auth_return_to\", callbackUrl.toString());\n setCrossDomainParam(nextUrl.searchParams, \"afterCallbackRedirectUrl\", afterCallbackRedirectUrl);\n if (options.crossDomainHandoffParams != null) {\n setCrossDomainParam(nextUrl.searchParams, \"state\", options.crossDomainHandoffParams.state);\n setCrossDomainParam(nextUrl.searchParams, \"codeChallenge\", options.crossDomainHandoffParams.codeChallenge);\n }\n }\n } else if (options.currentUrl.protocol === nextUrl.protocol && options.currentUrl.host === nextUrl.host && !nextUrl.searchParams.has(\"after_auth_return_to\")) {\n nextUrl.searchParams.set(\"after_auth_return_to\", getRelativePart(options.currentUrl));\n }\n\n return nextUrl.origin === options.currentUrl.origin ? getRelativePart(nextUrl) : nextUrl.toString();\n}\n\nfunction getHandlerRedirectPolicy(handlerName: keyof HandlerUrls): HandlerRedirectPolicy {\n if (handlerName === \"afterSignIn\" || handlerName === \"afterSignUp\") {\n return \"after-auth-return\";\n }\n if (isRedirectBackAwareHandlerName(handlerName)) {\n return \"redirect-back-aware\";\n }\n return \"none\";\n}\n\ntype RedirectToHandlerPlan =\n | { type: \"redirect\", url: string }\n | {\n type: \"cross-domain-authorize\",\n redirectUri: string,\n state: string,\n codeChallenge: string,\n afterCallbackRedirectUrl: string,\n };\n\n\nasync function resolveRedirectBackAwareHandlerUrlForRedirect(options: {\n handlerName: RedirectBackAwareHandlerName,\n rawHandlerUrl: string,\n currentUrl: URL,\n localOAuthCallbackUrl: string,\n getCrossDomainHandoffParams: (currentUrl: URL) => Promise<CrossDomainHandoffParams>,\n}): Promise<string> {\n const initial = buildRedirectBackAwareHandlerUrl({\n handlerName: options.handlerName,\n rawHandlerUrl: options.rawHandlerUrl,\n currentUrl: options.currentUrl,\n crossDomainHandoffParams: null,\n localOAuthCallbackUrl: options.localOAuthCallbackUrl,\n });\n if (options.handlerName === \"signOut\") {\n return initial;\n }\n\n const initialTarget = new URL(initial, options.currentUrl);\n const isCrossDomainHandlerRedirect = options.currentUrl.origin !== initialTarget.origin;\n if (!isCrossDomainHandlerRedirect || hasCrossDomainHandoffParams(initialTarget)) {\n return initial;\n }\n\n const crossDomainHandoffParams = await options.getCrossDomainHandoffParams(options.currentUrl);\n return buildRedirectBackAwareHandlerUrl({\n handlerName: options.handlerName,\n rawHandlerUrl: options.rawHandlerUrl,\n currentUrl: options.currentUrl,\n crossDomainHandoffParams,\n localOAuthCallbackUrl: options.localOAuthCallbackUrl,\n });\n}\n\nexport async function planRedirectToHandler(options: {\n handlerName: keyof HandlerUrls,\n rawHandlerUrl: string,\n noRedirectBack: boolean,\n currentUrl: URL | null,\n localOAuthCallbackUrl: string,\n getCrossDomainHandoffParams: (currentUrl: URL) => Promise<CrossDomainHandoffParams>,\n}): Promise<RedirectToHandlerPlan> {\n if (options.noRedirectBack || options.currentUrl == null) {\n return { type: \"redirect\", url: options.rawHandlerUrl };\n }\n\n const policy = getHandlerRedirectPolicy(options.handlerName);\n if (policy === \"none\") {\n return { type: \"redirect\", url: options.rawHandlerUrl };\n }\n\n if (policy === \"after-auth-return\") {\n const redirectBackUrl = options.currentUrl.searchParams.get(\"after_auth_return_to\");\n if (redirectBackUrl == null) {\n return { type: \"redirect\", url: options.rawHandlerUrl };\n }\n const redirectBackTarget = new URL(redirectBackUrl, options.currentUrl);\n const crossDomainHandoff = getCrossDomainHandoffForRedirect({\n currentUrl: options.currentUrl,\n redirectBackTarget,\n });\n if (crossDomainHandoff == null) {\n return { type: \"redirect\", url: redirectBackUrl };\n }\n let state = crossDomainHandoff.handoffParams.state;\n let codeChallenge = crossDomainHandoff.handoffParams.codeChallenge;\n let afterCallbackRedirectUrl = crossDomainHandoff.handoffParams.afterCallbackRedirectUrl;\n if (state == null || codeChallenge == null) {\n const generatedHandoffParams = await options.getCrossDomainHandoffParams(options.currentUrl);\n state ??= generatedHandoffParams.state;\n codeChallenge ??= generatedHandoffParams.codeChallenge;\n }\n afterCallbackRedirectUrl ??= options.currentUrl.toString();\n return {\n type: \"cross-domain-authorize\",\n redirectUri: crossDomainHandoff.redirectBackTarget.toString(),\n state,\n codeChallenge,\n afterCallbackRedirectUrl,\n };\n }\n\n if (\n options.handlerName !== \"signIn\"\n && options.handlerName !== \"signUp\"\n && options.handlerName !== \"onboarding\"\n && options.handlerName !== \"signOut\"\n ) {\n throw new HexclaveAssertionError(\"Unexpected redirect-back-aware handler policy mismatch\", {\n handlerName: options.handlerName,\n policy,\n });\n }\n\n return {\n type: \"redirect\",\n url: await resolveRedirectBackAwareHandlerUrlForRedirect({\n handlerName: options.handlerName,\n rawHandlerUrl: options.rawHandlerUrl,\n currentUrl: options.currentUrl,\n localOAuthCallbackUrl: options.localOAuthCallbackUrl,\n getCrossDomainHandoffParams: options.getCrossDomainHandoffParams,\n }),\n };\n}\n\nfunction readCrossDomainHandoffParams(currentUrl: URL, redirectBackTarget: URL): CrossDomainHandoffParamsMaybeMissing {\n // Hexclave rebrand: accept either param name from both URLs.\n const state = getCrossDomainParam(currentUrl.searchParams, \"state\")\n ?? getCrossDomainParam(redirectBackTarget.searchParams, \"state\");\n const codeChallenge = getCrossDomainParam(currentUrl.searchParams, \"codeChallenge\")\n ?? getCrossDomainParam(redirectBackTarget.searchParams, \"codeChallenge\");\n const afterCallbackRedirectUrl = getCrossDomainParam(currentUrl.searchParams, \"afterCallbackRedirectUrl\")\n ?? getCrossDomainParam(redirectBackTarget.searchParams, \"afterCallbackRedirectUrl\");\n return {\n state,\n codeChallenge,\n afterCallbackRedirectUrl,\n };\n}\n\nfunction resolveCrossDomainRedirectBackTarget(options: {\n currentUrl: URL,\n redirectBackTarget: URL,\n handoffParams: CrossDomainHandoffParamsMaybeMissing,\n}): URL | null {\n if (options.redirectBackTarget.origin !== options.currentUrl.origin) {\n return options.redirectBackTarget;\n }\n if (\n options.handoffParams.state == null\n || options.handoffParams.codeChallenge == null\n || options.handoffParams.afterCallbackRedirectUrl == null\n ) {\n return null;\n }\n const afterCallbackRedirectTarget = new URL(options.handoffParams.afterCallbackRedirectUrl, options.currentUrl);\n if (afterCallbackRedirectTarget.origin === options.currentUrl.origin) {\n return null;\n }\n return new URL(\n `${options.redirectBackTarget.pathname}${options.redirectBackTarget.search}${options.redirectBackTarget.hash}`,\n afterCallbackRedirectTarget.origin,\n );\n}\n\nfunction getCrossDomainHandoffForRedirect(options: {\n currentUrl: URL,\n redirectBackTarget: URL,\n}): {\n redirectBackTarget: URL,\n handoffParams: CrossDomainHandoffParamsMaybeMissing,\n} | null {\n const handoffParams = readCrossDomainHandoffParams(options.currentUrl, options.redirectBackTarget);\n const crossDomainRedirectBackTarget = resolveCrossDomainRedirectBackTarget({\n currentUrl: options.currentUrl,\n redirectBackTarget: options.redirectBackTarget,\n handoffParams,\n });\n if (crossDomainRedirectBackTarget == null) {\n return null;\n }\n return {\n redirectBackTarget: crossDomainRedirectBackTarget,\n handoffParams,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"redirect-page-urls.js","names":["HexclaveAssertionError"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/redirect-page-urls.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { HexclaveAssertionError } from \"@hexclave/shared/dist/utils/errors\";\nimport { getRelativePart } from \"@hexclave/shared/dist/utils/urls\";\nimport { HandlerUrls } from \"../../common\";\n\nexport const crossDomainAuthQueryParams = {\n marker: \"hexclave_cross_domain_auth\",\n state: \"hexclave_cross_domain_state\",\n codeChallenge: \"hexclave_cross_domain_code_challenge\",\n afterCallbackRedirectUrl: \"hexclave_cross_domain_after_callback_redirect_url\",\n} as const;\n\ntype CrossDomainAuthQueryParamKey = keyof typeof crossDomainAuthQueryParams;\n\nfunction getCrossDomainParam(params: URLSearchParams, key: CrossDomainAuthQueryParamKey): string | null {\n return params.get(crossDomainAuthQueryParams[key]);\n}\n\nfunction hasCrossDomainParam(params: URLSearchParams, key: CrossDomainAuthQueryParamKey): boolean {\n return params.has(crossDomainAuthQueryParams[key]);\n}\n\nfunction setCrossDomainParam(params: URLSearchParams, key: CrossDomainAuthQueryParamKey, value: string): void {\n params.set(crossDomainAuthQueryParams[key], value);\n}\n\nexport type CrossDomainHandoffParams = {\n state: string,\n codeChallenge: string,\n};\n\nexport function getCrossDomainHandoffParamsFromCurrentUrl(currentUrl: URL): CrossDomainHandoffParams | null {\n const state = getCrossDomainParam(currentUrl.searchParams, \"state\");\n const codeChallenge = getCrossDomainParam(currentUrl.searchParams, \"codeChallenge\");\n if (state == null || codeChallenge == null) {\n return null;\n }\n return { state, codeChallenge };\n}\n\ntype RedirectBackAwareHandlerName = \"signIn\" | \"signUp\" | \"onboarding\" | \"signOut\";\ntype ContinuationAwareHandlerName = \"forgotPassword\" | \"passwordReset\";\ntype HandlerRedirectPolicy = \"none\" | \"redirect-back-aware\" | \"continuation-aware\" | \"after-auth-return\";\n\ntype CrossDomainHandoffParamsMaybeMissing = {\n state: string | null,\n codeChallenge: string | null,\n afterCallbackRedirectUrl: string | null,\n};\n\nfunction isRedirectBackAwareHandlerName(handlerName: keyof HandlerUrls): handlerName is RedirectBackAwareHandlerName {\n return handlerName === \"signIn\"\n || handlerName === \"signUp\"\n || handlerName === \"onboarding\"\n || handlerName === \"signOut\";\n}\n\nfunction isContinuationAwareHandlerName(handlerName: keyof HandlerUrls): handlerName is ContinuationAwareHandlerName {\n return handlerName === \"forgotPassword\" || handlerName === \"passwordReset\";\n}\n\nfunction hasCrossDomainHandoffParams(url: URL): boolean {\n return (\n hasCrossDomainParam(url.searchParams, \"state\")\n && hasCrossDomainParam(url.searchParams, \"codeChallenge\")\n && hasCrossDomainParam(url.searchParams, \"afterCallbackRedirectUrl\")\n );\n}\n\nfunction buildCrossDomainAuthCallbackUrl(options: {\n currentUrl: URL,\n localOAuthCallbackUrl: string,\n state?: string,\n codeChallenge?: string,\n afterCallbackRedirectUrl?: string,\n}): URL {\n const localOAuthCallbackUrl = new URL(options.localOAuthCallbackUrl, options.currentUrl);\n if (localOAuthCallbackUrl.origin !== options.currentUrl.origin) {\n throw new HexclaveAssertionError(\"Cross-domain auth callback URL must stay on the current origin\", {\n localOAuthCallbackUrl: localOAuthCallbackUrl.toString(),\n currentUrl: options.currentUrl.toString(),\n });\n }\n setCrossDomainParam(localOAuthCallbackUrl.searchParams, \"marker\", \"1\");\n if (options.state != null) {\n setCrossDomainParam(localOAuthCallbackUrl.searchParams, \"state\", options.state);\n }\n if (options.codeChallenge != null) {\n setCrossDomainParam(localOAuthCallbackUrl.searchParams, \"codeChallenge\", options.codeChallenge);\n }\n if (options.afterCallbackRedirectUrl != null) {\n setCrossDomainParam(localOAuthCallbackUrl.searchParams, \"afterCallbackRedirectUrl\", options.afterCallbackRedirectUrl);\n }\n return localOAuthCallbackUrl;\n}\n\nfunction buildRedirectBackAwareHandlerUrl(options: {\n handlerName: RedirectBackAwareHandlerName,\n rawHandlerUrl: string,\n currentUrl: URL,\n crossDomainHandoffParams: CrossDomainHandoffParams | null,\n localOAuthCallbackUrl: string,\n noRedirectBack: boolean,\n afterCallbackRedirectUrlOverride: string | null,\n}): string {\n const nextUrl = new URL(options.rawHandlerUrl, options.currentUrl);\n // Preserve after_auth_return_to verbatim (not a rebranded param).\n const currentAfterAuthReturnTo = options.currentUrl.searchParams.get(\"after_auth_return_to\");\n if (!options.noRedirectBack && currentAfterAuthReturnTo != null && !nextUrl.searchParams.has(\"after_auth_return_to\")) {\n nextUrl.searchParams.set(\"after_auth_return_to\", currentAfterAuthReturnTo);\n }\n for (const preservedParam of [\"state\", \"codeChallenge\", \"afterCallbackRedirectUrl\"] as const) {\n if (options.noRedirectBack && preservedParam === \"afterCallbackRedirectUrl\") {\n continue;\n }\n const currentValue = getCrossDomainParam(options.currentUrl.searchParams, preservedParam);\n if (currentValue != null && !hasCrossDomainParam(nextUrl.searchParams, preservedParam)) {\n setCrossDomainParam(nextUrl.searchParams, preservedParam, currentValue);\n }\n }\n\n if (options.handlerName === \"signOut\") {\n if (!nextUrl.searchParams.has(\"after_auth_return_to\")) {\n if (options.currentUrl.protocol === nextUrl.protocol && options.currentUrl.host === nextUrl.host) {\n nextUrl.searchParams.set(\"after_auth_return_to\", getRelativePart(options.currentUrl));\n } else {\n nextUrl.searchParams.set(\"after_auth_return_to\", options.currentUrl.toString());\n }\n }\n return nextUrl.origin === options.currentUrl.origin ? getRelativePart(nextUrl) : nextUrl.toString();\n }\n\n const isCrossDomainHandlerRedirect = options.currentUrl.origin !== nextUrl.origin;\n if (isCrossDomainHandlerRedirect) {\n if (!hasCrossDomainHandoffParams(nextUrl)) {\n const inheritedAfterAuthReturnTo = options.currentUrl.searchParams.get(\"after_auth_return_to\");\n const afterCallbackRedirectUrl = options.afterCallbackRedirectUrlOverride ?? (\n inheritedAfterAuthReturnTo\n ? new URL(inheritedAfterAuthReturnTo, options.currentUrl).toString()\n : options.currentUrl.toString()\n );\n const handoffParams = options.crossDomainHandoffParams\n ?? getCrossDomainHandoffParamsFromCurrentUrl(options.currentUrl);\n const callbackUrl = buildCrossDomainAuthCallbackUrl({\n currentUrl: options.currentUrl,\n localOAuthCallbackUrl: options.localOAuthCallbackUrl,\n state: handoffParams?.state,\n codeChallenge: handoffParams?.codeChallenge,\n afterCallbackRedirectUrl,\n });\n\n nextUrl.searchParams.set(\"after_auth_return_to\", callbackUrl.toString());\n setCrossDomainParam(nextUrl.searchParams, \"afterCallbackRedirectUrl\", afterCallbackRedirectUrl);\n if (handoffParams != null) {\n setCrossDomainParam(nextUrl.searchParams, \"state\", handoffParams.state);\n setCrossDomainParam(nextUrl.searchParams, \"codeChallenge\", handoffParams.codeChallenge);\n }\n }\n } else if (!options.noRedirectBack && options.currentUrl.protocol === nextUrl.protocol && options.currentUrl.host === nextUrl.host && !nextUrl.searchParams.has(\"after_auth_return_to\")) {\n nextUrl.searchParams.set(\"after_auth_return_to\", getRelativePart(options.currentUrl));\n }\n\n return nextUrl.origin === options.currentUrl.origin ? getRelativePart(nextUrl) : nextUrl.toString();\n}\n\n/**\n * Carries an auth flow's existing return state without ever making the current page the return\n * target. This distinction is important for password flows: forgot-password and password-reset\n * are continuation pages, not destinations customers should return to after signing in.\n */\nfunction buildContinuationAwareHandlerUrl(options: {\n rawHandlerUrl: string,\n currentUrl: URL,\n}): string {\n const nextUrl = new URL(options.rawHandlerUrl, options.currentUrl);\n const inheritedAfterAuthReturnTo = options.currentUrl.searchParams.get(\"after_auth_return_to\");\n if (inheritedAfterAuthReturnTo != null && !nextUrl.searchParams.has(\"after_auth_return_to\")) {\n nextUrl.searchParams.set(\"after_auth_return_to\", inheritedAfterAuthReturnTo);\n }\n for (const preservedParam of [\"state\", \"codeChallenge\", \"afterCallbackRedirectUrl\"] as const) {\n const currentValue = getCrossDomainParam(options.currentUrl.searchParams, preservedParam);\n if (currentValue != null && !hasCrossDomainParam(nextUrl.searchParams, preservedParam)) {\n setCrossDomainParam(nextUrl.searchParams, preservedParam, currentValue);\n }\n }\n return nextUrl.origin === options.currentUrl.origin ? getRelativePart(nextUrl) : nextUrl.toString();\n}\n\nfunction getHandlerRedirectPolicy(handlerName: keyof HandlerUrls): HandlerRedirectPolicy {\n if (handlerName === \"afterSignIn\" || handlerName === \"afterSignUp\") {\n return \"after-auth-return\";\n }\n if (isRedirectBackAwareHandlerName(handlerName)) {\n return \"redirect-back-aware\";\n }\n if (isContinuationAwareHandlerName(handlerName)) {\n return \"continuation-aware\";\n }\n return \"none\";\n}\n\ntype RedirectToHandlerPlan =\n | { type: \"redirect\", url: string }\n | {\n type: \"cross-domain-authorize\",\n redirectUri: string,\n state: string,\n codeChallenge: string,\n afterCallbackRedirectUrl: string,\n };\n\n\nasync function resolveRedirectBackAwareHandlerUrlForRedirect(options: {\n handlerName: RedirectBackAwareHandlerName,\n rawHandlerUrl: string,\n currentUrl: URL,\n localOAuthCallbackUrl: string,\n getCrossDomainHandoffParams: (currentUrl: URL) => Promise<CrossDomainHandoffParams>,\n noRedirectBack: boolean,\n afterCallbackRedirectUrlOverride: string | null,\n}): Promise<string> {\n const initial = buildRedirectBackAwareHandlerUrl({\n handlerName: options.handlerName,\n rawHandlerUrl: options.rawHandlerUrl,\n currentUrl: options.currentUrl,\n crossDomainHandoffParams: null,\n localOAuthCallbackUrl: options.localOAuthCallbackUrl,\n noRedirectBack: options.noRedirectBack,\n afterCallbackRedirectUrlOverride: options.afterCallbackRedirectUrlOverride,\n });\n if (options.handlerName === \"signOut\") {\n return initial;\n }\n\n const initialTarget = new URL(initial, options.currentUrl);\n const isCrossDomainHandlerRedirect = options.currentUrl.origin !== initialTarget.origin;\n if (!isCrossDomainHandlerRedirect || hasCrossDomainHandoffParams(initialTarget)) {\n return initial;\n }\n\n const crossDomainHandoffParams = await options.getCrossDomainHandoffParams(options.currentUrl);\n return buildRedirectBackAwareHandlerUrl({\n handlerName: options.handlerName,\n rawHandlerUrl: options.rawHandlerUrl,\n currentUrl: options.currentUrl,\n crossDomainHandoffParams,\n localOAuthCallbackUrl: options.localOAuthCallbackUrl,\n noRedirectBack: options.noRedirectBack,\n afterCallbackRedirectUrlOverride: options.afterCallbackRedirectUrlOverride,\n });\n}\n\nexport async function planRedirectToHandler(options: {\n handlerName: keyof HandlerUrls,\n rawHandlerUrl: string,\n noRedirectBack: boolean,\n currentUrl: URL | null,\n localOAuthCallbackUrl: string,\n rawHomeUrl: string,\n getCrossDomainHandoffParams: (currentUrl: URL) => Promise<CrossDomainHandoffParams>,\n}): Promise<RedirectToHandlerPlan> {\n if (options.currentUrl == null) {\n return { type: \"redirect\", url: options.rawHandlerUrl };\n }\n\n const policy = getHandlerRedirectPolicy(options.handlerName);\n if (options.noRedirectBack) {\n // Sign-in uses noRedirectBack from password-reset pages to avoid capturing the reset page.\n // Same-domain sign-in must still carry an already-inherited continuation. Cross-domain\n // sign-in falls through to the redirect-back-aware planner below because it needs to return\n // through the source app's callback before continuing to home.\n if (options.handlerName === \"signIn\") {\n const signInTarget = new URL(options.rawHandlerUrl, options.currentUrl);\n if (signInTarget.origin === options.currentUrl.origin) {\n return {\n type: \"redirect\",\n url: buildContinuationAwareHandlerUrl({\n rawHandlerUrl: options.rawHandlerUrl,\n currentUrl: options.currentUrl,\n }),\n };\n }\n } else {\n // Other handlers retain the historical noRedirectBack behavior, including\n // afterSignIn/afterSignUp ignoring all return state.\n return {\n type: \"redirect\",\n url: options.rawHandlerUrl,\n };\n }\n }\n if (policy === \"none\") {\n return { type: \"redirect\", url: options.rawHandlerUrl };\n }\n if (policy === \"continuation-aware\") {\n return {\n type: \"redirect\",\n url: buildContinuationAwareHandlerUrl({\n rawHandlerUrl: options.rawHandlerUrl,\n currentUrl: options.currentUrl,\n }),\n };\n }\n\n if (policy === \"after-auth-return\") {\n if (options.noRedirectBack) {\n return { type: \"redirect\", url: options.rawHandlerUrl };\n }\n const redirectBackUrl = options.currentUrl.searchParams.get(\"after_auth_return_to\");\n if (redirectBackUrl == null) {\n return { type: \"redirect\", url: options.rawHandlerUrl };\n }\n const redirectBackTarget = new URL(redirectBackUrl, options.currentUrl);\n const crossDomainHandoff = getCrossDomainHandoffForRedirect({\n currentUrl: options.currentUrl,\n redirectBackTarget,\n });\n if (crossDomainHandoff == null) {\n return { type: \"redirect\", url: redirectBackUrl };\n }\n let state = crossDomainHandoff.handoffParams.state;\n let codeChallenge = crossDomainHandoff.handoffParams.codeChallenge;\n let afterCallbackRedirectUrl = crossDomainHandoff.handoffParams.afterCallbackRedirectUrl;\n if (state == null || codeChallenge == null) {\n const generatedHandoffParams = await options.getCrossDomainHandoffParams(options.currentUrl);\n state ??= generatedHandoffParams.state;\n codeChallenge ??= generatedHandoffParams.codeChallenge;\n }\n afterCallbackRedirectUrl ??= options.currentUrl.toString();\n return {\n type: \"cross-domain-authorize\",\n redirectUri: crossDomainHandoff.redirectBackTarget.toString(),\n state,\n codeChallenge,\n afterCallbackRedirectUrl,\n };\n }\n\n if (\n options.handlerName !== \"signIn\"\n && options.handlerName !== \"signUp\"\n && options.handlerName !== \"onboarding\"\n && options.handlerName !== \"signOut\"\n ) {\n throw new HexclaveAssertionError(\"Unexpected redirect-back-aware handler policy mismatch\", {\n handlerName: options.handlerName,\n policy,\n });\n }\n\n // noRedirectBack suppresses the initiating deep link, not the return journey. Cross-domain auth\n // still needs to return through the source app's callback, but its final destination is home.\n const afterCallbackRedirectUrlOverride = options.noRedirectBack\n ? new URL(options.rawHomeUrl, options.currentUrl).toString()\n : null;\n\n return {\n type: \"redirect\",\n url: await resolveRedirectBackAwareHandlerUrlForRedirect({\n handlerName: options.handlerName,\n rawHandlerUrl: options.rawHandlerUrl,\n currentUrl: options.currentUrl,\n localOAuthCallbackUrl: options.localOAuthCallbackUrl,\n getCrossDomainHandoffParams: options.getCrossDomainHandoffParams,\n noRedirectBack: options.noRedirectBack,\n afterCallbackRedirectUrlOverride,\n }),\n };\n}\n\nfunction readCrossDomainHandoffParams(currentUrl: URL, redirectBackTarget: URL): CrossDomainHandoffParamsMaybeMissing {\n // Hexclave rebrand: accept either param name from both URLs.\n const state = getCrossDomainParam(currentUrl.searchParams, \"state\")\n ?? getCrossDomainParam(redirectBackTarget.searchParams, \"state\");\n const codeChallenge = getCrossDomainParam(currentUrl.searchParams, \"codeChallenge\")\n ?? getCrossDomainParam(redirectBackTarget.searchParams, \"codeChallenge\");\n const afterCallbackRedirectUrl = getCrossDomainParam(currentUrl.searchParams, \"afterCallbackRedirectUrl\")\n ?? getCrossDomainParam(redirectBackTarget.searchParams, \"afterCallbackRedirectUrl\");\n return {\n state,\n codeChallenge,\n afterCallbackRedirectUrl,\n };\n}\n\nfunction resolveCrossDomainRedirectBackTarget(options: {\n currentUrl: URL,\n redirectBackTarget: URL,\n handoffParams: CrossDomainHandoffParamsMaybeMissing,\n}): URL | null {\n if (options.redirectBackTarget.origin !== options.currentUrl.origin) {\n return options.redirectBackTarget;\n }\n if (\n options.handoffParams.state == null\n || options.handoffParams.codeChallenge == null\n || options.handoffParams.afterCallbackRedirectUrl == null\n ) {\n return null;\n }\n const afterCallbackRedirectTarget = new URL(options.handoffParams.afterCallbackRedirectUrl, options.currentUrl);\n if (afterCallbackRedirectTarget.origin === options.currentUrl.origin) {\n return null;\n }\n return new URL(\n `${options.redirectBackTarget.pathname}${options.redirectBackTarget.search}${options.redirectBackTarget.hash}`,\n afterCallbackRedirectTarget.origin,\n );\n}\n\nfunction getCrossDomainHandoffForRedirect(options: {\n currentUrl: URL,\n redirectBackTarget: URL,\n}): {\n redirectBackTarget: URL,\n handoffParams: CrossDomainHandoffParamsMaybeMissing,\n} | null {\n const handoffParams = readCrossDomainHandoffParams(options.currentUrl, options.redirectBackTarget);\n const crossDomainRedirectBackTarget = resolveCrossDomainRedirectBackTarget({\n currentUrl: options.currentUrl,\n redirectBackTarget: options.redirectBackTarget,\n handoffParams,\n });\n if (crossDomainRedirectBackTarget == null) {\n return null;\n }\n return {\n redirectBackTarget: crossDomainRedirectBackTarget,\n handoffParams,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
let vitest = require("vitest");
|
|
2
|
+
let __redirect_page_urls_js = require("./redirect-page-urls.js");
|
|
3
|
+
//#region src/lib/hexclave-app/apps/implementations/redirect-page-urls.test.ts
|
|
4
|
+
const localOAuthCallbackUrl = "https://hosted.example.test/handler/oauth-callback";
|
|
5
|
+
async function plan(options) {
|
|
6
|
+
return await (0, __redirect_page_urls_js.planRedirectToHandler)({
|
|
7
|
+
...options,
|
|
8
|
+
noRedirectBack: options.noRedirectBack === true,
|
|
9
|
+
localOAuthCallbackUrl,
|
|
10
|
+
getCrossDomainHandoffParams: async () => {
|
|
11
|
+
throw new Error("Continuation-only redirects must not mint a new cross-domain handoff");
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function expectRedirectUrl(planResult, baseUrl) {
|
|
16
|
+
if (planResult.type !== "redirect") throw new Error("Expected a direct redirect plan");
|
|
17
|
+
return new URL(planResult.url, baseUrl);
|
|
18
|
+
}
|
|
19
|
+
function expectContinuation(url, expectedReturnTo) {
|
|
20
|
+
(0, vitest.expect)(url.searchParams.get("after_auth_return_to")).toBe(expectedReturnTo);
|
|
21
|
+
(0, vitest.expect)(url.searchParams.get(__redirect_page_urls_js.crossDomainAuthQueryParams.state)).toBe("outer-state");
|
|
22
|
+
(0, vitest.expect)(url.searchParams.get(__redirect_page_urls_js.crossDomainAuthQueryParams.codeChallenge)).toBe("outer-challenge");
|
|
23
|
+
(0, vitest.expect)(url.searchParams.get(__redirect_page_urls_js.crossDomainAuthQueryParams.afterCallbackRedirectUrl)).toBe("https://customer.example.test/dashboard");
|
|
24
|
+
}
|
|
25
|
+
(0, vitest.describe)("password-flow redirect continuation", () => {
|
|
26
|
+
(0, vitest.it)("carries the complete customer return state through forgot-password and a reset-email new tab", async () => {
|
|
27
|
+
const customerReturnTo = "https://customer.example.test/handler/oauth-callback?hexclave_cross_domain_auth=1";
|
|
28
|
+
const signInUrl = new URL("https://hosted.example.test/handler/sign-in");
|
|
29
|
+
signInUrl.searchParams.set("after_auth_return_to", customerReturnTo);
|
|
30
|
+
signInUrl.searchParams.set(__redirect_page_urls_js.crossDomainAuthQueryParams.state, "outer-state");
|
|
31
|
+
signInUrl.searchParams.set(__redirect_page_urls_js.crossDomainAuthQueryParams.codeChallenge, "outer-challenge");
|
|
32
|
+
signInUrl.searchParams.set(__redirect_page_urls_js.crossDomainAuthQueryParams.afterCallbackRedirectUrl, "https://customer.example.test/dashboard");
|
|
33
|
+
const forgotUrl = expectRedirectUrl(await plan({
|
|
34
|
+
handlerName: "forgotPassword",
|
|
35
|
+
rawHandlerUrl: "/handler/forgot-password",
|
|
36
|
+
currentUrl: signInUrl
|
|
37
|
+
}), signInUrl);
|
|
38
|
+
expectContinuation(forgotUrl, customerReturnTo);
|
|
39
|
+
const resetEmailCallbackUrl = expectRedirectUrl(await plan({
|
|
40
|
+
handlerName: "passwordReset",
|
|
41
|
+
rawHandlerUrl: "/handler/password-reset",
|
|
42
|
+
currentUrl: forgotUrl
|
|
43
|
+
}), forgotUrl);
|
|
44
|
+
expectContinuation(resetEmailCallbackUrl, customerReturnTo);
|
|
45
|
+
resetEmailCallbackUrl.searchParams.set("code", "reset-code");
|
|
46
|
+
const finalSignInUrl = expectRedirectUrl(await plan({
|
|
47
|
+
handlerName: "signIn",
|
|
48
|
+
rawHandlerUrl: "/handler/sign-in",
|
|
49
|
+
currentUrl: resetEmailCallbackUrl,
|
|
50
|
+
noRedirectBack: true
|
|
51
|
+
}), resetEmailCallbackUrl);
|
|
52
|
+
expectContinuation(finalSignInUrl, customerReturnTo);
|
|
53
|
+
(0, vitest.expect)(finalSignInUrl.searchParams.has("code")).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
vitest.it.each([["forgotPassword", "/handler/forgot-password"], ["passwordReset", "/handler/password-reset"]])("never synthesizes the current %s page as a return target", async (handlerName, rawHandlerUrl) => {
|
|
56
|
+
const currentUrl = new URL(`https://hosted.example.test${rawHandlerUrl}?code=reset-code`);
|
|
57
|
+
const result = expectRedirectUrl(await plan({
|
|
58
|
+
handlerName,
|
|
59
|
+
rawHandlerUrl,
|
|
60
|
+
currentUrl
|
|
61
|
+
}), currentUrl);
|
|
62
|
+
(0, vitest.expect)(result.searchParams.has("after_auth_return_to")).toBe(false);
|
|
63
|
+
(0, vitest.expect)(result.searchParams.has("code")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
(0, vitest.it)("makes sign-in noRedirectBack suppress capture while preserving only inherited state", async () => {
|
|
66
|
+
const resetUrlWithoutContinuation = new URL("https://hosted.example.test/handler/password-reset?code=reset-code");
|
|
67
|
+
(0, vitest.expect)(expectRedirectUrl(await plan({
|
|
68
|
+
handlerName: "signIn",
|
|
69
|
+
rawHandlerUrl: "/handler/sign-in",
|
|
70
|
+
currentUrl: resetUrlWithoutContinuation,
|
|
71
|
+
noRedirectBack: true
|
|
72
|
+
}), resetUrlWithoutContinuation).toString()).toBe("https://hosted.example.test/handler/sign-in");
|
|
73
|
+
});
|
|
74
|
+
vitest.it.each(["afterSignIn", "afterSignUp"])("keeps existing %s noRedirectBack semantics", async (handlerName) => {
|
|
75
|
+
const currentUrl = new URL("https://hosted.example.test/handler/sign-in?after_auth_return_to=%2Fcustomer");
|
|
76
|
+
(0, vitest.expect)(expectRedirectUrl(await plan({
|
|
77
|
+
handlerName,
|
|
78
|
+
rawHandlerUrl: "/default-after-auth",
|
|
79
|
+
currentUrl,
|
|
80
|
+
noRedirectBack: true
|
|
81
|
+
}), currentUrl).toString()).toBe("https://hosted.example.test/default-after-auth");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
//#endregion
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=redirect-page-urls.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-page-urls.test.js","names":["crossDomainAuthQueryParams"],"sources":["../../../../../src/lib/hexclave-app/apps/implementations/redirect-page-urls.test.ts"],"sourcesContent":["\n//===========================================\n// THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template\n//===========================================\nimport { describe, expect, it } from \"vitest\";\nimport { crossDomainAuthQueryParams, planRedirectToHandler } from \"./redirect-page-urls\";\n\nconst localOAuthCallbackUrl = \"https://hosted.example.test/handler/oauth-callback\";\n\nasync function plan(options: {\n handlerName: \"signIn\" | \"forgotPassword\" | \"passwordReset\" | \"afterSignIn\" | \"afterSignUp\",\n rawHandlerUrl: string,\n currentUrl: URL,\n noRedirectBack?: boolean,\n}) {\n return await planRedirectToHandler({\n ...options,\n noRedirectBack: options.noRedirectBack === true,\n localOAuthCallbackUrl,\n getCrossDomainHandoffParams: async () => {\n throw new Error(\"Continuation-only redirects must not mint a new cross-domain handoff\");\n },\n });\n}\n\nfunction expectRedirectUrl(planResult: Awaited<ReturnType<typeof plan>>, baseUrl: URL): URL {\n if (planResult.type !== \"redirect\") {\n throw new Error(\"Expected a direct redirect plan\");\n }\n return new URL(planResult.url, baseUrl);\n}\n\nfunction expectContinuation(url: URL, expectedReturnTo: string): void {\n expect(url.searchParams.get(\"after_auth_return_to\")).toBe(expectedReturnTo);\n expect(url.searchParams.get(crossDomainAuthQueryParams.state)).toBe(\"outer-state\");\n expect(url.searchParams.get(crossDomainAuthQueryParams.codeChallenge)).toBe(\"outer-challenge\");\n expect(url.searchParams.get(crossDomainAuthQueryParams.afterCallbackRedirectUrl)).toBe(\"https://customer.example.test/dashboard\");\n}\n\ndescribe(\"password-flow redirect continuation\", () => {\n it(\"carries the complete customer return state through forgot-password and a reset-email new tab\", async () => {\n const customerReturnTo = \"https://customer.example.test/handler/oauth-callback?hexclave_cross_domain_auth=1\";\n const signInUrl = new URL(\"https://hosted.example.test/handler/sign-in\");\n signInUrl.searchParams.set(\"after_auth_return_to\", customerReturnTo);\n signInUrl.searchParams.set(crossDomainAuthQueryParams.state, \"outer-state\");\n signInUrl.searchParams.set(crossDomainAuthQueryParams.codeChallenge, \"outer-challenge\");\n signInUrl.searchParams.set(crossDomainAuthQueryParams.afterCallbackRedirectUrl, \"https://customer.example.test/dashboard\");\n\n const forgotPlan = await plan({\n handlerName: \"forgotPassword\",\n rawHandlerUrl: \"/handler/forgot-password\",\n currentUrl: signInUrl,\n });\n const forgotUrl = expectRedirectUrl(forgotPlan, signInUrl);\n expectContinuation(forgotUrl, customerReturnTo);\n\n const callbackPlan = await plan({\n handlerName: \"passwordReset\",\n rawHandlerUrl: \"/handler/password-reset\",\n currentUrl: forgotUrl,\n });\n const resetEmailCallbackUrl = expectRedirectUrl(callbackPlan, forgotUrl);\n expectContinuation(resetEmailCallbackUrl, customerReturnTo);\n\n // Model opening the callback in a new tab: only URL state is available.\n resetEmailCallbackUrl.searchParams.set(\"code\", \"reset-code\");\n const signInPlan = await plan({\n handlerName: \"signIn\",\n rawHandlerUrl: \"/handler/sign-in\",\n currentUrl: resetEmailCallbackUrl,\n noRedirectBack: true,\n });\n const finalSignInUrl = expectRedirectUrl(signInPlan, resetEmailCallbackUrl);\n expectContinuation(finalSignInUrl, customerReturnTo);\n expect(finalSignInUrl.searchParams.has(\"code\")).toBe(false);\n });\n\n it.each([\n [\"forgotPassword\", \"/handler/forgot-password\"],\n [\"passwordReset\", \"/handler/password-reset\"],\n ] as const)(\"never synthesizes the current %s page as a return target\", async (handlerName, rawHandlerUrl) => {\n const currentUrl = new URL(`https://hosted.example.test${rawHandlerUrl}?code=reset-code`);\n const result = expectRedirectUrl(await plan({\n handlerName,\n rawHandlerUrl,\n currentUrl,\n }), currentUrl);\n\n expect(result.searchParams.has(\"after_auth_return_to\")).toBe(false);\n expect(result.searchParams.has(\"code\")).toBe(false);\n });\n\n it(\"makes sign-in noRedirectBack suppress capture while preserving only inherited state\", async () => {\n const resetUrlWithoutContinuation = new URL(\"https://hosted.example.test/handler/password-reset?code=reset-code\");\n const result = expectRedirectUrl(await plan({\n handlerName: \"signIn\",\n rawHandlerUrl: \"/handler/sign-in\",\n currentUrl: resetUrlWithoutContinuation,\n noRedirectBack: true,\n }), resetUrlWithoutContinuation);\n\n expect(result.toString()).toBe(\"https://hosted.example.test/handler/sign-in\");\n });\n\n it.each([\"afterSignIn\", \"afterSignUp\"] as const)(\n \"keeps existing %s noRedirectBack semantics\",\n async (handlerName) => {\n const currentUrl = new URL(\"https://hosted.example.test/handler/sign-in?after_auth_return_to=%2Fcustomer\");\n const result = expectRedirectUrl(await plan({\n handlerName,\n rawHandlerUrl: \"/default-after-auth\",\n currentUrl,\n noRedirectBack: true,\n }), currentUrl);\n\n expect(result.toString()).toBe(\"https://hosted.example.test/default-after-auth\");\n },\n );\n});\n"],"mappings":"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"}
|