@shipfox/client-shell 3.0.1 → 5.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.
- package/dist/components/auth-shell.d.ts +13 -0
- package/dist/components/auth-shell.d.ts.map +1 -0
- package/dist/components/auth-shell.js +68 -0
- package/dist/components/auth-shell.js.map +1 -0
- package/dist/core/session.d.ts +20 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +3 -0
- package/dist/core/session.js.map +1 -0
- package/dist/hooks/api/session-mapper.d.ts +5 -0
- package/dist/hooks/api/session-mapper.d.ts.map +1 -0
- package/dist/hooks/api/session-mapper.js +20 -0
- package/dist/hooks/api/session-mapper.js.map +1 -0
- package/dist/runtime/auth.d.ts +14 -22
- package/dist/runtime/auth.d.ts.map +1 -1
- package/dist/runtime/auth.js +83 -31
- package/dist/runtime/auth.js.map +1 -1
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/last-workspace.d.ts +1 -1
- package/dist/runtime/last-workspace.d.ts.map +1 -1
- package/dist/runtime/last-workspace.js +33 -14
- package/dist/runtime/last-workspace.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +5 -27
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type HeaderProps } from '@shipfox/react-ui/typography';
|
|
2
|
+
import type { PropsWithChildren, ReactNode, Ref } from 'react';
|
|
3
|
+
export interface AuthShellProps {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
headingProps?: Omit<HeaderProps, 'children' | 'id'> | undefined;
|
|
9
|
+
headingRef?: Ref<HTMLHeadingElement> | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function AuthShell({ title, description, children, className, headingProps, headingRef, }: AuthShellProps): import("react").JSX.Element;
|
|
12
|
+
export declare function AuthActions({ children }: PropsWithChildren): import("react").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=auth-shell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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,+BA0ChB;AAED,wBAAgB,WAAW,CAAC,EAAC,QAAQ,EAAC,EAAE,iBAAiB,+BAExD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Icon } from '@shipfox/react-ui/icon';
|
|
3
|
+
import { Header, Text } from '@shipfox/react-ui/typography';
|
|
4
|
+
export function AuthShell({ title, description, children, className, headingProps, headingRef }) {
|
|
5
|
+
return /*#__PURE__*/ _jsxs("main", {
|
|
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
|
+
children: [
|
|
8
|
+
/*#__PURE__*/ _jsx("div", {
|
|
9
|
+
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",
|
|
10
|
+
"aria-hidden": "true",
|
|
11
|
+
style: {
|
|
12
|
+
maskImage: 'radial-gradient(ellipse 85% 100% at 50% 0%, #000 0%, #000 24%, transparent 78%)',
|
|
13
|
+
WebkitMaskImage: 'radial-gradient(ellipse 85% 100% at 50% 0%, #000 0%, #000 24%, transparent 78%)'
|
|
14
|
+
},
|
|
15
|
+
children: /*#__PURE__*/ _jsx("div", {
|
|
16
|
+
className: "h-full w-full bg-[radial-gradient(circle,rgba(230,62,0,0.48)_1.6px,transparent_1.8px)] bg-[length:44px_44px]"
|
|
17
|
+
})
|
|
18
|
+
}),
|
|
19
|
+
/*#__PURE__*/ _jsxs("section", {
|
|
20
|
+
className: className ?? 'relative flex w-full max-w-[384px] flex-col items-stretch gap-28',
|
|
21
|
+
"aria-labelledby": "auth-title",
|
|
22
|
+
children: [
|
|
23
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
24
|
+
className: "flex flex-col items-center gap-16",
|
|
25
|
+
children: [
|
|
26
|
+
/*#__PURE__*/ _jsx("div", {
|
|
27
|
+
className: "flex size-64 items-center justify-center rounded-12 border border-border-neutral-base bg-background-neutral-base p-10 shadow-button-neutral",
|
|
28
|
+
children: /*#__PURE__*/ _jsx(Icon, {
|
|
29
|
+
name: "shipfox",
|
|
30
|
+
className: "size-42 text-background-highlight-interactive"
|
|
31
|
+
})
|
|
32
|
+
}),
|
|
33
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
34
|
+
className: "flex min-w-[128px] flex-col items-center gap-4 text-center",
|
|
35
|
+
children: [
|
|
36
|
+
/*#__PURE__*/ _jsx(Header, {
|
|
37
|
+
id: "auth-title",
|
|
38
|
+
variant: "h1",
|
|
39
|
+
tabIndex: -1,
|
|
40
|
+
...headingProps,
|
|
41
|
+
...headingRef ? {
|
|
42
|
+
ref: headingRef
|
|
43
|
+
} : {},
|
|
44
|
+
children: title
|
|
45
|
+
}),
|
|
46
|
+
/*#__PURE__*/ _jsx(Text, {
|
|
47
|
+
size: "sm",
|
|
48
|
+
className: "text-foreground-neutral-subtle",
|
|
49
|
+
children: description
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
})
|
|
53
|
+
]
|
|
54
|
+
}),
|
|
55
|
+
children
|
|
56
|
+
]
|
|
57
|
+
})
|
|
58
|
+
]
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function AuthActions({ children }) {
|
|
62
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
63
|
+
className: "flex flex-col gap-20",
|
|
64
|
+
children: children
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=auth-shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 <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;;;0BAEjB,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
|
+
export interface UserIdentity {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
emailVerifiedAt?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AuthenticatedSession {
|
|
8
|
+
accessToken: string;
|
|
9
|
+
user: UserIdentity;
|
|
10
|
+
}
|
|
11
|
+
export interface WorkspaceMembership {
|
|
12
|
+
id: string;
|
|
13
|
+
workspaceId: string;
|
|
14
|
+
}
|
|
15
|
+
export interface WorkspaceSummary {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
membershipId: string;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/core/session.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/session.ts"],"sourcesContent":["export interface UserIdentity {\n id: string;\n email: string;\n name?: string;\n emailVerifiedAt?: string;\n}\n\nexport interface AuthenticatedSession {\n accessToken: string;\n user: UserIdentity;\n}\n\nexport interface WorkspaceMembership {\n id: string;\n workspaceId: string;\n}\n\nexport interface WorkspaceSummary {\n id: string;\n name: string;\n membershipId: string;\n}\n"],"names":[],"mappings":"AAiBA,WAIC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LoginResponseDto, UserDto } from '@shipfox/api-auth-dto';
|
|
2
|
+
import type { AuthenticatedSession, UserIdentity } from '#core/session.js';
|
|
3
|
+
export declare function toUserIdentity(dto: UserDto): UserIdentity;
|
|
4
|
+
export declare function toAuthenticatedSession(dto: LoginResponseDto): AuthenticatedSession;
|
|
5
|
+
//# sourceMappingURL=session-mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-mapper.d.ts","sourceRoot":"","sources":["../../../src/hooks/api/session-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,gBAAgB,EAAE,OAAO,EAAC,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAC,oBAAoB,EAAE,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAEzE,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAOzD;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,gBAAgB,GAAG,oBAAoB,CAElF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function toUserIdentity(dto) {
|
|
2
|
+
return {
|
|
3
|
+
id: dto.id,
|
|
4
|
+
email: dto.email,
|
|
5
|
+
...dto.name ? {
|
|
6
|
+
name: dto.name
|
|
7
|
+
} : {},
|
|
8
|
+
...dto.email_verified_at ? {
|
|
9
|
+
emailVerifiedAt: dto.email_verified_at
|
|
10
|
+
} : {}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function toAuthenticatedSession(dto) {
|
|
14
|
+
return {
|
|
15
|
+
accessToken: dto.token,
|
|
16
|
+
user: toUserIdentity(dto.user)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=session-mapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/api/session-mapper.ts"],"sourcesContent":["import type {LoginResponseDto, UserDto} from '@shipfox/api-auth-dto';\nimport type {AuthenticatedSession, UserIdentity} from '#core/session.js';\n\nexport function toUserIdentity(dto: UserDto): UserIdentity {\n return {\n id: dto.id,\n email: dto.email,\n ...(dto.name ? {name: dto.name} : {}),\n ...(dto.email_verified_at ? {emailVerifiedAt: dto.email_verified_at} : {}),\n };\n}\n\nexport function toAuthenticatedSession(dto: LoginResponseDto): AuthenticatedSession {\n return {accessToken: dto.token, user: toUserIdentity(dto.user)};\n}\n"],"names":["toUserIdentity","dto","id","email","name","email_verified_at","emailVerifiedAt","toAuthenticatedSession","accessToken","token","user"],"mappings":"AAGA,OAAO,SAASA,eAAeC,GAAY;IACzC,OAAO;QACLC,IAAID,IAAIC,EAAE;QACVC,OAAOF,IAAIE,KAAK;QAChB,GAAIF,IAAIG,IAAI,GAAG;YAACA,MAAMH,IAAIG,IAAI;QAAA,IAAI,CAAC,CAAC;QACpC,GAAIH,IAAII,iBAAiB,GAAG;YAACC,iBAAiBL,IAAII,iBAAiB;QAAA,IAAI,CAAC,CAAC;IAC3E;AACF;AAEA,OAAO,SAASE,uBAAuBN,GAAqB;IAC1D,OAAO;QAACO,aAAaP,IAAIQ,KAAK;QAAEC,MAAMV,eAAeC,IAAIS,IAAI;IAAC;AAChE"}
|
package/dist/runtime/auth.d.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import type { LoginResponseDto, UserDto } from '@shipfox/api-auth-dto';
|
|
2
|
-
import type { MembershipWithWorkspaceDto } from '@shipfox/api-workspaces-dto';
|
|
3
1
|
import type { PropsWithChildren } from 'react';
|
|
2
|
+
import type { AuthenticatedSession, UserIdentity, WorkspaceSummary } from '#core/session.js';
|
|
4
3
|
export type AuthStatus = 'loading' | 'authenticated' | 'guest';
|
|
5
|
-
export
|
|
6
|
-
id: string;
|
|
7
|
-
name: string;
|
|
8
|
-
membershipId: string;
|
|
9
|
-
}
|
|
4
|
+
export type Workspace = WorkspaceSummary;
|
|
10
5
|
export interface AuthState {
|
|
11
6
|
status: AuthStatus;
|
|
12
7
|
token?: string;
|
|
13
|
-
user?:
|
|
8
|
+
user?: UserIdentity;
|
|
14
9
|
workspaces?: Workspace[];
|
|
15
10
|
}
|
|
16
11
|
export interface AuthStateValue extends AuthState {
|
|
@@ -25,24 +20,21 @@ export declare const authStateAtom: import("jotai").PrimitiveAtom<AuthState> & {
|
|
|
25
20
|
};
|
|
26
21
|
export declare const authRefreshQueryKey: readonly ["auth", "refresh"];
|
|
27
22
|
export declare const userWorkspacesQueryKey: readonly ["workspaces", "mine"];
|
|
28
|
-
export declare function toAuthenticatedState(
|
|
23
|
+
export declare function toAuthenticatedState(session: AuthenticatedSession, workspaces?: WorkspaceSummary[]): AuthState;
|
|
29
24
|
export declare function useAuthState(): AuthStateValue;
|
|
30
25
|
export declare function listUserWorkspaces(token?: string): Promise<{
|
|
31
|
-
memberships:
|
|
32
|
-
}>;
|
|
33
|
-
export declare function getAuthRefreshDelayMs(token: string, nowMs?: number): number | undefined;
|
|
34
|
-
export declare function useRefreshAuth(): () => Promise<{
|
|
35
|
-
token: string;
|
|
36
|
-
user: {
|
|
26
|
+
memberships: {
|
|
37
27
|
id: string;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
status: "active" | "suspended" | "deleted";
|
|
42
|
-
created_at: string;
|
|
43
|
-
updated_at: string;
|
|
44
|
-
};
|
|
28
|
+
name: string;
|
|
29
|
+
membershipId: string;
|
|
30
|
+
}[];
|
|
45
31
|
}>;
|
|
32
|
+
export declare function getAuthRefreshDelayMs(token: string, nowMs?: number): number | undefined;
|
|
33
|
+
export declare function useAuthTransition(): {
|
|
34
|
+
enterAuthenticated: (session: AuthenticatedSession) => Promise<void>;
|
|
35
|
+
enterGuest: () => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
export declare function useRefreshAuth(): () => Promise<AuthenticatedSession>;
|
|
46
38
|
export interface AuthRuntimeProps extends PropsWithChildren {
|
|
47
39
|
effects?: boolean;
|
|
48
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/runtime/auth.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/runtime/auth.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAE7C,OAAO,KAAK,EAAC,oBAAoB,EAAE,YAAY,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAO3F,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,eAAe,GAAG,OAAO,CAAC;AAE/D,MAAM,MAAM,SAAS,GAAG,gBAAgB,CAAC;AAEzC,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,eAAO,MAAM,gBAAgB,EAAE,SAA+B,CAAC;AAC/D,eAAO,MAAM,aAAa;;CAAoC,CAAC;AAE/D,eAAO,MAAM,mBAAmB,8BAA+B,CAAC;AAChE,eAAO,MAAM,sBAAsB,iCAAkC,CAAC;AAEtE,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,oBAAoB,EAC7B,UAAU,GAAE,gBAAgB,EAAO,GAClC,SAAS,CAOX;AAED,wBAAgB,YAAY,IAAI,cAAc,CAY7C;AAED,wBAAsB,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM;;;;;;GAatD;AAyBD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAa,GAAG,MAAM,GAAG,SAAS,CAG3F;AAED,wBAAgB,iBAAiB;kCAmBb,oBAAoB;;EAgCvC;AAED,wBAAgB,cAAc,wCAkB7B;AAED,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACzD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAC,QAAQ,EAAE,OAAc,EAAC,EAAE,gBAAgB,6BAyEvE"}
|
package/dist/runtime/auth.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loginResponseSchema } from '@shipfox/api-auth-dto';
|
|
2
|
+
import { listUserWorkspacesResponseSchema } from '@shipfox/api-workspaces-dto';
|
|
3
|
+
import { ApiError, checkedApiRequest, configureApiClient } from '@shipfox/client-api';
|
|
2
4
|
import { useQueryClient } from '@tanstack/react-query';
|
|
3
5
|
import { atom, useAtomValue, useSetAtom, useStore } from 'jotai';
|
|
4
6
|
import { useCallback, useEffect, useMemo } from 'react';
|
|
7
|
+
import { toAuthenticatedSession } from '#hooks/api/session-mapper.js';
|
|
5
8
|
const REFRESH_EARLY_MS = 5 * 60 * 1000;
|
|
6
9
|
const REFRESH_RETRY_DELAY_MS = 60_000;
|
|
7
10
|
const BASE64_URL_REPLACEMENTS = {
|
|
@@ -12,6 +15,7 @@ export const initialAuthState = {
|
|
|
12
15
|
status: 'loading'
|
|
13
16
|
};
|
|
14
17
|
export const authStateAtom = atom(initialAuthState);
|
|
18
|
+
const authTransitionEpochAtom = atom(0);
|
|
15
19
|
export const authRefreshQueryKey = [
|
|
16
20
|
'auth',
|
|
17
21
|
'refresh'
|
|
@@ -20,16 +24,12 @@ export const userWorkspacesQueryKey = [
|
|
|
20
24
|
'workspaces',
|
|
21
25
|
'mine'
|
|
22
26
|
];
|
|
23
|
-
export function toAuthenticatedState(
|
|
27
|
+
export function toAuthenticatedState(session, workspaces = []) {
|
|
24
28
|
return {
|
|
25
29
|
status: 'authenticated',
|
|
26
|
-
token:
|
|
27
|
-
user:
|
|
28
|
-
workspaces
|
|
29
|
-
id: membership.workspace_id,
|
|
30
|
-
name: membership.workspace_name,
|
|
31
|
-
membershipId: membership.id
|
|
32
|
-
}))
|
|
30
|
+
token: session.accessToken,
|
|
31
|
+
user: session.user,
|
|
32
|
+
workspaces
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
export function useAuthState() {
|
|
@@ -45,16 +45,24 @@ export function useAuthState() {
|
|
|
45
45
|
]);
|
|
46
46
|
}
|
|
47
47
|
export async function listUserWorkspaces(token) {
|
|
48
|
-
|
|
48
|
+
const response = await checkedApiRequest(listUserWorkspacesResponseSchema, '/workspaces', token ? {
|
|
49
49
|
headers: {
|
|
50
50
|
authorization: `Bearer ${token}`
|
|
51
51
|
}
|
|
52
52
|
} : {});
|
|
53
|
+
return {
|
|
54
|
+
memberships: response.memberships.map((membership)=>({
|
|
55
|
+
id: membership.workspace_id,
|
|
56
|
+
name: membership.workspace_name,
|
|
57
|
+
membershipId: membership.id
|
|
58
|
+
}))
|
|
59
|
+
};
|
|
53
60
|
}
|
|
54
61
|
async function refreshAuthRequest() {
|
|
55
|
-
|
|
62
|
+
const response = await checkedApiRequest(loginResponseSchema, '/auth/refresh', {
|
|
56
63
|
method: 'POST'
|
|
57
64
|
});
|
|
65
|
+
return toAuthenticatedSession(response);
|
|
58
66
|
}
|
|
59
67
|
function decodeBase64Url(value) {
|
|
60
68
|
const base64 = value.replace(BASE64_URL_REPLACEMENTS.dash, '+').replace(BASE64_URL_REPLACEMENTS.underscore, '/');
|
|
@@ -74,9 +82,66 @@ export function getAuthRefreshDelayMs(token, nowMs = Date.now()) {
|
|
|
74
82
|
const exp = readJwtExp(token);
|
|
75
83
|
return exp === undefined ? undefined : exp * 1000 - nowMs - REFRESH_EARLY_MS;
|
|
76
84
|
}
|
|
77
|
-
export function
|
|
85
|
+
export function useAuthTransition() {
|
|
78
86
|
const queryClient = useQueryClient();
|
|
87
|
+
const store = useStore();
|
|
79
88
|
const setState = useSetAtom(authStateAtom);
|
|
89
|
+
const clearPrivateState = useCallback(async ()=>{
|
|
90
|
+
await queryClient.cancelQueries();
|
|
91
|
+
queryClient.clear();
|
|
92
|
+
}, [
|
|
93
|
+
queryClient
|
|
94
|
+
]);
|
|
95
|
+
const enterGuest = useCallback(async ()=>{
|
|
96
|
+
const transitionEpoch = store.get(authTransitionEpochAtom) + 1;
|
|
97
|
+
store.set(authTransitionEpochAtom, transitionEpoch);
|
|
98
|
+
await clearPrivateState();
|
|
99
|
+
if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;
|
|
100
|
+
setState({
|
|
101
|
+
status: 'guest'
|
|
102
|
+
});
|
|
103
|
+
}, [
|
|
104
|
+
clearPrivateState,
|
|
105
|
+
setState,
|
|
106
|
+
store
|
|
107
|
+
]);
|
|
108
|
+
const enterAuthenticated = useCallback(async (session)=>{
|
|
109
|
+
const transitionEpoch = store.get(authTransitionEpochAtom) + 1;
|
|
110
|
+
store.set(authTransitionEpochAtom, transitionEpoch);
|
|
111
|
+
const previousState = store.get(authStateAtom);
|
|
112
|
+
const principalChanged = previousState.status === 'authenticated' && previousState.user?.id !== session.user.id;
|
|
113
|
+
if (principalChanged) await clearPrivateState();
|
|
114
|
+
if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;
|
|
115
|
+
queryClient.setQueryData(authRefreshQueryKey, session);
|
|
116
|
+
let workspaces = [];
|
|
117
|
+
try {
|
|
118
|
+
const hydratedWorkspaces = await queryClient.fetchQuery({
|
|
119
|
+
queryKey: userWorkspacesQueryKey,
|
|
120
|
+
queryFn: ()=>listUserWorkspaces(session.accessToken),
|
|
121
|
+
retry: false,
|
|
122
|
+
staleTime: 0
|
|
123
|
+
});
|
|
124
|
+
workspaces = hydratedWorkspaces.memberships;
|
|
125
|
+
queryClient.setQueryData(userWorkspacesQueryKey, hydratedWorkspaces);
|
|
126
|
+
} catch {
|
|
127
|
+
// The authenticated session remains usable while workspace hydration retries on the next route load.
|
|
128
|
+
}
|
|
129
|
+
if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;
|
|
130
|
+
setState(toAuthenticatedState(session, workspaces));
|
|
131
|
+
}, [
|
|
132
|
+
clearPrivateState,
|
|
133
|
+
queryClient,
|
|
134
|
+
setState,
|
|
135
|
+
store
|
|
136
|
+
]);
|
|
137
|
+
return {
|
|
138
|
+
enterAuthenticated,
|
|
139
|
+
enterGuest
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function useRefreshAuth() {
|
|
143
|
+
const queryClient = useQueryClient();
|
|
144
|
+
const { enterAuthenticated, enterGuest } = useAuthTransition();
|
|
80
145
|
return useCallback(async ()=>{
|
|
81
146
|
try {
|
|
82
147
|
const result = await queryClient.fetchQuery({
|
|
@@ -85,29 +150,16 @@ export function useRefreshAuth() {
|
|
|
85
150
|
retry: false,
|
|
86
151
|
staleTime: 0
|
|
87
152
|
});
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const workspaces = await queryClient.fetchQuery({
|
|
91
|
-
queryKey: userWorkspacesQueryKey,
|
|
92
|
-
queryFn: ()=>listUserWorkspaces(result.token),
|
|
93
|
-
retry: false,
|
|
94
|
-
staleTime: 0
|
|
95
|
-
});
|
|
96
|
-
memberships = workspaces.memberships;
|
|
97
|
-
} catch {
|
|
98
|
-
// A valid session remains usable while workspace hydration retries on the next route load.
|
|
99
|
-
}
|
|
100
|
-
setState(toAuthenticatedState(result, memberships));
|
|
153
|
+
await enterAuthenticated(result);
|
|
101
154
|
return result;
|
|
102
155
|
} catch (error) {
|
|
103
|
-
if (error instanceof ApiError && error.status === 401)
|
|
104
|
-
status: 'guest'
|
|
105
|
-
});
|
|
156
|
+
if (error instanceof ApiError && error.status === 401) await enterGuest();
|
|
106
157
|
throw error;
|
|
107
158
|
}
|
|
108
159
|
}, [
|
|
109
|
-
|
|
110
|
-
|
|
160
|
+
enterAuthenticated,
|
|
161
|
+
enterGuest,
|
|
162
|
+
queryClient
|
|
111
163
|
]);
|
|
112
164
|
}
|
|
113
165
|
export function AuthRuntime({ children, effects = true }) {
|
|
@@ -118,7 +170,7 @@ export function AuthRuntime({ children, effects = true }) {
|
|
|
118
170
|
if (!effects) return;
|
|
119
171
|
configureApiClient({
|
|
120
172
|
getAccessToken: ()=>store.get(authStateAtom).token,
|
|
121
|
-
refreshAccessToken: async ()=>(await refreshAuth()).
|
|
173
|
+
refreshAccessToken: async ()=>(await refreshAuth()).accessToken
|
|
122
174
|
});
|
|
123
175
|
}, [
|
|
124
176
|
effects,
|
package/dist/runtime/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/runtime/auth.tsx"],"sourcesContent":["import type {LoginResponseDto, UserDto} from '@shipfox/api-auth-dto';\nimport type {MembershipWithWorkspaceDto} from '@shipfox/api-workspaces-dto';\nimport {ApiError, apiRequest, configureApiClient} from '@shipfox/client-api';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {atom, useAtomValue, useSetAtom, useStore} from 'jotai';\nimport type {PropsWithChildren} from 'react';\nimport {useCallback, useEffect, useMemo} from 'react';\n\nconst REFRESH_EARLY_MS = 5 * 60 * 1000;\nconst REFRESH_RETRY_DELAY_MS = 60_000;\nconst BASE64_URL_REPLACEMENTS = {dash: /-/g, underscore: /_/g} as const;\n\nexport type AuthStatus = 'loading' | 'authenticated' | 'guest';\n\nexport interface Workspace {\n id: string;\n name: string;\n membershipId: string;\n}\n\nexport interface AuthState {\n status: AuthStatus;\n token?: string;\n user?: UserDto;\n workspaces?: Workspace[];\n}\n\nexport interface AuthStateValue extends AuthState {\n isLoading: boolean;\n isAuthenticated: boolean;\n workspaces: Workspace[];\n hasWorkspace: boolean;\n}\n\nexport const initialAuthState: AuthState = {status: 'loading'};\nexport const authStateAtom = atom<AuthState>(initialAuthState);\nexport const authRefreshQueryKey = ['auth', 'refresh'] as const;\nexport const userWorkspacesQueryKey = ['workspaces', 'mine'] as const;\n\nexport function toAuthenticatedState(\n result: LoginResponseDto,\n memberships: MembershipWithWorkspaceDto[] = [],\n): AuthState {\n return {\n status: 'authenticated',\n token: result.token,\n user: result.user,\n workspaces: memberships.map((membership) => ({\n id: membership.workspace_id,\n name: membership.workspace_name,\n membershipId: membership.id,\n })),\n };\n}\n\nexport function useAuthState(): AuthStateValue {\n const state = useAtomValue(authStateAtom);\n return useMemo(\n () => ({\n ...state,\n workspaces: state.workspaces ?? [],\n isLoading: state.status === 'loading',\n isAuthenticated: state.status === 'authenticated',\n hasWorkspace: (state.workspaces ?? []).length > 0,\n }),\n [state],\n );\n}\n\nexport async function listUserWorkspaces(token?: string) {\n return await apiRequest<{memberships: MembershipWithWorkspaceDto[]}>(\n '/workspaces',\n token ? {headers: {authorization: `Bearer ${token}`}} : {},\n );\n}\n\nasync function refreshAuthRequest() {\n return await apiRequest<LoginResponseDto>('/auth/refresh', {method: 'POST'});\n}\n\nfunction decodeBase64Url(value: string): string {\n const base64 = value\n .replace(BASE64_URL_REPLACEMENTS.dash, '+')\n .replace(BASE64_URL_REPLACEMENTS.underscore, '/');\n return atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '='));\n}\n\nfunction readJwtExp(token: string): number | undefined {\n const [, payload] = token.split('.');\n if (!payload) return undefined;\n try {\n const parsed = JSON.parse(decodeBase64Url(payload)) as {exp?: unknown};\n return typeof parsed.exp === 'number' && Number.isFinite(parsed.exp) ? parsed.exp : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function getAuthRefreshDelayMs(token: string, nowMs = Date.now()): number | undefined {\n const exp = readJwtExp(token);\n return exp === undefined ? undefined : exp * 1000 - nowMs - REFRESH_EARLY_MS;\n}\n\nexport function useRefreshAuth() {\n const queryClient = useQueryClient();\n const setState = useSetAtom(authStateAtom);\n return useCallback(async () => {\n try {\n const result = await queryClient.fetchQuery({\n queryKey: authRefreshQueryKey,\n queryFn: refreshAuthRequest,\n retry: false,\n staleTime: 0,\n });\n let memberships: MembershipWithWorkspaceDto[] = [];\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 } catch {\n // A valid session remains usable while workspace hydration retries on the next route load.\n }\n setState(toAuthenticatedState(result, memberships));\n return result;\n } catch (error) {\n if (error instanceof ApiError && error.status === 401) setState({status: 'guest'});\n throw error;\n }\n }, [queryClient, setState]);\n}\n\nexport interface AuthRuntimeProps extends PropsWithChildren {\n effects?: boolean;\n}\n\nexport function AuthRuntime({children, effects = true}: AuthRuntimeProps) {\n const store = useStore();\n const authState = useAtomValue(authStateAtom);\n const refreshAuth = useRefreshAuth();\n\n useEffect(() => {\n if (!effects) return;\n configureApiClient({\n getAccessToken: () => store.get(authStateAtom).token,\n refreshAccessToken: async () => (await refreshAuth()).token,\n });\n }, [effects, refreshAuth, store]);\n\n useEffect(() => {\n if (!effects) return;\n refreshAuth().catch(() => undefined);\n }, [effects, refreshAuth]);\n\n useEffect(() => {\n if (!effects || authState.status !== 'authenticated' || !authState.token) return;\n\n let timeout: ReturnType<typeof setTimeout> | undefined;\n let disposed = false;\n let refreshing = false;\n const clearRefreshTimer = () => {\n if (timeout !== undefined) clearTimeout(timeout);\n timeout = undefined;\n };\n const scheduleRefresh = (delayMs: number) => {\n clearRefreshTimer();\n timeout = setTimeout(runRefresh, Math.max(0, delayMs));\n };\n const retryIfStillDue = () => {\n const current = store.get(authStateAtom);\n if (current.status !== 'authenticated' || !current.token) return;\n const delay = getAuthRefreshDelayMs(current.token);\n if (delay !== undefined && delay <= 0) scheduleRefresh(REFRESH_RETRY_DELAY_MS);\n };\n function runRefresh() {\n if (disposed || refreshing) return;\n refreshing = true;\n clearRefreshTimer();\n refreshAuth()\n .catch(() => undefined)\n .finally(() => {\n refreshing = false;\n if (!disposed) retryIfStillDue();\n });\n }\n const refreshIfDue = () => {\n const current = store.get(authStateAtom);\n if (current.status !== 'authenticated' || !current.token) return;\n const delay = getAuthRefreshDelayMs(current.token);\n if (delay !== undefined && delay <= 0) runRefresh();\n };\n const refreshIfVisible = () => {\n if (document.visibilityState === 'visible') refreshIfDue();\n };\n const delay = getAuthRefreshDelayMs(authState.token);\n if (delay !== undefined) scheduleRefresh(delay);\n window.addEventListener('focus', refreshIfDue);\n window.addEventListener('online', refreshIfDue);\n document.addEventListener('visibilitychange', refreshIfVisible);\n return () => {\n disposed = true;\n clearRefreshTimer();\n window.removeEventListener('focus', refreshIfDue);\n window.removeEventListener('online', refreshIfDue);\n document.removeEventListener('visibilitychange', refreshIfVisible);\n };\n }, [authState.status, authState.token, effects, refreshAuth, store]);\n\n return children;\n}\n"],"names":["ApiError","apiRequest","configureApiClient","useQueryClient","atom","useAtomValue","useSetAtom","useStore","useCallback","useEffect","useMemo","REFRESH_EARLY_MS","REFRESH_RETRY_DELAY_MS","BASE64_URL_REPLACEMENTS","dash","underscore","initialAuthState","status","authStateAtom","authRefreshQueryKey","userWorkspacesQueryKey","toAuthenticatedState","result","memberships","token","user","workspaces","map","membership","id","workspace_id","name","workspace_name","membershipId","useAuthState","state","isLoading","isAuthenticated","hasWorkspace","length","listUserWorkspaces","headers","authorization","refreshAuthRequest","method","decodeBase64Url","value","base64","replace","atob","padEnd","Math","ceil","readJwtExp","payload","split","undefined","parsed","JSON","parse","exp","Number","isFinite","getAuthRefreshDelayMs","nowMs","Date","now","useRefreshAuth","queryClient","setState","fetchQuery","queryKey","queryFn","retry","staleTime","error","AuthRuntime","children","effects","store","authState","refreshAuth","getAccessToken","get","refreshAccessToken","catch","timeout","disposed","refreshing","clearRefreshTimer","clearTimeout","scheduleRefresh","delayMs","setTimeout","runRefresh","max","retryIfStillDue","current","delay","finally","refreshIfDue","refreshIfVisible","document","visibilityState","window","addEventListener","removeEventListener"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/runtime/auth.tsx"],"sourcesContent":["import {loginResponseSchema} from '@shipfox/api-auth-dto';\nimport {listUserWorkspacesResponseSchema} from '@shipfox/api-workspaces-dto';\nimport {ApiError, checkedApiRequest, configureApiClient} from '@shipfox/client-api';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {atom, useAtomValue, useSetAtom, useStore} from 'jotai';\nimport type {PropsWithChildren} from 'react';\nimport {useCallback, useEffect, useMemo} from 'react';\nimport type {AuthenticatedSession, UserIdentity, WorkspaceSummary} from '#core/session.js';\nimport {toAuthenticatedSession} from '#hooks/api/session-mapper.js';\n\nconst REFRESH_EARLY_MS = 5 * 60 * 1000;\nconst REFRESH_RETRY_DELAY_MS = 60_000;\nconst BASE64_URL_REPLACEMENTS = {dash: /-/g, underscore: /_/g} as const;\n\nexport type AuthStatus = 'loading' | 'authenticated' | 'guest';\n\nexport type Workspace = WorkspaceSummary;\n\nexport interface AuthState {\n status: AuthStatus;\n token?: string;\n user?: UserIdentity;\n workspaces?: Workspace[];\n}\n\nexport interface AuthStateValue extends AuthState {\n isLoading: boolean;\n isAuthenticated: boolean;\n workspaces: Workspace[];\n hasWorkspace: boolean;\n}\n\nexport const initialAuthState: AuthState = {status: 'loading'};\nexport const authStateAtom = atom<AuthState>(initialAuthState);\nconst authTransitionEpochAtom = atom(0);\nexport const authRefreshQueryKey = ['auth', 'refresh'] as const;\nexport const userWorkspacesQueryKey = ['workspaces', 'mine'] as const;\n\nexport function toAuthenticatedState(\n session: AuthenticatedSession,\n workspaces: WorkspaceSummary[] = [],\n): AuthState {\n return {\n status: 'authenticated',\n token: session.accessToken,\n user: session.user,\n workspaces,\n };\n}\n\nexport function useAuthState(): AuthStateValue {\n const state = useAtomValue(authStateAtom);\n return useMemo(\n () => ({\n ...state,\n workspaces: state.workspaces ?? [],\n isLoading: state.status === 'loading',\n isAuthenticated: state.status === 'authenticated',\n hasWorkspace: (state.workspaces ?? []).length > 0,\n }),\n [state],\n );\n}\n\nexport async function listUserWorkspaces(token?: string) {\n const response = await checkedApiRequest(\n listUserWorkspacesResponseSchema,\n '/workspaces',\n token ? {headers: {authorization: `Bearer ${token}`}} : {},\n );\n return {\n memberships: response.memberships.map((membership) => ({\n id: membership.workspace_id,\n name: membership.workspace_name,\n membershipId: membership.id,\n })),\n };\n}\n\nasync function refreshAuthRequest(): Promise<AuthenticatedSession> {\n const response = await checkedApiRequest(loginResponseSchema, '/auth/refresh', {method: 'POST'});\n return toAuthenticatedSession(response);\n}\n\nfunction decodeBase64Url(value: string): string {\n const base64 = value\n .replace(BASE64_URL_REPLACEMENTS.dash, '+')\n .replace(BASE64_URL_REPLACEMENTS.underscore, '/');\n return atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '='));\n}\n\nfunction readJwtExp(token: string): number | undefined {\n const [, payload] = token.split('.');\n if (!payload) return undefined;\n try {\n const parsed = JSON.parse(decodeBase64Url(payload)) as {exp?: unknown};\n return typeof parsed.exp === 'number' && Number.isFinite(parsed.exp) ? parsed.exp : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function getAuthRefreshDelayMs(token: string, nowMs = Date.now()): number | undefined {\n const exp = readJwtExp(token);\n return exp === undefined ? undefined : exp * 1000 - nowMs - REFRESH_EARLY_MS;\n}\n\nexport function useAuthTransition() {\n const queryClient = useQueryClient();\n const store = useStore();\n const setState = useSetAtom(authStateAtom);\n\n const clearPrivateState = useCallback(async () => {\n await queryClient.cancelQueries();\n queryClient.clear();\n }, [queryClient]);\n\n const enterGuest = useCallback(async () => {\n const transitionEpoch = store.get(authTransitionEpochAtom) + 1;\n store.set(authTransitionEpochAtom, transitionEpoch);\n await clearPrivateState();\n if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;\n setState({status: 'guest'});\n }, [clearPrivateState, setState, store]);\n\n const enterAuthenticated = useCallback(\n async (session: AuthenticatedSession) => {\n const transitionEpoch = store.get(authTransitionEpochAtom) + 1;\n store.set(authTransitionEpochAtom, transitionEpoch);\n const previousState = store.get(authStateAtom);\n const principalChanged =\n previousState.status === 'authenticated' && previousState.user?.id !== session.user.id;\n\n if (principalChanged) await clearPrivateState();\n if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;\n\n queryClient.setQueryData(authRefreshQueryKey, session);\n let workspaces: WorkspaceSummary[] = [];\n try {\n const hydratedWorkspaces = await queryClient.fetchQuery({\n queryKey: userWorkspacesQueryKey,\n queryFn: () => listUserWorkspaces(session.accessToken),\n retry: false,\n staleTime: 0,\n });\n workspaces = hydratedWorkspaces.memberships;\n queryClient.setQueryData(userWorkspacesQueryKey, hydratedWorkspaces);\n } catch {\n // The authenticated session remains usable while workspace hydration retries on the next route load.\n }\n if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;\n\n setState(toAuthenticatedState(session, workspaces));\n },\n [clearPrivateState, queryClient, setState, store],\n );\n\n return {enterAuthenticated, enterGuest};\n}\n\nexport function useRefreshAuth() {\n const queryClient = useQueryClient();\n const {enterAuthenticated, enterGuest} = useAuthTransition();\n return useCallback(async () => {\n try {\n const result = await queryClient.fetchQuery({\n queryKey: authRefreshQueryKey,\n queryFn: refreshAuthRequest,\n retry: false,\n staleTime: 0,\n });\n await enterAuthenticated(result);\n return result;\n } catch (error) {\n if (error instanceof ApiError && error.status === 401) await enterGuest();\n throw error;\n }\n }, [enterAuthenticated, enterGuest, queryClient]);\n}\n\nexport interface AuthRuntimeProps extends PropsWithChildren {\n effects?: boolean;\n}\n\nexport function AuthRuntime({children, effects = true}: AuthRuntimeProps) {\n const store = useStore();\n const authState = useAtomValue(authStateAtom);\n const refreshAuth = useRefreshAuth();\n\n useEffect(() => {\n if (!effects) return;\n configureApiClient({\n getAccessToken: () => store.get(authStateAtom).token,\n refreshAccessToken: async () => (await refreshAuth()).accessToken,\n });\n }, [effects, refreshAuth, store]);\n\n useEffect(() => {\n if (!effects) return;\n refreshAuth().catch(() => undefined);\n }, [effects, refreshAuth]);\n\n useEffect(() => {\n if (!effects || authState.status !== 'authenticated' || !authState.token) return;\n\n let timeout: ReturnType<typeof setTimeout> | undefined;\n let disposed = false;\n let refreshing = false;\n const clearRefreshTimer = () => {\n if (timeout !== undefined) clearTimeout(timeout);\n timeout = undefined;\n };\n const scheduleRefresh = (delayMs: number) => {\n clearRefreshTimer();\n timeout = setTimeout(runRefresh, Math.max(0, delayMs));\n };\n const retryIfStillDue = () => {\n const current = store.get(authStateAtom);\n if (current.status !== 'authenticated' || !current.token) return;\n const delay = getAuthRefreshDelayMs(current.token);\n if (delay !== undefined && delay <= 0) scheduleRefresh(REFRESH_RETRY_DELAY_MS);\n };\n function runRefresh() {\n if (disposed || refreshing) return;\n refreshing = true;\n clearRefreshTimer();\n refreshAuth()\n .catch(() => undefined)\n .finally(() => {\n refreshing = false;\n if (!disposed) retryIfStillDue();\n });\n }\n const refreshIfDue = () => {\n const current = store.get(authStateAtom);\n if (current.status !== 'authenticated' || !current.token) return;\n const delay = getAuthRefreshDelayMs(current.token);\n if (delay !== undefined && delay <= 0) runRefresh();\n };\n const refreshIfVisible = () => {\n if (document.visibilityState === 'visible') refreshIfDue();\n };\n const delay = getAuthRefreshDelayMs(authState.token);\n if (delay !== undefined) scheduleRefresh(delay);\n window.addEventListener('focus', refreshIfDue);\n window.addEventListener('online', refreshIfDue);\n document.addEventListener('visibilitychange', refreshIfVisible);\n return () => {\n disposed = true;\n clearRefreshTimer();\n window.removeEventListener('focus', refreshIfDue);\n window.removeEventListener('online', refreshIfDue);\n document.removeEventListener('visibilitychange', refreshIfVisible);\n };\n }, [authState.status, authState.token, effects, refreshAuth, store]);\n\n return children;\n}\n"],"names":["loginResponseSchema","listUserWorkspacesResponseSchema","ApiError","checkedApiRequest","configureApiClient","useQueryClient","atom","useAtomValue","useSetAtom","useStore","useCallback","useEffect","useMemo","toAuthenticatedSession","REFRESH_EARLY_MS","REFRESH_RETRY_DELAY_MS","BASE64_URL_REPLACEMENTS","dash","underscore","initialAuthState","status","authStateAtom","authTransitionEpochAtom","authRefreshQueryKey","userWorkspacesQueryKey","toAuthenticatedState","session","workspaces","token","accessToken","user","useAuthState","state","isLoading","isAuthenticated","hasWorkspace","length","listUserWorkspaces","response","headers","authorization","memberships","map","membership","id","workspace_id","name","workspace_name","membershipId","refreshAuthRequest","method","decodeBase64Url","value","base64","replace","atob","padEnd","Math","ceil","readJwtExp","payload","split","undefined","parsed","JSON","parse","exp","Number","isFinite","getAuthRefreshDelayMs","nowMs","Date","now","useAuthTransition","queryClient","store","setState","clearPrivateState","cancelQueries","clear","enterGuest","transitionEpoch","get","set","enterAuthenticated","previousState","principalChanged","setQueryData","hydratedWorkspaces","fetchQuery","queryKey","queryFn","retry","staleTime","useRefreshAuth","result","error","AuthRuntime","children","effects","authState","refreshAuth","getAccessToken","refreshAccessToken","catch","timeout","disposed","refreshing","clearRefreshTimer","clearTimeout","scheduleRefresh","delayMs","setTimeout","runRefresh","max","retryIfStillDue","current","delay","finally","refreshIfDue","refreshIfVisible","document","visibilityState","window","addEventListener","removeEventListener"],"mappings":"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"}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
export { AuthActions, AuthShell, type AuthShellProps } from '#components/auth-shell.js';
|
|
1
2
|
export { WorkspaceCrumb, type WorkspaceCrumbProps } from '#components/workspace-crumb.js';
|
|
2
3
|
export { WorkspaceSwitcher } from '#components/workspace-switcher.js';
|
|
4
|
+
export type { AuthenticatedSession, UserIdentity, WorkspaceMembership, WorkspaceSummary, } from '#core/session.js';
|
|
5
|
+
export { toAuthenticatedSession, toUserIdentity } from '#hooks/api/session-mapper.js';
|
|
3
6
|
export * from '../compose/compose-routes.js';
|
|
4
7
|
export * from '../compose/errors.js';
|
|
5
8
|
export * from '../compose/merge-config.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAE,KAAK,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AACxF,OAAO,EAAC,iBAAiB,EAAC,MAAM,mCAAmC,CAAC;AACpE,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,SAAS,EAAE,KAAK,cAAc,EAAC,MAAM,2BAA2B,CAAC;AACtF,OAAO,EAAC,cAAc,EAAE,KAAK,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AACxF,OAAO,EAAC,iBAAiB,EAAC,MAAM,mCAAmC,CAAC;AACpE,YAAY,EACV,oBAAoB,EACpB,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAC,sBAAsB,EAAE,cAAc,EAAC,MAAM,8BAA8B,CAAC;AACpF,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD,cAAc,mCAAmC,CAAC;AAClD,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export { AuthActions, AuthShell } from '#components/auth-shell.js';
|
|
1
2
|
export { WorkspaceCrumb } from '#components/workspace-crumb.js';
|
|
2
3
|
export { WorkspaceSwitcher } from '#components/workspace-switcher.js';
|
|
4
|
+
export { toAuthenticatedSession, toUserIdentity } from '#hooks/api/session-mapper.js';
|
|
3
5
|
export * from '../compose/compose-routes.js';
|
|
4
6
|
export * from '../compose/errors.js';
|
|
5
7
|
export * from '../compose/merge-config.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/runtime/index.ts"],"sourcesContent":["export {WorkspaceCrumb, type WorkspaceCrumbProps} from '#components/workspace-crumb.js';\nexport {WorkspaceSwitcher} from '#components/workspace-switcher.js';\nexport * from '../compose/compose-routes.js';\nexport * from '../compose/errors.js';\nexport * from '../compose/merge-config.js';\nexport * from '../compose/normalize-route-path.js';\nexport * from '../compose/validate-providers.js';\nexport * from '../compose/validate-registries.js';\nexport * from './active-workspace.js';\nexport * from './anchor-paths.js';\nexport * from './anchors.js';\nexport * from './auth.js';\nexport * from './chrome-context.js';\nexport * from './compose-client-app.js';\nexport * from './define-route.js';\nexport * from './last-workspace.js';\nexport * from './nav-order.js';\nexport * from './router-context.js';\nexport * from './workspace-setup.js';\n"],"names":["WorkspaceCrumb","WorkspaceSwitcher"],"mappings":"AAAA,SAAQA,cAAc,QAAiC,iCAAiC;AACxF,SAAQC,iBAAiB,QAAO,oCAAoC;
|
|
1
|
+
{"version":3,"sources":["../../src/runtime/index.ts"],"sourcesContent":["export {AuthActions, AuthShell, type AuthShellProps} from '#components/auth-shell.js';\nexport {WorkspaceCrumb, type WorkspaceCrumbProps} from '#components/workspace-crumb.js';\nexport {WorkspaceSwitcher} from '#components/workspace-switcher.js';\nexport type {\n AuthenticatedSession,\n UserIdentity,\n WorkspaceMembership,\n WorkspaceSummary,\n} from '#core/session.js';\nexport {toAuthenticatedSession, toUserIdentity} from '#hooks/api/session-mapper.js';\nexport * from '../compose/compose-routes.js';\nexport * from '../compose/errors.js';\nexport * from '../compose/merge-config.js';\nexport * from '../compose/normalize-route-path.js';\nexport * from '../compose/validate-providers.js';\nexport * from '../compose/validate-registries.js';\nexport * from './active-workspace.js';\nexport * from './anchor-paths.js';\nexport * from './anchors.js';\nexport * from './auth.js';\nexport * from './chrome-context.js';\nexport * from './compose-client-app.js';\nexport * from './define-route.js';\nexport * from './last-workspace.js';\nexport * from './nav-order.js';\nexport * from './router-context.js';\nexport * from './workspace-setup.js';\n"],"names":["AuthActions","AuthShell","WorkspaceCrumb","WorkspaceSwitcher","toAuthenticatedSession","toUserIdentity"],"mappings":"AAAA,SAAQA,WAAW,EAAEC,SAAS,QAA4B,4BAA4B;AACtF,SAAQC,cAAc,QAAiC,iCAAiC;AACxF,SAAQC,iBAAiB,QAAO,oCAAoC;AAOpE,SAAQC,sBAAsB,EAAEC,cAAc,QAAO,+BAA+B;AACpF,cAAc,+BAA+B;AAC7C,cAAc,uBAAuB;AACrC,cAAc,6BAA6B;AAC3C,cAAc,qCAAqC;AACnD,cAAc,mCAAmC;AACjD,cAAc,oCAAoC;AAClD,cAAc,wBAAwB;AACtC,cAAc,oBAAoB;AAClC,cAAc,eAAe;AAC7B,cAAc,YAAY;AAC1B,cAAc,sBAAsB;AACpC,cAAc,0BAA0B;AACxC,cAAc,oBAAoB;AAClC,cAAc,sBAAsB;AACpC,cAAc,iBAAiB;AAC/B,cAAc,sBAAsB;AACpC,cAAc,uBAAuB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const lastWorkspaceIdAtom: import("jotai").WritableAtom<string | undefined, [
|
|
1
|
+
export declare const lastWorkspaceIdAtom: import("jotai").WritableAtom<string | undefined, [workspaceId: string | undefined], void>;
|
|
2
2
|
export declare function getLastWorkspaceId(): string | undefined;
|
|
3
3
|
export declare function rememberLastWorkspaceId(workspaceId: string): void;
|
|
4
4
|
//# sourceMappingURL=last-workspace.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"last-workspace.d.ts","sourceRoot":"","sources":["../../src/runtime/last-workspace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"last-workspace.d.ts","sourceRoot":"","sources":["../../src/runtime/last-workspace.ts"],"names":[],"mappings":"AAgCA,eAAO,MAAM,mBAAmB,2FAO/B,CAAC;AAEF,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAEjE"}
|
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { createTypedBrowserStorage, localStorageOrUndefined } from '@shipfox/client-ui';
|
|
2
|
+
import { atom } from 'jotai';
|
|
3
|
+
const lastWorkspaceStorageKey = 'shipfox.lastWorkspaceId';
|
|
4
|
+
const lastWorkspaceStorage = createTypedBrowserStorage(localStorageOrUndefined, {
|
|
5
|
+
key: lastWorkspaceStorageKey,
|
|
6
|
+
lifetime: 'persistent',
|
|
7
|
+
principalScope: 'principal',
|
|
8
|
+
serialize: (workspaceId)=>JSON.stringify(workspaceId),
|
|
9
|
+
parse: (raw)=>{
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
return typeof parsed === 'string' ? parsed : undefined;
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
13
16
|
}
|
|
17
|
+
});
|
|
18
|
+
const storedLastWorkspaceIdAtom = atom(getLastWorkspaceId());
|
|
19
|
+
storedLastWorkspaceIdAtom.onMount = (setLastWorkspaceId)=>{
|
|
20
|
+
if (typeof window === 'undefined') return undefined;
|
|
21
|
+
const syncFromStorage = (event)=>{
|
|
22
|
+
if (event.key === lastWorkspaceStorageKey) setLastWorkspaceId(getLastWorkspaceId());
|
|
23
|
+
};
|
|
24
|
+
window.addEventListener('storage', syncFromStorage);
|
|
25
|
+
return ()=>window.removeEventListener('storage', syncFromStorage);
|
|
26
|
+
};
|
|
27
|
+
export const lastWorkspaceIdAtom = atom((get)=>get(storedLastWorkspaceIdAtom), (_get, set, workspaceId)=>{
|
|
28
|
+
set(storedLastWorkspaceIdAtom, workspaceId);
|
|
29
|
+
if (workspaceId === undefined) lastWorkspaceStorage.remove();
|
|
30
|
+
else lastWorkspaceStorage.write(workspaceId);
|
|
31
|
+
});
|
|
32
|
+
export function getLastWorkspaceId() {
|
|
33
|
+
return lastWorkspaceStorage.read();
|
|
14
34
|
}
|
|
15
35
|
export function rememberLastWorkspaceId(workspaceId) {
|
|
16
|
-
|
|
17
|
-
window.localStorage.setItem(storageKey, JSON.stringify(workspaceId));
|
|
36
|
+
lastWorkspaceStorage.write(workspaceId);
|
|
18
37
|
}
|
|
19
38
|
|
|
20
39
|
//# sourceMappingURL=last-workspace.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/runtime/last-workspace.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"sources":["../../src/runtime/last-workspace.ts"],"sourcesContent":["import {createTypedBrowserStorage, localStorageOrUndefined} from '@shipfox/client-ui';\nimport {atom} from 'jotai';\n\nconst lastWorkspaceStorageKey = 'shipfox.lastWorkspaceId';\n\nconst lastWorkspaceStorage = createTypedBrowserStorage(localStorageOrUndefined, {\n key: lastWorkspaceStorageKey,\n lifetime: 'persistent',\n principalScope: 'principal',\n serialize: (workspaceId: string) => JSON.stringify(workspaceId),\n parse: (raw) => {\n try {\n const parsed: unknown = JSON.parse(raw);\n return typeof parsed === 'string' ? parsed : undefined;\n } catch {\n return undefined;\n }\n },\n});\n\nconst storedLastWorkspaceIdAtom = atom<string | undefined>(getLastWorkspaceId());\n\nstoredLastWorkspaceIdAtom.onMount = (setLastWorkspaceId) => {\n if (typeof window === 'undefined') return undefined;\n\n const syncFromStorage = (event: StorageEvent) => {\n if (event.key === lastWorkspaceStorageKey) setLastWorkspaceId(getLastWorkspaceId());\n };\n window.addEventListener('storage', syncFromStorage);\n return () => window.removeEventListener('storage', syncFromStorage);\n};\n\nexport const lastWorkspaceIdAtom = atom(\n (get) => get(storedLastWorkspaceIdAtom),\n (_get, set, workspaceId: string | undefined) => {\n set(storedLastWorkspaceIdAtom, workspaceId);\n if (workspaceId === undefined) lastWorkspaceStorage.remove();\n else lastWorkspaceStorage.write(workspaceId);\n },\n);\n\nexport function getLastWorkspaceId(): string | undefined {\n return lastWorkspaceStorage.read();\n}\n\nexport function rememberLastWorkspaceId(workspaceId: string): void {\n lastWorkspaceStorage.write(workspaceId);\n}\n"],"names":["createTypedBrowserStorage","localStorageOrUndefined","atom","lastWorkspaceStorageKey","lastWorkspaceStorage","key","lifetime","principalScope","serialize","workspaceId","JSON","stringify","parse","raw","parsed","undefined","storedLastWorkspaceIdAtom","getLastWorkspaceId","onMount","setLastWorkspaceId","window","syncFromStorage","event","addEventListener","removeEventListener","lastWorkspaceIdAtom","get","_get","set","remove","write","read","rememberLastWorkspaceId"],"mappings":"AAAA,SAAQA,yBAAyB,EAAEC,uBAAuB,QAAO,qBAAqB;AACtF,SAAQC,IAAI,QAAO,QAAQ;AAE3B,MAAMC,0BAA0B;AAEhC,MAAMC,uBAAuBJ,0BAA0BC,yBAAyB;IAC9EI,KAAKF;IACLG,UAAU;IACVC,gBAAgB;IAChBC,WAAW,CAACC,cAAwBC,KAAKC,SAAS,CAACF;IACnDG,OAAO,CAACC;QACN,IAAI;YACF,MAAMC,SAAkBJ,KAAKE,KAAK,CAACC;YACnC,OAAO,OAAOC,WAAW,WAAWA,SAASC;QAC/C,EAAE,OAAM;YACN,OAAOA;QACT;IACF;AACF;AAEA,MAAMC,4BAA4Bd,KAAyBe;AAE3DD,0BAA0BE,OAAO,GAAG,CAACC;IACnC,IAAI,OAAOC,WAAW,aAAa,OAAOL;IAE1C,MAAMM,kBAAkB,CAACC;QACvB,IAAIA,MAAMjB,GAAG,KAAKF,yBAAyBgB,mBAAmBF;IAChE;IACAG,OAAOG,gBAAgB,CAAC,WAAWF;IACnC,OAAO,IAAMD,OAAOI,mBAAmB,CAAC,WAAWH;AACrD;AAEA,OAAO,MAAMI,sBAAsBvB,KACjC,CAACwB,MAAQA,IAAIV,4BACb,CAACW,MAAMC,KAAKnB;IACVmB,IAAIZ,2BAA2BP;IAC/B,IAAIA,gBAAgBM,WAAWX,qBAAqByB,MAAM;SACrDzB,qBAAqB0B,KAAK,CAACrB;AAClC,GACA;AAEF,OAAO,SAASQ;IACd,OAAOb,qBAAqB2B,IAAI;AAClC;AAEA,OAAO,SAASC,wBAAwBvB,WAAmB;IACzDL,qBAAqB0B,KAAK,CAACrB;AAC7B"}
|