@korajs/auth 0.1.0 → 0.3.1

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