@civic/auth 0.6.1-beta.4 → 0.7.0-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 (86) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/nextjs/config.d.ts.map +1 -1
  3. package/dist/nextjs/config.js +3 -1
  4. package/dist/nextjs/config.js.map +1 -1
  5. package/dist/nextjs/hooks/useUserCookie.d.ts.map +1 -1
  6. package/dist/nextjs/hooks/useUserCookie.js.map +1 -1
  7. package/dist/nextjs/providers/NextAuthProvider.d.ts.map +1 -1
  8. package/dist/nextjs/providers/NextAuthProvider.js +1 -0
  9. package/dist/nextjs/providers/NextAuthProvider.js.map +1 -1
  10. package/dist/shared/hooks/useSignIn.d.ts +9 -4
  11. package/dist/shared/hooks/useSignIn.d.ts.map +1 -1
  12. package/dist/shared/hooks/useSignIn.js +75 -42
  13. package/dist/shared/hooks/useSignIn.js.map +1 -1
  14. package/dist/shared/providers/AuthContext.d.ts +7 -2
  15. package/dist/shared/providers/AuthContext.d.ts.map +1 -1
  16. package/dist/shared/providers/AuthContext.js.map +1 -1
  17. package/dist/shared/providers/UserProvider.d.ts +5 -1
  18. package/dist/shared/providers/UserProvider.d.ts.map +1 -1
  19. package/dist/shared/providers/UserProvider.js.map +1 -1
  20. package/dist/shared/version.d.ts +1 -1
  21. package/dist/shared/version.js +1 -1
  22. package/dist/shared/version.js.map +1 -1
  23. package/dist/vanillajs/auth/AuthenticationEvents.d.ts.map +1 -1
  24. package/dist/vanillajs/auth/AuthenticationEvents.js +2 -2
  25. package/dist/vanillajs/auth/AuthenticationEvents.js.map +1 -1
  26. package/dist/vanillajs/auth/CivicAuth.d.ts +69 -107
  27. package/dist/vanillajs/auth/CivicAuth.d.ts.map +1 -1
  28. package/dist/vanillajs/auth/CivicAuth.js +415 -440
  29. package/dist/vanillajs/auth/CivicAuth.js.map +1 -1
  30. package/dist/vanillajs/auth/SessionManager.d.ts.map +1 -1
  31. package/dist/vanillajs/auth/SessionManager.js +2 -2
  32. package/dist/vanillajs/auth/SessionManager.js.map +1 -1
  33. package/dist/vanillajs/auth/TokenRefresher.d.ts.map +1 -1
  34. package/dist/vanillajs/auth/TokenRefresher.js +2 -2
  35. package/dist/vanillajs/auth/TokenRefresher.js.map +1 -1
  36. package/dist/vanillajs/auth/config/ConfigProcessor.d.ts +6 -0
  37. package/dist/vanillajs/auth/config/ConfigProcessor.d.ts.map +1 -0
  38. package/dist/vanillajs/auth/config/ConfigProcessor.js +68 -0
  39. package/dist/vanillajs/auth/config/ConfigProcessor.js.map +1 -0
  40. package/dist/vanillajs/auth/handlers/IframeAuthHandler.d.ts +40 -0
  41. package/dist/vanillajs/auth/handlers/IframeAuthHandler.d.ts.map +1 -0
  42. package/dist/vanillajs/auth/handlers/IframeAuthHandler.js +388 -0
  43. package/dist/vanillajs/auth/handlers/IframeAuthHandler.js.map +1 -0
  44. package/dist/vanillajs/auth/handlers/MessageHandler.d.ts +170 -0
  45. package/dist/vanillajs/auth/handlers/MessageHandler.d.ts.map +1 -0
  46. package/dist/vanillajs/auth/handlers/MessageHandler.js +367 -0
  47. package/dist/vanillajs/auth/handlers/MessageHandler.js.map +1 -0
  48. package/dist/vanillajs/auth/{OAuthCallbackHandler.d.ts → handlers/OAuthCallbackHandler.d.ts} +18 -27
  49. package/dist/vanillajs/auth/handlers/OAuthCallbackHandler.d.ts.map +1 -0
  50. package/dist/vanillajs/auth/handlers/OAuthCallbackHandler.js +292 -0
  51. package/dist/vanillajs/auth/handlers/OAuthCallbackHandler.js.map +1 -0
  52. package/dist/vanillajs/auth/handlers/PopupHandler.d.ts +108 -0
  53. package/dist/vanillajs/auth/handlers/PopupHandler.d.ts.map +1 -0
  54. package/dist/vanillajs/auth/handlers/PopupHandler.js +333 -0
  55. package/dist/vanillajs/auth/handlers/PopupHandler.js.map +1 -0
  56. package/dist/vanillajs/auth/types/AuthTypes.d.ts +135 -0
  57. package/dist/vanillajs/auth/types/AuthTypes.d.ts.map +1 -0
  58. package/dist/vanillajs/auth/types/AuthTypes.js +40 -0
  59. package/dist/vanillajs/auth/types/AuthTypes.js.map +1 -0
  60. package/dist/vanillajs/iframe/IframeManager.d.ts +33 -0
  61. package/dist/vanillajs/iframe/IframeManager.d.ts.map +1 -1
  62. package/dist/vanillajs/iframe/IframeManager.js +163 -36
  63. package/dist/vanillajs/iframe/IframeManager.js.map +1 -1
  64. package/dist/vanillajs/index.d.ts +2 -2
  65. package/dist/vanillajs/index.d.ts.map +1 -1
  66. package/dist/vanillajs/index.js +2 -2
  67. package/dist/vanillajs/index.js.map +1 -1
  68. package/dist/vanillajs/services/ApiService.d.ts.map +1 -1
  69. package/dist/vanillajs/services/ApiService.js +2 -2
  70. package/dist/vanillajs/services/ApiService.js.map +1 -1
  71. package/dist/vanillajs/types/index.d.ts +15 -10
  72. package/dist/vanillajs/types/index.d.ts.map +1 -1
  73. package/dist/vanillajs/types/index.js +15 -10
  74. package/dist/vanillajs/types/index.js.map +1 -1
  75. package/dist/vanillajs/utils/auth-utils.d.ts +2 -1
  76. package/dist/vanillajs/utils/auth-utils.d.ts.map +1 -1
  77. package/dist/vanillajs/utils/auth-utils.js +6 -3
  78. package/dist/vanillajs/utils/auth-utils.js.map +1 -1
  79. package/dist/vanillajs/utils/logger.d.ts +16 -15
  80. package/dist/vanillajs/utils/logger.d.ts.map +1 -1
  81. package/dist/vanillajs/utils/logger.js +35 -19
  82. package/dist/vanillajs/utils/logger.js.map +1 -1
  83. package/package.json +6 -1
  84. package/dist/vanillajs/auth/OAuthCallbackHandler.d.ts.map +0 -1
  85. package/dist/vanillajs/auth/OAuthCallbackHandler.js +0 -143
  86. package/dist/vanillajs/auth/OAuthCallbackHandler.js.map +0 -1
