@civic/auth 0.6.1-beta.1 → 0.6.1-beta.2

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