@every-app/sdk 0.0.4 → 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 +90 -179
- 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,85 +3,91 @@ 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
|
-
this.parentOrigin = import.meta.env.VITE_GATEWAY_URL;
|
|
30
|
-
this.messageTimeout = 5000;
|
|
31
|
-
this.debug = config.debug ?? false;
|
|
32
|
-
|
|
33
28
|
if (!config.appId) {
|
|
34
29
|
throw new Error("[SessionManager] appId is required.");
|
|
35
30
|
}
|
|
36
|
-
this.appId = config.appId;
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
);
|
|
32
|
+
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL;
|
|
33
|
+
if (!gatewayUrl) {
|
|
34
|
+
throw new Error("[SessionManager] VITE_GATEWAY_URL env var is required.");
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
try {
|
|
45
|
-
new URL(
|
|
38
|
+
new URL(gatewayUrl);
|
|
46
39
|
} catch {
|
|
47
|
-
throw new Error(
|
|
48
|
-
`[SessionManager] Invalid parent origin URL: ${this.parentOrigin}`,
|
|
49
|
-
);
|
|
40
|
+
throw new Error(`[SessionManager] Invalid gateway URL: ${gatewayUrl}`);
|
|
50
41
|
}
|
|
51
42
|
|
|
52
|
-
this.
|
|
43
|
+
this.appId = config.appId;
|
|
44
|
+
this.parentOrigin = gatewayUrl;
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
59
52
|
}
|
|
60
53
|
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
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
|
+
};
|
|
72
64
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
};
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
const timeout = setTimeout(() => {
|
|
84
|
+
cleanup();
|
|
85
|
+
reject(new Error("Token request timeout - parent did not respond"));
|
|
86
|
+
}, MESSAGE_TIMEOUT_MS);
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
window.addEventListener("message", handler);
|
|
89
|
+
window.parent.postMessage(request, this.parentOrigin);
|
|
90
|
+
});
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
async requestNewToken(): Promise<string> {
|
|
@@ -89,97 +95,39 @@ export class SessionManager {
|
|
|
89
95
|
return this.refreshPromise;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
this.refreshPromise =
|
|
93
|
-
const requestId =
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
);
|
|
104
110
|
|
|
105
|
-
|
|
111
|
+
if (!response.token) {
|
|
112
|
+
throw new Error("No token in response");
|
|
113
|
+
}
|
|
106
114
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
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;
|
|
114
121
|
}
|
|
122
|
+
}
|
|
115
123
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
event.data.type === "SESSION_TOKEN_RESPONSE" &&
|
|
120
|
-
event.data.requestId === requestId
|
|
121
|
-
) {
|
|
122
|
-
clearTimeout(timeout);
|
|
123
|
-
window.removeEventListener("message", messageHandler);
|
|
124
|
-
|
|
125
|
-
if (event.data.error) {
|
|
126
|
-
this.log(`Token request #${requestId} failed`, {
|
|
127
|
-
error: event.data.error,
|
|
128
|
-
});
|
|
129
|
-
const error = new Error(event.data.error);
|
|
130
|
-
this.onError?.(error);
|
|
131
|
-
reject(error);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!event.data.token) {
|
|
136
|
-
this.log(
|
|
137
|
-
`Token request #${requestId} failed - no token in response`,
|
|
138
|
-
);
|
|
139
|
-
const error = new Error("No token in response");
|
|
140
|
-
this.onError?.(error);
|
|
141
|
-
reject(error);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
this.token = {
|
|
146
|
-
token: event.data.token,
|
|
147
|
-
expiresAt: event.data.expiresAt
|
|
148
|
-
? new Date(event.data.expiresAt).getTime()
|
|
149
|
-
: Date.now() + 60000,
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
this.log(`Token #${requestId} received successfully`, {
|
|
153
|
-
expiresAt: new Date(this.token.expiresAt).toISOString(),
|
|
154
|
-
});
|
|
155
|
-
resolve(this.token.token);
|
|
156
|
-
}
|
|
124
|
+
this.token = {
|
|
125
|
+
token: response.token,
|
|
126
|
+
expiresAt,
|
|
157
127
|
};
|
|
158
128
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.log(
|
|
162
|
-
`Requesting new session token #${requestId} for app "${this.appId}"`,
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
// Fire-and-forget postMessage to the parent
|
|
166
|
-
try {
|
|
167
|
-
window.parent.postMessage(
|
|
168
|
-
{
|
|
169
|
-
type: "SESSION_TOKEN_REQUEST",
|
|
170
|
-
requestId: requestId,
|
|
171
|
-
appId: this.appId,
|
|
172
|
-
},
|
|
173
|
-
this.parentOrigin,
|
|
174
|
-
);
|
|
175
|
-
this.log(`Message sent for token request #${requestId}`, {
|
|
176
|
-
targetOrigin: this.parentOrigin,
|
|
177
|
-
});
|
|
178
|
-
} catch (e) {
|
|
179
|
-
this.log(`postMessage failed for token request #${requestId}`, e);
|
|
180
|
-
// We don't reject the promise here because the timeout will handle it
|
|
181
|
-
}
|
|
182
|
-
});
|
|
129
|
+
return this.token.token;
|
|
130
|
+
})();
|
|
183
131
|
|
|
184
132
|
try {
|
|
185
133
|
return await this.refreshPromise;
|
|
@@ -189,27 +137,10 @@ export class SessionManager {
|
|
|
189
137
|
}
|
|
190
138
|
|
|
191
139
|
async getToken(): Promise<string> {
|
|
192
|
-
|
|
193
|
-
if (this.isTokenExpiringSoon() || !this.token) {
|
|
194
|
-
this.log("Token expired or expiring soon, requesting new token", {
|
|
195
|
-
hasToken: !!this.token,
|
|
196
|
-
expiresAt: this.token
|
|
197
|
-
? new Date(this.token.expiresAt).toISOString()
|
|
198
|
-
: "N/A",
|
|
199
|
-
timeUntilExpiry: this.token ? this.token.expiresAt - Date.now() : "N/A",
|
|
200
|
-
});
|
|
140
|
+
if (this.isTokenExpiringSoon()) {
|
|
201
141
|
return this.requestNewToken();
|
|
202
142
|
}
|
|
203
|
-
|
|
204
|
-
return this.token.token;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
getParentOrigin(): string {
|
|
208
|
-
return this.parentOrigin;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
getAppId(): string {
|
|
212
|
-
return this.appId;
|
|
143
|
+
return this.token!.token;
|
|
213
144
|
}
|
|
214
145
|
|
|
215
146
|
getTokenState(): {
|
|
@@ -224,25 +155,13 @@ export class SessionManager {
|
|
|
224
155
|
return { status: "NO_TOKEN", token: null };
|
|
225
156
|
}
|
|
226
157
|
|
|
227
|
-
if (this.
|
|
158
|
+
if (this.isTokenExpiringSoon(0)) {
|
|
228
159
|
return { status: "EXPIRED", token: this.token.token };
|
|
229
160
|
}
|
|
230
161
|
|
|
231
162
|
return { status: "VALID", token: this.token.token };
|
|
232
163
|
}
|
|
233
164
|
|
|
234
|
-
onDebugEvent(callback: () => void): () => void {
|
|
235
|
-
// For now, we'll create a simple event system
|
|
236
|
-
// In a more complete implementation, you might want to emit events at various points
|
|
237
|
-
const intervalId = setInterval(() => {
|
|
238
|
-
// This will trigger the callback periodically to update the UI
|
|
239
|
-
callback();
|
|
240
|
-
}, 5000);
|
|
241
|
-
|
|
242
|
-
// Return an unsubscribe function
|
|
243
|
-
return () => clearInterval(intervalId);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
165
|
/**
|
|
247
166
|
* Extracts user information from the current JWT token.
|
|
248
167
|
* Returns null if no valid token is available.
|
|
@@ -253,20 +172,13 @@ export class SessionManager {
|
|
|
253
172
|
}
|
|
254
173
|
|
|
255
174
|
try {
|
|
256
|
-
// JWT structure: header.payload.signature
|
|
257
175
|
const parts = this.token.token.split(".");
|
|
258
176
|
if (parts.length !== 3) {
|
|
259
|
-
this.log("Invalid JWT format - expected 3 parts", {
|
|
260
|
-
parts: parts.length,
|
|
261
|
-
});
|
|
262
177
|
return null;
|
|
263
178
|
}
|
|
264
179
|
|
|
265
|
-
// Decode the payload (second part)
|
|
266
180
|
const payload = JSON.parse(atob(parts[1]));
|
|
267
|
-
|
|
268
181
|
if (!payload.sub) {
|
|
269
|
-
this.log("JWT payload missing 'sub' claim");
|
|
270
182
|
return null;
|
|
271
183
|
}
|
|
272
184
|
|
|
@@ -274,8 +186,7 @@ export class SessionManager {
|
|
|
274
186
|
userId: payload.sub,
|
|
275
187
|
email: payload.email ?? "",
|
|
276
188
|
};
|
|
277
|
-
} catch
|
|
278
|
-
this.log("Failed to decode JWT", error);
|
|
189
|
+
} catch {
|
|
279
190
|
return null;
|
|
280
191
|
}
|
|
281
192
|
}
|