@tidecloak/js 0.13.11 → 0.13.13

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.
@@ -1,11 +1,12 @@
1
1
  import { makePkce, fetchJson } from "./utils/index.js";
2
- import TideCloak from "../lib/tidecloak.js";
2
+ import TideCloak, { RequestEnclave } from "../lib/tidecloak.js";
3
3
  /**
4
4
  * Singleton IAMService wrapping the TideCloak client.
5
5
  *
6
- * Supports two modes:
6
+ * Supports three modes:
7
7
  * - **Front-channel mode**: Browser handles all token operations (standard OIDC)
8
8
  * - **Hybrid/BFF mode**: Browser handles PKCE, backend exchanges code for tokens (more secure)
9
+ * - **Native mode**: External browser for login, app handles tokens via adapter (Electron, Tauri, React Native)
9
10
  *
10
11
  * ---
11
12
  * ## Front-channel Mode
@@ -105,6 +106,21 @@ class IAMService {
105
106
  this._hybridCallbackHandled = false; // Guard against React StrictMode double-execution
106
107
  this._hybridCallbackPromise = null; // Promise for pending token exchange (for StrictMode)
107
108
  this._cachedCallbackData = null; // Cache for getHybridCallbackData to prevent data loss
109
+ // --- Native mode state ---
110
+ this._nativeAdapter = null;
111
+ this._nativeAuthenticated = false;
112
+ this._nativeTokens = null;
113
+ this._nativeCallbackUnsubscribe = null;
114
+ this._nativeCallbackHandled = false;
115
+ this._nativeCallbackPromise = null;
116
+ this._nativeCallbackProcessing = false; // Guard against concurrent callback processing
117
+ // --- Native mode encryption state ---
118
+ this._nativeDoken = null;
119
+ this._nativeDokenParsed = null;
120
+ this._nativeVoucher = null; // Voucher fetched during login for encryption
121
+ this._nativeRequestEnclave = null;
122
+ this._nativeEncryptionCallbackUnsubscribe = null;
123
+ this._pendingEncryptionRequests = new Map(); // requestId -> { resolve, reject }
108
124
  }
109
125
  /**
110
126
  * Register an event listener.
@@ -149,6 +165,14 @@ class IAMService {
149
165
  var _a;
150
166
  return (((_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode) || "frontchannel").toLowerCase() === "hybrid";
151
167
  }
168
+ /**
169
+ * Check if running in native mode.
170
+ * @returns {boolean}
171
+ */
172
+ isNativeMode() {
173
+ var _a;
174
+ return (((_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode) || "frontchannel").toLowerCase() === "native";
175
+ }
152
176
  /**
153
177
  * Load TideCloak configuration and instantiate the client once.
154
178
  * @param {Object} config - TideCloak configuration object.
@@ -166,6 +190,15 @@ class IAMService {
166
190
  if (this.isHybridMode()) {
167
191
  return this._config;
168
192
  }
193
+ // Native mode: store adapter, do not construct TideCloak client
194
+ if (this.isNativeMode()) {
195
+ if (!config.adapter) {
196
+ console.error("[loadConfig] Native mode requires config.adapter with platform-specific functions");
197
+ return null;
198
+ }
199
+ this._nativeAdapter = config.adapter;
200
+ return this._config;
201
+ }
169
202
  try {
170
203
  this._tc = new TideCloak({
171
204
  url: config["auth-server-url"],
@@ -248,6 +281,128 @@ class IAMService {
248
281
  })();
249
282
  return this._hybridCallbackPromise;
250
283
  }
284
+ // --- Native mode init: check stored tokens and subscribe to callbacks ---
285
+ if (this.isNativeMode()) {
286
+ // Guard against React StrictMode double-execution
287
+ if (this._nativeCallbackHandled) {
288
+ console.debug("[IAMService] Native callback already handled");
289
+ if (this._nativeCallbackPromise) {
290
+ console.debug("[IAMService] Waiting for pending native token exchange...");
291
+ return this._nativeCallbackPromise;
292
+ }
293
+ this._emit("ready", this._nativeAuthenticated);
294
+ return this._nativeAuthenticated;
295
+ }
296
+ this._nativeCallbackPromise = (async () => {
297
+ var _a;
298
+ // Check for stored tokens
299
+ const storedTokens = await this._nativeAdapter.getTokens();
300
+ // sessionMode: 'online' (default) = validate tokens, refresh if needed, require login if invalid
301
+ // sessionMode: 'offline' = accept stored tokens without validation (for offline-first apps)
302
+ const sessionMode = ((_a = this._config) === null || _a === void 0 ? void 0 : _a.sessionMode) || 'online';
303
+ if (storedTokens) {
304
+ console.debug("[IAMService] Found stored tokens in native mode, sessionMode:", sessionMode);
305
+ // Load doken if present
306
+ if (storedTokens.doken) {
307
+ this._nativeDoken = storedTokens.doken;
308
+ this._nativeDokenParsed = this._parseToken(storedTokens.doken);
309
+ console.debug("[IAMService] Loaded doken from stored tokens");
310
+ }
311
+ // Load voucher if present
312
+ if (storedTokens.voucher) {
313
+ this._nativeVoucher = storedTokens.voucher;
314
+ console.debug("[IAMService] Loaded voucher from stored tokens");
315
+ }
316
+ if (sessionMode === 'offline') {
317
+ // Offline mode: Accept stored tokens without validation
318
+ this._nativeTokens = storedTokens;
319
+ this._nativeAuthenticated = true;
320
+ }
321
+ else {
322
+ // Online mode: Validate tokens before accepting
323
+ try {
324
+ const payload = JSON.parse(atob(storedTokens.accessToken.split('.')[1]));
325
+ const exp = payload.exp * 1000;
326
+ const now = Date.now();
327
+ if (exp > now) {
328
+ // Token still valid
329
+ console.debug("[IAMService] Online mode: token valid, authenticating");
330
+ this._nativeTokens = storedTokens;
331
+ this._nativeAuthenticated = true;
332
+ }
333
+ else if (storedTokens.refreshToken) {
334
+ // Token expired, try refresh
335
+ console.debug("[IAMService] Online mode: token expired, attempting refresh");
336
+ try {
337
+ const newTokens = await this._refreshNativeToken(storedTokens.refreshToken);
338
+ this._nativeTokens = newTokens;
339
+ this._nativeAuthenticated = true;
340
+ await this._nativeAdapter.saveTokens(newTokens);
341
+ console.debug("[IAMService] Online mode: token refreshed successfully");
342
+ }
343
+ catch (refreshErr) {
344
+ console.debug("[IAMService] Online mode: refresh failed, user must login", refreshErr);
345
+ await this._nativeAdapter.deleteTokens();
346
+ this._nativeAuthenticated = false;
347
+ }
348
+ }
349
+ else {
350
+ // Token expired, no refresh token
351
+ console.debug("[IAMService] Online mode: token expired, no refresh token");
352
+ await this._nativeAdapter.deleteTokens();
353
+ this._nativeAuthenticated = false;
354
+ }
355
+ }
356
+ catch (parseErr) {
357
+ console.error("[IAMService] Online mode: failed to parse token", parseErr);
358
+ await this._nativeAdapter.deleteTokens();
359
+ this._nativeAuthenticated = false;
360
+ }
361
+ }
362
+ }
363
+ // Subscribe to auth callbacks from native app
364
+ this._nativeCallbackUnsubscribe = this._nativeAdapter.onAuthCallback(async ({ code, voucher, error, errorDescription }) => {
365
+ // Guard against duplicate callback processing
366
+ if (this._nativeCallbackProcessing || this._nativeAuthenticated) {
367
+ console.debug("[IAMService] Ignoring duplicate native callback");
368
+ return;
369
+ }
370
+ if (error) {
371
+ console.error("[IAMService] Native auth error:", error, errorDescription);
372
+ this._emit("authError", new Error(`${error}: ${errorDescription || "Unknown error"}`));
373
+ return;
374
+ }
375
+ if (code) {
376
+ await this._handleNativeCallback(code, voucher);
377
+ }
378
+ });
379
+ // Subscribe to encryption callbacks from native app (if adapter supports it)
380
+ if (this._nativeAdapter.onEncryptionCallback) {
381
+ this._nativeEncryptionCallbackUnsubscribe = this._nativeAdapter.onEncryptionCallback(({ operation, requestId, result, error }) => {
382
+ const pending = this._pendingEncryptionRequests.get(requestId);
383
+ if (pending) {
384
+ this._pendingEncryptionRequests.delete(requestId);
385
+ if (error) {
386
+ pending.reject(new Error(error));
387
+ }
388
+ else if (result) {
389
+ pending.resolve(result);
390
+ }
391
+ else {
392
+ pending.reject(new Error("Empty result from encryption callback"));
393
+ }
394
+ }
395
+ else {
396
+ console.warn("[IAMService] Received encryption callback for unknown requestId:", requestId);
397
+ }
398
+ });
399
+ }
400
+ this._emit("ready", this._nativeAuthenticated);
401
+ this._nativeCallbackPromise = null;
402
+ return this._nativeAuthenticated;
403
+ })();
404
+ return this._nativeCallbackPromise;
405
+ }
251
406
  // --- Front-channel mode ---
252
407
  if (!this._tc) {
253
408
  const err = new Error("TideCloak client not available");
@@ -291,10 +446,12 @@ class IAMService {
291
446
  }
292
447
  return this._config;
293
448
  }
294
- /** @returns {boolean} Whether there's a valid token (or session in hybrid mode) */
449
+ /** @returns {boolean} Whether there's a valid token (or session in hybrid/native mode) */
295
450
  isLoggedIn() {
296
451
  if (this.isHybridMode())
297
452
  return this._hybridAuthenticated;
453
+ if (this.isNativeMode())
454
+ return this._nativeAuthenticated;
298
455
  return !!this.getTideCloakClient().token;
299
456
  }
300
457
  /**
@@ -306,6 +463,28 @@ class IAMService {
306
463
  if (this.isHybridMode()) {
307
464
  throw new Error("getToken() not available in hybrid mode - tokens are server-side");
308
465
  }
466
+ if (this.isNativeMode()) {
467
+ const tokens = await this._nativeAdapter.getTokens();
468
+ if (!tokens)
469
+ return null;
470
+ // Check if token is expired or about to expire (30 second buffer)
471
+ const now = Date.now();
472
+ const bufferMs = 30 * 1000;
473
+ if (now >= tokens.expiresAt - bufferMs) {
474
+ console.debug("[IAMService] Native token expired, refreshing...");
475
+ try {
476
+ const newTokens = await this._refreshNativeToken(tokens.refreshToken);
477
+ await this._nativeAdapter.saveTokens(newTokens);
478
+ this._nativeTokens = newTokens;
479
+ return newTokens.accessToken;
480
+ }
481
+ catch (err) {
482
+ console.error("[IAMService] Native token refresh failed:", err);
483
+ return null;
484
+ }
485
+ }
486
+ return tokens.accessToken;
487
+ }
309
488
  const exp = this.getTokenExp();
310
489
  if (exp < 3)
311
490
  await this.updateIAMToken();
@@ -317,9 +496,22 @@ class IAMService {
317
496
  * @throws {Error} In hybrid mode (tokens are server-side)
318
497
  */
319
498
  getTokenExp() {
499
+ var _a;
320
500
  if (this.isHybridMode()) {
321
501
  throw new Error("getTokenExp() not available in hybrid mode - tokens are server-side");
322
502
  }
503
+ if (this.isNativeMode()) {
504
+ if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
505
+ return 0;
506
+ try {
507
+ const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
508
+ return Math.round(payload.exp - Date.now() / 1000);
509
+ }
510
+ catch (e) {
511
+ console.error("[IAMService] Failed to parse token for expiry:", e);
512
+ return 0;
513
+ }
514
+ }
323
515
  const kc = this.getTideCloakClient();
324
516
  return Math.round(kc.tokenParsed.exp + kc.timeSkew - Date.now() / 1000);
325
517
  }
@@ -329,9 +521,13 @@ class IAMService {
329
521
  * @throws {Error} In hybrid mode (tokens are server-side)
330
522
  */
331
523
  getIDToken() {
524
+ var _a;
332
525
  if (this.isHybridMode()) {
333
526
  throw new Error("getIDToken() not available in hybrid mode - tokens are server-side");
334
527
  }
528
+ if (this.isNativeMode()) {
529
+ return ((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.idToken) || null;
530
+ }
335
531
  return this.getTideCloakClient().idToken;
336
532
  }
337
533
  /**
@@ -343,6 +539,9 @@ class IAMService {
343
539
  if (this.isHybridMode()) {
344
540
  throw new Error("getName() not available in hybrid mode - tokens are server-side");
345
541
  }
542
+ if (this.isNativeMode()) {
543
+ return this.getValueFromToken('preferred_username');
544
+ }
346
545
  return this.getTideCloakClient().tokenParsed.preferred_username;
347
546
  }
348
547
  /**
@@ -360,9 +559,23 @@ class IAMService {
360
559
  * @throws {Error} In hybrid mode (role checks not available client-side)
361
560
  */
362
561
  hasRealmRole(role) {
562
+ var _a, _b;
363
563
  if (this.isHybridMode()) {
364
564
  throw new Error("hasRealmRole() not available in hybrid mode - tokens are server-side");
365
565
  }
566
+ if (this.isNativeMode()) {
567
+ if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
568
+ return false;
569
+ try {
570
+ const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
571
+ const realmRoles = ((_b = payload.realm_access) === null || _b === void 0 ? void 0 : _b.roles) || [];
572
+ return realmRoles.includes(role);
573
+ }
574
+ catch (e) {
575
+ console.error("[IAMService] Failed to parse token for realm role check:", e);
576
+ return false;
577
+ }
578
+ }
366
579
  return this.getTideCloakClient().hasRealmRole(role);
367
580
  }
368
581
  /**
@@ -373,9 +586,24 @@ class IAMService {
373
586
  * @throws {Error} In hybrid mode (role checks not available client-side)
374
587
  */
375
588
  hasClientRole(role, client) {
589
+ var _a, _b, _c, _d;
376
590
  if (this.isHybridMode()) {
377
591
  throw new Error("hasClientRole() not available in hybrid mode - tokens are server-side");
378
592
  }
593
+ if (this.isNativeMode()) {
594
+ if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
595
+ return false;
596
+ try {
597
+ const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
598
+ const clientId = client || ((_b = this._config) === null || _b === void 0 ? void 0 : _b.resource);
599
+ const clientRoles = ((_d = (_c = payload.resource_access) === null || _c === void 0 ? void 0 : _c[clientId]) === null || _d === void 0 ? void 0 : _d.roles) || [];
600
+ return clientRoles.includes(role);
601
+ }
602
+ catch (e) {
603
+ console.error("[IAMService] Failed to parse token for client role check:", e);
604
+ return false;
605
+ }
606
+ }
379
607
  return this.getTideCloakClient().hasResourceRole(role, client);
380
608
  }
381
609
  /**
@@ -385,11 +613,23 @@ class IAMService {
385
613
  * @throws {Error} In hybrid mode (tokens are server-side)
386
614
  */
387
615
  getValueFromToken(key) {
388
- var _a;
616
+ var _a, _b;
389
617
  if (this.isHybridMode()) {
390
618
  throw new Error("getValueFromToken() not available in hybrid mode - tokens are server-side");
391
619
  }
392
- return (_a = this.getTideCloakClient().tokenParsed[key]) !== null && _a !== void 0 ? _a : null;
620
+ if (this.isNativeMode()) {
621
+ if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
622
+ return null;
623
+ try {
624
+ const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
625
+ return payload[key] !== undefined ? payload[key] : null;
626
+ }
627
+ catch (e) {
628
+ console.error("[IAMService] Failed to parse access token:", e);
629
+ return null;
630
+ }
631
+ }
632
+ return (_b = this.getTideCloakClient().tokenParsed[key]) !== null && _b !== void 0 ? _b : null;
393
633
  }
394
634
  /**
395
635
  * Get custom claim from ID token.
@@ -398,11 +638,23 @@ class IAMService {
398
638
  * @throws {Error} In hybrid mode (tokens are server-side)
399
639
  */
400
640
  getValueFromIDToken(key) {
401
- var _a;
641
+ var _a, _b;
402
642
  if (this.isHybridMode()) {
403
643
  throw new Error("getValueFromIDToken() not available in hybrid mode - tokens are server-side");
404
644
  }
405
- return (_a = this.getTideCloakClient().idTokenParsed[key]) !== null && _a !== void 0 ? _a : null;
645
+ if (this.isNativeMode()) {
646
+ if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.idToken))
647
+ return null;
648
+ try {
649
+ const payload = JSON.parse(atob(this._nativeTokens.idToken.split('.')[1]));
650
+ return payload[key] !== undefined ? payload[key] : null;
651
+ }
652
+ catch (e) {
653
+ console.error("[IAMService] Failed to parse ID token:", e);
654
+ return null;
655
+ }
656
+ }
657
+ return (_b = this.getTideCloakClient().idTokenParsed[key]) !== null && _b !== void 0 ? _b : null;
406
658
  }
407
659
  /**
408
660
  * Refreshes token if expired or about to expire.
@@ -413,6 +665,16 @@ class IAMService {
413
665
  if (this.isHybridMode()) {
414
666
  throw new Error("updateIAMToken() not available in hybrid mode - tokens are server-side");
415
667
  }
668
+ if (this.isNativeMode()) {
669
+ // Native mode token refresh is handled in getToken()
670
+ // Just check if tokens need refresh and return status
671
+ const exp = this.getTokenExp();
672
+ if (exp < 30) {
673
+ // Force refresh via getToken
674
+ await this.getToken();
675
+ }
676
+ return exp < 30;
677
+ }
416
678
  const kc = this.getTideCloakClient();
417
679
  const refreshed = await kc.updateToken();
418
680
  const expiresIn = this.getTokenExp();
@@ -428,9 +690,26 @@ class IAMService {
428
690
  * @throws {Error} In hybrid mode (token refresh handled server-side)
429
691
  */
430
692
  async forceUpdateToken() {
693
+ var _a;
431
694
  if (this.isHybridMode()) {
432
695
  throw new Error("forceUpdateToken() not available in hybrid mode - tokens are server-side");
433
696
  }
697
+ if (this.isNativeMode()) {
698
+ // Force refresh by clearing cached tokens and refreshing
699
+ if ((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.refreshToken) {
700
+ try {
701
+ const newTokens = await this._refreshNativeToken(this._nativeTokens.refreshToken);
702
+ this._nativeTokens = newTokens;
703
+ await this._nativeAdapter.saveTokens(newTokens);
704
+ return true;
705
+ }
706
+ catch (err) {
707
+ console.error("[IAMService] Native force refresh failed:", err);
708
+ return false;
709
+ }
710
+ }
711
+ return false;
712
+ }
434
713
  document.cookie = 'kcToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
435
714
  const kc = this.getTideCloakClient();
436
715
  const refreshed = await kc.updateToken(-1);
@@ -444,12 +723,14 @@ class IAMService {
444
723
  /**
445
724
  * Start login redirect.
446
725
  * In hybrid mode, initiates PKCE flow and redirects to IdP.
447
- * @param {string} [returnUrl] - URL to redirect to after successful auth (hybrid mode only)
726
+ * In native mode, opens external browser with auth URL.
727
+ * @param {string} [returnUrl] - URL to redirect to after successful auth (hybrid/native mode)
448
728
  */
449
729
  doLogin(returnUrl = "") {
450
730
  var _a, _b;
451
731
  console.debug("[IAMService.doLogin] Called with returnUrl:", returnUrl);
452
732
  console.debug("[IAMService.doLogin] isHybridMode:", this.isHybridMode());
733
+ console.debug("[IAMService.doLogin] isNativeMode:", this.isNativeMode());
453
734
  console.debug("[IAMService.doLogin] authMode config:", (_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode);
454
735
  if (this.isHybridMode()) {
455
736
  // Catch and log any errors from the async function
@@ -458,6 +739,12 @@ class IAMService {
458
739
  throw err;
459
740
  });
460
741
  }
742
+ if (this.isNativeMode()) {
743
+ return this._startNativeLogin(returnUrl).catch(err => {
744
+ console.error("[IAMService.doLogin] Error in native login:", err);
745
+ throw err;
746
+ });
747
+ }
461
748
  this.getTideCloakClient().login({
462
749
  redirectUri: (_b = this._config["redirectUri"]) !== null && _b !== void 0 ? _b : `${window.location.origin}/auth/redirect`
463
750
  });
@@ -465,28 +752,39 @@ class IAMService {
465
752
  /**
466
753
  * Encrypt data via adapter.
467
754
  * Not available in hybrid mode (encryption requires client-side doken).
755
+ * @param {{ data: string | Uint8Array, tags: string[] }[]} data - Array of objects to encrypt
756
+ * @returns {Promise<(string | Uint8Array)[]>} Array of encrypted values
468
757
  */
469
758
  async doEncrypt(data) {
470
759
  if (this.isHybridMode()) {
471
760
  throw new Error("Encrypt not supported in hybrid mode (tokens are server-side)");
472
761
  }
762
+ if (this.isNativeMode()) {
763
+ return this._nativeEncrypt(data);
764
+ }
473
765
  return this.getTideCloakClient().encrypt(data);
474
766
  }
475
767
  /**
476
768
  * Decrypt data via adapter.
477
769
  * Not available in hybrid mode (decryption requires client-side doken).
770
+ * @param {{ encrypted: string | Uint8Array, tags: string[] }[]} data - Array of objects to decrypt
771
+ * @returns {Promise<(string | Uint8Array)[]>} Array of decrypted values
478
772
  */
479
773
  async doDecrypt(data) {
480
774
  if (this.isHybridMode()) {
481
775
  throw new Error("Decrypt not supported in hybrid mode (tokens are server-side)");
482
776
  }
777
+ if (this.isNativeMode()) {
778
+ return this._nativeDecrypt(data);
779
+ }
483
780
  return this.getTideCloakClient().decrypt(data);
484
781
  }
485
782
  /**
486
783
  * Logout, clear cookie/session, then redirect.
487
784
  * In hybrid mode, clears local state and emits logout event.
785
+ * In native mode, deletes tokens via adapter and emits logout event.
488
786
  */
489
- doLogout() {
787
+ async doLogout() {
490
788
  var _a;
491
789
  if (this.isHybridMode()) {
492
790
  this._hybridAuthenticated = false;
@@ -494,6 +792,39 @@ class IAMService {
494
792
  this._emit("logout");
495
793
  return;
496
794
  }
795
+ if (this.isNativeMode()) {
796
+ await this._nativeAdapter.deleteTokens();
797
+ this._nativeTokens = null;
798
+ this._nativeAuthenticated = false;
799
+ this._nativeDoken = null;
800
+ this._nativeDokenParsed = null;
801
+ this._nativeVoucher = null;
802
+ // Reset callback flags so next login reinitializes properly
803
+ this._nativeCallbackHandled = false;
804
+ this._nativeCallbackProcessing = false;
805
+ this._nativeCallbackPromise = null;
806
+ // Unsubscribe from callbacks (will be resubscribed on next init)
807
+ if (this._nativeCallbackUnsubscribe) {
808
+ this._nativeCallbackUnsubscribe();
809
+ this._nativeCallbackUnsubscribe = null;
810
+ }
811
+ if (this._nativeEncryptionCallbackUnsubscribe) {
812
+ this._nativeEncryptionCallbackUnsubscribe();
813
+ this._nativeEncryptionCallbackUnsubscribe = null;
814
+ }
815
+ // Close and cleanup the request enclave
816
+ if (this._nativeRequestEnclave) {
817
+ try {
818
+ this._nativeRequestEnclave.close();
819
+ }
820
+ catch (e) {
821
+ console.debug("[IAMService] Error closing request enclave:", e);
822
+ }
823
+ this._nativeRequestEnclave = null;
824
+ }
825
+ this._emit("logout");
826
+ return;
827
+ }
497
828
  document.cookie = 'kcToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
498
829
  this.getTideCloakClient().logout({
499
830
  redirectUri: (_a = this._config["redirectUri"]) !== null && _a !== void 0 ? _a : `${window.location.origin}/auth/redirect`
@@ -767,6 +1098,590 @@ class IAMService {
767
1098
  return { handled: true, authenticated: false, returnUrl };
768
1099
  }
769
1100
  }
1101
+ // ---------------------------------------------------------------------------
1102
+ // NATIVE MODE SUPPORT (private helpers)
1103
+ // ---------------------------------------------------------------------------
1104
+ /**
1105
+ * Get OIDC configuration from the native config.
1106
+ * @private
1107
+ * @returns {{ authServerUrl: string, realm: string, clientId: string, scope: string }}
1108
+ */
1109
+ _getNativeOIDCConfig() {
1110
+ const config = this._config;
1111
+ return {
1112
+ authServerUrl: config["auth-server-url"],
1113
+ realm: config.realm,
1114
+ clientId: config.resource,
1115
+ scope: config.scope || "openid profile email",
1116
+ };
1117
+ }
1118
+ /**
1119
+ * Get encryption configuration from the native config.
1120
+ * Automatically selects clientOriginAuth based on window.location.origin.
1121
+ * @private
1122
+ * @returns {{ vendorId: string, homeOrkUrl: string, clientOriginAuth: string } | null}
1123
+ */
1124
+ _getNativeEncryptionConfig() {
1125
+ const config = this._config;
1126
+ if (!config.vendorId) {
1127
+ return null;
1128
+ }
1129
+ // Auto-select clientOriginAuth based on current origin
1130
+ const origin = typeof window !== 'undefined' ? window.location.origin : '';
1131
+ const clientOriginAuthKey = `client-origin-auth-${origin}`;
1132
+ const clientOriginAuth = config[clientOriginAuthKey];
1133
+ return {
1134
+ vendorId: config.vendorId,
1135
+ homeOrkUrl: config.homeOrkUrl,
1136
+ clientOriginAuth,
1137
+ };
1138
+ }
1139
+ /**
1140
+ * Start native login flow: generate PKCE, open external browser with auth URL.
1141
+ * @private
1142
+ * @param {string} returnUrl - URL to redirect to after successful auth
1143
+ */
1144
+ async _startNativeLogin(returnUrl = "") {
1145
+ console.debug("[IAMService._startNativeLogin] Starting native login flow");
1146
+ // Reset auth state to allow new callback to be processed
1147
+ // This is needed for re-login after session expiry
1148
+ this._nativeAuthenticated = false;
1149
+ this._nativeCallbackProcessing = false;
1150
+ const adapter = this._nativeAdapter;
1151
+ if (!adapter) {
1152
+ throw new Error("Native adapter not configured");
1153
+ }
1154
+ // Get OIDC config from the main config
1155
+ const { authServerUrl, realm, clientId, scope } = this._getNativeOIDCConfig();
1156
+ // Generate PKCE
1157
+ const { verifier, challenge, method } = await makePkce();
1158
+ console.debug("[IAMService._startNativeLogin] PKCE generated, verifier length:", verifier.length);
1159
+ // Store PKCE verifier in sessionStorage (will be retrieved when callback is received)
1160
+ sessionStorage.setItem("kc_pkce_verifier", verifier);
1161
+ if (returnUrl) {
1162
+ sessionStorage.setItem("kc_return_url", returnUrl);
1163
+ }
1164
+ // Get redirect URI from adapter (can be async)
1165
+ const redirectUri = await Promise.resolve(adapter.getRedirectUri());
1166
+ sessionStorage.setItem("kc_redirect_uri", redirectUri);
1167
+ // Build auth URL
1168
+ const authUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/auth` +
1169
+ `?client_id=${encodeURIComponent(clientId)}` +
1170
+ `&redirect_uri=${encodeURIComponent(redirectUri)}` +
1171
+ `&response_type=code` +
1172
+ `&scope=${encodeURIComponent(scope)}` +
1173
+ `&code_challenge=${encodeURIComponent(challenge)}` +
1174
+ `&code_challenge_method=${encodeURIComponent(method)}` +
1175
+ `&prompt=login`;
1176
+ console.debug("[IAMService._startNativeLogin] Opening auth URL in external browser");
1177
+ // Open in external browser via adapter
1178
+ await adapter.openExternalUrl(authUrl);
1179
+ }
1180
+ /**
1181
+ * Handle native auth callback: exchange code for tokens.
1182
+ * @private
1183
+ * @param {string} code - Authorization code from IdP
1184
+ * @param {string} [voucher] - Optional voucher fetched during login (for encryption)
1185
+ */
1186
+ async _handleNativeCallback(code, voucher) {
1187
+ // Guard against concurrent/duplicate callback processing
1188
+ if (this._nativeCallbackProcessing) {
1189
+ console.debug("[IAMService._handleNativeCallback] Already processing callback, ignoring duplicate");
1190
+ return;
1191
+ }
1192
+ this._nativeCallbackProcessing = true;
1193
+ this._nativeCallbackHandled = true;
1194
+ console.debug("[IAMService._handleNativeCallback] Handling native callback with code");
1195
+ const adapter = this._nativeAdapter;
1196
+ const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
1197
+ // Retrieve PKCE verifier
1198
+ const verifier = sessionStorage.getItem("kc_pkce_verifier");
1199
+ const redirectUri = sessionStorage.getItem("kc_redirect_uri") || await Promise.resolve(adapter.getRedirectUri());
1200
+ const returnUrl = sessionStorage.getItem("kc_return_url") || "";
1201
+ // Clear session storage immediately to prevent reuse
1202
+ sessionStorage.removeItem("kc_pkce_verifier");
1203
+ sessionStorage.removeItem("kc_redirect_uri");
1204
+ sessionStorage.removeItem("kc_return_url");
1205
+ if (!verifier) {
1206
+ console.debug("[IAMService._handleNativeCallback] PKCE verifier not found, callback already processed");
1207
+ this._nativeCallbackProcessing = false;
1208
+ // Don't emit error - this is likely a duplicate callback after successful auth
1209
+ return;
1210
+ }
1211
+ try {
1212
+ // Exchange code for tokens at token endpoint
1213
+ const tokenUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/token`;
1214
+ const body = new URLSearchParams({
1215
+ grant_type: "authorization_code",
1216
+ client_id: clientId,
1217
+ code: code,
1218
+ redirect_uri: redirectUri,
1219
+ code_verifier: verifier,
1220
+ });
1221
+ const response = await fetch(tokenUrl, {
1222
+ method: "POST",
1223
+ headers: {
1224
+ "Content-Type": "application/x-www-form-urlencoded",
1225
+ },
1226
+ body: body.toString(),
1227
+ });
1228
+ if (!response.ok) {
1229
+ const errorText = await response.text();
1230
+ throw new Error(`Token exchange failed: ${errorText}`);
1231
+ }
1232
+ const data = await response.json();
1233
+ console.debug("[IAMService._handleNativeCallback] Token exchange successful");
1234
+ // Note: Voucher is NOT fetched here - the RequestEnclave will fetch it
1235
+ // via its ORK iframe when encryption/decryption is needed. This avoids
1236
+ // the complexity of session cookie handling during login.
1237
+ // Build token object for storage
1238
+ const tokens = {
1239
+ accessToken: data.access_token,
1240
+ refreshToken: data.refresh_token,
1241
+ idToken: data.id_token,
1242
+ doken: data.doken, // Tide doken for encryption/decryption
1243
+ expiresAt: Date.now() + data.expires_in * 1000,
1244
+ };
1245
+ // Save tokens via adapter
1246
+ await adapter.saveTokens(tokens);
1247
+ this._nativeTokens = tokens;
1248
+ this._nativeDoken = data.doken;
1249
+ this._nativeDokenParsed = data.doken ? this._parseToken(data.doken) : null;
1250
+ // Note: _nativeVoucher is NOT set here - RequestEnclave fetches it on-demand
1251
+ this._nativeAuthenticated = true;
1252
+ this._nativeCallbackProcessing = false;
1253
+ this._emit("authSuccess");
1254
+ console.debug("[IAMService._handleNativeCallback] Native auth complete, returnUrl:", returnUrl);
1255
+ }
1256
+ catch (err) {
1257
+ console.error("[IAMService._handleNativeCallback] Token exchange error:", err);
1258
+ this._nativeAuthenticated = false;
1259
+ this._nativeCallbackProcessing = false;
1260
+ this._emit("authError", err);
1261
+ }
1262
+ }
1263
+ /**
1264
+ * Refresh native token using refresh token.
1265
+ * @private
1266
+ * @param {string} refreshToken - The refresh token
1267
+ * @returns {Promise<Object>} New token data
1268
+ */
1269
+ async _refreshNativeToken(refreshToken) {
1270
+ const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
1271
+ const tokenUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/token`;
1272
+ const body = new URLSearchParams({
1273
+ grant_type: "refresh_token",
1274
+ client_id: clientId,
1275
+ refresh_token: refreshToken,
1276
+ });
1277
+ const response = await fetch(tokenUrl, {
1278
+ method: "POST",
1279
+ headers: {
1280
+ "Content-Type": "application/x-www-form-urlencoded",
1281
+ },
1282
+ body: body.toString(),
1283
+ });
1284
+ if (!response.ok) {
1285
+ const errorText = await response.text();
1286
+ throw new Error(`Token refresh failed: ${errorText}`);
1287
+ }
1288
+ const data = await response.json();
1289
+ // Update doken if present in refresh response
1290
+ if (data.doken) {
1291
+ this._nativeDoken = data.doken;
1292
+ this._nativeDokenParsed = this._parseToken(data.doken);
1293
+ console.debug("[IAMService] Updated doken from token refresh");
1294
+ }
1295
+ return {
1296
+ accessToken: data.access_token,
1297
+ refreshToken: data.refresh_token,
1298
+ idToken: data.id_token,
1299
+ doken: data.doken,
1300
+ expiresAt: Date.now() + data.expires_in * 1000,
1301
+ };
1302
+ }
1303
+ // ---------------------------------------------------------------------------
1304
+ // NATIVE MODE ENCRYPTION SUPPORT (private helpers)
1305
+ // ---------------------------------------------------------------------------
1306
+ /**
1307
+ * Initialize the native RequestEnclave for encryption/decryption.
1308
+ * @private
1309
+ * @param {string} voucherDataUrl - Pre-fetched voucher as data URL
1310
+ */
1311
+ _initNativeRequestEnclave(voucherDataUrl) {
1312
+ if (!this._nativeDoken) {
1313
+ throw new Error("[IAMService] No doken available for encryption - user must be authenticated with Tide");
1314
+ }
1315
+ if (!this._nativeDokenParsed) {
1316
+ throw new Error("[IAMService] Doken not parsed");
1317
+ }
1318
+ // Get encryption config from the main config (auto-selects clientOriginAuth based on origin)
1319
+ const encryptionConfig = this._getNativeEncryptionConfig();
1320
+ if (!encryptionConfig || !encryptionConfig.vendorId || !encryptionConfig.clientOriginAuth) {
1321
+ throw new Error("[IAMService] Native encryption requires vendorId and client-origin-auth-{origin} in config");
1322
+ }
1323
+ // Reuse existing enclave if already initialized
1324
+ if (this._nativeRequestEnclave) {
1325
+ console.debug("[IAMService] Reusing existing RequestEnclave");
1326
+ return;
1327
+ }
1328
+ const homeOrkOrigin = this._nativeDokenParsed['t.uho'] || encryptionConfig.homeOrkUrl;
1329
+ if (!homeOrkOrigin) {
1330
+ throw new Error("[IAMService] Home ORK URL not available - check doken or config.homeOrkUrl");
1331
+ }
1332
+ console.debug("[IAMService] Initializing native RequestEnclave", {
1333
+ homeOrkOrigin,
1334
+ vendorId: encryptionConfig.vendorId,
1335
+ voucherURL: voucherDataUrl,
1336
+ });
1337
+ this._nativeRequestEnclave = new RequestEnclave({
1338
+ homeOrkOrigin,
1339
+ signed_client_origin: encryptionConfig.clientOriginAuth,
1340
+ vendorId: encryptionConfig.vendorId,
1341
+ voucherURL: voucherDataUrl,
1342
+ isRunningLocal: false,
1343
+ }).init({
1344
+ doken: this._nativeDoken,
1345
+ dokenRefreshCallback: async () => {
1346
+ // Refresh tokens and return the new doken
1347
+ await this.forceUpdateToken();
1348
+ if (!this._nativeDoken) {
1349
+ throw new Error("[IAMService] No doken available after token refresh");
1350
+ }
1351
+ return this._nativeDoken;
1352
+ },
1353
+ requireReloginCallback: async () => {
1354
+ // User needs to re-authenticate
1355
+ console.warn("[IAMService] Re-authentication required for encryption");
1356
+ this._emit("authError", new Error("Re-authentication required"));
1357
+ },
1358
+ });
1359
+ }
1360
+ /**
1361
+ * Build the encryption page URL for external browser.
1362
+ * This page is hosted on the TideCloak server and has session cookies.
1363
+ * @private
1364
+ * @param {string} operation - 'encrypt' or 'decrypt'
1365
+ * @param {string} requestId - Unique request ID
1366
+ * @param {string} dataBase64 - Base64-encoded data to process
1367
+ * @param {string} tagsJson - JSON-encoded tags array
1368
+ * @param {string} callbackUrl - URL to redirect with result
1369
+ * @returns {string} URL to open in external browser
1370
+ */
1371
+ _buildEncryptionPageUrl(operation, requestId, dataBase64, tagsJson, callbackUrl) {
1372
+ const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
1373
+ const encryptionConfig = this._getNativeEncryptionConfig();
1374
+ // Build URL to a page on the TideCloak server that can perform encryption
1375
+ // This page will have access to session cookies
1376
+ const baseUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/tide-encrypt`;
1377
+ const url = new URL(baseUrl);
1378
+ url.searchParams.set('operation', operation);
1379
+ url.searchParams.set('requestId', requestId);
1380
+ url.searchParams.set('data', dataBase64);
1381
+ url.searchParams.set('tags', tagsJson);
1382
+ url.searchParams.set('callback', callbackUrl);
1383
+ url.searchParams.set('vendorId', (encryptionConfig === null || encryptionConfig === void 0 ? void 0 : encryptionConfig.vendorId) || '');
1384
+ url.searchParams.set('clientId', clientId);
1385
+ return url.toString();
1386
+ }
1387
+ /**
1388
+ * Perform an encryption operation via external browser.
1389
+ * @private
1390
+ * @param {'encrypt' | 'decrypt'} operation - The operation to perform
1391
+ * @param {string} dataBase64 - Base64-encoded data to process
1392
+ * @param {string[]} tags - Tags for the operation
1393
+ * @returns {Promise<string>} Base64-encoded result
1394
+ */
1395
+ async _doExternalBrowserOperation(operation, dataBase64, tags) {
1396
+ const adapter = this._nativeAdapter;
1397
+ // Generate unique request ID
1398
+ const requestId = `${operation}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1399
+ // Get callback URL for encryption operations
1400
+ let callbackUrl;
1401
+ if (adapter.getEncryptionRedirectUri) {
1402
+ callbackUrl = await Promise.resolve(adapter.getEncryptionRedirectUri());
1403
+ }
1404
+ else {
1405
+ // Default: use auth redirect URI with /encrypt/callback suffix
1406
+ const authRedirectUri = await Promise.resolve(adapter.getRedirectUri());
1407
+ // Replace /callback with /encrypt/callback, or append if no /callback
1408
+ if (authRedirectUri.endsWith('/callback')) {
1409
+ callbackUrl = authRedirectUri.replace('/callback', '/encrypt/callback');
1410
+ }
1411
+ else {
1412
+ callbackUrl = authRedirectUri + '/encrypt/callback';
1413
+ }
1414
+ }
1415
+ // Build the URL
1416
+ const tagsJson = JSON.stringify(tags);
1417
+ const encryptionUrl = this._buildEncryptionPageUrl(operation, requestId, dataBase64, tagsJson, callbackUrl);
1418
+ console.debug(`[IAMService] Opening external browser for ${operation}:`, {
1419
+ requestId,
1420
+ callbackUrl,
1421
+ urlLength: encryptionUrl.length,
1422
+ });
1423
+ // Create a promise that will resolve when we get the callback
1424
+ const resultPromise = new Promise((resolve, reject) => {
1425
+ // Store the pending request
1426
+ this._pendingEncryptionRequests.set(requestId, { resolve, reject });
1427
+ // Set a timeout (60 seconds)
1428
+ const timeout = setTimeout(() => {
1429
+ if (this._pendingEncryptionRequests.has(requestId)) {
1430
+ this._pendingEncryptionRequests.delete(requestId);
1431
+ reject(new Error(`Encryption ${operation} timed out after 60 seconds`));
1432
+ }
1433
+ }, 60000);
1434
+ // Update the stored request to include timeout cleanup
1435
+ const pending = this._pendingEncryptionRequests.get(requestId);
1436
+ const originalResolve = pending.resolve;
1437
+ const originalReject = pending.reject;
1438
+ pending.resolve = (result) => {
1439
+ clearTimeout(timeout);
1440
+ originalResolve(result);
1441
+ };
1442
+ pending.reject = (error) => {
1443
+ clearTimeout(timeout);
1444
+ originalReject(error);
1445
+ };
1446
+ });
1447
+ // Open the URL in external browser
1448
+ await adapter.openExternalUrl(encryptionUrl);
1449
+ // Wait for the callback
1450
+ return resultPromise;
1451
+ }
1452
+ /**
1453
+ * Get the voucher URL for native mode.
1454
+ * @private
1455
+ * @returns {string} Voucher URL
1456
+ */
1457
+ _getNativeVoucherUrl() {
1458
+ var _a;
1459
+ if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken)) {
1460
+ throw new Error("[IAMService] No access token available for voucher URL");
1461
+ }
1462
+ const tokenPayload = this._parseToken(this._nativeTokens.accessToken);
1463
+ if (!tokenPayload) {
1464
+ throw new Error("[IAMService] Failed to parse access token for voucher URL");
1465
+ }
1466
+ const sid = tokenPayload.sid;
1467
+ if (!sid) {
1468
+ throw new Error("[IAMService] No session ID in access token for voucher URL");
1469
+ }
1470
+ const { authServerUrl, realm } = this._getNativeOIDCConfig();
1471
+ return `${authServerUrl}/realms/${encodeURIComponent(realm)}/tidevouchers/fromUserSession?sessionId=${encodeURIComponent(sid)}`;
1472
+ }
1473
+ /**
1474
+ * Check if user has a realm role (native mode helper).
1475
+ * @private
1476
+ * @param {string} role - Role to check
1477
+ * @returns {boolean}
1478
+ */
1479
+ _nativeHasRealmRole(role) {
1480
+ var _a, _b;
1481
+ if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
1482
+ return false;
1483
+ try {
1484
+ const payload = this._parseToken(this._nativeTokens.accessToken);
1485
+ const realmRoles = ((_b = payload === null || payload === void 0 ? void 0 : payload.realm_access) === null || _b === void 0 ? void 0 : _b.roles) || [];
1486
+ return realmRoles.includes(role);
1487
+ }
1488
+ catch (e) {
1489
+ return false;
1490
+ }
1491
+ }
1492
+ /**
1493
+ * Native mode encryption using RequestEnclave.
1494
+ * Uses either a pre-fetched voucher (as data URL) or the live voucherURL
1495
+ * that the ORK iframe will fetch with session cookies.
1496
+ * @private
1497
+ * @param {{ data: string | Uint8Array, tags: string[] }[]} toEncrypt
1498
+ * @returns {Promise<(string | Uint8Array)[]>}
1499
+ */
1500
+ async _nativeEncrypt(toEncrypt) {
1501
+ // Ensure token is fresh
1502
+ await this.updateIAMToken();
1503
+ if (!Array.isArray(toEncrypt)) {
1504
+ throw new Error("Pass array as parameter");
1505
+ }
1506
+ if (!this._nativeAuthenticated) {
1507
+ throw new Error("Not authenticated");
1508
+ }
1509
+ // Convert and validate input
1510
+ const dataToSend = toEncrypt.map((e) => {
1511
+ if (typeof e !== 'object' || e === null) {
1512
+ throw new Error("All entries must be an object to encrypt");
1513
+ }
1514
+ for (const property of ['data', 'tags']) {
1515
+ if (!e[property]) {
1516
+ throw new Error(`The object is missing the required '${property}' property.`);
1517
+ }
1518
+ }
1519
+ if (!Array.isArray(e.tags)) {
1520
+ throw new Error("tags must be provided as a string array");
1521
+ }
1522
+ if (typeof e.data !== 'string' && !(e.data instanceof Uint8Array)) {
1523
+ throw new Error("data must be provided as string or Uint8Array");
1524
+ }
1525
+ // Check roles
1526
+ for (const tag of e.tags) {
1527
+ if (typeof tag !== 'string') {
1528
+ throw new Error("tags must be provided as an array of strings");
1529
+ }
1530
+ const tagAccess = this._nativeHasRealmRole(`_tide_${tag}.selfencrypt`);
1531
+ if (!tagAccess) {
1532
+ throw new Error(`User has not been given any access to '${tag}'`);
1533
+ }
1534
+ }
1535
+ return {
1536
+ data: typeof e.data === 'string' ? this._stringToUint8Array(e.data) : e.data,
1537
+ tags: e.tags,
1538
+ isRaw: typeof e.data === 'string' ? false : true,
1539
+ };
1540
+ });
1541
+ // Get voucher URL - RequestEnclave will fetch it via ORK iframe
1542
+ const voucherUrl = this._getNativeVoucherUrl();
1543
+ console.debug("[IAMService._nativeEncrypt] Using voucherURL:", voucherUrl);
1544
+ this._initNativeRequestEnclave(voucherUrl);
1545
+ const encrypted = await this._nativeRequestEnclave.encrypt(dataToSend);
1546
+ return encrypted.map((cipher, i) => (dataToSend[i].isRaw ? cipher : this._bytesToBase64(cipher)));
1547
+ }
1548
+ /**
1549
+ * Native mode decryption using RequestEnclave.
1550
+ * Uses either a pre-fetched voucher (as data URL) or the live voucherURL
1551
+ * that the ORK iframe will fetch with session cookies.
1552
+ * @private
1553
+ * @param {{ encrypted: string | Uint8Array, tags: string[] }[]} toDecrypt
1554
+ * @returns {Promise<(string | Uint8Array)[]>}
1555
+ */
1556
+ async _nativeDecrypt(toDecrypt) {
1557
+ // Ensure token is fresh
1558
+ await this.updateIAMToken();
1559
+ if (!Array.isArray(toDecrypt)) {
1560
+ throw new Error("Pass array as parameter");
1561
+ }
1562
+ if (!this._nativeAuthenticated) {
1563
+ throw new Error("Not authenticated");
1564
+ }
1565
+ // Convert and validate input
1566
+ const dataToSend = toDecrypt.map((e) => {
1567
+ if (typeof e !== 'object' || e === null) {
1568
+ throw new Error("All entries must be an object to decrypt");
1569
+ }
1570
+ for (const property of ['encrypted', 'tags']) {
1571
+ if (!e[property]) {
1572
+ throw new Error(`The object is missing the required '${property}' property.`);
1573
+ }
1574
+ }
1575
+ if (!Array.isArray(e.tags)) {
1576
+ throw new Error("tags must be provided as a string array");
1577
+ }
1578
+ if (typeof e.encrypted !== 'string' && !(e.encrypted instanceof Uint8Array)) {
1579
+ throw new Error("encrypted must be provided as string or Uint8Array");
1580
+ }
1581
+ // Check roles
1582
+ for (const tag of e.tags) {
1583
+ if (typeof tag !== 'string') {
1584
+ throw new Error("tags must be provided as an array of strings");
1585
+ }
1586
+ const tagAccess = this._nativeHasRealmRole(`_tide_${tag}.selfdecrypt`);
1587
+ if (!tagAccess) {
1588
+ throw new Error(`User has not been given any access to '${tag}'`);
1589
+ }
1590
+ }
1591
+ return {
1592
+ encrypted: typeof e.encrypted === 'string' ? this._base64ToBytes(e.encrypted) : e.encrypted,
1593
+ tags: e.tags,
1594
+ isRaw: typeof e.encrypted === 'string' ? false : true,
1595
+ };
1596
+ });
1597
+ // Get voucher URL - RequestEnclave will fetch it via ORK iframe
1598
+ const voucherUrl = this._getNativeVoucherUrl();
1599
+ console.debug("[IAMService._nativeDecrypt] Using voucherURL:", voucherUrl);
1600
+ this._initNativeRequestEnclave(voucherUrl);
1601
+ const decrypted = await this._nativeRequestEnclave.decrypt(dataToSend);
1602
+ return decrypted.map((d, i) => (dataToSend[i].isRaw ? d : this._stringFromUint8Array(d)));
1603
+ }
1604
+ /**
1605
+ * Convert string to Uint8Array.
1606
+ * @private
1607
+ */
1608
+ _stringToUint8Array(str) {
1609
+ return new TextEncoder().encode(str);
1610
+ }
1611
+ /**
1612
+ * Convert Uint8Array to string.
1613
+ * @private
1614
+ */
1615
+ _stringFromUint8Array(bytes) {
1616
+ return new TextDecoder('utf-8').decode(bytes);
1617
+ }
1618
+ /**
1619
+ * Convert bytes to base64.
1620
+ * @private
1621
+ */
1622
+ _bytesToBase64(bytes) {
1623
+ const binString = String.fromCodePoint(...bytes);
1624
+ return btoa(binString);
1625
+ }
1626
+ /**
1627
+ * Convert base64 to bytes.
1628
+ * @private
1629
+ */
1630
+ _base64ToBytes(base64) {
1631
+ const binString = atob(base64);
1632
+ const len = binString.length;
1633
+ const bytes = new Uint8Array(len);
1634
+ for (let i = 0; i < len; i++) {
1635
+ bytes[i] = binString.codePointAt(i);
1636
+ }
1637
+ return bytes;
1638
+ }
1639
+ /**
1640
+ * Parse a JWT token and return its payload.
1641
+ * @private
1642
+ * @param {string} token - JWT token string
1643
+ * @returns {Object|null} Parsed token payload or null on error
1644
+ */
1645
+ _parseToken(token) {
1646
+ if (!token)
1647
+ return null;
1648
+ try {
1649
+ const payload = JSON.parse(atob(token.split('.')[1]));
1650
+ return payload;
1651
+ }
1652
+ catch (e) {
1653
+ console.error("[IAMService] Failed to parse token:", e);
1654
+ return null;
1655
+ }
1656
+ }
1657
+ /**
1658
+ * Cleanup native mode resources.
1659
+ */
1660
+ destroy() {
1661
+ if (this._nativeCallbackUnsubscribe) {
1662
+ this._nativeCallbackUnsubscribe();
1663
+ this._nativeCallbackUnsubscribe = null;
1664
+ }
1665
+ if (this._nativeEncryptionCallbackUnsubscribe) {
1666
+ this._nativeEncryptionCallbackUnsubscribe();
1667
+ this._nativeEncryptionCallbackUnsubscribe = null;
1668
+ }
1669
+ // Cleanup pending encryption requests
1670
+ for (const [requestId, pending] of this._pendingEncryptionRequests) {
1671
+ pending.reject(new Error("IAMService destroyed"));
1672
+ }
1673
+ this._pendingEncryptionRequests.clear();
1674
+ // Cleanup request enclave
1675
+ if (this._nativeRequestEnclave) {
1676
+ try {
1677
+ this._nativeRequestEnclave.close();
1678
+ }
1679
+ catch (e) {
1680
+ console.debug("[IAMService] Error closing request enclave:", e);
1681
+ }
1682
+ this._nativeRequestEnclave = null;
1683
+ }
1684
+ }
770
1685
  }
771
1686
  const IAMServiceInstance = new IAMService();
772
1687
  export { IAMServiceInstance as IAMService };