@@ -1,105 +1,58 @@
1
1
  import { AuthEvent } from "../types/index.js";
2
- import { LocalStorageAdapter } from "../../browser/storage.js";
3
- import { handleOAuthRedirectPage } from "./OAuthCallbackHandler.js";
4
2
  import { buildAuthUrl } from "../utils/auth-utils.js";
5
- import { createLogger, configureLogging, setCurrentLogger, } from "../utils/logger.js";
6
- import { SignalObserver } from "../iframe/SignalObserver.js";
3
+ import { createMainLogger, configureLogging, setCurrentLogger, } from "../utils/logger.js";
7
4
  import { GenericPublicClientPKCEProducer } from "../../services/PKCE.js";
8
5
  import { generateState } from "../../lib/oauth.js";
9
6
  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
- }
7
+ import { AuthenticationEvents } from "./AuthenticationEvents.js";
8
+ import { PopupError } from "../../services/types.js";
9
+ import { handleOAuthRedirectPage } from "./handlers/OAuthCallbackHandler.js";
10
+ import { generateOauthLogoutUrl, clearTokens, retrieveTokens, } from "../../shared/lib/util.js";
11
+ import { getOauthEndpoints } from "../../lib/oauth.js";
12
+ import { CivicAuthError, CivicAuthErrorCode, CIVIC_AUTH_CONSTANTS, } from "./types/AuthTypes.js";
13
+ import { processConfigWithDefaults } from "./config/ConfigProcessor.js";
14
+ import { MessageHandler } from "./handlers/MessageHandler.js";
15
+ import { PopupHandler } from "./handlers/PopupHandler.js";
16
+ import { IframeAuthHandler } from "./handlers/IframeAuthHandler.js";
60
17
  /**
61
18
  * CivicAuth client for handling OAuth authentication
19
+ *
20
+ * This is a refactored version that uses a modular architecture for better maintainability.
62
21
  */
63
22
  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
23
  config;
72
- iframeElement;
73
- observer;
74
- authPromise;
75
- authPromiseResolve;
76
- authPromiseReject;
77
- authProcessTimeoutHandle;
78
- messageEventHandler;
79
24
  storage;
80
25
  endpoints;
81
26
  logger;
82
27
  sessionManager;
83
- iframeManager;
28
+ events;
29
+ initialDisplayMode;
30
+ // Authentication state
31
+ authPromise;
32
+ authPromiseResolve;
33
+ authPromiseReject;
34
+ authProcessTimeoutHandle;
35
+ popupFailureTimeoutHandle;
36
+ hasPopupFailed = false;
37
+ // Handlers
38
+ messageHandler;
39
+ popupHandler;
40
+ iframeAuthHandler;
84
41
  /**
85
- * Private constructor for CivicAuth.
42
+ * Private constructor - initializes configuration and handlers.
86
43
  * 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
44
  */
