@tidecloak/js 0.14.41-staging → 0.14.42-staging

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 (111) hide show
  1. package/README.md +32 -47
  2. package/dist/cjs/lib/tidecloak-dpop.js +151 -42
  3. package/dist/cjs/lib/tidecloak-dpop.js.map +1 -1
  4. package/dist/cjs/lib/tidecloak.js +300 -378
  5. package/dist/cjs/lib/tidecloak.js.map +1 -1
  6. package/dist/cjs/src/AdminAPI.js +12 -19
  7. package/dist/cjs/src/AdminAPI.js.map +1 -1
  8. package/dist/cjs/src/IAMService.js +155 -2098
  9. package/dist/cjs/src/IAMService.js.map +1 -1
  10. package/dist/cjs/src/index.js +1 -7
  11. package/dist/cjs/src/index.js.map +1 -1
  12. package/dist/esm/lib/tidecloak-dpop.js +151 -42
  13. package/dist/esm/lib/tidecloak-dpop.js.map +1 -1
  14. package/dist/esm/lib/tidecloak.js +300 -377
  15. package/dist/esm/lib/tidecloak.js.map +1 -1
  16. package/dist/esm/src/AdminAPI.js +3 -7
  17. package/dist/esm/src/AdminAPI.js.map +1 -1
  18. package/dist/esm/src/IAMService.js +152 -2065
  19. package/dist/esm/src/IAMService.js.map +1 -1
  20. package/dist/esm/src/index.js +1 -7
  21. package/dist/esm/src/index.js.map +1 -1
  22. package/dist/types/AdminAPI.d.ts +1 -1
  23. package/dist/types/IAMService.d.ts +52 -603
  24. package/dist/types/index.d.ts +1 -11
  25. package/dist/types/lib/tidecloak-dpop.d.ts +19 -3
  26. package/dist/types/lib/tidecloak.d.ts +46 -38
  27. package/dist/types/src/AdminAPI.d.ts +1 -1
  28. package/dist/types/src/IAMService.d.ts +52 -603
  29. package/dist/types/src/index.d.ts +1 -11
  30. package/docs/FRONT_CHANNEL.md +55 -55
  31. package/lib/README.md +10 -14
  32. package/package.json +4 -25
  33. package/dist/cjs/lib/secureFetchPolicy.js +0 -45
  34. package/dist/cjs/lib/secureFetchPolicy.js.map +0 -1
  35. package/dist/cjs/src/dpopRoute.js +0 -30
  36. package/dist/cjs/src/dpopRoute.js.map +0 -1
  37. package/dist/cjs/src/dpopServer.js +0 -77
  38. package/dist/cjs/src/dpopServer.js.map +0 -1
  39. package/dist/cjs/src/generated/dpopAuthAsset.js +0 -7
  40. package/dist/cjs/src/generated/dpopAuthAsset.js.map +0 -1
  41. package/dist/cjs/src/policy-react.js +0 -51
  42. package/dist/cjs/src/policy-react.js.map +0 -1
  43. package/dist/cjs/src/types.js +0 -6
  44. package/dist/cjs/src/types.js.map +0 -1
  45. package/dist/cjs/src/utils/dpopAuthPage.js +0 -69
  46. package/dist/cjs/src/utils/dpopAuthPage.js.map +0 -1
  47. package/dist/cjs/src/utils/fetch.js +0 -39
  48. package/dist/cjs/src/utils/fetch.js.map +0 -1
  49. package/dist/cjs/src/utils/index.js +0 -17
  50. package/dist/cjs/src/utils/index.js.map +0 -1
  51. package/dist/cjs/src/utils/initOptions.js +0 -78
  52. package/dist/cjs/src/utils/initOptions.js.map +0 -1
  53. package/dist/cjs/src/utils/pkce.js +0 -48
  54. package/dist/cjs/src/utils/pkce.js.map +0 -1
  55. package/dist/cjs/src/utils/silentCheckSso.js +0 -138
  56. package/dist/cjs/src/utils/silentCheckSso.js.map +0 -1
  57. package/dist/cjs/src/vite.js +0 -43
  58. package/dist/cjs/src/vite.js.map +0 -1
  59. package/dist/esm/lib/secureFetchPolicy.js +0 -42
  60. package/dist/esm/lib/secureFetchPolicy.js.map +0 -1
  61. package/dist/esm/src/dpopRoute.js +0 -27
  62. package/dist/esm/src/dpopRoute.js.map +0 -1
  63. package/dist/esm/src/dpopServer.js +0 -68
  64. package/dist/esm/src/dpopServer.js.map +0 -1
  65. package/dist/esm/src/generated/dpopAuthAsset.js +0 -4
  66. package/dist/esm/src/generated/dpopAuthAsset.js.map +0 -1
  67. package/dist/esm/src/policy-react.js +0 -45
  68. package/dist/esm/src/policy-react.js.map +0 -1
  69. package/dist/esm/src/types.js +0 -5
  70. package/dist/esm/src/types.js.map +0 -1
  71. package/dist/esm/src/utils/dpopAuthPage.js +0 -63
  72. package/dist/esm/src/utils/dpopAuthPage.js.map +0 -1
  73. package/dist/esm/src/utils/fetch.js +0 -36
  74. package/dist/esm/src/utils/fetch.js.map +0 -1
  75. package/dist/esm/src/utils/index.js +0 -6
  76. package/dist/esm/src/utils/index.js.map +0 -1
  77. package/dist/esm/src/utils/initOptions.js +0 -75
  78. package/dist/esm/src/utils/initOptions.js.map +0 -1
  79. package/dist/esm/src/utils/pkce.js +0 -43
  80. package/dist/esm/src/utils/pkce.js.map +0 -1
  81. package/dist/esm/src/utils/silentCheckSso.js +0 -134
  82. package/dist/esm/src/utils/silentCheckSso.js.map +0 -1
  83. package/dist/esm/src/vite.js +0 -40
  84. package/dist/esm/src/vite.js.map +0 -1
  85. package/dist/types/dpopRoute.d.ts +0 -11
  86. package/dist/types/dpopServer.d.ts +0 -35
  87. package/dist/types/generated/dpopAuthAsset.d.ts +0 -2
  88. package/dist/types/lib/secureFetchPolicy.d.ts +0 -37
  89. package/dist/types/policy-react.d.ts +0 -40
  90. package/dist/types/src/dpopRoute.d.ts +0 -11
  91. package/dist/types/src/dpopServer.d.ts +0 -35
  92. package/dist/types/src/generated/dpopAuthAsset.d.ts +0 -2
  93. package/dist/types/src/policy-react.d.ts +0 -40
  94. package/dist/types/src/types.d.ts +0 -114
  95. package/dist/types/src/utils/dpopAuthPage.d.ts +0 -22
  96. package/dist/types/src/utils/fetch.d.ts +0 -11
  97. package/dist/types/src/utils/index.d.ts +0 -5
  98. package/dist/types/src/utils/initOptions.d.ts +0 -69
  99. package/dist/types/src/utils/pkce.d.ts +0 -24
  100. package/dist/types/src/utils/silentCheckSso.d.ts +0 -82
  101. package/dist/types/src/vite.d.ts +0 -13
  102. package/dist/types/types.d.ts +0 -114
  103. package/dist/types/utils/dpopAuthPage.d.ts +0 -22
  104. package/dist/types/utils/fetch.d.ts +0 -11
  105. package/dist/types/utils/index.d.ts +0 -5
  106. package/dist/types/utils/initOptions.d.ts +0 -69
  107. package/dist/types/utils/pkce.d.ts +0 -24
  108. package/dist/types/utils/silentCheckSso.d.ts +0 -82
  109. package/dist/types/vite.d.ts +0 -13
  110. package/docs/HYBRID_MODE.md +0 -185
  111. package/docs/NATIVE_MODE.md +0 -284
