@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.
Files changed (43) hide show
  1. package/dist/core/authenticatedFetch.d.ts.map +1 -1
  2. package/dist/core/authenticatedFetch.js +4 -0
  3. package/dist/core/index.d.ts +2 -2
  4. package/dist/core/index.d.ts.map +1 -1
  5. package/dist/core/index.js +1 -1
  6. package/dist/core/sessionManager.d.ts +25 -0
  7. package/dist/core/sessionManager.d.ts.map +1 -1
  8. package/dist/core/sessionManager.js +232 -33
  9. package/dist/shared/bypassGatewayLocalOnly.d.ts +14 -0
  10. package/dist/shared/bypassGatewayLocalOnly.d.ts.map +1 -0
  11. package/dist/shared/bypassGatewayLocalOnly.js +41 -0
  12. package/dist/shared/parseMessagePayload.d.ts +3 -0
  13. package/dist/shared/parseMessagePayload.d.ts.map +1 -0
  14. package/dist/shared/parseMessagePayload.js +18 -0
  15. package/dist/tanstack/EmbeddedAppProvider.d.ts.map +1 -1
  16. package/dist/tanstack/EmbeddedAppProvider.js +3 -2
  17. package/dist/tanstack/_internal/useEveryAppSession.d.ts +2 -0
  18. package/dist/tanstack/_internal/useEveryAppSession.d.ts.map +1 -1
  19. package/dist/tanstack/_internal/useEveryAppSession.js +8 -2
  20. package/dist/tanstack/server/authConfig.d.ts.map +1 -1
  21. package/dist/tanstack/server/authConfig.js +7 -1
  22. package/dist/tanstack/server/authenticateRequest.d.ts.map +1 -1
  23. package/dist/tanstack/server/authenticateRequest.js +11 -0
  24. package/dist/tanstack/useEveryAppRouter.d.ts.map +1 -1
  25. package/dist/tanstack/useEveryAppRouter.js +20 -11
  26. package/dist/tanstack/useSessionTokenClientMiddleware.d.ts.map +1 -1
  27. package/dist/tanstack/useSessionTokenClientMiddleware.js +8 -0
  28. package/package.json +1 -1
  29. package/src/cloudflare/server/gateway.test.ts +41 -9
  30. package/src/core/authenticatedFetch.ts +9 -0
  31. package/src/core/index.ts +10 -2
  32. package/src/core/sessionManager.test.ts +143 -0
  33. package/src/core/sessionManager.ts +318 -35
  34. package/src/shared/bypassGatewayLocalOnly.ts +55 -0
  35. package/src/shared/parseMessagePayload.ts +22 -0
  36. package/src/tanstack/EmbeddedAppProvider.tsx +5 -2
  37. package/src/tanstack/_internal/useEveryAppSession.test.ts +40 -0
  38. package/src/tanstack/_internal/useEveryAppSession.tsx +16 -2
  39. package/src/tanstack/server/authConfig.ts +11 -1
  40. package/src/tanstack/server/authenticateRequest.test.ts +32 -0
  41. package/src/tanstack/server/authenticateRequest.ts +21 -0
  42. package/src/tanstack/useEveryAppRouter.tsx +21 -14
  43. 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
- if (event.origin !== sessionManager.parentOrigin)
12
+ // Validate origin based on environment
13
+ if (!sessionManager.isTrustedHostMessage(event))
12
14
  return;
13
- if (event.data.type === "ROUTE_CHANGE" &&
14
- event.data.direction === "parent-to-child") {
15
- const targetRoute = event.data.route;
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
- 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);
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":"AAGA,eAAO,MAAM,+BAA+B,6GA2B1C,CAAC"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@every-app/sdk",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,11 +56,46 @@ describe("gateway server helpers", () => {
56
56
  );
57
57
  });
58
58
 
59
- it("fetches via service binding when available", async () => {
60
- const bindingFetch = vi
61
- .fn()
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
- expect(bindingFetch).toHaveBeenCalledTimes(1);
77
- const [requestArg] = bindingFetch.mock.calls[0];
78
- const request = requestArg as Request;
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 { SessionManager, isRunningInIframe } from "./sessionManager.js";
2
- export type { SessionManagerConfig } from "./sessionManager.js";
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
  });