@hexclave/next 1.0.73 → 1.0.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/{admin-app-impl-D867jXHk.d.ts → admin-app-impl-sTQD2d-3.d.ts} +16 -10
  2. package/dist/admin-app-impl-sTQD2d-3.d.ts.map +1 -0
  3. package/dist/clickmap/clickmap-core.d.ts +1 -1
  4. package/dist/clickmap/index.d.ts +1 -1
  5. package/dist/{client-app-B931IBrx.d.ts → client-app-BCT5p_70.d.ts} +11 -3
  6. package/dist/{client-app-B931IBrx.d.ts.map → client-app-BCT5p_70.d.ts.map} +1 -1
  7. package/dist/components/selected-team-switcher.d.ts +1 -1
  8. package/dist/components/team-icon.d.ts +1 -1
  9. package/dist/components/team-switcher.d.ts +1 -1
  10. package/dist/components-page/account-settings/payments/payments-page.d.ts +1 -1
  11. package/dist/components-page/account-settings/teams/leave-team-section.d.ts +1 -1
  12. package/dist/components-page/account-settings/teams/team-api-keys-section.d.ts +1 -1
  13. package/dist/components-page/account-settings/teams/team-display-name-section.d.ts +1 -1
  14. package/dist/components-page/account-settings/teams/team-member-invitation-section.d.ts +1 -1
  15. package/dist/components-page/account-settings/teams/team-member-list-section.d.ts +1 -1
  16. package/dist/components-page/account-settings/teams/team-page.d.ts +1 -1
  17. package/dist/components-page/account-settings/teams/team-profile-image-section.d.ts +1 -1
  18. package/dist/components-page/account-settings/teams/team-profile-user-section.d.ts +1 -1
  19. package/dist/components-page/hexclave-handler-client.d.ts +1 -1
  20. package/dist/components-page/hexclave-handler.d.ts +1 -1
  21. package/dist/components-page/oauth-callback.js +3 -18
  22. package/dist/components-page/oauth-callback.js.map +1 -1
  23. package/dist/dev-tool/dev-tool-core.d.ts +1 -1
  24. package/dist/dev-tool/index.d.ts +1 -1
  25. package/dist/esm/components-page/oauth-callback.d.ts.map +1 -1
  26. package/dist/esm/components-page/oauth-callback.js +4 -19
  27. package/dist/esm/components-page/oauth-callback.js.map +1 -1
  28. package/dist/esm/lib/auth.d.ts.map +1 -1
  29. package/dist/esm/lib/auth.js +1 -1
  30. package/dist/esm/lib/auth.js.map +1 -1
  31. package/dist/esm/lib/auth.test.js +2 -2
  32. package/dist/esm/lib/auth.test.js.map +1 -1
  33. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +7 -4
  34. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  35. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.d.ts +1 -0
  36. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +102 -0
  37. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -0
  38. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +57 -1
  39. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  40. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +7 -4
  41. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  42. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +44 -12
  43. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  44. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +2 -2
  45. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts +13 -1
  46. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts.map +1 -1
  47. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.js +15 -1
  48. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.js.map +1 -1
  49. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.js +25 -1
  50. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-back-state.test.js.map +1 -1
  51. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.d.ts +1 -0
  52. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.d.ts.map +1 -1
  53. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.js +67 -12
  54. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.js.map +1 -1
  55. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.d.ts +1 -0
  56. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +87 -0
  57. package/dist/esm/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -0
  58. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts +9 -1
  59. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.d.ts.map +1 -1
  60. package/dist/esm/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  61. package/dist/esm/utils/url.js +10 -2
  62. package/dist/esm/utils/url.js.map +1 -1
  63. package/dist/esm/utils/url.test.d.ts +1 -0
  64. package/dist/esm/utils/url.test.js +28 -0
  65. package/dist/esm/utils/url.test.js.map +1 -0
  66. package/dist/{hexclave-provider-DkFUT3Fa.d.ts → hexclave-provider-BKxi3Vy-.d.ts} +2 -2
  67. package/dist/{hexclave-provider-DkFUT3Fa.d.ts.map → hexclave-provider-BKxi3Vy-.d.ts.map} +1 -1
  68. package/dist/{hooks-4PMrTtJr.d.ts → hooks-CUe5bc3s.d.ts} +2 -2
  69. package/dist/{hooks-4PMrTtJr.d.ts.map → hooks-CUe5bc3s.d.ts.map} +1 -1
  70. package/dist/{index-C9bAeWRr.d.ts → index-CvB7XacB.d.ts} +2 -2
  71. package/dist/{index-C9bAeWRr.d.ts.map → index-CvB7XacB.d.ts.map} +1 -1
  72. package/dist/index.d.ts +4 -4
  73. package/dist/lib/auth.d.ts.map +1 -1
  74. package/dist/lib/auth.js +1 -1
  75. package/dist/lib/auth.js.map +1 -1
  76. package/dist/lib/auth.test.js +2 -2
  77. package/dist/lib/auth.test.js.map +1 -1
  78. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
  79. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.d.ts +1 -0
  80. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js +101 -0
  81. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.js.map +1 -0
  82. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +57 -1
  83. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  84. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
  85. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +43 -11
  86. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  87. package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
  88. package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
  89. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts +13 -1
  90. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.d.ts.map +1 -1
  91. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.js +15 -0
  92. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.js.map +1 -1
  93. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.js +24 -0
  94. package/dist/lib/hexclave-app/apps/implementations/redirect-back-state.test.js.map +1 -1
  95. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.d.ts +1 -1
  96. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.js +67 -12
  97. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.js.map +1 -1
  98. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.d.ts +1 -0
  99. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js +86 -0
  100. package/dist/lib/hexclave-app/apps/implementations/redirect-page-urls.test.js.map +1 -0
  101. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  102. package/dist/lib/hexclave-app/apps/index.d.ts +1 -1
  103. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +1 -1
  104. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -1
  105. package/dist/lib/hexclave-app/apps/interfaces/client-app.js.map +1 -1
  106. package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
  107. package/dist/lib/hexclave-app/index.d.ts +1 -1
  108. package/dist/lib/hexclave-app/projects/index.d.ts +1 -1
  109. package/dist/lib/hexclave-app/teams/index.d.ts +1 -1
  110. package/dist/lib/hexclave-app/users/index.d.ts +1 -1
  111. package/dist/lib/hooks.d.ts +1 -1
  112. package/dist/oauth-callback-tCVTRbF2.d.ts.map +1 -1
  113. package/dist/providers/hexclave-context.d.ts +1 -1
  114. package/dist/providers/hexclave-provider-client.d.ts +1 -1
  115. package/dist/providers/hexclave-provider.d.ts +1 -1
  116. package/dist/pushed-config-error-overlay/index.d.ts +1 -1
  117. package/dist/{redirect-page-urls-NUHUchgR.d.ts → redirect-page-urls-BEWG0AtP.d.ts} +2 -1
  118. package/dist/redirect-page-urls-BEWG0AtP.d.ts.map +1 -0
  119. package/dist/utils/url.js +10 -2
  120. package/dist/utils/url.js.map +1 -1
  121. package/dist/utils/url.test.d.ts +1 -0
  122. package/dist/utils/url.test.js +27 -0
  123. package/dist/utils/url.test.js.map +1 -0
  124. package/package.json +4 -4
  125. package/src/components-page/oauth-callback.tsx +0 -16
  126. package/src/lib/auth.test.ts +2 -2
  127. package/src/lib/auth.ts +3 -1
  128. package/src/lib/hexclave-app/apps/implementations/client-app-impl.automatic-side-effects.test.ts +126 -0
  129. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +75 -0
  130. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +73 -16
  131. package/src/lib/hexclave-app/apps/implementations/redirect-back-state.test.ts +29 -1
  132. package/src/lib/hexclave-app/apps/implementations/redirect-back-state.ts +17 -0
  133. package/src/lib/hexclave-app/apps/implementations/redirect-page-urls.test.ts +119 -0
  134. package/src/lib/hexclave-app/apps/implementations/redirect-page-urls.ts +104 -12
  135. package/src/lib/hexclave-app/apps/interfaces/client-app.ts +12 -2
  136. package/src/utils/url.test.ts +54 -0
  137. package/src/utils/url.ts +12 -3
  138. package/dist/admin-app-impl-D867jXHk.d.ts.map +0 -1
  139. package/dist/redirect-page-urls-NUHUchgR.d.ts.map +0 -1
