@every-app/sdk 0.1.11 → 0.1.13

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 (33) hide show
  1. package/dist/cloudflare/server/gateway.d.ts.map +1 -1
  2. package/dist/cloudflare/server/gateway.js +10 -4
  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 +24 -2
  7. package/dist/core/sessionManager.d.ts.map +1 -1
  8. package/dist/core/sessionManager.js +191 -27
  9. package/dist/shared/parseMessagePayload.d.ts +3 -0
  10. package/dist/shared/parseMessagePayload.d.ts.map +1 -0
  11. package/dist/shared/parseMessagePayload.js +18 -0
  12. package/dist/tanstack/EmbeddedAppProvider.d.ts.map +1 -1
  13. package/dist/tanstack/EmbeddedAppProvider.js +3 -2
  14. package/dist/tanstack/_internal/useEveryAppSession.d.ts +2 -0
  15. package/dist/tanstack/_internal/useEveryAppSession.d.ts.map +1 -1
  16. package/dist/tanstack/_internal/useEveryAppSession.js +8 -2
  17. package/dist/tanstack/server/authenticateRequest.d.ts.map +1 -1
  18. package/dist/tanstack/server/authenticateRequest.js +6 -1
  19. package/dist/tanstack/useEveryAppRouter.d.ts.map +1 -1
  20. package/dist/tanstack/useEveryAppRouter.js +20 -11
  21. package/package.json +4 -1
  22. package/src/cloudflare/server/gateway.test.ts +87 -8
  23. package/src/cloudflare/server/gateway.ts +13 -4
  24. package/src/core/index.ts +10 -2
  25. package/src/core/sessionManager.test.ts +143 -0
  26. package/src/core/sessionManager.ts +265 -30
  27. package/src/shared/parseMessagePayload.ts +22 -0
  28. package/src/tanstack/EmbeddedAppProvider.tsx +5 -2
  29. package/src/tanstack/_internal/useEveryAppSession.test.ts +40 -0
  30. package/src/tanstack/_internal/useEveryAppSession.tsx +16 -3
  31. package/src/tanstack/server/authenticateRequest.test.ts +35 -0
  32. package/src/tanstack/server/authenticateRequest.ts +7 -1
  33. package/src/tanstack/useEveryAppRouter.tsx +21 -14
