@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 +1 @@
1
- {"version":3,"file":"authenticatedFetch.d.ts","sourceRoot":"","sources":["../../src/core/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"}
1
+ {"version":3,"file":"authenticatedFetch.d.ts","sourceRoot":"","sources":["../../src/core/authenticatedFetch.ts"],"names":[],"mappings":"AAaA;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAmBvD;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,WAAW,GAAG,GAAG,EACxB,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,QAAQ,CAAC,CAUnB"}
@@ -1,7 +1,11 @@
1
+ import { BYPASS_GATEWAY_LOCAL_ONLY_TOKEN, isBypassGatewayLocalOnlyClient, } from "../shared/bypassGatewayLocalOnly.js";
1
2
  /**
2
3
  * Gets the current session token from the embedded session manager
3
4
  */
4
5
  export async function getSessionToken() {
6
+ if (isBypassGatewayLocalOnlyClient()) {
7
+ return BYPASS_GATEWAY_LOCAL_ONLY_TOKEN;
8
+ }
5
9
  const windowWithSession = window;
6
10
  const sessionManager = windowWithSession.__embeddedSessionManager;
7
11
  if (!sessionManager) {
@@ -1,4 +1,4 @@
1
- export { SessionManager, isRunningInIframe } from "./sessionManager.js";
2
- export type { SessionManagerConfig } from "./sessionManager.js";
1
+ export { SessionManager, isRunningInIframe, isRunningInReactNativeWebView, detectEnvironment, } from "./sessionManager.js";
2
+ export type { SessionManagerConfig, EmbeddedEnvironment, } from "./sessionManager.js";
3
3
  export { authenticatedFetch, getSessionToken } from "./authenticatedFetch.js";
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxE,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEhE,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,6BAA6B,EAC7B,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC"}
@@ -1,2 +1,2 @@
1
- export { SessionManager, isRunningInIframe } from "./sessionManager.js";
1
+ export { SessionManager, isRunningInIframe, isRunningInReactNativeWebView, detectEnvironment, } from "./sessionManager.js";
2
2
  export { authenticatedFetch, getSessionToken } from "./authenticatedFetch.js";
@@ -1,20 +1,45 @@
1
1
  export interface SessionManagerConfig {
2
2
  appId: string;
3
3
  }
4
+ /**
5
+ * Environment detection types
6
+ */
7
+ export type EmbeddedEnvironment = "iframe" | "react-native-webview" | "standalone";
4
8
  /**
5
9
  * Detects whether the current window is running inside an iframe.
6
10
  * Returns true if in an iframe, false if running as top-level window.
7
11
  */
8
12
  export declare function isRunningInIframe(): boolean;
13
+ /**
14
+ * Detects whether the current window is running inside a React Native WebView.
15
+ * Returns true if window.ReactNativeWebView is available.
16
+ */
17
+ export declare function isRunningInReactNativeWebView(): boolean;
18
+ /**
19
+ * Detects the current embedded environment.
20
+ * Priority: React Native WebView > iframe > standalone
21
+ */
22
+ export declare function detectEnvironment(): EmbeddedEnvironment;
9
23
  export declare class SessionManager {
10
24
  readonly parentOrigin: string;
11
25
  readonly appId: string;
12
26
  readonly isInIframe: boolean;
27
+ readonly environment: EmbeddedEnvironment;
28
+ readonly isBypassGatewayLocalOnly: boolean;
13
29
  private token;
14
30
  private refreshPromise;
31
+ private tokenWaiters;
15
32
  constructor(config: SessionManagerConfig);
33
+ /**
34
+ * Check if running in an embedded environment (iframe or React Native WebView)
35
+ */
36
+ isEmbedded(): boolean;
37
+ isTrustedHostMessage(event: MessageEvent): boolean;
38
+ postToHost(message: object): void;
16
39
  private isTokenExpiringSoon;
17
40
  private postMessageWithResponse;
41
+ private setupReactNativeTokenListener;
42
+ private waitForReactNativeTokenPush;
18
43
  requestNewToken(): Promise<string>;
19
44
  getToken(): Promise<string>;
20
45
  getTokenState(): {
@@ -1 +1 @@
1
- {"version":3,"file":"sessionManager.d.ts","sourceRoot":"","sources":["../../src/core/sessionManager.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"}
1
+ {"version":3,"file":"sessionManager.d.ts","sourceRoot":"","sources":["../../src/core/sessionManager.ts"],"names":[],"mappings":"AAiEA,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAOD;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,sBAAsB,GACtB,YAAY,CAAC;AAEjB;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAY3C;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CASvD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,mBAAmB,CAWvD;AAED,qBAAa,cAAc;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;IAC1C,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAE3C,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,cAAc,CAAgC;IACtD,OAAO,CAAC,YAAY,CAIZ;gBAEI,MAAM,EAAE,oBAAoB;IAyCxC;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAelD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAiBjC,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,uBAAuB;IA+C/B,OAAO,CAAC,6BAA6B;IAgErC,OAAO,CAAC,2BAA2B;IAmB7B,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAgElC,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAcjC,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;CA0BpD"}
@@ -1,11 +1,47 @@
1
+ import { BYPASS_GATEWAY_LOCAL_ONLY_EMAIL, BYPASS_GATEWAY_LOCAL_ONLY_TOKEN, BYPASS_GATEWAY_LOCAL_ONLY_USER_ID, isBypassGatewayLocalOnlyClient, } from "../shared/bypassGatewayLocalOnly.js";
2
+ import { parseMessagePayload } from "../shared/parseMessagePayload.js";
3
+ function isTokenUpdateMessage(data) {
4
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
5
+ return false;
6
+ }
7
+ return data.type === "SESSION_TOKEN_UPDATE";
8
+ }
9
+ function decodeJwtPayload(token) {
10
+ try {
11
+ const parts = token.split(".");
12
+ if (parts.length !== 3) {
13
+ return null;
14
+ }
15
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
16
+ const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
17
+ return JSON.parse(atob(padded));
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function tokenAudienceMatchesApp(payload, appId) {
24
+ const aud = payload.aud;
25
+ if (typeof aud === "string") {
26
+ return aud === appId;
27
+ }
28
+ if (Array.isArray(aud)) {
29
+ return aud.some((value) => value === appId);
30
+ }
31
+ return false;
32
+ }
1
33
  const MESSAGE_TIMEOUT_MS = 5000;
2
34
  const TOKEN_EXPIRY_BUFFER_MS = 10000;
3
35
  const DEFAULT_TOKEN_LIFETIME_MS = 60000;
36
+ const REACT_NATIVE_TOKEN_WAIT_TIMEOUT_MS = 10000;
4
37
  /**
5
38
  * Detects whether the current window is running inside an iframe.
6
39
  * Returns true if in an iframe, false if running as top-level window.
7
40
  */
8
41
  export function isRunningInIframe() {
42
+ if (typeof window === "undefined") {
43
+ return false;
44
+ }
9
45
  try {
10
46
  return window.self !== window.top;
11
47
  }
@@ -15,29 +51,101 @@ export function isRunningInIframe() {
15
51
  return true;
16
52
  }
17
53
  }
54
+ /**
55
+ * Detects whether the current window is running inside a React Native WebView.
56
+ * Returns true if window.ReactNativeWebView is available.
57
+ */
58
+ export function isRunningInReactNativeWebView() {
59
+ if (typeof window === "undefined") {
60
+ return false;
61
+ }
62
+ return (typeof window.ReactNativeWebView?.postMessage === "function" ||
63
+ window.isReactNativeWebView === true);
64
+ }
65
+ /**
66
+ * Detects the current embedded environment.
67
+ * Priority: React Native WebView > iframe > standalone
68
+ */
69
+ export function detectEnvironment() {
70
+ const isRNWebView = isRunningInReactNativeWebView();
71
+ const isIframe = isRunningInIframe();
72
+ if (isRNWebView) {
73
+ return "react-native-webview";
74
+ }
75
+ if (isIframe) {
76
+ return "iframe";
77
+ }
78
+ return "standalone";
79
+ }
18
80
  export class SessionManager {
19
81
  parentOrigin;
20
82
  appId;
21
83
  isInIframe;
84
+ environment;
85
+ isBypassGatewayLocalOnly;
22
86
  token = null;
23
87
  refreshPromise = null;
88
+ tokenWaiters = [];
24
89
  constructor(config) {
25
90
  if (!config.appId) {
26
91
  throw new Error("[SessionManager] appId is required.");
27
92
  }
93
+ this.isBypassGatewayLocalOnly = isBypassGatewayLocalOnlyClient();
28
94
  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}`);
95
+ if (!this.isBypassGatewayLocalOnly) {
96
+ if (!gatewayUrl) {
97
+ throw new Error("[SessionManager] VITE_GATEWAY_URL env var is required.");
98
+ }
99
+ try {
100
+ new URL(gatewayUrl);
101
+ }
102
+ catch {
103
+ throw new Error(`[SessionManager] Invalid gateway URL: ${gatewayUrl}`);
104
+ }
37
105
  }
38
106
  this.appId = config.appId;
39
- this.parentOrigin = gatewayUrl;
107
+ this.parentOrigin = this.isBypassGatewayLocalOnly
108
+ ? window.location.origin
109
+ : gatewayUrl;
110
+ this.environment = detectEnvironment();
40
111
  this.isInIframe = isRunningInIframe();
112
+ if (this.environment === "react-native-webview") {
113
+ this.setupReactNativeTokenListener();
114
+ }
115
+ if (this.isBypassGatewayLocalOnly) {
116
+ this.token = {
117
+ token: BYPASS_GATEWAY_LOCAL_ONLY_TOKEN,
118
+ expiresAt: Date.now() + DEFAULT_TOKEN_LIFETIME_MS,
119
+ };
120
+ }
121
+ }
122
+ /**
123
+ * Check if running in an embedded environment (iframe or React Native WebView)
124
+ */
125
+ isEmbedded() {
126
+ return this.environment !== "standalone";
127
+ }
128
+ isTrustedHostMessage(event) {
129
+ if (this.environment === "react-native-webview") {
130
+ const origin = event
131
+ .origin;
132
+ return (origin === "react-native" ||
133
+ origin === "null" ||
134
+ origin === "" ||
135
+ origin == null);
136
+ }
137
+ return event.origin === this.parentOrigin;
138
+ }
139
+ postToHost(message) {
140
+ if (this.environment === "react-native-webview") {
141
+ const postMessage = window.ReactNativeWebView?.postMessage;
142
+ if (typeof postMessage !== "function") {
143
+ throw new Error("React Native WebView bridge is unavailable");
144
+ }
145
+ postMessage.call(window.ReactNativeWebView, JSON.stringify(message));
146
+ return;
147
+ }
148
+ window.parent.postMessage(message, this.parentOrigin);
41
149
  }
42
150
  isTokenExpiringSoon(bufferMs = TOKEN_EXPIRY_BUFFER_MS) {
43
151
  if (!this.token)
@@ -51,20 +159,19 @@ export class SessionManager {
51
159
  window.removeEventListener("message", handler);
52
160
  };
53
161
  const handler = (event) => {
54
- // Security: reject messages from wrong origin (including null from sandboxed iframes)
55
- if (event.origin !== this.parentOrigin)
162
+ // Security: validate message origin based on environment
163
+ if (!this.isTrustedHostMessage(event))
56
164
  return;
57
- // Safety: ignore malformed messages that could crash the handler
58
- if (!event.data || typeof event.data !== "object")
165
+ const data = parseMessagePayload(event.data);
166
+ if (!data)
59
167
  return;
60
- if (event.data.type === responseType &&
61
- event.data.requestId === requestId) {
168
+ if (data.type === responseType && data.requestId === requestId) {
62
169
  cleanup();
63
- if (event.data.error) {
64
- reject(new Error(event.data.error));
170
+ if (typeof data.error === "string" && data.error) {
171
+ reject(new Error(data.error));
65
172
  }
66
173
  else {
67
- resolve(event.data);
174
+ resolve(data);
68
175
  }
69
176
  }
70
177
  };
@@ -73,13 +180,99 @@ export class SessionManager {
73
180
  reject(new Error("Token request timeout - parent did not respond"));
74
181
  }, MESSAGE_TIMEOUT_MS);
75
182
  window.addEventListener("message", handler);
76
- window.parent.postMessage(request, this.parentOrigin);
183
+ try {
184
+ this.postToHost(request);
185
+ }
186
+ catch (error) {
187
+ cleanup();
188
+ reject(error instanceof Error
189
+ ? error
190
+ : new Error("Failed to post message to host"));
191
+ }
192
+ });
193
+ }
194
+ setupReactNativeTokenListener() {
195
+ window.addEventListener("message", (event) => {
196
+ if (!this.isTrustedHostMessage(event)) {
197
+ return;
198
+ }
199
+ const data = parseMessagePayload(event.data);
200
+ if (!data) {
201
+ return;
202
+ }
203
+ if (!isTokenUpdateMessage(data)) {
204
+ return;
205
+ }
206
+ const message = data;
207
+ if (!message.token || typeof message.token !== "string") {
208
+ return;
209
+ }
210
+ if (typeof message.appId !== "string" || message.appId !== this.appId) {
211
+ return;
212
+ }
213
+ if (message.expiresAt !== undefined &&
214
+ typeof message.expiresAt !== "string") {
215
+ return;
216
+ }
217
+ const payload = decodeJwtPayload(message.token);
218
+ if (!payload || !tokenAudienceMatchesApp(payload, this.appId)) {
219
+ return;
220
+ }
221
+ // Native controls token minting and pushes updates into the embedded app.
222
+ // We only consume pushed tokens in React Native WebView mode.
223
+ let expiresAt = Date.now() + DEFAULT_TOKEN_LIFETIME_MS;
224
+ if (message.expiresAt) {
225
+ const parsed = new Date(message.expiresAt).getTime();
226
+ if (!Number.isNaN(parsed)) {
227
+ expiresAt = parsed;
228
+ }
229
+ }
230
+ this.token = {
231
+ token: message.token,
232
+ expiresAt,
233
+ };
234
+ if (this.tokenWaiters.length > 0) {
235
+ const waiters = this.tokenWaiters;
236
+ this.tokenWaiters = [];
237
+ for (const waiter of waiters) {
238
+ clearTimeout(waiter.timeout);
239
+ waiter.resolve(message.token);
240
+ }
241
+ }
242
+ });
243
+ }
244
+ waitForReactNativeTokenPush() {
245
+ if (this.token && !this.isTokenExpiringSoon()) {
246
+ return Promise.resolve(this.token.token);
247
+ }
248
+ return new Promise((resolve, reject) => {
249
+ const timeout = setTimeout(() => {
250
+ this.tokenWaiters = this.tokenWaiters.filter((w) => w.timeout !== timeout);
251
+ reject(new Error("Timed out waiting for token from React Native bridge"));
252
+ }, REACT_NATIVE_TOKEN_WAIT_TIMEOUT_MS);
253
+ this.tokenWaiters.push({ resolve, reject, timeout });
77
254
  });
78
255
  }
79
256
  async requestNewToken() {
257
+ if (this.isBypassGatewayLocalOnly) {
258
+ this.token = {
259
+ token: BYPASS_GATEWAY_LOCAL_ONLY_TOKEN,
260
+ expiresAt: Date.now() + DEFAULT_TOKEN_LIFETIME_MS,
261
+ };
262
+ return this.token.token;
263
+ }
80
264
  if (this.refreshPromise) {
81
265
  return this.refreshPromise;
82
266
  }
267
+ if (this.environment === "react-native-webview") {
268
+ this.refreshPromise = this.waitForReactNativeTokenPush();
269
+ try {
270
+ return await this.refreshPromise;
271
+ }
272
+ finally {
273
+ this.refreshPromise = null;
274
+ }
275
+ }
83
276
  this.refreshPromise = (async () => {
84
277
  const requestId = crypto.randomUUID();
85
278
  const response = await this.postMessageWithResponse({
@@ -112,6 +305,12 @@ export class SessionManager {
112
305
  }
113
306
  }
114
307
  async getToken() {
308
+ if (this.isBypassGatewayLocalOnly) {
309
+ if (!this.token || this.isTokenExpiringSoon()) {
310
+ return this.requestNewToken();
311
+ }
312
+ return this.token.token;
313
+ }
115
314
  if (this.isTokenExpiringSoon()) {
116
315
  return this.requestNewToken();
117
316
  }
@@ -134,25 +333,25 @@ export class SessionManager {
134
333
  * Returns null if no valid token is available.
135
334
  */
136
335
  getUser() {
336
+ if (this.isBypassGatewayLocalOnly) {
337
+ return {
338
+ userId: BYPASS_GATEWAY_LOCAL_ONLY_USER_ID,
339
+ email: BYPASS_GATEWAY_LOCAL_ONLY_EMAIL,
340
+ };
341
+ }
137
342
  if (!this.token) {
138
343
  return null;
139
344
  }
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
- };
345
+ const payload = decodeJwtPayload(this.token.token);
346
+ if (!payload) {
347
+ return null;
153
348
  }
154
- catch {
349
+ if (typeof payload.sub !== "string") {
155
350
  return null;
156
351
  }
352
+ return {
353
+ userId: payload.sub,
354
+ email: typeof payload.email === "string" ? payload.email : "",
355
+ };
157
356
  }
158
357
  }
@@ -0,0 +1,14 @@
1
+ export declare const BYPASS_GATEWAY_LOCAL_ONLY_TOKEN = "BYPASS_GATEWAY_LOCAL_ONLY";
2
+ export declare const BYPASS_GATEWAY_LOCAL_ONLY_USER_ID = "demo-local-user";
3
+ export declare const BYPASS_GATEWAY_LOCAL_ONLY_EMAIL = "demo-local-user@local";
4
+ export declare function isBypassGatewayLocalOnlyClient(): boolean;
5
+ export declare function isBypassGatewayLocalOnlyServer(): boolean;
6
+ export declare function createBypassGatewayLocalOnlySessionPayload(audience: string): {
7
+ sub: string;
8
+ email: string;
9
+ iss: string;
10
+ aud: string;
11
+ iat: number;
12
+ exp: number;
13
+ };
14
+ //# sourceMappingURL=bypassGatewayLocalOnly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bypassGatewayLocalOnly.d.ts","sourceRoot":"","sources":["../../src/shared/bypassGatewayLocalOnly.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,+BAA+B,8BAA8B,CAAC;AAC3E,eAAO,MAAM,iCAAiC,oBAAoB,CAAC;AACnE,eAAO,MAAM,+BAA+B,0BAA0B,CAAC;AAEvE,wBAAgB,8BAA8B,IAAI,OAAO,CAYxD;AAED,wBAAgB,8BAA8B,IAAI,OAAO,CAsBxD;AAED,wBAAgB,0CAA0C,CAAC,QAAQ,EAAE,MAAM;;;;;;;EAY1E"}
@@ -0,0 +1,41 @@
1
+ export const BYPASS_GATEWAY_LOCAL_ONLY_TOKEN = "BYPASS_GATEWAY_LOCAL_ONLY";
2
+ export const BYPASS_GATEWAY_LOCAL_ONLY_USER_ID = "demo-local-user";
3
+ export const BYPASS_GATEWAY_LOCAL_ONLY_EMAIL = "demo-local-user@local";
4
+ export function isBypassGatewayLocalOnlyClient() {
5
+ if (import.meta.env.PROD) {
6
+ return false;
7
+ }
8
+ const metaEnv = import.meta
9
+ .env;
10
+ return (metaEnv?.VITE_BYPASS_GATEWAY_LOCAL_ONLY === "true" ||
11
+ metaEnv?.BYPASS_GATEWAY_LOCAL_ONLY === "true");
12
+ }
13
+ export function isBypassGatewayLocalOnlyServer() {
14
+ if (import.meta.env.PROD) {
15
+ return false;
16
+ }
17
+ const metaEnv = import.meta
18
+ .env;
19
+ const metaValue = metaEnv?.BYPASS_GATEWAY_LOCAL_ONLY ??
20
+ metaEnv?.VITE_BYPASS_GATEWAY_LOCAL_ONLY;
21
+ if (metaValue === "true") {
22
+ return true;
23
+ }
24
+ if (typeof process !== "undefined" &&
25
+ process.env?.BYPASS_GATEWAY_LOCAL_ONLY === "true") {
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ export function createBypassGatewayLocalOnlySessionPayload(audience) {
31
+ const issuedAt = Math.floor(Date.now() / 1000);
32
+ const expiresAt = issuedAt + 60 * 60;
33
+ return {
34
+ sub: BYPASS_GATEWAY_LOCAL_ONLY_USER_ID,
35
+ email: BYPASS_GATEWAY_LOCAL_ONLY_EMAIL,
36
+ iss: "local",
37
+ aud: audience,
38
+ iat: issuedAt,
39
+ exp: expiresAt,
40
+ };
41
+ }
@@ -0,0 +1,3 @@
1
+ export type MessagePayload = Record<string, unknown>;
2
+ export declare function parseMessagePayload(data: unknown): MessagePayload | null;
3
+ //# sourceMappingURL=parseMessagePayload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseMessagePayload.d.ts","sourceRoot":"","sources":["../../src/shared/parseMessagePayload.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAErD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAmBxE"}
@@ -0,0 +1,18 @@
1
+ export function parseMessagePayload(data) {
2
+ if (data && typeof data === "object" && !Array.isArray(data)) {
3
+ return data;
4
+ }
5
+ if (typeof data !== "string") {
6
+ return null;
7
+ }
8
+ try {
9
+ const parsed = JSON.parse(data);
10
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
11
+ return parsed;
12
+ }
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ return null;
18
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"EmbeddedAppProvider.d.ts","sourceRoot":"","sources":["../../src/tanstack/EmbeddedAppProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6C,MAAM,OAAO,CAAC;AAClE,OAAO,EAEL,oBAAoB,EACrB,MAAM,2BAA2B,CAAC;AAKnC,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"}
1
+ {"version":3,"file":"EmbeddedAppProvider.d.ts","sourceRoot":"","sources":["../../src/tanstack/EmbeddedAppProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6C,MAAM,OAAO,CAAC;AAClE,OAAO,EAEL,oBAAoB,EACrB,MAAM,2BAA2B,CAAC;AAKnC,UAAU,sBAAuB,SAAQ,oBAAoB;IAC3D,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAUD,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,GAAG,MAAM,EACV,EAAE,sBAAsB,kDAgCxB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,IAAI;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAmBzE"}
@@ -11,8 +11,9 @@ export function EmbeddedAppProvider({ children, ...config }) {
11
11
  useEveryAppRouter({ sessionManager });
12
12
  if (!sessionManager)
13
13
  return null;
14
- // Check if the app is running outside of the Gateway iframe
15
- if (!sessionManager.isInIframe) {
14
+ // Check if the app is running outside of the Gateway (iframe or React Native WebView)
15
+ if (!sessionManager.isEmbedded() &&
16
+ !sessionManager.isBypassGatewayLocalOnly) {
16
17
  return (_jsx(GatewayRequiredError, { gatewayOrigin: sessionManager.parentOrigin, appId: config.appId }));
17
18
  }
18
19
  const value = {
@@ -5,6 +5,8 @@ interface SessionManagerConfig {
5
5
  interface UseEveryAppSessionParams {
6
6
  sessionManagerConfig: SessionManagerConfig;
7
7
  }
8
+ type SessionBootstrapGate = Pick<SessionManager, "isEmbedded" | "isBypassGatewayLocalOnly">;
9
+ export declare function shouldBootstrapSession(sessionManager: SessionBootstrapGate): boolean;
8
10
  export declare function useEveryAppSession({ sessionManagerConfig, }: UseEveryAppSessionParams): {
9
11
  sessionManager: SessionManager | null;
10
12
  sessionTokenState: {
@@ -1 +1 @@
1
- {"version":3,"file":"useEveryAppSession.d.ts","sourceRoot":"","sources":["../../../src/tanstack/_internal/useEveryAppSession.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9D,UAAU,oBAAoB;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,wBAAwB;IAChC,oBAAoB,EAAE,oBAAoB,CAAC;CAC5C;AAED,wBAAgB,kBAAkB,CAAC,EACjC,oBAAoB,GACrB,EAAE,wBAAwB;;;;;;EA8C1B"}
1
+ {"version":3,"file":"useEveryAppSession.d.ts","sourceRoot":"","sources":["../../../src/tanstack/_internal/useEveryAppSession.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9D,UAAU,oBAAoB;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,wBAAwB;IAChC,oBAAoB,EAAE,oBAAoB,CAAC;CAC5C;AAED,KAAK,oBAAoB,GAAG,IAAI,CAC9B,cAAc,EACd,YAAY,GAAG,0BAA0B,CAC1C,CAAC;AAEF,wBAAgB,sBAAsB,CACpC,cAAc,EAAE,oBAAoB,GACnC,OAAO,CAKT;AAED,wBAAgB,kBAAkB,CAAC,EACjC,oBAAoB,GACrB,EAAE,wBAAwB;;;;;;EA8C1B"}
@@ -1,5 +1,11 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  import { SessionManager } from "../../core/sessionManager.js";
3
+ export function shouldBootstrapSession(sessionManager) {
4
+ // Bootstrapping should happen whenever the app is hosted by a trusted container.
5
+ // This intentionally keys off embedded environment semantics (iframe OR RN WebView),
6
+ // not iframe-only detection, so RN can initialize auth from pushed tokens.
7
+ return sessionManager.isEmbedded() || sessionManager.isBypassGatewayLocalOnly;
8
+ }
3
9
  export function useEveryAppSession({ sessionManagerConfig, }) {
4
10
  const sessionManagerRef = useRef(null);
5
11
  const [sessionTokenState, setSessionTokenState] = useState({
@@ -13,8 +19,8 @@ export function useEveryAppSession({ sessionManagerConfig, }) {
13
19
  useEffect(() => {
14
20
  if (!sessionManager)
15
21
  return;
16
- // Skip token requests when not in iframe - the app will show GatewayRequiredError instead
17
- if (!sessionManager.isInIframe)
22
+ // Skip token bootstrap when not embedded (unless in demo mode) - the app will show GatewayRequiredError instead
23
+ if (!shouldBootstrapSession(sessionManager))
18
24
  return;
19
25
  const interval = setInterval(() => {
20
26
  setSessionTokenState(sessionManager.getTokenState());
@@ -1 +1 @@
1
- {"version":3,"file":"authConfig.d.ts","sourceRoot":"","sources":["../../../src/tanstack/server/authConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,wBAAgB,aAAa,IAAI,UAAU,CAK1C"}
1
+ {"version":3,"file":"authConfig.d.ts","sourceRoot":"","sources":["../../../src/tanstack/server/authConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,wBAAgB,aAAa,IAAI,UAAU,CAc1C"}
@@ -1,7 +1,13 @@
1
1
  import { env } from "cloudflare:workers";
2
+ import { isBypassGatewayLocalOnlyServer } from "../../shared/bypassGatewayLocalOnly.js";
2
3
  export function getAuthConfig() {
4
+ const bypassGatewayLocalOnlyEnv = env.BYPASS_GATEWAY_LOCAL_ONLY;
5
+ const isBypassGatewayLocalOnly = import.meta.env.PROD !== true &&
6
+ (bypassGatewayLocalOnlyEnv === "true" ||
7
+ isBypassGatewayLocalOnlyServer() === true);
8
+ const issuer = env.GATEWAY_URL || (isBypassGatewayLocalOnly ? "local" : "");
3
9
  return {
4
- issuer: env.GATEWAY_URL,
10
+ issuer,
5
11
  audience: import.meta.env.VITE_APP_ID,
6
12
  };
7
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"authenticateRequest.d.ts","sourceRoot":"","sources":["../../../src/tanstack/server/authenticateRequest.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C;;;GAGG;AACH,UAAU,mBAAmB;IAC3B,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,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,CA8BrC;AAyCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK3E"}
1
+ {"version":3,"file":"authenticateRequest.d.ts","sourceRoot":"","sources":["../../../src/tanstack/server/authenticateRequest.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ7C;;;GAGG;AACH,UAAU,mBAAmB;IAC3B,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,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,CA8CrC;AAyCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK3E"}
@@ -1,9 +1,14 @@
1
1
  import { getRequest } from "@tanstack/react-start/server";
2
2
  import { createLocalJWKSet, jwtVerify, } from "jose";
3
3
  import { env } from "cloudflare:workers";
4
+ import { BYPASS_GATEWAY_LOCAL_ONLY_TOKEN, createBypassGatewayLocalOnlySessionPayload, isBypassGatewayLocalOnlyServer, } from "../../shared/bypassGatewayLocalOnly.js";
4
5
  export async function authenticateRequest(authConfig, providedRequest) {
5
6
  const request = providedRequest || getRequest();
6
7
  const authHeader = request.headers.get("authorization");
8
+ const bypassGatewayLocalOnlyEnv = env.BYPASS_GATEWAY_LOCAL_ONLY;
9
+ const isBypassGatewayLocalOnly = import.meta.env.PROD !== true &&
10
+ (bypassGatewayLocalOnlyEnv === "true" ||
11
+ isBypassGatewayLocalOnlyServer() === true);
7
12
  if (!authHeader) {
8
13
  return null;
9
14
  }
@@ -11,6 +16,12 @@ export async function authenticateRequest(authConfig, providedRequest) {
11
16
  if (!token) {
12
17
  return null;
13
18
  }
19
+ if (isBypassGatewayLocalOnly) {
20
+ if (token !== BYPASS_GATEWAY_LOCAL_ONLY_TOKEN) {
21
+ return null;
22
+ }
23
+ return createBypassGatewayLocalOnlySessionPayload(authConfig.audience);
24
+ }
14
25
  try {
15
26
  const session = await verifySessionToken(token, authConfig);
16
27
  return session;
@@ -1 +1 @@
1
- {"version":3,"file":"useEveryAppRouter.d.ts","sourceRoot":"","sources":["../../src/tanstack/useEveryAppRouter.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAG3D,UAAU,uBAAuB;IAC/B,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;CACvC;AAED,wBAAgB,iBAAiB,CAAC,EAAE,cAAc,EAAE,EAAE,uBAAuB,QAmE5E"}
1
+ {"version":3,"file":"useEveryAppRouter.d.ts","sourceRoot":"","sources":["../../src/tanstack/useEveryAppRouter.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAI3D,UAAU,uBAAuB;IAC/B,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;CACvC;AAED,wBAAgB,iBAAiB,CAAC,EAAE,cAAc,EAAE,EAAE,uBAAuB,QAyE5E"}