@@ -1,2106 +1,193 @@
1
- import { makePkce, fetchJson, resolveSilentCheckSsoRedirectUri, buildInitOptions, preflightDpopAuthPage, } from "./utils/index.js";
2
- import TideCloak, { RequestEnclave } from "../lib/tidecloak.js";
3
- /**
4
- * Singleton IAMService wrapping the TideCloak client.
5
- *
6
- * Supports three modes:
7
- * - **Front-channel mode**: Browser handles all token operations (standard OIDC)
8
- * - **Hybrid/BFF mode**: Browser handles PKCE, backend exchanges code for tokens (more secure)
9
- * - **Native mode**: External browser for login, app handles tokens via adapter (Electron, Tauri, React Native)
10
- *
11
- * ---
12
- * ## Front-channel Mode
13
- *
14
- * Handlers are called with the event name first, then the event's arguments.
15
- *
16
- * Usage A: pass an onReady callback directly
17
- * ```js
18
- * import { IAMService } from '@tidecloak/js';
19
- * import tidecloakConfig from './tidecloak.json';
20
- *
21
- * IAMService.initIAM(tidecloakConfig, (_event, authenticated) => {
22
- * if (!authenticated) IAMService.doLogin();
23
- * }).catch(console.error);
24
- * ```
25
- *
26
- * Usage B: attach multiple listeners, then init
27
- * ```js
28
- * IAMService
29
- * .on('ready', (_event, auth) => console.log('ready', auth))
30
- * .on('authError', (_event, err) => console.error('Auth failed', err));
31
- *
32
- * await IAMService.initIAM(tidecloakConfig);
33
- * ```
34
- *
35
- * ---
36
- * ## Hybrid/BFF Mode (Backend-For-Frontend)
37
- *
38
- * In hybrid mode, the browser generates PKCE and redirects to the IdP, but the
39
- * backend exchanges the authorization code for tokens. This keeps tokens server-side
40
- * for improved security.
41
- *
42
- * ### Config shape:
43
- * ```js
44
- * const hybridConfig = {
45
- * authMode: "hybrid",
46
- * oidc: {
47
- * authorizationEndpoint: "https://auth.example.com/realms/myrealm/protocol/openid-connect/auth",
48
- * clientId: "my-client",
49
- * redirectUri: "https://app.example.com/auth/callback",
50
- * scope: "openid profile email", // optional, defaults to "openid profile email"
51
- * prompt: "login" // optional
52
- * },
53
- * tokenExchange: {
54
- * endpoint: "/api/authenticate", // Backend endpoint that exchanges code for tokens
55
- * provider: "tidecloak-auth", // optional, defaults to "tidecloak-auth"
56
- * headers: () => ({ // optional, custom headers (e.g., CSRF token)
57
- * "anti-csrf-token": getCSRFToken()
58
- * })
59
- * }
60
- * };
61
- * ```
62
- *
63
- * ### Login Page:
64
- * ```js
65
- * // Load config and trigger login
66
- * await IAMService.loadConfig(hybridConfig);
67
- * IAMService.doLogin("/dashboard"); // returnUrl after successful auth
68
- * ```
69
- *
70
- * ### Redirect/Callback Page:
71
- * ```js
72
- * // initIAM handles the callback automatically - exchanges code for tokens via backend
73
- * const authenticated = await IAMService.initIAM(hybridConfig);
74
- * if (authenticated) {
75
- * const returnUrl = IAMService.getReturnUrl() || "/";
76
- * window.location.assign(returnUrl);
77
- * }
78
- * ```
79
- *
80
- * ### Token Exchange Request Format:
81
- * The backend endpoint receives a POST request with:
82
- * ```json
83
- * {
84
- * "accessToken": "{\"code\":\"...\",\"code_verifier\":\"...\",\"redirect_uri\":\"...\"}",
85
- * "provider": "tidecloak-auth"
86
- * }
87
- * ```
88
- *
89
- * ---
90
- * ## Events
91
- * Every handler receives `(eventName, ...args)`.
92
- * - `ready` - Emitted when initialization completes (with authenticated boolean)
93
- * - `initError` - Emitted when initialization fails
94
- * - `authSuccess` - Emitted on successful authentication
95
- * - `authError` - Emitted on authentication failure
96
- * - `authRefreshSuccess` - Emitted when token refresh succeeds (front-channel only)
97
- * - `authRefreshError` - Emitted when token refresh fails (front-channel only)
98
- * - `logout` - Emitted on logout
99
- * - `tokenExpired` - Emitted when token expires (front-channel only)
100
- */
101
- class IAMService {
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
7
+ if (kind === "m") throw new TypeError("Private method is not writable");
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
+ };
12
+ var _IAMServiceImplementation_instances, _IAMServiceImplementation_core, _IAMServiceImplementation_listeners, _IAMServiceImplementation_initPromise, _IAMServiceImplementation_tide, _IAMServiceImplementation_emit, _IAMServiceImplementation_registerEvents, _IAMServiceImplementation_setTokenCookie, _IAMServiceImplementation_defaultInitOptions, _IAMServiceImplementation_getCore;
13
+ import TideCloak from "../lib/tidecloak.js";
14
+ class IAMServiceImplementation {
102
15
  constructor() {
103
- this._tc = null;
104
- this._config = null;
105
- this._listeners = {};
106
- // --- Hybrid mode state ---
107
- this._hybridAuthenticated = false;
108
- this._hybridReturnUrl = null;
109
- this._hybridCallbackHandled = false; // Guard against React StrictMode double-execution
110
- this._hybridCallbackPromise = null; // Promise for pending token exchange (for StrictMode)
111
- this._cachedCallbackData = null; // Cache for getHybridCallbackData to prevent data loss
112
- // --- Native mode state ---
113
- this._nativeAdapter = null;
114
- this._nativeAuthenticated = false;
115
- this._nativeTokens = null;
116
- this._nativeCallbackUnsubscribe = null;
117
- this._nativeCallbackHandled = false;
118
- this._nativeCallbackPromise = null;
119
- this._nativeCallbackProcessing = false; // Guard against concurrent callback processing
120
- // --- Native mode encryption state ---
121
- this._nativeDoken = null;
122
- this._nativeDokenParsed = null;
123
- this._nativeVoucher = null; // Voucher fetched during login for encryption
124
- this._nativeRequestEnclave = null;
125
- this._nativeEncryptionCallbackUnsubscribe = null;
126
- this._pendingEncryptionRequests = new Map(); // requestId -> { resolve, reject }
127
- // --- DPoP state ---
128
- this._dpopProvider = null;
129
- // --- Front-channel init state ---
130
- // Set while `_tc.init()` is in flight, cleared once it settles. Lets a
131
- // concurrent/repeat `initIAM()` (React StrictMode double-mount, a provider
132
- // effect that re-runs before the first init resolves) join the in-flight init
133
- // instead of short-circuiting past it and reporting a not-yet-populated state.
134
- this._frontchannelInitPromise = null;
135
- }
136
- /**
137
- * Register an event listener. The handler is called as `handler(event, ...args)`,
138
- * so the event name comes first, e.g. `on('ready', (_event, authenticated) => ...)`.
139
- * @param {'ready'|'initError'|'authSuccess'|'authError'|'authRefreshSuccess'|'authRefreshError'|'logout'|'tokenExpired'} event
140
- * @param {Function} handler
141
- * @returns {this}
142
- */
143
- on(event, handler) {
144
- if (!this._listeners[event])
145
- this._listeners[event] = [];
146
- this._listeners[event].push(handler);
147
- return this;
148
- }
149
- /**
150
- * Unregister an event listener.
151
- * @param {string} event
152
- * @param {Function} handler
153
- * @returns {this}
154
- */
155
- off(event, handler) {
156
- if (this._listeners[event]) {
157
- this._listeners[event] = this._listeners[event].filter(fn => fn !== handler);
158
- }
159
- return this;
160
- }
161
- /** @private */
162
- _emit(event, ...args) {
163
- (this._listeners[event] || []).forEach(fn => {
164
- try {
165
- fn(event, ...args);
166
- }
167
- catch (e) {
168
- console.error(`Error in "${event}" handler:`, e);
169
- }
170
- });
171
- }
172
- /**
173
- * Check if running in hybrid mode.
174
- * @returns {boolean}
175
- */
176
- isHybridMode() {
177
- var _a;
178
- return (((_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode) || "frontchannel").toLowerCase() === "hybrid";
179
- }
180
- /**
181
- * Check if running in native mode.
182
- * @returns {boolean}
183
- */
184
- isNativeMode() {
185
- var _a;
186
- return (((_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode) || "frontchannel").toLowerCase() === "native";
187
- }
188
- /**
189
- * Load TideCloak configuration and instantiate the client once.
190
- * @param {Object} config - TideCloak configuration object.
191
- * @returns {Promise<Object|null>} The loaded config, or null on failure.
192
- */
193
- async loadConfig(config) {
194
- var _a;
195
- if (this._config)
196
- return this._config;
197
- if (!config || Object.keys(config).length === 0) {
198
- console.warn("[loadConfig] empty config");
199
- return null;
200
- }
201
- // Shallow-copy so we never mutate the caller's object.
202
- this._config = { ...config };
203
- // ---------------------------------------------------------------------
204
- // DPoP is OPT-IN. Do NOT default it on. Ever.
205
- //
206
- // `useDPoP` is passed through to `TideCloak.init()` VERBATIM and only when
207
- // the caller actually set it (see ./utils/initOptions.js). If it is absent,
208
- // no DPoP provider is constructed, no `dpop_jkt` is appended to the
209
- // authorization request, and the realm issues a PLAIN access token.
210
- //
211
- // A previous revision defaulted this to `{ mode: "strict" }` whenever the
212
- // caller omitted it ("DPoP on by default"). That silently broke every
213
- // consumer that runs a deliberately non-DPoP flow: the authorization
214
- // request grew a `dpop_jkt`, Keycloak issued a `typ: DPoP` /
215
- // `cnf.jkt`-bound token, and the consumer's plain-Bearer fetch - correct
216
- // for the flow it thought it was in - was rejected with a bare `401`
217
- // (RFC 9449: a DPoP-bound token presented as a plain Bearer is invalid).
218
- // The TideCloak admin console's bootstrap path is exactly this: it omits
219
- // `useDPoP` on purpose and fetches with a plain Bearer.
220
- //
221
- // Enabling a sender-constraint the caller did not ask for changes the
222
- // shape of the issued token. That is never a safe default to invent.
223
- // ---------------------------------------------------------------------
224
- // Hybrid mode: do not construct TideCloak client (tokens are server-side)
225
- if (this.isHybridMode()) {
226
- return this._config;
227
- }
228
- // Native mode: store adapter, do not construct TideCloak client
229
- if (this.isNativeMode()) {
230
- if (!config.adapter) {
231
- console.error("[loadConfig] Native mode requires config.adapter with platform-specific functions");
232
- return null;
233
- }
234
- this._nativeAdapter = config.adapter;
235
- return this._config;
236
- }
237
- try {
238
- this._tc = new TideCloak({
239
- url: config["auth-server-url"],
240
- realm: config.realm,
241
- clientId: config.resource,
242
- vendorId: config.vendorId,
243
- homeOrkUrl: config.homeOrkUrl,
244
- clientOriginAuth: config['client-origin-auth-' + window.location.origin],
245
- backgroundUrl: config['backgroundUrl'],
246
- logoUrl: config['logoUrl'],
247
- setupRequestEnclave: (_a = config["setupRequestEnclave"]) !== null && _a !== void 0 ? _a : true // set true by default as its most likely for a client to need it
248
- });
249
- }
250
- catch (err) {
251
- console.error("[loadConfig] Failed to initialize TideCloak client:", err);
252
- return null;
253
- }
254
- // wire Tidecloak callbacks → our emitter
255
- this._tc.onReady = auth => this._emit("ready", auth);
256
- this._tc.onAuthSuccess = () => this._emit("authSuccess");
257
- this._tc.onAuthError = err => this._emit("authError", err);
258
- this._tc.onAuthRefreshSuccess = () => this._emit("authRefreshSuccess");
259
- this._tc.onAuthRefreshError = err => this._emit("authRefreshError", err);
260
- this._tc.onAuthLogout = () => this._emit("logout");
261
- this._tc.onTokenExpired = () => this._emit("tokenExpired");
262
- return this._config;
263
- }
264
- /**
265
- * Initialize the TideCloak SSO client with silent SSO check.
266
- * In hybrid mode, handles the redirect callback if present.
267
- * @param {Object} config - TideCloak configuration object.
268
- * @param {Function} [onReady] - Optional 'ready' handler, called as `(event, authenticated)`.
269
- * @returns {Promise<boolean>} true if authenticated, else false.
270
- */
271
- async initIAM(config, onReady) {
272
- console.debug("[IAMService] Initializing IAM...");
273
- // register callback on "ready" if provided
274
- if (typeof onReady === "function") {
275
- this.on("ready", onReady);
276
- }
277
- // no-op on server
278
- if (typeof window === "undefined") {
279
- this._emit("initError", new Error("SSR context: cannot initIAM on server"));
280
- return false;
281
- }
282
- // load IAM config
283
- const loaded = await this.loadConfig(config);
284
- if (!loaded) {
285
- this._emit("initError", new Error("Failed to load config"));
286
- return false;
287
- }
288
- // --- Hybrid mode init: handle redirect callback if present ---
289
- if (this.isHybridMode()) {
290
- // Guard against React StrictMode double-execution
291
- if (this._hybridCallbackHandled) {
292
- console.debug("[IAMService] Hybrid callback already handled");
293
- // If there's a pending token exchange, wait for it instead of returning stale state
294
- if (this._hybridCallbackPromise) {
295
- console.debug("[IAMService] Waiting for pending token exchange...");
296
- return this._hybridCallbackPromise;
297
- }
298
- this._emit("ready", this._hybridAuthenticated);
299
- return this._hybridAuthenticated;
300
- }
301
- // Check if this is a callback page (has code) - mark as handled BEFORE processing
302
- const qs = new URLSearchParams(window.location.search);
303
- if (qs.get("code")) {
304
- this._hybridCallbackHandled = true;
305
- }
306
- // Store the promise so subsequent calls can wait for it (React StrictMode)
307
- this._hybridCallbackPromise = (async () => {
308
- const { handled, authenticated, returnUrl } = await this._handleHybridRedirectCallback({
309
- onMissingVerifierRedirectTo: "/login",
310
- });
311
- this._hybridReturnUrl = returnUrl || null;
312
- // If we weren't on a callback page, emit ready with current state (defaults false)
313
- if (!handled) {
314
- this._emit("ready", this._hybridAuthenticated);
315
- }
316
- // Clear the promise once complete
317
- this._hybridCallbackPromise = null;
318
- return authenticated || this._hybridAuthenticated;
319
- })();
320
- return this._hybridCallbackPromise;
321
- }
322
- // --- Native mode init: check stored tokens and subscribe to callbacks ---
323
- if (this.isNativeMode()) {
324
- // Guard against React StrictMode double-execution
325
- if (this._nativeCallbackHandled) {
326
- console.debug("[IAMService] Native callback already handled");
327
- if (this._nativeCallbackPromise) {
328
- console.debug("[IAMService] Waiting for pending native token exchange...");
329
- return this._nativeCallbackPromise;
330
- }
331
- this._emit("ready", this._nativeAuthenticated);
332
- return this._nativeAuthenticated;
333
- }
334
- // Mark as handled BEFORE creating the promise to prevent
335
- // React StrictMode double-execution from re-initializing
336
- this._nativeCallbackHandled = true;
337
- this._nativeCallbackPromise = (async () => {
338
- var _a, _b;
339
- // Initialize DPoP if configured
340
- if (((_a = this._config) === null || _a === void 0 ? void 0 : _a.useDPoP) && !this._dpopProvider) {
341
- try {
342
- const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
343
- const issuerUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}`;
344
- const { DPoPSignatureProvider, BrowserSignatureAlgs } = await import('../lib/tidecloak-dpop.js');
345
- const alg = this._config.useDPoP.alg || 'ES256';
346
- this._dpopProvider = new DPoPSignatureProvider({
347
- issuerUrl: new URL(issuerUrl),
348
- clientId: clientId,
349
- serverSupportedAlgorithms: [alg],
350
- requestedAlgorithm: BrowserSignatureAlgs[alg] || BrowserSignatureAlgs.ES256,
351
- });
352
- await this._dpopProvider.init();
353
- console.debug("[IAMService] DPoP initialized for native mode");
354
- }
355
- catch (err) {
356
- console.warn("[IAMService] Failed to initialize DPoP:", err);
357
- if (this._config.useDPoP.mode === 'strict') {
358
- throw err;
359
- }
360
- // In 'auto' mode, continue without DPoP
361
- }
362
- }
363
- // Check for stored tokens
364
- const storedTokens = await this._nativeAdapter.getTokens();
365
- // sessionMode: 'online' (default) = validate tokens, refresh if needed, require login if invalid
366
- // sessionMode: 'offline' = accept stored tokens without validation (for offline-first apps)
367
- const sessionMode = ((_b = this._config) === null || _b === void 0 ? void 0 : _b.sessionMode) || 'online';
368
- if (storedTokens) {
369
- console.debug("[IAMService] Found stored tokens in native mode, sessionMode:", sessionMode);
370
- // Load doken if present
371
- if (storedTokens.doken) {
372
- this._nativeDoken = storedTokens.doken;
373
- this._nativeDokenParsed = this._parseToken(storedTokens.doken);
374
- console.debug("[IAMService] Loaded doken from stored tokens");
375
- }
376
- // Load voucher if present
377
- if (storedTokens.voucher) {
378
- this._nativeVoucher = storedTokens.voucher;
379
- console.debug("[IAMService] Loaded voucher from stored tokens");
380
- }
381
- if (sessionMode === 'offline') {
382
- // Offline mode: Accept stored tokens without validation
383
- this._nativeTokens = storedTokens;
384
- this._nativeAuthenticated = true;
385
- }
386
- else {
387
- // Online mode: Validate tokens before accepting
388
- try {
389
- const payload = JSON.parse(atob(storedTokens.accessToken.split('.')[1]));
390
- const exp = payload.exp * 1000;
391
- const now = Date.now();
392
- if (exp > now) {
393
- // Token still valid
394
- console.debug("[IAMService] Online mode: token valid, authenticating");
395
- this._nativeTokens = storedTokens;
396
- this._nativeAuthenticated = true;
397
- }
398
- else if (storedTokens.refreshToken) {
399
- // Token expired, try refresh
400
- console.debug("[IAMService] Online mode: token expired, attempting refresh");
401
- try {
402
- const newTokens = await this._refreshNativeToken(storedTokens.refreshToken);
403
- this._nativeTokens = newTokens;
404
- this._nativeAuthenticated = true;
405
- await this._nativeAdapter.saveTokens(newTokens);
406
- console.debug("[IAMService] Online mode: token refreshed successfully");
407
- }
408
- catch (refreshErr) {
409
- console.debug("[IAMService] Online mode: refresh failed, user must login", refreshErr);
410
- await this._nativeAdapter.deleteTokens();
411
- this._nativeAuthenticated = false;
412
- }
413
- }
414
- else {
415
- // Token expired, no refresh token
416
- console.debug("[IAMService] Online mode: token expired, no refresh token");
417
- await this._nativeAdapter.deleteTokens();
418
- this._nativeAuthenticated = false;
419
- }
420
- }
421
- catch (parseErr) {
422
- console.error("[IAMService] Online mode: failed to parse token", parseErr);
423
- await this._nativeAdapter.deleteTokens();
424
- this._nativeAuthenticated = false;
425
- }
426
- }
427
- }
428
- // Subscribe to auth callbacks from native app
429
- this._nativeCallbackUnsubscribe = this._nativeAdapter.onAuthCallback(async ({ code, voucher, error, errorDescription }) => {
430
- // Guard against duplicate callback processing
431
- if (this._nativeCallbackProcessing || this._nativeAuthenticated) {
432
- console.debug("[IAMService] Ignoring duplicate native callback");
433
- return;
434
- }
435
- if (error) {
436
- console.error("[IAMService] Native auth error:", error, errorDescription);
437
- this._emit("authError", new Error(`${error}: ${errorDescription || "Unknown error"}`));
438
- return;
439
- }
440
- if (code) {
441
- await this._handleNativeCallback(code, voucher);
442
- }
443
- });
444
- // Subscribe to encryption callbacks from native app (if adapter supports it)
445
- if (this._nativeAdapter.onEncryptionCallback) {
446
- this._nativeEncryptionCallbackUnsubscribe = this._nativeAdapter.onEncryptionCallback(({ operation, requestId, result, error }) => {
447
- const pending = this._pendingEncryptionRequests.get(requestId);
448
- if (pending) {
449
- this._pendingEncryptionRequests.delete(requestId);
450
- if (error) {
451
- pending.reject(new Error(error));
452
- }
453
- else if (result) {
454
- pending.resolve(result);
455
- }
456
- else {
457
- pending.reject(new Error("Empty result from encryption callback"));
458
- }
459
- }
460
- else {
461
- console.warn("[IAMService] Received encryption callback for unknown requestId:", requestId);
462
- }
463
- });
464
- }
465
- this._emit("ready", this._nativeAuthenticated);
466
- this._nativeCallbackPromise = null;
467
- return this._nativeAuthenticated;
468
- })();
469
- return this._nativeCallbackPromise;
470
- }
471
- // --- Front-channel mode ---
472
- if (!this._tc) {
473
- const err = new Error("TideCloak client not available");
474
- this._emit("initError", err);
475
- return false;
476
- }
477
- if (this._tc.didInitialize) {
478
- console.debug("[IAMService] IAM Already initialized once.");
479
- // An init started by an EARLIER call may still be in flight (React
480
- // StrictMode double-mounts; a provider effect that re-runs before the first
481
- // init settles). Its trailing `_emit("ready", ...)` has not happened yet, so
482
- // it will still reach the handler we registered at the top of this call -
483
- // just join it and let the event do the work.
484
- if (this._frontchannelInitPromise) {
485
- return this._frontchannelInitPromise;
486
- }
487
- // Init has already SETTLED, and its `ready` event has been emitted and is
488
- // gone. Anyone registering a handler after the fact - a React provider
489
- // re-running its effect and re-subscribing after a `reload()` /
490
- // `approveTideRequests()` - would hear NOTHING, forever. It would go on
491
- // serving whatever auth state it last observed while this singleton kept
492
- // refreshing its own token underneath.
493
- //
494
- // That desync is exactly how a `401` is manufactured: the consumer sends a
495
- // stale `Authorization: Bearer <old token>`, `TideCloak.secureFetch` no
496
- // longer recognises it as the token it holds, silently falls back to a plain
497
- // non-DPoP `fetch`, and a `dpop.bound.access.tokens` realm rejects a
498
- // DPoP-bound token presented as a plain Bearer.
499
- //
500
- // `ready` is a STATE notification, not a one-shot lifecycle signal, so
501
- // re-emitting the current state is safe and idempotent - every listener
502
- // simply re-syncs to the truth.
503
- const alreadyAuthenticated = !!this._tc.tokenParsed;
504
- this._emit("ready", alreadyAuthenticated);
505
- return alreadyAuthenticated;
506
- }
507
- this._frontchannelInitPromise = this._initFrontchannel(config);
508
- try {
509
- return await this._frontchannelInitPromise;
510
- }
511
- finally {
512
- this._frontchannelInitPromise = null;
513
- }
514
- }
515
- /**
516
- * Drive the one-and-only `TideCloak.init()` for front-channel mode and emit the
517
- * resulting `ready` state. Split out of {@link initIAM} so the in-flight promise
518
- * can be shared with concurrent callers.
519
- * @private
520
- * @param {Object} config
16
+ _IAMServiceImplementation_instances.add(this);
17
+ /**
18
+ * @type {TideCloak | null}
19
+ */
20
+ _IAMServiceImplementation_core.set(this, null);
21
+ _IAMServiceImplementation_listeners.set(this, {});
22
+ _IAMServiceImplementation_initPromise.set(this, null);
23
+ _IAMServiceImplementation_tide.set(this, Object.freeze({
24
+ encrypt: (toEncrypt, decryptionPolicy) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).encrypt(toEncrypt, decryptionPolicy),
25
+ decrypt: (toDecrypt, decryptionPolicy) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).decrypt(toDecrypt, decryptionPolicy),
26
+ draftEncryption: (toEncrypt) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).draftEncryption(toEncrypt),
27
+ commitEncryption: (request, decryptionPolicy) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).commitEncryption(request, decryptionPolicy),
28
+ draftDecryption: (toDecrypt) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).draftDecryption(toDecrypt),
29
+ commitDecryption: (request, decryptionPolicy) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).commitDecryption(request, decryptionPolicy),
30
+ createTideRequest: (encodedRequest) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).createTideRequest(encodedRequest),
31
+ requestTideOperatorApproval: (requests) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).requestTideOperatorApproval(requests),
32
+ executeSignRequest: (request, waitForAll) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).executeSignRequest(request, waitForAll),
33
+ }));
34
+ }
35
+ get tide() {
36
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_tide, "f");
37
+ }
38
+ /**
39
+ * Initialize from the TideCloak adapter config (tidecloak.json)
40
+ * @param {Record<string, any>} json The contents of tidecloak.json
41
+ * @param {import("../lib/tidecloak.js").TideCloakInitOptions | undefined} options Overrides the defaults
42
+ * (check-sso, S256 PKCE, request enclave on, dpopConfig / checkLoginIframe from tidecloak.json, and
43
+ * silentCheckSsoRedirectUri at `<origin>/silent-check-sso.html`). Apps that serve the silent SSO page
44
+ * elsewhere pass their own `silentCheckSsoRedirectUri`.
521
45
  * @returns {Promise<boolean>}
522
46
  */
523
- async _initFrontchannel(config) {
524
- var _a, _b, _c;
525
- // Read an init option from the resolved config. `loadConfig` assigns
526
- // `this._config = config`, so these are normally the same object; we check
527
- // both so a caller that passes options only to `initIAM` is still honoured.
528
- const pick = (key) => { var _a; var _b; return (_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a[key]) !== null && _b !== void 0 ? _b : config === null || config === void 0 ? void 0 : config[key]; };
529
- // --- Silent-check-SSO redirect URI ---------------------------------------
530
- // This used to be hardcoded to `${window.location.origin}/silent-check-sso.html`
531
- // with NO caller override, which is only correct for an origin-root-hosted
532
- // app. Any app served under a sub-path got a URI that is neither served nor
533
- // registered on the OIDC client, so Keycloak answered `400 Invalid parameter:
534
- // redirect_uri`, the silent iframe never posted back, and the app hung on its
535
- // bootstrap spinner. Resolve it properly instead: an explicit caller value
536
- // always wins, otherwise derive from the app's declared base. See
537
- // ./utils/silentCheckSso.js.
538
- const hasBaseElement = typeof document !== "undefined" && !!document.querySelector("base[href]");
539
- const silentSso = resolveSilentCheckSsoRedirectUri({
540
- origin: window.location.origin,
541
- configured: pick("silentCheckSsoRedirectUri"),
542
- // `document.baseURI` is the browser's resolution of `<base href>`. Only pass
543
- // it when such an element actually exists - otherwise it is just the current
544
- // page URL, which for a deep SPA route would derive a bogus base.
545
- baseHref: hasBaseElement ? document.baseURI : undefined,
546
- redirectUri: pick("redirectUri"),
547
- });
548
- if (!silentSso.uri) {
549
- // FAIL LOUD. Passing `undefined` makes TideCloak skip the silent check
550
- // entirely and fall back to a redirect-based `prompt=none` login, which
551
- // still authenticates the user. That is strictly better than emitting a
552
- // URI we know will 400 and wedge the app on its spinner.
553
- console.error(`[IAMService] Cannot derive a silent-check-sso redirect URI: ${silentSso.reason}. ` +
554
- "Skipping silent check-sso and falling back to a redirect-based login. " +
555
- "Set `silentCheckSsoRedirectUri` in your TideCloak config to a URI that is " +
556
- "both served by your app and registered on the OIDC client.");
557
- }
558
- else if (silentSso.source !== "config") {
559
- console.debug(`[IAMService] silentCheckSsoRedirectUri derived from ${silentSso.source}: ${silentSso.uri}`);
560
- }
561
- // Not awaited: a diagnostic must not slow init.
562
- if (pick("useDPoP")) {
563
- const cfg = (_a = this._config) !== null && _a !== void 0 ? _a : config;
564
- void preflightDpopAuthPage({
565
- origin: window.location.origin,
566
- authServerUrl: cfg === null || cfg === void 0 ? void 0 : cfg["auth-server-url"],
567
- realm: cfg === null || cfg === void 0 ? void 0 : cfg.realm,
568
- clientId: cfg === null || cfg === void 0 ? void 0 : cfg.resource,
569
- });
570
- }
571
- let authenticated = false;
572
- try {
573
- authenticated = await this._tc.init(buildInitOptions({
574
- config: (_b = this._config) !== null && _b !== void 0 ? _b : config,
575
- setupRequestEnclave: (_c = config.setupRequestEnclave) !== null && _c !== void 0 ? _c : true, // true by default because most clients that uses this will need it on
576
- silentCheckSsoRedirectUri: silentSso.uri,
577
- }));
578
- // if successful, store token for middleware
579
- if (authenticated && this._tc.token) {
580
- this._setTokenCookie(this._tc.token);
581
- }
582
- }
583
- catch (err) {
584
- console.error("[IAMService] TideCloak init error:", err);
585
- this._emit("initError", err);
586
- }
587
- this._emit("ready", authenticated);
588
- return authenticated;
589
- }
590
- /** @private */
591
- getTideCloakClient() {
592
- if (!this._tc) {
593
- throw new Error("TideCloak client not initialized - call initIAM() first");
47
+ init(json, options) {
48
+ if (typeof window === 'undefined')
49
+ return Promise.resolve(false);
50
+ if (__classPrivateFieldGet(this, _IAMServiceImplementation_initPromise, "f"))
51
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_initPromise, "f");
52
+ const tidecloakConfig = {
53
+ ...json,
54
+ url: json['auth-server-url'],
55
+ realm: json.realm,
56
+ clientId: json.resource,
57
+ clientOriginAuth: json[`client-origin-auth-${window.location.origin}`],
58
+ };
59
+ if (!__classPrivateFieldGet(this, _IAMServiceImplementation_core, "f"))
60
+ __classPrivateFieldSet(this, _IAMServiceImplementation_core, new TideCloak(tidecloakConfig), "f");
61
+ __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_registerEvents).call(this);
62
+ if (!__classPrivateFieldGet(this, _IAMServiceImplementation_initPromise, "f")) {
63
+ __classPrivateFieldSet(this, _IAMServiceImplementation_initPromise, __classPrivateFieldGet(this, _IAMServiceImplementation_core, "f").init({ ...__classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_defaultInitOptions).call(this, json), ...options }).catch((err) => {
64
+ // A TideCloak instance can only be initialized once, so a retry needs a fresh one.
65
+ __classPrivateFieldSet(this, _IAMServiceImplementation_initPromise, null, "f");
66
+ __classPrivateFieldSet(this, _IAMServiceImplementation_core, null, "f");
67
+ throw err;
68
+ }), "f");
69
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_initPromise, "f");
594
70
  }
595
- return this._tc;
596
- }
597
- /**
598
- * Write the access token to the `kcToken` cookie used by the server middleware.
599
- * Uses `SameSite=Lax` to blunt CSRF (the cookie is not sent on cross-site
600
- * subrequests) while still allowing the post-login top-level redirect, and
601
- * marks it `Secure` whenever the page is served over HTTPS.
602
- * @private
603
- */
604
- _setTokenCookie(token) {
605
- var _a;
606
- const secure = typeof window !== "undefined" && ((_a = window.location) === null || _a === void 0 ? void 0 : _a.protocol) === "https:" ? "; Secure" : "";
607
- document.cookie = `kcToken=${token}; path=/; SameSite=Lax${secure}`;
608
- }
609
- /** @private */
610
- _clearTokenCookie() {
611
- document.cookie =
612
- "kcToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
613
71
  }
614
72
  /**
615
- * Restrict a post-login return URL to a same-origin, root-relative path to
616
- * prevent open-redirect. The OAuth `state` (and the sessionStorage fallback)
617
- * round-trips this value and is then handed to `window.location.assign`, so an
618
- * attacker-seeded absolute/protocol-relative URL ("https://evil", "//evil",
619
- * "/\\evil", "javascript:...") must be rejected. Falls back to "/".
620
- * @private
73
+ *
74
+ * @param {import("../lib/tidecloak.js").TideCloakLoginOptions?} loginOpts
621
75
  */
622
- _sanitizeReturnUrl(url) {
623
- if (typeof url !== "string" || url.length === 0)
624
- return "";
625
- // Must be a single-leading-slash path; reject protocol-relative "//host"
626
- // and backslash variants browsers normalise to "//".
627
- if (url[0] !== "/" || url[1] === "/" || url[1] === "\\")
628
- return "/";
629
- // Defensive: reject anything that still parses as having a URL scheme.
630
- if (/^[a-z][a-z0-9+.-]*:/i.test(url))
631
- return "/";
632
- return url;
633
- }
634
- /** @returns {Object} Loaded config */
635
- getConfig() {
636
- if (!this._config) {
637
- throw new Error("Config not loaded - call initIAM() first");
638
- }
639
- return this._config;
76
+ async doLogin(loginOpts) {
77
+ await __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).login(loginOpts);
640
78
  }
641
79
  /**
642
- * Whether initialization has already run to completion for the current mode.
643
- *
644
- * "Settled", not "started": while an init is still in flight this returns
645
- * `false`, because the auth state it will produce does not exist yet and reading
646
- * it would report a spurious logged-out state.
647
- *
648
- * Consumers that subscribe to events LATE (a React provider re-running its
649
- * effect and re-subscribing, say) must use this to re-read the current auth
650
- * state synchronously rather than waiting on a `ready` event that has already
651
- * been emitted and is gone.
652
80
  *
653
- * @returns {boolean}
81
+ * @param {import("../lib/tidecloak.js").TideCloakLogoutOptions?} logoutOpts
654
82
  */
655
- isInitialized() {
656
- var _a;
657
- if (this.isHybridMode()) {
658
- return this._hybridCallbackHandled && !this._hybridCallbackPromise;
659
- }
660
- if (this.isNativeMode()) {
661
- return this._nativeCallbackHandled && !this._nativeCallbackPromise;
662
- }
663
- return !!((_a = this._tc) === null || _a === void 0 ? void 0 : _a.didInitialize) && !this._frontchannelInitPromise;
83
+ async doLogout(logoutOpts) {
84
+ __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_setTokenCookie).call(this);
85
+ await __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).logout(logoutOpts);
664
86
  }
665
- /** @returns {boolean} Whether there's a valid token (or session in hybrid/native mode) */
666
- isLoggedIn() {
667
- if (this.isHybridMode())
668
- return this._hybridAuthenticated;
669
- if (this.isNativeMode())
670
- return this._nativeAuthenticated;
671
- return !!this.getTideCloakClient().token;
87
+ getToken() {
88
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).token;
672
89
  }
673
90
  /**
674
- * Get valid token (refreshing if needed). Resolves to `null` in native mode
675
- * when no tokens are stored or a refresh fails.
676
- * @returns {Promise<string|null>}
677
- * @throws {Error} In hybrid mode (tokens are server-side)
678
- */
679
- async getToken() {
680
- var _a, _b;
681
- if (this.isHybridMode()) {
682
- throw new Error("getToken() not available in hybrid mode - tokens are server-side");
683
- }
684
- if (this.isNativeMode()) {
685
- const tokens = await this._nativeAdapter.getTokens();
686
- if (!tokens)
687
- return null;
688
- // Check if token is expired or about to expire (30 second buffer)
689
- const now = Date.now();
690
- const bufferMs = 30 * 1000;
691
- if (now >= tokens.expiresAt - bufferMs) {
692
- console.debug("[IAMService] Native token expired, refreshing...");
693
- try {
694
- const newTokens = await this._refreshNativeToken(tokens.refreshToken);
695
- await this._nativeAdapter.saveTokens(newTokens);
696
- this._nativeTokens = newTokens;
697
- return newTokens.accessToken;
698
- }
699
- catch (err) {
700
- console.error("[IAMService] Native token refresh failed:", err);
701
- // The refresh token is no longer usable - drop the session so callers
702
- // don't keep sending a stale/`null` bearer token, and surface the error.
703
- this._nativeAuthenticated = false;
704
- this._nativeTokens = null;
705
- try {
706
- await ((_b = (_a = this._nativeAdapter) === null || _a === void 0 ? void 0 : _a.deleteTokens) === null || _b === void 0 ? void 0 : _b.call(_a));
707
- }
708
- catch (_) { /* best-effort cleanup */ }
709
- this._emit("authError", err);
710
- return null;
711
- }
712
- }
713
- return tokens.accessToken;
714
- }
715
- const exp = this.getTokenExp();
716
- if (exp < 3)
717
- await this.updateIAMToken();
718
- return this.getTideCloakClient().token;
719
- }
720
- /**
721
- * Seconds until token expiry.
722
- * @returns {number}
723
- * @throws {Error} In hybrid mode (tokens are server-side)
91
+ * When the access token expires, in browser time (adjusted for server clock skew).
92
+ * @returns {Date | undefined}
724
93
  */
725
94
  getTokenExp() {
726
- var _a;
727
- if (this.isHybridMode()) {
728
- throw new Error("getTokenExp() not available in hybrid mode - tokens are server-side");
729
- }
730
- if (this.isNativeMode()) {
731
- if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
732
- return 0;
733
- try {
734
- const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
735
- return Math.round(payload.exp - Date.now() / 1000);
736
- }
737
- catch (e) {
738
- console.error("[IAMService] Failed to parse token for expiry:", e);
739
- return 0;
740
- }
741
- }
742
- const kc = this.getTideCloakClient();
743
- return Math.round(kc.tokenParsed.exp + kc.timeSkew - Date.now() / 1000);
744
- }
745
- /**
746
- * Get ID token.
747
- * @returns {string|null}
748
- * @throws {Error} In hybrid mode (tokens are server-side)
749
- */
750
- getIDToken() {
751
- var _a;
752
- if (this.isHybridMode()) {
753
- throw new Error("getIDToken() not available in hybrid mode - tokens are server-side");
754
- }
755
- if (this.isNativeMode()) {
756
- return ((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.idToken) || null;
757
- }
758
- return this.getTideCloakClient().idToken;
759
- }
760
- /**
761
- * Get username (preferred_username claim).
762
- * @returns {string}
763
- * @throws {Error} In hybrid mode (tokens are server-side)
764
- */
765
- getName() {
766
- if (this.isHybridMode()) {
767
- throw new Error("getName() not available in hybrid mode - tokens are server-side");
768
- }
769
- if (this.isNativeMode()) {
770
- return this.getValueFromToken('preferred_username');
771
- }
772
- return this.getTideCloakClient().tokenParsed.preferred_username;
773
- }
774
- /**
775
- * Get the return URL after successful hybrid authentication.
776
- * Only available in hybrid mode after successful auth.
777
- * @returns {string|null}
778
- */
779
- getReturnUrl() {
780
- return this._hybridReturnUrl;
781
- }
782
- /**
783
- * Check if user has a realm role.
784
- * @param {string} role - the name of the role to check
785
- * @returns {boolean} Whether the user has a given realm role
786
- * @throws {Error} In hybrid mode (role checks not available client-side)
787
- */
788
- hasRealmRole(role) {
789
- var _a, _b;
790
- if (this.isHybridMode()) {
791
- throw new Error("hasRealmRole() not available in hybrid mode - tokens are server-side");
792
- }
793
- if (this.isNativeMode()) {
794
- if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
795
- return false;
796
- try {
797
- const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
798
- const realmRoles = ((_b = payload.realm_access) === null || _b === void 0 ? void 0 : _b.roles) || [];
799
- return realmRoles.includes(role);
800
- }
801
- catch (e) {
802
- console.error("[IAMService] Failed to parse token for realm role check:", e);
803
- return false;
804
- }
805
- }
806
- return this.getTideCloakClient().hasRealmRole(role);
807
- }
808
- /**
809
- * Check if user has a client role.
810
- * @param {string} role - the name of the role to check
811
- * @param {string} [client] - optional client-ID (defaults to the configured adapter resource)
812
- * @returns {boolean} - whether the user has that role
813
- * @throws {Error} In hybrid mode (role checks not available client-side)
814
- */
815
- hasClientRole(role, client) {
816
- var _a, _b, _c, _d;
817
- if (this.isHybridMode()) {
818
- throw new Error("hasClientRole() not available in hybrid mode - tokens are server-side");
819
- }
820
- if (this.isNativeMode()) {
821
- if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
822
- return false;
823
- try {
824
- const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
825
- const clientId = client || ((_b = this._config) === null || _b === void 0 ? void 0 : _b.resource);
826
- const clientRoles = ((_d = (_c = payload.resource_access) === null || _c === void 0 ? void 0 : _c[clientId]) === null || _d === void 0 ? void 0 : _d.roles) || [];
827
- return clientRoles.includes(role);
828
- }
829
- catch (e) {
830
- console.error("[IAMService] Failed to parse token for client role check:", e);
831
- return false;
832
- }
833
- }
834
- return this.getTideCloakClient().hasResourceRole(role, client);
835
- }
836
- /**
837
- * Get custom claim from access token.
838
- * @param {string} key - The name of the claim to retrieve from the Access token's payload.
839
- * @returns {*} Custom claim from access token
840
- * @throws {Error} In hybrid mode (tokens are server-side)
841
- */
842
- getValueFromToken(key) {
843
95
  var _a;
844
96
  var _b;
845
- if (this.isHybridMode()) {
846
- throw new Error("getValueFromToken() not available in hybrid mode - tokens are server-side");
847
- }
848
- if (this.isNativeMode()) {
849
- if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
850
- return null;
851
- try {
852
- const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
853
- return payload[key] !== undefined ? payload[key] : null;
854
- }
855
- catch (e) {
856
- console.error("[IAMService] Failed to parse access token:", e);
857
- return null;
858
- }
859
- }
860
- return (_b = this.getTideCloakClient().tokenParsed[key]) !== null && _b !== void 0 ? _b : null;
861
- }
862
- /**
863
- * Get custom claim from ID token.
864
- * @param {string} key - The name of the claim to retrieve from the ID token's payload.
865
- * @returns {*} Custom claim from ID token
866
- * @throws {Error} In hybrid mode (tokens are server-side)
867
- */
868
- getValueFromIDToken(key) {
869
- var _a;
870
- var _b;
871
- if (this.isHybridMode()) {
872
- throw new Error("getValueFromIDToken() not available in hybrid mode - tokens are server-side");
873
- }
874
- if (this.isNativeMode()) {
875
- if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.idToken))
876
- return null;
877
- try {
878
- const payload = JSON.parse(atob(this._nativeTokens.idToken.split('.')[1]));
879
- return payload[key] !== undefined ? payload[key] : null;
880
- }
881
- catch (e) {
882
- console.error("[IAMService] Failed to parse ID token:", e);
883
- return null;
884
- }
885
- }
886
- return (_b = this.getTideCloakClient().idTokenParsed[key]) !== null && _b !== void 0 ? _b : null;
887
- }
888
- /**
889
- * Alias of {@link IAMService#getValueFromIDToken}, with the same casing as the React context.
890
- * @param {string} key - The name of the claim to retrieve from the ID token's payload.
891
- * @returns {*} Custom claim from ID token
892
- * @throws {Error} In hybrid mode (tokens are server-side)
893
- */
894
- getValueFromIdToken(key) {
895
- return this.getValueFromIDToken(key);
896
- }
897
- /**
898
- * Refreshes token if expired or about to expire.
899
- * @returns {Promise<boolean>}
900
- * @throws {Error} In hybrid mode (token refresh handled server-side)
901
- */
902
- async updateIAMToken() {
903
- if (this.isHybridMode()) {
904
- throw new Error("updateIAMToken() not available in hybrid mode - tokens are server-side");
905
- }
906
- if (this.isNativeMode()) {
907
- // Native mode token refresh is handled in getToken(). Only report a refresh
908
- // as performed when the token was actually expiring AND getToken() returned a
909
- // fresh token (a failed refresh resolves to null and must not report success).
910
- const exp = this.getTokenExp();
911
- if (exp < 30) {
912
- const refreshedToken = await this.getToken();
913
- return refreshedToken != null;
914
- }
915
- return false;
916
- }
917
- const kc = this.getTideCloakClient();
918
- const refreshed = await kc.updateToken();
919
- const expiresIn = this.getTokenExp();
920
- console.debug(refreshed
921
- ? `[updateIAMToken] Refreshed: ${expiresIn}s`
922
- : `[updateIAMToken] Still valid: ${expiresIn}s`);
923
- this._setTokenCookie(kc.token);
924
- return refreshed;
97
+ const exp = (_a = __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).tokenParsed) === null || _a === void 0 ? void 0 : _a.exp;
98
+ if (exp === undefined)
99
+ return undefined;
100
+ return new Date((exp + ((_b = __classPrivateFieldGet(this, _IAMServiceImplementation_core, "f").timeSkew) !== null && _b !== void 0 ? _b : 0)) * 1000);
925
101
  }
926
- /**
927
- * Force immediate refresh (min validity = -1).
928
- * @returns {Promise<boolean>}
929
- * @throws {Error} In hybrid mode (token refresh handled server-side)
930
- */
931
- async forceUpdateToken() {
102
+ getClaim(name) {
932
103
  var _a;
933
- if (this.isHybridMode()) {
934
- throw new Error("forceUpdateToken() not available in hybrid mode - tokens are server-side");
935
- }
936
- if (this.isNativeMode()) {
937
- // Force refresh by clearing cached tokens and refreshing
938
- if ((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.refreshToken) {
939
- try {
940
- const newTokens = await this._refreshNativeToken(this._nativeTokens.refreshToken);
941
- this._nativeTokens = newTokens;
942
- await this._nativeAdapter.saveTokens(newTokens);
943
- return true;
944
- }
945
- catch (err) {
946
- console.error("[IAMService] Native force refresh failed:", err);
947
- return false;
948
- }
949
- }
950
- return false;
951
- }
952
- this._clearTokenCookie();
953
- const kc = this.getTideCloakClient();
954
- const refreshed = await kc.updateToken(-1);
955
- const expiresIn = this.getTokenExp();
956
- console.debug(refreshed
957
- ? `[updateToken] Immediately refreshed: ${expiresIn}s`
958
- : `[updateToken] No refresh needed: ${expiresIn}s`);
959
- this._setTokenCookie(kc.token);
960
- return refreshed;
104
+ return (_a = __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).tokenParsed) === null || _a === void 0 ? void 0 : _a[name];
961
105
  }
962
- /**
963
- * Start login redirect.
964
- * In hybrid mode, initiates PKCE flow and redirects to IdP.
965
- * In native mode, opens external browser with auth URL.
966
- * @param {string} [returnUrl] - URL to redirect to after successful auth (hybrid/native mode)
967
- * @returns {void|Promise<void>} Resolves once the redirect/external-browser flow is initiated (hybrid/native); returns void in front-channel mode.
968
- */
969
- doLogin(returnUrl = "") {
106
+ getName() {
970
107
  var _a;
971
- var _b;
972
- console.debug("[IAMService.doLogin] Called with returnUrl:", returnUrl);
973
- console.debug("[IAMService.doLogin] isHybridMode:", this.isHybridMode());
974
- console.debug("[IAMService.doLogin] isNativeMode:", this.isNativeMode());
975
- console.debug("[IAMService.doLogin] authMode config:", (_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode);
976
- if (this.isHybridMode()) {
977
- // Catch and log any errors from the async function
978
- return this._startHybridLogin(returnUrl).catch(err => {
979
- console.error("[IAMService.doLogin] Error in hybrid login:", err);
980
- throw err;
981
- });
982
- }
983
- if (this.isNativeMode()) {
984
- return this._startNativeLogin(returnUrl).catch(err => {
985
- console.error("[IAMService.doLogin] Error in native login:", err);
986
- throw err;
987
- });
988
- }
989
- this.getTideCloakClient().login({
990
- redirectUri: (_b = this._config["redirectUri"]) !== null && _b !== void 0 ? _b : `${window.location.origin}/auth/redirect`
991
- });
992
- }
993
- /**
994
- * Encrypt data via adapter.
995
- * Not available in hybrid mode (encryption requires client-side doken).
996
- * In native mode, passing a decryption policy throws.
997
- * @param {{ data: string | Uint8Array, tags: string[] }[]} data - Array of objects to encrypt
998
- * @param {Uint8Array} [decryption_policy] Optional policy to protect the encrypted data
999
- * @returns {Promise<(string | Uint8Array)[]>} Array of encrypted values
1000
- */
1001
- async doEncrypt(data, decryption_policy = null) {
1002
- if (this.isHybridMode()) {
1003
- throw new Error("Encrypt not supported in hybrid mode (tokens are server-side)");
1004
- }
1005
- if (this.isNativeMode()) {
1006
- // The native path doesn't forward policies, so fail rather than drop it.
1007
- if (decryption_policy != null) {
1008
- throw new Error("encrypt with a decryption policy not supported in native mode");
1009
- }
1010
- return this._nativeEncrypt(data);
1011
- }
1012
- return this.getTideCloakClient().encrypt(data, decryption_policy);
108
+ return (_a = __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).tokenParsed) === null || _a === void 0 ? void 0 : _a.preferred_username;
1013
109
  }
1014
- /**
1015
- * Decrypt data via adapter.
1016
- * Not available in hybrid mode (decryption requires client-side doken).
1017
- * In native mode, passing a decryption policy throws.
1018
- * @param {{ encrypted: string | Uint8Array, tags: string[] }[]} data - Array of objects to decrypt
1019
- * @param {Uint8Array} [decryption_policy] Optional policy the data was encrypted under
1020
- * @returns {Promise<(string | Uint8Array)[]>} Array of decrypted values
1021
- */
1022
- async doDecrypt(data, decryption_policy = null) {
1023
- if (this.isHybridMode()) {
1024
- throw new Error("Decrypt not supported in hybrid mode (tokens are server-side)");
1025
- }
1026
- if (this.isNativeMode()) {
1027
- // The native path doesn't forward policies, so fail rather than drop it.
1028
- if (decryption_policy != null) {
1029
- throw new Error("decrypt with a decryption policy not supported in native mode");
1030
- }
1031
- return this._nativeDecrypt(data);
1032
- }
1033
- return this.getTideCloakClient().decrypt(data, decryption_policy);
1034
- }
1035
- /**
1036
- * Draft an encryption request (two-step encryption flow).
1037
- * Not available in hybrid or native mode.
1038
- * @param {{ data: Uint8Array, tags: string[] }[]} data - Array of objects to draft encryption for
1039
- * @returns {Promise<Uint8Array>} Draft encryption request
1040
- */
1041
- async doDraftEncryption(data) {
1042
- if (this.isHybridMode()) {
1043
- throw new Error("draftEncryption not supported in hybrid mode (tokens are server-side)");
1044
- }
1045
- if (this.isNativeMode()) {
1046
- throw new Error("draftEncryption not supported in native mode");
1047
- }
1048
- return this.getTideCloakClient().draftEncryption(data);
110
+ hasRealmRole(role) {
111
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).hasRealmRole(role);
1049
112
  }
1050
- /**
1051
- * Commit a drafted encryption request with a specified policy.
1052
- * Not available in hybrid or native mode.
1053
- * @param {Uint8Array} request - Draft request from doDraftEncryption
1054
- * @param {Uint8Array} decryption_policy - Policy to protect the encrypted data
1055
- * @returns {Promise<Uint8Array[]>} Encrypted data
1056
- */
1057
- async doCommitEncryption(request, decryption_policy) {
1058
- if (this.isHybridMode()) {
1059
- throw new Error("commitEncryption not supported in hybrid mode (tokens are server-side)");
1060
- }
1061
- if (this.isNativeMode()) {
1062
- throw new Error("commitEncryption not supported in native mode");
1063
- }
1064
- return this.getTideCloakClient().commitEncryption(request, decryption_policy);
113
+ hasClientRole(role) {
114
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).hasResourceRole(role);
1065
115
  }
1066
- /**
1067
- * Draft a decryption request (two-step decryption flow).
1068
- * Not available in hybrid or native mode.
1069
- * @param {{ encrypted: Uint8Array, tags: string[] }[]} data - Array of objects to draft decryption for
1070
- * @returns {Promise<Uint8Array>} Draft decryption request
1071
- */
1072
- async doDraftDecryption(data) {
1073
- if (this.isHybridMode()) {
1074
- throw new Error("draftDecryption not supported in hybrid mode (tokens are server-side)");
1075
- }
1076
- if (this.isNativeMode()) {
1077
- throw new Error("draftDecryption not supported in native mode");
1078
- }
1079
- return this.getTideCloakClient().draftDecryption(data);
116
+ updateToken() {
117
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).updateToken();
1080
118
  }
1081
- /**
1082
- * Commit a drafted decryption request.
1083
- * Not available in hybrid or native mode.
1084
- * @param {Uint8Array} request - Draft request from doDraftDecryption
1085
- * @param {Uint8Array} decryption_policy - Policy to decrypt the data
1086
- * @returns {Promise<Uint8Array[]>} Decrypted data
1087
- */
1088
- async doCommitDecryption(request, decryption_policy) {
1089
- if (this.isHybridMode()) {
1090
- throw new Error("commitDecryption not supported in hybrid mode (tokens are server-side)");
1091
- }
1092
- if (this.isNativeMode()) {
1093
- throw new Error("commitDecryption not supported in native mode");
1094
- }
1095
- return this.getTideCloakClient().commitDecryption(request, decryption_policy);
119
+ forceUpdateToken() {
120
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).updateToken(-1);
1096
121
  }
1097
122
  /**
1098
- * Drop-in replacement for fetch that automatically handles DPoP authentication.
1099
- * If the request includes a Bearer token matching the TideCloak-managed token,
1100
- * it's replaced with DPoP authorization and proof. Also manages resource server
1101
- * nonces automatically. Otherwise, behaves like regular fetch.
1102
- *
1103
- * @param {string|URL|RequestInfo} url - The resource URL to fetch
1104
- * @param {RequestInit} [init] - Optional fetch init options (same as standard fetch)
1105
- * @returns {Promise<Response>} A promise that resolves to the fetch Response
1106
- * @throws {Error} In hybrid mode (tokens are server-side)
1107
123
  *
1108
- * @example
1109
- * ```js
1110
- * const token = await IAMService.getToken();
1111
- * const response = await IAMService.secureFetch('https://api.example.com/data', {
1112
- * method: 'POST',
1113
- * headers: {
1114
- * 'Authorization': `Bearer ${token}`,
1115
- * 'Content-Type': 'application/json',
1116
- * },
1117
- * body: JSON.stringify({ key: 'value' }),
1118
- * });
1119
- * ```
1120
- */
1121
- async secureFetch(url, init) {
1122
- if (this.isHybridMode()) {
1123
- throw new Error("secureFetch() not available in hybrid mode - tokens are server-side");
1124
- }
1125
- if (this.isNativeMode()) {
1126
- return this._nativeSecureFetch(url, init);
1127
- }
1128
- return this.getTideCloakClient().secureFetch(url, init);
1129
- }
1130
- /**
1131
- * Native mode secure fetch with DPoP support.
1132
- * @private
1133
- */
1134
- async _nativeSecureFetch(url, init = {}) {
1135
- if (!this._dpopProvider) {
1136
- // No DPoP configured, fall through to regular fetch
1137
- return fetch(url, init);
1138
- }
1139
- const token = await this.getToken();
1140
- if (!token) {
1141
- return fetch(url, init);
1142
- }
1143
- const urlStr = typeof url === "string" ? url : url.toString();
1144
- const method = (init.method || "GET").toUpperCase();
1145
- const origin = new URL(urlStr).origin;
1146
- // Check if the Authorization header contains the managed token
1147
- const headers = new Headers(init.headers || {});
1148
- const authHeader = headers.get("Authorization") || "";
1149
- const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.substring(7) : null;
1150
- if (!bearerToken || bearerToken !== token) {
1151
- // Not our managed token, use regular fetch
1152
- return fetch(url, init);
1153
- }
1154
- // Generate DPoP proof and replace Bearer with DPoP scheme
1155
- const nonce = this._dpopProvider.getResourceServerNonce(origin);
1156
- const proof = await this._dpopProvider.generateDPoPProof(urlStr, method, token, nonce);
1157
- headers.set("Authorization", `DPoP ${token}`);
1158
- headers.set("DPoP", proof);
1159
- let response = await fetch(url, { ...init, headers });
1160
- // Handle DPoP-Nonce from response
1161
- const dpopNonce = response.headers.get("DPoP-Nonce");
1162
- if (dpopNonce) {
1163
- this._dpopProvider.updateResourceServerNonce(origin, dpopNonce);
1164
- }
1165
- // Retry only when the server explicitly asks for a DPoP nonce
1166
- // (WWW-Authenticate: ... error="use_dpop_nonce"). A blind retry on any 401
1167
- // doubles unrelated auth failures into two requests.
1168
- const wwwAuth = response.headers.get("WWW-Authenticate") || "";
1169
- if (response.status === 401 && dpopNonce && wwwAuth.includes('error="use_dpop_nonce"')) {
1170
- const retryProof = await this._dpopProvider.generateDPoPProof(urlStr, method, token, dpopNonce);
1171
- headers.set("DPoP", retryProof);
1172
- response = await fetch(url, { ...init, headers });
1173
- const retryNonce = response.headers.get("DPoP-Nonce");
1174
- if (retryNonce) {
1175
- this._dpopProvider.updateResourceServerNonce(origin, retryNonce);
1176
- }
1177
- }
1178
- return response;
1179
- }
1180
- /**
1181
- * Logout, clear cookie/session, then redirect.
1182
- * In hybrid mode, clears local state and emits logout event.
1183
- * In native mode, deletes tokens via adapter and emits logout event.
124
+ * @param {'ready'|'authSuccess'|'authError'|'authRefreshSuccess'|'authRefreshError'|'logout'|'tokenExpired'} event
125
+ * @param {*Function} callback
1184
126
  */
1185
- async doLogout() {
127
+ on(event, callback) {
1186
128
  var _a;
1187
- if (this.isHybridMode()) {
1188
- this._hybridAuthenticated = false;
1189
- this._hybridReturnUrl = null;
1190
- this._emit("logout");
1191
- return;
1192
- }
1193
- if (this.isNativeMode()) {
1194
- await this._nativeAdapter.deleteTokens();
1195
- this._nativeTokens = null;
1196
- this._nativeAuthenticated = false;
1197
- this._nativeDoken = null;
1198
- this._nativeDokenParsed = null;
1199
- this._nativeVoucher = null;
1200
- // Reset callback flags so next login reinitializes properly
1201
- this._nativeCallbackHandled = false;
1202
- this._nativeCallbackProcessing = false;
1203
- this._nativeCallbackPromise = null;
1204
- // Unsubscribe from callbacks (will be resubscribed on next init)
1205
- if (this._nativeCallbackUnsubscribe) {
1206
- this._nativeCallbackUnsubscribe();
1207
- this._nativeCallbackUnsubscribe = null;
1208
- }
1209
- if (this._nativeEncryptionCallbackUnsubscribe) {
1210
- this._nativeEncryptionCallbackUnsubscribe();
1211
- this._nativeEncryptionCallbackUnsubscribe = null;
1212
- }
1213
- // Close and cleanup the request enclave
1214
- if (this._nativeRequestEnclave) {
1215
- try {
1216
- this._nativeRequestEnclave.close();
1217
- }
1218
- catch (e) {
1219
- console.debug("[IAMService] Error closing request enclave:", e);
1220
- }
1221
- this._nativeRequestEnclave = null;
1222
- }
1223
- this._emit("logout");
1224
- return;
1225
- }
1226
- this._clearTokenCookie();
1227
- this.getTideCloakClient().logout({
1228
- redirectUri: (_a = this._config["redirectUri"]) !== null && _a !== void 0 ? _a : `${window.location.origin}/auth/redirect`
1229
- });
1230
- }
1231
- /**
1232
- * Base URL for Tidecloak realm (no trailing slash).
1233
- * In hybrid mode returns empty string.
1234
- */
1235
- getBaseUrl() {
1236
- var _a, _b;
1237
- if (this.isHybridMode())
1238
- return "";
1239
- return ((_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a["auth-server-url"]) === null || _b === void 0 ? void 0 : _b.replace(/\/$/, "")) || "";
1240
- }
1241
- // ---------------------------------------------------------------------------
1242
- // HYBRID MODE SUPPORT (private helpers)
1243
- // ---------------------------------------------------------------------------
1244
- /**
1245
- * Start hybrid login flow: generate PKCE, store verifier, redirect to IdP.
1246
- * @private
1247
- * @param {string} returnUrl - URL to redirect to after successful auth
1248
- */
1249
- async _startHybridLogin(returnUrl = "") {
1250
- var _a, _b;
1251
- if (typeof window === "undefined") {
1252
- throw new Error("Cannot login in SSR context");
1253
- }
1254
- const oidc = (_a = this._config) === null || _a === void 0 ? void 0 : _a.oidc;
1255
- const tokenExchange = (_b = this._config) === null || _b === void 0 ? void 0 : _b.tokenExchange;
1256
- console.debug("[IAMService._startHybridLogin] Config:", {
1257
- authorizationEndpoint: oidc === null || oidc === void 0 ? void 0 : oidc.authorizationEndpoint,
1258
- clientId: oidc === null || oidc === void 0 ? void 0 : oidc.clientId,
1259
- redirectUri: oidc === null || oidc === void 0 ? void 0 : oidc.redirectUri,
1260
- tokenExchangeEndpoint: tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.endpoint,
1261
- });
1262
- if (!(oidc === null || oidc === void 0 ? void 0 : oidc.authorizationEndpoint) || !(oidc === null || oidc === void 0 ? void 0 : oidc.clientId) || !(oidc === null || oidc === void 0 ? void 0 : oidc.redirectUri)) {
1263
- throw new Error("Hybrid mode requires config.oidc.authorizationEndpoint, clientId, and redirectUri");
1264
- }
1265
- if (!(tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.endpoint)) {
1266
- throw new Error("Hybrid mode requires config.tokenExchange.endpoint");
1267
- }
1268
- console.debug("[IAMService] Generating PKCE...");
1269
- const { verifier, challenge, method } = await makePkce();
1270
- console.debug("[IAMService] PKCE generated, verifier length:", verifier.length);
1271
- // Store PKCE verifier and return URL in sessionStorage
1272
- sessionStorage.setItem("kc_pkce_verifier", verifier);
1273
- sessionStorage.setItem("kc_return_url", returnUrl || "");
1274
- // Verify storage worked
1275
- const storedVerifier = sessionStorage.getItem("kc_pkce_verifier");
1276
- console.debug("[IAMService] Stored verifier in sessionStorage, retrieved length:", storedVerifier === null || storedVerifier === void 0 ? void 0 : storedVerifier.length);
1277
- // Encode return URL in state parameter
1278
- const state = returnUrl ? `__url_${returnUrl}` : "";
1279
- const scope = oidc.scope || "openid profile email";
1280
- const authUrl = `${oidc.authorizationEndpoint}` +
1281
- `?client_id=${encodeURIComponent(oidc.clientId)}` +
1282
- `&redirect_uri=${encodeURIComponent(oidc.redirectUri)}` +
1283
- `&response_type=code` +
1284
- `&scope=${encodeURIComponent(scope)}` +
1285
- `&state=${encodeURIComponent(state)}` +
1286
- `&code_challenge=${encodeURIComponent(challenge)}` +
1287
- `&code_challenge_method=${encodeURIComponent(method)}` +
1288
- (oidc.prompt ? `&prompt=${encodeURIComponent(oidc.prompt)}` : "");
1289
- console.debug("[IAMService] Redirecting to:", authUrl.substring(0, 100) + "...");
1290
- window.location.assign(authUrl);
1291
- }
1292
- /**
1293
- * Get hybrid callback data for custom token exchange.
1294
- * Use this when you need to handle token exchange with your own auth system
1295
- * (e.g., using a custom useLogin hook) instead of IAMService's built-in fetchJson.
1296
- *
1297
- * @param {Object} [opts] - Options
1298
- * @param {boolean} [opts.clearStorage=true] - Whether to clear sessionStorage after reading
1299
- * @param {string} [opts.redirectUri] - Override redirect URI (use when config isn't loaded)
1300
- * @param {string} [opts.provider] - Override provider (defaults to "tidecloak-auth")
1301
- * @returns {{
1302
- * isCallback: boolean,
1303
- * code: string,
1304
- * verifier: string,
1305
- * redirectUri: string,
1306
- * returnUrl: string,
1307
- * provider: string,
1308
- * error: string|null,
1309
- * errorDescription: string|null
1310
- * }}
1311
- *
1312
- * @example
1313
- * // With redirectUri override (recommended for callback pages after full navigation)
1314
- * const data = IAMService.getHybridCallbackData({
1315
- * redirectUri: process.env.KEYCLOAK_REDIRECTURI,
1316
- * });
1317
- * if (data.isCallback && data.code && data.verifier) {
1318
- * login.execute({
1319
- * accessToken: JSON.stringify({
1320
- * code: data.code,
1321
- * code_verifier: data.verifier,
1322
- * redirect_uri: data.redirectUri,
1323
- * }),
1324
- * provider: data.provider,
1325
- * });
1326
- * }
1327
- */
1328
- getHybridCallbackData(opts = {}) {
1329
- var _a, _b, _c, _d;
1330
- const { clearStorage = true, redirectUri: optsRedirectUri, provider: optsProvider } = opts;
1331
- if (typeof window === "undefined") {
1332
- return {
1333
- isCallback: false,
1334
- code: "",
1335
- verifier: "",
1336
- redirectUri: "",
1337
- returnUrl: "",
1338
- provider: "",
1339
- error: null,
1340
- errorDescription: null,
1341
- };
1342
- }
1343
- // Return cached data if available (prevents data loss from multiple calls or initIAM clearing storage)
1344
- if (this._cachedCallbackData) {
1345
- console.debug("[IAMService.getHybridCallbackData] Returning cached data");
1346
- // Allow overriding redirectUri and provider even when returning cached data
1347
- return {
1348
- ...this._cachedCallbackData,
1349
- redirectUri: optsRedirectUri || this._cachedCallbackData.redirectUri,
1350
- provider: optsProvider || this._cachedCallbackData.provider,
1351
- };
1352
- }
1353
- const qs = new URLSearchParams(window.location.search);
1354
- const error = qs.get("error");
1355
- const errorDescription = qs.get("error_description");
1356
- const code = qs.get("code") || "";
1357
- const state = qs.get("state") || "";
1358
- // Decode return URL from state, fallback to sessionStorage
1359
- const stateReturnUrl = state.startsWith("__url_") ? state.substring(6) : "";
1360
- const returnUrl = this._sanitizeReturnUrl(stateReturnUrl || sessionStorage.getItem("kc_return_url") || "");
1361
- const verifier = sessionStorage.getItem("kc_pkce_verifier") || "";
1362
- // Use opts override, then config, then empty string
1363
- const redirectUri = optsRedirectUri || ((_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a.oidc) === null || _b === void 0 ? void 0 : _b.redirectUri) || "";
1364
- const provider = optsProvider || ((_d = (_c = this._config) === null || _c === void 0 ? void 0 : _c.tokenExchange) === null || _d === void 0 ? void 0 : _d.provider) || "tidecloak-auth";
1365
- const isCallback = !!(code || error);
1366
- console.debug("[IAMService.getHybridCallbackData] code:", code ? code.substring(0, 20) + "..." : "(empty)");
1367
- console.debug("[IAMService.getHybridCallbackData] verifier:", verifier ? `(length: ${verifier.length})` : "(empty)");
1368
- console.debug("[IAMService.getHybridCallbackData] redirectUri:", redirectUri);
1369
- console.debug("[IAMService.getHybridCallbackData] returnUrl:", returnUrl);
1370
- console.debug("[IAMService.getHybridCallbackData] clearStorage:", clearStorage, "isCallback:", isCallback);
1371
- const data = {
1372
- isCallback,
1373
- code,
1374
- verifier,
1375
- redirectUri,
1376
- returnUrl,
1377
- provider,
1378
- error,
1379
- errorDescription,
1380
- };
1381
- // Cache the data before clearing storage
1382
- if (isCallback && verifier) {
1383
- this._cachedCallbackData = data;
1384
- console.debug("[IAMService.getHybridCallbackData] Cached callback data");
1385
- }
1386
- if (clearStorage && isCallback) {
1387
- sessionStorage.removeItem("kc_pkce_verifier");
1388
- sessionStorage.removeItem("kc_return_url");
1389
- console.debug("[IAMService.getHybridCallbackData] Cleared sessionStorage");
1390
- }
1391
- return data;
129
+ var _b;
130
+ ((_a = (_b = __classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f"))[event]) !== null && _a !== void 0 ? _a : (_b[event] = [])).push(callback);
131
+ return this;
1392
132
  }
1393
133
  /**
1394
- * Handle hybrid redirect callback: exchange code for tokens via backend endpoint.
1395
- * @private
1396
- * @param {Object} opts - Options
1397
- * @param {string} [opts.onMissingVerifierRedirectTo] - URL to redirect if verifier is missing
1398
- * @returns {Promise<{handled: boolean, authenticated: boolean, returnUrl: string}>}
1399
- */
1400
- async _handleHybridRedirectCallback(opts = {}) {
1401
- var _a, _b, _c, _d, _e;
1402
- if (typeof window === "undefined") {
1403
- return { handled: false, authenticated: false, returnUrl: "" };
1404
- }
1405
- const qs = new URLSearchParams(window.location.search);
1406
- const error = qs.get("error");
1407
- const errorDescription = qs.get("error_description") || "An error occurred";
1408
- const code = qs.get("code") || "";
1409
- const state = qs.get("state") || "";
1410
- // Decode return URL from state, fallback to sessionStorage (Keycloak broker modifies state)
1411
- const stateReturnUrl = state.startsWith("__url_") ? state.substring(6) : "";
1412
- const returnUrl = this._sanitizeReturnUrl(stateReturnUrl || sessionStorage.getItem("kc_return_url") || "");
1413
- // Handle error response from IdP
1414
- if (error) {
1415
- this._emit("authError", new Error(`${error}: ${errorDescription}`));
1416
- this._emit("ready", false);
1417
- return { handled: true, authenticated: false, returnUrl };
1418
- }
1419
- // No code = not a callback page
1420
- if (!code) {
1421
- console.debug("[IAMService] No code in URL, not a callback page");
1422
- return { handled: false, authenticated: false, returnUrl: "" };
1423
- }
1424
- console.debug("[IAMService] Code found in URL, checking for PKCE verifier...");
1425
- const verifier = sessionStorage.getItem("kc_pkce_verifier") || "";
1426
- const redirectUri = ((_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a.oidc) === null || _b === void 0 ? void 0 : _b.redirectUri) || "";
1427
- console.debug("[IAMService] Retrieved verifier from sessionStorage, length:", verifier.length);
1428
- console.debug("[IAMService] Current origin:", window.location.origin);
1429
- // Cache the callback data so getHybridCallbackData() can access it even after we clear storage
1430
- if (code && verifier) {
1431
- const provider = ((_d = (_c = this._config) === null || _c === void 0 ? void 0 : _c.tokenExchange) === null || _d === void 0 ? void 0 : _d.provider) || "tidecloak-auth";
1432
- this._cachedCallbackData = {
1433
- isCallback: true,
1434
- code,
1435
- verifier,
1436
- redirectUri,
1437
- returnUrl,
1438
- provider,
1439
- error: null,
1440
- errorDescription: null,
1441
- };
1442
- console.debug("[IAMService] Cached callback data from _handleHybridRedirectCallback");
1443
- }
1444
- // Code present but verifier missing (e.g., page refresh after verifier consumed)
1445
- if (code.length > 0 && verifier.length === 0) {
1446
- console.error("[IAMService] PKCE verifier missing! Code present but no verifier in sessionStorage.");
1447
- console.debug("[IAMService] All sessionStorage keys:", Object.keys(sessionStorage));
1448
- if (opts.onMissingVerifierRedirectTo) {
1449
- window.location.assign(opts.onMissingVerifierRedirectTo);
1450
- }
1451
- this._emit("authError", new Error("Missing PKCE verifier (likely page refresh after it was consumed)"));
1452
- this._emit("ready", false);
1453
- return { handled: true, authenticated: false, returnUrl };
1454
- }
1455
- // Clear session storage
1456
- sessionStorage.removeItem("kc_pkce_verifier");
1457
- sessionStorage.removeItem("kc_return_url");
1458
- const tokenExchange = (_e = this._config) === null || _e === void 0 ? void 0 : _e.tokenExchange;
1459
- const exchangeEndpoint = tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.endpoint;
1460
- const provider = (tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.provider) || "tidecloak-auth";
1461
- // Support custom headers (static object or dynamic function)
1462
- const customHeaders = typeof (tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.headers) === "function"
1463
- ? tokenExchange.headers()
1464
- : ((tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.headers) || {});
1465
- console.debug("[IAMService] Token exchange endpoint:", exchangeEndpoint);
1466
- console.debug("[IAMService] Custom headers:", customHeaders);
1467
- try {
1468
- // Exchange code for tokens via backend endpoint
1469
- // Payload format matches existing backend expectation
1470
- await fetchJson(exchangeEndpoint, {
1471
- method: "POST",
1472
- headers: customHeaders,
1473
- body: JSON.stringify({
1474
- accessToken: JSON.stringify({
1475
- code: code,
1476
- code_verifier: verifier,
1477
- redirect_uri: redirectUri,
1478
- }),
1479
- provider,
1480
- }),
1481
- });
1482
- this._hybridAuthenticated = true;
1483
- this._hybridReturnUrl = returnUrl || null; // Set BEFORE emitting so getReturnUrl() works in handler
1484
- this._emit("authSuccess");
1485
- // Clean URL (remove code, state, etc.)
1486
- const url = new URL(window.location.href);
1487
- ["code", "state", "session_state", "iss", "error", "error_description"].forEach(k => url.searchParams.delete(k));
1488
- window.history.replaceState({}, document.title, url.toString());
1489
- this._emit("ready", true);
1490
- return { handled: true, authenticated: true, returnUrl };
1491
- }
1492
- catch (err) {
1493
- this._hybridAuthenticated = false;
1494
- this._emit("authError", err);
1495
- this._emit("ready", false);
1496
- return { handled: true, authenticated: false, returnUrl };
134
+ * Unregister an event listener.
135
+ * @param {string} event
136
+ * @param {Function} handler
137
+ * @returns {this}
138
+ */
139
+ off(event, handler) {
140
+ if (__classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f")[event]) {
141
+ __classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f")[event] = __classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f")[event].filter(fn => fn !== handler);
1497
142
  }
143
+ return this;
1498
144
  }
1499
- // ---------------------------------------------------------------------------
1500
- // NATIVE MODE SUPPORT (private helpers)
1501
- // ---------------------------------------------------------------------------
1502
- /**
1503
- * Get OIDC configuration from the native config.
1504
- * @private
1505
- * @returns {{ authServerUrl: string, realm: string, clientId: string, scope: string }}
1506
- */
1507
- _getNativeOIDCConfig() {
1508
- const config = this._config;
1509
- return {
1510
- authServerUrl: config["auth-server-url"],
1511
- realm: config.realm,
1512
- clientId: config.resource,
1513
- scope: config.scope || "openid profile email",
1514
- };
145
+ fetch(url, init = {}) {
146
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).fetch(url, init);
1515
147
  }
1516
- /**
1517
- * Get encryption configuration from the native config.
1518
- * Automatically selects clientOriginAuth based on window.location.origin.
1519
- * @private
1520
- * @returns {{ vendorId: string, homeOrkUrl: string, clientOriginAuth: string } | null}
1521
- */
1522
- _getNativeEncryptionConfig() {
1523
- const config = this._config;
1524
- if (!config.vendorId) {
1525
- return null;
1526
- }
1527
- // Auto-select clientOriginAuth based on current origin
1528
- const origin = typeof window !== 'undefined' ? window.location.origin : '';
1529
- const clientOriginAuthKey = `client-origin-auth-${origin}`;
1530
- const clientOriginAuth = config[clientOriginAuthKey];
1531
- return {
1532
- vendorId: config.vendorId,
1533
- homeOrkUrl: config.homeOrkUrl,
1534
- clientOriginAuth,
1535
- };
1536
- }
1537
- /**
1538
- * Start native login flow: generate PKCE, open external browser with auth URL.
1539
- * @private
1540
- * @param {string} returnUrl - URL to redirect to after successful auth
1541
- */
1542
- async _startNativeLogin(returnUrl = "") {
1543
- console.debug("[IAMService._startNativeLogin] Starting native login flow");
1544
- // Reset auth state to allow new callback to be processed
1545
- // This is needed for re-login after session expiry
1546
- this._nativeAuthenticated = false;
1547
- this._nativeCallbackProcessing = false;
1548
- const adapter = this._nativeAdapter;
1549
- if (!adapter) {
1550
- throw new Error("Native adapter not configured");
1551
- }
1552
- // Get OIDC config from the main config
1553
- const { authServerUrl, realm, clientId, scope } = this._getNativeOIDCConfig();
1554
- // Generate PKCE
1555
- const { verifier, challenge, method } = await makePkce();
1556
- console.debug("[IAMService._startNativeLogin] PKCE generated, verifier length:", verifier.length);
1557
- // Store PKCE verifier in sessionStorage (will be retrieved when callback is received)
1558
- sessionStorage.setItem("kc_pkce_verifier", verifier);
1559
- if (returnUrl) {
1560
- sessionStorage.setItem("kc_return_url", returnUrl);
1561
- }
1562
- // Get redirect URI from adapter (can be async)
1563
- const redirectUri = await Promise.resolve(adapter.getRedirectUri());
1564
- sessionStorage.setItem("kc_redirect_uri", redirectUri);
1565
- // Build auth URL
1566
- const authUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/auth` +
1567
- `?client_id=${encodeURIComponent(clientId)}` +
1568
- `&redirect_uri=${encodeURIComponent(redirectUri)}` +
1569
- `&response_type=code` +
1570
- `&scope=${encodeURIComponent(scope)}` +
1571
- `&code_challenge=${encodeURIComponent(challenge)}` +
1572
- `&code_challenge_method=${encodeURIComponent(method)}` +
1573
- `&prompt=login`;
1574
- console.debug("[IAMService._startNativeLogin] Opening auth URL in external browser");
1575
- // Open in external browser via adapter
1576
- await adapter.openExternalUrl(authUrl);
148
+ isLoggedIn() {
149
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).authenticated;
1577
150
  }
1578
- /**
1579
- * Handle native auth callback: exchange code for tokens.
1580
- * @private
1581
- * @param {string} code - Authorization code from IdP
1582
- * @param {string} [voucher] - Optional voucher fetched during login (for encryption)
1583
- */
1584
- async _handleNativeCallback(code, voucher) {
1585
- // Guard against concurrent/duplicate callback processing
1586
- if (this._nativeCallbackProcessing) {
1587
- console.debug("[IAMService._handleNativeCallback] Already processing callback, ignoring duplicate");
1588
- return;
1589
- }
1590
- this._nativeCallbackProcessing = true;
1591
- this._nativeCallbackHandled = true;
1592
- console.debug("[IAMService._handleNativeCallback] Handling native callback with code");
1593
- const adapter = this._nativeAdapter;
1594
- const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
1595
- // Retrieve PKCE verifier
1596
- const verifier = sessionStorage.getItem("kc_pkce_verifier");
1597
- const redirectUri = sessionStorage.getItem("kc_redirect_uri") || await Promise.resolve(adapter.getRedirectUri());
1598
- const returnUrl = sessionStorage.getItem("kc_return_url") || "";
1599
- // Clear session storage immediately to prevent reuse
1600
- sessionStorage.removeItem("kc_pkce_verifier");
1601
- sessionStorage.removeItem("kc_redirect_uri");
1602
- sessionStorage.removeItem("kc_return_url");
1603
- if (!verifier) {
1604
- console.debug("[IAMService._handleNativeCallback] PKCE verifier not found, callback already processed");
1605
- this._nativeCallbackProcessing = false;
1606
- // Don't emit error - this is likely a duplicate callback after successful auth
1607
- return;
1608
- }
151
+ }
152
+ _IAMServiceImplementation_core = new WeakMap(), _IAMServiceImplementation_listeners = new WeakMap(), _IAMServiceImplementation_initPromise = new WeakMap(), _IAMServiceImplementation_tide = new WeakMap(), _IAMServiceImplementation_instances = new WeakSet(), _IAMServiceImplementation_emit = function _IAMServiceImplementation_emit(event, ...args) {
153
+ var _a;
154
+ for (const callback of (_a = __classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f")[event]) !== null && _a !== void 0 ? _a : []) {
1609
155
  try {
1610
- // Exchange code for tokens at token endpoint
1611
- const tokenUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/token`;
1612
- const body = new URLSearchParams({
1613
- grant_type: "authorization_code",
1614
- client_id: clientId,
1615
- code: code,
1616
- redirect_uri: redirectUri,
1617
- code_verifier: verifier,
1618
- });
1619
- // Build headers, including DPoP proof if enabled
1620
- const headers = {
1621
- "Content-Type": "application/x-www-form-urlencoded",
1622
- };
1623
- if (this._dpopProvider) {
1624
- const nonce = await this._dpopProvider.getAuthServerNonce();
1625
- headers['DPoP'] = await this._dpopProvider.generateDPoPProof(tokenUrl, 'POST', undefined, nonce);
1626
- }
1627
- let response = await fetch(tokenUrl, {
1628
- method: "POST",
1629
- headers,
1630
- body: body.toString(),
1631
- });
1632
- // Handle DPoP nonce retry: server may respond with use_dpop_nonce error
1633
- if (!response.ok && this._dpopProvider) {
1634
- const dpopNonce = response.headers.get('DPoP-Nonce');
1635
- if (dpopNonce) {
1636
- await this._dpopProvider.updateAuthServerNonce(dpopNonce);
1637
- headers['DPoP'] = await this._dpopProvider.generateDPoPProof(tokenUrl, 'POST', undefined, dpopNonce);
1638
- response = await fetch(tokenUrl, {
1639
- method: "POST",
1640
- headers,
1641
- body: body.toString(),
1642
- });
1643
- }
1644
- }
1645
- if (!response.ok) {
1646
- const errorText = await response.text();
1647
- throw new Error(`Token exchange failed: ${errorText}`);
1648
- }
1649
- const data = await response.json();
1650
- console.debug("[IAMService._handleNativeCallback] Token exchange successful");
1651
- // Note: Voucher is NOT fetched here - the RequestEnclave will fetch it
1652
- // via its ORK iframe when encryption/decryption is needed. This avoids
1653
- // the complexity of session cookie handling during login.
1654
- // Build token object for storage
1655
- const tokens = {
1656
- accessToken: data.access_token,
1657
- refreshToken: data.refresh_token,
1658
- idToken: data.id_token,
1659
- doken: data.doken, // Tide doken for encryption/decryption
1660
- expiresAt: Date.now() + data.expires_in * 1000,
1661
- };
1662
- // Save tokens via adapter
1663
- await adapter.saveTokens(tokens);
1664
- this._nativeTokens = tokens;
1665
- this._nativeDoken = data.doken;
1666
- this._nativeDokenParsed = data.doken ? this._parseToken(data.doken) : null;
1667
- // Note: _nativeVoucher is NOT set here - RequestEnclave fetches it on-demand
1668
- this._nativeAuthenticated = true;
1669
- this._nativeCallbackProcessing = false;
1670
- this._emit("authSuccess");
1671
- console.debug("[IAMService._handleNativeCallback] Native auth complete, returnUrl:", returnUrl);
156
+ callback(...args);
1672
157
  }
1673
158
  catch (err) {
1674
- console.error("[IAMService._handleNativeCallback] Token exchange error:", err);
1675
- this._nativeAuthenticated = false;
1676
- this._nativeCallbackProcessing = false;
1677
- this._emit("authError", err);
1678
- }
1679
- }
1680
- /**
1681
- * Refresh native token using refresh token.
1682
- * @private
1683
- * @param {string} refreshToken - The refresh token
1684
- * @returns {Promise<Object>} New token data
1685
- */
1686
- async _refreshNativeToken(refreshToken) {
1687
- const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
1688
- const tokenUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/token`;
1689
- const body = new URLSearchParams({
1690
- grant_type: "refresh_token",
1691
- client_id: clientId,
1692
- refresh_token: refreshToken,
1693
- });
1694
- const response = await fetch(tokenUrl, {
1695
- method: "POST",
1696
- headers: {
1697
- "Content-Type": "application/x-www-form-urlencoded",
1698
- },
1699
- body: body.toString(),
1700
- });
1701
- if (!response.ok) {
1702
- const errorText = await response.text();
1703
- throw new Error(`Token refresh failed: ${errorText}`);
1704
- }
1705
- const data = await response.json();
1706
- // Update doken if present in refresh response
1707
- if (data.doken) {
1708
- this._nativeDoken = data.doken;
1709
- this._nativeDokenParsed = this._parseToken(data.doken);
1710
- console.debug("[IAMService] Updated doken from token refresh");
1711
- }
1712
- return {
1713
- accessToken: data.access_token,
1714
- refreshToken: data.refresh_token,
1715
- idToken: data.id_token,
1716
- doken: data.doken,
1717
- expiresAt: Date.now() + data.expires_in * 1000,
1718
- };
1719
- }
1720
- // ---------------------------------------------------------------------------
1721
- // NATIVE MODE ENCRYPTION SUPPORT (private helpers)
1722
- // ---------------------------------------------------------------------------
1723
- /**
1724
- * Initialize the native RequestEnclave for encryption/decryption.
1725
- * @private
1726
- * @param {string} voucherDataUrl - Pre-fetched voucher as data URL
1727
- */
1728
- _initNativeRequestEnclave(voucherDataUrl) {
1729
- if (!this._nativeDoken) {
1730
- throw new Error("[IAMService] No doken available for encryption - user must be authenticated with Tide");
1731
- }
1732
- if (!this._nativeDokenParsed) {
1733
- throw new Error("[IAMService] Doken not parsed");
1734
- }
1735
- // Get encryption config from the main config (auto-selects clientOriginAuth based on origin)
1736
- const encryptionConfig = this._getNativeEncryptionConfig();
1737
- if (!encryptionConfig || !encryptionConfig.vendorId || !encryptionConfig.clientOriginAuth) {
1738
- throw new Error("[IAMService] Native encryption requires vendorId and client-origin-auth-{origin} in config");
1739
- }
1740
- // Reuse existing enclave if already initialized
1741
- if (this._nativeRequestEnclave) {
1742
- console.debug("[IAMService] Reusing existing RequestEnclave");
1743
- return;
1744
- }
1745
- const homeOrkOrigin = this._nativeDokenParsed['t.uho'] || encryptionConfig.homeOrkUrl;
1746
- if (!homeOrkOrigin) {
1747
- throw new Error("[IAMService] Home ORK URL not available - check doken or config.homeOrkUrl");
1748
- }
1749
- console.debug("[IAMService] Initializing native RequestEnclave", {
1750
- homeOrkOrigin,
1751
- vendorId: encryptionConfig.vendorId,
1752
- voucherURL: voucherDataUrl,
1753
- });
1754
- this._nativeRequestEnclave = new RequestEnclave({
1755
- homeOrkOrigin,
1756
- signed_client_origin: encryptionConfig.clientOriginAuth,
1757
- vendorId: encryptionConfig.vendorId,
1758
- voucherURL: voucherDataUrl,
1759
- isRunningLocal: false,
1760
- }).init({
1761
- doken: this._nativeDoken,
1762
- dokenRefreshCallback: async () => {
1763
- // Refresh tokens and return the new doken
1764
- await this.forceUpdateToken();
1765
- if (!this._nativeDoken) {
1766
- throw new Error("[IAMService] No doken available after token refresh");
1767
- }
1768
- return this._nativeDoken;
1769
- },
1770
- requireReloginCallback: async () => {
1771
- // User needs to re-authenticate
1772
- console.warn("[IAMService] Re-authentication required for encryption");
1773
- this._emit("authError", new Error("Re-authentication required"));
1774
- },
1775
- });
1776
- }
1777
- /**
1778
- * Build the encryption page URL for external browser.
1779
- * This page is hosted on the TideCloak server and has session cookies.
1780
- * @private
1781
- * @param {string} operation - 'encrypt' or 'decrypt'
1782
- * @param {string} requestId - Unique request ID
1783
- * @param {string} dataBase64 - Base64-encoded data to process
1784
- * @param {string} tagsJson - JSON-encoded tags array
1785
- * @param {string} callbackUrl - URL to redirect with result
1786
- * @returns {string} URL to open in external browser
1787
- */
1788
- _buildEncryptionPageUrl(operation, requestId, dataBase64, tagsJson, callbackUrl) {
1789
- const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
1790
- const encryptionConfig = this._getNativeEncryptionConfig();
1791
- // Build URL to a page on the TideCloak server that can perform encryption
1792
- // This page will have access to session cookies
1793
- const baseUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/tide-encrypt`;
1794
- const url = new URL(baseUrl);
1795
- url.searchParams.set('operation', operation);
1796
- url.searchParams.set('requestId', requestId);
1797
- url.searchParams.set('data', dataBase64);
1798
- url.searchParams.set('tags', tagsJson);
1799
- url.searchParams.set('callback', callbackUrl);
1800
- url.searchParams.set('vendorId', (encryptionConfig === null || encryptionConfig === void 0 ? void 0 : encryptionConfig.vendorId) || '');
1801
- url.searchParams.set('clientId', clientId);
1802
- return url.toString();
1803
- }
1804
- /**
1805
- * Perform an encryption operation via external browser.
1806
- * @private
1807
- * @param {'encrypt' | 'decrypt'} operation - The operation to perform
1808
- * @param {string} dataBase64 - Base64-encoded data to process
1809
- * @param {string[]} tags - Tags for the operation
1810
- * @returns {Promise<string>} Base64-encoded result
1811
- */
1812
- async _doExternalBrowserOperation(operation, dataBase64, tags) {
1813
- const adapter = this._nativeAdapter;
1814
- // Generate unique request ID
1815
- const requestId = `${operation}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1816
- // Get callback URL for encryption operations
1817
- let callbackUrl;
1818
- if (adapter.getEncryptionRedirectUri) {
1819
- callbackUrl = await Promise.resolve(adapter.getEncryptionRedirectUri());
1820
- }
1821
- else {
1822
- // Default: use auth redirect URI with /encrypt/callback suffix
1823
- const authRedirectUri = await Promise.resolve(adapter.getRedirectUri());
1824
- // Replace /callback with /encrypt/callback, or append if no /callback
1825
- if (authRedirectUri.endsWith('/callback')) {
1826
- callbackUrl = authRedirectUri.replace('/callback', '/encrypt/callback');
1827
- }
1828
- else {
1829
- callbackUrl = authRedirectUri + '/encrypt/callback';
1830
- }
1831
- }
1832
- // Build the URL
1833
- const tagsJson = JSON.stringify(tags);
1834
- const encryptionUrl = this._buildEncryptionPageUrl(operation, requestId, dataBase64, tagsJson, callbackUrl);
1835
- console.debug(`[IAMService] Opening external browser for ${operation}:`, {
1836
- requestId,
1837
- callbackUrl,
1838
- urlLength: encryptionUrl.length,
1839
- });
1840
- // Create a promise that will resolve when we get the callback
1841
- const resultPromise = new Promise((resolve, reject) => {
1842
- // Store the pending request
1843
- this._pendingEncryptionRequests.set(requestId, { resolve, reject });
1844
- // Set a timeout (60 seconds)
1845
- const timeout = setTimeout(() => {
1846
- if (this._pendingEncryptionRequests.has(requestId)) {
1847
- this._pendingEncryptionRequests.delete(requestId);
1848
- reject(new Error(`Encryption ${operation} timed out after 60 seconds`));
1849
- }
1850
- }, 60000);
1851
- // Update the stored request to include timeout cleanup
1852
- const pending = this._pendingEncryptionRequests.get(requestId);
1853
- const originalResolve = pending.resolve;
1854
- const originalReject = pending.reject;
1855
- pending.resolve = (result) => {
1856
- clearTimeout(timeout);
1857
- originalResolve(result);
1858
- };
1859
- pending.reject = (error) => {
1860
- clearTimeout(timeout);
1861
- originalReject(error);
1862
- };
1863
- });
1864
- // Open the URL in external browser
1865
- await adapter.openExternalUrl(encryptionUrl);
1866
- // Wait for the callback
1867
- return resultPromise;
1868
- }
1869
- /**
1870
- * Get the voucher URL for native mode.
1871
- * @private
1872
- * @returns {string} Voucher URL
1873
- */
1874
- _getNativeVoucherUrl() {
1875
- var _a;
1876
- if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken)) {
1877
- throw new Error("[IAMService] No access token available for voucher URL");
1878
- }
1879
- const tokenPayload = this._parseToken(this._nativeTokens.accessToken);
1880
- if (!tokenPayload) {
1881
- throw new Error("[IAMService] Failed to parse access token for voucher URL");
1882
- }
1883
- const sid = tokenPayload.sid;
1884
- if (!sid) {
1885
- throw new Error("[IAMService] No session ID in access token for voucher URL");
1886
- }
1887
- const { authServerUrl, realm } = this._getNativeOIDCConfig();
1888
- return `${authServerUrl}/realms/${encodeURIComponent(realm)}/tidevouchers/fromUserSession?sessionId=${encodeURIComponent(sid)}`;
1889
- }
1890
- /**
1891
- * Check if user has a realm role (native mode helper).
1892
- * @private
1893
- * @param {string} role - Role to check
1894
- * @returns {boolean}
1895
- */
1896
- _nativeHasRealmRole(role) {
1897
- var _a, _b;
1898
- if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
1899
- return false;
1900
- try {
1901
- const payload = this._parseToken(this._nativeTokens.accessToken);
1902
- const realmRoles = ((_b = payload === null || payload === void 0 ? void 0 : payload.realm_access) === null || _b === void 0 ? void 0 : _b.roles) || [];
1903
- return realmRoles.includes(role);
1904
- }
1905
- catch (e) {
1906
- return false;
1907
- }
1908
- }
1909
- /**
1910
- * Native mode encryption using RequestEnclave.
1911
- * Uses either a pre-fetched voucher (as data URL) or the live voucherURL
1912
- * that the ORK iframe will fetch with session cookies.
1913
- * @private
1914
- * @param {{ data: string | Uint8Array, tags: string[] }[]} toEncrypt
1915
- * @returns {Promise<(string | Uint8Array)[]>}
1916
- */
1917
- async _nativeEncrypt(toEncrypt) {
1918
- // Ensure token is fresh
1919
- await this.updateIAMToken();
1920
- if (!Array.isArray(toEncrypt)) {
1921
- throw new Error("Pass array as parameter");
1922
- }
1923
- if (!this._nativeAuthenticated) {
1924
- throw new Error("Not authenticated");
1925
- }
1926
- // Convert and validate input
1927
- const dataToSend = toEncrypt.map((e) => {
1928
- if (typeof e !== 'object' || e === null) {
1929
- throw new Error("All entries must be an object to encrypt");
1930
- }
1931
- for (const property of ['data', 'tags']) {
1932
- if (!e[property]) {
1933
- throw new Error(`The object is missing the required '${property}' property.`);
1934
- }
1935
- }
1936
- if (!Array.isArray(e.tags)) {
1937
- throw new Error("tags must be provided as a string array");
1938
- }
1939
- if (typeof e.data !== 'string' && !(e.data instanceof Uint8Array)) {
1940
- throw new Error("data must be provided as string or Uint8Array");
1941
- }
1942
- // Check roles
1943
- for (const tag of e.tags) {
1944
- if (typeof tag !== 'string') {
1945
- throw new Error("tags must be provided as an array of strings");
1946
- }
1947
- const tagAccess = this._nativeHasRealmRole(`_tide_${tag}.selfencrypt`);
1948
- if (!tagAccess) {
1949
- throw new Error(`User has not been given any access to '${tag}'`);
1950
- }
1951
- }
1952
- return {
1953
- data: typeof e.data === 'string' ? this._stringToUint8Array(e.data) : e.data,
1954
- tags: e.tags,
1955
- isRaw: typeof e.data === 'string' ? false : true,
1956
- };
1957
- });
1958
- // Get voucher URL - RequestEnclave will fetch it via ORK iframe
1959
- const voucherUrl = this._getNativeVoucherUrl();
1960
- console.debug("[IAMService._nativeEncrypt] Using voucherURL:", voucherUrl);
1961
- this._initNativeRequestEnclave(voucherUrl);
1962
- const encrypted = await this._nativeRequestEnclave.encrypt(dataToSend);
1963
- return encrypted.map((cipher, i) => (dataToSend[i].isRaw ? cipher : this._bytesToBase64(cipher)));
1964
- }
1965
- /**
1966
- * Native mode decryption using RequestEnclave.
1967
- * Uses either a pre-fetched voucher (as data URL) or the live voucherURL
1968
- * that the ORK iframe will fetch with session cookies.
1969
- * @private
1970
- * @param {{ encrypted: string | Uint8Array, tags: string[] }[]} toDecrypt
1971
- * @returns {Promise<(string | Uint8Array)[]>}
1972
- */
1973
- async _nativeDecrypt(toDecrypt) {
1974
- // Ensure token is fresh
1975
- await this.updateIAMToken();
1976
- if (!Array.isArray(toDecrypt)) {
1977
- throw new Error("Pass array as parameter");
1978
- }
1979
- if (!this._nativeAuthenticated) {
1980
- throw new Error("Not authenticated");
1981
- }
1982
- // Convert and validate input
1983
- const dataToSend = toDecrypt.map((e) => {
1984
- if (typeof e !== 'object' || e === null) {
1985
- throw new Error("All entries must be an object to decrypt");
1986
- }
1987
- for (const property of ['encrypted', 'tags']) {
1988
- if (!e[property]) {
1989
- throw new Error(`The object is missing the required '${property}' property.`);
1990
- }
1991
- }
1992
- if (!Array.isArray(e.tags)) {
1993
- throw new Error("tags must be provided as a string array");
1994
- }
1995
- if (typeof e.encrypted !== 'string' && !(e.encrypted instanceof Uint8Array)) {
1996
- throw new Error("encrypted must be provided as string or Uint8Array");
1997
- }
1998
- // Check roles
1999
- for (const tag of e.tags) {
2000
- if (typeof tag !== 'string') {
2001
- throw new Error("tags must be provided as an array of strings");
2002
- }
2003
- const tagAccess = this._nativeHasRealmRole(`_tide_${tag}.selfdecrypt`);
2004
- if (!tagAccess) {
2005
- throw new Error(`User has not been given any access to '${tag}'`);
2006
- }
2007
- }
2008
- return {
2009
- encrypted: typeof e.encrypted === 'string' ? this._base64ToBytes(e.encrypted) : e.encrypted,
2010
- tags: e.tags,
2011
- isRaw: typeof e.encrypted === 'string' ? false : true,
2012
- };
2013
- });
2014
- // Get voucher URL - RequestEnclave will fetch it via ORK iframe
2015
- const voucherUrl = this._getNativeVoucherUrl();
2016
- console.debug("[IAMService._nativeDecrypt] Using voucherURL:", voucherUrl);
2017
- this._initNativeRequestEnclave(voucherUrl);
2018
- const decrypted = await this._nativeRequestEnclave.decrypt(dataToSend);
2019
- return decrypted.map((d, i) => (dataToSend[i].isRaw ? d : this._stringFromUint8Array(d)));
2020
- }
2021
- /**
2022
- * Convert string to Uint8Array.
2023
- * @private
2024
- */
2025
- _stringToUint8Array(str) {
2026
- return new TextEncoder().encode(str);
2027
- }
2028
- /**
2029
- * Convert Uint8Array to string.
2030
- * @private
2031
- */
2032
- _stringFromUint8Array(bytes) {
2033
- return new TextDecoder('utf-8').decode(bytes);
2034
- }
2035
- /**
2036
- * Convert bytes to base64.
2037
- * @private
2038
- */
2039
- _bytesToBase64(bytes) {
2040
- const binString = String.fromCodePoint(...bytes);
2041
- return btoa(binString);
2042
- }
2043
- /**
2044
- * Convert base64 to bytes.
2045
- * @private
2046
- */
2047
- _base64ToBytes(base64) {
2048
- const binString = atob(base64);
2049
- const len = binString.length;
2050
- const bytes = new Uint8Array(len);
2051
- for (let i = 0; i < len; i++) {
2052
- bytes[i] = binString.codePointAt(i);
2053
- }
2054
- return bytes;
2055
- }
2056
- /**
2057
- * Parse a JWT token and return its payload.
2058
- * @private
2059
- * @param {string} token - JWT token string
2060
- * @returns {Object|null} Parsed token payload or null on error
2061
- */
2062
- _parseToken(token) {
2063
- if (!token)
2064
- return null;
2065
- try {
2066
- const payload = JSON.parse(atob(token.split('.')[1]));
2067
- return payload;
2068
- }
2069
- catch (e) {
2070
- console.error("[IAMService] Failed to parse token:", e);
2071
- return null;
2072
- }
2073
- }
2074
- /**
2075
- * Cleanup native mode resources.
2076
- */
2077
- destroy() {
2078
- if (this._nativeCallbackUnsubscribe) {
2079
- this._nativeCallbackUnsubscribe();
2080
- this._nativeCallbackUnsubscribe = null;
2081
- }
2082
- if (this._nativeEncryptionCallbackUnsubscribe) {
2083
- this._nativeEncryptionCallbackUnsubscribe();
2084
- this._nativeEncryptionCallbackUnsubscribe = null;
2085
- }
2086
- // Cleanup pending encryption requests
2087
- for (const [requestId, pending] of this._pendingEncryptionRequests) {
2088
- pending.reject(new Error("IAMService destroyed"));
2089
- }
2090
- this._pendingEncryptionRequests.clear();
2091
- // Cleanup request enclave
2092
- if (this._nativeRequestEnclave) {
2093
- try {
2094
- this._nativeRequestEnclave.close();
2095
- }
2096
- catch (e) {
2097
- console.debug("[IAMService] Error closing request enclave:", e);
2098
- }
2099
- this._nativeRequestEnclave = null;
2100
- }
2101
- }
2102
- }
2103
- const IAMServiceInstance = new IAMService();
2104
- export { IAMServiceInstance as IAMService };
2105
- export default IAMServiceInstance;
159
+ console.error(`[IAMService] '${event}' listener failed:`, err);
160
+ }
161
+ }
162
+ }, _IAMServiceImplementation_registerEvents = function _IAMServiceImplementation_registerEvents() {
163
+ const core = __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this);
164
+ core.onReady = (auth) => { __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_setTokenCookie).call(this, core.token); __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'ready', auth); };
165
+ core.onAuthSuccess = () => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'authSuccess');
166
+ core.onAuthError = (err) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'authError', err);
167
+ core.onAuthRefreshSuccess = () => { __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_setTokenCookie).call(this, core.token); __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'authRefreshSuccess'); };
168
+ core.onAuthRefreshError = () => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'authRefreshError');
169
+ core.onAuthLogout = () => { __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_setTokenCookie).call(this); __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'logout'); };
170
+ core.onTokenExpired = () => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'tokenExpired');
171
+ }, _IAMServiceImplementation_setTokenCookie = function _IAMServiceImplementation_setTokenCookie(token) {
172
+ const attributes = `path=/; SameSite=Lax${window.location.protocol === 'https:' ? '; Secure' : ''}`;
173
+ document.cookie = token
174
+ ? `kcToken=${token}; ${attributes}`
175
+ : `kcToken=; expires=Thu, 01 Jan 1970 00:00:00 GMT; ${attributes}`;
176
+ }, _IAMServiceImplementation_defaultInitOptions = function _IAMServiceImplementation_defaultInitOptions(json) {
177
+ var _a;
178
+ return {
179
+ onLoad: 'check-sso',
180
+ silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
181
+ pkceMethod: 'S256',
182
+ setupRequestEnclave: (_a = json.setupRequestEnclave) !== null && _a !== void 0 ? _a : true,
183
+ // DPoP is opt-in: only enable it when tidecloak.json asks for it.
184
+ ...(json.dpopConfig && { dpopConfig: json.dpopConfig }),
185
+ ...(json.checkLoginIframe === false && { checkLoginIframe: false }),
186
+ };
187
+ }, _IAMServiceImplementation_getCore = function _IAMServiceImplementation_getCore() {
188
+ if (!__classPrivateFieldGet(this, _IAMServiceImplementation_core, "f"))
189
+ throw Error('[IAMService] TidecloakJS not initialized. Make sure to call IAMService.init before using functionalities.');
190
+ return __classPrivateFieldGet(this, _IAMServiceImplementation_core, "f");
191
+ };
192
+ export const IAMService = new IAMServiceImplementation();
2106
193
  //# sourceMappingURL=IAMService.js.map