91
45
  constructor(config) {
92
- // Process config with defaults
46
+ // Process config with defaults and validation
93
47
  this.config = processConfigWithDefaults(config);
94
- // Configure logging based on config
48
+ this.initialDisplayMode = this.config.displayMode;
49
+ // Configure logging
95
50
  configureLogging(this.config.logging);
96
- // Initialize logger based on config
51
+ // Initialize logger - always use "vanillajs" as base namespace
97
52
  if (this.config.logging?.enabled) {
98
- const namespace = this.config.logging.namespace || "iframe";
99
- this.logger = createLogger(namespace);
53
+ this.logger = createMainLogger("vanillajs"); // Always use "vanillajs"
100
54
  }
101
55
  else {
102
- // Create a no-op logger when logging is disabled
103
56
  this.logger = {
104
57
  debug: () => { },
105
58
  info: () => { },
@@ -107,26 +60,14 @@ export class CivicAuth {
107
60
  error: () => { },
108
61
  };
109
62
  }
110
- // Set this logger as the current logger
111
63
  setCurrentLogger(this.logger);
112
- // Use the storage adapter from processed config (guaranteed to be present)
113
64
  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);
65
+ // Always initialize events - use provided events or create default instance
66
+ this.events = config.events || new AuthenticationEvents();
67
+ // Always initialize SessionManager
68
+ this.sessionManager = new SessionManager(this.storage, this.events);
69
+ // Initialize handlers
70
+ this.initializeHandlers();
130
71
  }
131
72
  /**
132
73
  * Creates and initializes a new instance of CivicAuth.
@@ -140,9 +81,12 @@ export class CivicAuth {
140
81
  * ```typescript
141
82
  * const auth = await CivicAuth.create({
142
83
  * clientId: "your-client-id",
143
- * redirectUrl: "https://your-app.com/callback",
144
- * oauthServerBaseUrl: "https://auth-server.com/",
145
- * scopes: ["openid", "profile"],
84
+ * // redirectUrl is optional - defaults to current page (window.location.origin + window.location.pathname)
85
+ * redirectUrl: "https://your-app.com/callback", // optional
86
+ * // oauthServerBaseUrl is optional - defaults to "https://auth.civic.com/oauth"
87
+ * oauthServerBaseUrl: "https://auth-server.com/", // optional
88
+ * // scopes is optional - defaults to ['openid', 'profile', 'email', 'offline_access']
89
+ * scopes: ["openid", "profile"], // optional
146
90
  * targetContainerElement: "auth-container",
147
91
  * textSignals: {
148
92
  * success: "Authentication successful!"
@@ -157,19 +101,21 @@ export class CivicAuth {
157
101
  }
158
102
  /**
159
103
  * Initializes the auth client and checks for callback handling
160
- * @throws {CivicAuthError} If initialization fails
161
104
  */
