@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.
- package/dist/cloudflare/server/gateway.d.ts.map +1 -1
- package/dist/cloudflare/server/gateway.js +10 -4
- 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 +24 -2
- package/dist/core/sessionManager.d.ts.map +1 -1
- package/dist/core/sessionManager.js +191 -27
- 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/authenticateRequest.d.ts.map +1 -1
- package/dist/tanstack/server/authenticateRequest.js +6 -1
- package/dist/tanstack/useEveryAppRouter.d.ts.map +1 -1
- package/dist/tanstack/useEveryAppRouter.js +20 -11
- package/package.json +4 -1
- package/src/cloudflare/server/gateway.test.ts +87 -8
- package/src/cloudflare/server/gateway.ts +13 -4
- package/src/core/index.ts +10 -2
- package/src/core/sessionManager.test.ts +143 -0
- package/src/core/sessionManager.ts +265 -30
- 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 -3
- package/src/tanstack/server/authenticateRequest.test.ts +35 -0
- package/src/tanstack/server/authenticateRequest.ts +7 -1
- package/src/tanstack/useEveryAppRouter.tsx +21 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/cloudflare/server/gateway.ts"],"names":[],"mappings":"AAGA,UAAU,cAAc;IACtB,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxE;AAED,UAAU,UAAU;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,cAAc,CAAC;IACnC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,mBAAmB;IAC3B,GAAG,EAAE,UAAU,CAAC;IAChB;8DAC0D;IAC1D,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;IAC5B,kFAAkF;IAClF,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAMrD;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,EACjC,GAAG,EACH,GAAG,EACH,IAAI,GACL,EAAE,mBAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/cloudflare/server/gateway.ts"],"names":[],"mappings":"AAGA,UAAU,cAAc;IACtB,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxE;AAED,UAAU,UAAU;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,cAAc,CAAC;IACnC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,mBAAmB;IAC3B,GAAG,EAAE,UAAU,CAAC;IAChB;8DAC0D;IAC1D,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;IAC5B,kFAAkF;IAClF,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAMrD;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,EACjC,GAAG,EACH,GAAG,EACH,IAAI,GACL,EAAE,mBAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiBzC"}
|
|
@@ -19,18 +19,24 @@ export async function fetchGateway({ env, url, init, }) {
|
|
|
19
19
|
const gatewayBaseUrl = getGatewayUrl(env);
|
|
20
20
|
const resolvedRequest = toRequest(url, init, gatewayBaseUrl);
|
|
21
21
|
const authenticatedRequest = applyAppTokenAuth(resolvedRequest, env);
|
|
22
|
-
// Use service binding
|
|
23
|
-
//
|
|
24
|
-
|
|
22
|
+
// Use service binding in production for zero-latency internal routing.
|
|
23
|
+
// In local dev wrangler still exposes the binding object, but the target
|
|
24
|
+
// service usually isn't running locally, so we skip it and use HTTP fetch.
|
|
25
|
+
if (import.meta.env.PROD && env.EVERY_APP_GATEWAY) {
|
|
25
26
|
const url = new URL(authenticatedRequest.url);
|
|
26
27
|
const bindingUrl = `${SERVICE_BINDING_ORIGIN}${url.pathname}${url.search}`;
|
|
27
28
|
const bindingRequest = new Request(bindingUrl, authenticatedRequest);
|
|
28
29
|
return env.EVERY_APP_GATEWAY.fetch(bindingRequest);
|
|
29
30
|
}
|
|
30
|
-
//
|
|
31
|
+
// HTTP fetch – used in local dev, or as a fallback when no binding exists
|
|
31
32
|
return fetch(authenticatedRequest);
|
|
32
33
|
}
|
|
33
34
|
function applyAppTokenAuth(request, env) {
|
|
35
|
+
const gatewayOrigin = new URL(getGatewayUrl(env)).origin;
|
|
36
|
+
const requestOrigin = new URL(request.url).origin;
|
|
37
|
+
if (requestOrigin !== gatewayOrigin) {
|
|
38
|
+
throw new Error(`Refusing to send gateway token to non-gateway origin: ${requestOrigin}`);
|
|
39
|
+
}
|
|
34
40
|
const appToken = getGatewayAppApiToken(env);
|
|
35
41
|
if (!appToken) {
|
|
36
42
|
throw new Error("GATEWAY_APP_API_TOKEN is required. Run `npx everyapp app deploy` to provision one.");
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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"}
|
package/dist/core/index.js
CHANGED
|
@@ -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,23 +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;
|
|
13
28
|
readonly isBypassGatewayLocalOnly: boolean;
|
|
14
|
-
/** @deprecated Use isBypassGatewayLocalOnly instead. */
|
|
15
|
-
readonly isDemoModeLocalOnly: boolean;
|
|
16
29
|
private token;
|
|
17
30
|
private refreshPromise;
|
|
31
|
+
private tokenWaiters;
|
|
18
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;
|
|
19
39
|
private isTokenExpiringSoon;
|
|
20
40
|
private postMessageWithResponse;
|
|
41
|
+
private setupReactNativeTokenListener;
|
|
42
|
+
private waitForReactNativeTokenPush;
|
|
21
43
|
requestNewToken(): Promise<string>;
|
|
22
44
|
getToken(): Promise<string>;
|
|
23
45
|
getTokenState(): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionManager.d.ts","sourceRoot":"","sources":["../../src/core/sessionManager.ts"],"names":[],"mappings":"
|
|
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,12 +1,47 @@
|
|
|
1
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
|
+
}
|
|
2
33
|
const MESSAGE_TIMEOUT_MS = 5000;
|
|
3
34
|
const TOKEN_EXPIRY_BUFFER_MS = 10000;
|
|
4
35
|
const DEFAULT_TOKEN_LIFETIME_MS = 60000;
|
|
36
|
+
const REACT_NATIVE_TOKEN_WAIT_TIMEOUT_MS = 10000;
|
|
5
37
|
/**
|
|
6
38
|
* Detects whether the current window is running inside an iframe.
|
|
7
39
|
* Returns true if in an iframe, false if running as top-level window.
|
|
8
40
|
*/
|
|
9
41
|
export function isRunningInIframe() {
|
|
42
|
+
if (typeof window === "undefined") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
10
45
|
try {
|
|
11
46
|
return window.self !== window.top;
|
|
12
47
|
}
|
|
@@ -16,21 +51,46 @@ export function isRunningInIframe() {
|
|
|
16
51
|
return true;
|
|
17
52
|
}
|
|
18
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
|
+
}
|
|
19
80
|
export class SessionManager {
|
|
20
81
|
parentOrigin;
|
|
21
82
|
appId;
|
|
22
83
|
isInIframe;
|
|
84
|
+
environment;
|
|
23
85
|
isBypassGatewayLocalOnly;
|
|
24
|
-
/** @deprecated Use isBypassGatewayLocalOnly instead. */
|
|
25
|
-
isDemoModeLocalOnly;
|
|
26
86
|
token = null;
|
|
27
87
|
refreshPromise = null;
|
|
88
|
+
tokenWaiters = [];
|
|
28
89
|
constructor(config) {
|
|
29
90
|
if (!config.appId) {
|
|
30
91
|
throw new Error("[SessionManager] appId is required.");
|
|
31
92
|
}
|
|
32
93
|
this.isBypassGatewayLocalOnly = isBypassGatewayLocalOnlyClient();
|
|
33
|
-
this.isDemoModeLocalOnly = this.isBypassGatewayLocalOnly;
|
|
34
94
|
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL;
|
|
35
95
|
if (!this.isBypassGatewayLocalOnly) {
|
|
36
96
|
if (!gatewayUrl) {
|
|
@@ -47,7 +107,11 @@ export class SessionManager {
|
|
|
47
107
|
this.parentOrigin = this.isBypassGatewayLocalOnly
|
|
48
108
|
? window.location.origin
|
|
49
109
|
: gatewayUrl;
|
|
110
|
+
this.environment = detectEnvironment();
|
|
50
111
|
this.isInIframe = isRunningInIframe();
|
|
112
|
+
if (this.environment === "react-native-webview") {
|
|
113
|
+
this.setupReactNativeTokenListener();
|
|
114
|
+
}
|
|
51
115
|
if (this.isBypassGatewayLocalOnly) {
|
|
52
116
|
this.token = {
|
|
53
117
|
token: BYPASS_GATEWAY_LOCAL_ONLY_TOKEN,
|
|
@@ -55,6 +119,34 @@ export class SessionManager {
|
|
|
55
119
|
};
|
|
56
120
|
}
|
|
57
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);
|
|
149
|
+
}
|
|
58
150
|
isTokenExpiringSoon(bufferMs = TOKEN_EXPIRY_BUFFER_MS) {
|
|
59
151
|
if (!this.token)
|
|
60
152
|
return true;
|
|
@@ -67,20 +159,19 @@ export class SessionManager {
|
|
|
67
159
|
window.removeEventListener("message", handler);
|
|
68
160
|
};
|
|
69
161
|
const handler = (event) => {
|
|
70
|
-
// Security:
|
|
71
|
-
if (
|
|
162
|
+
// Security: validate message origin based on environment
|
|
163
|
+
if (!this.isTrustedHostMessage(event))
|
|
72
164
|
return;
|
|
73
|
-
|
|
74
|
-
if (!
|
|
165
|
+
const data = parseMessagePayload(event.data);
|
|
166
|
+
if (!data)
|
|
75
167
|
return;
|
|
76
|
-
if (
|
|
77
|
-
event.data.requestId === requestId) {
|
|
168
|
+
if (data.type === responseType && data.requestId === requestId) {
|
|
78
169
|
cleanup();
|
|
79
|
-
if (
|
|
80
|
-
reject(new Error(
|
|
170
|
+
if (typeof data.error === "string" && data.error) {
|
|
171
|
+
reject(new Error(data.error));
|
|
81
172
|
}
|
|
82
173
|
else {
|
|
83
|
-
resolve(
|
|
174
|
+
resolve(data);
|
|
84
175
|
}
|
|
85
176
|
}
|
|
86
177
|
};
|
|
@@ -89,7 +180,77 @@ export class SessionManager {
|
|
|
89
180
|
reject(new Error("Token request timeout - parent did not respond"));
|
|
90
181
|
}, MESSAGE_TIMEOUT_MS);
|
|
91
182
|
window.addEventListener("message", handler);
|
|
92
|
-
|
|
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 });
|
|
93
254
|
});
|
|
94
255
|
}
|
|
95
256
|
async requestNewToken() {
|
|
@@ -103,6 +264,15 @@ export class SessionManager {
|
|
|
103
264
|
if (this.refreshPromise) {
|
|
104
265
|
return this.refreshPromise;
|
|
105
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
|
+
}
|
|
106
276
|
this.refreshPromise = (async () => {
|
|
107
277
|
const requestId = crypto.randomUUID();
|
|
108
278
|
const response = await this.postMessageWithResponse({
|
|
@@ -172,22 +342,16 @@ export class SessionManager {
|
|
|
172
342
|
if (!this.token) {
|
|
173
343
|
return null;
|
|
174
344
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
const payload = JSON.parse(atob(parts[1]));
|
|
181
|
-
if (!payload.sub) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
userId: payload.sub,
|
|
186
|
-
email: payload.email ?? "",
|
|
187
|
-
};
|
|
345
|
+
const payload = decodeJwtPayload(this.token.token);
|
|
346
|
+
if (!payload) {
|
|
347
|
+
return null;
|
|
188
348
|
}
|
|
189
|
-
|
|
349
|
+
if (typeof payload.sub !== "string") {
|
|
190
350
|
return null;
|
|
191
351
|
}
|
|
352
|
+
return {
|
|
353
|
+
userId: payload.sub,
|
|
354
|
+
email: typeof payload.email === "string" ? payload.email : "",
|
|
355
|
+
};
|
|
192
356
|
}
|
|
193
357
|
}
|
|
@@ -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,
|
|
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.
|
|
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;;;;;;
|
|
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
|
|
17
|
-
if (!sessionManager
|
|
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":"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,
|
|
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,CAoDrC;AAyCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK3E"}
|
|
@@ -27,10 +27,15 @@ export async function authenticateRequest(authConfig, providedRequest) {
|
|
|
27
27
|
return session;
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|
|
30
|
+
const isProd = import.meta.env.PROD === true;
|
|
30
31
|
console.error(JSON.stringify({
|
|
31
32
|
message: "Error verifying session token",
|
|
32
33
|
error: error instanceof Error ? error.message : String(error),
|
|
33
|
-
stack:
|
|
34
|
+
stack: isProd === true
|
|
35
|
+
? undefined
|
|
36
|
+
: error instanceof Error
|
|
37
|
+
? error.stack
|
|
38
|
+
: undefined,
|
|
34
39
|
errorType: error instanceof Error ? error.constructor.name : "Unknown",
|
|
35
40
|
issuer: authConfig.issuer,
|
|
36
41
|
audience: authConfig.audience,
|
|
@@ -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;
|
|
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"}
|
|
@@ -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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@every-app/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
"src"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
|
+
"postinstall": "tsc -p tsconfig.build.json",
|
|
33
34
|
"build": "tsc -p tsconfig.build.json",
|
|
35
|
+
"build:prepublish": "pnpm install --ignore-scripts && npm run types:check && npm run build",
|
|
36
|
+
"prepublishOnly": "npm run build:prepublish",
|
|
34
37
|
"types:check": "tsc --noEmit",
|
|
35
38
|
"format:check": "prettier --check .",
|
|
36
39
|
"format:write": "prettier . --write",
|