@korajs/auth 0.1.0 → 0.3.0

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.
package/dist/index.cjs CHANGED
@@ -22,13 +22,40 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthClient: () => AuthClient,
24
24
  AuthError: () => AuthError,
25
+ AutoLockManager: () => AutoLockManager,
25
26
  CryptoUnavailableError: () => CryptoUnavailableError,
26
27
  DeviceIdentityError: () => DeviceIdentityError,
28
+ DeviceKeyStoreError: () => DeviceKeyStoreError,
29
+ EncryptedTokenStore: () => EncryptedTokenStore,
30
+ EncryptedTokenStoreError: () => EncryptedTokenStoreError,
31
+ EncryptionError: () => EncryptionError,
32
+ InMemoryDeviceKeyStore: () => InMemoryDeviceKeyStore,
33
+ IndexedDBDeviceKeyStore: () => IndexedDBDeviceKeyStore,
34
+ KeyDerivationError: () => KeyDerivationError,
35
+ OperationEncryptionError: () => OperationEncryptionError,
36
+ OperationEncryptor: () => OperationEncryptor,
37
+ OrgClient: () => OrgClient,
38
+ OrgClientError: () => OrgClientError,
39
+ PasskeyError: () => PasskeyError,
40
+ PasskeyUnsupportedError: () => PasskeyUnsupportedError,
27
41
  TokenStore: () => TokenStore,
42
+ authenticateWithPasskey: () => authenticateWithPasskey,
28
43
  computePublicKeyThumbprint: () => computePublicKeyThumbprint,
44
+ createDeviceKeyStore: () => createDeviceKeyStore,
45
+ createPasskeyCredential: () => createPasskeyCredential,
46
+ decryptData: () => decryptData,
47
+ deriveEncryptionKey: () => deriveEncryptionKey,
48
+ encryptData: () => encryptData,
49
+ exportKey: () => exportKey,
29
50
  exportPublicKeyJwk: () => exportPublicKeyJwk,
30
51
  fromBase64Url: () => fromBase64Url,
31
52
  generateDeviceKeyPair: () => generateDeviceKeyPair,
53
+ generateEncryptionKey: () => generateEncryptionKey,
54
+ generateSalt: () => generateSalt,
55
+ importKey: () => importKey,
56
+ isEncryptedField: () => isEncryptedField,
57
+ isPasskeySupported: () => isPasskeySupported,
58
+ isPlatformAuthenticatorAvailable: () => isPlatformAuthenticatorAvailable,
32
59
  signChallenge: () => signChallenge,
33
60
  toBase64Url: () => toBase64Url,
34
61
  verifyChallenge: () => verifyChallenge