@@ -56,31 +56,110 @@ 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("throws when absolute URL points to non-gateway origin", async () => {
60
+ const fetchMock = vi
61
+ .spyOn(globalThis, "fetch")
62
+ .mockResolvedValue(new Response("ok", { status: 200 }));
63
+
64
+ const env: TestEnv = {
65
+ GATEWAY_URL: "https://gateway.example.com",
66
+ GATEWAY_APP_API_TOKEN: "eat_test_token",
67
+ };
68
+
69
+ await expect(
70
+ fetchGateway({
71
+ env,
72
+ url: "https://evil.example.com/api/ai/openai/v1/responses",
73
+ init: { method: "POST" },
74
+ }),
75
+ ).rejects.toThrow(
76
+ "Refusing to send gateway token to non-gateway origin: https://evil.example.com",
77
+ );
78
+
79
+ expect(fetchMock).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it("allows absolute URL when it matches gateway origin", async () => {
83
+ const fetchMock = vi
84
+ .spyOn(globalThis, "fetch")
62
85
  .mockResolvedValue(new Response("ok", { status: 200 }));
63
86
 
64
87
  const env: TestEnv = {
65
88
  GATEWAY_URL: "https://gateway.example.com",
66
- EVERY_APP_GATEWAY: { fetch: bindingFetch },
67
89
  GATEWAY_APP_API_TOKEN: "eat_test_token",
68
90
  };
69
91
 
70
92
  await fetchGateway({
71
93
  env,
72
- url: "/api/ai/openai/v1/chat/completions",
94
+ url: "https://gateway.example.com/api/ai/openai/v1/responses",
73
95
  init: { method: "POST" },
74
96
  });
75
97
 
76
- expect(bindingFetch).toHaveBeenCalledTimes(1);
77
- const [requestArg] = bindingFetch.mock.calls[0];
98
+ expect(fetchMock).toHaveBeenCalledTimes(1);
99
+ const [requestArg] = fetchMock.mock.calls[0];
78
100
  const request = requestArg as Request;
79
101
  expect(request.url).toBe(
80
- "http://localhost/api/ai/openai/v1/chat/completions",
102
+ "https://gateway.example.com/api/ai/openai/v1/responses",
81
103
  );
82
104
  });
83
105
 
106
+ it("fetches via service binding when available in production", async () => {
107
+ // Service binding is only used when import.meta.env.PROD is true
108
+ const originalProd = import.meta.env.PROD;
109
+ import.meta.env.PROD = true;
110
+
111
+ try {
112
+ const bindingFetch = vi
113
+ .fn()
114
+ .mockResolvedValue(new Response("ok", { status: 200 }));
115
+
116
+ const env: TestEnv = {
117
+ GATEWAY_URL: "https://gateway.example.com",
118
+ EVERY_APP_GATEWAY: { fetch: bindingFetch },
119
+ GATEWAY_APP_API_TOKEN: "eat_test_token",
120
+ };
121
+
122
+ await fetchGateway({
123
+ env,
124
+ url: "/api/ai/openai/v1/chat/completions",
125
+ init: { method: "POST" },
126
+ });
127
+
128
+ expect(bindingFetch).toHaveBeenCalledTimes(1);
129
+ const [requestArg] = bindingFetch.mock.calls[0];
130
+ const request = requestArg as Request;
131
+ expect(request.url).toBe(
132
+ "http://localhost/api/ai/openai/v1/chat/completions",
133
+ );
134
+ } finally {
135
+ import.meta.env.PROD = originalProd;
136
+ }
137
+ });
138
+
139
+ it("skips service binding in development and uses HTTP fetch", async () => {
140
+ const fetchMock = vi
141
+ .spyOn(globalThis, "fetch")
142
+ .mockResolvedValue(new Response("ok", { status: 200 }));
143
+
144
+ const bindingFetch = vi.fn();
145
+
146
+ const env: TestEnv = {
147
+ GATEWAY_URL: "https://gateway.example.com",
148
+ EVERY_APP_GATEWAY: { fetch: bindingFetch },
149
+ GATEWAY_APP_API_TOKEN: "eat_test_token",
150
+ };
151
+
152
+ await fetchGateway({
153
+ env,
154
+ url: "/api/ai/openai/v1/chat/completions",
155
+ init: { method: "POST" },
156
+ });
157
+
158
+ // In dev, should use HTTP fetch, not service binding
159
+ expect(bindingFetch).not.toHaveBeenCalled();
160
+ expect(fetchMock).toHaveBeenCalledTimes(1);
161
+ });
162
+
84
163
  it("always injects app token and strips Authorization header", async () => {
85
164
  const fetchMock = vi
86
165
  .spyOn(globalThis, "fetch")
@@ -46,20 +46,29 @@ export async function fetchGateway({
46
46
  const resolvedRequest = toRequest(url, init, gatewayBaseUrl);
47
47
  const authenticatedRequest = applyAppTokenAuth(resolvedRequest, env);
48
48
 
49
- // Use service binding when available for zero-latency internal routing
50
- // (available in production Workers, not in local dev)
51
- if (env.EVERY_APP_GATEWAY) {
49
+ // Use service binding in production for zero-latency internal routing.
50
+ // In local dev wrangler still exposes the binding object, but the target
51
+ // service usually isn't running locally, so we skip it and use HTTP fetch.
52
+ if (import.meta.env.PROD && env.EVERY_APP_GATEWAY) {
52
53
  const url = new URL(authenticatedRequest.url);
53
54
  const bindingUrl = `${SERVICE_BINDING_ORIGIN}${url.pathname}${url.search}`;
54
55
  const bindingRequest = new Request(bindingUrl, authenticatedRequest);
55
56
  return env.EVERY_APP_GATEWAY.fetch(bindingRequest);
56
57
  }
57
58
 
58
- // Fall back to HTTP fetch for local dev
59
+ // HTTP fetch used in local dev, or as a fallback when no binding exists
59
60
  return fetch(authenticatedRequest);
60
61
  }
61
62
 
62
63
  function applyAppTokenAuth(request: Request, env: GatewayEnv): Request {
64
+ const gatewayOrigin = new URL(getGatewayUrl(env)).origin;
65
+ const requestOrigin = new URL(request.url).origin;
66
+ if (requestOrigin !== gatewayOrigin) {
67
+ throw new Error(
68
+ `Refusing to send gateway token to non-gateway origin: ${requestOrigin}`,
69
+ );
70
+ }
71
+
63
72
  const appToken = getGatewayAppApiToken(env);
64
73
  if (!appToken) {
65
74
  throw new Error(
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
  });