@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.
@@ -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
- this.parentOrigin = import.meta.env.VITE_GATEWAY_URL;
30
- this.appId = config.appId || this.detectAppId();
31
- this.messageTimeout = 5000;
32
- this.debug = config.debug ?? false;
28
+ if (!config.appId) {
29
+ throw new Error("[SessionManager] appId is required.");
30
+ }
33
31
 
34
- if (!this.parentOrigin) {
35
- throw new Error(
36
- "[SessionManager] Set the Parent Origin by specifying the VITE_GATEWAY_URL env var.",
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(this.parentOrigin);
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.setupMessageListener();
43
+ this.appId = config.appId;
44
+ this.parentOrigin = gatewayUrl;
49
45
  }
50
46
 
51
- private detectAppId(): string {
52
- if (typeof window === "undefined") return "";
53
-
54
- const url = new URL(window.location.href);
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 log(message: string, data?: unknown) {
63
- if (this.debug) {
64
- console.log(`[SessionManager - Logger] ${message}`, data);
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
- private setupMessageListener() {
69
- if (typeof window === "undefined") return;
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
- window.addEventListener("message", (event) => {
72
- if (event.origin !== this.parentOrigin) {
73
- this.log("Message rejected due to origin mismatch", {
74
- expected: this.parentOrigin,
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
- this.log("Accepted message from parent", event.data);
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 = new Promise((resolve, reject) => {
100
- const requestId = Date.now().toString();
101
-
102
- const timeout = setTimeout(() => {
103
- this.pendingRequests.delete(requestId);
104
- this.log(`Token request #${requestId} timed out`);
105
- const error = new Error(
106
- "Token refresh timeout - parent did not respond",
107
- );
108
- this.onError?.(error);
109
- reject(error);
110
- }, this.messageTimeout);
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
- this.pendingRequests.set(requestId, { resolve, reject, timeout });
111
+ if (!response.token) {
112
+ throw new Error("No token in response");
113
+ }
113
114
 
114
- const messageHandler = (event: MessageEvent) => {
115
- if (event.origin !== this.parentOrigin) {
116
- this.log("Ignoring message from unexpected origin", {
117
- expected: this.parentOrigin,
118
- received: event.origin,
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
- this.log(`Received message for request #${requestId}`, event.data);
124
-
125
- if (
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
- window.addEventListener("message", messageHandler);
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
- // If token is expired or expiring soon (within 10 seconds), get a new one
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.isTokenExpired()) {
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 (error) {
285
- this.log("Failed to decode JWT", error);
189
+ } catch {
286
190
  return null;
287
191
  }
288
192
  }
@@ -3,7 +3,6 @@ import { env } from "cloudflare:workers";
3
3
 
4
4
  export function getAuthConfig(): AuthConfig {
5
5
  return {
6
- jwksUrl: `${env.GATEWAY_URL}/api/embedded/jwks`,
7
6
  issuer: env.GATEWAY_URL,
8
7
  audience: import.meta.env.VITE_APP_ID,
9
8
  };