@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
@@ -4,6 +4,7 @@ import {
4
4
  BYPASS_GATEWAY_LOCAL_ONLY_USER_ID,
5
5
  isBypassGatewayLocalOnlyClient,
6
6
  } from "../shared/bypassGatewayLocalOnly.js";
7
+ import { parseMessagePayload } from "../shared/parseMessagePayload.js";
7
8
 
8
9
  interface SessionToken {
9
10
  token: string;
@@ -16,6 +17,52 @@ interface TokenResponse {
16
17
  error?: string;
17
18
  }
18
19
 
20
+ interface TokenUpdateMessage {
21
+ type: "SESSION_TOKEN_UPDATE";
22
+ token?: string;
23
+ expiresAt?: string;
24
+ appId?: string;
25
+ }
26
+
27
+ function isTokenUpdateMessage(data: unknown): data is TokenUpdateMessage {
28
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
29
+ return false;
30
+ }
31
+
32
+ return (data as { type?: unknown }).type === "SESSION_TOKEN_UPDATE";
33
+ }
34
+
35
+ function decodeJwtPayload(token: string): Record<string, unknown> | null {
36
+ try {
37
+ const parts = token.split(".");
38
+ if (parts.length !== 3) {
39
+ return null;
40
+ }
41
+
42
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
43
+ const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
44
+ return JSON.parse(atob(padded)) as Record<string, unknown>;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function tokenAudienceMatchesApp(
51
+ payload: Record<string, unknown>,
52
+ appId: string,
53
+ ): boolean {
54
+ const aud = payload.aud;
55
+ if (typeof aud === "string") {
56
+ return aud === appId;
57
+ }
58
+
59
+ if (Array.isArray(aud)) {
60
+ return aud.some((value) => value === appId);
61
+ }
62
+
63
+ return false;
64
+ }
65
+
19
66
  export interface SessionManagerConfig {
20
67
  appId: string;
21
68
  }
@@ -23,12 +70,25 @@ export interface SessionManagerConfig {
23
70
  const MESSAGE_TIMEOUT_MS = 5000;
24
71
  const TOKEN_EXPIRY_BUFFER_MS = 10000;
25
72
  const DEFAULT_TOKEN_LIFETIME_MS = 60000;
73
+ const REACT_NATIVE_TOKEN_WAIT_TIMEOUT_MS = 10000;
74
+
75
+ /**
76
+ * Environment detection types
77
+ */
78
+ export type EmbeddedEnvironment =
79
+ | "iframe"
80
+ | "react-native-webview"
81
+ | "standalone";
26
82
 
27
83
  /**
28
84
  * Detects whether the current window is running inside an iframe.
29
85
  * Returns true if in an iframe, false if running as top-level window.
30
86
  */
31
87
  export function isRunningInIframe(): boolean {
88
+ if (typeof window === "undefined") {
89
+ return false;
90
+ }
91
+
32
92
  try {
33
93
  return window.self !== window.top;
34
94
  } catch {
@@ -38,16 +98,52 @@ export function isRunningInIframe(): boolean {
38
98
  }
39
99
  }
40
100
 
101
+ /**
102
+ * Detects whether the current window is running inside a React Native WebView.
103
+ * Returns true if window.ReactNativeWebView is available.
104
+ */
105
+ export function isRunningInReactNativeWebView(): boolean {
106
+ if (typeof window === "undefined") {
107
+ return false;
108
+ }
109
+
110
+ return (
111
+ typeof (window as any).ReactNativeWebView?.postMessage === "function" ||
112
+ (window as any).isReactNativeWebView === true
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Detects the current embedded environment.
118
+ * Priority: React Native WebView > iframe > standalone
119
+ */
120
+ export function detectEnvironment(): EmbeddedEnvironment {
121
+ const isRNWebView = isRunningInReactNativeWebView();
122
+ const isIframe = isRunningInIframe();
123
+
124
+ if (isRNWebView) {
125
+ return "react-native-webview";
126
+ }
127
+ if (isIframe) {
128
+ return "iframe";
129
+ }
130
+ return "standalone";
131
+ }
132
+
41
133
  export class SessionManager {
42
134
  readonly parentOrigin: string;
43
135
  readonly appId: string;
44
136
  readonly isInIframe: boolean;
137
+ readonly environment: EmbeddedEnvironment;
45
138
  readonly isBypassGatewayLocalOnly: boolean;
46
- /** @deprecated Use isBypassGatewayLocalOnly instead. */
47
- readonly isDemoModeLocalOnly: boolean;
48
139
 
49
140
  private token: SessionToken | null = null;
50
141
  private refreshPromise: Promise<string> | null = null;
142
+ private tokenWaiters: Array<{
143
+ resolve: (token: string) => void;
144
+ reject: (error: Error) => void;
145
+ timeout: ReturnType<typeof setTimeout>;
146
+ }> = [];
51
147
 
52
148
  constructor(config: SessionManagerConfig) {
53
149
  if (!config.appId) {
@@ -55,7 +151,6 @@ export class SessionManager {
55
151
  }
56
152
 
57
153
  this.isBypassGatewayLocalOnly = isBypassGatewayLocalOnlyClient();
58
- this.isDemoModeLocalOnly = this.isBypassGatewayLocalOnly;
59
154
 
60
155
  const gatewayUrl = import.meta.env.VITE_GATEWAY_URL;
61
156
  if (!this.isBypassGatewayLocalOnly) {
@@ -76,8 +171,13 @@ export class SessionManager {
76
171
  this.parentOrigin = this.isBypassGatewayLocalOnly
77
172
  ? window.location.origin
78
173
  : gatewayUrl;
174
+ this.environment = detectEnvironment();
79
175
  this.isInIframe = isRunningInIframe();
80
176
 
177
+ if (this.environment === "react-native-webview") {
178
+ this.setupReactNativeTokenListener();
179
+ }
180
+
81
181
  if (this.isBypassGatewayLocalOnly) {
82
182
  this.token = {
83
183
  token: BYPASS_GATEWAY_LOCAL_ONLY_TOKEN,
@@ -86,6 +186,45 @@ export class SessionManager {
86
186
  }
87
187
  }
88
188
 
189
+ /**
190
+ * Check if running in an embedded environment (iframe or React Native WebView)
191
+ */
192
+ isEmbedded(): boolean {
193
+ return this.environment !== "standalone";
194
+ }
195
+
196
+ isTrustedHostMessage(event: MessageEvent): boolean {
197
+ if (this.environment === "react-native-webview") {
198
+ const origin = (event as MessageEvent & { origin?: string | null })
199
+ .origin;
200
+ return (
201
+ origin === "react-native" ||
202
+ origin === "null" ||
203
+ origin === "" ||
204
+ origin == null
205
+ );
206
+ }
207
+
208
+ return event.origin === this.parentOrigin;
209
+ }
210
+
211
+ postToHost(message: object): void {
212
+ if (this.environment === "react-native-webview") {
213
+ const postMessage = (window as any).ReactNativeWebView?.postMessage;
214
+ if (typeof postMessage !== "function") {
215
+ throw new Error("React Native WebView bridge is unavailable");
216
+ }
217
+
218
+ postMessage.call(
219
+ (window as any).ReactNativeWebView,
220
+ JSON.stringify(message),
221
+ );
222
+ return;
223
+ }
224
+
225
+ window.parent.postMessage(message, this.parentOrigin);
226
+ }
227
+
89
228
  private isTokenExpiringSoon(
90
229
  bufferMs: number = TOKEN_EXPIRY_BUFFER_MS,
91
230
  ): boolean {
@@ -105,19 +244,17 @@ export class SessionManager {
105
244
  };
106
245
 
107
246
  const handler = (event: MessageEvent) => {
108
- // Security: reject messages from wrong origin (including null from sandboxed iframes)
109
- if (event.origin !== this.parentOrigin) return;
110
- // Safety: ignore malformed messages that could crash the handler
111
- if (!event.data || typeof event.data !== "object") return;
112
- if (
113
- event.data.type === responseType &&
114
- event.data.requestId === requestId
115
- ) {
247
+ // Security: validate message origin based on environment
248
+ if (!this.isTrustedHostMessage(event)) return;
249
+ const data = parseMessagePayload(event.data);
250
+ if (!data) return;
251
+
252
+ if (data.type === responseType && data.requestId === requestId) {
116
253
  cleanup();
117
- if (event.data.error) {
118
- reject(new Error(event.data.error));
254
+ if (typeof data.error === "string" && data.error) {
255
+ reject(new Error(data.error));
119
256
  } else {
120
- resolve(event.data as T);
257
+ resolve(data as T);
121
258
  }
122
259
  }
123
260
  };
@@ -128,7 +265,100 @@ export class SessionManager {
128
265
  }, MESSAGE_TIMEOUT_MS);
129
266
 
130
267
  window.addEventListener("message", handler);
131
- window.parent.postMessage(request, this.parentOrigin);
268
+
269
+ try {
270
+ this.postToHost(request);
271
+ } catch (error) {
272
+ cleanup();
273
+ reject(
274
+ error instanceof Error
275
+ ? error
276
+ : new Error("Failed to post message to host"),
277
+ );
278
+ }
279
+ });
280
+ }
281
+
282
+ private setupReactNativeTokenListener(): void {
283
+ window.addEventListener("message", (event: MessageEvent) => {
284
+ if (!this.isTrustedHostMessage(event)) {
285
+ return;
286
+ }
287
+
288
+ const data = parseMessagePayload(event.data);
289
+ if (!data) {
290
+ return;
291
+ }
292
+
293
+ if (!isTokenUpdateMessage(data)) {
294
+ return;
295
+ }
296
+
297
+ const message = data;
298
+
299
+ if (!message.token || typeof message.token !== "string") {
300
+ return;
301
+ }
302
+
303
+ if (typeof message.appId !== "string" || message.appId !== this.appId) {
304
+ return;
305
+ }
306
+
307
+ if (
308
+ message.expiresAt !== undefined &&
309
+ typeof message.expiresAt !== "string"
310
+ ) {
311
+ return;
312
+ }
313
+
314
+ const payload = decodeJwtPayload(message.token);
315
+ if (!payload || !tokenAudienceMatchesApp(payload, this.appId)) {
316
+ return;
317
+ }
318
+
319
+ // Native controls token minting and pushes updates into the embedded app.
320
+ // We only consume pushed tokens in React Native WebView mode.
321
+ let expiresAt = Date.now() + DEFAULT_TOKEN_LIFETIME_MS;
322
+ if (message.expiresAt) {
323
+ const parsed = new Date(message.expiresAt).getTime();
324
+ if (!Number.isNaN(parsed)) {
325
+ expiresAt = parsed;
326
+ }
327
+ }
328
+
329
+ this.token = {
330
+ token: message.token,
331
+ expiresAt,
332
+ };
333
+
334
+ if (this.tokenWaiters.length > 0) {
335
+ const waiters = this.tokenWaiters;
336
+ this.tokenWaiters = [];
337
+
338
+ for (const waiter of waiters) {
339
+ clearTimeout(waiter.timeout);
340
+ waiter.resolve(message.token);
341
+ }
342
+ }
343
+ });
344
+ }
345
+
346
+ private waitForReactNativeTokenPush(): Promise<string> {
347
+ if (this.token && !this.isTokenExpiringSoon()) {
348
+ return Promise.resolve(this.token.token);
349
+ }
350
+
351
+ return new Promise((resolve, reject) => {
352
+ const timeout = setTimeout(() => {
353
+ this.tokenWaiters = this.tokenWaiters.filter(
354
+ (w) => w.timeout !== timeout,
355
+ );
356
+ reject(
357
+ new Error("Timed out waiting for token from React Native bridge"),
358
+ );
359
+ }, REACT_NATIVE_TOKEN_WAIT_TIMEOUT_MS);
360
+
361
+ this.tokenWaiters.push({ resolve, reject, timeout });
132
362
  });
133
363
  }
134
364
 
@@ -145,6 +375,16 @@ export class SessionManager {
145
375
  return this.refreshPromise;
146
376
  }
147
377
 
378
+ if (this.environment === "react-native-webview") {
379
+ this.refreshPromise = this.waitForReactNativeTokenPush();
380
+
381
+ try {
382
+ return await this.refreshPromise;
383
+ } finally {
384
+ this.refreshPromise = null;
385
+ }
386
+ }
387
+
148
388
  this.refreshPromise = (async () => {
149
389
  const requestId = crypto.randomUUID();
150
390
 
@@ -235,23 +475,18 @@ export class SessionManager {
235
475
  return null;
236
476
  }
237
477
 
238
- try {
239
- const parts = this.token.token.split(".");
240
- if (parts.length !== 3) {
241
- return null;
242
- }
243
-
244
- const payload = JSON.parse(atob(parts[1]));
245
- if (!payload.sub) {
246
- return null;
247
- }
478
+ const payload = decodeJwtPayload(this.token.token);
479
+ if (!payload) {
480
+ return null;
481
+ }
248
482
 
249
- return {
250
- userId: payload.sub,
251
- email: payload.email ?? "",
252
- };
253
- } catch {
483
+ if (typeof payload.sub !== "string") {
254
484
  return null;
255
485
  }
486
+
487
+ return {
488
+ userId: payload.sub,
489
+ email: typeof payload.email === "string" ? payload.email : "",
490
+ };
256
491
  }
257
492
  }
@@ -0,0 +1,22 @@
1
+ export type MessagePayload = Record<string, unknown>;
2
+
3
+ export function parseMessagePayload(data: unknown): MessagePayload | null {
4
+ if (data && typeof data === "object" && !Array.isArray(data)) {
5
+ return data as MessagePayload;
6
+ }
7
+
8
+ if (typeof data !== "string") {
9
+ return null;
10
+ }
11
+
12
+ try {
13
+ const parsed = JSON.parse(data);
14
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
15
+ return parsed as MessagePayload;
16
+ }
17
+ } catch {
18
+ return null;
19
+ }
20
+
21
+ return null;
22
+ }
@@ -30,8 +30,11 @@ export function EmbeddedAppProvider({
30
30
 
31
31
  if (!sessionManager) return null;
32
32
 
33
- // Check if the app is running outside of the Gateway iframe (skip in demo mode)
34
- if (!sessionManager.isInIframe && !sessionManager.isBypassGatewayLocalOnly) {
33
+ // Check if the app is running outside of the Gateway (iframe or React Native WebView)
34
+ if (
35
+ !sessionManager.isEmbedded() &&
36
+ !sessionManager.isBypassGatewayLocalOnly
37
+ ) {
35
38
  return (
36
39
  <GatewayRequiredError
37
40
  gatewayOrigin={sessionManager.parentOrigin}
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { shouldBootstrapSession } from "./useEveryAppSession";
3
+
4
+ describe("shouldBootstrapSession", () => {
5
+ it("returns false for standalone apps without bypass", () => {
6
+ expect(
7
+ shouldBootstrapSession({
8
+ isEmbedded: () => false,
9
+ isBypassGatewayLocalOnly: false,
10
+ }),
11
+ ).toBe(false);
12
+ });
13
+
14
+ it("returns true for embedded apps", () => {
15
+ expect(
16
+ shouldBootstrapSession({
17
+ isEmbedded: () => true,
18
+ isBypassGatewayLocalOnly: false,
19
+ }),
20
+ ).toBe(true);
21
+ });
22
+
23
+ it("returns true for local bypass mode", () => {
24
+ expect(
25
+ shouldBootstrapSession({
26
+ isEmbedded: () => false,
27
+ isBypassGatewayLocalOnly: true,
28
+ }),
29
+ ).toBe(true);
30
+ });
31
+
32
+ it("does not depend on iframe detection for RN webviews", () => {
33
+ expect(
34
+ shouldBootstrapSession({
35
+ isEmbedded: () => true,
36
+ isBypassGatewayLocalOnly: false,
37
+ }),
38
+ ).toBe(true);
39
+ });
40
+ });
@@ -9,6 +9,20 @@ interface UseEveryAppSessionParams {
9
9
  sessionManagerConfig: SessionManagerConfig;
10
10
  }
11
11
 
12
+ type SessionBootstrapGate = Pick<
13
+ SessionManager,
14
+ "isEmbedded" | "isBypassGatewayLocalOnly"
15
+ >;
16
+
17
+ export function shouldBootstrapSession(
18
+ sessionManager: SessionBootstrapGate,
19
+ ): boolean {
20
+ // Bootstrapping should happen whenever the app is hosted by a trusted container.
21
+ // This intentionally keys off embedded environment semantics (iframe OR RN WebView),
22
+ // not iframe-only detection, so RN can initialize auth from pushed tokens.
23
+ return sessionManager.isEmbedded() || sessionManager.isBypassGatewayLocalOnly;
24
+ }
25
+
12
26
  export function useEveryAppSession({
13
27
  sessionManagerConfig,
14
28
  }: UseEveryAppSessionParams) {
@@ -28,9 +42,8 @@ export function useEveryAppSession({
28
42
 
29
43
  useEffect(() => {
30
44
  if (!sessionManager) return;
31
- // Skip token requests when not in iframe (unless in demo mode) - the app will show GatewayRequiredError instead
32
- if (!sessionManager.isInIframe && !sessionManager.isBypassGatewayLocalOnly)
33
- return;
45
+ // Skip token bootstrap when not embedded (unless in demo mode) - the app will show GatewayRequiredError instead
46
+ if (!shouldBootstrapSession(sessionManager)) return;
34
47
 
35
48
  const interval = setInterval(() => {
36
49
  setSessionTokenState(sessionManager.getTokenState());
@@ -444,4 +444,39 @@ describe("authenticateRequest", () => {
444
444
  expect(result).toBeNull();
445
445
  });
446
446
  });
447
+
448
+ describe("error logging", () => {
449
+ it("omits stack details in production logs", async () => {
450
+ const originalProd = import.meta.env.PROD;
451
+ import.meta.env.PROD = true;
452
+
453
+ const consoleErrorSpy = vi
454
+ .spyOn(console, "error")
455
+ .mockImplementation(() => undefined);
456
+
457
+ try {
458
+ const request = createRequest("Bearer malformed.token");
459
+ const result = await authenticateRequest(authConfig, request);
460
+
461
+ expect(result).toBeNull();
462
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
463
+
464
+ const [payload] = consoleErrorSpy.mock.calls[0];
465
+ expect(typeof payload).toBe("string");
466
+
467
+ const parsed = JSON.parse(payload as string) as {
468
+ stack?: string;
469
+ message: string;
470
+ issuer: string;
471
+ audience: string;
472
+ };
473
+ expect(parsed.message).toBe("Error verifying session token");
474
+ expect(parsed.issuer).toBe(authConfig.issuer);
475
+ expect(parsed.audience).toBe(authConfig.audience);
476
+ expect(parsed.stack).toBeUndefined();
477
+ } finally {
478
+ import.meta.env.PROD = originalProd;
479
+ }
480
+ });
481
+ });
447
482
  });
@@ -70,11 +70,17 @@ export async function authenticateRequest(
70
70
  const session = await verifySessionToken(token, authConfig);
71
71
  return session;
72
72
  } catch (error) {
73
+ const isProd = import.meta.env.PROD === true;
73
74
  console.error(
74
75
  JSON.stringify({
75
76
  message: "Error verifying session token",
76
77
  error: error instanceof Error ? error.message : String(error),
77
- stack: error instanceof Error ? error.stack : undefined,
78
+ stack:
79
+ isProd === true
80
+ ? undefined
81
+ : error instanceof Error
82
+ ? error.stack
83
+ : undefined,
78
84
  errorType: error instanceof Error ? error.constructor.name : "Unknown",
79
85
  issuer: authConfig.issuer,
80
86
  audience: authConfig.audience,
@@ -1,6 +1,7 @@
1
1
  import { useEffect } from "react";
2
2
  import { SessionManager } from "../core/sessionManager.js";
3
3
  import { useRouter } from "@tanstack/react-router";
4
+ import { parseMessagePayload } from "../shared/parseMessagePayload.js";
4
5
 
5
6
  interface UseEveryAppRouterParams {
6
7
  sessionManager: SessionManager | null;
@@ -11,15 +12,20 @@ export function useEveryAppRouter({ sessionManager }: UseEveryAppRouterParams) {
11
12
  // Route synchronization effect
12
13
  useEffect(() => {
13
14
  if (!sessionManager) return;
15
+
14
16
  // Listen for route sync messages from parent
15
17
  const handleMessage = (event: MessageEvent) => {
16
- if (event.origin !== sessionManager.parentOrigin) return;
18
+ // Validate origin based on environment
19
+ if (!sessionManager.isTrustedHostMessage(event)) return;
20
+
21
+ const data = parseMessagePayload(event.data);
22
+ if (!data) return;
17
23
 
18
24
  if (
19
- event.data.type === "ROUTE_CHANGE" &&
20
- event.data.direction === "parent-to-child"
25
+ data.type === "ROUTE_CHANGE" &&
26
+ data.direction === "parent-to-child"
21
27
  ) {
22
- const targetRoute = event.data.route;
28
+ const targetRoute = typeof data.route === "string" ? data.route : null;
23
29
  const currentRoute = window.location.pathname;
24
30
 
25
31
  // Only navigate if the route is different from current location
@@ -44,16 +50,17 @@ export function useEveryAppRouter({ sessionManager }: UseEveryAppRouterParams) {
44
50
 
45
51
  lastReportedPath = currentPath;
46
52
 
47
- if (window.parent !== window) {
48
- window.parent.postMessage(
49
- {
50
- type: "ROUTE_CHANGE",
51
- route: currentPath,
52
- appId: sessionManager.appId,
53
- direction: "child-to-parent",
54
- },
55
- sessionManager.parentOrigin,
56
- );
53
+ const message = {
54
+ type: "ROUTE_CHANGE",
55
+ route: currentPath,
56
+ appId: sessionManager.appId,
57
+ direction: "child-to-parent",
58
+ };
59
+
60
+ try {
61
+ sessionManager.postToHost(message);
62
+ } catch {
63
+ return;
57
64
  }
58
65
  };
59
66
  // Listen to popstate for browser back/forward