@sybilion/uilib 1.0.32 → 1.2.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/README.md +8 -8
- package/assets/{mini-app-global.css → standalone-global.css} +1 -1
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.constants.js +11 -0
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +2 -1
- package/dist/esm/components/ui/Sidebar/Sidebar.js +1 -8
- package/dist/esm/components/ui/Sidebar/Sidebar.styl.js +2 -2
- package/dist/esm/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.js +48 -0
- package/dist/esm/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.styl.js +7 -0
- package/dist/esm/components/widgets/SidebarDatasetsItemsGrouped/groupSidebarDatasets.js +38 -0
- package/dist/esm/index.js +6 -6
- package/dist/esm/sybilion-auth/SybilionAuthProvider.js +185 -0
- package/dist/esm/sybilion-auth/authPaths.js +7 -0
- package/dist/esm/sybilion-auth/exchangeSybilionToken.js +40 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.constants.d.ts +9 -0
- package/dist/esm/types/src/components/ui/Input/Input.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Sidebar/Sidebar.d.ts +1 -5
- package/dist/esm/types/src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.d.ts +11 -0
- package/dist/esm/types/src/components/widgets/SidebarDatasetsItemsGrouped/groupSidebarDatasets.d.ts +8 -0
- package/dist/esm/types/src/components/widgets/SidebarDatasetsItemsGrouped/index.d.ts +3 -0
- package/dist/esm/types/src/docs/pages/SidebarDatasetsItemsGroupedPage.d.ts +1 -0
- package/dist/esm/types/src/docs/pages/SybilionAuthProviderPage.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +3 -1
- package/dist/esm/types/src/sybilion-auth/SybilionAuthProvider.d.ts +39 -0
- package/dist/esm/types/src/sybilion-auth/authPaths.d.ts +3 -0
- package/dist/esm/types/src/sybilion-auth/exchangeSybilionToken.d.ts +2 -0
- package/dist/esm/types/src/sybilion-auth/index.d.ts +4 -0
- package/dist/esm/types/src/{mini-app/miniAppDataTypes.d.ts → types/sybilionDatasetSnapshots.d.ts} +5 -8
- package/docs/standalone-apps.md +65 -0
- package/package.json +10 -3
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.constants.ts +9 -0
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +2 -1
- package/src/components/ui/Sidebar/Sidebar.styl +0 -30
- package/src/components/ui/Sidebar/Sidebar.styl.d.ts +0 -2
- package/src/components/ui/Sidebar/Sidebar.tsx +0 -30
- package/src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.styl +29 -0
- package/src/{mini-app/MiniAppRoot.styl.d.ts → components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.styl.d.ts} +4 -2
- package/src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.tsx +128 -0
- package/src/components/widgets/SidebarDatasetsItemsGrouped/groupSidebarDatasets.ts +51 -0
- package/src/components/widgets/SidebarDatasetsItemsGrouped/index.ts +10 -0
- package/src/docs/components/DocsSidebar/DocsSidebar.tsx +47 -50
- package/src/docs/pages/SidebarDatasetsItemsGroupedPage.tsx +136 -0
- package/src/docs/pages/SidebarPage.tsx +21 -28
- package/src/docs/pages/SybilionAuthProviderPage.tsx +37 -0
- package/src/docs/registry.ts +9 -3
- package/src/index.ts +3 -1
- package/src/sybilion-auth/SybilionAuthProvider.tsx +322 -0
- package/src/sybilion-auth/authPaths.ts +6 -0
- package/src/sybilion-auth/exchangeSybilionToken.ts +47 -0
- package/src/sybilion-auth/index.ts +16 -0
- package/src/{mini-app/miniAppDataTypes.ts → types/sybilionDatasetSnapshots.ts} +5 -8
- package/dist/esm/mini-app/MiniAppRoot.js +0 -82
- package/dist/esm/mini-app/MiniAppRoot.styl.js +0 -7
- package/dist/esm/mini-app/miniAppChatBridge.js +0 -45
- package/dist/esm/mini-app/miniAppDataClient.js +0 -98
- package/dist/esm/mini-app/miniAppProtocol.js +0 -153
- package/dist/esm/mini-app/miniAppThemeConfig.js +0 -40
- package/dist/esm/types/src/docs/pages/MiniAppRootPage.d.ts +0 -1
- package/dist/esm/types/src/mini-app/MiniAppRoot.d.ts +0 -18
- package/dist/esm/types/src/mini-app/index.d.ts +0 -10
- package/dist/esm/types/src/mini-app/miniAppChatBridge.d.ts +0 -6
- package/dist/esm/types/src/mini-app/miniAppDataClient.d.ts +0 -16
- package/dist/esm/types/src/mini-app/miniAppProtocol.d.ts +0 -89
- package/dist/esm/types/src/mini-app/miniAppThemeConfig.d.ts +0 -3
- package/docs/workspace-mini-apps.md +0 -51
- package/src/docs/pages/MiniAppRootPage.tsx +0 -58
- package/src/mini-app/MiniAppRoot.styl +0 -24
- package/src/mini-app/MiniAppRoot.tsx +0 -150
- package/src/mini-app/index.ts +0 -43
- package/src/mini-app/miniAppChatBridge.ts +0 -55
- package/src/mini-app/miniAppDataClient.ts +0 -165
- package/src/mini-app/miniAppProtocol.ts +0 -247
- package/src/mini-app/miniAppThemeConfig.ts +0 -45
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Auth0Provider,
|
|
3
|
+
type RedirectLoginOptions,
|
|
4
|
+
useAuth0,
|
|
5
|
+
} from '@auth0/auth0-react';
|
|
6
|
+
import type { JSX, ReactNode } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
createContext,
|
|
9
|
+
useCallback,
|
|
10
|
+
useContext,
|
|
11
|
+
useEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
} from 'react';
|
|
16
|
+
|
|
17
|
+
import { normalizeApiBaseUrl } from '#uilib/sybilion-auth/authPaths';
|
|
18
|
+
import { exchangeAuth0AccessTokenForSybilionJwt } from '#uilib/sybilion-auth/exchangeSybilionToken';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_TOKEN_KEY = 'sybilion.standalone.jwt';
|
|
21
|
+
|
|
22
|
+
export type SybilionAuthProviderProps = {
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
apiBaseUrl: string;
|
|
25
|
+
auth0Domain: string;
|
|
26
|
+
auth0ClientId: string;
|
|
27
|
+
redirectUri: string;
|
|
28
|
+
/**
|
|
29
|
+
* Defaults match sybilion-client AuthProvider (Management API audience + metadata scope).
|
|
30
|
+
* Override if your Auth0 SPA app uses a Resource Server audience for `/v1/auth/login`.
|
|
31
|
+
*/
|
|
32
|
+
authorizationParams?: {
|
|
33
|
+
audience?: string;
|
|
34
|
+
scope?: string;
|
|
35
|
+
redirect_uri?: string;
|
|
36
|
+
};
|
|
37
|
+
sybilionTokenStorageKey?: string;
|
|
38
|
+
logoutReturnTo?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type SybilionAuthContextValue = {
|
|
42
|
+
apiBaseUrl: string;
|
|
43
|
+
isLoading: boolean;
|
|
44
|
+
/** Auth0 authenticated and Sybilion JWT present. */
|
|
45
|
+
isAuthenticated: boolean;
|
|
46
|
+
sybilionAccessToken: string | null;
|
|
47
|
+
error: string | null;
|
|
48
|
+
loginWithRedirect: (options?: RedirectLoginOptions) => Promise<void>;
|
|
49
|
+
logout: () => void;
|
|
50
|
+
getAuth0AccessToken: () => Promise<string | undefined>;
|
|
51
|
+
getSybilionAccessToken: () => Promise<string | null>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const SybilionAuthContext = createContext<SybilionAuthContextValue | null>(
|
|
55
|
+
null,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export function useSybilionAuth(): SybilionAuthContextValue {
|
|
59
|
+
const v = useContext(SybilionAuthContext);
|
|
60
|
+
if (!v) {
|
|
61
|
+
throw new Error('useSybilionAuth must be used within SybilionAuthProvider');
|
|
62
|
+
}
|
|
63
|
+
return v;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** fetch() relative to Sybilion API base with Bearer Sybilion JWT. */
|
|
67
|
+
export async function sybilionApiFetch(
|
|
68
|
+
apiBaseUrl: string,
|
|
69
|
+
bearerToken: string,
|
|
70
|
+
path: string,
|
|
71
|
+
init: RequestInit = {},
|
|
72
|
+
): Promise<Response> {
|
|
73
|
+
const base = normalizeApiBaseUrl(apiBaseUrl);
|
|
74
|
+
const url = path.startsWith('http')
|
|
75
|
+
? path
|
|
76
|
+
: `${base}${path.startsWith('/') ? path : `/${path}`}`;
|
|
77
|
+
const headers = new Headers(init.headers);
|
|
78
|
+
headers.set('Authorization', `Bearer ${bearerToken}`);
|
|
79
|
+
if (!headers.has('Accept')) headers.set('Accept', 'application/json');
|
|
80
|
+
return fetch(url, { ...init, headers });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createSybilionApiFetch(
|
|
84
|
+
apiBaseUrl: string,
|
|
85
|
+
getSybilionAccessToken: () => Promise<string | null>,
|
|
86
|
+
) {
|
|
87
|
+
return async function sybilionFetch(
|
|
88
|
+
path: string,
|
|
89
|
+
init?: RequestInit,
|
|
90
|
+
): Promise<Response> {
|
|
91
|
+
const token = await getSybilionAccessToken();
|
|
92
|
+
if (!token)
|
|
93
|
+
throw new Error('Sybilion API: missing token — user not signed in.');
|
|
94
|
+
return sybilionApiFetch(apiBaseUrl, token, path, init);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Authenticated fetch using {@link useSybilionAuth} context. */
|
|
99
|
+
export function useSybilionApiFetch(): (
|
|
100
|
+
path: string,
|
|
101
|
+
init?: RequestInit,
|
|
102
|
+
) => Promise<Response> {
|
|
103
|
+
const { apiBaseUrl, getSybilionAccessToken } = useSybilionAuth();
|
|
104
|
+
return useMemo(
|
|
105
|
+
() => createSybilionApiFetch(apiBaseUrl, getSybilionAccessToken),
|
|
106
|
+
[apiBaseUrl, getSybilionAccessToken],
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeLs(key: string, value: string | null): void {
|
|
111
|
+
if (typeof localStorage === 'undefined') return;
|
|
112
|
+
try {
|
|
113
|
+
if (value === null) localStorage.removeItem(key);
|
|
114
|
+
else localStorage.setItem(key, value);
|
|
115
|
+
} catch {
|
|
116
|
+
/* quota / blocked */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function InnerSybilionSession({
|
|
121
|
+
children,
|
|
122
|
+
apiBaseUrl,
|
|
123
|
+
storageKey,
|
|
124
|
+
logoutReturnTo,
|
|
125
|
+
}: {
|
|
126
|
+
children: ReactNode;
|
|
127
|
+
apiBaseUrl: string;
|
|
128
|
+
storageKey: string;
|
|
129
|
+
logoutReturnTo?: string;
|
|
130
|
+
}): JSX.Element {
|
|
131
|
+
const auth0 = useAuth0();
|
|
132
|
+
const auth0Ref = useRef(auth0);
|
|
133
|
+
auth0Ref.current = auth0;
|
|
134
|
+
|
|
135
|
+
const [sybilionToken, setSybilionToken] = useState<string | null>(null);
|
|
136
|
+
const [exchangeLoading, setExchangeLoading] = useState(false);
|
|
137
|
+
const [exchangeError, setExchangeError] = useState<string | null>(null);
|
|
138
|
+
|
|
139
|
+
const persistToken = useCallback(
|
|
140
|
+
(t: string | null) => {
|
|
141
|
+
setSybilionToken(t);
|
|
142
|
+
writeLs(storageKey, t);
|
|
143
|
+
},
|
|
144
|
+
[storageKey],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const doLogout = useCallback(() => {
|
|
148
|
+
persistToken(null);
|
|
149
|
+
setExchangeError(null);
|
|
150
|
+
const returnTo =
|
|
151
|
+
logoutReturnTo ??
|
|
152
|
+
(typeof window !== 'undefined' ? window.location.origin : undefined);
|
|
153
|
+
auth0Ref.current.logout({
|
|
154
|
+
logoutParams: returnTo ? { returnTo } : undefined,
|
|
155
|
+
});
|
|
156
|
+
}, [persistToken, logoutReturnTo]);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!auth0.isAuthenticated || !auth0.user?.sub) {
|
|
160
|
+
persistToken(null);
|
|
161
|
+
}
|
|
162
|
+
}, [auth0.isAuthenticated, auth0.user?.sub, persistToken]);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!auth0.isAuthenticated || !auth0.user?.sub) {
|
|
166
|
+
setExchangeLoading(false);
|
|
167
|
+
setExchangeError(null);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let cancelled = false;
|
|
172
|
+
const runExchange = async () => {
|
|
173
|
+
setExchangeLoading(true);
|
|
174
|
+
setExchangeError(null);
|
|
175
|
+
try {
|
|
176
|
+
const access = await auth0Ref.current.getAccessTokenSilently();
|
|
177
|
+
const jwt = await exchangeAuth0AccessTokenForSybilionJwt(
|
|
178
|
+
apiBaseUrl,
|
|
179
|
+
access,
|
|
180
|
+
);
|
|
181
|
+
if (cancelled) return;
|
|
182
|
+
persistToken(jwt);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (cancelled) return;
|
|
185
|
+
persistToken(null);
|
|
186
|
+
const msg =
|
|
187
|
+
e instanceof Error ? e.message : 'Sybilion auth exchange failed';
|
|
188
|
+
setExchangeError(msg);
|
|
189
|
+
doLogout();
|
|
190
|
+
} finally {
|
|
191
|
+
if (!cancelled) setExchangeLoading(false);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
runExchange();
|
|
196
|
+
|
|
197
|
+
return () => {
|
|
198
|
+
cancelled = true;
|
|
199
|
+
};
|
|
200
|
+
}, [
|
|
201
|
+
apiBaseUrl,
|
|
202
|
+
auth0.isAuthenticated,
|
|
203
|
+
auth0.user?.sub,
|
|
204
|
+
persistToken,
|
|
205
|
+
doLogout,
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const getAuth0AccessToken = useCallback(async () => {
|
|
209
|
+
if (!auth0.isAuthenticated) return undefined;
|
|
210
|
+
try {
|
|
211
|
+
return await auth0.getAccessTokenSilently();
|
|
212
|
+
} catch {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
}, [auth0]);
|
|
216
|
+
|
|
217
|
+
const getSybilionAccessToken = useCallback(async (): Promise<
|
|
218
|
+
string | null
|
|
219
|
+
> => {
|
|
220
|
+
return sybilionToken;
|
|
221
|
+
}, [sybilionToken]);
|
|
222
|
+
|
|
223
|
+
const logout = useCallback(() => {
|
|
224
|
+
doLogout();
|
|
225
|
+
}, [doLogout]);
|
|
226
|
+
|
|
227
|
+
const isAuthenticated = Boolean(
|
|
228
|
+
auth0.isAuthenticated && sybilionToken && !exchangeError,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const isLoading =
|
|
232
|
+
auth0.isLoading ||
|
|
233
|
+
exchangeLoading ||
|
|
234
|
+
(Boolean(auth0.isAuthenticated && auth0.user) &&
|
|
235
|
+
sybilionToken === null &&
|
|
236
|
+
exchangeError === null);
|
|
237
|
+
|
|
238
|
+
const value = useMemo(
|
|
239
|
+
(): SybilionAuthContextValue => ({
|
|
240
|
+
apiBaseUrl,
|
|
241
|
+
isLoading,
|
|
242
|
+
isAuthenticated,
|
|
243
|
+
sybilionAccessToken: sybilionToken,
|
|
244
|
+
error: exchangeError ?? auth0.error?.message ?? null,
|
|
245
|
+
loginWithRedirect: auth0.loginWithRedirect,
|
|
246
|
+
logout,
|
|
247
|
+
getAuth0AccessToken,
|
|
248
|
+
getSybilionAccessToken,
|
|
249
|
+
}),
|
|
250
|
+
[
|
|
251
|
+
apiBaseUrl,
|
|
252
|
+
isLoading,
|
|
253
|
+
isAuthenticated,
|
|
254
|
+
sybilionToken,
|
|
255
|
+
exchangeError,
|
|
256
|
+
auth0.error?.message,
|
|
257
|
+
auth0.loginWithRedirect,
|
|
258
|
+
logout,
|
|
259
|
+
getAuth0AccessToken,
|
|
260
|
+
getSybilionAccessToken,
|
|
261
|
+
],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<SybilionAuthContext.Provider value={value}>
|
|
266
|
+
{children}
|
|
267
|
+
</SybilionAuthContext.Provider>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function SybilionAuthProvider({
|
|
272
|
+
children,
|
|
273
|
+
apiBaseUrl,
|
|
274
|
+
auth0Domain,
|
|
275
|
+
auth0ClientId,
|
|
276
|
+
redirectUri,
|
|
277
|
+
authorizationParams,
|
|
278
|
+
sybilionTokenStorageKey = DEFAULT_TOKEN_KEY,
|
|
279
|
+
logoutReturnTo,
|
|
280
|
+
}: SybilionAuthProviderProps): JSX.Element {
|
|
281
|
+
const mergedAuthParams = useMemo(
|
|
282
|
+
() => ({
|
|
283
|
+
redirect_uri: authorizationParams?.redirect_uri ?? redirectUri,
|
|
284
|
+
audience:
|
|
285
|
+
authorizationParams?.audience ?? `https://${auth0Domain}/api/v2/`,
|
|
286
|
+
scope:
|
|
287
|
+
authorizationParams?.scope ??
|
|
288
|
+
'openid profile email offline_access update:current_user_metadata',
|
|
289
|
+
}),
|
|
290
|
+
[
|
|
291
|
+
authorizationParams?.audience,
|
|
292
|
+
authorizationParams?.scope,
|
|
293
|
+
authorizationParams?.redirect_uri,
|
|
294
|
+
redirectUri,
|
|
295
|
+
auth0Domain,
|
|
296
|
+
],
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const cookieOpts =
|
|
300
|
+
typeof window !== 'undefined'
|
|
301
|
+
? { cookieDomain: window.location.hostname }
|
|
302
|
+
: {};
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<Auth0Provider
|
|
306
|
+
domain={auth0Domain}
|
|
307
|
+
clientId={auth0ClientId}
|
|
308
|
+
authorizationParams={mergedAuthParams}
|
|
309
|
+
cacheLocation="localstorage"
|
|
310
|
+
useRefreshTokens
|
|
311
|
+
{...cookieOpts}
|
|
312
|
+
>
|
|
313
|
+
<InnerSybilionSession
|
|
314
|
+
apiBaseUrl={apiBaseUrl}
|
|
315
|
+
storageKey={sybilionTokenStorageKey}
|
|
316
|
+
logoutReturnTo={logoutReturnTo}
|
|
317
|
+
>
|
|
318
|
+
{children}
|
|
319
|
+
</InnerSybilionSession>
|
|
320
|
+
</Auth0Provider>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SYBILION_AUTH_LOGIN_PATH,
|
|
3
|
+
normalizeApiBaseUrl,
|
|
4
|
+
} from '#uilib/sybilion-auth/authPaths';
|
|
5
|
+
|
|
6
|
+
/** POST `{ identity: auth0AccessToken, type: 'auth0' }` → Sybilion API JWT string. */
|
|
7
|
+
export async function exchangeAuth0AccessTokenForSybilionJwt(
|
|
8
|
+
apiBaseUrl: string,
|
|
9
|
+
auth0AccessToken: string,
|
|
10
|
+
): Promise<string> {
|
|
11
|
+
const base = normalizeApiBaseUrl(apiBaseUrl);
|
|
12
|
+
const res = await fetch(`${base}${SYBILION_AUTH_LOGIN_PATH}`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: {
|
|
15
|
+
Accept: 'application/json',
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({ identity: auth0AccessToken, type: 'auth0' }),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const json: unknown = await res.json().catch(() => ({}));
|
|
22
|
+
const pickToken = (): string | undefined => {
|
|
23
|
+
if (!json || typeof json !== 'object') return undefined;
|
|
24
|
+
const o = json as Record<string, unknown>;
|
|
25
|
+
const nested = o.data;
|
|
26
|
+
if (nested && typeof nested === 'object' && 'token' in nested) {
|
|
27
|
+
const t = (nested as { token?: unknown }).token;
|
|
28
|
+
return typeof t === 'string' ? t : undefined;
|
|
29
|
+
}
|
|
30
|
+
const t = o.token;
|
|
31
|
+
return typeof t === 'string' ? t : undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const token = pickToken();
|
|
35
|
+
if (!res.ok || token === undefined) {
|
|
36
|
+
const msg =
|
|
37
|
+
json &&
|
|
38
|
+
typeof json === 'object' &&
|
|
39
|
+
'message' in json &&
|
|
40
|
+
typeof (json as { message?: unknown }).message === 'string'
|
|
41
|
+
? (json as { message: string }).message
|
|
42
|
+
: `Sybilion auth failed (${res.status})`;
|
|
43
|
+
throw new Error(msg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return token;
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SybilionAuthProviderProps,
|
|
3
|
+
SybilionAuthContextValue,
|
|
4
|
+
} from '#uilib/sybilion-auth/SybilionAuthProvider';
|
|
5
|
+
export {
|
|
6
|
+
SybilionAuthProvider,
|
|
7
|
+
useSybilionAuth,
|
|
8
|
+
sybilionApiFetch,
|
|
9
|
+
createSybilionApiFetch,
|
|
10
|
+
useSybilionApiFetch,
|
|
11
|
+
} from '#uilib/sybilion-auth/SybilionAuthProvider';
|
|
12
|
+
export {
|
|
13
|
+
SYBILION_AUTH_LOGIN_PATH,
|
|
14
|
+
normalizeApiBaseUrl,
|
|
15
|
+
} from '#uilib/sybilion-auth/authPaths';
|
|
16
|
+
export { exchangeAuth0AccessTokenForSybilionJwt } from '#uilib/sybilion-auth/exchangeSybilionToken';
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Subset of Sybilion `Dataset` / API types — use `unknown` where payloads are large or evolving.
|
|
2
|
+
* Loose JSON shapes aligned with Sybilion dataset payloads (standalone apps / widgets).
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
export type
|
|
5
|
+
export type SybilionDatasetSnapshot = {
|
|
7
6
|
id: number;
|
|
8
7
|
user_id?: number;
|
|
9
8
|
name: string;
|
|
@@ -24,16 +23,14 @@ export type MiniAppDataset = {
|
|
|
24
23
|
};
|
|
25
24
|
|
|
26
25
|
/** `forecastData` slice: analysis id (string) → forecast series blob. */
|
|
27
|
-
export type
|
|
26
|
+
export type SybilionForecastMap = Record<string, unknown>;
|
|
28
27
|
|
|
29
|
-
export type
|
|
30
|
-
/** Cached Performance-tab API payload, or null if user never opened Performance / cache empty. */
|
|
28
|
+
export type SybilionPerformanceSnapshot = {
|
|
31
29
|
table: unknown;
|
|
32
|
-
/** In-memory spaghetti lines from shell context, or null. */
|
|
33
30
|
spaghetti: unknown;
|
|
34
31
|
};
|
|
35
32
|
|
|
36
|
-
export type
|
|
33
|
+
export type SybilionDriversComparisonSnapshot = {
|
|
37
34
|
byRegion: unknown;
|
|
38
35
|
byCountry: unknown;
|
|
39
36
|
byCategory: unknown;
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import cn from 'classnames';
|
|
3
|
-
import { createContext, useContext, useState, useRef, useMemo, useCallback, useEffect } from 'react';
|
|
4
|
-
import { Theme, Scroll } from '@homecode/ui';
|
|
5
|
-
import S from './MiniAppRoot.styl.js';
|
|
6
|
-
import { resolveParentOriginFromReferrer, applyThemeToDocument, buildReadyMessage, isTrustedMiniAppParentMessage, parseThemeSyncMessage } from './miniAppProtocol.js';
|
|
7
|
-
import { getDefaultMiniAppThemeConfig } from './miniAppThemeConfig.js';
|
|
8
|
-
|
|
9
|
-
const defaultTheme = {
|
|
10
|
-
mode: 'light',
|
|
11
|
-
isDarkMode: false,
|
|
12
|
-
};
|
|
13
|
-
function isEmbeddedMiniApp() {
|
|
14
|
-
return typeof window !== 'undefined' && window.parent !== window;
|
|
15
|
-
}
|
|
16
|
-
function themeFromDocument() {
|
|
17
|
-
if (typeof document === 'undefined')
|
|
18
|
-
return defaultTheme;
|
|
19
|
-
const isDark = document.documentElement.classList.contains('dark');
|
|
20
|
-
return {
|
|
21
|
-
mode: isDark ? 'dark' : 'light',
|
|
22
|
-
isDarkMode: isDark,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const MiniAppShellContext = createContext(null);
|
|
26
|
-
function useMiniAppShellTheme() {
|
|
27
|
-
const v = useContext(MiniAppShellContext);
|
|
28
|
-
if (!v) {
|
|
29
|
-
throw new Error('useMiniAppShellTheme must be used within MiniAppRoot');
|
|
30
|
-
}
|
|
31
|
-
return v;
|
|
32
|
-
}
|
|
33
|
-
function MiniAppRoot({ children, className, appId, onThemeChange, getThemeConfig, }) {
|
|
34
|
-
const [theme, setTheme] = useState(() => isEmbeddedMiniApp() ? defaultTheme : themeFromDocument());
|
|
35
|
-
const onThemeChangeRef = useRef(onThemeChange);
|
|
36
|
-
onThemeChangeRef.current = onThemeChange;
|
|
37
|
-
const getThemeConfigRef = useRef(getThemeConfig ?? getDefaultMiniAppThemeConfig);
|
|
38
|
-
getThemeConfigRef.current = getThemeConfig ?? getDefaultMiniAppThemeConfig;
|
|
39
|
-
const currThemeConfig = useMemo(() => getThemeConfigRef.current(theme.isDarkMode), [theme.isDarkMode]);
|
|
40
|
-
const sendReady = useCallback(() => {
|
|
41
|
-
if (!window.parent || window.parent === window)
|
|
42
|
-
return;
|
|
43
|
-
const payload = appId ? { appId } : {};
|
|
44
|
-
const msg = buildReadyMessage(payload);
|
|
45
|
-
const target = resolveParentOriginFromReferrer();
|
|
46
|
-
if (target) {
|
|
47
|
-
window.parent.postMessage(msg, target);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
window.parent.postMessage(msg, '*');
|
|
51
|
-
}
|
|
52
|
-
}, [appId]);
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (!isEmbeddedMiniApp())
|
|
55
|
-
return;
|
|
56
|
-
applyThemeToDocument(theme.mode);
|
|
57
|
-
}, [theme.mode]);
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
const onMessage = (event) => {
|
|
60
|
-
if (!isTrustedMiniAppParentMessage(event))
|
|
61
|
-
return;
|
|
62
|
-
const parsed = parseThemeSyncMessage(event.data);
|
|
63
|
-
if (!parsed)
|
|
64
|
-
return;
|
|
65
|
-
setTheme(parsed);
|
|
66
|
-
onThemeChangeRef.current?.(parsed);
|
|
67
|
-
};
|
|
68
|
-
window.addEventListener('message', onMessage);
|
|
69
|
-
return () => window.removeEventListener('message', onMessage);
|
|
70
|
-
}, []);
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
sendReady();
|
|
73
|
-
if (document.readyState === 'complete')
|
|
74
|
-
return;
|
|
75
|
-
window.addEventListener('load', sendReady);
|
|
76
|
-
return () => window.removeEventListener('load', sendReady);
|
|
77
|
-
}, [sendReady]);
|
|
78
|
-
const ctx = useMemo(() => ({ theme }), [theme]);
|
|
79
|
-
return (jsxs(MiniAppShellContext.Provider, { value: ctx, children: [jsx(Theme, { config: currThemeConfig }), jsx(Scroll, { y: true, fadeSize: "l", className: cn(S.root, className), innerClassName: S.inner, offset: { y: { before: 50, after: 50 } }, autoHide: true, children: children })] }));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export { MiniAppRoot, useMiniAppShellTheme };
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import styleInject from 'style-inject';
|
|
2
|
-
|
|
3
|
-
var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.MiniAppRoot_root__UVklz{height:100vh;min-height:0;overflow:hidden;position:relative;width:100%}@media (max-width:768px){.MiniAppRoot_root__UVklz{flex:1;height:auto;min-height:0}}.MiniAppRoot_inner__1ZFfl{box-sizing:border-box;max-height:100%;padding:var(--page-y-padding) var(--page-x-padding);width:100%}";
|
|
4
|
-
var S = {"root":"MiniAppRoot_root__UVklz","inner":"MiniAppRoot_inner__1ZFfl"};
|
|
5
|
-
styleInject(css_248z);
|
|
6
|
-
|
|
7
|
-
export { S as default };
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { resolveParentOriginFromReferrer, buildChatSendMessage, isTrustedMiniAppParentMessage, parseChatSendResultMessage } from './miniAppProtocol.js';
|
|
2
|
-
|
|
3
|
-
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
4
|
-
/**
|
|
5
|
-
* Ask the Sybilion host to send a chat message with its auth token.
|
|
6
|
-
* Does not update host ChatSheet state — same session as `chatId` on the agent only.
|
|
7
|
-
*/
|
|
8
|
-
async function sendChatMessage(chatId, message) {
|
|
9
|
-
if (typeof window === 'undefined' || window.parent === window) {
|
|
10
|
-
throw new Error('sendChatMessage requires an embedded mini-app (iframe)');
|
|
11
|
-
}
|
|
12
|
-
const requestId = crypto.randomUUID();
|
|
13
|
-
const target = resolveParentOriginFromReferrer();
|
|
14
|
-
const payload = { requestId, chatId, message };
|
|
15
|
-
const msg = buildChatSendMessage(payload);
|
|
16
|
-
return new Promise((resolve, reject) => {
|
|
17
|
-
const onMessage = (event) => {
|
|
18
|
-
if (!isTrustedMiniAppParentMessage(event))
|
|
19
|
-
return;
|
|
20
|
-
const parsed = parseChatSendResultMessage(event.data);
|
|
21
|
-
if (!parsed || parsed.requestId !== requestId)
|
|
22
|
-
return;
|
|
23
|
-
window.removeEventListener('message', onMessage);
|
|
24
|
-
clearTimeout(timer);
|
|
25
|
-
if (parsed.ok === true) {
|
|
26
|
-
resolve(parsed.result);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
reject(new Error(parsed.error));
|
|
30
|
-
};
|
|
31
|
-
window.addEventListener('message', onMessage);
|
|
32
|
-
const timer = setTimeout(() => {
|
|
33
|
-
window.removeEventListener('message', onMessage);
|
|
34
|
-
reject(new Error('Mini-app chat request timed out'));
|
|
35
|
-
}, DEFAULT_TIMEOUT_MS);
|
|
36
|
-
if (target) {
|
|
37
|
-
window.parent.postMessage(msg, target);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
window.parent.postMessage(msg, '*');
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export { sendChatMessage };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { buildDataRequestMessage, resolveParentOriginFromReferrer, isTrustedMiniAppParentMessage, parseDataResponseMessage } from './miniAppProtocol.js';
|
|
2
|
-
|
|
3
|
-
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
4
|
-
/** One listener per browsing context; tracks in-flight bridge requests by `requestId`. */
|
|
5
|
-
function createMiniAppDataClient(options) {
|
|
6
|
-
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
7
|
-
const pending = new Map();
|
|
8
|
-
let listening = false;
|
|
9
|
-
const settle = (rid, fn) => {
|
|
10
|
-
const p = pending.get(rid);
|
|
11
|
-
if (!p)
|
|
12
|
-
return;
|
|
13
|
-
clearTimeout(p.timer);
|
|
14
|
-
pending.delete(rid);
|
|
15
|
-
fn(p);
|
|
16
|
-
};
|
|
17
|
-
function ensureListener() {
|
|
18
|
-
if (listening || typeof window === 'undefined')
|
|
19
|
-
return;
|
|
20
|
-
listening = true;
|
|
21
|
-
window.addEventListener('message', (event) => {
|
|
22
|
-
if (!isTrustedMiniAppParentMessage(event))
|
|
23
|
-
return;
|
|
24
|
-
const msg = parseDataResponseMessage(event.data);
|
|
25
|
-
if (!msg)
|
|
26
|
-
return;
|
|
27
|
-
settle(msg.requestId, p => {
|
|
28
|
-
if (msg.ok) {
|
|
29
|
-
p.resolve(msg.result);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
p.reject(new Error(typeof msg.error === 'string'
|
|
33
|
-
? msg.error
|
|
34
|
-
: 'Mini-app data request failed'));
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function request(payload) {
|
|
40
|
-
if (typeof window === 'undefined' || window.parent === window) {
|
|
41
|
-
throw new Error('MiniAppDataClient works only inside an iframe embed');
|
|
42
|
-
}
|
|
43
|
-
ensureListener();
|
|
44
|
-
const requestId = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
45
|
-
? crypto.randomUUID()
|
|
46
|
-
: `miniapp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
47
|
-
const envelope = buildDataRequestMessage({
|
|
48
|
-
...payload,
|
|
49
|
-
requestId,
|
|
50
|
-
});
|
|
51
|
-
const target = resolveParentOriginFromReferrer();
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
const timer = setTimeout(() => {
|
|
54
|
-
settle(requestId, p => {
|
|
55
|
-
p.reject(new Error('Mini-app data request timed out'));
|
|
56
|
-
});
|
|
57
|
-
}, timeoutMs);
|
|
58
|
-
pending.set(requestId, {
|
|
59
|
-
resolve,
|
|
60
|
-
reject,
|
|
61
|
-
timer,
|
|
62
|
-
});
|
|
63
|
-
try {
|
|
64
|
-
if (target) {
|
|
65
|
-
window.parent.postMessage(envelope, target);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
window.parent.postMessage(envelope, '*');
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch (e) {
|
|
72
|
-
settle(requestId, p => {
|
|
73
|
-
p.reject(e instanceof Error ? e : new Error('Mini-app postMessage failed'));
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
async function rq(op, params) {
|
|
79
|
-
return request({ op, params });
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
getDatasets: () => rq('getDatasets'),
|
|
83
|
-
getDataset: id => rq('getDataset', { id }),
|
|
84
|
-
getForecasts: datasetId => rq('getForecasts', { datasetId }),
|
|
85
|
-
getForecast: (datasetId, analysisId) => rq('getForecast', { datasetId, analysisId }),
|
|
86
|
-
getDrivers: (datasetId, analysisId) => rq('getDrivers', { datasetId, analysisId }),
|
|
87
|
-
getPerformanceData: (datasetId, analysisId) => rq('getPerformanceData', {
|
|
88
|
-
datasetId,
|
|
89
|
-
analysisId,
|
|
90
|
-
}),
|
|
91
|
-
getDriversComparisonData: (datasetId, analysisId) => rq('getDriversComparisonData', {
|
|
92
|
-
datasetId,
|
|
93
|
-
analysisId,
|
|
94
|
-
}),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export { createMiniAppDataClient };
|