@shipfox/client-auth 3.0.1 → 4.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.
Files changed (71) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/dist/components/auth-shell.d.ts +6 -4
  4. package/dist/components/auth-shell.d.ts.map +1 -1
  5. package/dist/components/auth-shell.js +6 -1
  6. package/dist/components/auth-shell.js.map +1 -1
  7. package/dist/components/email-code-verification.d.ts +20 -0
  8. package/dist/components/email-code-verification.d.ts.map +1 -0
  9. package/dist/components/email-code-verification.js +136 -0
  10. package/dist/components/email-code-verification.js.map +1 -0
  11. package/dist/components/redirect-context.d.ts +11 -0
  12. package/dist/components/redirect-context.d.ts.map +1 -0
  13. package/dist/components/redirect-context.js +22 -0
  14. package/dist/components/redirect-context.js.map +1 -0
  15. package/dist/components/redirect-target.d.ts.map +1 -1
  16. package/dist/components/redirect-target.js +13 -12
  17. package/dist/components/redirect-target.js.map +1 -1
  18. package/dist/continuation.d.ts +4 -0
  19. package/dist/continuation.d.ts.map +1 -0
  20. package/dist/continuation.js +5 -0
  21. package/dist/continuation.js.map +1 -0
  22. package/dist/hooks/api/login-auth.d.ts.map +1 -1
  23. package/dist/hooks/api/login-auth.js +4 -28
  24. package/dist/hooks/api/login-auth.js.map +1 -1
  25. package/dist/hooks/api/logout-auth.d.ts.map +1 -1
  26. package/dist/hooks/api/logout-auth.js +4 -14
  27. package/dist/hooks/api/logout-auth.js.map +1 -1
  28. package/dist/hooks/api/password-reset-auth.d.ts.map +1 -1
  29. package/dist/hooks/api/password-reset-auth.js +4 -23
  30. package/dist/hooks/api/password-reset-auth.js.map +1 -1
  31. package/dist/hooks/api/verify-email-auth.d.ts.map +1 -1
  32. package/dist/hooks/api/verify-email-auth.js +4 -23
  33. package/dist/hooks/api/verify-email-auth.js.map +1 -1
  34. package/dist/pages/invitation-context.d.ts +0 -5
  35. package/dist/pages/invitation-context.d.ts.map +1 -1
  36. package/dist/pages/invitation-context.js +3 -19
  37. package/dist/pages/invitation-context.js.map +1 -1
  38. package/dist/pages/signup-page.d.ts.map +1 -1
  39. package/dist/pages/signup-page.js +50 -144
  40. package/dist/pages/signup-page.js.map +1 -1
  41. package/dist/state/auth.d.ts +1 -1
  42. package/dist/state/auth.d.ts.map +1 -1
  43. package/dist/state/auth.js +1 -1
  44. package/dist/state/auth.js.map +1 -1
  45. package/dist/tsconfig.test.tsbuildinfo +1 -1
  46. package/package.json +9 -39
  47. package/src/components/auth-provider.test.tsx +173 -3
  48. package/src/components/auth-shell.tsx +20 -5
  49. package/src/components/email-code-verification.test.tsx +159 -0
  50. package/src/components/email-code-verification.tsx +177 -0
  51. package/src/components/redirect-context.test.ts +51 -0
  52. package/src/components/redirect-context.ts +26 -0
  53. package/src/components/redirect-target.test.ts +8 -0
  54. package/src/components/redirect-target.ts +13 -12
  55. package/src/continuation.test.ts +15 -0
  56. package/src/continuation.ts +6 -0
  57. package/src/hooks/api/login-auth.ts +4 -28
  58. package/src/hooks/api/logout-auth.ts +4 -10
  59. package/src/hooks/api/password-reset-auth.ts +4 -23
  60. package/src/hooks/api/verify-email-auth.ts +4 -23
  61. package/src/pages/invitation-context.ts +2 -20
  62. package/src/pages/signup-page.tsx +33 -129
  63. package/src/state/auth.ts +1 -0
  64. package/src/state/last-workspace.test.ts +16 -0
  65. package/tsconfig.build.tsbuildinfo +1 -1
  66. package/dist/pages/email-verification-resend-model.d.ts +0 -6
  67. package/dist/pages/email-verification-resend-model.d.ts.map +0 -1
  68. package/dist/pages/email-verification-resend-model.js +0 -11
  69. package/dist/pages/email-verification-resend-model.js.map +0 -1
  70. package/src/pages/email-verification-resend-model.test.ts +0 -42
  71. package/src/pages/email-verification-resend-model.ts +0 -15
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 36 files with swc (473.63ms)
2
+ Successfully compiled: 38 files with swc (507ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @shipfox/client-auth
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 20e4feb: Adds provider-neutral authentication continuation primitives for reusable shells, email-code verification, and redirect context.
8
+
9
+ ### Patch Changes
10
+
11
+ - 11b10f7: Prevents private React Query data from persisting across logout and authenticated user changes.
12
+ - Updated dependencies [2e5b718]
13
+ - Updated dependencies [769d919]
14
+ - Updated dependencies [6b4a575]
15
+ - Updated dependencies [11b10f7]
16
+ - Updated dependencies [781a45b]
17
+ - @shipfox/client-ui@4.0.0
18
+ - @shipfox/client-shell@4.0.0
19
+ - @shipfox/api-auth-dto@7.1.0
20
+ - @shipfox/client-api@4.0.0
21
+
3
22
  ## 3.0.1
4
23
 
5
24
  ### Patch Changes
@@ -1,11 +1,13 @@
1
- import type { PropsWithChildren, ReactNode } from 'react';
2
- interface AuthShellProps {
1
+ import { type HeaderProps } from '@shipfox/react-ui/typography';
2
+ import type { PropsWithChildren, ReactNode, Ref } from 'react';
3
+ export interface AuthShellProps {
3
4
  title: string;
4
5
  description: string;
5
6
  children: ReactNode;
6
7
  className?: string;
8
+ headingProps?: Omit<HeaderProps, 'children' | 'id'> | undefined;
9
+ headingRef?: Ref<HTMLHeadingElement> | undefined;
7
10
  }
8
- export declare function AuthShell({ title, description, children, className }: AuthShellProps): import("react").JSX.Element;
11
+ export declare function AuthShell({ title, description, children, className, headingProps, headingRef, }: AuthShellProps): import("react").JSX.Element;
9
12
  export declare function AuthActions({ children }: PropsWithChildren): import("react").JSX.Element;
10
- export {};
11
13
  //# sourceMappingURL=auth-shell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-shell.d.ts","sourceRoot":"","sources":["../../src/components/auth-shell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,iBAAiB,EAAE,SAAS,EAAC,MAAM,OAAO,CAAC;AAExD,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,SAAS,CAAC,EAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAC,EAAE,cAAc,+BAqClF;AAED,wBAAgB,WAAW,CAAC,EAAC,QAAQ,EAAC,EAAE,iBAAiB,+BAExD"}
1
+ {"version":3,"file":"auth-shell.d.ts","sourceRoot":"","sources":["../../src/components/auth-shell.tsx"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,WAAW,EAAO,MAAM,8BAA8B,CAAC;AAC5E,OAAO,KAAK,EAAC,iBAAiB,EAAE,SAAS,EAAE,GAAG,EAAC,MAAM,OAAO,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;IAChE,UAAU,CAAC,EAAE,GAAG,CAAC,kBAAkB,CAAC,GAAG,SAAS,CAAC;CAClD;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,WAAW,EACX,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,UAAU,GACX,EAAE,cAAc,+BA2ChB;AAED,wBAAgB,WAAW,CAAC,EAAC,QAAQ,EAAC,EAAE,iBAAiB,+BAExD"}
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Icon } from '@shipfox/react-ui/icon';
3
3
  import { Header, Text } from '@shipfox/react-ui/typography';
4
- export function AuthShell({ title, description, children, className }) {
4
+ export function AuthShell({ title, description, children, className, headingProps, headingRef }) {
5
5
  return /*#__PURE__*/ _jsxs("main", {
6
6
  className: "relative flex min-h-screen items-center justify-center overflow-hidden bg-background-subtle-base px-24 py-32 max-[520px]:items-start max-[520px]:px-20 max-[520px]:pb-32 max-[520px]:pt-56",
7
7
  children: [
@@ -36,6 +36,11 @@ export function AuthShell({ title, description, children, className }) {
36
36
  /*#__PURE__*/ _jsx(Header, {
37
37
  id: "auth-title",
38
38
  variant: "h1",
39
+ tabIndex: -1,
40
+ ...headingProps,
41
+ ...headingRef ? {
42
+ ref: headingRef
43
+ } : {},
39
44
  children: title
40
45
  }),
41
46
  /*#__PURE__*/ _jsx(Text, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/auth-shell.tsx"],"sourcesContent":["import {Icon} from '@shipfox/react-ui/icon';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport type {PropsWithChildren, ReactNode} from 'react';\n\ninterface AuthShellProps {\n title: string;\n description: string;\n children: ReactNode;\n className?: string;\n}\n\nexport function AuthShell({title, description, children, className}: AuthShellProps) {\n return (\n <main className=\"relative flex min-h-screen items-center justify-center overflow-hidden bg-background-subtle-base px-24 py-32 max-[520px]:items-start max-[520px]:px-20 max-[520px]:pb-32 max-[520px]:pt-56\">\n <div\n className=\"pointer-events-none absolute left-1/2 top-0 h-[380px] w-full max-w-[880px] -translate-x-1/2 -translate-y-[120px] opacity-55 max-[520px]:-translate-y-[170px] max-[520px]:opacity-35\"\n aria-hidden=\"true\"\n style={{\n maskImage:\n 'radial-gradient(ellipse 85% 100% at 50% 0%, #000 0%, #000 24%, transparent 78%)',\n WebkitMaskImage:\n 'radial-gradient(ellipse 85% 100% at 50% 0%, #000 0%, #000 24%, transparent 78%)',\n }}\n >\n <div className=\"h-full w-full bg-[radial-gradient(circle,rgba(230,62,0,0.48)_1.6px,transparent_1.8px)] bg-[length:44px_44px]\" />\n </div>\n\n <section\n className={className ?? 'relative flex w-full max-w-[384px] flex-col items-stretch gap-28'}\n aria-labelledby=\"auth-title\"\n >\n <div className=\"flex flex-col items-center gap-16\">\n <div className=\"flex size-64 items-center justify-center rounded-12 border border-border-neutral-base bg-background-neutral-base p-10 shadow-button-neutral\">\n <Icon name=\"shipfox\" className=\"size-42 text-background-highlight-interactive\" />\n </div>\n <div className=\"flex min-w-[128px] flex-col items-center gap-4 text-center\">\n <Header id=\"auth-title\" variant=\"h1\">\n {title}\n </Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-subtle\">\n {description}\n </Text>\n </div>\n </div>\n {children}\n </section>\n </main>\n );\n}\n\nexport function AuthActions({children}: PropsWithChildren) {\n return <div className=\"flex flex-col gap-20\">{children}</div>;\n}\n"],"names":["Icon","Header","Text","AuthShell","title","description","children","className","main","div","aria-hidden","style","maskImage","WebkitMaskImage","section","aria-labelledby","name","id","variant","size","AuthActions"],"mappings":";AAAA,SAAQA,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,MAAM,EAAEC,IAAI,QAAO,+BAA+B;AAU1D,OAAO,SAASC,UAAU,EAACC,KAAK,EAAEC,WAAW,EAAEC,QAAQ,EAAEC,SAAS,EAAiB;IACjF,qBACE,MAACC;QAAKD,WAAU;;0BACd,KAACE;gBACCF,WAAU;gBACVG,eAAY;gBACZC,OAAO;oBACLC,WACE;oBACFC,iBACE;gBACJ;0BAEA,cAAA,KAACJ;oBAAIF,WAAU;;;0BAGjB,MAACO;gBACCP,WAAWA,aAAa;gBACxBQ,mBAAgB;;kCAEhB,MAACN;wBAAIF,WAAU;;0CACb,KAACE;gCAAIF,WAAU;0CACb,cAAA,KAACP;oCAAKgB,MAAK;oCAAUT,WAAU;;;0CAEjC,MAACE;gCAAIF,WAAU;;kDACb,KAACN;wCAAOgB,IAAG;wCAAaC,SAAQ;kDAC7Bd;;kDAEH,KAACF;wCAAKiB,MAAK;wCAAKZ,WAAU;kDACvBF;;;;;;oBAINC;;;;;AAIT;AAEA,OAAO,SAASc,YAAY,EAACd,QAAQ,EAAoB;IACvD,qBAAO,KAACG;QAAIF,WAAU;kBAAwBD;;AAChD"}
1
+ {"version":3,"sources":["../../src/components/auth-shell.tsx"],"sourcesContent":["import {Icon} from '@shipfox/react-ui/icon';\nimport {Header, type HeaderProps, Text} from '@shipfox/react-ui/typography';\nimport type {PropsWithChildren, ReactNode, Ref} from 'react';\n\nexport interface AuthShellProps {\n title: string;\n description: string;\n children: ReactNode;\n className?: string;\n headingProps?: Omit<HeaderProps, 'children' | 'id'> | undefined;\n headingRef?: Ref<HTMLHeadingElement> | undefined;\n}\n\nexport function AuthShell({\n title,\n description,\n children,\n className,\n headingProps,\n headingRef,\n}: AuthShellProps) {\n return (\n <main className=\"relative flex min-h-screen items-center justify-center overflow-hidden bg-background-subtle-base px-24 py-32 max-[520px]:items-start max-[520px]:px-20 max-[520px]:pb-32 max-[520px]:pt-56\">\n <div\n className=\"pointer-events-none absolute left-1/2 top-0 h-[380px] w-full max-w-[880px] -translate-x-1/2 -translate-y-[120px] opacity-55 max-[520px]:-translate-y-[170px] max-[520px]:opacity-35\"\n aria-hidden=\"true\"\n style={{\n maskImage:\n 'radial-gradient(ellipse 85% 100% at 50% 0%, #000 0%, #000 24%, transparent 78%)',\n WebkitMaskImage:\n 'radial-gradient(ellipse 85% 100% at 50% 0%, #000 0%, #000 24%, transparent 78%)',\n }}\n >\n <div className=\"h-full w-full bg-[radial-gradient(circle,rgba(230,62,0,0.48)_1.6px,transparent_1.8px)] bg-[length:44px_44px]\" />\n </div>\n\n <section\n className={className ?? 'relative flex w-full max-w-[384px] flex-col items-stretch gap-28'}\n aria-labelledby=\"auth-title\"\n >\n <div className=\"flex flex-col items-center gap-16\">\n <div className=\"flex size-64 items-center justify-center rounded-12 border border-border-neutral-base bg-background-neutral-base p-10 shadow-button-neutral\">\n <Icon name=\"shipfox\" className=\"size-42 text-background-highlight-interactive\" />\n </div>\n <div className=\"flex min-w-[128px] flex-col items-center gap-4 text-center\">\n <Header\n id=\"auth-title\"\n variant=\"h1\"\n tabIndex={-1}\n {...headingProps}\n {...(headingRef ? ({ref: headingRef} as HeaderProps) : {})}\n >\n {title}\n </Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-subtle\">\n {description}\n </Text>\n </div>\n </div>\n {children}\n </section>\n </main>\n );\n}\n\nexport function AuthActions({children}: PropsWithChildren) {\n return <div className=\"flex flex-col gap-20\">{children}</div>;\n}\n"],"names":["Icon","Header","Text","AuthShell","title","description","children","className","headingProps","headingRef","main","div","aria-hidden","style","maskImage","WebkitMaskImage","section","aria-labelledby","name","id","variant","tabIndex","ref","size","AuthActions"],"mappings":";AAAA,SAAQA,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,MAAM,EAAoBC,IAAI,QAAO,+BAA+B;AAY5E,OAAO,SAASC,UAAU,EACxBC,KAAK,EACLC,WAAW,EACXC,QAAQ,EACRC,SAAS,EACTC,YAAY,EACZC,UAAU,EACK;IACf,qBACE,MAACC;QAAKH,WAAU;;0BACd,KAACI;gBACCJ,WAAU;gBACVK,eAAY;gBACZC,OAAO;oBACLC,WACE;oBACFC,iBACE;gBACJ;0BAEA,cAAA,KAACJ;oBAAIJ,WAAU;;;0BAGjB,MAACS;gBACCT,WAAWA,aAAa;gBACxBU,mBAAgB;;kCAEhB,MAACN;wBAAIJ,WAAU;;0CACb,KAACI;gCAAIJ,WAAU;0CACb,cAAA,KAACP;oCAAKkB,MAAK;oCAAUX,WAAU;;;0CAEjC,MAACI;gCAAIJ,WAAU;;kDACb,KAACN;wCACCkB,IAAG;wCACHC,SAAQ;wCACRC,UAAU,CAAC;wCACV,GAAGb,YAAY;wCACf,GAAIC,aAAc;4CAACa,KAAKb;wCAAU,IAAoB,CAAC,CAAC;kDAExDL;;kDAEH,KAACF;wCAAKqB,MAAK;wCAAKhB,WAAU;kDACvBF;;;;;;oBAINC;;;;;AAIT;AAEA,OAAO,SAASkB,YAAY,EAAClB,QAAQ,EAAoB;IACvD,qBAAO,KAACK;QAAIJ,WAAU;kBAAwBD;;AAChD"}
@@ -0,0 +1,20 @@
1
+ import type { HeaderProps } from '@shipfox/react-ui/typography';
2
+ import type { ReactNode, Ref } from 'react';
3
+ export interface EmailCodeVerificationProps {
4
+ destination: string;
5
+ expiresAt?: string | undefined;
6
+ nextResendAvailableAt?: string | undefined;
7
+ isResending?: boolean;
8
+ isVerifying?: boolean;
9
+ error?: string | undefined;
10
+ title?: string;
11
+ description?: string;
12
+ children?: ReactNode;
13
+ headingProps?: Omit<HeaderProps, 'children' | 'id'> | undefined;
14
+ headingRef?: Ref<HTMLHeadingElement> | undefined;
15
+ onResend: () => void | Promise<void>;
16
+ onUseAnotherEmail: () => void;
17
+ onVerify: (code: string) => void | Promise<void>;
18
+ }
19
+ export declare function EmailCodeVerification({ destination, expiresAt, nextResendAvailableAt, isResending, isVerifying, error, title, description, children, headingProps, headingRef, onResend, onUseAnotherEmail, onVerify, }: EmailCodeVerificationProps): import("react").JSX.Element;
20
+ //# sourceMappingURL=email-code-verification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-code-verification.d.ts","sourceRoot":"","sources":["../../src/components/email-code-verification.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,8BAA8B,CAAC;AAC9D,OAAO,KAAK,EAAY,SAAS,EAAE,GAAG,EAAC,MAAM,OAAO,CAAC;AAMrD,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,YAAY,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,IAAI,CAAC,GAAG,SAAS,CAAC;IAChE,UAAU,CAAC,EAAE,GAAG,CAAC,kBAAkB,CAAC,GAAG,SAAS,CAAC;IACjD,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,iBAAiB,EAAE,MAAM,IAAI,CAAC;IAC9B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAQD,wBAAgB,qBAAqB,CAAC,EACpC,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,WAAmB,EACnB,WAAmB,EACnB,KAAK,EACL,KAA0B,EAC1B,WAA2E,EAC3E,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,iBAAiB,EACjB,QAAQ,GACT,EAAE,0BAA0B,+BAgI5B"}
@@ -0,0 +1,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from '@shipfox/react-ui/button';
3
+ import { Callout } from '@shipfox/react-ui/callout';
4
+ import { FormField, FormFieldInput } from '@shipfox/react-ui/form-field';
5
+ import { useCallback, useEffect, useRef, useState } from 'react';
6
+ import { AuthShell } from './auth-shell.js';
7
+ const EIGHT_DIGIT_CODE_RE = /^\d{8}$/u;
8
+ function timestamp(value) {
9
+ if (!value) return undefined;
10
+ const parsed = Date.parse(value);
11
+ return Number.isNaN(parsed) ? undefined : parsed;
12
+ }
13
+ export function EmailCodeVerification({ destination, expiresAt, nextResendAvailableAt, isResending = false, isVerifying = false, error, title = 'Check your email', description = `We sent an eight-digit verification code to ${destination}.`, children, headingProps, headingRef, onResend, onUseAnotherEmail, onVerify }) {
14
+ const [code, setCode] = useState('');
15
+ const [now, setNow] = useState(()=>Date.now());
16
+ const [codeError, setCodeError] = useState();
17
+ const localHeadingRef = useRef(null);
18
+ const resendAvailableAt = timestamp(nextResendAvailableAt);
19
+ const expiration = timestamp(expiresAt);
20
+ const resendRemainingSeconds = resendAvailableAt ? Math.max(0, Math.ceil((resendAvailableAt - now) / 1_000)) : 0;
21
+ const isResendCoolingDown = resendRemainingSeconds > 0;
22
+ const isExpired = expiration !== undefined && expiration <= now;
23
+ const isPending = isResending || isVerifying;
24
+ useEffect(()=>{
25
+ const isPending = (tickNow)=>resendAvailableAt !== undefined && resendAvailableAt > tickNow || expiration !== undefined && expiration > tickNow;
26
+ const tickNow = Date.now();
27
+ setNow(tickNow);
28
+ if (!isPending(tickNow)) return;
29
+ const interval = window.setInterval(()=>{
30
+ const nextTick = Date.now();
31
+ setNow(nextTick);
32
+ if (!isPending(nextTick)) {
33
+ window.clearInterval(interval);
34
+ }
35
+ }, 1_000);
36
+ return ()=>window.clearInterval(interval);
37
+ }, [
38
+ expiration,
39
+ resendAvailableAt
40
+ ]);
41
+ useEffect(()=>{
42
+ localHeadingRef.current?.focus();
43
+ }, []);
44
+ const setHeadingRef = useCallback((heading)=>{
45
+ localHeadingRef.current = heading;
46
+ if (typeof headingRef === 'function') {
47
+ headingRef(heading);
48
+ } else if (headingRef) {
49
+ headingRef.current = heading;
50
+ }
51
+ }, [
52
+ headingRef
53
+ ]);
54
+ function submit(event) {
55
+ event.preventDefault();
56
+ if (isExpired || isVerifying) return;
57
+ if (!validateCode()) return;
58
+ void onVerify(code);
59
+ }
60
+ function validateCode() {
61
+ const error = EIGHT_DIGIT_CODE_RE.test(code) ? undefined : 'Enter the eight-digit verification code.';
62
+ setCodeError(error);
63
+ return error === undefined;
64
+ }
65
+ return /*#__PURE__*/ _jsxs(AuthShell, {
66
+ title: title,
67
+ description: description,
68
+ headingProps: headingProps,
69
+ headingRef: setHeadingRef,
70
+ children: [
71
+ error ? /*#__PURE__*/ _jsx(Callout, {
72
+ role: "alert",
73
+ type: "error",
74
+ children: error
75
+ }) : null,
76
+ isExpired ? /*#__PURE__*/ _jsx(Callout, {
77
+ role: "alert",
78
+ type: "error",
79
+ children: "This verification code has expired. Use another email to try again."
80
+ }) : null,
81
+ /*#__PURE__*/ _jsxs("form", {
82
+ className: "flex flex-col gap-8",
83
+ noValidate: true,
84
+ onSubmit: submit,
85
+ children: [
86
+ /*#__PURE__*/ _jsx(FormField, {
87
+ label: "Verification code",
88
+ id: "verification-code",
89
+ error: codeError,
90
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
91
+ autoComplete: "one-time-code",
92
+ inputMode: "numeric",
93
+ maxLength: 8,
94
+ pattern: "[0-9]{8}",
95
+ value: code,
96
+ onChange: (event)=>{
97
+ setCode(event.target.value.replace(/\D/gu, '').slice(0, 8));
98
+ setCodeError(undefined);
99
+ },
100
+ onBlur: validateCode
101
+ })
102
+ }),
103
+ /*#__PURE__*/ _jsx(Button, {
104
+ className: "w-full",
105
+ type: "submit",
106
+ disabled: isExpired,
107
+ isLoading: isVerifying,
108
+ children: "Verify email"
109
+ }),
110
+ /*#__PURE__*/ _jsx(Button, {
111
+ "aria-disabled": isResendCoolingDown ? true : undefined,
112
+ className: "w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70",
113
+ variant: "secondary",
114
+ type: "button",
115
+ isLoading: isResending,
116
+ onClick: ()=>{
117
+ if (!isResendCoolingDown && !isResending) void onResend();
118
+ },
119
+ children: isResending ? 'Sending email...' : isResendCoolingDown ? `Resend in ${resendRemainingSeconds}s` : 'Resend verification email'
120
+ })
121
+ ]
122
+ }),
123
+ /*#__PURE__*/ _jsx(Button, {
124
+ className: "w-full",
125
+ variant: "transparent",
126
+ type: "button",
127
+ disabled: isPending,
128
+ onClick: onUseAnotherEmail,
129
+ children: "Use another email"
130
+ }),
131
+ children
132
+ ]
133
+ });
134
+ }
135
+
136
+ //# sourceMappingURL=email-code-verification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/email-code-verification.tsx"],"sourcesContent":["import {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput} from '@shipfox/react-ui/form-field';\nimport type {HeaderProps} from '@shipfox/react-ui/typography';\nimport type {FormEvent, ReactNode, Ref} from 'react';\nimport {useCallback, useEffect, useRef, useState} from 'react';\nimport {AuthShell} from './auth-shell.js';\n\nconst EIGHT_DIGIT_CODE_RE = /^\\d{8}$/u;\n\nexport interface EmailCodeVerificationProps {\n destination: string;\n expiresAt?: string | undefined;\n nextResendAvailableAt?: string | undefined;\n isResending?: boolean;\n isVerifying?: boolean;\n error?: string | undefined;\n title?: string;\n description?: string;\n children?: ReactNode;\n headingProps?: Omit<HeaderProps, 'children' | 'id'> | undefined;\n headingRef?: Ref<HTMLHeadingElement> | undefined;\n onResend: () => void | Promise<void>;\n onUseAnotherEmail: () => void;\n onVerify: (code: string) => void | Promise<void>;\n}\n\nfunction timestamp(value: string | undefined): number | undefined {\n if (!value) return undefined;\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n\nexport function EmailCodeVerification({\n destination,\n expiresAt,\n nextResendAvailableAt,\n isResending = false,\n isVerifying = false,\n error,\n title = 'Check your email',\n description = `We sent an eight-digit verification code to ${destination}.`,\n children,\n headingProps,\n headingRef,\n onResend,\n onUseAnotherEmail,\n onVerify,\n}: EmailCodeVerificationProps) {\n const [code, setCode] = useState('');\n const [now, setNow] = useState(() => Date.now());\n const [codeError, setCodeError] = useState<string>();\n const localHeadingRef = useRef<HTMLHeadingElement>(null);\n const resendAvailableAt = timestamp(nextResendAvailableAt);\n const expiration = timestamp(expiresAt);\n const resendRemainingSeconds = resendAvailableAt\n ? Math.max(0, Math.ceil((resendAvailableAt - now) / 1_000))\n : 0;\n const isResendCoolingDown = resendRemainingSeconds > 0;\n const isExpired = expiration !== undefined && expiration <= now;\n const isPending = isResending || isVerifying;\n\n useEffect(() => {\n const isPending = (tickNow: number) =>\n (resendAvailableAt !== undefined && resendAvailableAt > tickNow) ||\n (expiration !== undefined && expiration > tickNow);\n\n const tickNow = Date.now();\n setNow(tickNow);\n if (!isPending(tickNow)) return;\n\n const interval = window.setInterval(() => {\n const nextTick = Date.now();\n setNow(nextTick);\n if (!isPending(nextTick)) {\n window.clearInterval(interval);\n }\n }, 1_000);\n return () => window.clearInterval(interval);\n }, [expiration, resendAvailableAt]);\n\n useEffect(() => {\n localHeadingRef.current?.focus();\n }, []);\n\n const setHeadingRef = useCallback(\n (heading: HTMLHeadingElement | null) => {\n localHeadingRef.current = heading;\n if (typeof headingRef === 'function') {\n headingRef(heading);\n } else if (headingRef) {\n headingRef.current = heading;\n }\n },\n [headingRef],\n );\n\n function submit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n if (isExpired || isVerifying) return;\n if (!validateCode()) return;\n void onVerify(code);\n }\n\n function validateCode() {\n const error = EIGHT_DIGIT_CODE_RE.test(code)\n ? undefined\n : 'Enter the eight-digit verification code.';\n setCodeError(error);\n return error === undefined;\n }\n\n return (\n <AuthShell\n title={title}\n description={description}\n headingProps={headingProps}\n headingRef={setHeadingRef}\n >\n {error ? (\n <Callout role=\"alert\" type=\"error\">\n {error}\n </Callout>\n ) : null}\n {isExpired ? (\n <Callout role=\"alert\" type=\"error\">\n This verification code has expired. Use another email to try again.\n </Callout>\n ) : null}\n <form className=\"flex flex-col gap-8\" noValidate onSubmit={submit}>\n <FormField label=\"Verification code\" id=\"verification-code\" error={codeError}>\n <FormFieldInput\n autoComplete=\"one-time-code\"\n inputMode=\"numeric\"\n maxLength={8}\n pattern=\"[0-9]{8}\"\n value={code}\n onChange={(event) => {\n setCode(event.target.value.replace(/\\D/gu, '').slice(0, 8));\n setCodeError(undefined);\n }}\n onBlur={validateCode}\n />\n </FormField>\n <Button className=\"w-full\" type=\"submit\" disabled={isExpired} isLoading={isVerifying}>\n Verify email\n </Button>\n <Button\n aria-disabled={isResendCoolingDown ? true : undefined}\n className=\"w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70\"\n variant=\"secondary\"\n type=\"button\"\n isLoading={isResending}\n onClick={() => {\n if (!isResendCoolingDown && !isResending) void onResend();\n }}\n >\n {isResending\n ? 'Sending email...'\n : isResendCoolingDown\n ? `Resend in ${resendRemainingSeconds}s`\n : 'Resend verification email'}\n </Button>\n </form>\n <Button\n className=\"w-full\"\n variant=\"transparent\"\n type=\"button\"\n disabled={isPending}\n onClick={onUseAnotherEmail}\n >\n Use another email\n </Button>\n {children}\n </AuthShell>\n );\n}\n"],"names":["Button","Callout","FormField","FormFieldInput","useCallback","useEffect","useRef","useState","AuthShell","EIGHT_DIGIT_CODE_RE","timestamp","value","undefined","parsed","Date","parse","Number","isNaN","EmailCodeVerification","destination","expiresAt","nextResendAvailableAt","isResending","isVerifying","error","title","description","children","headingProps","headingRef","onResend","onUseAnotherEmail","onVerify","code","setCode","now","setNow","codeError","setCodeError","localHeadingRef","resendAvailableAt","expiration","resendRemainingSeconds","Math","max","ceil","isResendCoolingDown","isExpired","isPending","tickNow","interval","window","setInterval","nextTick","clearInterval","current","focus","setHeadingRef","heading","submit","event","preventDefault","validateCode","test","role","type","form","className","noValidate","onSubmit","label","id","autoComplete","inputMode","maxLength","pattern","onChange","target","replace","slice","onBlur","disabled","isLoading","aria-disabled","variant","onClick"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export interface RedirectContext {
2
+ invitationToken?: string;
3
+ returnTo?: string;
4
+ }
5
+ /**
6
+ * Separates a safe post-authentication destination from an invitation token.
7
+ * The token never remains in `returnTo`, so callers can keep it in their
8
+ * short-lived invitation flow instead of forwarding it through generic redirects.
9
+ */
10
+ export declare function parseRedirectContext(value: unknown): RedirectContext;
11
+ //# sourceMappingURL=redirect-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect-context.d.ts","sourceRoot":"","sources":["../../src/components/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,CAWpE"}
@@ -0,0 +1,22 @@
1
+ import { sanitizeRedirectPath } from './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 redirect = sanitizeRedirectPath(value);
9
+ if (!redirect) return {};
10
+ const decoded = decodeURIComponent(redirect);
11
+ const [path, queryString = ''] = decoded.split('?', 2);
12
+ if (path !== INVITATION_ACCEPT_PATH) return {
13
+ returnTo: redirect
14
+ };
15
+ const params = new URLSearchParams(queryString);
16
+ const invitationToken = params.get('token');
17
+ return invitationToken ? {
18
+ invitationToken
19
+ } : {};
20
+ }
21
+
22
+ //# sourceMappingURL=redirect-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/redirect-context.ts"],"sourcesContent":["import {sanitizeRedirectPath} from './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 redirect = sanitizeRedirectPath(value);\n if (!redirect) return {};\n\n const decoded = decodeURIComponent(redirect);\n const [path, queryString = ''] = decoded.split('?', 2);\n if (path !== INVITATION_ACCEPT_PATH) return {returnTo: redirect};\n\n const params = new URLSearchParams(queryString);\n const invitationToken = params.get('token');\n return invitationToken ? {invitationToken} : {};\n}\n"],"names":["sanitizeRedirectPath","INVITATION_ACCEPT_PATH","parseRedirectContext","value","redirect","decoded","decodeURIComponent","path","queryString","split","returnTo","params","URLSearchParams","invitationToken","get"],"mappings":"AAAA,SAAQA,oBAAoB,QAAO,uBAAuB;AAE1D,MAAMC,yBAAyB;AAO/B;;;;CAIC,GACD,OAAO,SAASC,qBAAqBC,KAAc;IACjD,MAAMC,WAAWJ,qBAAqBG;IACtC,IAAI,CAACC,UAAU,OAAO,CAAC;IAEvB,MAAMC,UAAUC,mBAAmBF;IACnC,MAAM,CAACG,MAAMC,cAAc,EAAE,CAAC,GAAGH,QAAQI,KAAK,CAAC,KAAK;IACpD,IAAIF,SAASN,wBAAwB,OAAO;QAACS,UAAUN;IAAQ;IAE/D,MAAMO,SAAS,IAAIC,gBAAgBJ;IACnC,MAAMK,kBAAkBF,OAAOG,GAAG,CAAC;IACnC,OAAOD,kBAAkB;QAACA;IAAe,IAAI,CAAC;AAChD"}
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-target.d.ts","sourceRoot":"","sources":["../../src/components/redirect-target.ts"],"names":[],"mappings":"AAKA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAevE"}
1
+ {"version":3,"file":"redirect-target.d.ts","sourceRoot":"","sources":["../../src/components/redirect-target.ts"],"names":[],"mappings":"AAIA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAiBvE"}
@@ -1,22 +1,23 @@
1
- const SEARCH_OR_HASH_RE = /[?#]/u;
2
- // Returns the input string only if it is a same-origin internal path safe to
3
- // navigate to after authentication. We decode before checking so percent-encoded
4
- // variants like `/%61uth/login` cannot bypass the `/auth/*` rejection.
1
+ const REDIRECT_ORIGIN = 'https://shipfox-redirect.invalid';
2
+ // Resolves and canonicalizes an internal path before returning it, so browser URL
3
+ // parsing cannot turn a seemingly safe path into an external or auth route.
5
4
  export function sanitizeRedirectPath(value) {
6
- if (typeof value !== 'string') return undefined;
5
+ if (typeof value !== 'string' || !value.startsWith('/')) return undefined;
7
6
  let decoded;
8
7
  try {
9
8
  decoded = decodeURIComponent(value);
10
9
  } catch {
11
10
  return undefined;
12
11
  }
13
- if (!decoded.startsWith('/')) return undefined;
14
- if (decoded.startsWith('//')) return undefined;
15
- // Strip search/hash before the /auth check so /auth?token=x and /auth#x
16
- // cannot bypass the prefix match.
17
- const pathOnly = decoded.split(SEARCH_OR_HASH_RE, 1)[0] ?? decoded;
18
- if (pathOnly === '/auth' || pathOnly.startsWith('/auth/')) return undefined;
19
- return value;
12
+ let target;
13
+ try {
14
+ target = new URL(decoded, REDIRECT_ORIGIN);
15
+ } catch {
16
+ return undefined;
17
+ }
18
+ if (target.origin !== REDIRECT_ORIGIN) return undefined;
19
+ if (target.pathname === '/auth' || target.pathname.startsWith('/auth/')) return undefined;
20
+ return `${target.pathname}${target.search}${target.hash}`;
20
21
  }
21
22
 
22
23
  //# sourceMappingURL=redirect-target.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/redirect-target.ts"],"sourcesContent":["const SEARCH_OR_HASH_RE = /[?#]/u;\n\n// Returns the input string only if it is a same-origin internal path safe to\n// navigate to after authentication. We decode before checking so percent-encoded\n// variants like `/%61uth/login` cannot bypass the `/auth/*` rejection.\nexport function sanitizeRedirectPath(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n let decoded: string;\n try {\n decoded = decodeURIComponent(value);\n } catch {\n return undefined;\n }\n if (!decoded.startsWith('/')) return undefined;\n if (decoded.startsWith('//')) return undefined;\n // Strip search/hash before the /auth check so /auth?token=x and /auth#x\n // cannot bypass the prefix match.\n const pathOnly = decoded.split(SEARCH_OR_HASH_RE, 1)[0] ?? decoded;\n if (pathOnly === '/auth' || pathOnly.startsWith('/auth/')) return undefined;\n return value;\n}\n"],"names":["SEARCH_OR_HASH_RE","sanitizeRedirectPath","value","undefined","decoded","decodeURIComponent","startsWith","pathOnly","split"],"mappings":"AAAA,MAAMA,oBAAoB;AAE1B,6EAA6E;AAC7E,iFAAiF;AACjF,uEAAuE;AACvE,OAAO,SAASC,qBAAqBC,KAAc;IACjD,IAAI,OAAOA,UAAU,UAAU,OAAOC;IACtC,IAAIC;IACJ,IAAI;QACFA,UAAUC,mBAAmBH;IAC/B,EAAE,OAAM;QACN,OAAOC;IACT;IACA,IAAI,CAACC,QAAQE,UAAU,CAAC,MAAM,OAAOH;IACrC,IAAIC,QAAQE,UAAU,CAAC,OAAO,OAAOH;IACrC,wEAAwE;IACxE,kCAAkC;IAClC,MAAMI,WAAWH,QAAQI,KAAK,CAACR,mBAAmB,EAAE,CAAC,EAAE,IAAII;IAC3D,IAAIG,aAAa,WAAWA,SAASD,UAAU,CAAC,WAAW,OAAOH;IAClE,OAAOD;AACT"}
1
+ {"version":3,"sources":["../../src/components/redirect-target.ts"],"sourcesContent":["const REDIRECT_ORIGIN = 'https://shipfox-redirect.invalid';\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 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 if (target.pathname === '/auth' || target.pathname.startsWith('/auth/')) return undefined;\n return `${target.pathname}${target.search}${target.hash}`;\n}\n"],"names":["REDIRECT_ORIGIN","sanitizeRedirectPath","value","startsWith","undefined","decoded","decodeURIComponent","target","URL","origin","pathname","search","hash"],"mappings":"AAAA,MAAMA,kBAAkB;AAExB,kFAAkF;AAClF,4EAA4E;AAC5E,OAAO,SAASC,qBAAqBC,KAAc;IACjD,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,SAASL;IAC5B,EAAE,OAAM;QACN,OAAOI;IACT;IACA,IAAIG,OAAOE,MAAM,KAAKT,iBAAiB,OAAOI;IAC9C,IAAIG,OAAOG,QAAQ,KAAK,WAAWH,OAAOG,QAAQ,CAACP,UAAU,CAAC,WAAW,OAAOC;IAChF,OAAO,GAAGG,OAAOG,QAAQ,GAAGH,OAAOI,MAAM,GAAGJ,OAAOK,IAAI,EAAE;AAC3D"}
@@ -0,0 +1,4 @@
1
+ export { AuthActions, AuthShell, type AuthShellProps } from './components/auth-shell.js';
2
+ export { EmailCodeVerification, type EmailCodeVerificationProps, } from './components/email-code-verification.js';
3
+ export { parseRedirectContext, type RedirectContext } from './components/redirect-context.js';
4
+ //# sourceMappingURL=continuation.d.ts.map
@@ -0,0 +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,4BAA4B,CAAC;AACvF,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAC,oBAAoB,EAAE,KAAK,eAAe,EAAC,MAAM,kCAAkC,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { AuthActions, AuthShell } from './components/auth-shell.js';
2
+ export { EmailCodeVerification } from './components/email-code-verification.js';
3
+ export { parseRedirectContext } from './components/redirect-context.js';
4
+
5
+ //# sourceMappingURL=continuation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/continuation.ts"],"sourcesContent":["export {AuthActions, AuthShell, type AuthShellProps} from './components/auth-shell.js';\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,6BAA6B;AACvF,SACEC,qBAAqB,QAEhB,0CAA0C;AACjD,SAAQC,oBAAoB,QAA6B,mCAAmC"}
@@ -1 +1 @@
1
- {"version":3,"file":"login-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/login-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,YAAY,EAAmB,MAAM,uBAAuB,CAAC;AAQ1E,wBAAsB,SAAS,CAAC,IAAI,EAAE,YAAY;;;;;;;;;;;GAEjD;AAED,wBAAgB,YAAY;;;;;;;;;;;;;;YA4B3B"}
1
+ {"version":3,"file":"login-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/login-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,YAAY,EAAmB,MAAM,uBAAuB,CAAC;AAK1E,wBAAsB,SAAS,CAAC,IAAI,EAAE,YAAY;;;;;;;;;;;GAEjD;AAED,wBAAgB,YAAY;;;;;;;;;;;;;;YAO3B"}
@@ -1,9 +1,6 @@
1
1
  import { apiRequest } from '@shipfox/client-api';
2
- import { useMutation, useQueryClient } from '@tanstack/react-query';
3
- import { useSetAtom } from 'jotai';
4
- import { authStateAtom, toAuthenticatedState } from '#state/auth.js';
5
- import { authRefreshQueryKey } from './refresh-auth.js';
6
- import { listUserWorkspaces, userWorkspacesQueryKey } from './workspace-auth.js';
2
+ import { useMutation } from '@tanstack/react-query';
3
+ import { useAuthTransition } from '#state/auth.js';
7
4
  export async function loginAuth(body) {
8
5
  return await apiRequest('/auth/login', {
9
6
  method: 'POST',
@@ -11,31 +8,10 @@ export async function loginAuth(body) {
11
8
  });
12
9
  }
13
10
  export function useLoginAuth() {
14
- const queryClient = useQueryClient();
15
- const setState = useSetAtom(authStateAtom);
11
+ const { enterAuthenticated } = useAuthTransition();
16
12
  return useMutation({
17
13
  mutationFn: loginAuth,
18
- onSuccess: async (result)=>{
19
- queryClient.setQueryData(authRefreshQueryKey, result);
20
- // Resolve workspaces before flipping auth state to authenticated. A
21
- // single atomic setState avoids the intermediate window where the user
22
- // appears authenticated with zero workspaces, which sent users with
23
- // workspaces straight to `/setup/workspaces/new` after form login.
24
- let memberships = [];
25
- try {
26
- const workspaces = await queryClient.fetchQuery({
27
- queryKey: userWorkspacesQueryKey,
28
- queryFn: ()=>listUserWorkspaces(result.token),
29
- retry: false,
30
- staleTime: 0
31
- });
32
- memberships = workspaces.memberships;
33
- queryClient.setQueryData(userWorkspacesQueryKey, workspaces);
34
- } catch {
35
- // The user is authenticated even if workspace hydration fails.
36
- }
37
- setState(toAuthenticatedState(result, memberships));
38
- }
14
+ onSuccess: enterAuthenticated
39
15
  });
40
16
  }
41
17
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/api/login-auth.ts"],"sourcesContent":["import type {LoginBodyDto, LoginResponseDto} from '@shipfox/api-auth-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {useMutation, useQueryClient} from '@tanstack/react-query';\nimport {useSetAtom} from 'jotai';\nimport {authStateAtom, toAuthenticatedState} from '#state/auth.js';\nimport {authRefreshQueryKey} from './refresh-auth.js';\nimport {listUserWorkspaces, userWorkspacesQueryKey} from './workspace-auth.js';\n\nexport async function loginAuth(body: LoginBodyDto) {\n return await apiRequest<LoginResponseDto>('/auth/login', {method: 'POST', body});\n}\n\nexport function useLoginAuth() {\n const queryClient = useQueryClient();\n const setState = useSetAtom(authStateAtom);\n\n return useMutation({\n mutationFn: loginAuth,\n onSuccess: async (result) => {\n queryClient.setQueryData(authRefreshQueryKey, result);\n // Resolve workspaces before flipping auth state to authenticated. A\n // single atomic setState avoids the intermediate window where the user\n // appears authenticated with zero workspaces, which sent users with\n // workspaces straight to `/setup/workspaces/new` after form login.\n let memberships: Awaited<ReturnType<typeof listUserWorkspaces>>['memberships'] = [];\n try {\n const workspaces = await queryClient.fetchQuery({\n queryKey: userWorkspacesQueryKey,\n queryFn: () => listUserWorkspaces(result.token),\n retry: false,\n staleTime: 0,\n });\n memberships = workspaces.memberships;\n queryClient.setQueryData(userWorkspacesQueryKey, workspaces);\n } catch {\n // The user is authenticated even if workspace hydration fails.\n }\n setState(toAuthenticatedState(result, memberships));\n },\n });\n}\n"],"names":["apiRequest","useMutation","useQueryClient","useSetAtom","authStateAtom","toAuthenticatedState","authRefreshQueryKey","listUserWorkspaces","userWorkspacesQueryKey","loginAuth","body","method","useLoginAuth","queryClient","setState","mutationFn","onSuccess","result","setQueryData","memberships","workspaces","fetchQuery","queryKey","queryFn","token","retry","staleTime"],"mappings":"AACA,SAAQA,UAAU,QAAO,sBAAsB;AAC/C,SAAQC,WAAW,EAAEC,cAAc,QAAO,wBAAwB;AAClE,SAAQC,UAAU,QAAO,QAAQ;AACjC,SAAQC,aAAa,EAAEC,oBAAoB,QAAO,iBAAiB;AACnE,SAAQC,mBAAmB,QAAO,oBAAoB;AACtD,SAAQC,kBAAkB,EAAEC,sBAAsB,QAAO,sBAAsB;AAE/E,OAAO,eAAeC,UAAUC,IAAkB;IAChD,OAAO,MAAMV,WAA6B,eAAe;QAACW,QAAQ;QAAQD;IAAI;AAChF;AAEA,OAAO,SAASE;IACd,MAAMC,cAAcX;IACpB,MAAMY,WAAWX,WAAWC;IAE5B,OAAOH,YAAY;QACjBc,YAAYN;QACZO,WAAW,OAAOC;YAChBJ,YAAYK,YAAY,CAACZ,qBAAqBW;YAC9C,oEAAoE;YACpE,uEAAuE;YACvE,oEAAoE;YACpE,mEAAmE;YACnE,IAAIE,cAA6E,EAAE;YACnF,IAAI;gBACF,MAAMC,aAAa,MAAMP,YAAYQ,UAAU,CAAC;oBAC9CC,UAAUd;oBACVe,SAAS,IAAMhB,mBAAmBU,OAAOO,KAAK;oBAC9CC,OAAO;oBACPC,WAAW;gBACb;gBACAP,cAAcC,WAAWD,WAAW;gBACpCN,YAAYK,YAAY,CAACV,wBAAwBY;YACnD,EAAE,OAAM;YACN,+DAA+D;YACjE;YACAN,SAAST,qBAAqBY,QAAQE;QACxC;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/hooks/api/login-auth.ts"],"sourcesContent":["import type {LoginBodyDto, LoginResponseDto} from '@shipfox/api-auth-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {useMutation} from '@tanstack/react-query';\nimport {useAuthTransition} from '#state/auth.js';\n\nexport async function loginAuth(body: LoginBodyDto) {\n return await apiRequest<LoginResponseDto>('/auth/login', {method: 'POST', body});\n}\n\nexport function useLoginAuth() {\n const {enterAuthenticated} = useAuthTransition();\n\n return useMutation({\n mutationFn: loginAuth,\n onSuccess: enterAuthenticated,\n });\n}\n"],"names":["apiRequest","useMutation","useAuthTransition","loginAuth","body","method","useLoginAuth","enterAuthenticated","mutationFn","onSuccess"],"mappings":"AACA,SAAQA,UAAU,QAAO,sBAAsB;AAC/C,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SAAQC,iBAAiB,QAAO,iBAAiB;AAEjD,OAAO,eAAeC,UAAUC,IAAkB;IAChD,OAAO,MAAMJ,WAA6B,eAAe;QAACK,QAAQ;QAAQD;IAAI;AAChF;AAEA,OAAO,SAASE;IACd,MAAM,EAACC,kBAAkB,EAAC,GAAGL;IAE7B,OAAOD,YAAY;QACjBO,YAAYL;QACZM,WAAWF;IACb;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"logout-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/logout-auth.ts"],"names":[],"mappings":"AAcA,wBAAgB,aAAa,kFAW5B"}
1
+ {"version":3,"file":"logout-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/logout-auth.ts"],"names":[],"mappings":"AAYA,wBAAgB,aAAa,kFAO5B"}
@@ -1,8 +1,6 @@
1
1
  import { apiRequest } from '@shipfox/client-api';
2
- import { useMutation, useQueryClient } from '@tanstack/react-query';
3
- import { useSetAtom } from 'jotai';
4
- import { authStateAtom } from '#state/auth.js';
5
- import { authRefreshQueryKey } from './refresh-auth.js';
2
+ import { useMutation } from '@tanstack/react-query';
3
+ import { useAuthTransition } from '#state/auth.js';
6
4
  async function logoutAuth() {
7
5
  try {
8
6
  await apiRequest('/auth/logout', {
@@ -14,18 +12,10 @@ async function logoutAuth() {
14
12
  }
15
13
  }
16
14
  export function useLogoutAuth() {
17
- const queryClient = useQueryClient();
18
- const setState = useSetAtom(authStateAtom);
15
+ const { enterGuest } = useAuthTransition();
19
16
  return useMutation({
20
17
  mutationFn: logoutAuth,
21
- onSettled: ()=>{
22
- setState({
23
- status: 'guest'
24
- });
25
- queryClient.removeQueries({
26
- queryKey: authRefreshQueryKey
27
- });
28
- }
18
+ onSettled: enterGuest
29
19
  });
30
20
  }
31
21
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/api/logout-auth.ts"],"sourcesContent":["import {apiRequest} from '@shipfox/client-api';\nimport {useMutation, useQueryClient} from '@tanstack/react-query';\nimport {useSetAtom} from 'jotai';\nimport {authStateAtom} from '#state/auth.js';\nimport {authRefreshQueryKey} from './refresh-auth.js';\n\nasync function logoutAuth() {\n try {\n await apiRequest<void>('/auth/logout', {method: 'POST', body: {}});\n } catch {\n // Logout is best-effort: local session state must clear even if the API is offline.\n }\n}\n\nexport function useLogoutAuth() {\n const queryClient = useQueryClient();\n const setState = useSetAtom(authStateAtom);\n\n return useMutation({\n mutationFn: logoutAuth,\n onSettled: () => {\n setState({status: 'guest'});\n queryClient.removeQueries({queryKey: authRefreshQueryKey});\n },\n });\n}\n"],"names":["apiRequest","useMutation","useQueryClient","useSetAtom","authStateAtom","authRefreshQueryKey","logoutAuth","method","body","useLogoutAuth","queryClient","setState","mutationFn","onSettled","status","removeQueries","queryKey"],"mappings":"AAAA,SAAQA,UAAU,QAAO,sBAAsB;AAC/C,SAAQC,WAAW,EAAEC,cAAc,QAAO,wBAAwB;AAClE,SAAQC,UAAU,QAAO,QAAQ;AACjC,SAAQC,aAAa,QAAO,iBAAiB;AAC7C,SAAQC,mBAAmB,QAAO,oBAAoB;AAEtD,eAAeC;IACb,IAAI;QACF,MAAMN,WAAiB,gBAAgB;YAACO,QAAQ;YAAQC,MAAM,CAAC;QAAC;IAClE,EAAE,OAAM;IACN,oFAAoF;IACtF;AACF;AAEA,OAAO,SAASC;IACd,MAAMC,cAAcR;IACpB,MAAMS,WAAWR,WAAWC;IAE5B,OAAOH,YAAY;QACjBW,YAAYN;QACZO,WAAW;YACTF,SAAS;gBAACG,QAAQ;YAAO;YACzBJ,YAAYK,aAAa,CAAC;gBAACC,UAAUX;YAAmB;QAC1D;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/hooks/api/logout-auth.ts"],"sourcesContent":["import {apiRequest} from '@shipfox/client-api';\nimport {useMutation} from '@tanstack/react-query';\nimport {useAuthTransition} from '#state/auth.js';\n\nasync function logoutAuth() {\n try {\n await apiRequest<void>('/auth/logout', {method: 'POST', body: {}});\n } catch {\n // Logout is best-effort: local session state must clear even if the API is offline.\n }\n}\n\nexport function useLogoutAuth() {\n const {enterGuest} = useAuthTransition();\n\n return useMutation({\n mutationFn: logoutAuth,\n onSettled: enterGuest,\n });\n}\n"],"names":["apiRequest","useMutation","useAuthTransition","logoutAuth","method","body","useLogoutAuth","enterGuest","mutationFn","onSettled"],"mappings":"AAAA,SAAQA,UAAU,QAAO,sBAAsB;AAC/C,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SAAQC,iBAAiB,QAAO,iBAAiB;AAEjD,eAAeC;IACb,IAAI;QACF,MAAMH,WAAiB,gBAAgB;YAACI,QAAQ;YAAQC,MAAM,CAAC;QAAC;IAClE,EAAE,OAAM;IACN,oFAAoF;IACtF;AACF;AAEA,OAAO,SAASC;IACd,MAAM,EAACC,UAAU,EAAC,GAAGL;IAErB,OAAOD,YAAY;QACjBO,YAAYL;QACZM,WAAWF;IACb;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"password-reset-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/password-reset-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAE3B,2BAA2B,EAC5B,MAAM,uBAAuB,CAAC;AAQ/B,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,2BAA2B,iBAE3E;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,2BAA2B;;;;;;;;;;;GAK3E;AAED,wBAAgB,2BAA2B;;YAE1C;AAED,wBAAgB,2BAA2B;;;;;;;;;;;;;;YAuB1C"}
1
+ {"version":3,"file":"password-reset-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/password-reset-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAE3B,2BAA2B,EAC5B,MAAM,uBAAuB,CAAC;AAK/B,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,2BAA2B,iBAE3E;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,2BAA2B;;;;;;;;;;;GAK3E;AAED,wBAAgB,2BAA2B;;YAE1C;AAED,wBAAgB,2BAA2B;;;;;;;;;;;;;;YAO1C"}
@@ -1,9 +1,6 @@
1
1
  import { apiRequest } from '@shipfox/client-api';
2
- import { useMutation, useQueryClient } from '@tanstack/react-query';
3
- import { useSetAtom } from 'jotai';
4
- import { authStateAtom, toAuthenticatedState } from '#state/auth.js';
5
- import { authRefreshQueryKey } from './refresh-auth.js';
6
- import { listUserWorkspaces, userWorkspacesQueryKey } from './workspace-auth.js';
2
+ import { useMutation } from '@tanstack/react-query';
3
+ import { useAuthTransition } from '#state/auth.js';
7
4
  export async function requestPasswordReset(body) {
8
5
  await apiRequest('/auth/password-reset', {
9
6
  method: 'POST',
@@ -22,26 +19,10 @@ export function useRequestPasswordResetAuth() {
22
19
  });
23
20
  }
24
21
  export function useConfirmPasswordResetAuth() {
25
- const queryClient = useQueryClient();
26
- const setState = useSetAtom(authStateAtom);
22
+ const { enterAuthenticated } = useAuthTransition();
27
23
  return useMutation({
28
24
  mutationFn: confirmPasswordReset,
29
- onSuccess: async (result)=>{
30
- setState(toAuthenticatedState(result));
31
- queryClient.setQueryData(authRefreshQueryKey, result);
32
- try {
33
- const workspaces = await queryClient.fetchQuery({
34
- queryKey: userWorkspacesQueryKey,
35
- queryFn: ()=>listUserWorkspaces(result.token),
36
- retry: false,
37
- staleTime: 0
38
- });
39
- setState(toAuthenticatedState(result, workspaces.memberships));
40
- queryClient.setQueryData(userWorkspacesQueryKey, workspaces);
41
- } catch {
42
- // The user is authenticated even if workspace hydration fails.
43
- }
44
- }
25
+ onSuccess: enterAuthenticated
45
26
  });
46
27
  }
47
28
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/api/password-reset-auth.ts"],"sourcesContent":["import type {\n PasswordResetConfirmBodyDto,\n PasswordResetConfirmResponseDto,\n PasswordResetRequestBodyDto,\n} from '@shipfox/api-auth-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {useMutation, useQueryClient} from '@tanstack/react-query';\nimport {useSetAtom} from 'jotai';\nimport {authStateAtom, toAuthenticatedState} from '#state/auth.js';\nimport {authRefreshQueryKey} from './refresh-auth.js';\nimport {listUserWorkspaces, userWorkspacesQueryKey} from './workspace-auth.js';\n\nexport async function requestPasswordReset(body: PasswordResetRequestBodyDto) {\n await apiRequest<void>('/auth/password-reset', {method: 'POST', body});\n}\n\nexport async function confirmPasswordReset(body: PasswordResetConfirmBodyDto) {\n return await apiRequest<PasswordResetConfirmResponseDto>('/auth/password-reset/confirm', {\n method: 'POST',\n body,\n });\n}\n\nexport function useRequestPasswordResetAuth() {\n return useMutation({mutationFn: requestPasswordReset});\n}\n\nexport function useConfirmPasswordResetAuth() {\n const queryClient = useQueryClient();\n const setState = useSetAtom(authStateAtom);\n\n return useMutation({\n mutationFn: confirmPasswordReset,\n onSuccess: async (result) => {\n setState(toAuthenticatedState(result));\n queryClient.setQueryData(authRefreshQueryKey, result);\n try {\n const workspaces = await queryClient.fetchQuery({\n queryKey: userWorkspacesQueryKey,\n queryFn: () => listUserWorkspaces(result.token),\n retry: false,\n staleTime: 0,\n });\n setState(toAuthenticatedState(result, workspaces.memberships));\n queryClient.setQueryData(userWorkspacesQueryKey, workspaces);\n } catch {\n // The user is authenticated even if workspace hydration fails.\n }\n },\n });\n}\n"],"names":["apiRequest","useMutation","useQueryClient","useSetAtom","authStateAtom","toAuthenticatedState","authRefreshQueryKey","listUserWorkspaces","userWorkspacesQueryKey","requestPasswordReset","body","method","confirmPasswordReset","useRequestPasswordResetAuth","mutationFn","useConfirmPasswordResetAuth","queryClient","setState","onSuccess","result","setQueryData","workspaces","fetchQuery","queryKey","queryFn","token","retry","staleTime","memberships"],"mappings":"AAKA,SAAQA,UAAU,QAAO,sBAAsB;AAC/C,SAAQC,WAAW,EAAEC,cAAc,QAAO,wBAAwB;AAClE,SAAQC,UAAU,QAAO,QAAQ;AACjC,SAAQC,aAAa,EAAEC,oBAAoB,QAAO,iBAAiB;AACnE,SAAQC,mBAAmB,QAAO,oBAAoB;AACtD,SAAQC,kBAAkB,EAAEC,sBAAsB,QAAO,sBAAsB;AAE/E,OAAO,eAAeC,qBAAqBC,IAAiC;IAC1E,MAAMV,WAAiB,wBAAwB;QAACW,QAAQ;QAAQD;IAAI;AACtE;AAEA,OAAO,eAAeE,qBAAqBF,IAAiC;IAC1E,OAAO,MAAMV,WAA4C,gCAAgC;QACvFW,QAAQ;QACRD;IACF;AACF;AAEA,OAAO,SAASG;IACd,OAAOZ,YAAY;QAACa,YAAYL;IAAoB;AACtD;AAEA,OAAO,SAASM;IACd,MAAMC,cAAcd;IACpB,MAAMe,WAAWd,WAAWC;IAE5B,OAAOH,YAAY;QACjBa,YAAYF;QACZM,WAAW,OAAOC;YAChBF,SAASZ,qBAAqBc;YAC9BH,YAAYI,YAAY,CAACd,qBAAqBa;YAC9C,IAAI;gBACF,MAAME,aAAa,MAAML,YAAYM,UAAU,CAAC;oBAC9CC,UAAUf;oBACVgB,SAAS,IAAMjB,mBAAmBY,OAAOM,KAAK;oBAC9CC,OAAO;oBACPC,WAAW;gBACb;gBACAV,SAASZ,qBAAqBc,QAAQE,WAAWO,WAAW;gBAC5DZ,YAAYI,YAAY,CAACZ,wBAAwBa;YACnD,EAAE,OAAM;YACN,+DAA+D;YACjE;QACF;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/hooks/api/password-reset-auth.ts"],"sourcesContent":["import type {\n PasswordResetConfirmBodyDto,\n PasswordResetConfirmResponseDto,\n PasswordResetRequestBodyDto,\n} from '@shipfox/api-auth-dto';\nimport {apiRequest} from '@shipfox/client-api';\nimport {useMutation} from '@tanstack/react-query';\nimport {useAuthTransition} from '#state/auth.js';\n\nexport async function requestPasswordReset(body: PasswordResetRequestBodyDto) {\n await apiRequest<void>('/auth/password-reset', {method: 'POST', body});\n}\n\nexport async function confirmPasswordReset(body: PasswordResetConfirmBodyDto) {\n return await apiRequest<PasswordResetConfirmResponseDto>('/auth/password-reset/confirm', {\n method: 'POST',\n body,\n });\n}\n\nexport function useRequestPasswordResetAuth() {\n return useMutation({mutationFn: requestPasswordReset});\n}\n\nexport function useConfirmPasswordResetAuth() {\n const {enterAuthenticated} = useAuthTransition();\n\n return useMutation({\n mutationFn: confirmPasswordReset,\n onSuccess: enterAuthenticated,\n });\n}\n"],"names":["apiRequest","useMutation","useAuthTransition","requestPasswordReset","body","method","confirmPasswordReset","useRequestPasswordResetAuth","mutationFn","useConfirmPasswordResetAuth","enterAuthenticated","onSuccess"],"mappings":"AAKA,SAAQA,UAAU,QAAO,sBAAsB;AAC/C,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SAAQC,iBAAiB,QAAO,iBAAiB;AAEjD,OAAO,eAAeC,qBAAqBC,IAAiC;IAC1E,MAAMJ,WAAiB,wBAAwB;QAACK,QAAQ;QAAQD;IAAI;AACtE;AAEA,OAAO,eAAeE,qBAAqBF,IAAiC;IAC1E,OAAO,MAAMJ,WAA4C,gCAAgC;QACvFK,QAAQ;QACRD;IACF;AACF;AAEA,OAAO,SAASG;IACd,OAAON,YAAY;QAACO,YAAYL;IAAoB;AACtD;AAEA,OAAO,SAASM;IACd,MAAM,EAACC,kBAAkB,EAAC,GAAGR;IAE7B,OAAOD,YAAY;QACjBO,YAAYF;QACZK,WAAWD;IACb;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"verify-email-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/verify-email-auth.ts"],"names":[],"mappings":"AA2BA,wBAAgB,8BAA8B;;;;;YAE7C;AAED,wBAAgB,kBAAkB;;;;;;;;;;;;;;;YAuBjC"}
1
+ {"version":3,"file":"verify-email-auth.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/verify-email-auth.ts"],"names":[],"mappings":"AAwBA,wBAAgB,8BAA8B;;;;;YAE7C;AAED,wBAAgB,kBAAkB;;;;;;;;;;;;;;;YAOjC"}