@civic/auth 0.6.0-beta.3 → 0.6.1-beta.1

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.
Files changed (70) hide show
  1. package/dist/shared/components/CivicAuthIframeContainer.js +1 -1
  2. package/dist/shared/components/CivicAuthIframeContainer.js.map +1 -1
  3. package/dist/shared/lib/BrowserAuthenticationRefresher.d.ts +7 -1
  4. package/dist/shared/lib/BrowserAuthenticationRefresher.d.ts.map +1 -1
  5. package/dist/shared/lib/BrowserAuthenticationRefresher.js +15 -2
  6. package/dist/shared/lib/BrowserAuthenticationRefresher.js.map +1 -1
  7. package/dist/shared/lib/util.d.ts +1 -1
  8. package/dist/shared/lib/util.d.ts.map +1 -1
  9. package/dist/shared/lib/util.js +6 -1
  10. package/dist/shared/lib/util.js.map +1 -1
  11. package/dist/shared/version.d.ts +1 -1
  12. package/dist/shared/version.js +1 -1
  13. package/dist/shared/version.js.map +1 -1
  14. package/dist/vanillajs/auth/AuthenticationEvents.d.ts +11 -0
  15. package/dist/vanillajs/auth/AuthenticationEvents.d.ts.map +1 -0
  16. package/dist/vanillajs/auth/AuthenticationEvents.js +36 -0
  17. package/dist/vanillajs/auth/AuthenticationEvents.js.map +1 -0
  18. package/dist/vanillajs/auth/CivicAuth.d.ts +205 -0
  19. package/dist/vanillajs/auth/CivicAuth.d.ts.map +1 -0
  20. package/dist/vanillajs/auth/CivicAuth.js +689 -0
  21. package/dist/vanillajs/auth/CivicAuth.js.map +1 -0
  22. package/dist/vanillajs/auth/OAuthCallbackHandler.d.ts +90 -0
  23. package/dist/vanillajs/auth/OAuthCallbackHandler.d.ts.map +1 -0
  24. package/dist/vanillajs/auth/OAuthCallbackHandler.js +143 -0
  25. package/dist/vanillajs/auth/OAuthCallbackHandler.js.map +1 -0
  26. package/dist/vanillajs/auth/SessionManager.d.ts +48 -0
  27. package/dist/vanillajs/auth/SessionManager.d.ts.map +1 -0
  28. package/dist/vanillajs/auth/SessionManager.js +121 -0
  29. package/dist/vanillajs/auth/SessionManager.js.map +1 -0
  30. package/dist/vanillajs/auth/TokenRefresher.d.ts +54 -0
  31. package/dist/vanillajs/auth/TokenRefresher.d.ts.map +1 -0
  32. package/dist/vanillajs/auth/TokenRefresher.js +166 -0
  33. package/dist/vanillajs/auth/TokenRefresher.js.map +1 -0
  34. package/dist/vanillajs/iframe/IframeManager.d.ts +82 -0
  35. package/dist/vanillajs/iframe/IframeManager.d.ts.map +1 -0
  36. package/dist/vanillajs/iframe/IframeManager.js +487 -0
  37. package/dist/vanillajs/iframe/IframeManager.js.map +1 -0
  38. package/dist/vanillajs/iframe/IframeResizer.d.ts +15 -0
  39. package/dist/vanillajs/iframe/IframeResizer.d.ts.map +1 -0
  40. package/dist/vanillajs/iframe/IframeResizer.js +127 -0
  41. package/dist/vanillajs/iframe/IframeResizer.js.map +1 -0
  42. package/dist/vanillajs/iframe/SignalObserver.d.ts +33 -0
  43. package/dist/vanillajs/iframe/SignalObserver.d.ts.map +1 -0
  44. package/dist/vanillajs/iframe/SignalObserver.js +162 -0
  45. package/dist/vanillajs/iframe/SignalObserver.js.map +1 -0
  46. package/dist/vanillajs/index.d.ts +17 -0
  47. package/dist/vanillajs/index.d.ts.map +1 -0
  48. package/dist/vanillajs/index.js +18 -0
  49. package/dist/vanillajs/index.js.map +1 -0
  50. package/dist/vanillajs/services/ApiService.d.ts +22 -0
  51. package/dist/vanillajs/services/ApiService.d.ts.map +1 -0
  52. package/dist/vanillajs/services/ApiService.js +82 -0
  53. package/dist/vanillajs/services/ApiService.js.map +1 -0
  54. package/dist/vanillajs/types/index.d.ts +28 -0
  55. package/dist/vanillajs/types/index.d.ts.map +1 -0
  56. package/dist/vanillajs/types/index.js +14 -0
  57. package/dist/vanillajs/types/index.js.map +1 -0
  58. package/dist/vanillajs/ui/LoadingComponents.d.ts +51 -0
  59. package/dist/vanillajs/ui/LoadingComponents.d.ts.map +1 -0
  60. package/dist/vanillajs/ui/LoadingComponents.js +363 -0
  61. package/dist/vanillajs/ui/LoadingComponents.js.map +1 -0
  62. package/dist/vanillajs/utils/auth-utils.d.ts +13 -0
  63. package/dist/vanillajs/utils/auth-utils.d.ts.map +1 -0
  64. package/dist/vanillajs/utils/auth-utils.js +15 -0
  65. package/dist/vanillajs/utils/auth-utils.js.map +1 -0
  66. package/dist/vanillajs/utils/logger.d.ts +29 -0
  67. package/dist/vanillajs/utils/logger.d.ts.map +1 -0
  68. package/dist/vanillajs/utils/logger.js +62 -0
  69. package/dist/vanillajs/utils/logger.js.map +1 -0
  70. package/package.json +1 -1
