@shipfox/client-shell 5.0.0 → 6.0.2

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 (60) hide show
  1. package/dist/components/main-layout.d.ts.map +1 -1
  2. package/dist/components/main-layout.js +3 -4
  3. package/dist/components/main-layout.js.map +1 -1
  4. package/dist/components/nav-tabs.d.ts.map +1 -1
  5. package/dist/components/nav-tabs.js +3 -4
  6. package/dist/components/nav-tabs.js.map +1 -1
  7. package/dist/components/settings-nav.d.ts.map +1 -1
  8. package/dist/components/settings-nav.js +3 -4
  9. package/dist/components/settings-nav.js.map +1 -1
  10. package/dist/components/workspace-switcher.d.ts.map +1 -1
  11. package/dist/components/workspace-switcher.js +3 -2
  12. package/dist/components/workspace-switcher.js.map +1 -1
  13. package/dist/compose/compose-client-features.d.ts +11 -0
  14. package/dist/compose/compose-client-features.d.ts.map +1 -0
  15. package/dist/compose/compose-client-features.js +19 -0
  16. package/dist/compose/compose-client-features.js.map +1 -0
  17. package/dist/compose/compose-routes.d.ts +1 -0
  18. package/dist/compose/compose-routes.d.ts.map +1 -1
  19. package/dist/compose/compose-routes.js +4 -2
  20. package/dist/compose/compose-routes.js.map +1 -1
  21. package/dist/compose/validate-registries.d.ts +9 -2
  22. package/dist/compose/validate-registries.d.ts.map +1 -1
  23. package/dist/compose/validate-registries.js +32 -8
  24. package/dist/compose/validate-registries.js.map +1 -1
  25. package/dist/contract.d.ts +6 -0
  26. package/dist/contract.d.ts.map +1 -1
  27. package/dist/contract.js.map +1 -1
  28. package/dist/hooks/api/session-auth.d.ts +15 -0
  29. package/dist/hooks/api/session-auth.d.ts.map +1 -0
  30. package/dist/hooks/api/session-auth.js +55 -0
  31. package/dist/hooks/api/session-auth.js.map +1 -0
  32. package/dist/runtime/active-workspace.d.ts.map +1 -1
  33. package/dist/runtime/active-workspace.js +2 -4
  34. package/dist/runtime/active-workspace.js.map +1 -1
  35. package/dist/runtime/anchors.js +5 -5
  36. package/dist/runtime/anchors.js.map +1 -1
  37. package/dist/runtime/auth.d.ts +4 -11
  38. package/dist/runtime/auth.d.ts.map +1 -1
  39. package/dist/runtime/auth.js +73 -64
  40. package/dist/runtime/auth.js.map +1 -1
  41. package/dist/runtime/compose-client-app.d.ts.map +1 -1
  42. package/dist/runtime/compose-client-app.js +3 -9
  43. package/dist/runtime/compose-client-app.js.map +1 -1
  44. package/dist/runtime/index.d.ts +2 -0
  45. package/dist/runtime/index.d.ts.map +1 -1
  46. package/dist/runtime/index.js +2 -0
  47. package/dist/runtime/index.js.map +1 -1
  48. package/dist/runtime/last-workspace.d.ts +6 -3
  49. package/dist/runtime/last-workspace.d.ts.map +1 -1
  50. package/dist/runtime/last-workspace.js +12 -21
  51. package/dist/runtime/last-workspace.js.map +1 -1
  52. package/dist/runtime/route-inputs.d.ts +18 -0
  53. package/dist/runtime/route-inputs.d.ts.map +1 -0
  54. package/dist/runtime/route-inputs.js +49 -0
  55. package/dist/runtime/route-inputs.js.map +1 -0
  56. package/dist/tsconfig.test.tsbuildinfo +1 -1
  57. package/dist/vite/plugin.d.ts.map +1 -1
  58. package/dist/vite/plugin.js +6 -14
  59. package/dist/vite/plugin.js.map +1 -1
  60. package/package.json +10 -11
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runtime/anchors.tsx"],"sourcesContent":["import {ShipfoxLoader} from '@shipfox/react-ui/loader';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {createRootRouteWithContext, createRoute, Outlet, redirect} from '@tanstack/react-router';\nimport {MainLayout} from '#components/main-layout.js';\nimport {NotFoundPage} from '#components/not-found-page.js';\nimport {SettingsNav} from '#components/settings-nav.js';\nimport type {NavTabEntry, SettingsSectionEntry} from '#contract.js';\nimport {useActiveWorkspace} from './active-workspace.js';\nimport {anchorPaths} from './anchor-paths.js';\nimport {useChrome} from './chrome-context.js';\nimport {rememberLastWorkspaceId} from './last-workspace.js';\nimport type {RouterContext} from './router-context.js';\nimport {WorkspaceLayoutErrorRoute, WorkspaceSetupPending} from './workspace-setup.js';\n\nexport {routePathForAnchor} from './anchor-paths.js';\n\nexport function buildAnchorSkeleton({\n navigation,\n settingsSections,\n}: {\n navigation: readonly NavTabEntry[];\n settingsSections: readonly SettingsSectionEntry[];\n}) {\n const rootRoute = createRootRouteWithContext<RouterContext>()({\n component: Outlet,\n notFoundComponent: NotFoundPage,\n });\n const workspaceLayout = createRoute({\n getParentRoute: () => rootRoute,\n path: anchorPaths.workspaceLayout,\n beforeLoad: async ({context, params, location}) => {\n try {\n rememberLastWorkspaceId(params.wid);\n } catch {\n // Local storage is best effort.\n }\n const auth = context.auth;\n if (!auth || auth.isLoading || !context.queryClient) return;\n if (!auth.isAuthenticated)\n throw redirect({to: '/auth/login' as never, search: {redirect: location.href} as never});\n if (!auth.workspaces.some((workspace) => workspace.id === params.wid))\n throw redirect({to: '/'});\n if (!context.workspaceSetup)\n throw new Error('Client composition includes workspace routes but no workspaceSetup gate.');\n return await context.workspaceSetup({\n queryClient: context.queryClient,\n workspaceId: params.wid,\n pathname: location.pathname,\n });\n },\n pendingComponent: () => (\n <div className=\"flex h-screen items-center justify-center\">\n <ShipfoxLoader size={64} animation=\"circular\" color=\"orange\" background=\"dark\" />\n </div>\n ),\n errorComponent: WorkspaceLayoutErrorRoute,\n component: () => {\n const setupState = workspaceLayout.useRouteContext() as {hideProjectNavigation?: boolean};\n return setupState.hideProjectNavigation === undefined ? (\n <WorkspaceSetupPending />\n ) : (\n <MainLayout\n navigation={navigation}\n hideProjectNavigation={setupState.hideProjectNavigation}\n />\n );\n },\n });\n const projectLayout = createRoute({\n getParentRoute: () => workspaceLayout,\n path: '/projects/$pid',\n component: () => {\n const {ProjectLayoutGuard} = useChrome();\n return <ProjectLayoutGuard />;\n },\n });\n const workspaceSettings = createRoute({\n getParentRoute: () => workspaceLayout,\n path: '/settings',\n component: () => {\n const workspace = useActiveWorkspace();\n return (\n <div className=\"flex w-full flex-col gap-24\">\n <header className=\"flex flex-col gap-6\">\n <Header variant=\"h2\">Workspace settings</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Configure {workspace.name}.\n </Text>\n </header>\n\n <div className=\"grid grid-cols-[180px_minmax(0,1fr)] gap-32 max-[760px]:grid-cols-1\">\n <SettingsNav entries={settingsSections} />\n <Outlet />\n </div>\n </div>\n );\n },\n });\n return {\n anchors: {root: rootRoute, workspaceLayout, projectLayout, workspaceSettings},\n rootRoute,\n workspaceLayout,\n projectLayout,\n workspaceSettings,\n };\n}\n"],"names":["ShipfoxLoader","Header","Text","createRootRouteWithContext","createRoute","Outlet","redirect","MainLayout","NotFoundPage","SettingsNav","useActiveWorkspace","anchorPaths","useChrome","rememberLastWorkspaceId","WorkspaceLayoutErrorRoute","WorkspaceSetupPending","routePathForAnchor","buildAnchorSkeleton","navigation","settingsSections","rootRoute","component","notFoundComponent","workspaceLayout","getParentRoute","path","beforeLoad","context","params","location","wid","auth","isLoading","queryClient","isAuthenticated","to","search","href","workspaces","some","workspace","id","workspaceSetup","Error","workspaceId","pathname","pendingComponent","div","className","size","animation","color","background","errorComponent","setupState","useRouteContext","hideProjectNavigation","undefined","projectLayout","ProjectLayoutGuard","workspaceSettings","header","variant","name","entries","anchors","root"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/runtime/anchors.tsx"],"sourcesContent":["import {ShipfoxLoader} from '@shipfox/react-ui/loader';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {createRootRouteWithContext, createRoute, Outlet, redirect} from '@tanstack/react-router';\nimport {MainLayout} from '#components/main-layout.js';\nimport {NotFoundPage} from '#components/not-found-page.js';\nimport {SettingsNav} from '#components/settings-nav.js';\nimport type {NavTabEntry, SettingsSectionEntry} from '#contract.js';\nimport {useActiveWorkspace} from './active-workspace.js';\nimport {anchorPaths} from './anchor-paths.js';\nimport {useChrome} from './chrome-context.js';\nimport {rememberLastWorkspaceId} from './last-workspace.js';\nimport type {RouterContext} from './router-context.js';\nimport {WorkspaceLayoutErrorRoute, WorkspaceSetupPending} from './workspace-setup.js';\n\nexport {routePathForAnchor} from './anchor-paths.js';\n\nexport function buildAnchorSkeleton({\n navigation,\n settingsSections,\n}: {\n navigation: readonly NavTabEntry[];\n settingsSections: readonly SettingsSectionEntry[];\n}) {\n const rootRoute = createRootRouteWithContext<RouterContext>()({\n component: Outlet,\n notFoundComponent: NotFoundPage,\n });\n const workspaceLayout = createRoute({\n getParentRoute: () => rootRoute,\n path: anchorPaths.workspaceLayout,\n beforeLoad: async ({context, params, location}) => {\n const auth = context.auth;\n if (!auth || auth.isLoading || !context.queryClient) return;\n if (!auth.isAuthenticated)\n throw redirect({to: '/auth/login' as never, search: {redirect: location.href} as never});\n if (!auth.workspaces.some((workspace) => workspace.id === params.wid))\n throw redirect({to: '/'});\n try {\n if (auth.user?.id) rememberLastWorkspaceId(auth.user.id, params.wid);\n } catch {\n // Local storage is best effort.\n }\n if (!context.workspaceSetup)\n throw new Error('Client composition includes workspace routes but no workspaceSetup gate.');\n return await context.workspaceSetup({\n queryClient: context.queryClient,\n workspaceId: params.wid,\n pathname: location.pathname,\n });\n },\n pendingComponent: () => (\n <div className=\"flex h-screen items-center justify-center\">\n <ShipfoxLoader size={64} animation=\"circular\" color=\"orange\" background=\"dark\" />\n </div>\n ),\n errorComponent: WorkspaceLayoutErrorRoute,\n component: () => {\n const setupState = workspaceLayout.useRouteContext() as {hideProjectNavigation?: boolean};\n return setupState.hideProjectNavigation === undefined ? (\n <WorkspaceSetupPending />\n ) : (\n <MainLayout\n navigation={navigation}\n hideProjectNavigation={setupState.hideProjectNavigation}\n />\n );\n },\n });\n const projectLayout = createRoute({\n getParentRoute: () => workspaceLayout,\n path: '/projects/$pid',\n component: () => {\n const {ProjectLayoutGuard} = useChrome();\n return <ProjectLayoutGuard />;\n },\n });\n const workspaceSettings = createRoute({\n getParentRoute: () => workspaceLayout,\n path: '/settings',\n component: () => {\n const workspace = useActiveWorkspace();\n return (\n <div className=\"flex w-full flex-col gap-24\">\n <header className=\"flex flex-col gap-6\">\n <Header variant=\"h2\">Workspace settings</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Configure {workspace.name}.\n </Text>\n </header>\n\n <div className=\"grid grid-cols-[180px_minmax(0,1fr)] gap-32 max-[760px]:grid-cols-1\">\n <SettingsNav entries={settingsSections} />\n <Outlet />\n </div>\n </div>\n );\n },\n });\n return {\n anchors: {root: rootRoute, workspaceLayout, projectLayout, workspaceSettings},\n rootRoute,\n workspaceLayout,\n projectLayout,\n workspaceSettings,\n };\n}\n"],"names":["ShipfoxLoader","Header","Text","createRootRouteWithContext","createRoute","Outlet","redirect","MainLayout","NotFoundPage","SettingsNav","useActiveWorkspace","anchorPaths","useChrome","rememberLastWorkspaceId","WorkspaceLayoutErrorRoute","WorkspaceSetupPending","routePathForAnchor","buildAnchorSkeleton","navigation","settingsSections","rootRoute","component","notFoundComponent","workspaceLayout","getParentRoute","path","beforeLoad","context","params","location","auth","isLoading","queryClient","isAuthenticated","to","search","href","workspaces","some","workspace","id","wid","user","workspaceSetup","Error","workspaceId","pathname","pendingComponent","div","className","size","animation","color","background","errorComponent","setupState","useRouteContext","hideProjectNavigation","undefined","projectLayout","ProjectLayoutGuard","workspaceSettings","header","variant","name","entries","anchors","root"],"mappings":"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"}
@@ -18,21 +18,14 @@ export declare const initialAuthState: AuthState;
18
18
  export declare const authStateAtom: import("jotai").PrimitiveAtom<AuthState> & {
19
19
  init: AuthState;
20
20
  };
21
- export declare const authRefreshQueryKey: readonly ["auth", "refresh"];
22
- export declare const userWorkspacesQueryKey: readonly ["workspaces", "mine"];
23
21
  export declare function toAuthenticatedState(session: AuthenticatedSession, workspaces?: WorkspaceSummary[]): AuthState;
24
22
  export declare function useAuthState(): AuthStateValue;
25
- export declare function listUserWorkspaces(token?: string): Promise<{
26
- memberships: {
27
- id: string;
28
- name: string;
29
- membershipId: string;
30
- }[];
31
- }>;
23
+ export { authRefreshQueryKey, authRefreshQueryOptions, listUserWorkspaces, userWorkspacesQueryKey, userWorkspacesQueryOptions, } from '#hooks/api/session-auth.js';
32
24
  export declare function getAuthRefreshDelayMs(token: string, nowMs?: number): number | undefined;
33
25
  export declare function useAuthTransition(): {
34
- enterAuthenticated: (session: AuthenticatedSession) => Promise<void>;
35
- enterGuest: () => Promise<void>;
26
+ beginAuthTransition: () => number;
27
+ enterAuthenticated: (session: AuthenticatedSession, transitionEpoch?: number) => Promise<boolean>;
28
+ enterGuest: (transitionEpoch?: number) => Promise<boolean>;
36
29
  };
37
30
  export declare function useRefreshAuth(): () => Promise<AuthenticatedSession>;
38
31
  export interface AuthRuntimeProps extends PropsWithChildren {
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/runtime/auth.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAE7C,OAAO,KAAK,EAAC,oBAAoB,EAAE,YAAY,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAiB3F,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;AAG/D,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,oBAAoB,EAC7B,UAAU,GAAE,gBAAgB,EAAO,GAClC,SAAS,CAOX;AAED,wBAAgB,YAAY,IAAI,cAAc,CAY7C;AAED,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,4BAA4B,CAAC;AAoBpC,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAa,GAAG,MAAM,GAAG,SAAS,CAG3F;AAED,wBAAgB,iBAAiB;;kCAkCb,oBAAoB,oBAAoB,MAAM;mCAhBrC,MAAM;EAiDlC;AAED,wBAAgB,cAAc,wCAuC7B;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"}
@@ -1,29 +1,24 @@
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';
1
+ import { ApiError, configureApiClient } from '@shipfox/client-api';
4
2
  import { useQueryClient } from '@tanstack/react-query';
5
3
  import { atom, useAtomValue, useSetAtom, useStore } from 'jotai';
6
4
  import { useCallback, useEffect, useMemo } from 'react';
7
- import { toAuthenticatedSession } from '#hooks/api/session-mapper.js';
5
+ import { authRefreshQueryKey, authRefreshQueryOptions, userWorkspacesQueryOptions } from '#hooks/api/session-auth.js';
6
+ import { lastWorkspaceIdAtom } from './last-workspace.js';
8
7
  const REFRESH_EARLY_MS = 5 * 60 * 1000;
9
8
  const REFRESH_RETRY_DELAY_MS = 60_000;
10
9
  const BASE64_URL_REPLACEMENTS = {
11
10
  dash: /-/g,
12
11
  underscore: /_/g
13
12
  };
13
+ const refreshPromises = new WeakMap();
14
+ function invalidateRefresh(queryClient) {
15
+ refreshPromises.delete(queryClient);
16
+ }
14
17
  export const initialAuthState = {
15
18
  status: 'loading'
16
19
  };
17
20
  export const authStateAtom = atom(initialAuthState);
18
21
  const authTransitionEpochAtom = atom(0);
19
- export const authRefreshQueryKey = [
20
- 'auth',
21
- 'refresh'
22
- ];
23
- export const userWorkspacesQueryKey = [
24
- 'workspaces',
25
- 'mine'
26
- ];
27
22
  export function toAuthenticatedState(session, workspaces = []) {
28
23
  return {
29
24
  status: 'authenticated',
@@ -44,26 +39,7 @@ export function useAuthState() {
44
39
  state
45
40
  ]);
46
41
  }
47
- export async function listUserWorkspaces(token) {
48
- const response = await checkedApiRequest(listUserWorkspacesResponseSchema, '/workspaces', token ? {
49
- headers: {
50
- authorization: `Bearer ${token}`
51
- }
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
- };
60
- }
61
- async function refreshAuthRequest() {
62
- const response = await checkedApiRequest(loginResponseSchema, '/auth/refresh', {
63
- method: 'POST'
64
- });
65
- return toAuthenticatedSession(response);
66
- }
42
+ export { authRefreshQueryKey, authRefreshQueryOptions, listUserWorkspaces, userWorkspacesQueryKey, userWorkspacesQueryOptions } from '#hooks/api/session-auth.js';
67
43
  function decodeBase64Url(value) {
68
44
  const base64 = value.replace(BASE64_URL_REPLACEMENTS.dash, '+').replace(BASE64_URL_REPLACEMENTS.underscore, '/');
69
45
  return atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, '='));
@@ -86,77 +62,110 @@ export function useAuthTransition() {
86
62
  const queryClient = useQueryClient();
87
63
  const store = useStore();
88
64
  const setState = useSetAtom(authStateAtom);
65
+ const setLastWorkspaceId = useSetAtom(lastWorkspaceIdAtom);
66
+ const beginAuthTransition = useCallback(()=>{
67
+ const transitionEpoch = store.get(authTransitionEpochAtom) + 1;
68
+ store.set(authTransitionEpochAtom, transitionEpoch);
69
+ return transitionEpoch;
70
+ }, [
71
+ store
72
+ ]);
89
73
  const clearPrivateState = useCallback(async ()=>{
90
74
  await queryClient.cancelQueries();
91
75
  queryClient.clear();
92
76
  }, [
93
77
  queryClient
94
78
  ]);
95
- const enterGuest = useCallback(async ()=>{
96
- const transitionEpoch = store.get(authTransitionEpochAtom) + 1;
97
- store.set(authTransitionEpochAtom, transitionEpoch);
79
+ const enterGuest = useCallback(async (transitionEpoch)=>{
80
+ const isExternalTransition = transitionEpoch === undefined;
81
+ const epoch = transitionEpoch ?? beginAuthTransition();
82
+ if (isExternalTransition) invalidateRefresh(queryClient);
83
+ if (store.get(authTransitionEpochAtom) !== epoch) return false;
98
84
  await clearPrivateState();
99
- if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;
85
+ if (store.get(authTransitionEpochAtom) !== epoch) return false;
86
+ setLastWorkspaceId(undefined);
100
87
  setState({
101
88
  status: 'guest'
102
89
  });
90
+ return true;
103
91
  }, [
92
+ beginAuthTransition,
104
93
  clearPrivateState,
94
+ queryClient,
95
+ setLastWorkspaceId,
105
96
  setState,
106
97
  store
107
98
  ]);
108
- const enterAuthenticated = useCallback(async (session)=>{
109
- const transitionEpoch = store.get(authTransitionEpochAtom) + 1;
110
- store.set(authTransitionEpochAtom, transitionEpoch);
99
+ const enterAuthenticated = useCallback(async (session, transitionEpoch)=>{
100
+ const isExternalTransition = transitionEpoch === undefined;
101
+ const epoch = transitionEpoch ?? beginAuthTransition();
102
+ if (isExternalTransition) invalidateRefresh(queryClient);
103
+ if (store.get(authTransitionEpochAtom) !== epoch) return false;
111
104
  const previousState = store.get(authStateAtom);
112
- const principalChanged = previousState.status === 'authenticated' && previousState.user?.id !== session.user.id;
105
+ const principalChanged = previousState.status !== 'authenticated' || previousState.user?.id !== session.user.id;
113
106
  if (principalChanged) await clearPrivateState();
114
- if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;
107
+ if (store.get(authTransitionEpochAtom) !== epoch) return false;
108
+ if (principalChanged) setLastWorkspaceId(undefined);
115
109
  queryClient.setQueryData(authRefreshQueryKey, session);
116
110
  let workspaces = [];
117
111
  try {
118
- const hydratedWorkspaces = await queryClient.fetchQuery({
119
- queryKey: userWorkspacesQueryKey,
120
- queryFn: ()=>listUserWorkspaces(session.accessToken),
121
- retry: false,
122
- staleTime: 0
123
- });
112
+ const hydratedWorkspaces = await queryClient.fetchQuery(userWorkspacesQueryOptions(session.accessToken));
124
113
  workspaces = hydratedWorkspaces.memberships;
125
- queryClient.setQueryData(userWorkspacesQueryKey, hydratedWorkspaces);
126
114
  } catch {
127
115
  // The authenticated session remains usable while workspace hydration retries on the next route load.
128
116
  }
129
- if (store.get(authTransitionEpochAtom) !== transitionEpoch) return;
117
+ if (store.get(authTransitionEpochAtom) !== epoch) return false;
130
118
  setState(toAuthenticatedState(session, workspaces));
119
+ return true;
131
120
  }, [
121
+ beginAuthTransition,
132
122
  clearPrivateState,
133
123
  queryClient,
124
+ setLastWorkspaceId,
134
125
  setState,
135
126
  store
136
127
  ]);
137
128
  return {
129
+ beginAuthTransition,
138
130
  enterAuthenticated,
139
131
  enterGuest
140
132
  };
141
133
  }
142
134
  export function useRefreshAuth() {
143
135
  const queryClient = useQueryClient();
144
- const { enterAuthenticated, enterGuest } = useAuthTransition();
145
- return useCallback(async ()=>{
146
- try {
147
- const result = await queryClient.fetchQuery({
148
- queryKey: authRefreshQueryKey,
149
- queryFn: refreshAuthRequest,
150
- retry: false,
151
- staleTime: 0
152
- });
153
- await enterAuthenticated(result);
154
- return result;
155
- } catch (error) {
156
- if (error instanceof ApiError && error.status === 401) await enterGuest();
157
- throw error;
158
- }
136
+ const { beginAuthTransition, enterAuthenticated, enterGuest } = useAuthTransition();
137
+ return useCallback(()=>{
138
+ const existingRefresh = refreshPromises.get(queryClient);
139
+ if (existingRefresh) return existingRefresh;
140
+ const transitionEpoch = beginAuthTransition();
141
+ const refresh = (async ()=>{
142
+ try {
143
+ const result = await queryClient.fetchQuery(authRefreshQueryOptions());
144
+ const accepted = await enterAuthenticated(result, transitionEpoch);
145
+ if (!accepted) {
146
+ throw new ApiError({
147
+ message: 'Authentication refresh was superseded.',
148
+ code: 'unauthorized',
149
+ status: 401
150
+ });
151
+ }
152
+ return result;
153
+ } catch (error) {
154
+ if (error instanceof ApiError && error.status === 401) {
155
+ await enterGuest(transitionEpoch);
156
+ }
157
+ throw error;
158
+ }
159
+ })();
160
+ refreshPromises.set(queryClient, refresh);
161
+ void refresh.then(()=>{
162
+ if (refreshPromises.get(queryClient) === refresh) refreshPromises.delete(queryClient);
163
+ }, ()=>{
164
+ if (refreshPromises.get(queryClient) === refresh) refreshPromises.delete(queryClient);
165
+ });
166
+ return refresh;
159
167
  }, [
168
+ beginAuthTransition,
160
169
  enterAuthenticated,
161
170
  enterGuest,
162
171
  queryClient
@@ -1 +1 @@
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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"}
1
+ {"version":3,"sources":["../../src/runtime/auth.tsx"],"sourcesContent":["import {ApiError, configureApiClient} from '@shipfox/client-api';\nimport {type QueryClient, 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 {\n authRefreshQueryKey,\n authRefreshQueryOptions,\n userWorkspacesQueryOptions,\n} from '#hooks/api/session-auth.js';\nimport {lastWorkspaceIdAtom} from './last-workspace.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;\nconst refreshPromises = new WeakMap<QueryClient, Promise<AuthenticatedSession>>();\n\nfunction invalidateRefresh(queryClient: QueryClient): void {\n refreshPromises.delete(queryClient);\n}\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);\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 {\n authRefreshQueryKey,\n authRefreshQueryOptions,\n listUserWorkspaces,\n userWorkspacesQueryKey,\n userWorkspacesQueryOptions,\n} from '#hooks/api/session-auth.js';\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 const setLastWorkspaceId = useSetAtom(lastWorkspaceIdAtom);\n\n const beginAuthTransition = useCallback(() => {\n const transitionEpoch = store.get(authTransitionEpochAtom) + 1;\n store.set(authTransitionEpochAtom, transitionEpoch);\n return transitionEpoch;\n }, [store]);\n\n const clearPrivateState = useCallback(async () => {\n await queryClient.cancelQueries();\n queryClient.clear();\n }, [queryClient]);\n\n const enterGuest = useCallback(\n async (transitionEpoch?: number) => {\n const isExternalTransition = transitionEpoch === undefined;\n const epoch = transitionEpoch ?? beginAuthTransition();\n if (isExternalTransition) invalidateRefresh(queryClient);\n if (store.get(authTransitionEpochAtom) !== epoch) return false;\n\n await clearPrivateState();\n if (store.get(authTransitionEpochAtom) !== epoch) return false;\n setLastWorkspaceId(undefined);\n setState({status: 'guest'});\n return true;\n },\n [beginAuthTransition, clearPrivateState, queryClient, setLastWorkspaceId, setState, store],\n );\n\n const enterAuthenticated = useCallback(\n async (session: AuthenticatedSession, transitionEpoch?: number) => {\n const isExternalTransition = transitionEpoch === undefined;\n const epoch = transitionEpoch ?? beginAuthTransition();\n if (isExternalTransition) invalidateRefresh(queryClient);\n if (store.get(authTransitionEpochAtom) !== epoch) return false;\n\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) !== epoch) return false;\n if (principalChanged) setLastWorkspaceId(undefined);\n\n queryClient.setQueryData(authRefreshQueryKey, session);\n let workspaces: WorkspaceSummary[] = [];\n try {\n const hydratedWorkspaces = await queryClient.fetchQuery(\n userWorkspacesQueryOptions(session.accessToken),\n );\n workspaces = hydratedWorkspaces.memberships;\n } catch {\n // The authenticated session remains usable while workspace hydration retries on the next route load.\n }\n if (store.get(authTransitionEpochAtom) !== epoch) return false;\n\n setState(toAuthenticatedState(session, workspaces));\n return true;\n },\n [beginAuthTransition, clearPrivateState, queryClient, setLastWorkspaceId, setState, store],\n );\n\n return {beginAuthTransition, enterAuthenticated, enterGuest};\n}\n\nexport function useRefreshAuth() {\n const queryClient = useQueryClient();\n const {beginAuthTransition, enterAuthenticated, enterGuest} = useAuthTransition();\n\n return useCallback(() => {\n const existingRefresh = refreshPromises.get(queryClient);\n if (existingRefresh) return existingRefresh;\n\n const transitionEpoch = beginAuthTransition();\n const refresh = (async () => {\n try {\n const result = await queryClient.fetchQuery(authRefreshQueryOptions());\n const accepted = await enterAuthenticated(result, transitionEpoch);\n if (!accepted) {\n throw new ApiError({\n message: 'Authentication refresh was superseded.',\n code: 'unauthorized',\n status: 401,\n });\n }\n return result;\n } catch (error) {\n if (error instanceof ApiError && error.status === 401) {\n await enterGuest(transitionEpoch);\n }\n throw error;\n }\n })();\n refreshPromises.set(queryClient, refresh);\n void refresh.then(\n () => {\n if (refreshPromises.get(queryClient) === refresh) refreshPromises.delete(queryClient);\n },\n () => {\n if (refreshPromises.get(queryClient) === refresh) refreshPromises.delete(queryClient);\n },\n );\n return refresh;\n }, [beginAuthTransition, 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":["ApiError","configureApiClient","useQueryClient","atom","useAtomValue","useSetAtom","useStore","useCallback","useEffect","useMemo","authRefreshQueryKey","authRefreshQueryOptions","userWorkspacesQueryOptions","lastWorkspaceIdAtom","REFRESH_EARLY_MS","REFRESH_RETRY_DELAY_MS","BASE64_URL_REPLACEMENTS","dash","underscore","refreshPromises","WeakMap","invalidateRefresh","queryClient","delete","initialAuthState","status","authStateAtom","authTransitionEpochAtom","toAuthenticatedState","session","workspaces","token","accessToken","user","useAuthState","state","isLoading","isAuthenticated","hasWorkspace","length","listUserWorkspaces","userWorkspacesQueryKey","decodeBase64Url","value","base64","replace","atob","padEnd","Math","ceil","readJwtExp","payload","split","undefined","parsed","JSON","parse","exp","Number","isFinite","getAuthRefreshDelayMs","nowMs","Date","now","useAuthTransition","store","setState","setLastWorkspaceId","beginAuthTransition","transitionEpoch","get","set","clearPrivateState","cancelQueries","clear","enterGuest","isExternalTransition","epoch","enterAuthenticated","previousState","principalChanged","id","setQueryData","hydratedWorkspaces","fetchQuery","memberships","useRefreshAuth","existingRefresh","refresh","result","accepted","message","code","error","then","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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"compose-client-app.d.ts","sourceRoot":"","sources":["../../src/runtime/compose-client-app.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAC,KAAK,SAAS,EAAiB,MAAM,wBAAwB,CAAC;AAQtE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAiB,KAAK,WAAW,EAAC,MAAM,qBAAqB,CAAC;AAErE,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AAE7D,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,MAAM,EACN,MAAM,EACN,cAAc,GACf,EAAE;IACD,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,cAAc,CAAC,EAAE,kBAAkB,CAAC;CACrC;mBAkBkB,WAAW,GAAG,IAAI;EAiCpC"}
1
+ {"version":3,"file":"compose-client-app.d.ts","sourceRoot":"","sources":["../../src/runtime/compose-client-app.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAC,KAAK,SAAS,EAAiB,MAAM,wBAAwB,CAAC;AAKtE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAiB,KAAK,WAAW,EAAC,MAAM,qBAAqB,CAAC;AAErE,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AAE7D,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,MAAM,EACN,MAAM,EACN,cAAc,GACf,EAAE;IACD,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,cAAc,CAAC,EAAE,kBAAkB,CAAC;CACrC;mBASkB,WAAW,GAAG,IAAI;EAiCpC"}
@@ -9,19 +9,13 @@ import { RouterProvider } from '@tanstack/react-router';
9
9
  import { createStore } from 'jotai';
10
10
  import { StrictMode, useEffect } from 'react';
11
11
  import { createRoot } from 'react-dom/client';
12
- import { composeRoutes } from '#compose/compose-routes.js';
13
- import { mergeConfigShapes } from '#compose/merge-config.js';
14
- import { validateProviderIds } from '#compose/validate-providers.js';
15
- import { validateNavigation, validateSettingsSections } from '#compose/validate-registries.js';
12
+ import { composeClientFeatures } from '#compose/compose-client-features.js';
16
13
  import { useAuthState } from './auth.js';
17
14
  import { ChromeProvider } from './chrome-context.js';
18
15
  import { ShellProviderStack } from './provider-stack.js';
19
16
  export function composeClientApp({ features, router, chrome, workspaceSetup }) {
20
- const routes = composeRoutes(features);
21
- validateProviderIds(features);
22
- validateNavigation(features, routes.map((route)=>route.path));
23
- validateSettingsSections(features, routes.map((route)=>route.path));
24
- const config = loadConfig(mergeConfigShapes(features), {
17
+ const composition = composeClientFeatures(features);
18
+ const config = loadConfig(composition.configShape, {
25
19
  runtime: getWindowRuntimeConfig(),
26
20
  build: import.meta.env
27
21
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runtime/compose-client-app.tsx"],"sourcesContent":["import {configureApiClient} from '@shipfox/client-api';\nimport {\n ConfigErrorScreen,\n getWindowRuntimeConfig,\n loadConfig,\n setLoadedConfig,\n} from '@shipfox/client-config';\nimport {ThemeProvider} from '@shipfox/react-ui/theme';\nimport {Toaster} from '@shipfox/react-ui/toast';\nimport {TooltipProvider} from '@shipfox/react-ui/tooltip';\nimport {QueryClient} from '@tanstack/react-query';\nimport {type AnyRouter, RouterProvider} from '@tanstack/react-router';\nimport {createStore} from 'jotai';\nimport {StrictMode, useEffect} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport {composeRoutes} from '#compose/compose-routes.js';\nimport {mergeConfigShapes} from '#compose/merge-config.js';\nimport {validateProviderIds} from '#compose/validate-providers.js';\nimport {validateNavigation, validateSettingsSections} from '#compose/validate-registries.js';\nimport type {ClientFeature} from '#contract.js';\nimport {useAuthState} from './auth.js';\nimport {ChromeProvider, type ChromeSlots} from './chrome-context.js';\nimport {ShellProviderStack} from './provider-stack.js';\nimport type {WorkspaceSetupGate} from './workspace-setup.js';\n\nexport function composeClientApp({\n features,\n router,\n chrome,\n workspaceSetup,\n}: {\n features: readonly ClientFeature[];\n router: AnyRouter;\n chrome?: ChromeSlots;\n workspaceSetup?: WorkspaceSetupGate;\n}) {\n const routes = composeRoutes(features);\n validateProviderIds(features);\n validateNavigation(\n features,\n routes.map((route) => route.path),\n );\n validateSettingsSections(\n features,\n routes.map((route) => route.path),\n );\n const config = loadConfig(mergeConfigShapes(features), {\n runtime: getWindowRuntimeConfig(),\n build: (import.meta as ImportMeta & {env?: Record<string, unknown>}).env,\n });\n if (config.ok) setLoadedConfig(config.config);\n\n return {\n mount(element: HTMLElement): void {\n const root = createRoot(element);\n if (!config.ok) {\n root.render(\n <StrictMode>\n <ThemeProvider>\n <TooltipProvider>\n <ConfigErrorScreen errors={config.errors} />\n </TooltipProvider>\n </ThemeProvider>\n </StrictMode>,\n );\n return;\n }\n\n configureApiClient({baseUrl: configApiUrl(config.config)});\n const queryClient = new QueryClient();\n root.render(\n <StrictMode>\n <ChromeProvider chrome={chrome}>\n <ShellProviderStack features={features} queryClient={queryClient} store={createStore()}>\n <RoutedApp\n router={router}\n queryClient={queryClient}\n workspaceSetup={workspaceSetup}\n />\n <Toaster />\n </ShellProviderStack>\n </ChromeProvider>\n </StrictMode>,\n );\n },\n };\n}\n\nfunction RoutedApp({\n router,\n queryClient,\n workspaceSetup,\n}: {\n router: AnyRouter;\n queryClient: QueryClient;\n workspaceSetup: WorkspaceSetupGate | undefined;\n}) {\n const auth = useAuthState();\n\n useEffect(() => {\n if (!auth.isLoading) router.invalidate();\n }, [auth.isLoading, router]);\n\n return (\n <RouterProvider\n router={router as never}\n context={{auth, queryClient, workspaceSetup} as never}\n />\n );\n}\n\nfunction configApiUrl(config: unknown): string {\n if (\n typeof config !== 'object' ||\n config === null ||\n !('apiUrl' in config) ||\n typeof config.apiUrl !== 'string'\n ) {\n throw new Error('Composed client configuration must include a string apiUrl.');\n }\n return config.apiUrl;\n}\n"],"names":["configureApiClient","ConfigErrorScreen","getWindowRuntimeConfig","loadConfig","setLoadedConfig","ThemeProvider","Toaster","TooltipProvider","QueryClient","RouterProvider","createStore","StrictMode","useEffect","createRoot","composeRoutes","mergeConfigShapes","validateProviderIds","validateNavigation","validateSettingsSections","useAuthState","ChromeProvider","ShellProviderStack","composeClientApp","features","router","chrome","workspaceSetup","routes","map","route","path","config","runtime","build","env","ok","mount","element","root","render","errors","baseUrl","configApiUrl","queryClient","store","RoutedApp","auth","isLoading","invalidate","context","apiUrl","Error"],"mappings":";AAAA,SAAQA,kBAAkB,QAAO,sBAAsB;AACvD,SACEC,iBAAiB,EACjBC,sBAAsB,EACtBC,UAAU,EACVC,eAAe,QACV,yBAAyB;AAChC,SAAQC,aAAa,QAAO,0BAA0B;AACtD,SAAQC,OAAO,QAAO,0BAA0B;AAChD,SAAQC,eAAe,QAAO,4BAA4B;AAC1D,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SAAwBC,cAAc,QAAO,yBAAyB;AACtE,SAAQC,WAAW,QAAO,QAAQ;AAClC,SAAQC,UAAU,EAAEC,SAAS,QAAO,QAAQ;AAC5C,SAAQC,UAAU,QAAO,mBAAmB;AAC5C,SAAQC,aAAa,QAAO,6BAA6B;AACzD,SAAQC,iBAAiB,QAAO,2BAA2B;AAC3D,SAAQC,mBAAmB,QAAO,iCAAiC;AACnE,SAAQC,kBAAkB,EAAEC,wBAAwB,QAAO,kCAAkC;AAE7F,SAAQC,YAAY,QAAO,YAAY;AACvC,SAAQC,cAAc,QAAyB,sBAAsB;AACrE,SAAQC,kBAAkB,QAAO,sBAAsB;AAGvD,OAAO,SAASC,iBAAiB,EAC/BC,QAAQ,EACRC,MAAM,EACNC,MAAM,EACNC,cAAc,EAMf;IACC,MAAMC,SAASb,cAAcS;IAC7BP,oBAAoBO;IACpBN,mBACEM,UACAI,OAAOC,GAAG,CAAC,CAACC,QAAUA,MAAMC,IAAI;IAElCZ,yBACEK,UACAI,OAAOC,GAAG,CAAC,CAACC,QAAUA,MAAMC,IAAI;IAElC,MAAMC,SAAS5B,WAAWY,kBAAkBQ,WAAW;QACrDS,SAAS9B;QACT+B,OAAO,AAAC,YAA6DC,GAAG;IAC1E;IACA,IAAIH,OAAOI,EAAE,EAAE/B,gBAAgB2B,OAAOA,MAAM;IAE5C,OAAO;QACLK,OAAMC,OAAoB;YACxB,MAAMC,OAAOzB,WAAWwB;YACxB,IAAI,CAACN,OAAOI,EAAE,EAAE;gBACdG,KAAKC,MAAM,eACT,KAAC5B;8BACC,cAAA,KAACN;kCACC,cAAA,KAACE;sCACC,cAAA,KAACN;gCAAkBuC,QAAQT,OAAOS,MAAM;;;;;gBAKhD;YACF;YAEAxC,mBAAmB;gBAACyC,SAASC,aAAaX,OAAOA,MAAM;YAAC;YACxD,MAAMY,cAAc,IAAInC;YACxB8B,KAAKC,MAAM,eACT,KAAC5B;0BACC,cAAA,KAACS;oBAAeK,QAAQA;8BACtB,cAAA,MAACJ;wBAAmBE,UAAUA;wBAAUoB,aAAaA;wBAAaC,OAAOlC;;0CACvE,KAACmC;gCACCrB,QAAQA;gCACRmB,aAAaA;gCACbjB,gBAAgBA;;0CAElB,KAACpB;;;;;QAKX;IACF;AACF;AAEA,SAASuC,UAAU,EACjBrB,MAAM,EACNmB,WAAW,EACXjB,cAAc,EAKf;IACC,MAAMoB,OAAO3B;IAEbP,UAAU;QACR,IAAI,CAACkC,KAAKC,SAAS,EAAEvB,OAAOwB,UAAU;IACxC,GAAG;QAACF,KAAKC,SAAS;QAAEvB;KAAO;IAE3B,qBACE,KAACf;QACCe,QAAQA;QACRyB,SAAS;YAACH;YAAMH;YAAajB;QAAc;;AAGjD;AAEA,SAASgB,aAAaX,MAAe;IACnC,IACE,OAAOA,WAAW,YAClBA,WAAW,QACX,CAAE,CAAA,YAAYA,MAAK,KACnB,OAAOA,OAAOmB,MAAM,KAAK,UACzB;QACA,MAAM,IAAIC,MAAM;IAClB;IACA,OAAOpB,OAAOmB,MAAM;AACtB"}
1
+ {"version":3,"sources":["../../src/runtime/compose-client-app.tsx"],"sourcesContent":["import {configureApiClient} from '@shipfox/client-api';\nimport {\n ConfigErrorScreen,\n getWindowRuntimeConfig,\n loadConfig,\n setLoadedConfig,\n} from '@shipfox/client-config';\nimport {ThemeProvider} from '@shipfox/react-ui/theme';\nimport {Toaster} from '@shipfox/react-ui/toast';\nimport {TooltipProvider} from '@shipfox/react-ui/tooltip';\nimport {QueryClient} from '@tanstack/react-query';\nimport {type AnyRouter, RouterProvider} from '@tanstack/react-router';\nimport {createStore} from 'jotai';\nimport {StrictMode, useEffect} from 'react';\nimport {createRoot} from 'react-dom/client';\nimport {composeClientFeatures} from '#compose/compose-client-features.js';\nimport type {ClientFeature} from '#contract.js';\nimport {useAuthState} from './auth.js';\nimport {ChromeProvider, type ChromeSlots} from './chrome-context.js';\nimport {ShellProviderStack} from './provider-stack.js';\nimport type {WorkspaceSetupGate} from './workspace-setup.js';\n\nexport function composeClientApp({\n features,\n router,\n chrome,\n workspaceSetup,\n}: {\n features: readonly ClientFeature[];\n router: AnyRouter;\n chrome?: ChromeSlots;\n workspaceSetup?: WorkspaceSetupGate;\n}) {\n const composition = composeClientFeatures(features);\n const config = loadConfig(composition.configShape, {\n runtime: getWindowRuntimeConfig(),\n build: (import.meta as ImportMeta & {env?: Record<string, unknown>}).env,\n });\n if (config.ok) setLoadedConfig(config.config);\n\n return {\n mount(element: HTMLElement): void {\n const root = createRoot(element);\n if (!config.ok) {\n root.render(\n <StrictMode>\n <ThemeProvider>\n <TooltipProvider>\n <ConfigErrorScreen errors={config.errors} />\n </TooltipProvider>\n </ThemeProvider>\n </StrictMode>,\n );\n return;\n }\n\n configureApiClient({baseUrl: configApiUrl(config.config)});\n const queryClient = new QueryClient();\n root.render(\n <StrictMode>\n <ChromeProvider chrome={chrome}>\n <ShellProviderStack features={features} queryClient={queryClient} store={createStore()}>\n <RoutedApp\n router={router}\n queryClient={queryClient}\n workspaceSetup={workspaceSetup}\n />\n <Toaster />\n </ShellProviderStack>\n </ChromeProvider>\n </StrictMode>,\n );\n },\n };\n}\n\nfunction RoutedApp({\n router,\n queryClient,\n workspaceSetup,\n}: {\n router: AnyRouter;\n queryClient: QueryClient;\n workspaceSetup: WorkspaceSetupGate | undefined;\n}) {\n const auth = useAuthState();\n\n useEffect(() => {\n if (!auth.isLoading) router.invalidate();\n }, [auth.isLoading, router]);\n\n return (\n <RouterProvider\n router={router as never}\n context={{auth, queryClient, workspaceSetup} as never}\n />\n );\n}\n\nfunction configApiUrl(config: unknown): string {\n if (\n typeof config !== 'object' ||\n config === null ||\n !('apiUrl' in config) ||\n typeof config.apiUrl !== 'string'\n ) {\n throw new Error('Composed client configuration must include a string apiUrl.');\n }\n return config.apiUrl;\n}\n"],"names":["configureApiClient","ConfigErrorScreen","getWindowRuntimeConfig","loadConfig","setLoadedConfig","ThemeProvider","Toaster","TooltipProvider","QueryClient","RouterProvider","createStore","StrictMode","useEffect","createRoot","composeClientFeatures","useAuthState","ChromeProvider","ShellProviderStack","composeClientApp","features","router","chrome","workspaceSetup","composition","config","configShape","runtime","build","env","ok","mount","element","root","render","errors","baseUrl","configApiUrl","queryClient","store","RoutedApp","auth","isLoading","invalidate","context","apiUrl","Error"],"mappings":";AAAA,SAAQA,kBAAkB,QAAO,sBAAsB;AACvD,SACEC,iBAAiB,EACjBC,sBAAsB,EACtBC,UAAU,EACVC,eAAe,QACV,yBAAyB;AAChC,SAAQC,aAAa,QAAO,0BAA0B;AACtD,SAAQC,OAAO,QAAO,0BAA0B;AAChD,SAAQC,eAAe,QAAO,4BAA4B;AAC1D,SAAQC,WAAW,QAAO,wBAAwB;AAClD,SAAwBC,cAAc,QAAO,yBAAyB;AACtE,SAAQC,WAAW,QAAO,QAAQ;AAClC,SAAQC,UAAU,EAAEC,SAAS,QAAO,QAAQ;AAC5C,SAAQC,UAAU,QAAO,mBAAmB;AAC5C,SAAQC,qBAAqB,QAAO,sCAAsC;AAE1E,SAAQC,YAAY,QAAO,YAAY;AACvC,SAAQC,cAAc,QAAyB,sBAAsB;AACrE,SAAQC,kBAAkB,QAAO,sBAAsB;AAGvD,OAAO,SAASC,iBAAiB,EAC/BC,QAAQ,EACRC,MAAM,EACNC,MAAM,EACNC,cAAc,EAMf;IACC,MAAMC,cAAcT,sBAAsBK;IAC1C,MAAMK,SAASrB,WAAWoB,YAAYE,WAAW,EAAE;QACjDC,SAASxB;QACTyB,OAAO,AAAC,YAA6DC,GAAG;IAC1E;IACA,IAAIJ,OAAOK,EAAE,EAAEzB,gBAAgBoB,OAAOA,MAAM;IAE5C,OAAO;QACLM,OAAMC,OAAoB;YACxB,MAAMC,OAAOnB,WAAWkB;YACxB,IAAI,CAACP,OAAOK,EAAE,EAAE;gBACdG,KAAKC,MAAM,eACT,KAACtB;8BACC,cAAA,KAACN;kCACC,cAAA,KAACE;sCACC,cAAA,KAACN;gCAAkBiC,QAAQV,OAAOU,MAAM;;;;;gBAKhD;YACF;YAEAlC,mBAAmB;gBAACmC,SAASC,aAAaZ,OAAOA,MAAM;YAAC;YACxD,MAAMa,cAAc,IAAI7B;YACxBwB,KAAKC,MAAM,eACT,KAACtB;0BACC,cAAA,KAACK;oBAAeK,QAAQA;8BACtB,cAAA,MAACJ;wBAAmBE,UAAUA;wBAAUkB,aAAaA;wBAAaC,OAAO5B;;0CACvE,KAAC6B;gCACCnB,QAAQA;gCACRiB,aAAaA;gCACbf,gBAAgBA;;0CAElB,KAAChB;;;;;QAKX;IACF;AACF;AAEA,SAASiC,UAAU,EACjBnB,MAAM,EACNiB,WAAW,EACXf,cAAc,EAKf;IACC,MAAMkB,OAAOzB;IAEbH,UAAU;QACR,IAAI,CAAC4B,KAAKC,SAAS,EAAErB,OAAOsB,UAAU;IACxC,GAAG;QAACF,KAAKC,SAAS;QAAErB;KAAO;IAE3B,qBACE,KAACX;QACCW,QAAQA;QACRuB,SAAS;YAACH;YAAMH;YAAaf;QAAc;;AAGjD;AAEA,SAASc,aAAaZ,MAAe;IACnC,IACE,OAAOA,WAAW,YAClBA,WAAW,QACX,CAAE,CAAA,YAAYA,MAAK,KACnB,OAAOA,OAAOoB,MAAM,KAAK,UACzB;QACA,MAAM,IAAIC,MAAM;IAClB;IACA,OAAOrB,OAAOoB,MAAM;AACtB"}
@@ -3,6 +3,7 @@ export { WorkspaceCrumb, type WorkspaceCrumbProps } from '#components/workspace-
3
3
  export { WorkspaceSwitcher } from '#components/workspace-switcher.js';
4
4
  export type { AuthenticatedSession, UserIdentity, WorkspaceMembership, WorkspaceSummary, } from '#core/session.js';
5
5
  export { toAuthenticatedSession, toUserIdentity } from '#hooks/api/session-mapper.js';
6
+ export * from '../compose/compose-client-features.js';
6
7
  export * from '../compose/compose-routes.js';
7
8
  export * from '../compose/errors.js';
8
9
  export * from '../compose/merge-config.js';
@@ -18,6 +19,7 @@ export * from './compose-client-app.js';
18
19
  export * from './define-route.js';
19
20
  export * from './last-workspace.js';
20
21
  export * from './nav-order.js';
22
+ export * from './route-inputs.js';
21
23
  export * from './router-context.js';
22
24
  export * from './workspace-setup.js';
23
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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"}
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,uCAAuC,CAAC;AACtD,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,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC"}
@@ -2,6 +2,7 @@ export { AuthActions, AuthShell } from '#components/auth-shell.js';
2
2
  export { WorkspaceCrumb } from '#components/workspace-crumb.js';
3
3
  export { WorkspaceSwitcher } from '#components/workspace-switcher.js';
4
4
  export { toAuthenticatedSession, toUserIdentity } from '#hooks/api/session-mapper.js';
5
+ export * from '../compose/compose-client-features.js';
5
6
  export * from '../compose/compose-routes.js';
6
7
  export * from '../compose/errors.js';
7
8
  export * from '../compose/merge-config.js';
@@ -17,6 +18,7 @@ export * from './compose-client-app.js';
17
18
  export * from './define-route.js';
18
19
  export * from './last-workspace.js';
19
20
  export * from './nav-order.js';
21
+ export * from './route-inputs.js';
20
22
  export * from './router-context.js';
21
23
  export * from './workspace-setup.js';
22
24
 
@@ -1 +1 @@
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
+ {"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-client-features.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 './route-inputs.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,wCAAwC;AACtD,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,oBAAoB;AAClC,cAAc,sBAAsB;AACpC,cAAc,uBAAuB"}
@@ -1,4 +1,7 @@
1
- export declare const lastWorkspaceIdAtom: import("jotai").WritableAtom<string | undefined, [workspaceId: string | undefined], void>;
2
- export declare function getLastWorkspaceId(): string | undefined;
3
- export declare function rememberLastWorkspaceId(workspaceId: string): void;
1
+ /** The current workspace selection for consumers that need in-memory UI state. */
2
+ export declare const lastWorkspaceIdAtom: import("jotai").PrimitiveAtom<string | undefined> & {
3
+ init: string | undefined;
4
+ };
5
+ export declare function getLastWorkspaceId(principalId: string): string | undefined;
6
+ export declare function rememberLastWorkspaceId(principalId: string, workspaceId: string): void;
4
7
  //# 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":"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
+ {"version":3,"file":"last-workspace.d.ts","sourceRoot":"","sources":["../../src/runtime/last-workspace.ts"],"names":[],"mappings":"AAkBA,kFAAkF;AAClF,eAAO,MAAM,mBAAmB;;CAAsC,CAAC;AAEvE,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE1E;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAEtF"}
@@ -1,8 +1,7 @@
1
1
  import { createTypedBrowserStorage, localStorageOrUndefined } from '@shipfox/client-ui';
2
2
  import { atom } from 'jotai';
3
- const lastWorkspaceStorageKey = 'shipfox.lastWorkspaceId';
4
- const lastWorkspaceStorage = createTypedBrowserStorage(localStorageOrUndefined, {
5
- key: lastWorkspaceStorageKey,
3
+ const lastWorkspaceStorageKey = {
4
+ key: 'shipfox.lastWorkspaceId',
6
5
  lifetime: 'persistent',
7
6
  principalScope: 'principal',
8
7
  serialize: (workspaceId)=>JSON.stringify(workspaceId),
@@ -14,26 +13,18 @@ const lastWorkspaceStorage = createTypedBrowserStorage(localStorageOrUndefined,
14
13
  return undefined;
15
14
  }
16
15
  }
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
16
  };
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();
17
+ /** The current workspace selection for consumers that need in-memory UI state. */ export const lastWorkspaceIdAtom = atom(undefined);
18
+ export function getLastWorkspaceId(principalId) {
19
+ return lastWorkspaceStorage(principalId).read();
34
20
  }
35
- export function rememberLastWorkspaceId(workspaceId) {
36
- lastWorkspaceStorage.write(workspaceId);
21
+ export function rememberLastWorkspaceId(principalId, workspaceId) {
22
+ lastWorkspaceStorage(principalId).write(workspaceId);
23
+ }
24
+ function lastWorkspaceStorage(principalId) {
25
+ return createTypedBrowserStorage(localStorageOrUndefined, lastWorkspaceStorageKey, {
26
+ principalId
27
+ });
37
28
  }
38
29
 
39
30
  //# sourceMappingURL=last-workspace.js.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"sources":["../../src/runtime/last-workspace.ts"],"sourcesContent":["import {createTypedBrowserStorage, localStorageOrUndefined} from '@shipfox/client-ui';\nimport {atom} from 'jotai';\n\nconst lastWorkspaceStorageKey = {\n key: 'shipfox.lastWorkspaceId',\n lifetime: 'persistent',\n principalScope: 'principal',\n serialize: (workspaceId: string) => JSON.stringify(workspaceId),\n parse: (raw: string) => {\n try {\n const parsed: unknown = JSON.parse(raw);\n return typeof parsed === 'string' ? parsed : undefined;\n } catch {\n return undefined;\n }\n },\n} as const;\n\n/** The current workspace selection for consumers that need in-memory UI state. */\nexport const lastWorkspaceIdAtom = atom<string | undefined>(undefined);\n\nexport function getLastWorkspaceId(principalId: string): string | undefined {\n return lastWorkspaceStorage(principalId).read();\n}\n\nexport function rememberLastWorkspaceId(principalId: string, workspaceId: string): void {\n lastWorkspaceStorage(principalId).write(workspaceId);\n}\n\nfunction lastWorkspaceStorage(principalId: string) {\n return createTypedBrowserStorage(localStorageOrUndefined, lastWorkspaceStorageKey, {\n principalId,\n });\n}\n"],"names":["createTypedBrowserStorage","localStorageOrUndefined","atom","lastWorkspaceStorageKey","key","lifetime","principalScope","serialize","workspaceId","JSON","stringify","parse","raw","parsed","undefined","lastWorkspaceIdAtom","getLastWorkspaceId","principalId","lastWorkspaceStorage","read","rememberLastWorkspaceId","write"],"mappings":"AAAA,SAAQA,yBAAyB,EAAEC,uBAAuB,QAAO,qBAAqB;AACtF,SAAQC,IAAI,QAAO,QAAQ;AAE3B,MAAMC,0BAA0B;IAC9BC,KAAK;IACLC,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,gFAAgF,GAChF,OAAO,MAAMC,sBAAsBb,KAAyBY,WAAW;AAEvE,OAAO,SAASE,mBAAmBC,WAAmB;IACpD,OAAOC,qBAAqBD,aAAaE,IAAI;AAC/C;AAEA,OAAO,SAASC,wBAAwBH,WAAmB,EAAET,WAAmB;IAC9EU,qBAAqBD,aAAaI,KAAK,CAACb;AAC1C;AAEA,SAASU,qBAAqBD,WAAmB;IAC/C,OAAOjB,0BAA0BC,yBAAyBE,yBAAyB;QACjFc;IACF;AACF"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Reads inputs at the dynamic feature-route boundary. Feature routes must immediately
3
+ * validate the result and pass typed values to pages; Shell never interprets feature input.
4
+ */
5
+ export declare function useRouteSearch<T>(parse: (search: Record<string, unknown>) => T): T;
6
+ /**
7
+ * Reads path parameters at the dynamic feature-route boundary without making Shell
8
+ * responsible for a feature's parameter contract.
9
+ */
10
+ export declare function useRouteParams<T>(parse: (params: Record<string, unknown>) => T): T;
11
+ export declare function parseWorkspaceParams(input: Record<string, unknown>): {
12
+ wid?: string;
13
+ };
14
+ export declare function parseWorkspaceProjectParams(input: Record<string, unknown>): {
15
+ wid?: string;
16
+ pid?: string;
17
+ };
18
+ //# sourceMappingURL=route-inputs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-inputs.d.ts","sourceRoot":"","sources":["../../src/runtime/route-inputs.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAGlF;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAGlF;AAMD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAC,CAGnF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAC3E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAOA"}