@@ -129,6 +156,7 @@ var AuthClient = class {
129
156
  _state = "loading";
130
157
  _user = null;
131
158
  _refreshPromise = null;
159
+ _initialized = false;
132
160
  /**
133
161
  * Creates a new AuthClient.
134
162
  *
@@ -165,6 +193,10 @@ var AuthClient = class {
165
193
  * Safe to call multiple times -- subsequent calls are no-ops once initialized.
166
194
  */
167
195
  async initialize() {
196
+ if (this._initialized) {
197
+ return;
198
+ }
199
+ this._initialized = true;
168
200
  const accessToken = this.storage.getAccessToken();
169
201
  const refreshToken = this.storage.getRefreshToken();
170
202
  if (!accessToken || !refreshToken) {
@@ -226,13 +258,26 @@ var AuthClient = class {
226
258
  /**
227
259
  * Sign out the current user.
228
260
  *
229
- * Clears local tokens and state. Does not make a network request to the
230
- * server -- tokens are simply discarded locally.
261
+ * Clears local tokens and attempts to revoke the refresh token on the server
262
+ * (best-effort succeeds even if the server is unreachable). This ensures that
263
+ * stolen refresh tokens cannot be used after the user explicitly signs out.
231
264
  */
232
265
  async signOut() {
266
+ const accessToken = this.storage.getAccessToken();
267
+ const refreshToken = this.storage.getRefreshToken();
233
268
  this.storage.clear();
234
269
  this._refreshPromise = null;
235
270
  this.setState("unauthenticated", null);
271
+ if (accessToken) {
272
+ try {
273
+ await this.request("/auth/signout", {
274
+ method: "POST",
275
+ body: { refreshToken: refreshToken ?? void 0 },
276
+ token: accessToken
277
+ });
278
+ } catch {
279
+ }
280
+ }
236
281
  }
237
282
  // -----------------------------------------------------------------------
238
283
  // Token access
@@ -439,9 +484,272 @@ var AuthClient = class {
439
484
  }
440
485
  };
441
486
 
442
- // src/device/device-identity.ts
487
+ // src/client/org-client.ts
443
488
  var import_core2 = require("@korajs/core");
444
- var CryptoUnavailableError = class extends import_core2.KoraError {
489
+ var OrgClientError = class extends import_core2.KoraError {
490
+ constructor(message, code, context) {
491
+ super(message, code, context);
492
+ this.name = "OrgClientError";
493
+ }
494
+ };
495
+ var OrgClient = class {
496
+ serverUrl;
497
+ getAccessToken;
498
+ listeners = /* @__PURE__ */ new Set();
499
+ _activeOrgId = null;
500
+ _activeOrg = null;
501
+ _activeRole = null;
502
+ constructor(config) {
503
+ this.serverUrl = config.serverUrl.replace(/\/+$/, "");
504
+ this.getAccessToken = config.getAccessToken;
505
+ }
506
+ // --- Getters ---
507
+ /** Currently active organization ID */
508
+ get activeOrgId() {
509
+ return this._activeOrgId;
510
+ }
511
+ /** Currently active organization */
512
+ get activeOrg() {
513
+ return this._activeOrg;
514
+ }
515
+ /** Current user's role in the active organization */
516
+ get activeRole() {
517
+ return this._activeRole;
518
+ }
519
+ // --- Organization Operations ---
520
+ /**
521
+ * Create a new organization.
522
+ */
523
+ async createOrg(params) {
524
+ return this.request("/orgs", {
525
+ method: "POST",
526
+ body: params
527
+ });
528
+ }
529
+ /**
530
+ * List all organizations the current user belongs to.
531
+ */
532
+ async listOrgs() {
533
+ return this.request("/orgs", { method: "GET" });
534
+ }
535
+ /**
536
+ * Get an organization by ID.
537
+ */
538
+ async getOrg(orgId) {
539
+ return this.request(`/orgs/${orgId}`, { method: "GET" });
540
+ }
541
+ /**
542
+ * Update an organization.
543
+ */
544
+ async updateOrg(orgId, params) {
545
+ const result = await this.request(`/orgs/${orgId}`, {
546
+ method: "PATCH",
547
+ body: params
548
+ });
549
+ if (this._activeOrgId === orgId) {
550
+ this._activeOrg = result;
551
+ }
552
+ return result;
553
+ }
554
+ /**
555
+ * Delete an organization.
556
+ */
557
+ async deleteOrg(orgId) {
558
+ await this.request(`/orgs/${orgId}`, { method: "DELETE" });
559
+ if (this._activeOrgId === orgId) {
560
+ this._activeOrgId = null;
561
+ this._activeOrg = null;
562
+ this._activeRole = null;
563
+ this.notifyListeners();
564
+ }
565
+ }
566
+ // --- Org Switching ---
567
+ /**
568
+ * Switch the active organization context.
569
+ * Fetches the org details and the user's membership/role.
570
+ */
571
+ async switchOrg(orgId) {
572
+ const org = await this.request(`/orgs/${orgId}`, { method: "GET" });
573
+ const membership = await this.request(`/orgs/${orgId}/membership`, {
574
+ method: "GET"
575
+ });
576
+ this._activeOrgId = orgId;
577
+ this._activeOrg = org;
578
+ this._activeRole = membership.role;
579
+ this.notifyListeners();
580
+ }
581
+ /**
582
+ * Clear the active organization (no org selected).
583
+ */
584
+ clearActiveOrg() {
585
+ this._activeOrgId = null;
586
+ this._activeOrg = null;
587
+ this._activeRole = null;
588
+ this.notifyListeners();
589
+ }
590
+ // --- Member Management ---
591
+ /**
592
+ * List members of an organization.
593
+ */
594
+ async listMembers(orgId) {
595
+ return this.request(`/orgs/${orgId}/members`, { method: "GET" });
596
+ }
597
+ /**
598
+ * Remove a member from an organization.
599
+ */
600
+ async removeMember(orgId, userId) {
601
+ await this.request(`/orgs/${orgId}/members/${userId}`, { method: "DELETE" });
602
+ }
603
+ /**
604
+ * Update a member's role.
605
+ */
606
+ async updateMemberRole(orgId, userId, role) {
607
+ return this.request(`/orgs/${orgId}/members/${userId}/role`, {
608
+ method: "PATCH",
609
+ body: { role }
610
+ });
611
+ }
612
+ /**
613
+ * Transfer ownership to another member.
614
+ */
615
+ async transferOwnership(orgId, newOwnerId) {
616
+ await this.request(`/orgs/${orgId}/transfer`, {
617
+ method: "POST",
618
+ body: { newOwnerId }
619
+ });
620
+ }
621
+ /**
622
+ * Leave an organization (remove yourself).
623
+ */
624
+ async leaveOrg(orgId) {
625
+ await this.request(`/orgs/${orgId}/leave`, { method: "POST" });
626
+ if (this._activeOrgId === orgId) {
627
+ this._activeOrgId = null;
628
+ this._activeOrg = null;
629
+ this._activeRole = null;
630
+ this.notifyListeners();
631
+ }
632
+ }
633
+ // --- Invitations ---
634
+ /**
635
+ * Invite a user to an organization by email.
636
+ */
637
+ async inviteMember(orgId, params) {
638
+ return this.request(`/orgs/${orgId}/invitations`, {
639
+ method: "POST",
640
+ body: params
641
+ });
642
+ }
643
+ /**
644
+ * Accept an invitation by token.
645
+ */
646
+ async acceptInvitation(token) {
647
+ return this.request("/invitations/accept", {
648
+ method: "POST",
649
+ body: { token }
650
+ });
651
+ }
652
+ /**
653
+ * List pending invitations for an organization.
654
+ */
655
+ async listInvitations(orgId) {
656
+ return this.request(`/orgs/${orgId}/invitations`, { method: "GET" });
657
+ }
658
+ /**
659
+ * Revoke a pending invitation.
660
+ */
661
+ async revokeInvitation(orgId, invitationId) {
662
+ await this.request(`/orgs/${orgId}/invitations/${invitationId}`, { method: "DELETE" });
663
+ }
664
+ /**
665
+ * List pending invitations for the current user's email.
666
+ */
667
+ async listMyInvitations(email) {
668
+ return this.request(
669
+ `/invitations?email=${encodeURIComponent(email)}`,
670
+ { method: "GET" }
671
+ );
672
+ }
673
+ // --- Subscriptions ---
674
+ /**
675
+ * Subscribe to active org changes.
676
+ * @returns Unsubscribe function
677
+ */
678
+ onOrgChange(callback) {
679
+ this.listeners.add(callback);
680
+ return () => {
681
+ this.listeners.delete(callback);
682
+ };
683
+ }
684
+ // --- Internal ---
685
+ notifyListeners() {
686
+ for (const listener of this.listeners) {
687
+ try {
688
+ listener(this._activeOrgId);
689
+ } catch {
690
+ }
691
+ }
692
+ }
693
+ async request(path, options) {
694
+ const token = await this.getAccessToken();
695
+ if (!token) {
696
+ throw new OrgClientError(
697
+ "Not authenticated. Sign in before performing organization operations.",
698
+ "ORG_NOT_AUTHENTICATED"
699
+ );
700
+ }
701
+ const url = `${this.serverUrl}${path}`;
702
+ const headers = {
703
+ Authorization: `Bearer ${token}`
704
+ };
705
+ if (options.body) {
706
+ headers["Content-Type"] = "application/json";
707
+ }
708
+ let response;
709
+ try {
710
+ response = await fetch(url, {
711
+ method: options.method,
712
+ headers,
713
+ body: options.body ? JSON.stringify(options.body) : void 0
714
+ });
715
+ } catch (cause) {
716
+ throw new OrgClientError(
717
+ `Network request to ${path} failed.`,
718
+ "ORG_NETWORK_ERROR",
719
+ { path, cause: cause instanceof Error ? cause.message : String(cause) }
720
+ );
721
+ }
722
+ if (!response.ok) {
723
+ let errorMessage = `Server returned HTTP ${response.status}`;
724
+ try {
725
+ const body = await response.json();
726
+ if (typeof body["error"] === "string") {
727
+ errorMessage = body["error"];
728
+ }
729
+ } catch {
730
+ }
731
+ throw new OrgClientError(errorMessage, "ORG_SERVER_ERROR", {
732
+ path,
733
+ status: response.status
734
+ });
735
+ }
736
+ const text = await response.text();
737
+ if (text.length === 0) return void 0;
738
+ try {
739
+ const body = JSON.parse(text);
740
+ if (body && typeof body === "object" && "data" in body) {
741
+ return body.data;
742
+ }
743
+ return body;
744
+ } catch {
745
+ return void 0;
746
+ }
747
+ }
748
+ };
749
+
750
+ // src/device/device-identity.ts
751
+ var import_core3 = require("@korajs/core");
752
+ var CryptoUnavailableError = class extends import_core3.KoraError {
445
753
  constructor() {
446
754
  super(
447
755
  "Web Crypto API (crypto.subtle) is not available in this environment. Device identity requires crypto.subtle, which is available in modern browsers and Node.js 20+. If running in SSR, ensure your runtime provides the Web Crypto API.",
@@ -450,7 +758,7 @@ var CryptoUnavailableError = class extends import_core2.KoraError {
450
758
  this.name = "CryptoUnavailableError";
451
759
  }
452
760
  };
453
- var DeviceIdentityError = class extends import_core2.KoraError {
761
+ var DeviceIdentityError = class extends import_core3.KoraError {
454
762
  constructor(message, context) {
455
763
  super(message, "DEVICE_IDENTITY_ERROR", context);
456
764
  this.name = "DeviceIdentityError";
@@ -601,6 +909,229 @@ async function computePublicKeyThumbprint(publicKeyJwk) {
601
909
  }
602
910
  }
603
911
 
912
+ // src/device/device-store.ts
913
+ var import_core4 = require("@korajs/core");
914
+ var DeviceKeyStoreError = class extends import_core4.KoraError {
915
+ constructor(message, context) {
916
+ super(message, "DEVICE_KEY_STORE_ERROR", context);
917
+ this.name = "DeviceKeyStoreError";
918
+ }
919
+ };
920
+ var IDB_DATABASE_NAME = "kora_device_keys";
921
+ var IDB_STORE_NAME = "keypairs";
922
+ var IDB_VERSION = 1;
923
+ var IndexedDBDeviceKeyStore = class {
924
+ dbPromise = null;
925
+ /**
926
+ * Opens (or creates) the IndexedDB database.
927
+ *
928
+ * The database connection is lazily initialized on first use and
929
+ * reused for subsequent operations. If the database does not exist,
930
+ * it is created with the `keypairs` object store.
931
+ */
932
+ openDatabase() {
933
+ if (this.dbPromise !== null) {
934
+ return this.dbPromise;
935
+ }
936
+ this.dbPromise = new Promise((resolve, reject) => {
937
+ let request;
938
+ try {
939
+ request = globalThis.indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION);
940
+ } catch (cause) {
941
+ this.dbPromise = null;
942
+ reject(
943
+ new DeviceKeyStoreError(
944
+ "Failed to open IndexedDB database for device key storage. IndexedDB may be unavailable or access may be denied.",
945
+ { cause: cause instanceof Error ? cause.message : String(cause) }
946
+ )
947
+ );
948
+ return;
949
+ }
950
+ request.onupgradeneeded = () => {
951
+ const db = request.result;
952
+ if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
953
+ db.createObjectStore(IDB_STORE_NAME);
954
+ }
955
+ };
956
+ request.onsuccess = () => {
957
+ resolve(request.result);
958
+ };
959
+ request.onerror = () => {
960
+ this.dbPromise = null;
961
+ reject(
962
+ new DeviceKeyStoreError(
963
+ "Failed to open IndexedDB database for device key storage.",
964
+ { error: request.error?.message }
965
+ )
966
+ );
967
+ };
968
+ request.onblocked = () => {
969
+ this.dbPromise = null;
970
+ reject(
971
+ new DeviceKeyStoreError(
972
+ "IndexedDB database open was blocked. Another tab may have an older version of the database open. Close other tabs and try again."
973
+ )
974
+ );
975
+ };
976
+ });
977
+ return this.dbPromise;
978
+ }
979
+ /** @inheritdoc */
980
+ async saveKeyPair(deviceId, keyPair) {
981
+ const db = await this.openDatabase();
982
+ return new Promise((resolve, reject) => {
983
+ try {
984
+ const tx = db.transaction(IDB_STORE_NAME, "readwrite");
985
+ const store = tx.objectStore(IDB_STORE_NAME);
986
+ store.put(keyPair, deviceId);
987
+ tx.oncomplete = () => {
988
+ resolve();
989
+ };
990
+ tx.onerror = () => {
991
+ reject(
992
+ new DeviceKeyStoreError(
993
+ `Failed to save key pair for device "${deviceId}".`,
994
+ { deviceId, error: tx.error?.message }
995
+ )
996
+ );
997
+ };
998
+ } catch (cause) {
999
+ reject(
1000
+ new DeviceKeyStoreError(
1001
+ `Failed to save key pair for device "${deviceId}".`,
1002
+ {
1003
+ deviceId,
1004
+ cause: cause instanceof Error ? cause.message : String(cause)
1005
+ }
1006
+ )
1007
+ );
1008
+ }
1009
+ });
1010
+ }
1011
+ /** @inheritdoc */
1012
+ async loadKeyPair(deviceId) {
1013
+ const db = await this.openDatabase();
1014
+ return new Promise((resolve, reject) => {
1015
+ try {
1016
+ const tx = db.transaction(IDB_STORE_NAME, "readonly");
1017
+ const store = tx.objectStore(IDB_STORE_NAME);
1018
+ const request = store.get(deviceId);
1019
+ request.onsuccess = () => {
1020
+ const result = request.result;
1021
+ resolve(result ?? null);
1022
+ };
1023
+ request.onerror = () => {
1024
+ reject(
1025
+ new DeviceKeyStoreError(
1026
+ `Failed to load key pair for device "${deviceId}".`,
1027
+ { deviceId, error: request.error?.message }
1028
+ )
1029
+ );
1030
+ };
1031
+ } catch (cause) {
1032
+ reject(
1033
+ new DeviceKeyStoreError(
1034
+ `Failed to load key pair for device "${deviceId}".`,
1035
+ {
1036
+ deviceId,
1037
+ cause: cause instanceof Error ? cause.message : String(cause)
1038
+ }
1039
+ )
1040
+ );
1041
+ }
1042
+ });
1043
+ }
1044
+ /** @inheritdoc */
1045
+ async deleteKeyPair(deviceId) {
1046
+ const db = await this.openDatabase();
1047
+ return new Promise((resolve, reject) => {
1048
+ try {
1049
+ const tx = db.transaction(IDB_STORE_NAME, "readwrite");
1050
+ const store = tx.objectStore(IDB_STORE_NAME);
1051
+ store.delete(deviceId);
1052
+ tx.oncomplete = () => {
1053
+ resolve();
1054
+ };
1055
+ tx.onerror = () => {
1056
+ reject(
1057
+ new DeviceKeyStoreError(
1058
+ `Failed to delete key pair for device "${deviceId}".`,
1059
+ { deviceId, error: tx.error?.message }
1060
+ )
1061
+ );
1062
+ };
1063
+ } catch (cause) {
1064
+ reject(
1065
+ new DeviceKeyStoreError(
1066
+ `Failed to delete key pair for device "${deviceId}".`,
1067
+ {
1068
+ deviceId,
1069
+ cause: cause instanceof Error ? cause.message : String(cause)
1070
+ }
1071
+ )
1072
+ );
1073
+ }
1074
+ });
1075
+ }
1076
+ /** @inheritdoc */
1077
+ async hasKeyPair(deviceId) {
1078
+ const db = await this.openDatabase();
1079
+ return new Promise((resolve, reject) => {
1080
+ try {
1081
+ const tx = db.transaction(IDB_STORE_NAME, "readonly");
1082
+ const store = tx.objectStore(IDB_STORE_NAME);
1083
+ const request = store.count(deviceId);
1084
+ request.onsuccess = () => {
1085
+ resolve(request.result > 0);
1086
+ };
1087
+ request.onerror = () => {
1088
+ reject(
1089
+ new DeviceKeyStoreError(
1090
+ `Failed to check if key pair exists for device "${deviceId}".`,
1091
+ { deviceId, error: request.error?.message }
1092
+ )
1093
+ );
1094
+ };
1095
+ } catch (cause) {
1096
+ reject(
1097
+ new DeviceKeyStoreError(
1098
+ `Failed to check if key pair exists for device "${deviceId}".`,
1099
+ {
1100
+ deviceId,
1101
+ cause: cause instanceof Error ? cause.message : String(cause)
1102
+ }
1103
+ )
1104
+ );
1105
+ }
1106
+ });
1107
+ }
1108
+ };
1109
+ var InMemoryDeviceKeyStore = class {
1110
+ store = /* @__PURE__ */ new Map();
1111
+ /** @inheritdoc */
1112
+ async saveKeyPair(deviceId, keyPair) {
1113
+ this.store.set(deviceId, keyPair);
1114
+ }
1115
+ /** @inheritdoc */
1116
+ async loadKeyPair(deviceId) {
1117
+ return this.store.get(deviceId) ?? null;
1118
+ }
1119
+ /** @inheritdoc */
1120
+ async deleteKeyPair(deviceId) {
1121
+ this.store.delete(deviceId);
1122
+ }
1123
+ /** @inheritdoc */
1124
+ async hasKeyPair(deviceId) {
1125
+ return this.store.has(deviceId);
1126
+ }
1127
+ };
1128
+ function createDeviceKeyStore() {
1129
+ if (typeof globalThis.indexedDB !== "undefined") {
1130
+ return new IndexedDBDeviceKeyStore();
1131
+ }
1132
+ return new InMemoryDeviceKeyStore();
1133
+ }
1134
+
604
1135
  // src/tokens/token-store.ts
605
1136
  var DEFAULT_STORAGE_KEY = "kora_auth_tokens";
606
1137
  var MemoryStorage = class {
@@ -722,17 +1253,1019 @@ var TokenStore = class {
722
1253
  return tokens?.refreshToken ?? null;
723
1254
  }
724
1255
  };
1256
+
1257
+ // src/tokens/encrypted-token-store.ts
1258
+ var import_core6 = require("@korajs/core");
1259
+
1260
+ // src/encryption/database-encryption.ts
1261
+ var import_core5 = require("@korajs/core");
1262
+ var EncryptionError = class extends import_core5.KoraError {
1263
+ constructor(message, context) {
1264
+ super(message, "ENCRYPTION_ERROR", context);
1265
+ this.name = "EncryptionError";
1266
+ }
1267
+ };
1268
+ var CryptoUnavailableError2 = class extends import_core5.KoraError {
1269
+ constructor() {
1270
+ super(
1271
+ "Web Crypto API (crypto.subtle) is not available in this environment. Database encryption requires crypto.subtle, which is available in modern browsers and Node.js 20+. If running in SSR, ensure your runtime provides the Web Crypto API.",
1272
+ "CRYPTO_UNAVAILABLE"
1273
+ );
1274
+ this.name = "CryptoUnavailableError";
1275
+ }
1276
+ };
1277
+ var AES_GCM = "AES-GCM";
1278
+ var AES_KEY_LENGTH = 256;
1279
+ var IV_LENGTH = 12;
1280
+ function assertCryptoAvailable2() {
1281
+ if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
1282
+ throw new CryptoUnavailableError2();
1283
+ }
1284
+ }
1285
+ async function generateEncryptionKey() {
1286
+ assertCryptoAvailable2();
1287
+ try {
1288
+ const key = await globalThis.crypto.subtle.generateKey(
1289
+ { name: AES_GCM, length: AES_KEY_LENGTH },
1290
+ // extractable: true so the key can be exported and persisted
1291
+ true,
1292
+ ["encrypt", "decrypt"]
1293
+ );
1294
+ return key;
1295
+ } catch (cause) {
1296
+ throw new EncryptionError(
1297
+ "Failed to generate AES-256-GCM encryption key. Ensure the runtime supports the AES-GCM algorithm.",
1298
+ { cause: cause instanceof Error ? cause.message : String(cause) }
1299
+ );
1300
+ }
1301
+ }
1302
+ async function encryptData(key, plaintext) {
1303
+ assertCryptoAvailable2();
1304
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_LENGTH));
1305
+ try {
1306
+ const ciphertextBuffer = await globalThis.crypto.subtle.encrypt(
1307
+ { name: AES_GCM, iv },
1308
+ key,
1309
+ plaintext
1310
+ );
1311
+ return {
1312
+ ciphertext: new Uint8Array(ciphertextBuffer),
1313
+ iv
1314
+ };
1315
+ } catch (cause) {
1316
+ throw new EncryptionError(
1317
+ 'Failed to encrypt data with AES-256-GCM. Ensure the key is a valid AES-GCM CryptoKey with "encrypt" usage.',
1318
+ { cause: cause instanceof Error ? cause.message : String(cause) }
1319
+ );
1320
+ }
1321
+ }
1322
+ async function decryptData(key, ciphertext, iv) {
1323
+ assertCryptoAvailable2();
1324
+ try {
1325
+ const plaintextBuffer = await globalThis.crypto.subtle.decrypt(
1326
+ { name: AES_GCM, iv },
1327
+ key,
1328
+ ciphertext
1329
+ );
1330
+ return new Uint8Array(plaintextBuffer);
1331
+ } catch (cause) {
1332
+ throw new EncryptionError(
1333
+ "Failed to decrypt data with AES-256-GCM. This may indicate a wrong key, tampered ciphertext, or incorrect IV.",
1334
+ { cause: cause instanceof Error ? cause.message : String(cause) }
1335
+ );
1336
+ }
1337
+ }
1338
+ async function exportKey(key) {
1339
+ assertCryptoAvailable2();
1340
+ try {
1341
+ const rawBuffer = await globalThis.crypto.subtle.exportKey("raw", key);
1342
+ return new Uint8Array(rawBuffer);
1343
+ } catch (cause) {
1344
+ throw new EncryptionError(
1345
+ "Failed to export AES-256-GCM key. The key may not be extractable. Only keys generated with extractable=true can be exported.",
1346
+ { cause: cause instanceof Error ? cause.message : String(cause) }
1347
+ );
1348
+ }
1349
+ }
1350
+ async function importKey(rawKey) {
1351
+ assertCryptoAvailable2();
1352
+ if (rawKey.length !== 32) {
1353
+ throw new EncryptionError(
1354
+ `Invalid key length: expected 32 bytes (256 bits) for AES-256, but received ${rawKey.length} bytes.`,
1355
+ { actualLength: rawKey.length, expectedLength: 32 }
1356
+ );
1357
+ }
1358
+ try {
1359
+ const key = await globalThis.crypto.subtle.importKey(
1360
+ "raw",
1361
+ rawKey,
1362
+ { name: AES_GCM, length: AES_KEY_LENGTH },
1363
+ true,
1364
+ ["encrypt", "decrypt"]
1365
+ );
1366
+ return key;
1367
+ } catch (cause) {
1368
+ throw new EncryptionError(
1369
+ "Failed to import raw key bytes as AES-256-GCM key. Ensure the key material is valid.",
1370
+ { cause: cause instanceof Error ? cause.message : String(cause) }
1371
+ );
1372
+ }
1373
+ }
1374
+
1375
+ // src/tokens/encrypted-token-store.ts
1376
+ var EncryptedTokenStoreError = class extends import_core6.KoraError {
1377
+ constructor(message, context) {
1378
+ super(message, "ENCRYPTED_TOKEN_STORE_ERROR", context);
1379
+ this.name = "EncryptedTokenStoreError";
1380
+ }
1381
+ };
1382
+ function toBase64Url2(bytes) {
1383
+ let binary = "";
1384
+ for (let i = 0; i < bytes.length; i++) {
1385
+ binary += String.fromCharCode(bytes[i]);
1386
+ }
1387
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1388
+ }
1389
+ function fromBase64Url2(str) {
1390
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
1391
+ const paddingNeeded = (4 - base64.length % 4) % 4;
1392
+ base64 += "=".repeat(paddingNeeded);
1393
+ const binary = atob(base64);
1394
+ const bytes = new Uint8Array(binary.length);
1395
+ for (let i = 0; i < binary.length; i++) {
1396
+ bytes[i] = binary.charCodeAt(i);
1397
+ }
1398
+ return bytes;
1399
+ }
1400
+ var MemoryStorage2 = class {
1401
+ store = /* @__PURE__ */ new Map();
1402
+ getItem(key) {
1403
+ return this.store.get(key) ?? null;
1404
+ }
1405
+ setItem(key, value) {
1406
+ this.store.set(key, value);
1407
+ }
1408
+ removeItem(key) {
1409
+ this.store.delete(key);
1410
+ }
1411
+ };
1412
+ function tryGetLocalStorage2() {
1413
+ try {
1414
+ if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
1415
+ const storage = globalThis.localStorage;
1416
+ const testKey = "__kora_encrypted_storage_test__";
1417
+ storage.setItem(testKey, "1");
1418
+ storage.removeItem(testKey);
1419
+ return storage;
1420
+ }
1421
+ } catch {
1422
+ }
1423
+ return null;
1424
+ }
1425
+ var DEFAULT_STORAGE_KEY2 = "kora_auth_encrypted";
1426
+ var EncryptedTokenStore = class {
1427
+ storageKey;
1428
+ key;
1429
+ storage;
1430
+ /**
1431
+ * Creates a new EncryptedTokenStore instance.
1432
+ *
1433
+ * @param config - Configuration including the encryption key and optional storage key
1434
+ */
1435
+ constructor(config) {
1436
+ this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY2;
1437
+ this.key = config.key;
1438
+ this.storage = tryGetLocalStorage2() ?? new MemoryStorage2();
1439
+ }
1440
+ /**
1441
+ * Encrypt and save tokens to persistent storage.
1442
+ *
1443
+ * Serializes the tokens as JSON, encrypts with AES-256-GCM using a fresh
1444
+ * random IV, then stores the result as a JSON object containing the
1445
+ * base64url-encoded IV and ciphertext.
1446
+ *
1447
+ * Overwrites any previously stored tokens.
1448
+ *
1449
+ * @param tokens - The token set to encrypt and store
1450
+ * @throws {EncryptedTokenStoreError} If encryption fails
1451
+ */
1452
+ async saveTokens(tokens) {
1453
+ const serialized = {
1454
+ accessToken: tokens.accessToken,
1455
+ refreshToken: tokens.refreshToken
1456
+ };
1457
+ if (tokens.deviceCredential !== void 0) {
1458
+ serialized.deviceCredential = tokens.deviceCredential;
1459
+ }
1460
+ const plaintext = new TextEncoder().encode(JSON.stringify(serialized));
1461
+ try {
1462
+ const { ciphertext, iv } = await encryptData(this.key, plaintext);
1463
+ const payload = {
1464
+ iv: toBase64Url2(iv),
1465
+ data: toBase64Url2(ciphertext)
1466
+ };
1467
+ this.storage.setItem(this.storageKey, JSON.stringify(payload));
1468
+ } catch (cause) {
1469
+ if (cause instanceof EncryptedTokenStoreError) {
1470
+ throw cause;
1471
+ }
1472
+ throw new EncryptedTokenStoreError(
1473
+ "Failed to encrypt and save auth tokens. Ensure the encryption key is a valid AES-256-GCM CryptoKey.",
1474
+ { cause: cause instanceof Error ? cause.message : String(cause) }
1475
+ );
1476
+ }
1477
+ }
1478
+ /**
1479
+ * Load and decrypt tokens from storage.
1480
+ *
1481
+ * Reads the encrypted payload from localStorage, decodes the base64url IV
1482
+ * and ciphertext, decrypts with AES-256-GCM, and parses the resulting JSON.
1483
+ *
1484
+ * Returns null (without throwing) if:
1485
+ * - No tokens have been saved
1486
+ * - The stored data is corrupted or not valid JSON
1487
+ * - Decryption fails (wrong key, tampered ciphertext, or wrong IV)
1488
+ * - The decrypted data does not contain valid token fields
1489
+ *
1490
+ * This fail-silent design prevents decryption errors from crashing the
1491
+ * application. The caller should treat null as "no valid tokens available"
1492
+ * and initiate a re-authentication flow.
1493
+ *
1494
+ * @returns The decrypted {@link AuthTokens}, or null if unavailable or decryption fails
1495
+ */
1496
+ async loadTokens() {
1497
+ const raw = this.storage.getItem(this.storageKey);
1498
+ if (raw === null) {
1499
+ return null;
1500
+ }
1501
+ try {
1502
+ const parsed = JSON.parse(raw);
1503
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1504
+ return null;
1505
+ }
1506
+ const record = parsed;
1507
+ if (typeof record["iv"] !== "string" || typeof record["data"] !== "string") {
1508
+ return null;
1509
+ }
1510
+ const iv = fromBase64Url2(record["iv"]);
1511
+ const ciphertext = fromBase64Url2(record["data"]);
1512
+ const plaintextBytes = await decryptData(this.key, ciphertext, iv);
1513
+ const json = new TextDecoder().decode(plaintextBytes);
1514
+ const tokenData = JSON.parse(json);
1515
+ if (typeof tokenData !== "object" || tokenData === null || Array.isArray(tokenData)) {
1516
+ return null;
1517
+ }
1518
+ const tokenRecord = tokenData;
1519
+ if (typeof tokenRecord["accessToken"] !== "string" || typeof tokenRecord["refreshToken"] !== "string") {
1520
+ return null;
1521
+ }
1522
+ const tokens = {
1523
+ accessToken: tokenRecord["accessToken"],
1524
+ refreshToken: tokenRecord["refreshToken"]
1525
+ };
1526
+ if (typeof tokenRecord["deviceCredential"] === "string") {
1527
+ tokens.deviceCredential = tokenRecord["deviceCredential"];
1528
+ }
1529
+ return tokens;
1530
+ } catch {
1531
+ return null;
1532
+ }
1533
+ }
1534
+ /**
1535
+ * Clear all stored encrypted tokens.
1536
+ *
1537
+ * Removes the encrypted payload from storage. This is a synchronous
1538
+ * operation since it only removes the localStorage entry.
1539
+ *
1540
+ * Call this on logout to ensure no encrypted credential material
1541
+ * remains in persistent storage.
1542
+ */
1543
+ clearTokens() {
1544
+ this.storage.removeItem(this.storageKey);
1545
+ }
1546
+ /**
1547
+ * Get the current access token by decrypting stored tokens.
1548
+ *
1549
+ * Returns the raw token string without validating expiration.
1550
+ * The caller is responsible for checking whether the token is
1551
+ * still valid and initiating a refresh if needed.
1552
+ *
1553
+ * @returns The decrypted access token string, or null if no valid tokens are stored
1554
+ */
1555
+ async getAccessToken() {
1556
+ const tokens = await this.loadTokens();
1557
+ return tokens?.accessToken ?? null;
1558
+ }
1559
+ /**
1560
+ * Get the current refresh token by decrypting stored tokens.
1561
+ *
1562
+ * @returns The decrypted refresh token string, or null if no valid tokens are stored
1563
+ */
1564
+ async getRefreshToken() {
1565
+ const tokens = await this.loadTokens();
1566
+ return tokens?.refreshToken ?? null;
1567
+ }
1568
+ };
1569
+
1570
+ // src/passkey/passkey-client.ts
1571
+ var import_core7 = require("@korajs/core");
1572
+ var PasskeyError = class extends import_core7.KoraError {
1573
+ constructor(message, context) {
1574
+ super(message, "PASSKEY_ERROR", context);
1575
+ this.name = "PasskeyError";
1576
+ }
1577
+ };
1578
+ var PasskeyUnsupportedError = class extends import_core7.KoraError {
1579
+ constructor() {
1580
+ super(
1581
+ "WebAuthn is not supported in this browser. Passkey authentication requires a modern browser with WebAuthn support.",
1582
+ "PASSKEY_UNSUPPORTED"
1583
+ );
1584
+ this.name = "PasskeyUnsupportedError";
1585
+ }
1586
+ };
1587
+ function isPasskeySupported() {
1588
+ return typeof globalThis.navigator !== "undefined" && typeof globalThis.navigator.credentials !== "undefined" && typeof globalThis.navigator.credentials.create === "function" && typeof globalThis.navigator.credentials.get === "function";
1589
+ }
1590
+ async function isPlatformAuthenticatorAvailable() {
1591
+ if (!isPasskeySupported()) {
1592
+ return false;
1593
+ }
1594
+ if (typeof PublicKeyCredential !== "undefined" && typeof PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === "function") {
1595
+ try {
1596
+ return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
1597
+ } catch {
1598
+ return false;
1599
+ }
1600
+ }
1601
+ return false;
1602
+ }
1603
+ async function createPasskeyCredential(options) {
1604
+ if (!isPasskeySupported()) {
1605
+ throw new PasskeyUnsupportedError();
1606
+ }
1607
+ const excludeCredentials = (options.excludeCredentialIds ?? []).map((id) => ({
1608
+ type: "public-key",
1609
+ id: fromBase64Url(id).buffer
1610
+ }));
1611
+ const authenticatorSelection = {
1612
+ authenticatorAttachment: options.authenticatorSelection?.authenticatorAttachment ?? "platform",
1613
+ residentKey: options.authenticatorSelection?.residentKey ?? "preferred",
1614
+ userVerification: options.authenticatorSelection?.userVerification ?? "required"
1615
+ };
1616
+ if (authenticatorSelection.residentKey === "required") {
1617
+ authenticatorSelection.requireResidentKey = true;
1618
+ }
1619
+ const publicKeyOptions = {
1620
+ challenge: fromBase64Url(options.challenge).buffer,
1621
+ rp: {
1622
+ id: options.rpId,
1623
+ name: options.rpName
1624
+ },
1625
+ user: {
1626
+ id: fromBase64Url(options.userId).buffer,
1627
+ name: options.userName,
1628
+ displayName: options.userDisplayName
1629
+ },
1630
+ pubKeyCredParams: [
1631
+ // ES256 (ECDSA w/ SHA-256) — the most widely supported algorithm
1632
+ { type: "public-key", alg: -7 },
1633
+ // RS256 (RSASSA-PKCS1-v1_5 w/ SHA-256) — fallback for older authenticators
1634
+ { type: "public-key", alg: -257 }
1635
+ ],
1636
+ excludeCredentials,
1637
+ authenticatorSelection,
1638
+ timeout: 6e4,
1639
+ attestation: "none"
1640
+ };
1641
+ let credential;
1642
+ try {
1643
+ const result = await navigator.credentials.create({
1644
+ publicKey: publicKeyOptions
1645
+ });
1646
+ if (result === null) {
1647
+ throw new PasskeyError(
1648
+ "Credential creation returned null. The user may have cancelled the operation.",
1649
+ { rpId: options.rpId }
1650
+ );
1651
+ }
1652
+ credential = result;
1653
+ } catch (error) {
1654
+ if (error instanceof PasskeyError) {
1655
+ throw error;
1656
+ }
1657
+ const domError = error;
1658
+ if (domError.name === "NotAllowedError") {
1659
+ throw new PasskeyError(
1660
+ "Passkey creation was cancelled or not allowed. The user may have dismissed the prompt or the operation timed out.",
1661
+ { rpId: options.rpId, errorName: domError.name }
1662
+ );
1663
+ }
1664
+ if (domError.name === "InvalidStateError") {
1665
+ throw new PasskeyError(
1666
+ "A passkey already exists for this user on this authenticator. Use the existing passkey to sign in.",
1667
+ { rpId: options.rpId, errorName: domError.name }
1668
+ );
1669
+ }
1670
+ throw new PasskeyError(
1671
+ `Passkey creation failed: ${error instanceof Error ? error.message : String(error)}`,
1672
+ {
1673
+ rpId: options.rpId,
1674
+ errorName: error instanceof Error ? error.name : void 0
1675
+ }
1676
+ );
1677
+ }
1678
+ const response = credential.response;
1679
+ const attestationObject = new Uint8Array(response.attestationObject);
1680
+ const clientDataJSON = new Uint8Array(response.clientDataJSON);
1681
+ const publicKeyBytes = extractPublicKeyFromAttestationObject(attestationObject);
1682
+ return {
1683
+ credentialId: toBase64Url(credential.rawId),
1684
+ publicKey: toBase64Url(publicKeyBytes.buffer),
1685
+ clientDataJSON: toBase64Url(clientDataJSON.buffer),
1686
+ attestationObject: toBase64Url(attestationObject.buffer)
1687
+ };
1688
+ }
1689
+ async function authenticateWithPasskey(options) {
1690
+ if (!isPasskeySupported()) {
1691
+ throw new PasskeyUnsupportedError();
1692
+ }
1693
+ const allowCredentials = options.allowCredentialIds?.map((id) => ({
1694
+ type: "public-key",
1695
+ id: fromBase64Url(id).buffer
1696
+ }));
1697
+ const publicKeyOptions = {
1698
+ challenge: fromBase64Url(options.challenge).buffer,
1699
+ rpId: options.rpId,
1700
+ allowCredentials,
1701
+ userVerification: options.userVerification ?? "preferred",
1702
+ timeout: options.timeout ?? 6e4
1703
+ };
1704
+ let credential;
1705
+ try {
1706
+ const result = await navigator.credentials.get({
1707
+ publicKey: publicKeyOptions
1708
+ });
1709
+ if (result === null) {
1710
+ throw new PasskeyError(
1711
+ "Authentication returned null. The user may have cancelled the operation.",
1712
+ { rpId: options.rpId }
1713
+ );
1714
+ }
1715
+ credential = result;
1716
+ } catch (error) {
1717
+ if (error instanceof PasskeyError) {
1718
+ throw error;
1719
+ }
1720
+ const domError = error;
1721
+ if (domError.name === "NotAllowedError") {
1722
+ throw new PasskeyError(
1723
+ "Passkey authentication was cancelled or not allowed. The user may have dismissed the prompt or the operation timed out.",
1724
+ { rpId: options.rpId, errorName: domError.name }
1725
+ );
1726
+ }
1727
+ throw new PasskeyError(
1728
+ `Passkey authentication failed: ${error instanceof Error ? error.message : String(error)}`,
1729
+ {
1730
+ rpId: options.rpId,
1731
+ errorName: error instanceof Error ? error.name : void 0
1732
+ }
1733
+ );
1734
+ }
1735
+ const response = credential.response;
1736
+ const authenticatorData = new Uint8Array(response.authenticatorData);
1737
+ const clientDataJSON = new Uint8Array(response.clientDataJSON);
1738
+ const signature = new Uint8Array(response.signature);
1739
+ const userHandle = response.userHandle !== null && response.userHandle.byteLength > 0 ? toBase64Url(response.userHandle) : null;
1740
+ return {
1741
+ credentialId: toBase64Url(credential.rawId),
1742
+ authenticatorData: toBase64Url(authenticatorData.buffer),
1743
+ clientDataJSON: toBase64Url(clientDataJSON.buffer),
1744
+ signature: toBase64Url(signature.buffer),
1745
+ userHandle
1746
+ };
1747
+ }
1748
+ function extractPublicKeyFromAttestationObject(attestationObject) {
1749
+ const decoded = decodeCbor(attestationObject, 0);
1750
+ const topMap = decoded.value;
1751
+ const authData = topMap.get("authData");
1752
+ if (!(authData instanceof Uint8Array)) {
1753
+ throw new PasskeyError(
1754
+ "Invalid attestation object: authData is missing or not a byte string."
1755
+ );
1756
+ }
1757
+ let offset = 0;
1758
+ offset += 32;
1759
+ const flags = authData[offset];
1760
+ offset += 1;
1761
+ offset += 4;
1762
+ const hasAttestedCredentialData = (flags & 64) !== 0;
1763
+ if (!hasAttestedCredentialData) {
1764
+ throw new PasskeyError(
1765
+ "Attestation object does not contain attested credential data. The authenticator did not include a public key."
1766
+ );
1767
+ }
1768
+ offset += 16;
1769
+ const credentialIdLength = authData[offset] << 8 | authData[offset + 1];
1770
+ offset += 2;
1771
+ offset += credentialIdLength;
1772
+ const coseKeyResult = decodeCbor(authData, offset);
1773
+ const coseKeyLength = coseKeyResult.offset - offset;
1774
+ return authData.slice(offset, offset + coseKeyLength);
1775
+ }
1776
+ function decodeCbor(data, offset) {
1777
+ if (offset >= data.length) {
1778
+ throw new PasskeyError("CBOR decode error: unexpected end of data.");
1779
+ }
1780
+ const initialByte = data[offset];
1781
+ const majorType = initialByte >> 5;
1782
+ const additionalInfo = initialByte & 31;
1783
+ offset += 1;
1784
+ let argument;
1785
+ if (additionalInfo < 24) {
1786
+ argument = additionalInfo;
1787
+ } else if (additionalInfo === 24) {
1788
+ argument = data[offset];
1789
+ offset += 1;
1790
+ } else if (additionalInfo === 25) {
1791
+ argument = data[offset] << 8 | data[offset + 1];
1792
+ offset += 2;
1793
+ } else if (additionalInfo === 26) {
1794
+ argument = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
1795
+ argument = argument >>> 0;
1796
+ offset += 4;
1797
+ } else {
1798
+ throw new PasskeyError(
1799
+ `CBOR decode error: unsupported additional info ${additionalInfo} at byte ${offset - 1}. This CBOR decoder only supports definite-length encodings.`
1800
+ );
1801
+ }
1802
+ switch (majorType) {
1803
+ // Major type 0: Unsigned integer
1804
+ case 0:
1805
+ return { value: argument, offset };
1806
+ // Major type 1: Negative integer (-1 - argument)
1807
+ case 1:
1808
+ return { value: -1 - argument, offset };
1809
+ // Major type 2: Byte string
1810
+ case 2: {
1811
+ const bytes = data.slice(offset, offset + argument);
1812
+ return { value: bytes, offset: offset + argument };
1813
+ }
1814
+ // Major type 3: Text string (UTF-8)
1815
+ case 3: {
1816
+ const textBytes = data.slice(offset, offset + argument);
1817
+ const text = new TextDecoder().decode(textBytes);
1818
+ return { value: text, offset: offset + argument };
1819
+ }
1820
+ // Major type 4: Array
1821
+ case 4: {
1822
+ const arr = [];
1823
+ let currentOffset = offset;
1824
+ for (let i = 0; i < argument; i++) {
1825
+ const item = decodeCbor(data, currentOffset);
1826
+ arr.push(item.value);
1827
+ currentOffset = item.offset;
1828
+ }
1829
+ return { value: arr, offset: currentOffset };
1830
+ }
1831
+ // Major type 5: Map
1832
+ case 5: {
1833
+ const map = /* @__PURE__ */ new Map();
1834
+ let currentOffset = offset;
1835
+ for (let i = 0; i < argument; i++) {
1836
+ const keyResult = decodeCbor(data, currentOffset);
1837
+ const valResult = decodeCbor(data, keyResult.offset);
1838
+ map.set(keyResult.value, valResult.value);
1839
+ currentOffset = valResult.offset;
1840
+ }
1841
+ return { value: map, offset: currentOffset };
1842
+ }
1843
+ default:
1844
+ throw new PasskeyError(
1845
+ `CBOR decode error: unsupported major type ${majorType} at byte ${offset - 1}. This CBOR decoder only supports types 0-5 (integers, byte/text strings, arrays, maps).`
1846
+ );
1847
+ }
1848
+ }
1849
+
1850
+ // src/encryption/key-derivation.ts
1851
+ var import_core8 = require("@korajs/core");
1852
+ var KeyDerivationError = class extends import_core8.KoraError {
1853
+ constructor(message, context) {
1854
+ super(message, "KEY_DERIVATION_ERROR", context);
1855
+ this.name = "KeyDerivationError";
1856
+ }
1857
+ };
1858
+ var SALT_LENGTH = 32;
1859
+ var PBKDF2_ITERATIONS = 6e5;
1860
+ var DERIVED_KEY_LENGTH = 256;
1861
+ function assertCryptoAvailable3() {
1862
+ if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
1863
+ throw new KeyDerivationError(
1864
+ "Web Crypto API (crypto.subtle) is not available in this environment. Key derivation requires crypto.subtle, which is available in modern browsers and Node.js 20+."
1865
+ );
1866
+ }
1867
+ }
1868
+ function generateSalt() {
1869
+ if (typeof globalThis.crypto === "undefined") {
1870
+ throw new KeyDerivationError(
1871
+ "Web Crypto API (crypto) is not available. Cannot generate random salt."
1872
+ );
1873
+ }
1874
+ return globalThis.crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
1875
+ }
1876
+ async function deriveEncryptionKey(passphrase, salt) {
1877
+ assertCryptoAvailable3();
1878
+ if (passphrase.length === 0) {
1879
+ throw new KeyDerivationError(
1880
+ "Passphrase must not be empty. Provide a non-empty string for key derivation."
1881
+ );
1882
+ }
1883
+ const usedSalt = salt ?? generateSalt();
1884
+ try {
1885
+ const passphraseBytes = new TextEncoder().encode(passphrase);
1886
+ const baseKey = await globalThis.crypto.subtle.importKey(
1887
+ "raw",
1888
+ passphraseBytes,
1889
+ "PBKDF2",
1890
+ false,
1891
+ ["deriveBits", "deriveKey"]
1892
+ );
1893
+ const derivedKey = await globalThis.crypto.subtle.deriveKey(
1894
+ {
1895
+ name: "PBKDF2",
1896
+ salt: usedSalt,
1897
+ iterations: PBKDF2_ITERATIONS,
1898
+ hash: "SHA-256"
1899
+ },
1900
+ baseKey,
1901
+ { name: "AES-GCM", length: DERIVED_KEY_LENGTH },
1902
+ // extractable: true so the derived key can be exported if needed
1903
+ true,
1904
+ ["encrypt", "decrypt"]
1905
+ );
1906
+ return { key: derivedKey, salt: usedSalt };
1907
+ } catch (cause) {
1908
+ if (cause instanceof KeyDerivationError) {
1909
+ throw cause;
1910
+ }
1911
+ throw new KeyDerivationError(
1912
+ "Failed to derive encryption key from passphrase using PBKDF2. Ensure the runtime supports PBKDF2 with SHA-256.",
1913
+ { cause: cause instanceof Error ? cause.message : String(cause) }
1914
+ );
1915
+ }
1916
+ }
1917
+
1918
+ // src/encryption/auto-lock.ts
1919
+ var AutoLockManager = class {
1920
+ _timeout;
1921
+ _onLock;
1922
+ _timerId = null;
1923
+ _isLocked = false;
1924
+ _isRunning = false;
1925
+ constructor(config) {
1926
+ if (config.timeout <= 0) {
1927
+ throw new Error(
1928
+ `AutoLockManager timeout must be a positive number, but received ${config.timeout}.`
1929
+ );
1930
+ }
1931
+ this._timeout = config.timeout;
1932
+ this._onLock = config.onLock;
1933
+ }
1934
+ /**
1935
+ * Whether the manager is currently in the locked state.
1936
+ *
1937
+ * Becomes `true` when the inactivity timeout fires or {@link lock} is called manually.
1938
+ * Returns to `false` only when {@link unlock} is called.
1939
+ */
1940
+ get isLocked() {
1941
+ return this._isLocked;
1942
+ }
1943
+ /**
1944
+ * Starts monitoring for inactivity.
1945
+ *
1946
+ * Begins the inactivity countdown. If the manager is already running, this is
1947
+ * a no-op to prevent creating multiple timers. Calling `start()` also resets
1948
+ * the locked state if the manager was previously locked.
1949
+ */
1950
+ start() {
1951
+ if (this._isRunning) {
1952
+ return;
1953
+ }
1954
+ this._isRunning = true;
1955
+ this._isLocked = false;
1956
+ this._startTimer();
1957
+ }
1958
+ /**
1959
+ * Stops monitoring for inactivity.
1960
+ *
1961
+ * Clears the pending inactivity timer. Does not change the lock state: if
1962
+ * the manager was locked, it remains locked. If unlocked, it remains unlocked.
1963
+ * To resume monitoring, call {@link start} again.
1964
+ */
1965
+ stop() {
1966
+ this._isRunning = false;
1967
+ this._clearTimer();
1968
+ }
1969
+ /**
1970
+ * Reports user activity, resetting the inactivity timer.
1971
+ *
1972
+ * Call this whenever the user interacts with the application (clicks, key presses,
1973
+ * touches, etc.). If the manager is not running or is already locked, this is a no-op.
1974
+ */
1975
+ reportActivity() {
1976
+ if (!this._isRunning || this._isLocked) {
1977
+ return;
1978
+ }
1979
+ this._clearTimer();
1980
+ this._startTimer();
1981
+ }
1982
+ /**
1983
+ * Manually locks the manager immediately.
1984
+ *
1985
+ * Clears the inactivity timer and transitions to the locked state. The `onLock`
1986
+ * callback is invoked. The manager remains running but locked: call {@link unlock}
1987
+ * to return to the unlocked state, which will restart the inactivity timer.
1988
+ */
1989
+ lock() {
1990
+ this._clearTimer();
1991
+ if (!this._isLocked) {
1992
+ this._isLocked = true;
1993
+ this._onLock();
1994
+ }
1995
+ }
1996
+ /**
1997
+ * Unlocks the manager, returning to the unlocked state.
1998
+ *
1999
+ * If the manager is running, the inactivity timer is restarted. If the manager
2000
+ * is not running (was stopped), it simply clears the locked state without
2001
+ * starting a timer.
2002
+ */
2003
+ unlock() {
2004
+ this._isLocked = false;
2005
+ if (this._isRunning) {
2006
+ this._clearTimer();
2007
+ this._startTimer();
2008
+ }
2009
+ }
2010
+ /**
2011
+ * Starts the inactivity timer. When it fires, the manager locks.
2012
+ */
2013
+ _startTimer() {
2014
+ this._timerId = setTimeout(() => {
2015
+ this._timerId = null;
2016
+ this._isLocked = true;
2017
+ this._onLock();
2018
+ }, this._timeout);
2019
+ }
2020
+ /**
2021
+ * Clears the pending inactivity timer, if any.
2022
+ */
2023
+ _clearTimer() {
2024
+ if (this._timerId !== null) {
2025
+ clearTimeout(this._timerId);
2026
+ this._timerId = null;
2027
+ }
2028
+ }
2029
+ };
2030
+
2031
+ // src/encryption/operation-encryptor.ts
2032
+ var import_core9 = require("@korajs/core");
2033
+ var ENCRYPTED_MARKER = "__kora_encrypted";
2034
+ var ENCRYPTION_VERSION = 1;
2035
+ var OperationEncryptionError = class extends import_core9.KoraError {
2036
+ constructor(message, context) {
2037
+ super(message, "OPERATION_ENCRYPTION_ERROR", context);
2038
+ this.name = "OperationEncryptionError";
2039
+ }
2040
+ };
2041
+ function toBase64Url3(bytes) {
2042
+ let binary = "";
2043
+ for (let i = 0; i < bytes.length; i++) {
2044
+ binary += String.fromCharCode(bytes[i]);
2045
+ }
2046
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2047
+ }
2048
+ function fromBase64Url3(str) {
2049
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
2050
+ const paddingNeeded = (4 - base64.length % 4) % 4;
2051
+ base64 += "=".repeat(paddingNeeded);
2052
+ const binary = atob(base64);
2053
+ const bytes = new Uint8Array(binary.length);
2054
+ for (let i = 0; i < binary.length; i++) {
2055
+ bytes[i] = binary.charCodeAt(i);
2056
+ }
2057
+ return bytes;
2058
+ }
2059
+ var OperationEncryptor = class {
2060
+ key;
2061
+ constructor(config) {
2062
+ this.key = config.key;
2063
+ }
2064
+ /**
2065
+ * Encrypt an operation's data fields.
2066
+ *
2067
+ * Returns a new Operation with `data` and `previousData` replaced by
2068
+ * encrypted envelopes. The original operation is not mutated.
2069
+ *
2070
+ * If `data` or `previousData` is null (e.g., delete operations),
2071
+ * that field remains null — there is nothing to encrypt.
2072
+ *
2073
+ * @param operation - The operation to encrypt
2074
+ * @returns A new operation with encrypted data fields
2075
+ * @throws {OperationEncryptionError} If encryption fails
2076
+ */
2077
+ async encryptOperation(operation) {
2078
+ const [encryptedData, encryptedPreviousData] = await Promise.all([
2079
+ this.encryptField(operation.data, operation.id, "data"),
2080
+ this.encryptField(operation.previousData, operation.id, "previousData")
2081
+ ]);
2082
+ return {
2083
+ ...operation,
2084
+ data: encryptedData,
2085
+ previousData: encryptedPreviousData
2086
+ };
2087
+ }
2088
+ /**
2089
+ * Decrypt an operation's data fields.
2090
+ *
2091
+ * Returns a new Operation with the original `data` and `previousData`
2092
+ * restored from their encrypted envelopes. The original operation is
2093
+ * not mutated.
2094
+ *
2095
+ * If a field is null or not encrypted (no marker), it passes through unchanged.
2096
+ * This enables mixed plaintext/encrypted operations during migration.
2097
+ *
2098
+ * @param operation - The operation to decrypt
2099
+ * @returns A new operation with decrypted data fields
2100
+ * @throws {OperationEncryptionError} If decryption fails (wrong key, tampered data)
2101
+ */
2102
+ async decryptOperation(operation) {
2103
+ const [decryptedData, decryptedPreviousData] = await Promise.all([
2104
+ this.decryptField(operation.data, operation.id, "data"),
2105
+ this.decryptField(operation.previousData, operation.id, "previousData")
2106
+ ]);
2107
+ return {
2108
+ ...operation,
2109
+ data: decryptedData,
2110
+ previousData: decryptedPreviousData
2111
+ };
2112
+ }
2113
+ /**
2114
+ * Check if an operation's data fields are encrypted.
2115
+ *
2116
+ * Returns true if either `data` or `previousData` contains an encrypted
2117
+ * envelope marker. Useful for determining whether decryption is needed
2118
+ * before applying an operation.
2119
+ *
2120
+ * @param operation - The operation to check
2121
+ * @returns true if any data field is encrypted
2122
+ */
2123
+ isEncrypted(operation) {
2124
+ return isEncryptedEnvelope(operation.data) || isEncryptedEnvelope(operation.previousData);
2125
+ }
2126
+ /**
2127
+ * Encrypt a batch of operations.
2128
+ *
2129
+ * Convenience method for encrypting multiple operations at once.
2130
+ * Operations are encrypted in parallel for performance.
2131
+ *
2132
+ * @param operations - The operations to encrypt
2133
+ * @returns New operations with encrypted data fields
2134
+ */
2135
+ async encryptBatch(operations) {
2136
+ return Promise.all(operations.map((op) => this.encryptOperation(op)));
2137
+ }
2138
+ /**
2139
+ * Decrypt a batch of operations.
2140
+ *
2141
+ * Convenience method for decrypting multiple operations at once.
2142
+ * Operations are decrypted in parallel for performance.
2143
+ *
2144
+ * @param operations - The operations to decrypt
2145
+ * @returns New operations with decrypted data fields
2146
+ */
2147
+ async decryptBatch(operations) {
2148
+ return Promise.all(operations.map((op) => this.decryptOperation(op)));
2149
+ }
2150
+ // --- Private helpers ---
2151
+ async encryptField(field, operationId, fieldName) {
2152
+ if (field === null) {
2153
+ return null;
2154
+ }
2155
+ const plaintext = new TextEncoder().encode(JSON.stringify(field));
2156
+ try {
2157
+ const { ciphertext, iv } = await encryptData(this.key, plaintext);
2158
+ const envelope = {
2159
+ [ENCRYPTED_MARKER]: true,
2160
+ ciphertext: toBase64Url3(ciphertext),
2161
+ iv: toBase64Url3(iv),
2162
+ algorithm: "AES-256-GCM",
2163
+ version: ENCRYPTION_VERSION
2164
+ };
2165
+ return envelope;
2166
+ } catch (cause) {
2167
+ if (cause instanceof OperationEncryptionError) {
2168
+ throw cause;
2169
+ }
2170
+ throw new OperationEncryptionError(
2171
+ `Failed to encrypt operation ${fieldName} field.`,
2172
+ {
2173
+ operationId,
2174
+ fieldName,
2175
+ cause: cause instanceof Error ? cause.message : String(cause)
2176
+ }
2177
+ );
2178
+ }
2179
+ }
2180
+ async decryptField(field, operationId, fieldName) {
2181
+ if (field === null) {
2182
+ return null;
2183
+ }
2184
+ if (!isEncryptedEnvelope(field)) {
2185
+ return field;
2186
+ }
2187
+ const envelope = field;
2188
+ if (envelope.version > ENCRYPTION_VERSION) {
2189
+ throw new OperationEncryptionError(
2190
+ `Encrypted field uses version ${envelope.version}, but this client only supports version ${ENCRYPTION_VERSION}. Update your @korajs/auth package to decrypt this operation.`,
2191
+ { operationId, fieldName, version: envelope.version }
2192
+ );
2193
+ }
2194
+ try {
2195
+ const ciphertext = fromBase64Url3(envelope.ciphertext);
2196
+ const iv = fromBase64Url3(envelope.iv);
2197
+ const plaintextBytes = await decryptData(this.key, ciphertext, iv);
2198
+ const json = new TextDecoder().decode(plaintextBytes);
2199
+ const parsed = JSON.parse(json);
2200
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2201
+ throw new OperationEncryptionError(
2202
+ `Decrypted ${fieldName} is not a valid record object.`,
2203
+ { operationId, fieldName }
2204
+ );
2205
+ }
2206
+ return parsed;
2207
+ } catch (cause) {
2208
+ if (cause instanceof OperationEncryptionError) {
2209
+ throw cause;
2210
+ }
2211
+ throw new OperationEncryptionError(
2212
+ `Failed to decrypt operation ${fieldName} field. This may indicate a wrong encryption key or tampered data.`,
2213
+ {
2214
+ operationId,
2215
+ fieldName,
2216
+ cause: cause instanceof Error ? cause.message : String(cause)
2217
+ }
2218
+ );
2219
+ }
2220
+ }
2221
+ };
2222
+ function isEncryptedField(field) {
2223
+ return isEncryptedEnvelope(field);
2224
+ }
2225
+ function isEncryptedEnvelope(field) {
2226
+ if (field === null || typeof field !== "object") {
2227
+ return false;
2228
+ }
2229
+ return field[ENCRYPTED_MARKER] === true && typeof field["ciphertext"] === "string" && typeof field["iv"] === "string" && field["algorithm"] === "AES-256-GCM";
2230
+ }
725
2231
  // Annotate the CommonJS export names for ESM import in node:
