@shipfox/client-auth 6.0.2 → 7.0.0

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.
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 41 files with swc (106.33ms)
2
+ Successfully compiled: 41 files with swc (157.55ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @shipfox/client-auth
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 61309db: Adds a Node-safe redirect-context entrypoint for shared authentication continuation parsing.
8
+
9
+ ## 6.0.3
10
+
11
+ ### Patch Changes
12
+
13
+ - e6f831e: Reject deeply encoded authentication and invitation redirect paths before they can bypass path validation.
14
+
3
15
  ## 6.0.2
4
16
 
5
17
  ### Patch Changes
@@ -1,3 +1,10 @@
1
+ export interface ResolvedRedirectPath {
2
+ pathname: string;
3
+ redirect: string;
4
+ target: URL;
5
+ }
6
+ export declare function resolveRedirectPath(value: unknown): ResolvedRedirectPath | undefined;
7
+ export declare function isAuthPath(pathname: string): boolean;
1
8
  export declare function sanitizeRedirectPath(value: unknown): string | undefined;
2
9
  /**
3
10
  * Returns the destination used by the shared logout route.
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-target.d.ts","sourceRoot":"","sources":["../../src/components/redirect-target.ts"],"names":[],"mappings":"AAkCA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIvE;AAOD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOjE"}
1
+ {"version":3,"file":"redirect-target.d.ts","sourceRoot":"","sources":["../../src/components/redirect-target.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,GAAG,CAAC;CACb;AAyBD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,GAAG,SAAS,CAyBpF;AAMD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEpD;AAID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIvE;AAOD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUjE"}
@@ -3,7 +3,28 @@ const LOGIN_PATH = '/auth/login';
3
3
  const INVITATION_ACCEPT_PATH = '/invitations/accept';
4
4
  const DEFAULT_LOGOUT_REDIRECT = LOGIN_PATH;
5
5
  const TRAILING_SLASHES = /\/+$/;
6
- function resolveRedirectPath(value) {
6
+ const MAX_PATH_DECODE_ITERATIONS = 10;
7
+ // Repeated decoding can reveal `.`/`..` segments or a `//host` prefix that were
8
+ // hidden behind extra encoding layers (e.g. `%25252e%25252e` -> `..`). Those only
9
+ // get collapsed and re-checked against the origin by feeding the fully-decoded,
10
+ // percent-free string back through the URL parser; without this, a multiply
11
+ // encoded `/safe/../auth/login` or `/safe/../invitations/accept?token=...`
12
+ // evades the pathname checks below even though this loop "sees" the real path.
13
+ function decodePathToFixedPoint(pathname) {
14
+ let current = pathname;
15
+ for(let iteration = 0; iteration < MAX_PATH_DECODE_ITERATIONS; iteration += 1){
16
+ let decoded;
17
+ try {
18
+ decoded = decodeURIComponent(current);
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ if (decoded === current) return current;
23
+ current = decoded;
24
+ }
25
+ return undefined;
26
+ }
27
+ export function resolveRedirectPath(value) {
7
28
  if (typeof value !== 'string' || !value.startsWith('/')) return undefined;
8
29
  let decoded;
9
30
  try {
@@ -18,23 +39,36 @@ function resolveRedirectPath(value) {
18
39
  return undefined;
19
40
  }
20
41
  if (target.origin !== REDIRECT_ORIGIN) return undefined;
21
- return target;
42
+ const decodedPathname = decodePathToFixedPoint(target.pathname);
43
+ if (decodedPathname === undefined) return undefined;
44
+ let canonical;
45
+ try {
46
+ canonical = new URL(decodedPathname, REDIRECT_ORIGIN);
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ if (canonical.origin !== REDIRECT_ORIGIN) return undefined;
51
+ return {
52
+ pathname: canonical.pathname,
53
+ redirect: formatRedirectPath(target),
54
+ target
55
+ };
22
56
  }
23
57
  function formatRedirectPath(target) {
24
58
  return `${target.pathname}${target.search}${target.hash}`;
25
59
  }
26
- function isAuthPath(pathname) {
60
+ export function isAuthPath(pathname) {
27
61
  return pathname === '/auth' || pathname.startsWith('/auth/');
28
62
  }
29
63
  // Resolves and canonicalizes an internal path before returning it, so browser URL
30
64
  // parsing cannot turn a seemingly safe path into an external or auth route.
31
65
  export function sanitizeRedirectPath(value) {
32
- const target = resolveRedirectPath(value);
33
- if (!target || isAuthPath(target.pathname)) return undefined;
34
- return formatRedirectPath(target);
66
+ const resolved = resolveRedirectPath(value);
67
+ if (!resolved || isAuthPath(resolved.pathname)) return undefined;
68
+ return resolved.redirect;
35
69
  }
36
- function containsInvitationToken(target) {
37
- const normalizedPathname = target.pathname.replace(TRAILING_SLASHES, '') || '/';
70
+ function containsInvitationToken(target, pathname) {
71
+ const normalizedPathname = pathname.replace(TRAILING_SLASHES, '') || '/';
38
72
  return normalizedPathname === INVITATION_ACCEPT_PATH && target.searchParams.has('token');
39
73
  }
40
74
  /**
@@ -43,12 +77,12 @@ function containsInvitationToken(target) {
43
77
  * Login is the only auth destination allowed, and invitation tokens never
44
78
  * survive this boundary. Invalid values fail closed to login.
45
79
  */ export function sanitizeLogoutRedirectPath(value) {
46
- const target = resolveRedirectPath(value);
47
- if (!target) return DEFAULT_LOGOUT_REDIRECT;
48
- if (isAuthPath(target.pathname) || containsInvitationToken(target)) {
80
+ const resolved = resolveRedirectPath(value);
81
+ if (!resolved) return DEFAULT_LOGOUT_REDIRECT;
82
+ if (isAuthPath(resolved.pathname) || containsInvitationToken(resolved.target, resolved.pathname)) {
49
83
  return DEFAULT_LOGOUT_REDIRECT;
50
84
  }
51
- return formatRedirectPath(target);
85
+ return resolved.redirect;
52
86
  }
53
87
 
54
88
  //# sourceMappingURL=redirect-target.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/redirect-target.ts"],"sourcesContent":["const REDIRECT_ORIGIN = 'https://shipfox-redirect.invalid';\nconst LOGIN_PATH = '/auth/login';\nconst INVITATION_ACCEPT_PATH = '/invitations/accept';\nconst DEFAULT_LOGOUT_REDIRECT = LOGIN_PATH;\nconst TRAILING_SLASHES = /\\/+$/;\n\nfunction resolveRedirectPath(value: unknown): URL | undefined {\n if (typeof value !== 'string' || !value.startsWith('/')) return undefined;\n let decoded: string;\n try {\n decoded = decodeURIComponent(value);\n } catch {\n return undefined;\n }\n let target: URL;\n try {\n target = new URL(decoded, REDIRECT_ORIGIN);\n } catch {\n return undefined;\n }\n if (target.origin !== REDIRECT_ORIGIN) return undefined;\n return target;\n}\n\nfunction formatRedirectPath(target: URL): string {\n return `${target.pathname}${target.search}${target.hash}`;\n}\n\nfunction isAuthPath(pathname: string): boolean {\n return pathname === '/auth' || pathname.startsWith('/auth/');\n}\n\n// Resolves and canonicalizes an internal path before returning it, so browser URL\n// parsing cannot turn a seemingly safe path into an external or auth route.\nexport function sanitizeRedirectPath(value: unknown): string | undefined {\n const target = resolveRedirectPath(value);\n if (!target || isAuthPath(target.pathname)) return undefined;\n return formatRedirectPath(target);\n}\n\nfunction containsInvitationToken(target: URL): boolean {\n const normalizedPathname = target.pathname.replace(TRAILING_SLASHES, '') || '/';\n return normalizedPathname === INVITATION_ACCEPT_PATH && target.searchParams.has('token');\n}\n\n/**\n * Returns the destination used by the shared logout route.\n *\n * Login is the only auth destination allowed, and invitation tokens never\n * survive this boundary. Invalid values fail closed to login.\n */\nexport function sanitizeLogoutRedirectPath(value: unknown): string {\n const target = resolveRedirectPath(value);\n if (!target) return DEFAULT_LOGOUT_REDIRECT;\n if (isAuthPath(target.pathname) || containsInvitationToken(target)) {\n return DEFAULT_LOGOUT_REDIRECT;\n }\n return formatRedirectPath(target);\n}\n"],"names":["REDIRECT_ORIGIN","LOGIN_PATH","INVITATION_ACCEPT_PATH","DEFAULT_LOGOUT_REDIRECT","TRAILING_SLASHES","resolveRedirectPath","value","startsWith","undefined","decoded","decodeURIComponent","target","URL","origin","formatRedirectPath","pathname","search","hash","isAuthPath","sanitizeRedirectPath","containsInvitationToken","normalizedPathname","replace","searchParams","has","sanitizeLogoutRedirectPath"],"mappings":"AAAA,MAAMA,kBAAkB;AACxB,MAAMC,aAAa;AACnB,MAAMC,yBAAyB;AAC/B,MAAMC,0BAA0BF;AAChC,MAAMG,mBAAmB;AAEzB,SAASC,oBAAoBC,KAAc;IACzC,IAAI,OAAOA,UAAU,YAAY,CAACA,MAAMC,UAAU,CAAC,MAAM,OAAOC;IAChE,IAAIC;IACJ,IAAI;QACFA,UAAUC,mBAAmBJ;IAC/B,EAAE,OAAM;QACN,OAAOE;IACT;IACA,IAAIG;IACJ,IAAI;QACFA,SAAS,IAAIC,IAAIH,SAAST;IAC5B,EAAE,OAAM;QACN,OAAOQ;IACT;IACA,IAAIG,OAAOE,MAAM,KAAKb,iBAAiB,OAAOQ;IAC9C,OAAOG;AACT;AAEA,SAASG,mBAAmBH,MAAW;IACrC,OAAO,GAAGA,OAAOI,QAAQ,GAAGJ,OAAOK,MAAM,GAAGL,OAAOM,IAAI,EAAE;AAC3D;AAEA,SAASC,WAAWH,QAAgB;IAClC,OAAOA,aAAa,WAAWA,SAASR,UAAU,CAAC;AACrD;AAEA,kFAAkF;AAClF,4EAA4E;AAC5E,OAAO,SAASY,qBAAqBb,KAAc;IACjD,MAAMK,SAASN,oBAAoBC;IACnC,IAAI,CAACK,UAAUO,WAAWP,OAAOI,QAAQ,GAAG,OAAOP;IACnD,OAAOM,mBAAmBH;AAC5B;AAEA,SAASS,wBAAwBT,MAAW;IAC1C,MAAMU,qBAAqBV,OAAOI,QAAQ,CAACO,OAAO,CAAClB,kBAAkB,OAAO;IAC5E,OAAOiB,uBAAuBnB,0BAA0BS,OAAOY,YAAY,CAACC,GAAG,CAAC;AAClF;AAEA;;;;;CAKC,GACD,OAAO,SAASC,2BAA2BnB,KAAc;IACvD,MAAMK,SAASN,oBAAoBC;IACnC,IAAI,CAACK,QAAQ,OAAOR;IACpB,IAAIe,WAAWP,OAAOI,QAAQ,KAAKK,wBAAwBT,SAAS;QAClE,OAAOR;IACT;IACA,OAAOW,mBAAmBH;AAC5B"}
1
+ {"version":3,"sources":["../../src/components/redirect-target.ts"],"sourcesContent":["const REDIRECT_ORIGIN = 'https://shipfox-redirect.invalid';\nconst LOGIN_PATH = '/auth/login';\nconst INVITATION_ACCEPT_PATH = '/invitations/accept';\nconst DEFAULT_LOGOUT_REDIRECT = LOGIN_PATH;\nconst TRAILING_SLASHES = /\\/+$/;\nconst MAX_PATH_DECODE_ITERATIONS = 10;\n\nexport interface ResolvedRedirectPath {\n pathname: string;\n redirect: string;\n target: URL;\n}\n\n// Repeated decoding can reveal `.`/`..` segments or a `//host` prefix that were\n// hidden behind extra encoding layers (e.g. `%25252e%25252e` -> `..`). Those only\n// get collapsed and re-checked against the origin by feeding the fully-decoded,\n// percent-free string back through the URL parser; without this, a multiply\n// encoded `/safe/../auth/login` or `/safe/../invitations/accept?token=...`\n// evades the pathname checks below even though this loop \"sees\" the real path.\nfunction decodePathToFixedPoint(pathname: string): string | undefined {\n let current = pathname;\n\n for (let iteration = 0; iteration < MAX_PATH_DECODE_ITERATIONS; iteration += 1) {\n let decoded: string;\n try {\n decoded = decodeURIComponent(current);\n } catch {\n return undefined;\n }\n if (decoded === current) return current;\n current = decoded;\n }\n\n return undefined;\n}\n\nexport function resolveRedirectPath(value: unknown): ResolvedRedirectPath | undefined {\n if (typeof value !== 'string' || !value.startsWith('/')) return undefined;\n let decoded: string;\n try {\n decoded = decodeURIComponent(value);\n } catch {\n return undefined;\n }\n let target: URL;\n try {\n target = new URL(decoded, REDIRECT_ORIGIN);\n } catch {\n return undefined;\n }\n if (target.origin !== REDIRECT_ORIGIN) return undefined;\n const decodedPathname = decodePathToFixedPoint(target.pathname);\n if (decodedPathname === undefined) return undefined;\n let canonical: URL;\n try {\n canonical = new URL(decodedPathname, REDIRECT_ORIGIN);\n } catch {\n return undefined;\n }\n if (canonical.origin !== REDIRECT_ORIGIN) return undefined;\n return {pathname: canonical.pathname, redirect: formatRedirectPath(target), target};\n}\n\nfunction formatRedirectPath(target: URL): string {\n return `${target.pathname}${target.search}${target.hash}`;\n}\n\nexport function isAuthPath(pathname: string): boolean {\n return pathname === '/auth' || pathname.startsWith('/auth/');\n}\n\n// Resolves and canonicalizes an internal path before returning it, so browser URL\n// parsing cannot turn a seemingly safe path into an external or auth route.\nexport function sanitizeRedirectPath(value: unknown): string | undefined {\n const resolved = resolveRedirectPath(value);\n if (!resolved || isAuthPath(resolved.pathname)) return undefined;\n return resolved.redirect;\n}\n\nfunction containsInvitationToken(target: URL, pathname: string): boolean {\n const normalizedPathname = pathname.replace(TRAILING_SLASHES, '') || '/';\n return normalizedPathname === INVITATION_ACCEPT_PATH && target.searchParams.has('token');\n}\n\n/**\n * Returns the destination used by the shared logout route.\n *\n * Login is the only auth destination allowed, and invitation tokens never\n * survive this boundary. Invalid values fail closed to login.\n */\nexport function sanitizeLogoutRedirectPath(value: unknown): string {\n const resolved = resolveRedirectPath(value);\n if (!resolved) return DEFAULT_LOGOUT_REDIRECT;\n if (\n isAuthPath(resolved.pathname) ||\n containsInvitationToken(resolved.target, resolved.pathname)\n ) {\n return DEFAULT_LOGOUT_REDIRECT;\n }\n return resolved.redirect;\n}\n"],"names":["REDIRECT_ORIGIN","LOGIN_PATH","INVITATION_ACCEPT_PATH","DEFAULT_LOGOUT_REDIRECT","TRAILING_SLASHES","MAX_PATH_DECODE_ITERATIONS","decodePathToFixedPoint","pathname","current","iteration","decoded","decodeURIComponent","undefined","resolveRedirectPath","value","startsWith","target","URL","origin","decodedPathname","canonical","redirect","formatRedirectPath","search","hash","isAuthPath","sanitizeRedirectPath","resolved","containsInvitationToken","normalizedPathname","replace","searchParams","has","sanitizeLogoutRedirectPath"],"mappings":"AAAA,MAAMA,kBAAkB;AACxB,MAAMC,aAAa;AACnB,MAAMC,yBAAyB;AAC/B,MAAMC,0BAA0BF;AAChC,MAAMG,mBAAmB;AACzB,MAAMC,6BAA6B;AAQnC,gFAAgF;AAChF,kFAAkF;AAClF,gFAAgF;AAChF,4EAA4E;AAC5E,2EAA2E;AAC3E,+EAA+E;AAC/E,SAASC,uBAAuBC,QAAgB;IAC9C,IAAIC,UAAUD;IAEd,IAAK,IAAIE,YAAY,GAAGA,YAAYJ,4BAA4BI,aAAa,EAAG;QAC9E,IAAIC;QACJ,IAAI;YACFA,UAAUC,mBAAmBH;QAC/B,EAAE,OAAM;YACN,OAAOI;QACT;QACA,IAAIF,YAAYF,SAAS,OAAOA;QAChCA,UAAUE;IACZ;IAEA,OAAOE;AACT;AAEA,OAAO,SAASC,oBAAoBC,KAAc;IAChD,IAAI,OAAOA,UAAU,YAAY,CAACA,MAAMC,UAAU,CAAC,MAAM,OAAOH;IAChE,IAAIF;IACJ,IAAI;QACFA,UAAUC,mBAAmBG;IAC/B,EAAE,OAAM;QACN,OAAOF;IACT;IACA,IAAII;IACJ,IAAI;QACFA,SAAS,IAAIC,IAAIP,SAASV;IAC5B,EAAE,OAAM;QACN,OAAOY;IACT;IACA,IAAII,OAAOE,MAAM,KAAKlB,iBAAiB,OAAOY;IAC9C,MAAMO,kBAAkBb,uBAAuBU,OAAOT,QAAQ;IAC9D,IAAIY,oBAAoBP,WAAW,OAAOA;IAC1C,IAAIQ;IACJ,IAAI;QACFA,YAAY,IAAIH,IAAIE,iBAAiBnB;IACvC,EAAE,OAAM;QACN,OAAOY;IACT;IACA,IAAIQ,UAAUF,MAAM,KAAKlB,iBAAiB,OAAOY;IACjD,OAAO;QAACL,UAAUa,UAAUb,QAAQ;QAAEc,UAAUC,mBAAmBN;QAASA;IAAM;AACpF;AAEA,SAASM,mBAAmBN,MAAW;IACrC,OAAO,GAAGA,OAAOT,QAAQ,GAAGS,OAAOO,MAAM,GAAGP,OAAOQ,IAAI,EAAE;AAC3D;AAEA,OAAO,SAASC,WAAWlB,QAAgB;IACzC,OAAOA,aAAa,WAAWA,SAASQ,UAAU,CAAC;AACrD;AAEA,kFAAkF;AAClF,4EAA4E;AAC5E,OAAO,SAASW,qBAAqBZ,KAAc;IACjD,MAAMa,WAAWd,oBAAoBC;IACrC,IAAI,CAACa,YAAYF,WAAWE,SAASpB,QAAQ,GAAG,OAAOK;IACvD,OAAOe,SAASN,QAAQ;AAC1B;AAEA,SAASO,wBAAwBZ,MAAW,EAAET,QAAgB;IAC5D,MAAMsB,qBAAqBtB,SAASuB,OAAO,CAAC1B,kBAAkB,OAAO;IACrE,OAAOyB,uBAAuB3B,0BAA0Bc,OAAOe,YAAY,CAACC,GAAG,CAAC;AAClF;AAEA;;;;;CAKC,GACD,OAAO,SAASC,2BAA2BnB,KAAc;IACvD,MAAMa,WAAWd,oBAAoBC;IACrC,IAAI,CAACa,UAAU,OAAOxB;IACtB,IACEsB,WAAWE,SAASpB,QAAQ,KAC5BqB,wBAAwBD,SAASX,MAAM,EAAEW,SAASpB,QAAQ,GAC1D;QACA,OAAOJ;IACT;IACA,OAAOwB,SAASN,QAAQ;AAC1B"}
@@ -1,4 +1,4 @@
1
1
  export { AuthActions, AuthShell, type AuthShellProps } from '@shipfox/client-shell/runtime';
2
2
  export { EmailCodeVerification, type EmailCodeVerificationProps, } from './components/email-code-verification.js';
3
- export { parseRedirectContext, type RedirectContext } from './components/redirect-context.js';
3
+ export { parseRedirectContext, type RedirectContext } from './redirect-context.js';
4
4
  //# sourceMappingURL=continuation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"continuation.d.ts","sourceRoot":"","sources":["../src/continuation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,+BAA+B,CAAC;AAC1F,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAC,oBAAoB,EAAE,KAAK,eAAe,EAAC,MAAM,kCAAkC,CAAC"}
1
+ {"version":3,"file":"continuation.d.ts","sourceRoot":"","sources":["../src/continuation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,+BAA+B,CAAC;AAC1F,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAC,oBAAoB,EAAE,KAAK,eAAe,EAAC,MAAM,uBAAuB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  export { AuthActions, AuthShell } from '@shipfox/client-shell/runtime';
2
2
  export { EmailCodeVerification } from './components/email-code-verification.js';
3
- export { parseRedirectContext } from './components/redirect-context.js';
3
+ export { parseRedirectContext } from './redirect-context.js';
4
4
 
5
5
  //# sourceMappingURL=continuation.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/continuation.ts"],"sourcesContent":["export {AuthActions, AuthShell, type AuthShellProps} from '@shipfox/client-shell/runtime';\nexport {\n EmailCodeVerification,\n type EmailCodeVerificationProps,\n} from './components/email-code-verification.js';\nexport {parseRedirectContext, type RedirectContext} from './components/redirect-context.js';\n"],"names":["AuthActions","AuthShell","EmailCodeVerification","parseRedirectContext"],"mappings":"AAAA,SAAQA,WAAW,EAAEC,SAAS,QAA4B,gCAAgC;AAC1F,SACEC,qBAAqB,QAEhB,0CAA0C;AACjD,SAAQC,oBAAoB,QAA6B,mCAAmC"}
1
+ {"version":3,"sources":["../src/continuation.ts"],"sourcesContent":["export {AuthActions, AuthShell, type AuthShellProps} from '@shipfox/client-shell/runtime';\nexport {\n EmailCodeVerification,\n type EmailCodeVerificationProps,\n} from './components/email-code-verification.js';\nexport {parseRedirectContext, type RedirectContext} from './redirect-context.js';\n"],"names":["AuthActions","AuthShell","EmailCodeVerification","parseRedirectContext"],"mappings":"AAAA,SAAQA,WAAW,EAAEC,SAAS,QAA4B,gCAAgC;AAC1F,SACEC,qBAAqB,QAEhB,0CAA0C;AACjD,SAAQC,oBAAoB,QAA6B,wBAAwB"}
@@ -1,5 +1,5 @@
1
1
  import { pendingInvitation, usePreviewInvitation } from '@shipfox/client-invitations';
2
- import { parseRedirectContext } from '#/components/redirect-context.js';
2
+ import { parseRedirectContext } from '#/redirect-context.js';
3
3
  export function extractInvitationToken(redirect) {
4
4
  return parseRedirectContext(redirect).invitationToken;
5
5
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pages/invitation-context.ts"],"sourcesContent":["import {\n type InvitationPreview,\n pendingInvitation,\n usePreviewInvitation,\n} from '@shipfox/client-invitations';\nimport {parseRedirectContext} from '#/components/redirect-context.js';\n\nexport function extractInvitationToken(redirect: unknown): string | undefined {\n return parseRedirectContext(redirect).invitationToken;\n}\n\nexport function useInvitationContext(token: string | undefined) {\n return usePreviewInvitation(token);\n}\n\nexport {type InvitationPreview, pendingInvitation};\n"],"names":["pendingInvitation","usePreviewInvitation","parseRedirectContext","extractInvitationToken","redirect","invitationToken","useInvitationContext","token"],"mappings":"AAAA,SAEEA,iBAAiB,EACjBC,oBAAoB,QACf,8BAA8B;AACrC,SAAQC,oBAAoB,QAAO,mCAAmC;AAEtE,OAAO,SAASC,uBAAuBC,QAAiB;IACtD,OAAOF,qBAAqBE,UAAUC,eAAe;AACvD;AAEA,OAAO,SAASC,qBAAqBC,KAAyB;IAC5D,OAAON,qBAAqBM;AAC9B;AAEA,SAAgCP,iBAAiB,GAAE"}
1
+ {"version":3,"sources":["../../src/pages/invitation-context.ts"],"sourcesContent":["import {\n type InvitationPreview,\n pendingInvitation,\n usePreviewInvitation,\n} from '@shipfox/client-invitations';\nimport {parseRedirectContext} from '#/redirect-context.js';\n\nexport function extractInvitationToken(redirect: unknown): string | undefined {\n return parseRedirectContext(redirect).invitationToken;\n}\n\nexport function useInvitationContext(token: string | undefined) {\n return usePreviewInvitation(token);\n}\n\nexport {type InvitationPreview, pendingInvitation};\n"],"names":["pendingInvitation","usePreviewInvitation","parseRedirectContext","extractInvitationToken","redirect","invitationToken","useInvitationContext","token"],"mappings":"AAAA,SAEEA,iBAAiB,EACjBC,oBAAoB,QACf,8BAA8B;AACrC,SAAQC,oBAAoB,QAAO,wBAAwB;AAE3D,OAAO,SAASC,uBAAuBC,QAAiB;IACtD,OAAOF,qBAAqBE,UAAUC,eAAe;AACvD;AAEA,OAAO,SAASC,qBAAqBC,KAAyB;IAC5D,OAAON,qBAAqBM;AAC9B;AAEA,SAAgCP,iBAAiB,GAAE"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect-context.d.ts","sourceRoot":"","sources":["../src/redirect-context.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAQpE"}
@@ -0,0 +1,19 @@
1
+ import { isAuthPath, resolveRedirectPath } from './components/redirect-target.js';
2
+ const INVITATION_ACCEPT_PATH = '/invitations/accept';
3
+ /**
4
+ * Separates a safe post-authentication destination from an invitation token.
5
+ * The token never remains in `returnTo`, so callers can keep it in their
6
+ * short-lived invitation flow instead of forwarding it through generic redirects.
7
+ */ export function parseRedirectContext(value) {
8
+ const resolved = resolveRedirectPath(value);
9
+ if (!resolved || isAuthPath(resolved.pathname)) return {};
10
+ if (resolved.pathname !== INVITATION_ACCEPT_PATH) return {
11
+ returnTo: resolved.redirect
12
+ };
13
+ const invitationToken = resolved.target.searchParams.get('token');
14
+ return invitationToken ? {
15
+ invitationToken
16
+ } : {};
17
+ }
18
+
19
+ //# sourceMappingURL=redirect-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/redirect-context.ts"],"sourcesContent":["import {isAuthPath, resolveRedirectPath} from './components/redirect-target.js';\n\nconst INVITATION_ACCEPT_PATH = '/invitations/accept';\n\nexport interface RedirectContext {\n invitationToken?: string;\n returnTo?: string;\n}\n\n/**\n * Separates a safe post-authentication destination from an invitation token.\n * The token never remains in `returnTo`, so callers can keep it in their\n * short-lived invitation flow instead of forwarding it through generic redirects.\n */\nexport function parseRedirectContext(value: unknown): RedirectContext {\n const resolved = resolveRedirectPath(value);\n if (!resolved || isAuthPath(resolved.pathname)) return {};\n\n if (resolved.pathname !== INVITATION_ACCEPT_PATH) return {returnTo: resolved.redirect};\n\n const invitationToken = resolved.target.searchParams.get('token');\n return invitationToken ? {invitationToken} : {};\n}\n"],"names":["isAuthPath","resolveRedirectPath","INVITATION_ACCEPT_PATH","parseRedirectContext","value","resolved","pathname","returnTo","redirect","invitationToken","target","searchParams","get"],"mappings":"AAAA,SAAQA,UAAU,EAAEC,mBAAmB,QAAO,kCAAkC;AAEhF,MAAMC,yBAAyB;AAO/B;;;;CAIC,GACD,OAAO,SAASC,qBAAqBC,KAAc;IACjD,MAAMC,WAAWJ,oBAAoBG;IACrC,IAAI,CAACC,YAAYL,WAAWK,SAASC,QAAQ,GAAG,OAAO,CAAC;IAExD,IAAID,SAASC,QAAQ,KAAKJ,wBAAwB,OAAO;QAACK,UAAUF,SAASG,QAAQ;IAAA;IAErF,MAAMC,kBAAkBJ,SAASK,MAAM,CAACC,YAAY,CAACC,GAAG,CAAC;IACzD,OAAOH,kBAAkB;QAACA;IAAe,IAAI,CAAC;AAChD"}