162
105
  async init() {
106
+ this.logger.info("🚀 Initializing CivicAuth", {
107
+ currentUrl: window.location.href,
108
+ redirectUrl: this.config.redirectUrl,
109
+ oauthServerBaseUrl: this.config.oauthServerBaseUrl,
110
+ isCallbackUrl: window.location.href.startsWith(this.config.redirectUrl),
111
+ });
163
112
  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
113
+ // Get OAuth endpoints using shared function (handles trailing slash automatically)
114
+ this.endpoints = await getOauthEndpoints(this.config.oauthServerBaseUrl);
115
+ this.logger.info("🔗 OAuth endpoints configured", {
116
+ endpoints: this.endpoints,
117
+ });
118
+ // Initialize SessionManager with auth config
173
119
  if (this.sessionManager) {
174
120
  const authConfig = {
175
121
  clientId: this.config.clientId,
@@ -178,101 +124,66 @@ export class CivicAuth {
178
124
  scopes: this.config.scopes,
179
125
  endpoints: this.endpoints,
180
126
  };
127
+ this.logger.info("🔧 Initializing SessionManager", { authConfig });
181
128
  await this.sessionManager.initializeWithAuthConfig(authConfig);
182
129
  }
183
130
  // Check if we're on the callback page
184
- if (window.location.href.startsWith(this.config.redirectUrl)) {
131
+ const isCallbackPage = window.location.href.startsWith(this.config.redirectUrl);
132
+ this.logger.info("🔍 Callback page check", {
133
+ isCallbackPage,
134
+ currentUrl: window.location.href,
135
+ redirectUrl: this.config.redirectUrl,
136
+ });
137
+ if (isCallbackPage) {
138
+ this.logger.info("📞 Processing callback page");
185
139
  await this.handleCallback();
186
140
  }
141
+ else {
142
+ this.logger.info("🏠 Not a callback page, initialization complete");
143
+ }
187
144
  }
188
145
  catch (error) {
189
146
  const errorMessage = error instanceof Error
190
147
  ? error.message
191
148
  : "Failed to initialize authentication";
192
- this.logger.error("Failed to initialize CivicAuth:", { error });
193
- this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
149
+ this.logger.error(" CivicAuth initialization failed", {
150
+ error: errorMessage,
151
+ stack: error instanceof Error ? error.stack : undefined,
152
+ });
153
+ this.events.emit(AuthEvent.SIGN_IN_ERROR, {
194
154
  detail: errorMessage,
195
155
  });
196
156
  throw new CivicAuthError(errorMessage, CivicAuthErrorCode.INIT_FAILED);
197
157
  }
198
158
  }
199
159
  /**
200
- * Gets the container element for the auth iframe
201
- * @returns The container element or null if not found
160
+ * Initialize all handlers with proper configuration
202
161
  */
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";
162
+ initializeHandlers() {
163
+ const handlerConfig = {
164
+ config: this.config,
165
+ logger: this.logger,
166
+ onAuthSuccess: this.handleAuthSuccess.bind(this),
167
+ onAuthError: this.handleAuthError.bind(this),
168
+ cleanup: this.cleanup.bind(this),
169
+ };
170
+ this.messageHandler = new MessageHandler({
171
+ ...handlerConfig,
172
+ onPopupFailure: this.handlePopupFailure.bind(this),
173
+ });
174
+ this.popupHandler = new PopupHandler(handlerConfig);
175
+ this.iframeAuthHandler = new IframeAuthHandler({
176
+ ...handlerConfig,
177
+ messageHandler: this.messageHandler.handleMessage,
178
+ });
265
179
  }
266
180
  /**
267
181
  * Builds the authentication URL with PKCE challenge
268
- * @returns The complete authentication URL
269
- * @throws {CivicAuthError} If endpoints are not initialized
270
182
  */
271
183
  async buildAuthUrl() {
272
184
  if (!this.endpoints) {
273
185
  throw new CivicAuthError("OAuth endpoints not initialized. Please wait for initialization to complete.", CivicAuthErrorCode.ENDPOINTS_NOT_INITIALIZED);
274
186
  }
275
- // Use storage directly since it's now AuthStorage
276
187
  const pkceProducer = new GenericPublicClientPKCEProducer(this.storage);
277
188
  const codeChallenge = await pkceProducer.getCodeChallenge();
278
189
  const state = this.config.initialState ||
@@ -287,293 +198,276 @@ export class CivicAuth {
287
198
  codeChallenge,
288
199
  state,
289
200
  prompt: this.config.prompt,
201
+ nonce: this.config.nonce,
290
202
  });
291
203
  }
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
204
  /**
482
205
  * Starts the authentication process
483
206
  * @returns A promise that resolves with the authentication result
484
207
  * @throws {CivicAuthError} If authentication fails or times out
485
208
  */
