@every-app/sdk 0.0.3 → 0.0.5
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/package.json +7 -3
- package/src/client/_internal/useEveryAppRouter.tsx +3 -3
- package/src/client/_internal/useEveryAppSession.tsx +1 -6
- package/src/client/session-manager.test.ts +796 -0
- package/src/client/session-manager.ts +92 -188
- package/src/server/auth-config.ts +0 -1
- package/src/server/authenticateRequest.test.ts +416 -0
- package/src/server/authenticateRequest.ts +11 -6
- package/src/server/getLocalD1Url.ts +10 -12
- package/src/server/types.ts +0 -4
|
@@ -3,190 +3,131 @@ interface SessionToken {
|
|
|
3
3
|
expiresAt: number;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
interface TokenResponse {
|
|
7
|
+
token: string;
|
|
8
|
+
expiresAt?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
export interface SessionManagerConfig {
|
|
7
13
|
appId: string;
|
|
8
|
-
debug?: boolean;
|
|
9
14
|
}
|
|
10
15
|
|
|
16
|
+
const MESSAGE_TIMEOUT_MS = 5000;
|
|
17
|
+
const TOKEN_EXPIRY_BUFFER_MS = 10000;
|
|
18
|
+
const DEFAULT_TOKEN_LIFETIME_MS = 60000;
|
|
19
|
+
|
|
11
20
|
export class SessionManager {
|
|
21
|
+
readonly parentOrigin: string;
|
|
22
|
+
readonly appId: string;
|
|
23
|
+
|
|
12
24
|
private token: SessionToken | null = null;
|
|
13
25
|
private refreshPromise: Promise<string> | null = null;
|
|
14
|
-
private parentOrigin: string;
|
|
15
|
-
private appId: string;
|
|
16
|
-
private messageTimeout: number;
|
|
17
|
-
private debug: boolean;
|
|
18
|
-
private onError?: (error: Error) => void;
|
|
19
|
-
private pendingRequests = new Map<
|
|
20
|
-
string,
|
|
21
|
-
{
|
|
22
|
-
resolve: (token: string) => void;
|
|
23
|
-
reject: (error: Error) => void;
|
|
24
|
-
timeout: NodeJS.Timeout;
|
|
25
|
-
}
|
|
26
|
-
>();
|
|
27
26
|
|
|
28
27
|
constructor(config: SessionManagerConfig) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.debug = config.debug ?? false;
|
|
28
|
+
if (!config.appId) {
|
|
29
|
+
throw new Error("[SessionManager] appId is required.");
|
|
30
|
+
}
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
32
|
+
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL;
|
|
33
|
+
if (!gatewayUrl) {
|
|
34
|
+
throw new Error("[SessionManager] VITE_GATEWAY_URL env var is required.");
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
try {
|
|
41
|
-
new URL(
|
|
38
|
+
new URL(gatewayUrl);
|
|
42
39
|
} catch {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`[SessionManager] Invalid parent origin URL: ${this.parentOrigin}`,
|
|
45
|
-
);
|
|
40
|
+
throw new Error(`[SessionManager] Invalid gateway URL: ${gatewayUrl}`);
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
this.
|
|
43
|
+
this.appId = config.appId;
|
|
44
|
+
this.parentOrigin = gatewayUrl;
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
url.searchParams.get("appId") ||
|
|
57
|
-
url.hostname.split(".")[0] ||
|
|
58
|
-
"embedded-app"
|
|
59
|
-
);
|
|
47
|
+
private isTokenExpiringSoon(
|
|
48
|
+
bufferMs: number = TOKEN_EXPIRY_BUFFER_MS,
|
|
49
|
+
): boolean {
|
|
50
|
+
if (!this.token) return true;
|
|
51
|
+
return Date.now() >= this.token.expiresAt - bufferMs;
|
|
60
52
|
}
|
|
61
53
|
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
private postMessageWithResponse<T>(
|
|
55
|
+
request: object,
|
|
56
|
+
responseType: string,
|
|
57
|
+
requestId: string,
|
|
58
|
+
): Promise<T> {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const cleanup = () => {
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
window.removeEventListener("message", handler);
|
|
63
|
+
};
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
const handler = (event: MessageEvent) => {
|
|
66
|
+
// Security: reject messages from wrong origin (including null from sandboxed iframes)
|
|
67
|
+
if (event.origin !== this.parentOrigin) return;
|
|
68
|
+
// Safety: ignore malformed messages that could crash the handler
|
|
69
|
+
if (!event.data || typeof event.data !== "object") return;
|
|
70
|
+
if (
|
|
71
|
+
event.data.type === responseType &&
|
|
72
|
+
event.data.requestId === requestId
|
|
73
|
+
) {
|
|
74
|
+
cleanup();
|
|
75
|
+
if (event.data.error) {
|
|
76
|
+
reject(new Error(event.data.error));
|
|
77
|
+
} else {
|
|
78
|
+
resolve(event.data as T);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
received: event.origin,
|
|
76
|
-
});
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
83
|
+
const timeout = setTimeout(() => {
|
|
84
|
+
cleanup();
|
|
85
|
+
reject(new Error("Token request timeout - parent did not respond"));
|
|
86
|
+
}, MESSAGE_TIMEOUT_MS);
|
|
79
87
|
|
|
80
|
-
|
|
88
|
+
window.addEventListener("message", handler);
|
|
89
|
+
window.parent.postMessage(request, this.parentOrigin);
|
|
81
90
|
});
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
private isTokenExpired(): boolean {
|
|
85
|
-
if (!this.token) return true;
|
|
86
|
-
return Date.now() >= this.token.expiresAt;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private isTokenExpiringSoon(bufferMs: number = 10000): boolean {
|
|
90
|
-
if (!this.token) return true;
|
|
91
|
-
return Date.now() >= this.token.expiresAt - bufferMs;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
93
|
async requestNewToken(): Promise<string> {
|
|
95
94
|
if (this.refreshPromise) {
|
|
96
95
|
return this.refreshPromise;
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
this.refreshPromise =
|
|
100
|
-
const requestId =
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
this.refreshPromise = (async () => {
|
|
99
|
+
const requestId = crypto.randomUUID();
|
|
100
|
+
|
|
101
|
+
const response = await this.postMessageWithResponse<TokenResponse>(
|
|
102
|
+
{
|
|
103
|
+
type: "SESSION_TOKEN_REQUEST",
|
|
104
|
+
requestId,
|
|
105
|
+
appId: this.appId,
|
|
106
|
+
},
|
|
107
|
+
"SESSION_TOKEN_RESPONSE",
|
|
108
|
+
requestId,
|
|
109
|
+
);
|
|
111
110
|
|
|
112
|
-
|
|
111
|
+
if (!response.token) {
|
|
112
|
+
throw new Error("No token in response");
|
|
113
|
+
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return;
|
|
115
|
+
// Parse expiresAt, falling back to default lifetime if invalid
|
|
116
|
+
let expiresAt = Date.now() + DEFAULT_TOKEN_LIFETIME_MS;
|
|
117
|
+
if (response.expiresAt) {
|
|
118
|
+
const parsed = new Date(response.expiresAt).getTime();
|
|
119
|
+
if (!Number.isNaN(parsed)) {
|
|
120
|
+
expiresAt = parsed;
|
|
121
121
|
}
|
|
122
|
+
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
event.data.type === "SESSION_TOKEN_RESPONSE" &&
|
|
127
|
-
event.data.requestId === requestId
|
|
128
|
-
) {
|
|
129
|
-
clearTimeout(timeout);
|
|
130
|
-
window.removeEventListener("message", messageHandler);
|
|
131
|
-
|
|
132
|
-
if (event.data.error) {
|
|
133
|
-
this.log(`Token request #${requestId} failed`, {
|
|
134
|
-
error: event.data.error,
|
|
135
|
-
});
|
|
136
|
-
const error = new Error(event.data.error);
|
|
137
|
-
this.onError?.(error);
|
|
138
|
-
reject(error);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (!event.data.token) {
|
|
143
|
-
this.log(
|
|
144
|
-
`Token request #${requestId} failed - no token in response`,
|
|
145
|
-
);
|
|
146
|
-
const error = new Error("No token in response");
|
|
147
|
-
this.onError?.(error);
|
|
148
|
-
reject(error);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
this.token = {
|
|
153
|
-
token: event.data.token,
|
|
154
|
-
expiresAt: event.data.expiresAt
|
|
155
|
-
? new Date(event.data.expiresAt).getTime()
|
|
156
|
-
: Date.now() + 60000,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
this.log(`Token #${requestId} received successfully`, {
|
|
160
|
-
expiresAt: new Date(this.token.expiresAt).toISOString(),
|
|
161
|
-
});
|
|
162
|
-
resolve(this.token.token);
|
|
163
|
-
}
|
|
124
|
+
this.token = {
|
|
125
|
+
token: response.token,
|
|
126
|
+
expiresAt,
|
|
164
127
|
};
|
|
165
128
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.log(
|
|
169
|
-
`Requesting new session token #${requestId} for app "${this.appId}"`,
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Fire-and-forget postMessage to the parent
|
|
173
|
-
try {
|
|
174
|
-
window.parent.postMessage(
|
|
175
|
-
{
|
|
176
|
-
type: "SESSION_TOKEN_REQUEST",
|
|
177
|
-
requestId: requestId,
|
|
178
|
-
appId: this.appId,
|
|
179
|
-
},
|
|
180
|
-
this.parentOrigin,
|
|
181
|
-
);
|
|
182
|
-
this.log(`Message sent for token request #${requestId}`, {
|
|
183
|
-
targetOrigin: this.parentOrigin,
|
|
184
|
-
});
|
|
185
|
-
} catch (e) {
|
|
186
|
-
this.log(`postMessage failed for token request #${requestId}`, e);
|
|
187
|
-
// We don't reject the promise here because the timeout will handle it
|
|
188
|
-
}
|
|
189
|
-
});
|
|
129
|
+
return this.token.token;
|
|
130
|
+
})();
|
|
190
131
|
|
|
191
132
|
try {
|
|
192
133
|
return await this.refreshPromise;
|
|
@@ -196,27 +137,10 @@ export class SessionManager {
|
|
|
196
137
|
}
|
|
197
138
|
|
|
198
139
|
async getToken(): Promise<string> {
|
|
199
|
-
|
|
200
|
-
if (this.isTokenExpiringSoon() || !this.token) {
|
|
201
|
-
this.log("Token expired or expiring soon, requesting new token", {
|
|
202
|
-
hasToken: !!this.token,
|
|
203
|
-
expiresAt: this.token
|
|
204
|
-
? new Date(this.token.expiresAt).toISOString()
|
|
205
|
-
: "N/A",
|
|
206
|
-
timeUntilExpiry: this.token ? this.token.expiresAt - Date.now() : "N/A",
|
|
207
|
-
});
|
|
140
|
+
if (this.isTokenExpiringSoon()) {
|
|
208
141
|
return this.requestNewToken();
|
|
209
142
|
}
|
|
210
|
-
|
|
211
|
-
return this.token.token;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
getParentOrigin(): string {
|
|
215
|
-
return this.parentOrigin;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
getAppId(): string {
|
|
219
|
-
return this.appId;
|
|
143
|
+
return this.token!.token;
|
|
220
144
|
}
|
|
221
145
|
|
|
222
146
|
getTokenState(): {
|
|
@@ -231,25 +155,13 @@ export class SessionManager {
|
|
|
231
155
|
return { status: "NO_TOKEN", token: null };
|
|
232
156
|
}
|
|
233
157
|
|
|
234
|
-
if (this.
|
|
158
|
+
if (this.isTokenExpiringSoon(0)) {
|
|
235
159
|
return { status: "EXPIRED", token: this.token.token };
|
|
236
160
|
}
|
|
237
161
|
|
|
238
162
|
return { status: "VALID", token: this.token.token };
|
|
239
163
|
}
|
|
240
164
|
|
|
241
|
-
onDebugEvent(callback: () => void): () => void {
|
|
242
|
-
// For now, we'll create a simple event system
|
|
243
|
-
// In a more complete implementation, you might want to emit events at various points
|
|
244
|
-
const intervalId = setInterval(() => {
|
|
245
|
-
// This will trigger the callback periodically to update the UI
|
|
246
|
-
callback();
|
|
247
|
-
}, 5000);
|
|
248
|
-
|
|
249
|
-
// Return an unsubscribe function
|
|
250
|
-
return () => clearInterval(intervalId);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
165
|
/**
|
|
254
166
|
* Extracts user information from the current JWT token.
|
|
255
167
|
* Returns null if no valid token is available.
|
|
@@ -260,20 +172,13 @@ export class SessionManager {
|
|
|
260
172
|
}
|
|
261
173
|
|
|
262
174
|
try {
|
|
263
|
-
// JWT structure: header.payload.signature
|
|
264
175
|
const parts = this.token.token.split(".");
|
|
265
176
|
if (parts.length !== 3) {
|
|
266
|
-
this.log("Invalid JWT format - expected 3 parts", {
|
|
267
|
-
parts: parts.length,
|
|
268
|
-
});
|
|
269
177
|
return null;
|
|
270
178
|
}
|
|
271
179
|
|
|
272
|
-
// Decode the payload (second part)
|
|
273
180
|
const payload = JSON.parse(atob(parts[1]));
|
|
274
|
-
|
|
275
181
|
if (!payload.sub) {
|
|
276
|
-
this.log("JWT payload missing 'sub' claim");
|
|
277
182
|
return null;
|
|
278
183
|
}
|
|
279
184
|
|
|
@@ -281,8 +186,7 @@ export class SessionManager {
|
|
|
281
186
|
userId: payload.sub,
|
|
282
187
|
email: payload.email ?? "",
|
|
283
188
|
};
|
|
284
|
-
} catch
|
|
285
|
-
this.log("Failed to decode JWT", error);
|
|
189
|
+
} catch {
|
|
286
190
|
return null;
|
|
287
191
|
}
|
|
288
192
|
}
|