@every-app/sdk 0.1.10 → 0.1.12
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/core/authenticatedFetch.d.ts.map +1 -1
- package/dist/core/authenticatedFetch.js +4 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/sessionManager.d.ts +25 -0
- package/dist/core/sessionManager.d.ts.map +1 -1
- package/dist/core/sessionManager.js +232 -33
- package/dist/shared/bypassGatewayLocalOnly.d.ts +14 -0
- package/dist/shared/bypassGatewayLocalOnly.d.ts.map +1 -0
- package/dist/shared/bypassGatewayLocalOnly.js +41 -0
- package/dist/shared/parseMessagePayload.d.ts +3 -0
- package/dist/shared/parseMessagePayload.d.ts.map +1 -0
- package/dist/shared/parseMessagePayload.js +18 -0
- package/dist/tanstack/EmbeddedAppProvider.d.ts.map +1 -1
- package/dist/tanstack/EmbeddedAppProvider.js +3 -2
- package/dist/tanstack/_internal/useEveryAppSession.d.ts +2 -0
- package/dist/tanstack/_internal/useEveryAppSession.d.ts.map +1 -1
- package/dist/tanstack/_internal/useEveryAppSession.js +8 -2
- package/dist/tanstack/server/authConfig.d.ts.map +1 -1
- package/dist/tanstack/server/authConfig.js +7 -1
- package/dist/tanstack/server/authenticateRequest.d.ts.map +1 -1
- package/dist/tanstack/server/authenticateRequest.js +11 -0
- package/dist/tanstack/useEveryAppRouter.d.ts.map +1 -1
- package/dist/tanstack/useEveryAppRouter.js +20 -11
- package/dist/tanstack/useSessionTokenClientMiddleware.d.ts.map +1 -1
- package/dist/tanstack/useSessionTokenClientMiddleware.js +8 -0
- package/package.json +1 -1
- package/src/cloudflare/server/gateway.test.ts +41 -9
- package/src/core/authenticatedFetch.ts +9 -0
- package/src/core/index.ts +10 -2
- package/src/core/sessionManager.test.ts +143 -0
- package/src/core/sessionManager.ts +318 -35
- package/src/shared/bypassGatewayLocalOnly.ts +55 -0
- package/src/shared/parseMessagePayload.ts +22 -0
- package/src/tanstack/EmbeddedAppProvider.tsx +5 -2
- package/src/tanstack/_internal/useEveryAppSession.test.ts +40 -0
- package/src/tanstack/_internal/useEveryAppSession.tsx +16 -2
- package/src/tanstack/server/authConfig.ts +11 -1
- package/src/tanstack/server/authenticateRequest.test.ts +32 -0
- package/src/tanstack/server/authenticateRequest.ts +21 -0
- package/src/tanstack/useEveryAppRouter.tsx +21 -14
- package/src/tanstack/useSessionTokenClientMiddleware.ts +12 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
2
|
import { useRouter } from "@tanstack/react-router";
|
|
3
|
+
import { parseMessagePayload } from "../shared/parseMessagePayload.js";
|
|
3
4
|
export function useEveryAppRouter({ sessionManager }) {
|
|
4
5
|
const router = useRouter();
|
|
5
6
|
// Route synchronization effect
|
|
@@ -8,11 +9,15 @@ export function useEveryAppRouter({ sessionManager }) {
|
|
|
8
9
|
return;
|
|
9
10
|
// Listen for route sync messages from parent
|
|
10
11
|
const handleMessage = (event) => {
|
|
11
|
-
|
|
12
|
+
// Validate origin based on environment
|
|
13
|
+
if (!sessionManager.isTrustedHostMessage(event))
|
|
12
14
|
return;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const data = parseMessagePayload(event.data);
|
|
16
|
+
if (!data)
|
|
17
|
+
return;
|
|
18
|
+
if (data.type === "ROUTE_CHANGE" &&
|
|
19
|
+
data.direction === "parent-to-child") {
|
|
20
|
+
const targetRoute = typeof data.route === "string" ? data.route : null;
|
|
16
21
|
const currentRoute = window.location.pathname;
|
|
17
22
|
// Only navigate if the route is different from current location
|
|
18
23
|
if (targetRoute && targetRoute !== currentRoute) {
|
|
@@ -30,13 +35,17 @@ export function useEveryAppRouter({ sessionManager }) {
|
|
|
30
35
|
return;
|
|
31
36
|
}
|
|
32
37
|
lastReportedPath = currentPath;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const message = {
|
|
39
|
+
type: "ROUTE_CHANGE",
|
|
40
|
+
route: currentPath,
|
|
41
|
+
appId: sessionManager.appId,
|
|
42
|
+
direction: "child-to-parent",
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
sessionManager.postToHost(message);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return;
|
|
40
49
|
}
|
|
41
50
|
};
|
|
42
51
|
// Listen to popstate for browser back/forward
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSessionTokenClientMiddleware.d.ts","sourceRoot":"","sources":["../../src/tanstack/useSessionTokenClientMiddleware.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useSessionTokenClientMiddleware.d.ts","sourceRoot":"","sources":["../../src/tanstack/useSessionTokenClientMiddleware.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,+BAA+B,6GAmC1C,CAAC"}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { createMiddleware } from "@tanstack/react-start";
|
|
2
|
+
import { BYPASS_GATEWAY_LOCAL_ONLY_TOKEN, isBypassGatewayLocalOnlyClient, } from "../shared/bypassGatewayLocalOnly.js";
|
|
2
3
|
export const useSessionTokenClientMiddleware = createMiddleware({
|
|
3
4
|
type: "function",
|
|
4
5
|
}).client(async ({ next }) => {
|
|
6
|
+
if (isBypassGatewayLocalOnlyClient()) {
|
|
7
|
+
return next({
|
|
8
|
+
headers: {
|
|
9
|
+
Authorization: `Bearer ${BYPASS_GATEWAY_LOCAL_ONLY_TOKEN}`,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
5
13
|
// Get the global sessionManager - this MUST be available for embedded apps
|
|
6
14
|
const sessionManager = window
|
|
7
15
|
.__embeddedSessionManager;
|
package/package.json
CHANGED
|
@@ -56,11 +56,46 @@ describe("gateway server helpers", () => {
|
|
|
56
56
|
);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
it("fetches via service binding when available", async () => {
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
it("fetches via service binding when available in production", async () => {
|
|
60
|
+
// Service binding is only used when import.meta.env.PROD is true
|
|
61
|
+
const originalProd = import.meta.env.PROD;
|
|
62
|
+
import.meta.env.PROD = true;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const bindingFetch = vi
|
|
66
|
+
.fn()
|
|
67
|
+
.mockResolvedValue(new Response("ok", { status: 200 }));
|
|
68
|
+
|
|
69
|
+
const env: TestEnv = {
|
|
70
|
+
GATEWAY_URL: "https://gateway.example.com",
|
|
71
|
+
EVERY_APP_GATEWAY: { fetch: bindingFetch },
|
|
72
|
+
GATEWAY_APP_API_TOKEN: "eat_test_token",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await fetchGateway({
|
|
76
|
+
env,
|
|
77
|
+
url: "/api/ai/openai/v1/chat/completions",
|
|
78
|
+
init: { method: "POST" },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(bindingFetch).toHaveBeenCalledTimes(1);
|
|
82
|
+
const [requestArg] = bindingFetch.mock.calls[0];
|
|
83
|
+
const request = requestArg as Request;
|
|
84
|
+
expect(request.url).toBe(
|
|
85
|
+
"http://localhost/api/ai/openai/v1/chat/completions",
|
|
86
|
+
);
|
|
87
|
+
} finally {
|
|
88
|
+
import.meta.env.PROD = originalProd;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("skips service binding in development and uses HTTP fetch", async () => {
|
|
93
|
+
const fetchMock = vi
|
|
94
|
+
.spyOn(globalThis, "fetch")
|
|
62
95
|
.mockResolvedValue(new Response("ok", { status: 200 }));
|
|
63
96
|
|
|
97
|
+
const bindingFetch = vi.fn();
|
|
98
|
+
|
|
64
99
|
const env: TestEnv = {
|
|
65
100
|
GATEWAY_URL: "https://gateway.example.com",
|
|
66
101
|
EVERY_APP_GATEWAY: { fetch: bindingFetch },
|
|
@@ -73,12 +108,9 @@ describe("gateway server helpers", () => {
|
|
|
73
108
|
init: { method: "POST" },
|
|
74
109
|
});
|
|
75
110
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(request.url).toBe(
|
|
80
|
-
"http://localhost/api/ai/openai/v1/chat/completions",
|
|
81
|
-
);
|
|
111
|
+
// In dev, should use HTTP fetch, not service binding
|
|
112
|
+
expect(bindingFetch).not.toHaveBeenCalled();
|
|
113
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
82
114
|
});
|
|
83
115
|
|
|
84
116
|
it("always injects app token and strips Authorization header", async () => {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BYPASS_GATEWAY_LOCAL_ONLY_TOKEN,
|
|
3
|
+
isBypassGatewayLocalOnlyClient,
|
|
4
|
+
} from "../shared/bypassGatewayLocalOnly.js";
|
|
5
|
+
|
|
1
6
|
interface SessionManager {
|
|
2
7
|
getToken(): Promise<string>;
|
|
3
8
|
}
|
|
@@ -10,6 +15,10 @@ interface WindowWithSessionManager extends Window {
|
|
|
10
15
|
* Gets the current session token from the embedded session manager
|
|
11
16
|
*/
|
|
12
17
|
export async function getSessionToken(): Promise<string> {
|
|
18
|
+
if (isBypassGatewayLocalOnlyClient()) {
|
|
19
|
+
return BYPASS_GATEWAY_LOCAL_ONLY_TOKEN;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
const windowWithSession = window as WindowWithSessionManager;
|
|
14
23
|
const sessionManager = windowWithSession.__embeddedSessionManager;
|
|
15
24
|
|
package/src/core/index.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export {
|
|
2
|
+
SessionManager,
|
|
3
|
+
isRunningInIframe,
|
|
4
|
+
isRunningInReactNativeWebView,
|
|
5
|
+
detectEnvironment,
|
|
6
|
+
} from "./sessionManager.js";
|
|
7
|
+
export type {
|
|
8
|
+
SessionManagerConfig,
|
|
9
|
+
EmbeddedEnvironment,
|
|
10
|
+
} from "./sessionManager.js";
|
|
3
11
|
|
|
4
12
|
export { authenticatedFetch, getSessionToken } from "./authenticatedFetch.js";
|
|
@@ -47,6 +47,21 @@ describe("SessionManager", () => {
|
|
|
47
47
|
} as MessageEvent);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function createJwtLikeToken(payload: Record<string, unknown>): string {
|
|
51
|
+
return `header.${btoa(JSON.stringify(payload))}.signature`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createBase64UrlJwtLikeToken(
|
|
55
|
+
payload: Record<string, unknown>,
|
|
56
|
+
): string {
|
|
57
|
+
const encoded = btoa(JSON.stringify(payload))
|
|
58
|
+
.replace(/\+/g, "-")
|
|
59
|
+
.replace(/\//g, "_")
|
|
60
|
+
.replace(/=+$/g, "");
|
|
61
|
+
|
|
62
|
+
return `header.${encoded}.signature`;
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
beforeEach(async () => {
|
|
51
66
|
vi.resetModules();
|
|
52
67
|
vi.stubEnv("VITE_GATEWAY_URL", "https://gateway.example.com");
|
|
@@ -552,6 +567,31 @@ describe("SessionManager", () => {
|
|
|
552
567
|
email: "", // Defaults to empty string
|
|
553
568
|
});
|
|
554
569
|
});
|
|
570
|
+
|
|
571
|
+
it("extracts user info from base64url-encoded JWT payload", async () => {
|
|
572
|
+
const manager = new SessionManager({ appId: "test-app" });
|
|
573
|
+
|
|
574
|
+
const fakeToken = createBase64UrlJwtLikeToken({
|
|
575
|
+
sub: "user-123",
|
|
576
|
+
email: "test@example.com",
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const tokenPromise = manager.requestNewToken();
|
|
580
|
+
|
|
581
|
+
simulateTokenResponse({
|
|
582
|
+
token: fakeToken,
|
|
583
|
+
expiresAt: new Date(Date.now() + 60000).toISOString(),
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
await tokenPromise;
|
|
587
|
+
|
|
588
|
+
const user = manager.getUser();
|
|
589
|
+
|
|
590
|
+
expect(user).toEqual({
|
|
591
|
+
userId: "user-123",
|
|
592
|
+
email: "test@example.com",
|
|
593
|
+
});
|
|
594
|
+
});
|
|
555
595
|
});
|
|
556
596
|
|
|
557
597
|
describe("default token lifetime", () => {
|
|
@@ -793,4 +833,107 @@ describe("SessionManager", () => {
|
|
|
793
833
|
expect(token).toBe("success-token");
|
|
794
834
|
});
|
|
795
835
|
});
|
|
836
|
+
|
|
837
|
+
describe("react-native-webview token push", () => {
|
|
838
|
+
beforeEach(() => {
|
|
839
|
+
messageHandler = null;
|
|
840
|
+
addEventListenerSpy = vi.fn((event: string, handler: Function) => {
|
|
841
|
+
if (event === "message") {
|
|
842
|
+
messageHandler = handler as (event: MessageEvent) => void;
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
vi.stubGlobal("window", {
|
|
847
|
+
addEventListener: addEventListenerSpy,
|
|
848
|
+
removeEventListener: removeEventListenerSpy,
|
|
849
|
+
ReactNativeWebView: {
|
|
850
|
+
postMessage: vi.fn(),
|
|
851
|
+
},
|
|
852
|
+
parent: {
|
|
853
|
+
postMessage: mockPostMessage,
|
|
854
|
+
},
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it("accepts pushed token only when appId and audience match", async () => {
|
|
859
|
+
const manager = new SessionManager({ appId: "todo-app" });
|
|
860
|
+
const tokenPromise = manager.requestNewToken();
|
|
861
|
+
|
|
862
|
+
const token = createJwtLikeToken({
|
|
863
|
+
sub: "user-1",
|
|
864
|
+
aud: "todo-app",
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
simulateMalformedMessage("react-native", {
|
|
868
|
+
type: "SESSION_TOKEN_UPDATE",
|
|
869
|
+
appId: "todo-app",
|
|
870
|
+
token,
|
|
871
|
+
expiresAt: new Date(Date.now() + 60000).toISOString(),
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
await expect(tokenPromise).resolves.toBe(token);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it("accepts stringified token update payload with null-like origin", async () => {
|
|
878
|
+
const manager = new SessionManager({ appId: "todo-app" });
|
|
879
|
+
const tokenPromise = manager.requestNewToken();
|
|
880
|
+
|
|
881
|
+
const token = createJwtLikeToken({
|
|
882
|
+
sub: "user-1",
|
|
883
|
+
aud: "todo-app",
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
simulateMalformedMessage(
|
|
887
|
+
"null",
|
|
888
|
+
JSON.stringify({
|
|
889
|
+
type: "SESSION_TOKEN_UPDATE",
|
|
890
|
+
appId: "todo-app",
|
|
891
|
+
token,
|
|
892
|
+
expiresAt: new Date(Date.now() + 60000).toISOString(),
|
|
893
|
+
}),
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
await expect(tokenPromise).resolves.toBe(token);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it("ignores malformed JSON token updates", async () => {
|
|
900
|
+
vi.useFakeTimers();
|
|
901
|
+
|
|
902
|
+
const manager = new SessionManager({ appId: "todo-app" });
|
|
903
|
+
const tokenPromise = manager.requestNewToken();
|
|
904
|
+
|
|
905
|
+
simulateMalformedMessage("react-native", "{not-json");
|
|
906
|
+
|
|
907
|
+
vi.advanceTimersByTime(10001);
|
|
908
|
+
|
|
909
|
+
await expect(tokenPromise).rejects.toThrow(
|
|
910
|
+
"Timed out waiting for token from React Native bridge",
|
|
911
|
+
);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it("rejects pushed token when audience does not match expected app", async () => {
|
|
915
|
+
vi.useFakeTimers();
|
|
916
|
+
|
|
917
|
+
const manager = new SessionManager({ appId: "todo-app" });
|
|
918
|
+
const tokenPromise = manager.requestNewToken();
|
|
919
|
+
|
|
920
|
+
const wrongAudienceToken = createJwtLikeToken({
|
|
921
|
+
sub: "user-1",
|
|
922
|
+
aud: "chef-app",
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
simulateMalformedMessage("react-native", {
|
|
926
|
+
type: "SESSION_TOKEN_UPDATE",
|
|
927
|
+
appId: "todo-app",
|
|
928
|
+
token: wrongAudienceToken,
|
|
929
|
+
expiresAt: new Date(Date.now() + 60000).toISOString(),
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
vi.advanceTimersByTime(10001);
|
|
933
|
+
|
|
934
|
+
await expect(tokenPromise).rejects.toThrow(
|
|
935
|
+
"Timed out waiting for token from React Native bridge",
|
|
936
|
+
);
|
|
937
|
+
});
|
|
938
|
+
});
|
|
796
939
|
});
|