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