@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.
@@ -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
- if (!this.parentOrigin) {
39
- throw new Error(
40
- "[SessionManager] Set the Parent Origin by specifying the VITE_GATEWAY_URL env var.",
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(this.parentOrigin);
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.setupMessageListener();
43
+ this.appId = config.appId;
44
+ this.parentOrigin = gatewayUrl;
53
45
  }
54
46
 
55
- private log(message: string, data?: unknown) {
56
- if (this.debug) {
57
- console.log(`[SessionManager - Logger] ${message}`, data);
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 setupMessageListener() {
62
- if (typeof window === "undefined") return;
63
-
64
- window.addEventListener("message", (event) => {
65
- if (event.origin !== this.parentOrigin) {
66
- this.log("Message rejected due to origin mismatch", {
67
- expected: this.parentOrigin,
68
- received: event.origin,
69
- });
70
- return;
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
- this.log("Accepted message from parent", event.data);
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
- private isTokenExpired(): boolean {
78
- if (!this.token) return true;
79
- return Date.now() >= this.token.expiresAt;
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
- private isTokenExpiringSoon(bufferMs: number = 10000): boolean {
83
- if (!this.token) return true;
84
- return Date.now() >= this.token.expiresAt - bufferMs;
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 = new Promise((resolve, reject) => {
93
- const requestId = Date.now().toString();
94
-
95
- const timeout = setTimeout(() => {
96
- this.pendingRequests.delete(requestId);
97
- this.log(`Token request #${requestId} timed out`);
98
- const error = new Error(
99
- "Token refresh timeout - parent did not respond",
100
- );
101
- this.onError?.(error);
102
- reject(error);
103
- }, 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
+ );
104
110
 
105
- this.pendingRequests.set(requestId, { resolve, reject, timeout });
111
+ if (!response.token) {
112
+ throw new Error("No token in response");
113
+ }
106
114
 
107
- const messageHandler = (event: MessageEvent) => {
108
- if (event.origin !== this.parentOrigin) {
109
- this.log("Ignoring message from unexpected origin", {
110
- expected: this.parentOrigin,
111
- received: event.origin,
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
- this.log(`Received message for request #${requestId}`, event.data);
117
-
118
- if (
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
- window.addEventListener("message", messageHandler);
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
- // If token is expired or expiring soon (within 10 seconds), get a new one
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.isTokenExpired()) {
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 (error) {
278
- this.log("Failed to decode JWT", error);
189
+ } catch {
279
190
  return null;
280
191
  }
281
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
  };