@@ -14,8 +14,8 @@ import { Store } from "@hexclave/shared/dist/utils/stores";
14
14
  import { envVars } from "../../../../generated/env.js";
15
15
  import { resolveHandlerUrls } from "../../url-targets.js";
16
16
  //#region src/lib/hexclave-app/apps/implementations/common.ts
17
- const clientVersion = "js @hexclave/next@1.0.73";
18
- if ("js @hexclave/next@1.0.73".startsWith("STACK_COMPILE_TIME"));
17
+ const clientVersion = "js @hexclave/next@1.0.75";
18
+ if ("js @hexclave/next@1.0.75".startsWith("STACK_COMPILE_TIME"));
19
19
  const replaceHexclavePortPrefix = (input) => {
20
20
  if (!input) return input;
21
21
  const prefix = envVars.HEXCLAVE_PORT_PREFIX;
@@ -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;;;;;;iBAgCjF,wCAAA,CAAyC,OAAA;EAAW,UAAA,EAAY,GAAA;EAAK,SAAA;AAAA,IAAsB,GAAG"}
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"}
@@ -65,6 +65,20 @@ function readRedirectBackState(options) {
65
65
  return parsed;
66
66
  }
67
67
  /**
68
+ * Returns the raw return target carried by the current auth flow. The value is intentionally not
69
+ * described as trusted: callers must still pass it through the normal redirect validation before
70
+ * navigating to it.
71
+ *
72
+ * An explicit query parameter wins even when it is empty. This prevents a malformed URL from
73
+ * silently reviving an older flow from sessionStorage.
74
+ */
75
+ function getRawAfterAuthReturnTo(options) {
76
+ const explicitReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
77
+ if (explicitReturnTo != null) return explicitReturnTo.trim().length > 0 ? explicitReturnTo : null;
78
+ const persistedReturnTo = readRedirectBackState({ projectId: options.projectId })?.afterAuthReturnTo;
79
+ return persistedReturnTo != null && persistedReturnTo.trim().length > 0 ? persistedReturnTo : null;
80
+ }
81
+ /**
68
82
  * Returns a copy of `currentUrl` with any redirect-back query params that were dropped along the
69
83
  * way restored from the sessionStorage mirror. If `currentUrl` already carries
70
84
  * `after_auth_return_to`, it is returned unchanged — explicit query params always win.
@@ -84,6 +98,6 @@ function augmentUrlWithPersistedRedirectBackState(options) {
84
98
  return augmented;
85
99
  }
86
100
  //#endregion
87
- export { augmentUrlWithPersistedRedirectBackState, readRedirectBackState, saveRedirectBackStateFromUrl };
101
+ export { augmentUrlWithPersistedRedirectBackState, getRawAfterAuthReturnTo, readRedirectBackState, saveRedirectBackStateFromUrl };
88
102
 
89
103
  //# sourceMappingURL=redirect-back-state.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-back-state.js","names":[],"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,IAAI,2BAA2B,KAAK;EAC/E,0BAA0B,QAAQ,IAAI,aAAa,IAAI,2BAA2B,aAAa;EAC/F,qCAAqC,QAAQ,IAAI,aAAa,IAAI,2BAA2B,wBAAwB;EACrH,eAAe,KAAK,IAAI;CAC1B;CACA,wBAAwB,cAAc,QAAQ,SAAS,GAAG,KAAK,UAAU,KAAK,CAAC;AACjF;AAEA,SAAgB,sBAAsB,SAAmE;CACvG,MAAM,aAAa,cAAc,QAAQ,SAAS;CAClD,MAAM,MAAM,uBAAuB,UAAU;CAC7C,IAAI,OAAO,MACT,OAAO;CAET,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,QAAQ;EAGN,yBAAyB,UAAU;EACnC,OAAO;CACT;CACA,IAAI,CAAC,6BAA6B,MAAM,GAAG;EACzC,yBAAyB,UAAU;EACnC,OAAO;CACT;CACA,MAAM,YAAY,KAAK,IAAI,IAAI,OAAO;CACtC,IAAI,YAAY,KAAK,YAAY,4BAA4B;EAC3D,yBAAyB,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,CAAC,2BAA2B,OAAO,UAAU,gBAAgB;EAC7D,CAAC,2BAA2B,eAAe,UAAU,wBAAwB;EAC7E,CAAC,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":[],"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":";;;;;;;;;;;;;;;;;;;;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,IAAI,2BAA2B,KAAK;EAC/E,0BAA0B,QAAQ,IAAI,aAAa,IAAI,2BAA2B,aAAa;EAC/F,qCAAqC,QAAQ,IAAI,aAAa,IAAI,2BAA2B,wBAAwB;EACrH,eAAe,KAAK,IAAI;CAC1B;CACA,wBAAwB,cAAc,QAAQ,SAAS,GAAG,KAAK,UAAU,KAAK,CAAC;AACjF;AAEA,SAAgB,sBAAsB,SAAmE;CACvG,MAAM,aAAa,cAAc,QAAQ,SAAS;CAClD,MAAM,MAAM,uBAAuB,UAAU;CAC7C,IAAI,OAAO,MACT,OAAO;CAET,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,QAAQ;EAGN,yBAAyB,UAAU;EACnC,OAAO;CACT;CACA,IAAI,CAAC,6BAA6B,MAAM,GAAG;EACzC,yBAAyB,UAAU;EACnC,OAAO;CACT;CACA,MAAM,YAAY,KAAK,IAAI,IAAI,OAAO;CACtC,IAAI,YAAY,KAAK,YAAY,4BAA4B;EAC3D,yBAAyB,UAAU;EACnC,OAAO;CACT;CACA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,wBAAwB,SAAgE;CACtG,MAAM,mBAAmB,QAAQ,WAAW,aAAa,IAAI,sBAAsB;CACnF,IAAI,oBAAoB,MACtB,OAAO,iBAAiB,KAAK,CAAC,CAAC,SAAS,IAAI,mBAAmB;CAEjE,MAAM,oBAAoB,sBAAsB,EAAE,WAAW,QAAQ,UAAU,CAAC,CAAC,EAAE;CACnF,OAAO,qBAAqB,QAAQ,kBAAkB,KAAK,CAAC,CAAC,SAAS,IAAI,oBAAoB;AAChG;;;;;;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,CAAC,2BAA2B,OAAO,UAAU,gBAAgB;EAC7D,CAAC,2BAA2B,eAAe,UAAU,wBAAwB;EAC7E,CAAC,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,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { augmentUrlWithPersistedRedirectBackState, readRedirectBackState, saveRedirectBackStateFromUrl } from "./redirect-back-state.js";
2
+ import { augmentUrlWithPersistedRedirectBackState, getRawAfterAuthReturnTo, readRedirectBackState, saveRedirectBackStateFromUrl } from "./redirect-back-state.js";
3
3
  //#region src/lib/hexclave-app/apps/implementations/redirect-back-state.test.ts
4
4
  function createMockSessionStorage() {
5
5
  const map = /* @__PURE__ */ new Map();
@@ -105,6 +105,30 @@ describe("redirect-back state mirror", () => {
105
105
  projectId
106
106
  }).toString()).toBe(explicitUrl.toString());