486
209
  async startAuthentication() {
210
+ this.logger.info("🎬 Starting authentication process", {
211
+ displayMode: this.config.displayMode,
212
+ userAgent: navigator.userAgent,
213
+ currentUrl: window.location.href,
214
+ });
487
215
  if (!this.endpoints) {
488
216
  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, {
217
+ this.logger.error("❌ Endpoints not initialized", {
218
+ error: error.message,
219
+ });
220
+ this.events.emit(AuthEvent.SIGN_IN_ERROR, {
490
221
  detail: error.message,
491
222
  });
492
223
  throw error;
493
224
  }
494
225
  if (this.authPromise) {
495
- this.logger.info("Authentication already in progress, returning existing promise");
226
+ this.logger.info("Authentication already in progress, returning existing promise");
496
227
  return this.authPromise;
497
228
  }
498
229
  const fullAuthUrl = await this.buildAuthUrl();
499
- this.logger.info("Starting authentication process", {
500
- constructedIframeUrl: fullAuthUrl,
230
+ this.logger.info("🔗 Built authentication URL", {
231
+ url: fullAuthUrl,
501
232
  displayMode: this.config.displayMode,
502
233
  authProcessTimeout: this.config.authProcessTimeout,
503
234
  });
504
235
  this.authPromise = new Promise((resolve, reject) => {
505
236
  this.authPromiseResolve = resolve;
506
237
  this.authPromiseReject = reject;
507
- // Handle different display modes
238
+ this.handleAuthenticationByDisplayMode(fullAuthUrl);
239
+ this.setupAuthenticationTimeout();
240
+ });
241
+ return this.authPromise;
242
+ }
243
+ /**
244
+ * Handle authentication based on display mode
245
+ */
246
+ async handleAuthenticationByDisplayMode(fullAuthUrl) {
247
+ this.logger.info("🎯 Handling authentication with display mode", {
248
+ displayMode: this.config.displayMode,
249
+ });
250
+ try {
508
251
  switch (this.config.displayMode) {
509
252
  case "redirect":
510
- // For redirect mode, just navigate to the auth URL
253
+ this.logger.info("🌐 Using redirect mode");
511
254
  window.location.href = fullAuthUrl;
512
255
  break;
513
256
  case "new_tab":
514
- this.handleNewTabAuth(fullAuthUrl, reject);
257
+ this.logger.info("📱 Using new_tab mode");
258
+ if (!this.popupHandler) {
259
+ throw new Error("Popup handler not initialized");
260
+ }
261
+ await this.popupHandler.handleNewTabAuth(fullAuthUrl);
515
262
  break;
516
263
  case "iframe":
517
- default:
518
- this.handleIframeAuth(fullAuthUrl, reject);
264
+ default: {
265
+ this.logger.info("🖼️ Using iframe mode");
266
+ if (!this.iframeAuthHandler || !this.messageHandler) {
267
+ throw new Error("Iframe handler not initialized");
268
+ }
269
+ const iframeElement = await this.iframeAuthHandler.handleIframeAuth(fullAuthUrl);
270
+ this.messageHandler.updateIframeElement(iframeElement);
519
271
  break;
272
+ }
520
273
  }
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);
274
+ }
275
+ catch (error) {
276
+ if (error instanceof PopupError) {
277
+ await this.handlePopupErrorWithFallback(fullAuthUrl, error);
278
+ }
279
+ else {
280
+ this.handleAuthError(error instanceof Error ? error : new Error(String(error)));
539
281
  }
282
+ }
283
+ }
284
+ /**
285
+ * Handle popup error with redirect fallback
286
+ */
287
+ async handlePopupErrorWithFallback(fullAuthUrl, error) {
288
+ this.logger.warn("🚫 Popup failed, falling back to redirect mode", {
289
+ originalDisplayMode: this.config.displayMode,
290
+ error: error.message,
540
291
  });
541
- return this.authPromise;
292
+ this.events.emit(AuthEvent.SIGN_IN_ERROR, {
293
+ detail: "Popup blocked, falling back to redirect mode",
294
+ });
295
+ try {
296
+ this.logger.info("🔄 Attempting redirect fallback");
297
+ // Clean up current authentication attempt
298
+ this.cleanup();
299
+ // Always switch to redirect mode for Safari compatibility
300
+ this.config.displayMode = "redirect";
301
+ // Regenerate the auth URL with updated display mode in state
302
+ const fallbackAuthUrl = await this.buildAuthUrl();
303
+ this.logger.info("🌐 Redirecting to auth URL", { url: fallbackAuthUrl });
304
+ window.location.href = fallbackAuthUrl;
305
+ this.logger.info("✅ Redirect initiated successfully");
306
+ }
307
+ catch (redirectError) {
308
+ this.logger.error("❌ Redirect fallback failed", {
309
+ error: redirectError,
310
+ redirectUrl: fullAuthUrl,
311
+ });
312
+ const fallbackError = new CivicAuthError("Failed to open popup window and redirect fallback failed. Please check your browser's popup settings.", CivicAuthErrorCode.INIT_FAILED);
313
+ this.events.emit(AuthEvent.SIGN_IN_ERROR, {
314
+ detail: fallbackError.message,
315
+ });
316
+ this.handleAuthError(fallbackError);
317
+ }
542
318
  }
543
319
  /**
544
- * Cleans up resources and event listeners
320
+ * Setup authentication timeout
545
321
  */
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;
322
+ setupAuthenticationTimeout() {
323
+ // Skip timeout for embedded iframe mode - embedded iframes should stay persistent
324
+ if (this.config.displayMode === "iframe" &&
325
+ this.config.iframeDisplayMode === "embedded") {
326
+ this.logger.debug("⏰ Skipping authentication timeout for embedded iframe mode", {
327
+ displayMode: this.config.displayMode,
328
+ iframeDisplayMode: this.config.iframeDisplayMode,
329
+ });
330
+ return;
552
331
  }
553
- if (this.messageEventHandler) {
554
- window.removeEventListener("message", this.messageEventHandler);
555
- this.logger.debug("Removed 'message' event listener from window.");
332
+ if (this.config.authProcessTimeout && this.config.authProcessTimeout > 0) {
333
+ this.logger.debug("⏰ Setting up authentication timeout", {
334
+ authProcessTimeout: this.config.authProcessTimeout,
335
+ displayMode: this.config.displayMode,
336
+ iframeDisplayMode: this.config.iframeDisplayMode,
337
+ });
338
+ this.authProcessTimeoutHandle = window.setTimeout(() => {
339
+ this.logger.error("⏰ Authentication timed out", {
340
+ displayMode: this.config.displayMode,
341
+ iframeDisplayMode: this.config.iframeDisplayMode,
342
+ currentOrigin: window.location.origin,
343
+ authProcessTimeout: this.config.authProcessTimeout,
344
+ });
345
+ this.events.emit(AuthEvent.SIGN_IN_ERROR, {
346
+ detail: "Authentication timed out",
347
+ });
348
+ const error = new CivicAuthError("Authentication timed out", CivicAuthErrorCode.AUTH_PROCESS_TIMEOUT);
349
+ this.handleAuthError(error);
350
+ }, this.config.authProcessTimeout);
556
351
  }
557
- if (this.iframeManager) {
558
- this.logger.debug("Cleaning up iframe manager");
559
- this.iframeManager.cleanup();
560
- this.iframeManager = undefined;
352
+ }
353
+ /**
354
+ * Handle successful authentication
355
+ */
356
+ handleAuthSuccess(result) {
357
+ this.logger.info("✅ Authentication successful");
358
+ this.authPromiseResolve?.(result);
359
+ this.cleanup();
360
+ }
361
+ /**
362
+ * Handle authentication error
363
+ */
364
+ handleAuthError(error) {
365
+ this.logger.error("❌ Authentication failed", { error: error.message });
366
+ this.authPromiseReject?.(error);
367
+ this.cleanup();
368
+ }
369
+ /**
370
+ * Handle popup failure - simplified like React implementation
371
+ */
372
+ handlePopupFailure(failedUrl) {
373
+ this.hasPopupFailed = true;
374
+ this.logger.warn("Popup failed, using redirect mode instead...", {
375
+ failedUrl,
376
+ });
377
+ // Clean up iframe if it exists
378
+ if (this.iframeAuthHandler) {
379
+ this.iframeAuthHandler.cleanupIframe();
561
380
  }
562
- // Reset iframe element reference (actual removal handled by IframeManager)
563
- if (this.iframeElement) {
564
- this.iframeElement = undefined;
381
+ // Always redirect to the failed URL or build a new one
382
+ if (failedUrl) {
383
+ window.location.href = failedUrl;
565
384
  }
566
- if (this.authProcessTimeoutHandle) {
567
- this.logger.debug("Clearing authentication process timeout");
568
- window.clearTimeout(this.authProcessTimeoutHandle);
569
- this.authProcessTimeoutHandle = undefined;
385
+ else {
386
+ this.buildAuthUrl()
387
+ .then((authUrl) => {
388
+ window.location.href = authUrl;
389
+ })
390
+ .catch((error) => {
391
+ this.logger.error("Failed to build auth URL for redirect fallback", {
392
+ error,
393
+ });
394
+ const fallbackError = new CivicAuthError("Failed to redirect for authentication", CivicAuthErrorCode.INIT_FAILED);
395
+ this.handleAuthError(fallbackError);
396
+ });
570
397
  }
571
- this.logger.debug("Resetting promise state");
572
- this.authPromise = undefined;
573
- this.authPromiseResolve = undefined;
574
- this.authPromiseReject = undefined;
575
398
  }
399
+ /**
400
+ * Show popup failure message to user
401
+ */
402
+ showPopupFailureMessage(customMessage = "Authentication will continue in this window. Please wait...") {
403
+ const container = this.getContainerElement();
404
+ if (!container) {
405
+ this.logger.warn("Cannot show popup failure message - container not found");
406
+ return;
407
+ }
408
+ const existingMessage = document.getElementById("civic-auth-popup-failure-message");
409
+ if (existingMessage && existingMessage.parentNode) {
410
+ existingMessage.parentNode.removeChild(existingMessage);
411
+ }
412
+ const messageOverlay = document.createElement("div");
413
+ messageOverlay.id = "civic-auth-popup-failure-message";
414
+ messageOverlay.style.cssText =
415
+ "position:absolute;top:0;left:0;right:0;background:rgba(255,249,196,.95);border:1px solid #f59e0b;border-radius:6px;padding:12px;margin:8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:14px;color:#92400e;z-index:1000;box-shadow:0 2px 4px rgba(0,0,0,.1);";
416
+ messageOverlay.innerHTML =
417
+ '<div style="display:flex;align-items:center;gap:8px;">' +
418
+ '<span style="font-size:16px;">⚠️</span>' +
419
+ "<div>" +
420
+ "<strong>Popup blocked</strong><br>" +
421
+ '<span style="font-size:12px;">' +
422
+ customMessage +
423
+ "</span>" +
424
+ "</div>" +
425
+ "</div>";
426
+ if (getComputedStyle(container).position === "static") {
427
+ container.style.position = "relative";
428
+ }
429
+ container.appendChild(messageOverlay);
430
+ setTimeout(() => {
431
+ if (messageOverlay.parentNode) {
432
+ messageOverlay.parentNode.removeChild(messageOverlay);
433
+ }
434
+ }, 10000);
435
+ this.logger.info("Popup failure message displayed to user");
436
+ }
437
+ /**
438
+ * Setup popup failure timeout
439
+ */
440
+ setupPopupFailureTimeout() {
441
+ this.popupFailureTimeoutHandle = window.setTimeout(() => {
442
+ this.logger.info("⏰ Popup failure timeout reached");
443
+ this.events.emit(AuthEvent.SIGN_IN_ERROR, {
444
+ detail: "Authentication timeout - popup failure scenario",
445
+ });
446
+ const error = new CivicAuthError("Authentication timeout - popup failure scenario", CivicAuthErrorCode.AUTH_PROCESS_TIMEOUT);
447
+ this.handleAuthError(error);
448
+ }, 20000); // 20 seconds
449
+ }
450
+ /**
451
+ * Gets the container element for the auth iframe
452
+ */
453
+ getContainerElement() {
454
+ if (typeof this.config.targetContainerElement === "string") {
455
+ const element = document.getElementById(this.config.targetContainerElement);
456
+ if (!element) {
457
+ this.logger.warn(`Container element with ID "${this.config.targetContainerElement}" not found`);
458
+ }
459
+ return element;
460
+ }
461
+ return this.config.targetContainerElement ?? null;
462
+ }
463
+ /**
464
+ * Handle OAuth callback
465
+ */
576
466
  async handleCallback() {
467
+ this.logger.info("🔄 Handling OAuth callback", {
468
+ currentUrl: window.location.href,
469
+ redirectUrl: this.config.redirectUrl,
470
+ });
577
471
  try {
578
472
  const callbackHandled = await handleOAuthRedirectPage({
579
473
  clientId: this.config.clientId,
@@ -585,10 +479,12 @@ export class CivicAuth {
585
479
  },
586
480
  storageAdapter: this.storage,
587
481
  });
482
+ this.logger.info("📋 Callback processing result", { callbackHandled });
588
483
  if (callbackHandled) {
589
- const successSignal = document.getElementById(CONSTANTS.SUCCESS_SIGNAL_ID);
590
- const errorSignal = document.getElementById(CONSTANTS.ERROR_SIGNAL_ID);
484
+ const successSignal = document.getElementById(CIVIC_AUTH_CONSTANTS.SUCCESS_SIGNAL_ID);
485
+ const errorSignal = document.getElementById(CIVIC_AUTH_CONSTANTS.ERROR_SIGNAL_ID);
591
486
  if (successSignal) {
487
+ this.logger.info("✅ Success signal found");
592
488
  let userInfo = null;
593
489
  const userInfoAttr = successSignal.getAttribute("data-user-info");
594
490
  if (userInfoAttr) {
@@ -596,70 +492,92 @@ export class CivicAuth {
596
492
  userInfo = JSON.parse(userInfoAttr);
597
493
  }
598
494
  catch (error) {
599
- this.logger.error("Failed to parse user info:", { error });
495
+ this.logger.error("Failed to parse user info", { error });
600
496
  }
601
497
  }
602
- this.config.events?.emit(AuthEvent.SIGN_IN_COMPLETE, {
498
+ this.events.emit(AuthEvent.SIGN_IN_COMPLETE, {
603
499
  detail: "Callback processed successfully",
604
500
  user: userInfo,
605
501
  });
606
502
  }
607
503
  else if (errorSignal) {
608
- this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
504
+ this.logger.error("❌ Error signal found");
505
+ this.events.emit(AuthEvent.SIGN_IN_ERROR, {
609
506
  detail: errorSignal.textContent || "Unknown error during callback",
610
507
  });
611
508
  }
612
509
  }
613
510
  }
614
511
  catch (error) {
615
- this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
512
+ this.logger.error("❌ Callback handling failed", { error });
513
+ this.events.emit(AuthEvent.SIGN_IN_ERROR, {
616
514
  detail: error instanceof Error
617
515
  ? error.message
618
516
  : "Unknown error during callback",
619
517
  });
620
518
  }
621
519
  }
520
+ /**
521
+ * Cleans up resources and event listeners
522
+ */
523
+ cleanup() {
524
+ this.logger.info("Cleaning up authentication client");
525
+ // Clean up handlers
526
+ this.iframeAuthHandler?.cleanupIframe();
527
+ // Clean up timeouts
528
+ if (this.authProcessTimeoutHandle) {
529
+ window.clearTimeout(this.authProcessTimeoutHandle);
530
+ this.authProcessTimeoutHandle = undefined;
531
+ }
532
+ if (this.popupFailureTimeoutHandle) {
533
+ window.clearTimeout(this.popupFailureTimeoutHandle);
534
+ this.popupFailureTimeoutHandle = undefined;
535
+ }
536
+ // Reset state
537
+ this.hasPopupFailed = false;
538
+ this.authPromise = undefined;
539
+ this.authPromiseResolve = undefined;
540
+ this.authPromiseReject = undefined;
541
+ // Remove message event listener
542
+ if (this.messageHandler) {
543
+ window.removeEventListener("message", this.messageHandler.handleMessage);
544
+ }
545
+ }
622
546
  /**
623
547
  * Get the current session
624
548
  */
625
549
  async getCurrentSession() {
626
- return this.sessionManager?.getCurrentSession() || null;
550
+ return this.sessionManager.getCurrentSession() || null;
627
551
  }
628
552
  /**
629
553
  * Check if user is authenticated
630
554
  */
631
555
  async isAuthenticated() {
632
- return this.sessionManager?.isAuthenticated() || false;
556
+ return this.sessionManager.isAuthenticated() || false;
633
557
  }
634
558
  /**
635
559
  * Get the current user
636
560
  */
637
561
  async getCurrentUser() {
638
- return this.sessionManager?.getCurrentUser() || null;
562
+ return this.sessionManager.getCurrentUser() || null;
639
563
  }
640
564
  /**
641
565
  * Clear the current session
642
566
  */
643
567
  async clearSession() {
644
- if (!this.sessionManager) {
645
- throw new Error("SessionManager not initialized. Provide events in config.");
646
- }
647
568
  return this.sessionManager.clearSession();
648
569
  }
649
570
  /**
650
571
  * Manually refresh tokens
651
572
  */
652
573
  async refreshTokens() {
653
- if (!this.sessionManager) {
654
- throw new Error("SessionManager not initialized. Provide events in config.");
655
- }
656
574
  return this.sessionManager.refreshTokens();
657
575
  }
658
576
  /**
659
577
  * Get token refresher state for debugging
660
578
  */
661
579
  getTokenRefresherState() {
662
- return this.sessionManager?.getTokenRefresherState() || null;
580
+ return this.sessionManager.getTokenRefresherState() || null;
663
581
  }
664
582
  /**
665
583
  * Update the iframe display mode
@@ -681,9 +599,66 @@ export class CivicAuth {
681
599
  */
682
600
  async destroy() {
683
601
  this.cleanup();
684
- await this.sessionManager?.destroy();
685
- this.sessionManager = undefined;
602
+ await this.sessionManager.destroy();
686
603
  this.logger.info("CivicAuth destroyed");
687
604
  }
605
+ /**
606
+ * Handle logout
607
+ */
608
+ async logout() {
609
+ try {
610
+ this.logger.info("🔄 Starting logout process");
611
+ this.events.emit(AuthEvent.SIGN_OUT_STARTED, {
612
+ detail: "Logout process started",
613
+ });
614
+ // Get current tokens before clearing them
615
+ const tokens = await retrieveTokens(this.storage);
616
+ if (!tokens?.id_token) {
617
+ this.logger.warn("⚠️ No ID token found, clearing local session only");
618
+ await clearTokens(this.storage);
619
+ await this.sessionManager.clearSession();
620
+ this.events.emit(AuthEvent.SIGN_OUT_COMPLETE, {
621
+ detail: "Local session cleared",
622
+ });
623
+ return;
624
+ }
625
+ if (!this.endpoints) {
626
+ throw new Error("OAuth endpoints not initialized");
627
+ }
628
+ // Generate a state for logout
629
+ const state = generateState({
630
+ displayMode: this.config.displayMode || "iframe",
631
+ });
632
+ // Generate logout URL
633
+ const logoutUrl = await generateOauthLogoutUrl({
634
+ clientId: this.config.clientId,
635
+ redirectUrl: this.config.redirectUrl,
636
+ idToken: tokens.id_token,
637
+ state: state,
638
+ oauthServer: this.config.oauthServerBaseUrl,
639
+ });
640
+ this.logger.info("🔗 Generated logout URL", {
641
+ logoutUrl: logoutUrl.toString(),
642
+ });
643
+ // Clear local tokens and session
644
+ await clearTokens(this.storage);
645
+ await this.sessionManager.clearSession();
646
+ // Emit logout complete event before redirect
647
+ this.events.emit(AuthEvent.SIGN_OUT_COMPLETE, {
648
+ detail: "Logout successful",
649
+ });
650
+ // Redirect to logout URL
651
+ this.logger.info("🌐 Redirecting to logout URL");
652
+ window.location.href = logoutUrl.toString();
653
+ }
654
+ catch (error) {
655
+ this.logger.error("❌ Logout failed", { error });
656
+ this.events.emit(AuthEvent.SIGN_OUT_ERROR, {
657
+ detail: error instanceof Error ? error.message : String(error),
658
+ });
659
+ throw new CivicAuthError("Logout failed", CivicAuthErrorCode.LOGOUT_FAILED);
660
+ }
661
+ }
688
662
  }
663
+ export { CivicAuthErrorCode } from "./types/AuthTypes.js";
689
664
  //# sourceMappingURL=CivicAuth.js.map