@every-app/sdk 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/EmbeddedAppProvider.d.ts +30 -0
- package/dist/client/EmbeddedAppProvider.d.ts.map +1 -0
- package/dist/client/EmbeddedAppProvider.js +56 -0
- package/dist/client/GatewayRequiredError.d.ts +20 -0
- package/dist/client/GatewayRequiredError.d.ts.map +1 -0
- package/dist/client/GatewayRequiredError.js +97 -0
- package/dist/client/_internal/useEveryAppRouter.d.ts +7 -0
- package/dist/client/_internal/useEveryAppRouter.d.ts.map +1 -0
- package/dist/client/_internal/useEveryAppRouter.js +57 -0
- package/dist/client/_internal/useEveryAppSession.d.ts +13 -0
- package/dist/client/_internal/useEveryAppSession.d.ts.map +1 -0
- package/dist/client/_internal/useEveryAppSession.js +36 -0
- package/dist/client/authenticatedFetch.d.ts +9 -0
- package/dist/client/authenticatedFetch.d.ts.map +1 -0
- package/dist/client/authenticatedFetch.js +27 -0
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +6 -0
- package/dist/client/lazyInitForWorkers.d.ts +22 -0
- package/dist/client/lazyInitForWorkers.d.ts.map +1 -0
- package/dist/client/lazyInitForWorkers.js +65 -0
- package/dist/client/session-manager.d.ts +33 -0
- package/dist/client/session-manager.d.ts.map +1 -0
- package/dist/client/session-manager.js +158 -0
- package/dist/client/useSessionTokenClientMiddleware.d.ts +2 -0
- package/dist/client/useSessionTokenClientMiddleware.d.ts.map +1 -0
- package/dist/client/useSessionTokenClientMiddleware.js +21 -0
- package/dist/server/auth-config.d.ts +3 -0
- package/dist/server/auth-config.d.ts.map +1 -0
- package/dist/server/auth-config.js +7 -0
- package/dist/server/authenticateRequest.d.ts +21 -0
- package/dist/server/authenticateRequest.d.ts.map +1 -0
- package/dist/server/authenticateRequest.js +67 -0
- package/dist/server/getLocalD1Url.d.ts +2 -0
- package/dist/server/getLocalD1Url.d.ts.map +1 -0
- package/dist/server/getLocalD1Url.js +40 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/types.d.ts +5 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +1 -0
- package/package.json +18 -6
- package/src/client/EmbeddedAppProvider.tsx +11 -0
- package/src/client/GatewayRequiredError.tsx +161 -0
- package/src/client/_internal/useEveryAppSession.tsx +3 -0
- package/src/client/index.ts +2 -1
- package/src/client/session-manager.ts +16 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { SessionManagerConfig } from "./session-manager";
|
|
3
|
+
interface EmbeddedProviderConfig extends SessionManagerConfig {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
export declare function EmbeddedAppProvider({ children, ...config }: EmbeddedProviderConfig): import("react/jsx-runtime").JSX.Element | null;
|
|
7
|
+
/**
|
|
8
|
+
* Hook to get the current authenticated user.
|
|
9
|
+
* Returns the user's ID and email extracted from the JWT token,
|
|
10
|
+
* or null if not authenticated.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* function MyComponent() {
|
|
15
|
+
* const user = useCurrentUser();
|
|
16
|
+
*
|
|
17
|
+
* if (!user) {
|
|
18
|
+
* return <div>Not authenticated</div>;
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* return <div>Welcome, {user.email}</div>;
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function useCurrentUser(): {
|
|
26
|
+
userId: string;
|
|
27
|
+
email: string;
|
|
28
|
+
} | null;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=EmbeddedAppProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmbeddedAppProvider.d.ts","sourceRoot":"","sources":["../../src/client/EmbeddedAppProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6C,MAAM,OAAO,CAAC;AAClE,OAAO,EAAkB,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAKzE,UAAU,sBAAuB,SAAQ,oBAAoB;IAC3D,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAUD,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,GAAG,MAAM,EACV,EAAE,sBAAsB,kDA6BxB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,IAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAmBzE"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { useEveryAppSession } from "./_internal/useEveryAppSession";
|
|
4
|
+
import { useEveryAppRouter } from "./_internal/useEveryAppRouter";
|
|
5
|
+
import { GatewayRequiredError } from "./GatewayRequiredError";
|
|
6
|
+
const EmbeddedAppContext = createContext(null);
|
|
7
|
+
export function EmbeddedAppProvider({ children, ...config }) {
|
|
8
|
+
const { sessionManager, sessionTokenState } = useEveryAppSession({
|
|
9
|
+
sessionManagerConfig: config,
|
|
10
|
+
});
|
|
11
|
+
useEveryAppRouter({ sessionManager });
|
|
12
|
+
if (!sessionManager)
|
|
13
|
+
return null;
|
|
14
|
+
// Check if the app is running outside of the Gateway iframe
|
|
15
|
+
if (!sessionManager.isInIframe) {
|
|
16
|
+
return (_jsx(GatewayRequiredError, { gatewayOrigin: sessionManager.parentOrigin, appId: config.appId }));
|
|
17
|
+
}
|
|
18
|
+
const value = {
|
|
19
|
+
sessionManager,
|
|
20
|
+
isAuthenticated: sessionTokenState.status === "VALID",
|
|
21
|
+
sessionTokenState,
|
|
22
|
+
};
|
|
23
|
+
return (_jsx(EmbeddedAppContext.Provider, { value: value, children: children }));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hook to get the current authenticated user.
|
|
27
|
+
* Returns the user's ID and email extracted from the JWT token,
|
|
28
|
+
* or null if not authenticated.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* function MyComponent() {
|
|
33
|
+
* const user = useCurrentUser();
|
|
34
|
+
*
|
|
35
|
+
* if (!user) {
|
|
36
|
+
* return <div>Not authenticated</div>;
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* return <div>Welcome, {user.email}</div>;
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function useCurrentUser() {
|
|
44
|
+
const context = useContext(EmbeddedAppContext);
|
|
45
|
+
if (!context) {
|
|
46
|
+
throw new Error("useCurrentUser must be used within an EmbeddedAppProvider");
|
|
47
|
+
}
|
|
48
|
+
const { sessionManager, sessionTokenState } = context;
|
|
49
|
+
return useMemo(() => {
|
|
50
|
+
// Only return user if we have a valid token
|
|
51
|
+
if (sessionTokenState.status !== "VALID") {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return sessionManager.getUser();
|
|
55
|
+
}, [sessionManager, sessionTokenState]);
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface GatewayRequiredErrorProps {
|
|
2
|
+
/**
|
|
3
|
+
* The origin of the Gateway (e.g., "https://gateway.example.com").
|
|
4
|
+
*/
|
|
5
|
+
gatewayOrigin: string;
|
|
6
|
+
/**
|
|
7
|
+
* The app ID used in the Gateway URL path.
|
|
8
|
+
*/
|
|
9
|
+
appId: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Error component displayed when an embedded app is accessed directly
|
|
13
|
+
* instead of through the Every App Gateway.
|
|
14
|
+
*
|
|
15
|
+
* This component informs users that authentication requires accessing
|
|
16
|
+
* the app through the Gateway and provides a link to do so.
|
|
17
|
+
*/
|
|
18
|
+
export declare function GatewayRequiredError({ gatewayOrigin, appId, }: GatewayRequiredErrorProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=GatewayRequiredError.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GatewayRequiredError.d.ts","sourceRoot":"","sources":["../../src/client/GatewayRequiredError.tsx"],"names":[],"mappings":"AAEA,UAAU,yBAAyB;IACjC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAwFD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,aAAa,EACb,KAAK,GACN,EAAE,yBAAyB,2CAmD3B"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Button gradient styles
|
|
3
|
+
const BUTTON_GRADIENT = "linear-gradient(180deg, #424242 0%, #353535 50%, #2a2a2a 100%)";
|
|
4
|
+
const BUTTON_GRADIENT_HOVER = "linear-gradient(180deg, #4d4d4d 0%, #404040 50%, #353535 100%)";
|
|
5
|
+
// CSS custom properties for theming
|
|
6
|
+
const CSS_VARIABLES = `
|
|
7
|
+
@media (prefers-color-scheme: light) {
|
|
8
|
+
:root {
|
|
9
|
+
--gateway-bg: oklch(100% 0 0);
|
|
10
|
+
--gateway-text: oklch(0% 0 0);
|
|
11
|
+
--gateway-text-muted: oklch(40% 0 0);
|
|
12
|
+
--gateway-icon-bg: oklch(94% 0 0);
|
|
13
|
+
--gateway-icon-stroke: oklch(55% 0.22 25);
|
|
14
|
+
--gateway-border: oklch(94% 0 0);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
@media (prefers-color-scheme: dark) {
|
|
18
|
+
:root {
|
|
19
|
+
--gateway-bg: #0a0f0d;
|
|
20
|
+
--gateway-text: oklch(92% 0 0);
|
|
21
|
+
--gateway-text-muted: oklch(60% 0 0);
|
|
22
|
+
--gateway-icon-bg: oklch(22% 0 0);
|
|
23
|
+
--gateway-icon-stroke: oklch(65% 0.2 25);
|
|
24
|
+
--gateway-border: oklch(30% 0 0);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
const styles = {
|
|
29
|
+
container: {
|
|
30
|
+
display: "flex",
|
|
31
|
+
flexDirection: "column",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "center",
|
|
34
|
+
minHeight: "100vh",
|
|
35
|
+
padding: "24px",
|
|
36
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
37
|
+
backgroundColor: "var(--gateway-bg, oklch(100% 0 0))",
|
|
38
|
+
color: "var(--gateway-text, oklch(0% 0 0))",
|
|
39
|
+
colorScheme: "light dark",
|
|
40
|
+
},
|
|
41
|
+
content: {
|
|
42
|
+
maxWidth: "380px",
|
|
43
|
+
width: "100%",
|
|
44
|
+
textAlign: "left",
|
|
45
|
+
},
|
|
46
|
+
iconContainer: {
|
|
47
|
+
width: "44px",
|
|
48
|
+
height: "44px",
|
|
49
|
+
marginBottom: "16px",
|
|
50
|
+
borderRadius: "0.25rem",
|
|
51
|
+
backgroundColor: "var(--gateway-icon-bg, oklch(94% 0 0))",
|
|
52
|
+
display: "flex",
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
border: "1px solid var(--gateway-border, oklch(94% 0 0))",
|
|
56
|
+
},
|
|
57
|
+
title: {
|
|
58
|
+
fontSize: "18px",
|
|
59
|
+
fontWeight: 600,
|
|
60
|
+
marginBottom: "8px",
|
|
61
|
+
color: "var(--gateway-text, oklch(0% 0 0))",
|
|
62
|
+
letterSpacing: "-0.01em",
|
|
63
|
+
},
|
|
64
|
+
description: {
|
|
65
|
+
fontSize: "14px",
|
|
66
|
+
lineHeight: 1.5,
|
|
67
|
+
color: "var(--gateway-text-muted, oklch(40% 0 0))",
|
|
68
|
+
marginBottom: "20px",
|
|
69
|
+
},
|
|
70
|
+
button: {
|
|
71
|
+
display: "inline-block",
|
|
72
|
+
padding: "10px 20px",
|
|
73
|
+
fontSize: "14px",
|
|
74
|
+
fontWeight: 500,
|
|
75
|
+
color: "#ffffff",
|
|
76
|
+
background: BUTTON_GRADIENT,
|
|
77
|
+
borderRadius: "0.25rem",
|
|
78
|
+
textDecoration: "none",
|
|
79
|
+
border: "none",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Error component displayed when an embedded app is accessed directly
|
|
84
|
+
* instead of through the Every App Gateway.
|
|
85
|
+
*
|
|
86
|
+
* This component informs users that authentication requires accessing
|
|
87
|
+
* the app through the Gateway and provides a link to do so.
|
|
88
|
+
*/
|
|
89
|
+
export function GatewayRequiredError({ gatewayOrigin, appId, }) {
|
|
90
|
+
const displayName = appId || "This app";
|
|
91
|
+
const gatewayUrl = `${gatewayOrigin}/apps/${appId}${window.location.pathname}`;
|
|
92
|
+
return (_jsxs("div", { style: styles.container, children: [_jsx("style", { children: CSS_VARIABLES }), _jsxs("div", { style: styles.content, children: [_jsx("div", { style: styles.iconContainer, children: _jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "var(--gateway-icon-stroke, oklch(55% 0.22 25))", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }), _jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }), _jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })] }) }), _jsx("h1", { style: styles.title, children: "Gateway Required" }), _jsxs("p", { style: styles.description, children: [displayName, " needs to be accessed through the Gateway for authentication to work properly."] }), _jsx("a", { href: gatewayUrl, style: styles.button, onMouseOver: (e) => {
|
|
93
|
+
e.currentTarget.style.background = BUTTON_GRADIENT_HOVER;
|
|
94
|
+
}, onMouseOut: (e) => {
|
|
95
|
+
e.currentTarget.style.background = BUTTON_GRADIENT;
|
|
96
|
+
}, children: "Open in Gateway" })] })] }));
|
|
97
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SessionManager } from "../session-manager";
|
|
2
|
+
interface UseEveryAppRouterParams {
|
|
3
|
+
sessionManager: SessionManager | null;
|
|
4
|
+
}
|
|
5
|
+
export declare function useEveryAppRouter({ sessionManager }: UseEveryAppRouterParams): void;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=useEveryAppRouter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEveryAppRouter.d.ts","sourceRoot":"","sources":["../../../src/client/_internal/useEveryAppRouter.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGpD,UAAU,uBAAuB;IAC/B,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;CACvC;AACD,wBAAgB,iBAAiB,CAAC,EAAE,cAAc,EAAE,EAAE,uBAAuB,QAmE5E"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useRouter } from "@tanstack/react-router";
|
|
3
|
+
export function useEveryAppRouter({ sessionManager }) {
|
|
4
|
+
const router = useRouter();
|
|
5
|
+
// Route synchronization effect
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!sessionManager)
|
|
8
|
+
return;
|
|
9
|
+
// Listen for route sync messages from parent
|
|
10
|
+
const handleMessage = (event) => {
|
|
11
|
+
if (event.origin !== sessionManager.parentOrigin)
|
|
12
|
+
return;
|
|
13
|
+
if (event.data.type === "ROUTE_CHANGE" &&
|
|
14
|
+
event.data.direction === "parent-to-child") {
|
|
15
|
+
const targetRoute = event.data.route;
|
|
16
|
+
const currentRoute = window.location.pathname;
|
|
17
|
+
// Only navigate if the route is different from current location
|
|
18
|
+
if (targetRoute && targetRoute !== currentRoute) {
|
|
19
|
+
router.navigate({ to: targetRoute });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
window.addEventListener("message", handleMessage);
|
|
24
|
+
// Simplified route change detection with 2 reliable methods
|
|
25
|
+
let lastReportedPath = window.location.pathname;
|
|
26
|
+
const handleRouteChange = () => {
|
|
27
|
+
const currentPath = window.location.pathname;
|
|
28
|
+
// Only report if the path actually changed
|
|
29
|
+
if (currentPath === lastReportedPath) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
lastReportedPath = currentPath;
|
|
33
|
+
if (window.parent !== window) {
|
|
34
|
+
window.parent.postMessage({
|
|
35
|
+
type: "ROUTE_CHANGE",
|
|
36
|
+
route: currentPath,
|
|
37
|
+
appId: sessionManager.appId,
|
|
38
|
+
direction: "child-to-parent",
|
|
39
|
+
}, sessionManager.parentOrigin);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
// Listen to popstate for browser back/forward
|
|
43
|
+
window.addEventListener("popstate", handleRouteChange);
|
|
44
|
+
// Polling to detect route changes (catches router navigation)
|
|
45
|
+
const pollInterval = setInterval(() => {
|
|
46
|
+
const currentPath = window.location.pathname;
|
|
47
|
+
if (currentPath !== lastReportedPath) {
|
|
48
|
+
handleRouteChange();
|
|
49
|
+
}
|
|
50
|
+
}, 100);
|
|
51
|
+
return () => {
|
|
52
|
+
window.removeEventListener("message", handleMessage);
|
|
53
|
+
window.removeEventListener("popstate", handleRouteChange);
|
|
54
|
+
clearInterval(pollInterval);
|
|
55
|
+
};
|
|
56
|
+
}, [sessionManager]);
|
|
57
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SessionManager, SessionManagerConfig } from "../session-manager";
|
|
2
|
+
interface UseEveryAppSessionParams {
|
|
3
|
+
sessionManagerConfig: SessionManagerConfig;
|
|
4
|
+
}
|
|
5
|
+
export declare function useEveryAppSession({ sessionManagerConfig, }: UseEveryAppSessionParams): {
|
|
6
|
+
sessionManager: SessionManager | null;
|
|
7
|
+
sessionTokenState: {
|
|
8
|
+
status: "NO_TOKEN" | "VALID" | "EXPIRED" | "REFRESHING";
|
|
9
|
+
token: string | null;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=useEveryAppSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEveryAppSession.d.ts","sourceRoot":"","sources":["../../../src/client/_internal/useEveryAppSession.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1E,UAAU,wBAAwB;IAChC,oBAAoB,EAAE,oBAAoB,CAAC;CAC5C;AAED,wBAAgB,kBAAkB,CAAC,EACjC,oBAAoB,GACrB,EAAE,wBAAwB;;;;;;EAyC1B"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { SessionManager } from "../session-manager";
|
|
3
|
+
export function useEveryAppSession({ sessionManagerConfig, }) {
|
|
4
|
+
const sessionManagerRef = useRef(null);
|
|
5
|
+
const [sessionTokenState, setSessionTokenState] = useState({
|
|
6
|
+
status: "NO_TOKEN",
|
|
7
|
+
token: null,
|
|
8
|
+
});
|
|
9
|
+
if (!sessionManagerRef.current && typeof document !== "undefined") {
|
|
10
|
+
sessionManagerRef.current = new SessionManager(sessionManagerConfig);
|
|
11
|
+
}
|
|
12
|
+
const sessionManager = sessionManagerRef.current;
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!sessionManager)
|
|
15
|
+
return;
|
|
16
|
+
// Skip token requests when not in iframe - the app will show GatewayRequiredError instead
|
|
17
|
+
if (!sessionManager.isInIframe)
|
|
18
|
+
return;
|
|
19
|
+
const interval = setInterval(() => {
|
|
20
|
+
setSessionTokenState(sessionManager.getTokenState());
|
|
21
|
+
}, 5000);
|
|
22
|
+
sessionManager.getToken().catch((err) => {
|
|
23
|
+
console.error("[EmbeddedProvider] Initial token request failed:", err);
|
|
24
|
+
});
|
|
25
|
+
return () => {
|
|
26
|
+
clearInterval(interval);
|
|
27
|
+
};
|
|
28
|
+
}, [sessionManager]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!sessionManager)
|
|
31
|
+
return;
|
|
32
|
+
// Make sessionManager globally accessible for middleware
|
|
33
|
+
window.__embeddedSessionManager = sessionManager;
|
|
34
|
+
}, [sessionManager]);
|
|
35
|
+
return { sessionManager, sessionTokenState };
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets the current session token from the embedded session manager
|
|
3
|
+
*/
|
|
4
|
+
export declare function getSessionToken(): Promise<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Performs a fetch request with the authorization header automatically added
|
|
7
|
+
*/
|
|
8
|
+
export declare function authenticatedFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
9
|
+
//# sourceMappingURL=authenticatedFetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticatedFetch.d.ts","sourceRoot":"","sources":["../../src/client/authenticatedFetch.ts"],"names":[],"mappings":"AAQA;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAevD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,WAAW,GAAG,GAAG,EACxB,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,QAAQ,CAAC,CAUnB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets the current session token from the embedded session manager
|
|
3
|
+
*/
|
|
4
|
+
export async function getSessionToken() {
|
|
5
|
+
const windowWithSession = window;
|
|
6
|
+
const sessionManager = windowWithSession.__embeddedSessionManager;
|
|
7
|
+
if (!sessionManager) {
|
|
8
|
+
throw new Error("Session manager not available");
|
|
9
|
+
}
|
|
10
|
+
const token = await sessionManager.getToken();
|
|
11
|
+
if (!token) {
|
|
12
|
+
throw new Error("No token available");
|
|
13
|
+
}
|
|
14
|
+
return token;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Performs a fetch request with the authorization header automatically added
|
|
18
|
+
*/
|
|
19
|
+
export async function authenticatedFetch(input, init) {
|
|
20
|
+
const token = await getSessionToken();
|
|
21
|
+
const headers = new Headers(init?.headers);
|
|
22
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
23
|
+
return fetch(input, {
|
|
24
|
+
...init,
|
|
25
|
+
headers,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { SessionManager, isRunningInIframe } from "./session-manager";
|
|
2
|
+
export type { SessionManagerConfig } from "./session-manager";
|
|
3
|
+
export { useSessionTokenClientMiddleware } from "./useSessionTokenClientMiddleware";
|
|
4
|
+
export { EmbeddedAppProvider, useCurrentUser } from "./EmbeddedAppProvider";
|
|
5
|
+
export { GatewayRequiredError } from "./GatewayRequiredError";
|
|
6
|
+
export { lazyInitForWorkers } from "./lazyInitForWorkers";
|
|
7
|
+
export { authenticatedFetch, getSessionToken } from "./authenticatedFetch";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtE,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AAEpF,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { SessionManager, isRunningInIframe } from "./session-manager";
|
|
2
|
+
export { useSessionTokenClientMiddleware } from "./useSessionTokenClientMiddleware";
|
|
3
|
+
export { EmbeddedAppProvider, useCurrentUser } from "./EmbeddedAppProvider";
|
|
4
|
+
export { GatewayRequiredError } from "./GatewayRequiredError";
|
|
5
|
+
export { lazyInitForWorkers } from "./lazyInitForWorkers";
|
|
6
|
+
export { authenticatedFetch, getSessionToken } from "./authenticatedFetch";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a factory function in a Proxy to defer initialization until first access.
|
|
3
|
+
* This prevents async operations (Like creating Tanstack DB Collections) from running in Cloudflare Workers' global scope.
|
|
4
|
+
*
|
|
5
|
+
* @param factory - A function that creates and returns the resource.
|
|
6
|
+
* Must be a callback to defer execution; passing the value directly
|
|
7
|
+
* would evaluate it at module load time, triggering the Cloudflare error.
|
|
8
|
+
* @returns A Proxy that lazily initializes the resource on first property access
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* export const myCollection = lazyInitForWorkers(() =>
|
|
13
|
+
* createCollection(queryCollectionOptions({
|
|
14
|
+
* queryKey: ["myData"],
|
|
15
|
+
* queryFn: async () => fetchData(),
|
|
16
|
+
* // ... other options
|
|
17
|
+
* }))
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function lazyInitForWorkers<T extends object>(factory: () => T): T;
|
|
22
|
+
//# sourceMappingURL=lazyInitForWorkers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lazyInitForWorkers.d.ts","sourceRoot":"","sources":["../../src/client/lazyInitForWorkers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CA8CxE"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a factory function in a Proxy to defer initialization until first access.
|
|
3
|
+
* This prevents async operations (Like creating Tanstack DB Collections) from running in Cloudflare Workers' global scope.
|
|
4
|
+
*
|
|
5
|
+
* @param factory - A function that creates and returns the resource.
|
|
6
|
+
* Must be a callback to defer execution; passing the value directly
|
|
7
|
+
* would evaluate it at module load time, triggering the Cloudflare error.
|
|
8
|
+
* @returns A Proxy that lazily initializes the resource on first property access
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* export const myCollection = lazyInitForWorkers(() =>
|
|
13
|
+
* createCollection(queryCollectionOptions({
|
|
14
|
+
* queryKey: ["myData"],
|
|
15
|
+
* queryFn: async () => fetchData(),
|
|
16
|
+
* // ... other options
|
|
17
|
+
* }))
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function lazyInitForWorkers(factory) {
|
|
22
|
+
// Closure: This variable is captured by getInstance() and the Proxy traps below.
|
|
23
|
+
// It remains in memory as long as the returned Proxy is referenced, enabling singleton behavior.
|
|
24
|
+
let instance = null;
|
|
25
|
+
function getInstance() {
|
|
26
|
+
if (!instance) {
|
|
27
|
+
instance = factory();
|
|
28
|
+
}
|
|
29
|
+
return instance;
|
|
30
|
+
}
|
|
31
|
+
return new Proxy({}, {
|
|
32
|
+
get(_, prop) {
|
|
33
|
+
const inst = getInstance();
|
|
34
|
+
const value = inst[prop];
|
|
35
|
+
// Bind methods to the instance to preserve `this` context
|
|
36
|
+
return typeof value === "function" ? value.bind(inst) : value;
|
|
37
|
+
},
|
|
38
|
+
set(_, prop, value) {
|
|
39
|
+
const inst = getInstance();
|
|
40
|
+
inst[prop] = value;
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
deleteProperty(_, prop) {
|
|
44
|
+
const inst = getInstance();
|
|
45
|
+
delete inst[prop];
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
has(_, prop) {
|
|
49
|
+
const inst = getInstance();
|
|
50
|
+
return prop in inst;
|
|
51
|
+
},
|
|
52
|
+
ownKeys(_) {
|
|
53
|
+
const inst = getInstance();
|
|
54
|
+
return Reflect.ownKeys(inst);
|
|
55
|
+
},
|
|
56
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
57
|
+
const inst = getInstance();
|
|
58
|
+
return Reflect.getOwnPropertyDescriptor(inst, prop);
|
|
59
|
+
},
|
|
60
|
+
getPrototypeOf(_) {
|
|
61
|
+
const inst = getInstance();
|
|
62
|
+
return Reflect.getPrototypeOf(inst);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface SessionManagerConfig {
|
|
2
|
+
appId: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Detects whether the current window is running inside an iframe.
|
|
6
|
+
* Returns true if in an iframe, false if running as top-level window.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isRunningInIframe(): boolean;
|
|
9
|
+
export declare class SessionManager {
|
|
10
|
+
readonly parentOrigin: string;
|
|
11
|
+
readonly appId: string;
|
|
12
|
+
readonly isInIframe: boolean;
|
|
13
|
+
private token;
|
|
14
|
+
private refreshPromise;
|
|
15
|
+
constructor(config: SessionManagerConfig);
|
|
16
|
+
private isTokenExpiringSoon;
|
|
17
|
+
private postMessageWithResponse;
|
|
18
|
+
requestNewToken(): Promise<string>;
|
|
19
|
+
getToken(): Promise<string>;
|
|
20
|
+
getTokenState(): {
|
|
21
|
+
status: "NO_TOKEN" | "VALID" | "EXPIRED" | "REFRESHING";
|
|
22
|
+
token: string | null;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Extracts user information from the current JWT token.
|
|
26
|
+
* Returns null if no valid token is available.
|
|
27
|
+
*/
|
|
28
|
+
getUser(): {
|
|
29
|
+
userId: string;
|
|
30
|
+
email: string;
|
|
31
|
+
} | null;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=session-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/client/session-manager.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAMD;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAQ3C;AAED,qBAAa,cAAc;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAE7B,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,cAAc,CAAgC;gBAE1C,MAAM,EAAE,oBAAoB;IAqBxC,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,uBAAuB;IAuCzB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IA8ClC,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAOjC,aAAa,IAAI;QACf,MAAM,EAAE,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,YAAY,CAAC;QACxD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;KACtB;IAgBD;;;OAGG;IACH,OAAO,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAwBpD"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const MESSAGE_TIMEOUT_MS = 5000;
|
|
2
|
+
const TOKEN_EXPIRY_BUFFER_MS = 10000;
|
|
3
|
+
const DEFAULT_TOKEN_LIFETIME_MS = 60000;
|
|
4
|
+
/**
|
|
5
|
+
* Detects whether the current window is running inside an iframe.
|
|
6
|
+
* Returns true if in an iframe, false if running as top-level window.
|
|
7
|
+
*/
|
|
8
|
+
export function isRunningInIframe() {
|
|
9
|
+
try {
|
|
10
|
+
return window.self !== window.top;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// If we can't access window.top due to cross-origin restrictions,
|
|
14
|
+
// we're definitely in an iframe
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class SessionManager {
|
|
19
|
+
parentOrigin;
|
|
20
|
+
appId;
|
|
21
|
+
isInIframe;
|
|
22
|
+
token = null;
|
|
23
|
+
refreshPromise = null;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
if (!config.appId) {
|
|
26
|
+
throw new Error("[SessionManager] appId is required.");
|
|
27
|
+
}
|
|
28
|
+
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL;
|
|
29
|
+
if (!gatewayUrl) {
|
|
30
|
+
throw new Error("[SessionManager] VITE_GATEWAY_URL env var is required.");
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
new URL(gatewayUrl);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`[SessionManager] Invalid gateway URL: ${gatewayUrl}`);
|
|
37
|
+
}
|
|
38
|
+
this.appId = config.appId;
|
|
39
|
+
this.parentOrigin = gatewayUrl;
|
|
40
|
+
this.isInIframe = isRunningInIframe();
|
|
41
|
+
}
|
|
42
|
+
isTokenExpiringSoon(bufferMs = TOKEN_EXPIRY_BUFFER_MS) {
|
|
43
|
+
if (!this.token)
|
|
44
|
+
return true;
|
|
45
|
+
return Date.now() >= this.token.expiresAt - bufferMs;
|
|
46
|
+
}
|
|
47
|
+
postMessageWithResponse(request, responseType, requestId) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
window.removeEventListener("message", handler);
|
|
52
|
+
};
|
|
53
|
+
const handler = (event) => {
|
|
54
|
+
// Security: reject messages from wrong origin (including null from sandboxed iframes)
|
|
55
|
+
if (event.origin !== this.parentOrigin)
|
|
56
|
+
return;
|
|
57
|
+
// Safety: ignore malformed messages that could crash the handler
|
|
58
|
+
if (!event.data || typeof event.data !== "object")
|
|
59
|
+
return;
|
|
60
|
+
if (event.data.type === responseType &&
|
|
61
|
+
event.data.requestId === requestId) {
|
|
62
|
+
cleanup();
|
|
63
|
+
if (event.data.error) {
|
|
64
|
+
reject(new Error(event.data.error));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
resolve(event.data);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const timeout = setTimeout(() => {
|
|
72
|
+
cleanup();
|
|
73
|
+
reject(new Error("Token request timeout - parent did not respond"));
|
|
74
|
+
}, MESSAGE_TIMEOUT_MS);
|
|
75
|
+
window.addEventListener("message", handler);
|
|
76
|
+
window.parent.postMessage(request, this.parentOrigin);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async requestNewToken() {
|
|
80
|
+
if (this.refreshPromise) {
|
|
81
|
+
return this.refreshPromise;
|
|
82
|
+
}
|
|
83
|
+
this.refreshPromise = (async () => {
|
|
84
|
+
const requestId = crypto.randomUUID();
|
|
85
|
+
const response = await this.postMessageWithResponse({
|
|
86
|
+
type: "SESSION_TOKEN_REQUEST",
|
|
87
|
+
requestId,
|
|
88
|
+
appId: this.appId,
|
|
89
|
+
}, "SESSION_TOKEN_RESPONSE", requestId);
|
|
90
|
+
if (!response.token) {
|
|
91
|
+
throw new Error("No token in response");
|
|
92
|
+
}
|
|
93
|
+
// Parse expiresAt, falling back to default lifetime if invalid
|
|
94
|
+
let expiresAt = Date.now() + DEFAULT_TOKEN_LIFETIME_MS;
|
|
95
|
+
if (response.expiresAt) {
|
|
96
|
+
const parsed = new Date(response.expiresAt).getTime();
|
|
97
|
+
if (!Number.isNaN(parsed)) {
|
|
98
|
+
expiresAt = parsed;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.token = {
|
|
102
|
+
token: response.token,
|
|
103
|
+
expiresAt,
|
|
104
|
+
};
|
|
105
|
+
return this.token.token;
|
|
106
|
+
})();
|
|
107
|
+
try {
|
|
108
|
+
return await this.refreshPromise;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
this.refreshPromise = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async getToken() {
|
|
115
|
+
if (this.isTokenExpiringSoon()) {
|
|
116
|
+
return this.requestNewToken();
|
|
117
|
+
}
|
|
118
|
+
return this.token.token;
|
|
119
|
+
}
|
|
120
|
+
getTokenState() {
|
|
121
|
+
if (this.refreshPromise) {
|
|
122
|
+
return { status: "REFRESHING", token: null };
|
|
123
|
+
}
|
|
124
|
+
if (!this.token) {
|
|
125
|
+
return { status: "NO_TOKEN", token: null };
|
|
126
|
+
}
|
|
127
|
+
if (this.isTokenExpiringSoon(0)) {
|
|
128
|
+
return { status: "EXPIRED", token: this.token.token };
|
|
129
|
+
}
|
|
130
|
+
return { status: "VALID", token: this.token.token };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Extracts user information from the current JWT token.
|
|
134
|
+
* Returns null if no valid token is available.
|
|
135
|
+
*/
|
|
136
|
+
getUser() {
|
|
137
|
+
if (!this.token) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const parts = this.token.token.split(".");
|
|
142
|
+
if (parts.length !== 3) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
146
|
+
if (!payload.sub) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
userId: payload.sub,
|
|
151
|
+
email: payload.email ?? "",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSessionTokenClientMiddleware.d.ts","sourceRoot":"","sources":["../../src/client/useSessionTokenClientMiddleware.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,+BAA+B,6GA2B1C,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
2
|
+
export const useSessionTokenClientMiddleware = createMiddleware({
|
|
3
|
+
type: "function",
|
|
4
|
+
}).client(async ({ next }) => {
|
|
5
|
+
// Get the global sessionManager - this MUST be available for embedded apps
|
|
6
|
+
const sessionManager = window
|
|
7
|
+
.__embeddedSessionManager;
|
|
8
|
+
if (!sessionManager) {
|
|
9
|
+
throw new Error("[AuthMiddleware] SessionManager not available - embedded provider not initialized");
|
|
10
|
+
}
|
|
11
|
+
// INVARIANT: This is just an extra check and should never be the case if the sessionManager exists.
|
|
12
|
+
if (typeof sessionManager.getToken !== "function") {
|
|
13
|
+
throw new Error("[AuthMiddleware] SessionManager.getToken is not a function");
|
|
14
|
+
}
|
|
15
|
+
const token = await sessionManager.getToken();
|
|
16
|
+
return next({
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${token}`,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-config.d.ts","sourceRoot":"","sources":["../../src/server/auth-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAG1C,wBAAgB,aAAa,IAAI,UAAU,CAK1C"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AuthConfig } from "./types";
|
|
2
|
+
interface SessionTokenPayload {
|
|
3
|
+
sub: string;
|
|
4
|
+
iss: string;
|
|
5
|
+
aud: string;
|
|
6
|
+
exp: number;
|
|
7
|
+
iat: number;
|
|
8
|
+
appId?: string;
|
|
9
|
+
permissions?: string[];
|
|
10
|
+
email?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function authenticateRequest(authConfig: AuthConfig, providedRequest?: Request): Promise<SessionTokenPayload | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Extracts the bearer token from an Authorization header.
|
|
15
|
+
*
|
|
16
|
+
* @param authHeader - The Authorization header value (e.g., "Bearer eyJ...")
|
|
17
|
+
* @returns The token string if valid, null otherwise
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractBearerToken(authHeader: string | null): string | null;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=authenticateRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticateRequest.d.ts","sourceRoot":"","sources":["../../src/server/authenticateRequest.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAG1C,UAAU,mBAAmB;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,UAAU,EACtB,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CA+BrC;AAyCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK3E"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getRequest } from "@tanstack/react-start/server";
|
|
2
|
+
import { createLocalJWKSet, jwtVerify, } from "jose";
|
|
3
|
+
import { env } from "cloudflare:workers";
|
|
4
|
+
export async function authenticateRequest(authConfig, providedRequest) {
|
|
5
|
+
const request = providedRequest || getRequest();
|
|
6
|
+
const authHeader = request.headers.get("authorization");
|
|
7
|
+
if (!authHeader) {
|
|
8
|
+
console.log("No auth header found");
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const token = extractBearerToken(authHeader);
|
|
12
|
+
if (!token) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const session = await verifySessionToken(token, authConfig);
|
|
17
|
+
return session;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error(JSON.stringify({
|
|
21
|
+
message: "Error verifying session token",
|
|
22
|
+
error: error instanceof Error ? error.message : String(error),
|
|
23
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
24
|
+
errorType: error instanceof Error ? error.constructor.name : "Unknown",
|
|
25
|
+
issuer: authConfig.issuer,
|
|
26
|
+
audience: authConfig.audience,
|
|
27
|
+
}));
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function verifySessionToken(token, config) {
|
|
32
|
+
const { issuer, audience } = config;
|
|
33
|
+
if (!issuer) {
|
|
34
|
+
throw new Error("Issuer must be provided for token verification");
|
|
35
|
+
}
|
|
36
|
+
if (!audience) {
|
|
37
|
+
throw new Error("Audience must be provided for token verification");
|
|
38
|
+
}
|
|
39
|
+
// Fetch JWKS - use service binding in production, direct fetch in development
|
|
40
|
+
const jwksResponse = import.meta.env.PROD && env.EVERY_APP_GATEWAY
|
|
41
|
+
? await env.EVERY_APP_GATEWAY.fetch("http://localhost/api/embedded/jwks")
|
|
42
|
+
: await fetch(`${env.GATEWAY_URL}/api/embedded/jwks`);
|
|
43
|
+
if (!jwksResponse.ok) {
|
|
44
|
+
throw new Error(`Failed to fetch JWKS: ${jwksResponse.status} ${jwksResponse.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
const jwks = (await jwksResponse.json());
|
|
47
|
+
const localJWKS = createLocalJWKSet(jwks);
|
|
48
|
+
const options = {
|
|
49
|
+
issuer,
|
|
50
|
+
audience,
|
|
51
|
+
algorithms: ["RS256"],
|
|
52
|
+
};
|
|
53
|
+
const { payload } = await jwtVerify(token, localJWKS, options);
|
|
54
|
+
return payload;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extracts the bearer token from an Authorization header.
|
|
58
|
+
*
|
|
59
|
+
* @param authHeader - The Authorization header value (e.g., "Bearer eyJ...")
|
|
60
|
+
* @returns The token string if valid, null otherwise
|
|
61
|
+
*/
|
|
62
|
+
export function extractBearerToken(authHeader) {
|
|
63
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return authHeader.substring(7);
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getLocalD1Url.d.ts","sourceRoot":"","sources":["../../src/server/getLocalD1Url.ts"],"names":[],"mappings":"AAWA,wBAAgB,aAAa,kBAoD5B"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { parse } from "jsonc-parser";
|
|
5
|
+
function findSqliteFile(basePath) {
|
|
6
|
+
return fs
|
|
7
|
+
.readdirSync(basePath, { encoding: "utf-8", recursive: true })
|
|
8
|
+
.find((f) => f.endsWith(".sqlite"));
|
|
9
|
+
}
|
|
10
|
+
export function getLocalD1Url() {
|
|
11
|
+
const basePath = path.resolve(".wrangler");
|
|
12
|
+
// Check if .wrangler directory exists
|
|
13
|
+
if (!fs.existsSync(basePath)) {
|
|
14
|
+
console.error("================================================================================");
|
|
15
|
+
console.error("WARNING: .wrangler directory not found");
|
|
16
|
+
console.error("This is expected in CI/non-development environments.");
|
|
17
|
+
console.error("The local D1 database is only available after running 'wrangler dev' which you can trigger by running 'npm run dev'.");
|
|
18
|
+
console.error("================================================================================");
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
let dbFile = findSqliteFile(basePath);
|
|
22
|
+
if (!dbFile) {
|
|
23
|
+
// Read wrangler.jsonc to get the database name
|
|
24
|
+
const wranglerConfigPath = path.resolve("wrangler.jsonc");
|
|
25
|
+
const wranglerConfig = parse(fs.readFileSync(wranglerConfigPath, "utf-8"));
|
|
26
|
+
const databaseName = wranglerConfig.d1_databases?.[0]?.database_name;
|
|
27
|
+
if (!databaseName) {
|
|
28
|
+
throw new Error("Could not find database_name in wrangler.jsonc d1_databases configuration");
|
|
29
|
+
}
|
|
30
|
+
// Execute the command to initialize the local database
|
|
31
|
+
console.log(`Initializing local D1 database: ${databaseName}...`);
|
|
32
|
+
execSync(`npx wrangler d1 execute ${databaseName} --local --command "SELECT 1;"`, { stdio: "pipe" });
|
|
33
|
+
// Try to find the db file again after initialization
|
|
34
|
+
dbFile = findSqliteFile(basePath);
|
|
35
|
+
if (!dbFile) {
|
|
36
|
+
throw new Error(`Failed to initialize local D1 database. The sqlite file was not created.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return path.resolve(basePath, dbFile);
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/server/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@every-app/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "./
|
|
5
|
+
"main": "./dist/client/index.js",
|
|
6
|
+
"types": "./dist/client/index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
|
-
"./client":
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
"./client": {
|
|
9
|
+
"types": "./dist/client/index.d.ts",
|
|
10
|
+
"default": "./dist/client/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./server": {
|
|
13
|
+
"types": "./dist/server/index.d.ts",
|
|
14
|
+
"default": "./dist/server/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./server/getLocalD1Url": {
|
|
17
|
+
"types": "./dist/server/getLocalD1Url.d.ts",
|
|
18
|
+
"default": "./dist/server/getLocalD1Url.js"
|
|
19
|
+
}
|
|
10
20
|
},
|
|
11
21
|
"files": [
|
|
22
|
+
"dist",
|
|
12
23
|
"src"
|
|
13
24
|
],
|
|
14
25
|
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.build.json",
|
|
15
27
|
"types:check": "tsc --noEmit",
|
|
16
28
|
"format:check": "prettier --check .",
|
|
17
29
|
"format:write": "prettier . --write",
|
|
18
|
-
"test": "vitest run",
|
|
30
|
+
"test": "vitest run --silent",
|
|
19
31
|
"test:watch": "vitest"
|
|
20
32
|
},
|
|
21
33
|
"dependencies": {
|
|
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useMemo } from "react";
|
|
|
2
2
|
import { SessionManager, SessionManagerConfig } from "./session-manager";
|
|
3
3
|
import { useEveryAppSession } from "./_internal/useEveryAppSession";
|
|
4
4
|
import { useEveryAppRouter } from "./_internal/useEveryAppRouter";
|
|
5
|
+
import { GatewayRequiredError } from "./GatewayRequiredError";
|
|
5
6
|
|
|
6
7
|
interface EmbeddedProviderConfig extends SessionManagerConfig {
|
|
7
8
|
children: React.ReactNode;
|
|
@@ -26,6 +27,16 @@ export function EmbeddedAppProvider({
|
|
|
26
27
|
|
|
27
28
|
if (!sessionManager) return null;
|
|
28
29
|
|
|
30
|
+
// Check if the app is running outside of the Gateway iframe
|
|
31
|
+
if (!sessionManager.isInIframe) {
|
|
32
|
+
return (
|
|
33
|
+
<GatewayRequiredError
|
|
34
|
+
gatewayOrigin={sessionManager.parentOrigin}
|
|
35
|
+
appId={config.appId}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
const value: EmbeddedAppContextValue = {
|
|
30
41
|
sessionManager,
|
|
31
42
|
isAuthenticated: sessionTokenState.status === "VALID",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
interface GatewayRequiredErrorProps {
|
|
4
|
+
/**
|
|
5
|
+
* The origin of the Gateway (e.g., "https://gateway.example.com").
|
|
6
|
+
*/
|
|
7
|
+
gatewayOrigin: string;
|
|
8
|
+
/**
|
|
9
|
+
* The app ID used in the Gateway URL path.
|
|
10
|
+
*/
|
|
11
|
+
appId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Button gradient styles
|
|
15
|
+
const BUTTON_GRADIENT =
|
|
16
|
+
"linear-gradient(180deg, #424242 0%, #353535 50%, #2a2a2a 100%)";
|
|
17
|
+
const BUTTON_GRADIENT_HOVER =
|
|
18
|
+
"linear-gradient(180deg, #4d4d4d 0%, #404040 50%, #353535 100%)";
|
|
19
|
+
|
|
20
|
+
// CSS custom properties for theming
|
|
21
|
+
const CSS_VARIABLES = `
|
|
22
|
+
@media (prefers-color-scheme: light) {
|
|
23
|
+
:root {
|
|
24
|
+
--gateway-bg: oklch(100% 0 0);
|
|
25
|
+
--gateway-text: oklch(0% 0 0);
|
|
26
|
+
--gateway-text-muted: oklch(40% 0 0);
|
|
27
|
+
--gateway-icon-bg: oklch(94% 0 0);
|
|
28
|
+
--gateway-icon-stroke: oklch(55% 0.22 25);
|
|
29
|
+
--gateway-border: oklch(94% 0 0);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
@media (prefers-color-scheme: dark) {
|
|
33
|
+
:root {
|
|
34
|
+
--gateway-bg: #0a0f0d;
|
|
35
|
+
--gateway-text: oklch(92% 0 0);
|
|
36
|
+
--gateway-text-muted: oklch(60% 0 0);
|
|
37
|
+
--gateway-icon-bg: oklch(22% 0 0);
|
|
38
|
+
--gateway-icon-stroke: oklch(65% 0.2 25);
|
|
39
|
+
--gateway-border: oklch(30% 0 0);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const styles = {
|
|
45
|
+
container: {
|
|
46
|
+
display: "flex",
|
|
47
|
+
flexDirection: "column",
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
justifyContent: "center",
|
|
50
|
+
minHeight: "100vh",
|
|
51
|
+
padding: "24px",
|
|
52
|
+
fontFamily:
|
|
53
|
+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
|
|
54
|
+
backgroundColor: "var(--gateway-bg, oklch(100% 0 0))",
|
|
55
|
+
color: "var(--gateway-text, oklch(0% 0 0))",
|
|
56
|
+
colorScheme: "light dark",
|
|
57
|
+
} satisfies CSSProperties,
|
|
58
|
+
content: {
|
|
59
|
+
maxWidth: "380px",
|
|
60
|
+
width: "100%",
|
|
61
|
+
textAlign: "left",
|
|
62
|
+
} satisfies CSSProperties,
|
|
63
|
+
iconContainer: {
|
|
64
|
+
width: "44px",
|
|
65
|
+
height: "44px",
|
|
66
|
+
marginBottom: "16px",
|
|
67
|
+
borderRadius: "0.25rem",
|
|
68
|
+
backgroundColor: "var(--gateway-icon-bg, oklch(94% 0 0))",
|
|
69
|
+
display: "flex",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
justifyContent: "center",
|
|
72
|
+
border: "1px solid var(--gateway-border, oklch(94% 0 0))",
|
|
73
|
+
} satisfies CSSProperties,
|
|
74
|
+
title: {
|
|
75
|
+
fontSize: "18px",
|
|
76
|
+
fontWeight: 600,
|
|
77
|
+
marginBottom: "8px",
|
|
78
|
+
color: "var(--gateway-text, oklch(0% 0 0))",
|
|
79
|
+
letterSpacing: "-0.01em",
|
|
80
|
+
} satisfies CSSProperties,
|
|
81
|
+
description: {
|
|
82
|
+
fontSize: "14px",
|
|
83
|
+
lineHeight: 1.5,
|
|
84
|
+
color: "var(--gateway-text-muted, oklch(40% 0 0))",
|
|
85
|
+
marginBottom: "20px",
|
|
86
|
+
} satisfies CSSProperties,
|
|
87
|
+
button: {
|
|
88
|
+
display: "inline-block",
|
|
89
|
+
padding: "10px 20px",
|
|
90
|
+
fontSize: "14px",
|
|
91
|
+
fontWeight: 500,
|
|
92
|
+
color: "#ffffff",
|
|
93
|
+
background: BUTTON_GRADIENT,
|
|
94
|
+
borderRadius: "0.25rem",
|
|
95
|
+
textDecoration: "none",
|
|
96
|
+
border: "none",
|
|
97
|
+
} satisfies CSSProperties,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Error component displayed when an embedded app is accessed directly
|
|
102
|
+
* instead of through the Every App Gateway.
|
|
103
|
+
*
|
|
104
|
+
* This component informs users that authentication requires accessing
|
|
105
|
+
* the app through the Gateway and provides a link to do so.
|
|
106
|
+
*/
|
|
107
|
+
export function GatewayRequiredError({
|
|
108
|
+
gatewayOrigin,
|
|
109
|
+
appId,
|
|
110
|
+
}: GatewayRequiredErrorProps) {
|
|
111
|
+
const displayName = appId || "This app";
|
|
112
|
+
const gatewayUrl = `${gatewayOrigin}/apps/${appId}${window.location.pathname}`;
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div style={styles.container}>
|
|
116
|
+
<style>{CSS_VARIABLES}</style>
|
|
117
|
+
<div style={styles.content}>
|
|
118
|
+
{/* Warning Icon */}
|
|
119
|
+
<div style={styles.iconContainer}>
|
|
120
|
+
<svg
|
|
121
|
+
width="22"
|
|
122
|
+
height="22"
|
|
123
|
+
viewBox="0 0 24 24"
|
|
124
|
+
fill="none"
|
|
125
|
+
stroke="var(--gateway-icon-stroke, oklch(55% 0.22 25))"
|
|
126
|
+
strokeWidth="2"
|
|
127
|
+
strokeLinecap="round"
|
|
128
|
+
strokeLinejoin="round"
|
|
129
|
+
>
|
|
130
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
131
|
+
<line x1="12" y1="9" x2="12" y2="13" />
|
|
132
|
+
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
133
|
+
</svg>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Title */}
|
|
137
|
+
<h1 style={styles.title}>Gateway Required</h1>
|
|
138
|
+
|
|
139
|
+
{/* Description */}
|
|
140
|
+
<p style={styles.description}>
|
|
141
|
+
{displayName} needs to be accessed through the Gateway for
|
|
142
|
+
authentication to work properly.
|
|
143
|
+
</p>
|
|
144
|
+
|
|
145
|
+
{/* Redirect Link/Button - Metallic style */}
|
|
146
|
+
<a
|
|
147
|
+
href={gatewayUrl}
|
|
148
|
+
style={styles.button}
|
|
149
|
+
onMouseOver={(e) => {
|
|
150
|
+
e.currentTarget.style.background = BUTTON_GRADIENT_HOVER;
|
|
151
|
+
}}
|
|
152
|
+
onMouseOut={(e) => {
|
|
153
|
+
e.currentTarget.style.background = BUTTON_GRADIENT;
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
Open in Gateway
|
|
157
|
+
</a>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -24,6 +24,9 @@ export function useEveryAppSession({
|
|
|
24
24
|
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
if (!sessionManager) return;
|
|
27
|
+
// Skip token requests when not in iframe - the app will show GatewayRequiredError instead
|
|
28
|
+
if (!sessionManager.isInIframe) return;
|
|
29
|
+
|
|
27
30
|
const interval = setInterval(() => {
|
|
28
31
|
setSessionTokenState(sessionManager.getTokenState());
|
|
29
32
|
}, 5000);
|
package/src/client/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { SessionManager } from "./session-manager";
|
|
1
|
+
export { SessionManager, isRunningInIframe } from "./session-manager";
|
|
2
2
|
export type { SessionManagerConfig } from "./session-manager";
|
|
3
3
|
export { useSessionTokenClientMiddleware } from "./useSessionTokenClientMiddleware";
|
|
4
4
|
|
|
5
5
|
export { EmbeddedAppProvider, useCurrentUser } from "./EmbeddedAppProvider";
|
|
6
|
+
export { GatewayRequiredError } from "./GatewayRequiredError";
|
|
6
7
|
|
|
7
8
|
export { lazyInitForWorkers } from "./lazyInitForWorkers";
|
|
8
9
|
|
|
@@ -17,9 +17,24 @@ const MESSAGE_TIMEOUT_MS = 5000;
|
|
|
17
17
|
const TOKEN_EXPIRY_BUFFER_MS = 10000;
|
|
18
18
|
const DEFAULT_TOKEN_LIFETIME_MS = 60000;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Detects whether the current window is running inside an iframe.
|
|
22
|
+
* Returns true if in an iframe, false if running as top-level window.
|
|
23
|
+
*/
|
|
24
|
+
export function isRunningInIframe(): boolean {
|
|
25
|
+
try {
|
|
26
|
+
return window.self !== window.top;
|
|
27
|
+
} catch {
|
|
28
|
+
// If we can't access window.top due to cross-origin restrictions,
|
|
29
|
+
// we're definitely in an iframe
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
export class SessionManager {
|
|
21
35
|
readonly parentOrigin: string;
|
|
22
36
|
readonly appId: string;
|
|
37
|
+
readonly isInIframe: boolean;
|
|
23
38
|
|
|
24
39
|
private token: SessionToken | null = null;
|
|
25
40
|
private refreshPromise: Promise<string> | null = null;
|
|
@@ -42,6 +57,7 @@ export class SessionManager {
|
|
|
42
57
|
|
|
43
58
|
this.appId = config.appId;
|
|
44
59
|
this.parentOrigin = gatewayUrl;
|
|
60
|
+
this.isInIframe = isRunningInIframe();
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
private isTokenExpiringSoon(
|