107
107
  });
108
+ it("reads the explicit return target before persisted state", () => {
109
+ saveRedirectBackStateFromUrl({
110
+ url: new URL("https://hosted.example.test/handler/sign-in?after_auth_return_to=/persisted"),
111
+ projectId
112
+ });
113
+ expect(getRawAfterAuthReturnTo({
114
+ currentUrl: new URL("https://hosted.example.test/handler/mfa?after_auth_return_to=/explicit"),
115
+ projectId
116
+ })).toBe("/explicit");
117
+ expect(getRawAfterAuthReturnTo({
118
+ currentUrl: new URL("https://hosted.example.test/handler/mfa"),
119
+ projectId
120
+ })).toBe("/persisted");
121
+ });
122
+ it("treats an explicitly empty return target as missing instead of reviving persisted state", () => {
123
+ saveRedirectBackStateFromUrl({
124
+ url: new URL("https://hosted.example.test/handler/sign-in?after_auth_return_to=/persisted"),
125
+ projectId
126
+ });
127
+ expect(getRawAfterAuthReturnTo({
128
+ currentUrl: new URL("https://hosted.example.test/handler/mfa?after_auth_return_to=%20%20"),
129
+ projectId
130
+ })).toBeNull();
131
+ });
108
132
  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
  expect(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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"}
@@ -28,6 +28,7 @@ declare function planRedirectToHandler(options: {
28
28
  noRedirectBack: boolean;
29
29
  currentUrl: URL | null;
30
30
  localOAuthCallbackUrl: string;
31
+ rawHomeUrl: string;
31
32
  getCrossDomainHandoffParams: (currentUrl: URL) => Promise<CrossDomainHandoffParams>;
32
33
  }): Promise<RedirectToHandlerPlan>;
33
34
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-page-urls.d.ts","names":[],"sources":["../../../../../../src/lib/hexclave-app/apps/implementations/redirect-page-urls.ts"],"mappings":";;;cAQa,0BAAA;EAAA;;;;;KAqBD,wBAAA;EACV,KAAA;EACA,aAAa;AAAA;AAAA,iBAGC,yCAAA,CAA0C,UAAA,EAAY,GAAA,GAAM,wBAAwB;AAAA,KAkI/F,qBAAA;EACC,IAAA;EAAkB,GAAA;AAAA;EAEpB,IAAA;EACA,WAAA;EACA,KAAA;EACA,aAAA;EACA,wBAAA;AAAA;AAAA,iBAsCkB,qBAAA,CAAsB,OAAA;EAC1C,WAAA,QAAmB,WAAA;EACnB,aAAA;EACA,cAAA;EACA,UAAA,EAAY,GAAA;EACZ,qBAAA;EACA,2BAAA,GAA8B,UAAA,EAAY,GAAA,KAAQ,OAAA,CAAQ,wBAAA;AAAA,IACxD,OAAA,CAAQ,qBAAA"}
1
+ {"version":3,"file":"redirect-page-urls.d.ts","names":[],"sources":["../../../../../../src/lib/hexclave-app/apps/implementations/redirect-page-urls.ts"],"mappings":";;;cAQa,0BAAA;EAAA;;;;;KAqBD,wBAAA;EACV,KAAA;EACA,aAAa;AAAA;AAAA,iBAGC,yCAAA,CAA0C,UAAA,EAAY,GAAA,GAAM,wBAAwB;AAAA,KA0K/F,qBAAA;EACC,IAAA;EAAkB,GAAA;AAAA;EAEpB,IAAA;EACA,WAAA;EACA,KAAA;EACA,aAAA;EACA,wBAAA;AAAA;AAAA,iBA4CkB,qBAAA,CAAsB,OAAA;EAC1C,WAAA,QAAmB,WAAA;EACnB,aAAA;EACA,cAAA;EACA,UAAA,EAAY,GAAA;EACZ,qBAAA;EACA,UAAA;EACA,2BAAA,GAA8B,UAAA,EAAY,GAAA,KAAQ,OAAA,CAAQ,wBAAA;AAAA,IACxD,OAAA,CAAQ,qBAAA"}
@@ -28,6 +28,9 @@ function getCrossDomainHandoffParamsFromCurrentUrl(currentUrl) {
28
28
  function isRedirectBackAwareHandlerName(handlerName) {
29
29
  return handlerName === "signIn" || handlerName === "signUp" || handlerName === "onboarding" || handlerName === "signOut";
30
30
  }
31
+ function isContinuationAwareHandlerName(handlerName) {
32
+ return handlerName === "forgotPassword" || handlerName === "passwordReset";
33
+ }
31
34
  function hasCrossDomainHandoffParams(url) {
32
35
  return hasCrossDomainParam(url.searchParams, "state") && hasCrossDomainParam(url.searchParams, "codeChallenge") && hasCrossDomainParam(url.searchParams, "afterCallbackRedirectUrl");
33
36
  }
@@ -46,12 +49,13 @@ function buildCrossDomainAuthCallbackUrl(options) {
46
49
  function buildRedirectBackAwareHandlerUrl(options) {
47
50
  const nextUrl = new URL(options.rawHandlerUrl, options.currentUrl);
48
51
  const currentAfterAuthReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
49
- if (currentAfterAuthReturnTo != null && !nextUrl.searchParams.has("after_auth_return_to")) nextUrl.searchParams.set("after_auth_return_to", currentAfterAuthReturnTo);
52
+ if (!options.noRedirectBack && currentAfterAuthReturnTo != null && !nextUrl.searchParams.has("after_auth_return_to")) nextUrl.searchParams.set("after_auth_return_to", currentAfterAuthReturnTo);
50
53
  for (const preservedParam of [
51
54
  "state",
52
55
  "codeChallenge",
53
56
  "afterCallbackRedirectUrl"
54
57
  ]) {
58
+ if (options.noRedirectBack && preservedParam === "afterCallbackRedirectUrl") continue;
55
59
  const currentValue = getCrossDomainParam(options.currentUrl.searchParams, preservedParam);
56
60
  if (currentValue != null && !hasCrossDomainParam(nextUrl.searchParams, preservedParam)) setCrossDomainParam(nextUrl.searchParams, preservedParam, currentValue);
57
61
  }
@@ -63,27 +67,48 @@ function buildRedirectBackAwareHandlerUrl(options) {
63
67
  if (options.currentUrl.origin !== nextUrl.origin) {
64
68
  if (!hasCrossDomainHandoffParams(nextUrl)) {
65
69
  const inheritedAfterAuthReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
66
- const afterCallbackRedirectUrl = inheritedAfterAuthReturnTo ? new URL(inheritedAfterAuthReturnTo, options.currentUrl).toString() : options.currentUrl.toString();
70
+ const afterCallbackRedirectUrl = options.afterCallbackRedirectUrlOverride ?? (inheritedAfterAuthReturnTo ? new URL(inheritedAfterAuthReturnTo, options.currentUrl).toString() : options.currentUrl.toString());
71
+ const handoffParams = options.crossDomainHandoffParams ?? getCrossDomainHandoffParamsFromCurrentUrl(options.currentUrl);
67
72
  const callbackUrl = buildCrossDomainAuthCallbackUrl({
68
73
  currentUrl: options.currentUrl,
69
74
  localOAuthCallbackUrl: options.localOAuthCallbackUrl,
70
- state: options.crossDomainHandoffParams?.state,
71
- codeChallenge: options.crossDomainHandoffParams?.codeChallenge,
75
+ state: handoffParams?.state,
76
+ codeChallenge: handoffParams?.codeChallenge,
72
77
  afterCallbackRedirectUrl
73
78
  });
74
79
  nextUrl.searchParams.set("after_auth_return_to", callbackUrl.toString());
75
80
  setCrossDomainParam(nextUrl.searchParams, "afterCallbackRedirectUrl", afterCallbackRedirectUrl);
76
- if (options.crossDomainHandoffParams != null) {
77
- setCrossDomainParam(nextUrl.searchParams, "state", options.crossDomainHandoffParams.state);
78
- setCrossDomainParam(nextUrl.searchParams, "codeChallenge", options.crossDomainHandoffParams.codeChallenge);
81
+ if (handoffParams != null) {
82
+ setCrossDomainParam(nextUrl.searchParams, "state", handoffParams.state);
83
+ setCrossDomainParam(nextUrl.searchParams, "codeChallenge", handoffParams.codeChallenge);
79
84
  }
80
85
  }
81
- } 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", getRelativePart(options.currentUrl));
86
+ } 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", getRelativePart(options.currentUrl));
87
+ return nextUrl.origin === options.currentUrl.origin ? getRelativePart(nextUrl) : nextUrl.toString();
88
+ }
89
+ /**
90
+ * Carries an auth flow's existing return state without ever making the current page the return
91
+ * target. This distinction is important for password flows: forgot-password and password-reset
92
+ * are continuation pages, not destinations customers should return to after signing in.
93
+ */
94
+ function buildContinuationAwareHandlerUrl(options) {
95
+ const nextUrl = new URL(options.rawHandlerUrl, options.currentUrl);
96
+ const inheritedAfterAuthReturnTo = options.currentUrl.searchParams.get("after_auth_return_to");
97
+ if (inheritedAfterAuthReturnTo != null && !nextUrl.searchParams.has("after_auth_return_to")) nextUrl.searchParams.set("after_auth_return_to", inheritedAfterAuthReturnTo);
98
+ for (const preservedParam of [
99
+ "state",
100
+ "codeChallenge",
101
+ "afterCallbackRedirectUrl"
102
+ ]) {
103
+ const currentValue = getCrossDomainParam(options.currentUrl.searchParams, preservedParam);
104
+ if (currentValue != null && !hasCrossDomainParam(nextUrl.searchParams, preservedParam)) setCrossDomainParam(nextUrl.searchParams, preservedParam, currentValue);
105
+ }
82
106
  return nextUrl.origin === options.currentUrl.origin ? getRelativePart(nextUrl) : nextUrl.toString();
83
107
  }
84
108
  function getHandlerRedirectPolicy(handlerName) {
85
109
  if (handlerName === "afterSignIn" || handlerName === "afterSignUp") return "after-auth-return";
86
110
  if (isRedirectBackAwareHandlerName(handlerName)) return "redirect-back-aware";
111
+ if (isContinuationAwareHandlerName(handlerName)) return "continuation-aware";
87
112
  return "none";
88
113
  }
89
114
  async function resolveRedirectBackAwareHandlerUrlForRedirect(options) {
@@ -92,7 +117,9 @@ async function resolveRedirectBackAwareHandlerUrlForRedirect(options) {
92
117
  rawHandlerUrl: options.rawHandlerUrl,
93
118
  currentUrl: options.currentUrl,
94
119
  crossDomainHandoffParams: null,
95
- localOAuthCallbackUrl: options.localOAuthCallbackUrl
120
+ localOAuthCallbackUrl: options.localOAuthCallbackUrl,
121
+ noRedirectBack: options.noRedirectBack,
122
+ afterCallbackRedirectUrlOverride: options.afterCallbackRedirectUrlOverride
96
123
  });
97
124
  if (options.handlerName === "signOut") return initial;
98
125
  const initialTarget = new URL(initial, options.currentUrl);
@@ -103,20 +130,45 @@ async function resolveRedirectBackAwareHandlerUrlForRedirect(options) {
103
130
  rawHandlerUrl: options.rawHandlerUrl,
104
131
  currentUrl: options.currentUrl,
105
132
  crossDomainHandoffParams,
106
- localOAuthCallbackUrl: options.localOAuthCallbackUrl
133
+ localOAuthCallbackUrl: options.localOAuthCallbackUrl,
134
+ noRedirectBack: options.noRedirectBack,
135
+ afterCallbackRedirectUrlOverride: options.afterCallbackRedirectUrlOverride
107
136
  });
108
137
  }
109
138
  async function planRedirectToHandler(options) {
110
- if (options.noRedirectBack || options.currentUrl == null) return {
139
+ if (options.currentUrl == null) return {
111
140
  type: "redirect",
112
141
  url: options.rawHandlerUrl
113
142
  };
114
143
  const policy = getHandlerRedirectPolicy(options.handlerName);
144
+ if (options.noRedirectBack) if (options.handlerName === "signIn") {
145
+ if (new URL(options.rawHandlerUrl, options.currentUrl).origin === options.currentUrl.origin) return {
146
+ type: "redirect",
147
+ url: buildContinuationAwareHandlerUrl({
148
+ rawHandlerUrl: options.rawHandlerUrl,
149
+ currentUrl: options.currentUrl
150
+ })
151
+ };
152
+ } else return {
153
+ type: "redirect",
154
+ url: options.rawHandlerUrl
155
+ };
115
156
  if (policy === "none") return {
116
157
  type: "redirect",
117
158
  url: options.rawHandlerUrl
118
159
  };
160
+ if (policy === "continuation-aware") return {
161
+ type: "redirect",
162
+ url: buildContinuationAwareHandlerUrl({
163
+ rawHandlerUrl: options.rawHandlerUrl,
164
+ currentUrl: options.currentUrl
165
+ })
166
+ };
119
167
  if (policy === "after-auth-return") {
168
+ if (options.noRedirectBack) return {
169
+ type: "redirect",
170
+ url: options.rawHandlerUrl
171
+ };
120
172
  const redirectBackUrl = options.currentUrl.searchParams.get("after_auth_return_to");
121
173
  if (redirectBackUrl == null) return {
122
174
  type: "redirect",
@@ -152,6 +204,7 @@ async function planRedirectToHandler(options) {
152
204
  handlerName: options.handlerName,
153
205
  policy
154
206
  });
207
+ const afterCallbackRedirectUrlOverride = options.noRedirectBack ? new URL(options.rawHomeUrl, options.currentUrl).toString() : null;
155
208
  return {
156
209
  type: "redirect",
157
210
  url: await resolveRedirectBackAwareHandlerUrlForRedirect({
@@ -159,7 +212,9 @@ async function planRedirectToHandler(options) {
159
212
  rawHandlerUrl: options.rawHandlerUrl,
160
213
  currentUrl: options.currentUrl,
161
214
  localOAuthCallbackUrl: options.localOAuthCallbackUrl,
162
- getCrossDomainHandoffParams: options.getCrossDomainHandoffParams
215
+ getCrossDomainHandoffParams: options.getCrossDomainHandoffParams,
216
+ noRedirectBack: options.noRedirectBack,
217
+ afterCallbackRedirectUrlOverride
163
218
  })
164
219
  };
165
220
  }
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-page-urls.js","names":[],"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":";;;AAQA,MAAa,6BAA6B;CACxC,QAAQ;CACR,OAAO;CACP,eAAe;CACf,0BAA0B;AAC5B;AAIA,SAAS,oBAAoB,QAAyB,KAAkD;CACtG,OAAO,OAAO,IAAI,2BAA2B,IAAI;AACnD;AAEA,SAAS,oBAAoB,QAAyB,KAA4C;CAChG,OAAO,OAAO,IAAI,2BAA2B,IAAI;AACnD;AAEA,SAAS,oBAAoB,QAAyB,KAAmC,OAAqB;CAC5G,OAAO,IAAI,2BAA2B,MAAM,KAAK;AACnD;AAOA,SAAgB,0CAA0C,YAAkD;CAC1G,MAAM,QAAQ,oBAAoB,WAAW,cAAc,OAAO;CAClE,MAAM,gBAAgB,oBAAoB,WAAW,cAAc,eAAe;CAClF,IAAI,SAAS,QAAQ,iBAAiB,MACpC,OAAO;CAET,OAAO;EAAE;EAAO;CAAc;AAChC;AAWA,SAAS,+BAA+B,aAA6E;CACnH,OAAO,gBAAgB,YAClB,gBAAgB,YAChB,gBAAgB,gBAChB,gBAAgB;AACvB;AAEA,SAAS,4BAA4B,KAAmB;CACtD,OACE,oBAAoB,IAAI,cAAc,OAAO,KAC1C,oBAAoB,IAAI,cAAc,eAAe,KACrD,oBAAoB,IAAI,cAAc,0BAA0B;AAEvE;AAEA,SAAS,gCAAgC,SAMjC;CACN,MAAM,wBAAwB,IAAI,IAAI,QAAQ,uBAAuB,QAAQ,UAAU;CACvF,IAAI,sBAAsB,WAAW,QAAQ,WAAW,QACtD,MAAM,IAAI,uBAAuB,kEAAkE;EACjG,uBAAuB,sBAAsB,SAAS;EACtD,YAAY,QAAQ,WAAW,SAAS;CAC1C,CAAC;CAEH,oBAAoB,sBAAsB,cAAc,UAAU,GAAG;CACrE,IAAI,QAAQ,SAAS,MACnB,oBAAoB,sBAAsB,cAAc,SAAS,QAAQ,KAAK;CAEhF,IAAI,QAAQ,iBAAiB,MAC3B,oBAAoB,sBAAsB,cAAc,iBAAiB,QAAQ,aAAa;CAEhG,IAAI,QAAQ,4BAA4B,MACtC,oBAAoB,sBAAsB,cAAc,4BAA4B,QAAQ,wBAAwB;CAEtH,OAAO;AACT;AAEA,SAAS,iCAAiC,SAM/B;CACT,MAAM,UAAU,IAAI,IAAI,QAAQ,eAAe,QAAQ,UAAU;CAEjE,MAAM,2BAA2B,QAAQ,WAAW,aAAa,IAAI,sBAAsB;CAC3F,IAAI,4BAA4B,QAAQ,CAAC,QAAQ,aAAa,IAAI,sBAAsB,GACtF,QAAQ,aAAa,IAAI,wBAAwB,wBAAwB;CAE3E,KAAK,MAAM,kBAAkB;EAAC;EAAS;EAAiB;CAA0B,GAAY;EAC5F,MAAM,eAAe,oBAAoB,QAAQ,WAAW,cAAc,cAAc;EACxF,IAAI,gBAAgB,QAAQ,CAAC,oBAAoB,QAAQ,cAAc,cAAc,GACnF,oBAAoB,QAAQ,cAAc,gBAAgB,YAAY;CAE1E;CAEA,IAAI,QAAQ,gBAAgB,WAAW;EACrC,IAAI,CAAC,QAAQ,aAAa,IAAI,sBAAsB,GAClD,IAAI,QAAQ,WAAW,aAAa,QAAQ,YAAY,QAAQ,WAAW,SAAS,QAAQ,MAC1F,QAAQ,aAAa,IAAI,wBAAwB,gBAAgB,QAAQ,UAAU,CAAC;OAEpF,QAAQ,aAAa,IAAI,wBAAwB,QAAQ,WAAW,SAAS,CAAC;EAGlF,OAAO,QAAQ,WAAW,QAAQ,WAAW,SAAS,gBAAgB,OAAO,IAAI,QAAQ,SAAS;CACpG;CAGA,IADqC,QAAQ,WAAW,WAAW,QAAQ;MAErE,CAAC,4BAA4B,OAAO,GAAG;GACzC,MAAM,6BAA6B,QAAQ,WAAW,aAAa,IAAI,sBAAsB;GAC7F,MAAM,2BAA2B,6BAC7B,IAAI,IAAI,4BAA4B,QAAQ,UAAU,CAAC,CAAC,SAAS,IACjE,QAAQ,WAAW,SAAS;GAChC,MAAM,cAAc,gCAAgC;IAClD,YAAY,QAAQ;IACpB,uBAAuB,QAAQ;IAC/B,OAAO,QAAQ,0BAA0B;IACzC,eAAe,QAAQ,0BAA0B;IACjD;GACF,CAAC;GAED,QAAQ,aAAa,IAAI,wBAAwB,YAAY,SAAS,CAAC;GACvE,oBAAoB,QAAQ,cAAc,4BAA4B,wBAAwB;GAC9F,IAAI,QAAQ,4BAA4B,MAAM;IAC5C,oBAAoB,QAAQ,cAAc,SAAS,QAAQ,yBAAyB,KAAK;IACzF,oBAAoB,QAAQ,cAAc,iBAAiB,QAAQ,yBAAyB,aAAa;GAC3G;EACF;QACK,IAAI,QAAQ,WAAW,aAAa,QAAQ,YAAY,QAAQ,WAAW,SAAS,QAAQ,QAAQ,CAAC,QAAQ,aAAa,IAAI,sBAAsB,GACzJ,QAAQ,aAAa,IAAI,wBAAwB,gBAAgB,QAAQ,UAAU,CAAC;CAGtF,OAAO,QAAQ,WAAW,QAAQ,WAAW,SAAS,gBAAgB,OAAO,IAAI,QAAQ,SAAS;AACpG;AAEA,SAAS,yBAAyB,aAAuD;CACvF,IAAI,gBAAgB,iBAAiB,gBAAgB,eACnD,OAAO;CAET,IAAI,+BAA+B,WAAW,GAC5C,OAAO;CAET,OAAO;AACT;AAaA,eAAe,8CAA8C,SAMzC;CAClB,MAAM,UAAU,iCAAiC;EAC/C,aAAa,QAAQ;EACrB,eAAe,QAAQ;EACvB,YAAY,QAAQ;EACpB,0BAA0B;EAC1B,uBAAuB,QAAQ;CACjC,CAAC;CACD,IAAI,QAAQ,gBAAgB,WAC1B,OAAO;CAGT,MAAM,gBAAgB,IAAI,IAAI,SAAS,QAAQ,UAAU;CAEzD,IAAI,EADiC,QAAQ,WAAW,WAAW,cAAc,WAC5C,4BAA4B,aAAa,GAC5E,OAAO;CAGT,MAAM,2BAA2B,MAAM,QAAQ,4BAA4B,QAAQ,UAAU;CAC7F,OAAO,iCAAiC;EACtC,aAAa,QAAQ;EACrB,eAAe,QAAQ;EACvB,YAAY,QAAQ;EACpB;EACA,uBAAuB,QAAQ;CACjC,CAAC;AACH;AAEA,eAAsB,sBAAsB,SAOT;CACjC,IAAI,QAAQ,kBAAkB,QAAQ,cAAc,MAClD,OAAO;EAAE,MAAM;EAAY,KAAK,QAAQ;CAAc;CAGxD,MAAM,SAAS,yBAAyB,QAAQ,WAAW;CAC3D,IAAI,WAAW,QACb,OAAO;EAAE,MAAM;EAAY,KAAK,QAAQ;CAAc;CAGxD,IAAI,WAAW,qBAAqB;EAClC,MAAM,kBAAkB,QAAQ,WAAW,aAAa,IAAI,sBAAsB;EAClF,IAAI,mBAAmB,MACrB,OAAO;GAAE,MAAM;GAAY,KAAK,QAAQ;EAAc;EAExD,MAAM,qBAAqB,IAAI,IAAI,iBAAiB,QAAQ,UAAU;EACtE,MAAM,qBAAqB,iCAAiC;GAC1D,YAAY,QAAQ;GACpB;EACF,CAAC;EACD,IAAI,sBAAsB,MACxB,OAAO;GAAE,MAAM;GAAY,KAAK;EAAgB;EAElD,IAAI,QAAQ,mBAAmB,cAAc;EAC7C,IAAI,gBAAgB,mBAAmB,cAAc;EACrD,IAAI,2BAA2B,mBAAmB,cAAc;EAChE,IAAI,SAAS,QAAQ,iBAAiB,MAAM;GAC1C,MAAM,yBAAyB,MAAM,QAAQ,4BAA4B,QAAQ,UAAU;GAC3F,UAAU,uBAAuB;GACjC,kBAAkB,uBAAuB;EAC3C;EACA,6BAA6B,QAAQ,WAAW,SAAS;EACzD,OAAO;GACL,MAAM;GACN,aAAa,mBAAmB,mBAAmB,SAAS;GAC5D;GACA;GACA;EACF;CACF;CAEA,IACE,QAAQ,gBAAgB,YACrB,QAAQ,gBAAgB,YACxB,QAAQ,gBAAgB,gBACxB,QAAQ,gBAAgB,WAE3B,MAAM,IAAI,uBAAuB,0DAA0D;EACzF,aAAa,QAAQ;EACrB;CACF,CAAC;CAGH,OAAO;EACL,MAAM;EACN,KAAK,MAAM,8CAA8C;GACvD,aAAa,QAAQ;GACrB,eAAe,QAAQ;GACvB,YAAY,QAAQ;GACpB,uBAAuB,QAAQ;GAC/B,6BAA6B,QAAQ;EACvC,CAAC;CACH;AACF;AAEA,SAAS,6BAA6B,YAAiB,oBAA+D;CAQpH,OAAO;EACL,OAPY,oBAAoB,WAAW,cAAc,OAAO,KAC7D,oBAAoB,mBAAmB,cAAc,OAAO;EAO/D,eANoB,oBAAoB,WAAW,cAAc,eAAe,KAC7E,oBAAoB,mBAAmB,cAAc,eAAe;EAMvE,0BAL+B,oBAAoB,WAAW,cAAc,0BAA0B,KACnG,oBAAoB,mBAAmB,cAAc,0BAA0B;CAKpF;AACF;AAEA,SAAS,qCAAqC,SAI/B;CACb,IAAI,QAAQ,mBAAmB,WAAW,QAAQ,WAAW,QAC3D,OAAO,QAAQ;CAEjB,IACE,QAAQ,cAAc,SAAS,QAC5B,QAAQ,cAAc,iBAAiB,QACvC,QAAQ,cAAc,4BAA4B,MAErD,OAAO;CAET,MAAM,8BAA8B,IAAI,IAAI,QAAQ,cAAc,0BAA0B,QAAQ,UAAU;CAC9G,IAAI,4BAA4B,WAAW,QAAQ,WAAW,QAC5D,OAAO;CAET,OAAO,IAAI,IACT,GAAG,QAAQ,mBAAmB,WAAW,QAAQ,mBAAmB,SAAS,QAAQ,mBAAmB,QACxG,4BAA4B,MAC9B;AACF;AAEA,SAAS,iCAAiC,SAMjC;CACP,MAAM,gBAAgB,6BAA6B,QAAQ,YAAY,QAAQ,kBAAkB;CACjG,MAAM,gCAAgC,qCAAqC;EACzE,YAAY,QAAQ;EACpB,oBAAoB,QAAQ;EAC5B;CACF,CAAC;CACD,IAAI,iCAAiC,MACnC,OAAO;CAET,OAAO;EACL,oBAAoB;EACpB;CACF;AACF"}
1
+ {"version":3,"file":"redirect-page-urls.js","names":[],"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":";;;AAQA,MAAa,6BAA6B;CACxC,QAAQ;CACR,OAAO;CACP,eAAe;CACf,0BAA0B;AAC5B;AAIA,SAAS,oBAAoB,QAAyB,KAAkD;CACtG,OAAO,OAAO,IAAI,2BAA2B,IAAI;AACnD;AAEA,SAAS,oBAAoB,QAAyB,KAA4C;CAChG,OAAO,OAAO,IAAI,2BAA2B,IAAI;AACnD;AAEA,SAAS,oBAAoB,QAAyB,KAAmC,OAAqB;CAC5G,OAAO,IAAI,2BAA2B,MAAM,KAAK;AACnD;AAOA,SAAgB,0CAA0C,YAAkD;CAC1G,MAAM,QAAQ,oBAAoB,WAAW,cAAc,OAAO;CAClE,MAAM,gBAAgB,oBAAoB,WAAW,cAAc,eAAe;CAClF,IAAI,SAAS,QAAQ,iBAAiB,MACpC,OAAO;CAET,OAAO;EAAE;EAAO;CAAc;AAChC;AAYA,SAAS,+BAA+B,aAA6E;CACnH,OAAO,gBAAgB,YAClB,gBAAgB,YAChB,gBAAgB,gBAChB,gBAAgB;AACvB;AAEA,SAAS,+BAA+B,aAA6E;CACnH,OAAO,gBAAgB,oBAAoB,gBAAgB;AAC7D;AAEA,SAAS,4BAA4B,KAAmB;CACtD,OACE,oBAAoB,IAAI,cAAc,OAAO,KAC1C,oBAAoB,IAAI,cAAc,eAAe,KACrD,oBAAoB,IAAI,cAAc,0BAA0B;AAEvE;AAEA,SAAS,gCAAgC,SAMjC;CACN,MAAM,wBAAwB,IAAI,IAAI,QAAQ,uBAAuB,QAAQ,UAAU;CACvF,IAAI,sBAAsB,WAAW,QAAQ,WAAW,QACtD,MAAM,IAAI,uBAAuB,kEAAkE;EACjG,uBAAuB,sBAAsB,SAAS;EACtD,YAAY,QAAQ,WAAW,SAAS;CAC1C,CAAC;CAEH,oBAAoB,sBAAsB,cAAc,UAAU,GAAG;CACrE,IAAI,QAAQ,SAAS,MACnB,oBAAoB,sBAAsB,cAAc,SAAS,QAAQ,KAAK;CAEhF,IAAI,QAAQ,iBAAiB,MAC3B,oBAAoB,sBAAsB,cAAc,iBAAiB,QAAQ,aAAa;CAEhG,IAAI,QAAQ,4BAA4B,MACtC,oBAAoB,sBAAsB,cAAc,4BAA4B,QAAQ,wBAAwB;CAEtH,OAAO;AACT;AAEA,SAAS,iCAAiC,SAQ/B;CACT,MAAM,UAAU,IAAI,IAAI,QAAQ,eAAe,QAAQ,UAAU;CAEjE,MAAM,2BAA2B,QAAQ,WAAW,aAAa,IAAI,sBAAsB;CAC3F,IAAI,CAAC,QAAQ,kBAAkB,4BAA4B,QAAQ,CAAC,QAAQ,aAAa,IAAI,sBAAsB,GACjH,QAAQ,aAAa,IAAI,wBAAwB,wBAAwB;CAE3E,KAAK,MAAM,kBAAkB;EAAC;EAAS;EAAiB;CAA0B,GAAY;EAC5F,IAAI,QAAQ,kBAAkB,mBAAmB,4BAC/C;EAEF,MAAM,eAAe,oBAAoB,QAAQ,WAAW,cAAc,cAAc;EACxF,IAAI,gBAAgB,QAAQ,CAAC,oBAAoB,QAAQ,cAAc,cAAc,GACnF,oBAAoB,QAAQ,cAAc,gBAAgB,YAAY;CAE1E;CAEA,IAAI,QAAQ,gBAAgB,WAAW;EACrC,IAAI,CAAC,QAAQ,aAAa,IAAI,sBAAsB,GAClD,IAAI,QAAQ,WAAW,aAAa,QAAQ,YAAY,QAAQ,WAAW,SAAS,QAAQ,MAC1F,QAAQ,aAAa,IAAI,wBAAwB,gBAAgB,QAAQ,UAAU,CAAC;OAEpF,QAAQ,aAAa,IAAI,wBAAwB,QAAQ,WAAW,SAAS,CAAC;EAGlF,OAAO,QAAQ,WAAW,QAAQ,WAAW,SAAS,gBAAgB,OAAO,IAAI,QAAQ,SAAS;CACpG;CAGA,IADqC,QAAQ,WAAW,WAAW,QAAQ;MAErE,CAAC,4BAA4B,OAAO,GAAG;GACzC,MAAM,6BAA6B,QAAQ,WAAW,aAAa,IAAI,sBAAsB;GAC7F,MAAM,2BAA2B,QAAQ,qCACvC,6BACI,IAAI,IAAI,4BAA4B,QAAQ,UAAU,CAAC,CAAC,SAAS,IACjE,QAAQ,WAAW,SAAS;GAElC,MAAM,gBAAgB,QAAQ,4BACzB,0CAA0C,QAAQ,UAAU;GACjE,MAAM,cAAc,gCAAgC;IAClD,YAAY,QAAQ;IACpB,uBAAuB,QAAQ;IAC/B,OAAO,eAAe;IACtB,eAAe,eAAe;IAC9B;GACF,CAAC;GAED,QAAQ,aAAa,IAAI,wBAAwB,YAAY,SAAS,CAAC;GACvE,oBAAoB,QAAQ,cAAc,4BAA4B,wBAAwB;GAC9F,IAAI,iBAAiB,MAAM;IACzB,oBAAoB,QAAQ,cAAc,SAAS,cAAc,KAAK;IACtE,oBAAoB,QAAQ,cAAc,iBAAiB,cAAc,aAAa;GACxF;EACF;QACK,IAAI,CAAC,QAAQ,kBAAkB,QAAQ,WAAW,aAAa,QAAQ,YAAY,QAAQ,WAAW,SAAS,QAAQ,QAAQ,CAAC,QAAQ,aAAa,IAAI,sBAAsB,GACpL,QAAQ,aAAa,IAAI,wBAAwB,gBAAgB,QAAQ,UAAU,CAAC;CAGtF,OAAO,QAAQ,WAAW,QAAQ,WAAW,SAAS,gBAAgB,OAAO,IAAI,QAAQ,SAAS;AACpG;;;;;;AAOA,SAAS,iCAAiC,SAG/B;CACT,MAAM,UAAU,IAAI,IAAI,QAAQ,eAAe,QAAQ,UAAU;CACjE,MAAM,6BAA6B,QAAQ,WAAW,aAAa,IAAI,sBAAsB;CAC7F,IAAI,8BAA8B,QAAQ,CAAC,QAAQ,aAAa,IAAI,sBAAsB,GACxF,QAAQ,aAAa,IAAI,wBAAwB,0BAA0B;CAE7E,KAAK,MAAM,kBAAkB;EAAC;EAAS;EAAiB;CAA0B,GAAY;EAC5F,MAAM,eAAe,oBAAoB,QAAQ,WAAW,cAAc,cAAc;EACxF,IAAI,gBAAgB,QAAQ,CAAC,oBAAoB,QAAQ,cAAc,cAAc,GACnF,oBAAoB,QAAQ,cAAc,gBAAgB,YAAY;CAE1E;CACA,OAAO,QAAQ,WAAW,QAAQ,WAAW,SAAS,gBAAgB,OAAO,IAAI,QAAQ,SAAS;AACpG;AAEA,SAAS,yBAAyB,aAAuD;CACvF,IAAI,gBAAgB,iBAAiB,gBAAgB,eACnD,OAAO;CAET,IAAI,+BAA+B,WAAW,GAC5C,OAAO;CAET,IAAI,+BAA+B,WAAW,GAC5C,OAAO;CAET,OAAO;AACT;AAaA,eAAe,8CAA8C,SAQzC;CAClB,MAAM,UAAU,iCAAiC;EAC/C,aAAa,QAAQ;EACrB,eAAe,QAAQ;EACvB,YAAY,QAAQ;EACpB,0BAA0B;EAC1B,uBAAuB,QAAQ;EAC/B,gBAAgB,QAAQ;EACxB,kCAAkC,QAAQ;CAC5C,CAAC;CACD,IAAI,QAAQ,gBAAgB,WAC1B,OAAO;CAGT,MAAM,gBAAgB,IAAI,IAAI,SAAS,QAAQ,UAAU;CAEzD,IAAI,EADiC,QAAQ,WAAW,WAAW,cAAc,WAC5C,4BAA4B,aAAa,GAC5E,OAAO;CAGT,MAAM,2BAA2B,MAAM,QAAQ,4BAA4B,QAAQ,UAAU;CAC7F,OAAO,iCAAiC;EACtC,aAAa,QAAQ;EACrB,eAAe,QAAQ;EACvB,YAAY,QAAQ;EACpB;EACA,uBAAuB,QAAQ;EAC/B,gBAAgB,QAAQ;EACxB,kCAAkC,QAAQ;CAC5C,CAAC;AACH;AAEA,eAAsB,sBAAsB,SAQT;CACjC,IAAI,QAAQ,cAAc,MACxB,OAAO;EAAE,MAAM;EAAY,KAAK,QAAQ;CAAc;CAGxD,MAAM,SAAS,yBAAyB,QAAQ,WAAW;CAC3D,IAAI,QAAQ,gBAKV,IAAI,QAAQ,gBAAgB;MAEtB,IADqB,IAAI,QAAQ,eAAe,QAAQ,UAC7C,CAAC,CAAC,WAAW,QAAQ,WAAW,QAC7C,OAAO;GACL,MAAM;GACN,KAAK,iCAAiC;IACpC,eAAe,QAAQ;IACvB,YAAY,QAAQ;GACtB,CAAC;EACH;CAAA,OAKF,OAAO;EACL,MAAM;EACN,KAAK,QAAQ;CACf;CAGJ,IAAI,WAAW,QACb,OAAO;EAAE,MAAM;EAAY,KAAK,QAAQ;CAAc;CAExD,IAAI,WAAW,sBACb,OAAO;EACL,MAAM;EACN,KAAK,iCAAiC;GACpC,eAAe,QAAQ;GACvB,YAAY,QAAQ;EACtB,CAAC;CACH;CAGF,IAAI,WAAW,qBAAqB;EAClC,IAAI,QAAQ,gBACV,OAAO;GAAE,MAAM;GAAY,KAAK,QAAQ;EAAc;EAExD,MAAM,kBAAkB,QAAQ,WAAW,aAAa,IAAI,sBAAsB;EAClF,IAAI,mBAAmB,MACrB,OAAO;GAAE,MAAM;GAAY,KAAK,QAAQ;EAAc;EAExD,MAAM,qBAAqB,IAAI,IAAI,iBAAiB,QAAQ,UAAU;EACtE,MAAM,qBAAqB,iCAAiC;GAC1D,YAAY,QAAQ;GACpB;EACF,CAAC;EACD,IAAI,sBAAsB,MACxB,OAAO;GAAE,MAAM;GAAY,KAAK;EAAgB;EAElD,IAAI,QAAQ,mBAAmB,cAAc;EAC7C,IAAI,gBAAgB,mBAAmB,cAAc;EACrD,IAAI,2BAA2B,mBAAmB,cAAc;EAChE,IAAI,SAAS,QAAQ,iBAAiB,MAAM;GAC1C,MAAM,yBAAyB,MAAM,QAAQ,4BAA4B,QAAQ,UAAU;GAC3F,UAAU,uBAAuB;GACjC,kBAAkB,uBAAuB;EAC3C;EACA,6BAA6B,QAAQ,WAAW,SAAS;EACzD,OAAO;GACL,MAAM;GACN,aAAa,mBAAmB,mBAAmB,SAAS;GAC5D;GACA;GACA;EACF;CACF;CAEA,IACE,QAAQ,gBAAgB,YACrB,QAAQ,gBAAgB,YACxB,QAAQ,gBAAgB,gBACxB,QAAQ,gBAAgB,WAE3B,MAAM,IAAI,uBAAuB,0DAA0D;EACzF,aAAa,QAAQ;EACrB;CACF,CAAC;CAKH,MAAM,mCAAmC,QAAQ,iBAC7C,IAAI,IAAI,QAAQ,YAAY,QAAQ,UAAU,CAAC,CAAC,SAAS,IACzD;CAEJ,OAAO;EACL,MAAM;EACN,KAAK,MAAM,8CAA8C;GACvD,aAAa,QAAQ;GACrB,eAAe,QAAQ;GACvB,YAAY,QAAQ;GACpB,uBAAuB,QAAQ;GAC/B,6BAA6B,QAAQ;GACrC,gBAAgB,QAAQ;GACxB;EACF,CAAC;CACH;AACF;AAEA,SAAS,6BAA6B,YAAiB,oBAA+D;CAQpH,OAAO;EACL,OAPY,oBAAoB,WAAW,cAAc,OAAO,KAC7D,oBAAoB,mBAAmB,cAAc,OAAO;EAO/D,eANoB,oBAAoB,WAAW,cAAc,eAAe,KAC7E,oBAAoB,mBAAmB,cAAc,eAAe;EAMvE,0BAL+B,oBAAoB,WAAW,cAAc,0BAA0B,KACnG,oBAAoB,mBAAmB,cAAc,0BAA0B;CAKpF;AACF;AAEA,SAAS,qCAAqC,SAI/B;CACb,IAAI,QAAQ,mBAAmB,WAAW,QAAQ,WAAW,QAC3D,OAAO,QAAQ;CAEjB,IACE,QAAQ,cAAc,SAAS,QAC5B,QAAQ,cAAc,iBAAiB,QACvC,QAAQ,cAAc,4BAA4B,MAErD,OAAO;CAET,MAAM,8BAA8B,IAAI,IAAI,QAAQ,cAAc,0BAA0B,QAAQ,UAAU;CAC9G,IAAI,4BAA4B,WAAW,QAAQ,WAAW,QAC5D,OAAO;CAET,OAAO,IAAI,IACT,GAAG,QAAQ,mBAAmB,WAAW,QAAQ,mBAAmB,SAAS,QAAQ,mBAAmB,QACxG,4BAA4B,MAC9B;AACF;AAEA,SAAS,iCAAiC,SAMjC;CACP,MAAM,gBAAgB,6BAA6B,QAAQ,YAAY,QAAQ,kBAAkB;CACjG,MAAM,gCAAgC,qCAAqC;EACzE,YAAY,QAAQ;EACpB,oBAAoB,QAAQ;EAC5B;CACF,CAAC;CACD,IAAI,iCAAiC,MACnC,OAAO;CAET,OAAO;EACL,oBAAoB;EACpB;CACF;AACF"}
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { crossDomainAuthQueryParams, planRedirectToHandler } from "./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 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
+ expect(url.searchParams.get("after_auth_return_to")).toBe(expectedReturnTo);
21
+ expect(url.searchParams.get(crossDomainAuthQueryParams.state)).toBe("outer-state");
22
+ expect(url.searchParams.get(crossDomainAuthQueryParams.codeChallenge)).toBe("outer-challenge");
23
+ expect(url.searchParams.get(crossDomainAuthQueryParams.afterCallbackRedirectUrl)).toBe("https://customer.example.test/dashboard");
24
+ }
25
+ describe("password-flow redirect continuation", () => {
26
+ 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(crossDomainAuthQueryParams.state, "outer-state");
31
+ signInUrl.searchParams.set(crossDomainAuthQueryParams.codeChallenge, "outer-challenge");
32
+ signInUrl.searchParams.set(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
+ expect(finalSignInUrl.searchParams.has("code")).toBe(false);
54
+ });
55
+ 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
+ expect(result.searchParams.has("after_auth_return_to")).toBe(false);
63
+ expect(result.searchParams.has("code")).toBe(false);
64
+ });
65
+ 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
+ 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
+ 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
+ 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
+ export {};
86
+
87
+ //# sourceMappingURL=redirect-page-urls.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect-page-urls.test.js","names":[],"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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"}