726
2232
  0 && (module.exports = {
727
2233
  AuthClient,
728
2234
  AuthError,
2235
+ AutoLockManager,
729
2236
  CryptoUnavailableError,
730
2237
  DeviceIdentityError,
2238
+ DeviceKeyStoreError,
2239
+ EncryptedTokenStore,
2240
+ EncryptedTokenStoreError,
2241
+ EncryptionError,
2242
+ InMemoryDeviceKeyStore,
2243
+ IndexedDBDeviceKeyStore,
2244
+ KeyDerivationError,
2245
+ OperationEncryptionError,
2246
+ OperationEncryptor,
2247
+ OrgClient,
2248
+ OrgClientError,
2249
+ PasskeyError,
2250
+ PasskeyUnsupportedError,
731
2251
  TokenStore,
2252
+ authenticateWithPasskey,
732
2253
  computePublicKeyThumbprint,
2254
+ createDeviceKeyStore,
2255
+ createPasskeyCredential,
2256
+ decryptData,
2257
+ deriveEncryptionKey,
2258
+ encryptData,
2259
+ exportKey,
733
2260
  exportPublicKeyJwk,
734
2261
  fromBase64Url,
735
2262
  generateDeviceKeyPair,
2263
+ generateEncryptionKey,
2264
+ generateSalt,
2265
+ importKey,
2266
+ isEncryptedField,
2267
+ isPasskeySupported,
2268
+ isPlatformAuthenticatorAvailable,
736
2269
  signChallenge,
737
2270
  toBase64Url,
738
2271
  verifyChallenge