@every-app/sdk 0.1.13 → 0.1.14
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/shared/bypassGatewayLocalOnly.d.ts +2 -1
- package/dist/shared/bypassGatewayLocalOnly.d.ts.map +1 -1
- package/dist/shared/bypassGatewayLocalOnly.js +6 -2
- package/dist/tanstack/server/authenticateRequest.d.ts +2 -0
- package/dist/tanstack/server/authenticateRequest.d.ts.map +1 -1
- package/dist/tanstack/server/authenticateRequest.js +75 -3
- package/package.json +3 -3
- package/src/cloudflare/getLocalD1Url.ts +0 -64
- package/src/cloudflare/index.ts +0 -1
- package/src/cloudflare/lazyInit.ts +0 -67
- package/src/cloudflare/server/gateway.test.ts +0 -262
- package/src/cloudflare/server/gateway.ts +0 -114
- package/src/cloudflare/server/index.ts +0 -2
- package/src/core/authenticatedFetch.ts +0 -54
- package/src/core/index.ts +0 -12
- package/src/core/sessionManager.test.ts +0 -939
- package/src/core/sessionManager.ts +0 -492
- package/src/env.d.ts +0 -13
- package/src/shared/bypassGatewayLocalOnly.ts +0 -55
- package/src/shared/parseMessagePayload.ts +0 -22
- package/src/tanstack/EmbeddedAppProvider.tsx +0 -96
- package/src/tanstack/GatewayRequiredError.tsx +0 -150
- package/src/tanstack/_internal/useEveryAppSession.test.ts +0 -40
- package/src/tanstack/_internal/useEveryAppSession.tsx +0 -74
- package/src/tanstack/index.ts +0 -3
- package/src/tanstack/server/authConfig.ts +0 -19
- package/src/tanstack/server/authenticateRequest.test.ts +0 -482
- package/src/tanstack/server/authenticateRequest.ts +0 -143
- package/src/tanstack/server/index.ts +0 -3
- package/src/tanstack/server/types.ts +0 -4
- package/src/tanstack/useEveryAppRouter.tsx +0 -83
- package/src/tanstack/useSessionTokenClientMiddleware.ts +0 -43
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BYPASS_GATEWAY_LOCAL_ONLY_EMAIL,
|
|
3
|
-
BYPASS_GATEWAY_LOCAL_ONLY_TOKEN,
|
|
4
|
-
BYPASS_GATEWAY_LOCAL_ONLY_USER_ID,
|
|
5
|
-
isBypassGatewayLocalOnlyClient,
|
|
6
|
-
} from "../shared/bypassGatewayLocalOnly.js";
|
|
7
|
-
import { parseMessagePayload } from "../shared/parseMessagePayload.js";
|
|
8
|
-
|
|
9
|
-
interface SessionToken {
|
|
10
|
-
token: string;
|
|
11
|
-
expiresAt: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface TokenResponse {
|
|
15
|
-
token: string;
|
|
16
|
-
expiresAt?: string;
|
|
17
|
-
error?: string;
|
|
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
|
-
|
|
66
|
-
export interface SessionManagerConfig {
|
|
67
|
-
appId: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const MESSAGE_TIMEOUT_MS = 5000;
|
|
71
|
-
const TOKEN_EXPIRY_BUFFER_MS = 10000;
|
|
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";
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Detects whether the current window is running inside an iframe.
|
|
85
|
-
* Returns true if in an iframe, false if running as top-level window.
|
|
86
|
-
*/
|
|
87
|
-
export function isRunningInIframe(): boolean {
|
|
88
|
-
if (typeof window === "undefined") {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
return window.self !== window.top;
|
|
94
|
-
} catch {
|
|
95
|
-
// If we can't access window.top due to cross-origin restrictions,
|
|
96
|
-
// we're definitely in an iframe
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
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
|
-
|
|
133
|
-
export class SessionManager {
|
|
134
|
-
readonly parentOrigin: string;
|
|
135
|
-
readonly appId: string;
|
|
136
|
-
readonly isInIframe: boolean;
|
|
137
|
-
readonly environment: EmbeddedEnvironment;
|
|
138
|
-
readonly isBypassGatewayLocalOnly: boolean;
|
|
139
|
-
|
|
140
|
-
private token: SessionToken | null = null;
|
|
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
|
-
}> = [];
|
|
147
|
-
|
|
148
|
-
constructor(config: SessionManagerConfig) {
|
|
149
|
-
if (!config.appId) {
|
|
150
|
-
throw new Error("[SessionManager] appId is required.");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
this.isBypassGatewayLocalOnly = isBypassGatewayLocalOnlyClient();
|
|
154
|
-
|
|
155
|
-
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL;
|
|
156
|
-
if (!this.isBypassGatewayLocalOnly) {
|
|
157
|
-
if (!gatewayUrl) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
"[SessionManager] VITE_GATEWAY_URL env var is required.",
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
new URL(gatewayUrl);
|
|
165
|
-
} catch {
|
|
166
|
-
throw new Error(`[SessionManager] Invalid gateway URL: ${gatewayUrl}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
this.appId = config.appId;
|
|
171
|
-
this.parentOrigin = this.isBypassGatewayLocalOnly
|
|
172
|
-
? window.location.origin
|
|
173
|
-
: gatewayUrl;
|
|
174
|
-
this.environment = detectEnvironment();
|
|
175
|
-
this.isInIframe = isRunningInIframe();
|
|
176
|
-
|
|
177
|
-
if (this.environment === "react-native-webview") {
|
|
178
|
-
this.setupReactNativeTokenListener();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (this.isBypassGatewayLocalOnly) {
|
|
182
|
-
this.token = {
|
|
183
|
-
token: BYPASS_GATEWAY_LOCAL_ONLY_TOKEN,
|
|
184
|
-
expiresAt: Date.now() + DEFAULT_TOKEN_LIFETIME_MS,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
}
|
|
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
|
-
|
|
228
|
-
private isTokenExpiringSoon(
|
|
229
|
-
bufferMs: number = TOKEN_EXPIRY_BUFFER_MS,
|
|
230
|
-
): boolean {
|
|
231
|
-
if (!this.token) return true;
|
|
232
|
-
return Date.now() >= this.token.expiresAt - bufferMs;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private postMessageWithResponse<T>(
|
|
236
|
-
request: object,
|
|
237
|
-
responseType: string,
|
|
238
|
-
requestId: string,
|
|
239
|
-
): Promise<T> {
|
|
240
|
-
return new Promise((resolve, reject) => {
|
|
241
|
-
const cleanup = () => {
|
|
242
|
-
clearTimeout(timeout);
|
|
243
|
-
window.removeEventListener("message", handler);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const handler = (event: MessageEvent) => {
|
|
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) {
|
|
253
|
-
cleanup();
|
|
254
|
-
if (typeof data.error === "string" && data.error) {
|
|
255
|
-
reject(new Error(data.error));
|
|
256
|
-
} else {
|
|
257
|
-
resolve(data as T);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const timeout = setTimeout(() => {
|
|
263
|
-
cleanup();
|
|
264
|
-
reject(new Error("Token request timeout - parent did not respond"));
|
|
265
|
-
}, MESSAGE_TIMEOUT_MS);
|
|
266
|
-
|
|
267
|
-
window.addEventListener("message", handler);
|
|
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 });
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
async requestNewToken(): Promise<string> {
|
|
366
|
-
if (this.isBypassGatewayLocalOnly) {
|
|
367
|
-
this.token = {
|
|
368
|
-
token: BYPASS_GATEWAY_LOCAL_ONLY_TOKEN,
|
|
369
|
-
expiresAt: Date.now() + DEFAULT_TOKEN_LIFETIME_MS,
|
|
370
|
-
};
|
|
371
|
-
return this.token.token;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (this.refreshPromise) {
|
|
375
|
-
return this.refreshPromise;
|
|
376
|
-
}
|
|
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
|
-
|
|
388
|
-
this.refreshPromise = (async () => {
|
|
389
|
-
const requestId = crypto.randomUUID();
|
|
390
|
-
|
|
391
|
-
const response = await this.postMessageWithResponse<TokenResponse>(
|
|
392
|
-
{
|
|
393
|
-
type: "SESSION_TOKEN_REQUEST",
|
|
394
|
-
requestId,
|
|
395
|
-
appId: this.appId,
|
|
396
|
-
},
|
|
397
|
-
"SESSION_TOKEN_RESPONSE",
|
|
398
|
-
requestId,
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
if (!response.token) {
|
|
402
|
-
throw new Error("No token in response");
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Parse expiresAt, falling back to default lifetime if invalid
|
|
406
|
-
let expiresAt = Date.now() + DEFAULT_TOKEN_LIFETIME_MS;
|
|
407
|
-
if (response.expiresAt) {
|
|
408
|
-
const parsed = new Date(response.expiresAt).getTime();
|
|
409
|
-
if (!Number.isNaN(parsed)) {
|
|
410
|
-
expiresAt = parsed;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
this.token = {
|
|
415
|
-
token: response.token,
|
|
416
|
-
expiresAt,
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
return this.token.token;
|
|
420
|
-
})();
|
|
421
|
-
|
|
422
|
-
try {
|
|
423
|
-
return await this.refreshPromise;
|
|
424
|
-
} finally {
|
|
425
|
-
this.refreshPromise = null;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
async getToken(): Promise<string> {
|
|
430
|
-
if (this.isBypassGatewayLocalOnly) {
|
|
431
|
-
if (!this.token || this.isTokenExpiringSoon()) {
|
|
432
|
-
return this.requestNewToken();
|
|
433
|
-
}
|
|
434
|
-
return this.token.token;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (this.isTokenExpiringSoon()) {
|
|
438
|
-
return this.requestNewToken();
|
|
439
|
-
}
|
|
440
|
-
return this.token!.token;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
getTokenState(): {
|
|
444
|
-
status: "NO_TOKEN" | "VALID" | "EXPIRED" | "REFRESHING";
|
|
445
|
-
token: string | null;
|
|
446
|
-
} {
|
|
447
|
-
if (this.refreshPromise) {
|
|
448
|
-
return { status: "REFRESHING", token: null };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (!this.token) {
|
|
452
|
-
return { status: "NO_TOKEN", token: null };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (this.isTokenExpiringSoon(0)) {
|
|
456
|
-
return { status: "EXPIRED", token: this.token.token };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return { status: "VALID", token: this.token.token };
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Extracts user information from the current JWT token.
|
|
464
|
-
* Returns null if no valid token is available.
|
|
465
|
-
*/
|
|
466
|
-
getUser(): { userId: string; email: string } | null {
|
|
467
|
-
if (this.isBypassGatewayLocalOnly) {
|
|
468
|
-
return {
|
|
469
|
-
userId: BYPASS_GATEWAY_LOCAL_ONLY_USER_ID,
|
|
470
|
-
email: BYPASS_GATEWAY_LOCAL_ONLY_EMAIL,
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (!this.token) {
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const payload = decodeJwtPayload(this.token.token);
|
|
479
|
-
if (!payload) {
|
|
480
|
-
return null;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (typeof payload.sub !== "string") {
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
return {
|
|
488
|
-
userId: payload.sub,
|
|
489
|
-
email: typeof payload.email === "string" ? payload.email : "",
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
}
|
package/src/env.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// Type definitions for environment variables expected by the SDK
|
|
2
|
-
// Apps using this SDK should have these defined in their wrangler configuration
|
|
3
|
-
|
|
4
|
-
declare namespace Cloudflare {
|
|
5
|
-
interface Env {
|
|
6
|
-
GATEWAY_URL: string;
|
|
7
|
-
EVERY_APP_GATEWAY?: Fetcher;
|
|
8
|
-
GATEWAY_APP_API_TOKEN?: string;
|
|
9
|
-
APP_TOKEN?: string;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface Env extends Cloudflare.Env {}
|
|
@@ -1,55 +0,0 @@
|
|
|
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
|
-
|
|
5
|
-
export function isBypassGatewayLocalOnlyClient(): boolean {
|
|
6
|
-
if (import.meta.env.PROD) {
|
|
7
|
-
return false;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const metaEnv = (import.meta as { env?: Record<string, string | undefined> })
|
|
11
|
-
.env;
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
metaEnv?.VITE_BYPASS_GATEWAY_LOCAL_ONLY === "true" ||
|
|
15
|
-
metaEnv?.BYPASS_GATEWAY_LOCAL_ONLY === "true"
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isBypassGatewayLocalOnlyServer(): boolean {
|
|
20
|
-
if (import.meta.env.PROD) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const metaEnv = (import.meta as { env?: Record<string, string | undefined> })
|
|
25
|
-
.env;
|
|
26
|
-
const metaValue =
|
|
27
|
-
metaEnv?.BYPASS_GATEWAY_LOCAL_ONLY ??
|
|
28
|
-
metaEnv?.VITE_BYPASS_GATEWAY_LOCAL_ONLY;
|
|
29
|
-
if (metaValue === "true") {
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
typeof process !== "undefined" &&
|
|
35
|
-
process.env?.BYPASS_GATEWAY_LOCAL_ONLY === "true"
|
|
36
|
-
) {
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function createBypassGatewayLocalOnlySessionPayload(audience: string) {
|
|
44
|
-
const issuedAt = Math.floor(Date.now() / 1000);
|
|
45
|
-
const expiresAt = issuedAt + 60 * 60;
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
sub: BYPASS_GATEWAY_LOCAL_ONLY_USER_ID,
|
|
49
|
-
email: BYPASS_GATEWAY_LOCAL_ONLY_EMAIL,
|
|
50
|
-
iss: "local",
|
|
51
|
-
aud: audience,
|
|
52
|
-
iat: issuedAt,
|
|
53
|
-
exp: expiresAt,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useMemo } from "react";
|
|
2
|
-
import {
|
|
3
|
-
SessionManager,
|
|
4
|
-
SessionManagerConfig,
|
|
5
|
-
} from "../core/sessionManager.js";
|
|
6
|
-
import { useEveryAppSession } from "./_internal/useEveryAppSession.js";
|
|
7
|
-
import { useEveryAppRouter } from "./useEveryAppRouter.js";
|
|
8
|
-
import { GatewayRequiredError } from "./GatewayRequiredError.js";
|
|
9
|
-
|
|
10
|
-
interface EmbeddedProviderConfig extends SessionManagerConfig {
|
|
11
|
-
children: React.ReactNode;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface EmbeddedAppContextValue {
|
|
15
|
-
sessionManager: SessionManager;
|
|
16
|
-
isAuthenticated: boolean;
|
|
17
|
-
sessionTokenState: ReturnType<SessionManager["getTokenState"]>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const EmbeddedAppContext = createContext<EmbeddedAppContextValue | null>(null);
|
|
21
|
-
|
|
22
|
-
export function EmbeddedAppProvider({
|
|
23
|
-
children,
|
|
24
|
-
...config
|
|
25
|
-
}: EmbeddedProviderConfig) {
|
|
26
|
-
const { sessionManager, sessionTokenState } = useEveryAppSession({
|
|
27
|
-
sessionManagerConfig: config,
|
|
28
|
-
});
|
|
29
|
-
useEveryAppRouter({ sessionManager });
|
|
30
|
-
|
|
31
|
-
if (!sessionManager) return null;
|
|
32
|
-
|
|
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
|
-
) {
|
|
38
|
-
return (
|
|
39
|
-
<GatewayRequiredError
|
|
40
|
-
gatewayOrigin={sessionManager.parentOrigin}
|
|
41
|
-
appId={config.appId}
|
|
42
|
-
/>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const value: EmbeddedAppContextValue = {
|
|
47
|
-
sessionManager,
|
|
48
|
-
isAuthenticated: sessionTokenState.status === "VALID",
|
|
49
|
-
sessionTokenState,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<EmbeddedAppContext.Provider value={value}>
|
|
54
|
-
{children}
|
|
55
|
-
</EmbeddedAppContext.Provider>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Hook to get the current authenticated user.
|
|
61
|
-
* Returns the user's ID and email extracted from the JWT token,
|
|
62
|
-
* or null if not authenticated.
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* ```tsx
|
|
66
|
-
* function MyComponent() {
|
|
67
|
-
* const user = useCurrentUser();
|
|
68
|
-
*
|
|
69
|
-
* if (!user) {
|
|
70
|
-
* return <div>Not authenticated</div>;
|
|
71
|
-
* }
|
|
72
|
-
*
|
|
73
|
-
* return <div>Welcome, {user.email}</div>;
|
|
74
|
-
* }
|
|
75
|
-
* ```
|
|
76
|
-
*/
|
|
77
|
-
export function useCurrentUser(): { userId: string; email: string } | null {
|
|
78
|
-
const context = useContext(EmbeddedAppContext);
|
|
79
|
-
|
|
80
|
-
if (!context) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
"useCurrentUser must be used within an EmbeddedAppProvider",
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const { sessionManager, sessionTokenState } = context;
|
|
87
|
-
|
|
88
|
-
return useMemo(() => {
|
|
89
|
-
// Only return user if we have a valid token
|
|
90
|
-
if (sessionTokenState.status !== "VALID") {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return sessionManager.getUser();
|
|
95
|
-
}, [sessionManager, sessionTokenState]);
|
|
96
|
-
}
|