@@ -0,0 +1,689 @@
1
+ import { AuthEvent } from "../types/index.js";
2
+ import { LocalStorageAdapter } from "../../browser/storage.js";
3
+ import { handleOAuthRedirectPage } from "./OAuthCallbackHandler.js";
4
+ import { buildAuthUrl } from "../utils/auth-utils.js";
5
+ import { createLogger, configureLogging, setCurrentLogger, } from "../utils/logger.js";
6
+ import { SignalObserver } from "../iframe/SignalObserver.js";
7
+ import { GenericPublicClientPKCEProducer } from "../../services/PKCE.js";
8
+ import { generateState } from "../../lib/oauth.js";
9
+ import { SessionManager } from "./SessionManager.js";
10
+ import { IframeManager } from "../iframe/IframeManager.js";
11
+ /**
12
+ * Error codes for CivicAuth errors
13
+ */
14
+ export var CivicAuthErrorCode;
15
+ (function (CivicAuthErrorCode) {
16
+ CivicAuthErrorCode["CONFIG_REQUIRED"] = "CONFIG_REQUIRED";
17
+ CivicAuthErrorCode["INIT_FAILED"] = "INIT_FAILED";
18
+ CivicAuthErrorCode["ENDPOINTS_NOT_INITIALIZED"] = "ENDPOINTS_NOT_INITIALIZED";
19
+ CivicAuthErrorCode["CONTAINER_NOT_FOUND"] = "CONTAINER_NOT_FOUND";
20
+ CivicAuthErrorCode["AUTH_PROCESS_TIMEOUT"] = "AUTH_PROCESS_TIMEOUT";
21
+ CivicAuthErrorCode["IFRAME_LOAD_ERROR"] = "IFRAME_LOAD_ERROR";
22
+ CivicAuthErrorCode["INVALID_MESSAGE"] = "INVALID_MESSAGE";
23
+ })(CivicAuthErrorCode || (CivicAuthErrorCode = {}));
24
+ /**
25
+ * Constants for the auth client
26
+ */
27
+ const CONSTANTS = {
28
+ DEFAULT_IFRAME_ID: "civic-auth-iframe",
29
+ DEFAULT_AUTH_PROCESS_TIMEOUT: 60000, // 60 seconds
30
+ SUCCESS_SIGNAL_ID: "civic-auth-success-signal",
31
+ ERROR_SIGNAL_ID: "civic-auth-error-signal",
32
+ };
33
+ class CivicAuthError extends Error {
34
+ code;
35
+ constructor(message, code) {
36
+ super(message);
37
+ this.code = code;
38
+ this.name = "CivicAuthError";
39
+ }
40
+ }
41
+ /**
42
+ * Process the configuration with defaults
43
+ */
44
+ function processConfigWithDefaults(config) {
45
+ const loggingConfig = {
46
+ enabled: true,
47
+ namespace: "iframe",
48
+ level: "debug",
49
+ ...config.logging,
50
+ };
51
+ return {
52
+ ...config,
53
+ displayMode: config.displayMode || "iframe",
54
+ authProcessTimeout: config.authProcessTimeout || CONSTANTS.DEFAULT_AUTH_PROCESS_TIMEOUT,
55
+ iframeId: config.iframeId || CONSTANTS.DEFAULT_IFRAME_ID,
56
+ logging: loggingConfig,
57
+ storageAdapter: config.storageAdapter || new LocalStorageAdapter(),
58
+ };
59
+ }
60
+ /**
61
+ * CivicAuth client for handling OAuth authentication
62
+ */
63
+ export class CivicAuth {
64
+ /**
65
+ * Internal configuration with all optional properties resolved to required ones.
66
+ *
67
+ * We extend CivicAuthClientConfig rather than making these properties required
68
+ * in the public interface to maintain better DX (optional properties) while
69
+ * ensuring type safety internally after defaults are applied.
70
+ */
71
+ config;
72
+ iframeElement;
73
+ observer;
74
+ authPromise;
75
+ authPromiseResolve;
76
+ authPromiseReject;
77
+ authProcessTimeoutHandle;
78
+ messageEventHandler;
79
+ storage;
80
+ endpoints;
81
+ logger;
82
+ sessionManager;
83
+ iframeManager;
84
+ /**
85
+ * Private constructor for CivicAuth.
86
+ * Use {@link CivicAuth.create} to create a new instance.
87
+ * @param config - Configuration options for the auth client
88
+ * @throws {CivicAuthError} If required configuration is missing
89
+ * @private
90
+ */
91
+ constructor(config) {
92
+ // Process config with defaults
93
+ this.config = processConfigWithDefaults(config);
94
+ // Configure logging based on config
95
+ configureLogging(this.config.logging);
96
+ // Initialize logger based on config
97
+ if (this.config.logging?.enabled) {
98
+ const namespace = this.config.logging.namespace || "iframe";
99
+ this.logger = createLogger(namespace);
100
+ }
101
+ else {
102
+ // Create a no-op logger when logging is disabled
103
+ this.logger = {
104
+ debug: () => { },
105
+ info: () => { },
106
+ warn: () => { },
107
+ error: () => { },
108
+ };
109
+ }
110
+ // Set this logger as the current logger
111
+ setCurrentLogger(this.logger);
112
+ // Use the storage adapter from processed config (guaranteed to be present)
113
+ this.storage = this.config.storageAdapter;
114
+ // Initialize SessionManager if events are provided
115
+ if (config.events) {
116
+ this.sessionManager = new SessionManager(this.storage, config.events);
117
+ }
118
+ // Validate required configuration
119
+ const requiredConfigs = [
120
+ { key: "clientId", value: config.clientId },
121
+ { key: "targetContainerElement", value: config.targetContainerElement },
122
+ { key: "textSignals.success", value: config.textSignals?.success },
123
+ ];
124
+ for (const { key, value } of requiredConfigs) {
125
+ if (!value) {
126
+ throw new CivicAuthError(`CivicAuth: ${key} is required.`, CivicAuthErrorCode.CONFIG_REQUIRED);
127
+ }
128
+ }
129
+ this.messageEventHandler = this.handleIframeMessage.bind(this);
130
+ }
131
+ /**
132
+ * Creates and initializes a new instance of CivicAuth.
133
+ * This is the recommended way to create a CivicAuth instance.
134
+ *
135
+ * @param config - Configuration options for the auth client
136
+ * @returns A promise that resolves with the initialized CivicAuth instance
137
+ * @throws {CivicAuthError} If initialization fails or required configuration is missing
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const auth = await CivicAuth.create({
142
+ * clientId: "your-client-id",
143
+ * redirectUrl: "https://your-app.com/callback",
144
+ * oauthServerBaseUrl: "https://auth-server.com/",
145
+ * scopes: ["openid", "profile"],
146
+ * targetContainerElement: "auth-container",
147
+ * textSignals: {
148
+ * success: "Authentication successful!"
149
+ * }
150
+ * });
151
+ * ```
152
+ */
153
+ static async create(config) {
154
+ const instance = new CivicAuth(config);
155
+ await instance.init();
156
+ return instance;
157
+ }
158
+ /**
159
+ * Initializes the auth client and checks for callback handling
160
+ * @throws {CivicAuthError} If initialization fails
161
+ */
162
+ async init() {
163
+ try {
164
+ // Get OAuth endpoints from well-known configuration
165
+ this.endpoints = {
166
+ auth: `${this.config.oauthServerBaseUrl}auth`,
167
+ token: `${this.config.oauthServerBaseUrl}token`,
168
+ jwks: `${this.config.oauthServerBaseUrl}jwks`,
169
+ userinfo: `${this.config.oauthServerBaseUrl}userinfo`,
170
+ endsession: `${this.config.oauthServerBaseUrl}endsession`,
171
+ };
172
+ // Initialize SessionManager with auth config for token refresh
173
+ if (this.sessionManager) {
174
+ const authConfig = {
175
+ clientId: this.config.clientId,
176
+ redirectUrl: this.config.redirectUrl,
177
+ oauthServer: this.config.oauthServerBaseUrl,
178
+ scopes: this.config.scopes,
179
+ endpoints: this.endpoints,
180
+ };
181
+ await this.sessionManager.initializeWithAuthConfig(authConfig);
182
+ }
183
+ // Check if we're on the callback page
184
+ if (window.location.href.startsWith(this.config.redirectUrl)) {
185
+ await this.handleCallback();
186
+ }
187
+ }
188
+ catch (error) {
189
+ const errorMessage = error instanceof Error
190
+ ? error.message
191
+ : "Failed to initialize authentication";
192
+ this.logger.error("Failed to initialize CivicAuth:", { error });
193
+ this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
194
+ detail: errorMessage,
195
+ });
196
+ throw new CivicAuthError(errorMessage, CivicAuthErrorCode.INIT_FAILED);
197
+ }
198
+ }
199
+ /**
200
+ * Gets the container element for the auth iframe
201
+ * @returns The container element or null if not found
202
+ */
203
+ getContainerElement() {
204
+ if (typeof this.config.targetContainerElement === "string") {
205
+ const element = document.getElementById(this.config.targetContainerElement);
206
+ if (!element) {
207
+ this.logger.warn(`Container element with ID "${this.config.targetContainerElement}" not found`);
208
+ }
209
+ return element;
210
+ }
211
+ return this.config.targetContainerElement;
212
+ }
213
+ /**
214
+ * Determines the appropriate iframe display mode based on container characteristics
215
+ * @param container The HTML element that will contain the iframe
216
+ * @returns "modal" for full-screen overlay or "embedded" for in-container display
217
+ */
218
+ determineIframeDisplayMode(container) {
219
+ // Use explicit config if provided
220
+ if (this.config.iframeDisplayMode) {
221
+ this.logger.debug(`Using configured iframe display mode: ${this.config.iframeDisplayMode}`);
222
+ return this.config.iframeDisplayMode;
223
+ }
224
+ // Analyze container characteristics to determine best display mode
225
+ const containerRect = container.getBoundingClientRect();
226
+ const containerStyles = window.getComputedStyle(container);
227
+ // Get container dimensions
228
+ const containerWidth = containerRect.width;
229
+ const containerHeight = containerRect.height;
230
+ // Check if container is positioned in a way that suggests it's meant for modal display
231
+ const isFullScreenContainer = containerStyles.position === "fixed" &&
232
+ (containerWidth >= window.innerWidth * 0.9 ||
233
+ containerHeight >= window.innerHeight * 0.9);
234
+ // Modal mode thresholds - if container is too small, use modal for better UX
235
+ const MIN_EMBEDDED_WIDTH = 320; // Same as modal content wrapper width
236
+ const MIN_EMBEDDED_HEIGHT = 400; // Reasonable minimum for embedded auth flow
237
+ // Determine mode based on container characteristics
238
+ if (isFullScreenContainer) {
239
+ this.logger.debug("Container appears to be full-screen positioned, using modal mode", { containerWidth, containerHeight, position: containerStyles.position });
240
+ return "modal";
241
+ }
242
+ if (containerWidth < MIN_EMBEDDED_WIDTH ||
243
+ containerHeight < MIN_EMBEDDED_HEIGHT) {
244
+ this.logger.debug(`Container too small for embedded mode (${containerWidth}x${containerHeight}), using modal mode`, {
245
+ containerWidth,
246
+ containerHeight,
247
+ MIN_EMBEDDED_WIDTH,
248
+ MIN_EMBEDDED_HEIGHT,
249
+ });
250
+ return "modal";
251
+ }
252
+ // Check if container has sufficient space and is properly positioned for embedding
253
+ const hasAdequateSpace = containerWidth >= MIN_EMBEDDED_WIDTH &&
254
+ containerHeight >= MIN_EMBEDDED_HEIGHT;
255
+ const isEmbeddablePosition = containerStyles.position === "relative" ||
256
+ containerStyles.position === "static" ||
257
+ containerStyles.position === "";
258
+ if (hasAdequateSpace && isEmbeddablePosition) {
259
+ this.logger.debug("Container has adequate space and positioning for embedded mode", { containerWidth, containerHeight, position: containerStyles.position });
260
+ return "embedded";
261
+ }
262
+ // Default to modal mode if container characteristics are unclear
263
+ this.logger.debug("Container characteristics unclear, defaulting to modal mode for better UX", { containerWidth, containerHeight, position: containerStyles.position });
264
+ return "modal";
265
+ }
266
+ /**
267
+ * Builds the authentication URL with PKCE challenge
268
+ * @returns The complete authentication URL
269
+ * @throws {CivicAuthError} If endpoints are not initialized
270
+ */
271
+ async buildAuthUrl() {
272
+ if (!this.endpoints) {
273
+ throw new CivicAuthError("OAuth endpoints not initialized. Please wait for initialization to complete.", CivicAuthErrorCode.ENDPOINTS_NOT_INITIALIZED);
274
+ }
275
+ // Use storage directly since it's now AuthStorage
276
+ const pkceProducer = new GenericPublicClientPKCEProducer(this.storage);
277
+ const codeChallenge = await pkceProducer.getCodeChallenge();
278
+ const state = this.config.initialState ||
279
+ generateState({
280
+ displayMode: this.config.displayMode || "iframe",
281
+ });
282
+ return buildAuthUrl({
283
+ endpoints: this.endpoints,
284
+ clientId: this.config.clientId,
285
+ redirectUrl: this.config.redirectUrl,
286
+ scopes: this.config.scopes,
287
+ codeChallenge,
288
+ state,
289
+ prompt: this.config.prompt,
290
+ });
291
+ }
292
+ handleIframeMessage(event) {
293
+ const expectedOrigin = new URL(this.config.oauthServerBaseUrl).origin;
294
+ this.logIncomingMessage(event, expectedOrigin);
295
+ if (!this.isValidMessageSource(event, expectedOrigin)) {
296
+ return;
297
+ }
298
+ this.handleValidMessage(event);
299
+ }
300
+ logIncomingMessage(event, expectedOrigin) {
301
+ this.logger.debug("Global window received message:", {
302
+ data: event.data,
303
+ origin: event.origin,
304
+ sourceProvided: !!event.source,
305
+ iframeContentWindow: this.iframeElement?.contentWindow,
306
+ expectedIframeOrigin: expectedOrigin,
307
+ });
308
+ }
309
+ isValidMessageSource(event, expectedOrigin) {
310
+ const isValidOrigin = event.origin === expectedOrigin;
311
+ const isValidSource = event.source === this.iframeElement?.contentWindow;
312
+ if (!isValidOrigin) {
313
+ this.logger.warn("Ignored message from unexpected origin.", {
314
+ receivedOrigin: event.origin,
315
+ expectedOrigin,
316
+ iframeSrc: this.iframeElement?.src,
317
+ });
318
+ }
319
+ if (!isValidSource) {
320
+ this.logger.warn("Ignored message from unexpected source.", {
321
+ isSourceProvided: !!event.source,
322
+ isIframeContentWindowAvailable: !!this.iframeElement?.contentWindow,
323
+ iframeSrc: this.iframeElement?.src,
324
+ });
325
+ }
326
+ return isValidOrigin && isValidSource;
327
+ }
328
+ handleValidMessage(event) {
329
+ this.logger.info("Message from configured iframe source and origin received", {
330
+ data: event.data,
331
+ iframeSrc: this.iframeElement?.src,
332
+ });
333
+ const message = event.data;
334
+ const messageType = message?.type;
335
+ switch (messageType) {
336
+ case "auth_success":
337
+ this.handleAuthSuccess(message);
338
+ break;
339
+ case "auth_error":
340
+ this.handleAuthError(message);
341
+ break;
342
+ default:
343
+ this.logger.debug("Message from iframe did not match expected types (auth_success, auth_error)", { data: event.data });
344
+ }
345
+ }
346
+ handleAuthSuccess(data) {
347
+ this.config.events?.emit(AuthEvent.SIGN_IN_COMPLETE, {
348
+ detail: "Success signal received via postMessage",
349
+ data,
350
+ });
351
+ this.authPromiseResolve?.(data?.data || {});
352
+ this.cleanup();
353
+ }
354
+ handleAuthError(data) {
355
+ this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
356
+ detail: "Error signal received via postMessage",
357
+ error: data,
358
+ });
359
+ this.authPromiseReject?.(new CivicAuthError(data?.detail || "Error signal received via postMessage", CivicAuthErrorCode.INVALID_MESSAGE));
360
+ this.cleanup();
361
+ }
362
+ setupSignalObserver(iframeDoc) {
363
+ const signalObserver = new SignalObserver({
364
+ textSignals: this.config.textSignals,
365
+ events: this.config.events,
366
+ logger: this.logger,
367
+ }, this.authPromiseResolve, this.authPromiseReject, () => this.cleanup());
368
+ signalObserver.setup(iframeDoc);
369
+ }
370
+ async handleNewTabAuth(fullAuthUrl, reject) {
371
+ const popupWindow = window.open(fullAuthUrl, "_blank");
372
+ if (!popupWindow) {
373
+ const error = new CivicAuthError("Failed to open popup window. Please check your browser's popup settings.", CivicAuthErrorCode.INIT_FAILED);
374
+ this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
375
+ detail: error.message,
376
+ });
377
+ reject(error);
378
+ }
379
+ }
380
+ async handleIframeAuth(fullAuthUrl, reject) {
381
+ const container = this.getContainerElement();
382
+ if (!container) {
383
+ const error = new CivicAuthError("Target container element not found.", CivicAuthErrorCode.CONTAINER_NOT_FOUND);
384
+ this.logger.error(error.message);
385
+ reject(error);
386
+ return;
387
+ }
388
+ this.logger.debug("Creating iframe with modal backdrop", {
389
+ url: fullAuthUrl,
390
+ containerId: container?.id,
391
+ iframeId: this.config.iframeId,
392
+ origin: window.location.origin,
393
+ });
394
+ // Determine the actual display mode for IframeManager
395
+ // For iframe displayMode, we need to decide between modal and embedded based on container
396
+ const iframeDisplayMode = this.determineIframeDisplayMode(container);
397
+ this.logger.debug(`🎯 CivicAuth: Creating IframeManager with display mode: ${iframeDisplayMode}`);
398
+ // Create IframeManager with appropriate display mode
399
+ this.iframeManager = new IframeManager({
400
+ container: container,
401
+ displayMode: iframeDisplayMode,
402
+ iframeId: this.config.iframeId,
403
+ onClose: () => {
404
+ this.logger.debug("Authentication close requested by user (backdrop click, close button, or Escape key)");
405
+ this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
406
+ detail: "Authentication cancelled by user",
407
+ });
408
+ reject(new CivicAuthError("Authentication cancelled by user", CivicAuthErrorCode.AUTH_PROCESS_TIMEOUT));
409
+ this.cleanup();
410
+ },
411
+ });
412
+ // Create the iframe using IframeManager
413
+ this.iframeElement = this.iframeManager.createIframe(fullAuthUrl);
414
+ this.config.events?.emit(AuthEvent.SIGN_IN_STARTED, {
415
+ detail: "Iframe created with modal backdrop",
416
+ });
417
+ this.iframeElement.onload = () => {
418
+ this.logger.info("Iframe loaded", {
419
+ iframeSrc: this.iframeElement?.src,
420
+ currentOrigin: window.location.origin,
421
+ expectedAuthServerOrigin: new URL(this.config.oauthServerBaseUrl)
422
+ .origin,
423
+ });
424
+ if (!this.iframeElement?.contentWindow) {
425
+ const errorMsg = "Iframe content window not available after load.";
426
+ this.logger.error(errorMsg, {
427
+ iframeSrc: this.iframeElement?.src,
428
+ });
429
+ reject(new Error(errorMsg));
430
+ this.cleanup();
431
+ return;
432
+ }
433
+ // Set up postMessage listener for cross-origin communication
434
+ if (this.messageEventHandler) {
435
+ window.addEventListener("message", this.messageEventHandler);
436
+ this.logger.info("Added cross-origin message event listener for auth server communication", {
437
+ parentOrigin: window.location.origin,
438
+ authServerOrigin: new URL(this.config.oauthServerBaseUrl).origin,
439
+ });
440
+ }
441
+ else {
442
+ this.logger.error("messageEventHandler was not defined when trying to add listener.");
443
+ }
444
+ // Try to detect redirect to our domain
445
+ try {
446
+ const currentIframeHref = this.iframeElement.contentWindow.location.href;
447
+ if (currentIframeHref.startsWith(this.config.redirectUrl)) {
448
+ this.logger.info("Iframe has navigated to redirectUrl (same-origin). Setting up DOM observer.");
449
+ if (this.iframeElement.contentDocument &&
450
+ this.iframeElement.contentDocument.body) {
451
+ this.setupSignalObserver(this.iframeElement.contentDocument);
452
+ }
453
+ }
454
+ }
455
+ catch (error) {
456
+ this.logger.error("Error checking iframe href", {
457
+ error,
458
+ iframeSrc: this.iframeElement?.src,
459
+ });
460
+ // This is expected when the iframe is on the auth server domain
461
+ this.logger.info("Iframe is on auth server domain - using postMessage for communication", {
462
+ parentOrigin: window.location.origin,
463
+ authServerOrigin: new URL(this.config.oauthServerBaseUrl).origin,
464
+ });
465
+ }
466
+ };
467
+ this.iframeElement.onerror = (event) => {
468
+ this.logger.error("Iframe load error", {
469
+ event,
470
+ iframeSrc: this.iframeElement?.src,
471
+ currentOrigin: window.location.origin,
472
+ });
473
+ this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
474
+ detail: "Iframe load error",
475
+ error: event,
476
+ });
477
+ reject(new Error("Iframe failed to load."));
478
+ this.cleanup();
479
+ };
480
+ }
481
+ /**
482
+ * Starts the authentication process
483
+ * @returns A promise that resolves with the authentication result
484
+ * @throws {CivicAuthError} If authentication fails or times out
485
+ */
486
+ async startAuthentication() {
487
+ if (!this.endpoints) {
488
+ const error = new CivicAuthError("OAuth endpoints not initialized. Please wait for initialization to complete.", CivicAuthErrorCode.ENDPOINTS_NOT_INITIALIZED);
489
+ this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
490
+ detail: error.message,
491
+ });
492
+ throw error;
493
+ }
494
+ if (this.authPromise) {
495
+ this.logger.info("Authentication already in progress, returning existing promise");
496
+ return this.authPromise;
497
+ }
498
+ const fullAuthUrl = await this.buildAuthUrl();
499
+ this.logger.info("Starting authentication process", {
500
+ constructedIframeUrl: fullAuthUrl,
501
+ displayMode: this.config.displayMode,
502
+ authProcessTimeout: this.config.authProcessTimeout,
503
+ });
504
+ this.authPromise = new Promise((resolve, reject) => {
505
+ this.authPromiseResolve = resolve;
506
+ this.authPromiseReject = reject;
507
+ // Handle different display modes
508
+ switch (this.config.displayMode) {
509
+ case "redirect":
510
+ // For redirect mode, just navigate to the auth URL
511
+ window.location.href = fullAuthUrl;
512
+ break;
513
+ case "new_tab":
514
+ this.handleNewTabAuth(fullAuthUrl, reject);
515
+ break;
516
+ case "iframe":
517
+ default:
518
+ this.handleIframeAuth(fullAuthUrl, reject);
519
+ break;
520
+ }
521
+ // Set up timeout for all display modes
522
+ if (this.config.authProcessTimeout &&
523
+ this.config.authProcessTimeout > 0) {
524
+ this.logger.debug("Setting up authentication timeout", {
525
+ authProcessTimeout: this.config.authProcessTimeout,
526
+ displayMode: this.config.displayMode,
527
+ });
528
+ this.authProcessTimeoutHandle = window.setTimeout(() => {
529
+ this.logger.warn("Authentication timed out", {
530
+ displayMode: this.config.displayMode,
531
+ currentOrigin: window.location.origin,
532
+ });
533
+ this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
534
+ detail: "Authentication timed out",
535
+ });
536
+ reject(new Error("Authentication timed out."));
537
+ this.cleanup();
538
+ }, this.config.authProcessTimeout);
539
+ }
540
+ });
541
+ return this.authPromise;
542
+ }
543
+ /**
544
+ * Cleans up resources and event listeners
545
+ */
546
+ cleanup() {
547
+ this.logger.info("Cleaning up iframe authentication client");
548
+ if (this.observer) {
549
+ this.logger.debug("Disconnecting mutation observer");
550
+ this.observer.disconnect();
551
+ this.observer = undefined;
552
+ }
553
+ if (this.messageEventHandler) {
554
+ window.removeEventListener("message", this.messageEventHandler);
555
+ this.logger.debug("Removed 'message' event listener from window.");
556
+ }
557
+ if (this.iframeManager) {
558
+ this.logger.debug("Cleaning up iframe manager");
559
+ this.iframeManager.cleanup();
560
+ this.iframeManager = undefined;
561
+ }
562
+ // Reset iframe element reference (actual removal handled by IframeManager)
563
+ if (this.iframeElement) {
564
+ this.iframeElement = undefined;
565
+ }
566
+ if (this.authProcessTimeoutHandle) {
567
+ this.logger.debug("Clearing authentication process timeout");
568
+ window.clearTimeout(this.authProcessTimeoutHandle);
569
+ this.authProcessTimeoutHandle = undefined;
570
+ }
571
+ this.logger.debug("Resetting promise state");
572
+ this.authPromise = undefined;
573
+ this.authPromiseResolve = undefined;
574
+ this.authPromiseReject = undefined;
575
+ }
576
+ async handleCallback() {
577
+ try {
578
+ const callbackHandled = await handleOAuthRedirectPage({
579
+ clientId: this.config.clientId,
580
+ oauthServer: this.config.oauthServerBaseUrl,
581
+ redirectUrl: this.config.redirectUrl,
582
+ textSignals: {
583
+ success: this.config.textSignals.success,
584
+ error: this.config.textSignals.error || "Authentication failed",
585
+ },
586
+ storageAdapter: this.storage,
587
+ });
588
+ if (callbackHandled) {
589
+ const successSignal = document.getElementById(CONSTANTS.SUCCESS_SIGNAL_ID);
590
+ const errorSignal = document.getElementById(CONSTANTS.ERROR_SIGNAL_ID);
591
+ if (successSignal) {
592
+ let userInfo = null;
593
+ const userInfoAttr = successSignal.getAttribute("data-user-info");
594
+ if (userInfoAttr) {
595
+ try {
596
+ userInfo = JSON.parse(userInfoAttr);
597
+ }
598
+ catch (error) {
599
+ this.logger.error("Failed to parse user info:", { error });
600
+ }
601
+ }
602
+ this.config.events?.emit(AuthEvent.SIGN_IN_COMPLETE, {
603
+ detail: "Callback processed successfully",
604
+ user: userInfo,
605
+ });
606
+ }
607
+ else if (errorSignal) {
608
+ this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
609
+ detail: errorSignal.textContent || "Unknown error during callback",
610
+ });
611
+ }
612
+ }
613
+ }
614
+ catch (error) {
615
+ this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
616
+ detail: error instanceof Error
617
+ ? error.message
618
+ : "Unknown error during callback",
619
+ });
620
+ }
621
+ }
622
+ /**
623
+ * Get the current session
624
+ */
625
+ async getCurrentSession() {
626
+ return this.sessionManager?.getCurrentSession() || null;
627
+ }
628
+ /**
629
+ * Check if user is authenticated
630
+ */
631
+ async isAuthenticated() {
632
+ return this.sessionManager?.isAuthenticated() || false;
633
+ }
634
+ /**
635
+ * Get the current user
636
+ */
637
+ async getCurrentUser() {
638
+ return this.sessionManager?.getCurrentUser() || null;
639
+ }
640
+ /**
641
+ * Clear the current session
642
+ */
643
+ async clearSession() {
644
+ if (!this.sessionManager) {
645
+ throw new Error("SessionManager not initialized. Provide events in config.");
646
+ }
647
+ return this.sessionManager.clearSession();
648
+ }
649
+ /**
650
+ * Manually refresh tokens
651
+ */
652
+ async refreshTokens() {
653
+ if (!this.sessionManager) {
654
+ throw new Error("SessionManager not initialized. Provide events in config.");
655
+ }
656
+ return this.sessionManager.refreshTokens();
657
+ }
658
+ /**
659
+ * Get token refresher state for debugging
660
+ */
661
+ getTokenRefresherState() {
662
+ return this.sessionManager?.getTokenRefresherState() || null;
663
+ }
664
+ /**
665
+ * Update the iframe display mode
666
+ * @param mode - The display mode to use for the iframe
667
+ */
668
+ setIframeDisplayMode(mode) {
669
+ this.config.iframeDisplayMode = mode;
670
+ this.logger.debug(`Iframe display mode updated to: ${mode}`);
671
+ }
672
+ /**
673
+ * Get the current iframe display mode
674
+ * @returns The current iframe display mode
675
+ */
676
+ getIframeDisplayMode() {
677
+ return this.config.iframeDisplayMode;
678
+ }
679
+ /**
680
+ * Destroy the auth client and clean up all resources
681
+ */
682
+ async destroy() {
683
+ this.cleanup();
684
+ await this.sessionManager?.destroy();
685
+ this.sessionManager = undefined;
686
+ this.logger.info("CivicAuth destroyed");
687
+ }
688
+ }
689
+ //# sourceMappingURL=CivicAuth.js.map