@korajs/auth 0.4.0 → 0.5.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.js CHANGED
@@ -62,6 +62,36 @@ function getUserIdFromToken(token) {
62
62
  }
63
63
  return payload.sub;
64
64
  }
65
+ function getDefaultFetch() {
66
+ if (typeof globalThis.fetch !== "function") {
67
+ return async () => {
68
+ throw new AuthError(
69
+ "No fetch implementation is available in this runtime. Pass `fetch` to AuthClientConfig.",
70
+ "AUTH_FETCH_UNAVAILABLE"
71
+ );
72
+ };
73
+ }
74
+ return globalThis.fetch.bind(globalThis);
75
+ }
76
+ function normalizeAuthUser(user) {
77
+ return {
78
+ id: user.id,
79
+ email: user.email,
80
+ name: user.name ?? null
81
+ };
82
+ }
83
+ function canRedirectCurrentWindow() {
84
+ return typeof globalThis.window !== "undefined" && typeof globalThis.window.location?.assign === "function";
85
+ }
86
+ function redirectCurrentWindow(url) {
87
+ if (!canRedirectCurrentWindow()) {
88
+ throw new AuthError(
89
+ "OAuth redirect is not available in this runtime. Pass redirect: false and open the returned URL with your platform browser API.",
90
+ "AUTH_OAUTH_REDIRECT_UNAVAILABLE"
91
+ );
92
+ }
93
+ globalThis.window.location.assign(url);
94
+ }
65
95
  function createTokenStorage(prefix) {
66
96
  let useLocalStorage = false;
67
97
  try {
@@ -115,6 +145,8 @@ function createTokenStorage(prefix) {
115
145
  var AuthClient = class {
116
146
  serverUrl;
117
147
  storage;
148
+ fetchFn;
149
+ deviceIdentity;
118
150
  listeners = /* @__PURE__ */ new Set();
119
151
  _state = "loading";
120
152
  _user = null;
@@ -128,7 +160,9 @@ var AuthClient = class {
128
160
  constructor(config) {
129
161
  this.serverUrl = config.serverUrl.replace(/\/+$/, "");
130
162
  const prefix = config.storageKey ?? "kora_auth";
131
- this.storage = createTokenStorage(prefix);
163
+ this.storage = config.storage ?? createTokenStorage(prefix);
164
+ this.fetchFn = config.fetch ?? getDefaultFetch();
165
+ this.deviceIdentity = config.deviceIdentity;
132
166
  }
133
167
  // -----------------------------------------------------------------------
134
168
  // Public getters
@@ -160,8 +194,8 @@ var AuthClient = class {
160
194
  return;
161
195
  }
162
196
  this._initialized = true;
163
- const accessToken = this.storage.getAccessToken();
164
- const refreshToken = this.storage.getRefreshToken();
197
+ const accessToken = await this.storage.getAccessToken();
198
+ const refreshToken = await this.storage.getRefreshToken();
165
199
  if (!accessToken || !refreshToken) {
166
200
  this.setState("unauthenticated", null);
167
201
  return;
@@ -178,7 +212,7 @@ var AuthClient = class {
178
212
  }
179
213
  } catch {
180
214
  }
181
- this.storage.clear();
215
+ await this.storage.clear();
182
216
  this.setState("unauthenticated", null);
183
217
  }
184
218
  // -----------------------------------------------------------------------
@@ -192,12 +226,13 @@ var AuthClient = class {
192
226
  * @throws {AuthError} If the request fails or the server returns an error
193
227
  */
194
228
  async signUp(params) {
229
+ const body = await this.withDeviceIdentity(params);
195
230
  const response = await this.request("/auth/signup", {
196
231
  method: "POST",
197
- body: params
232
+ body
198
233
  });
199
234
  const tokens = "tokens" in response ? response.tokens : response;
200
- this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
235
+ await this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
201
236
  const user = await this.fetchUserProfile(tokens.accessToken);
202
237
  this.setState("authenticated", user);
203
238
  return user;
@@ -210,16 +245,88 @@ var AuthClient = class {
210
245
  * @throws {AuthError} If the credentials are invalid or the request fails
211
246
  */
212
247
  async signIn(params) {
248
+ const body = await this.withDeviceIdentity(params);
213
249
  const response = await this.request("/auth/signin", {
214
250
  method: "POST",
215
- body: params
251
+ body
216
252
  });
217
253
  const tokens = "tokens" in response ? response.tokens : response;
218
- this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
254
+ await this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
219
255
  const user = await this.fetchUserProfile(tokens.accessToken);
220
256
  this.setState("authenticated", user);
221
257
  return user;
222
258
  }
259
+ /**
260
+ * Create an OAuth authorization URL and optionally redirect the current window.
261
+ *
262
+ * For web apps, call this from a button click and keep the default redirect behavior.
263
+ * For desktop/mobile, pass `redirect: false`, open the returned URL with the runtime's
264
+ * browser API, then call `completeOAuthSignIn()` after receiving the callback.
265
+ */
266
+ async signInWithOAuth(provider, options = {}) {
267
+ const result = await this.createOAuthAuthorization(provider, options);
268
+ if (options.redirect ?? canRedirectCurrentWindow()) {
269
+ redirectCurrentWindow(result.url);
270
+ }
271
+ return result;
272
+ }
273
+ /**
274
+ * Complete an OAuth sign-in callback and store the issued Kora tokens.
275
+ */
276
+ async completeOAuthSignIn(provider, params) {
277
+ const body = await this.withDeviceIdentity(params);
278
+ const response = await this.request(
279
+ `/auth/oauth/${encodeURIComponent(provider)}/callback`,
280
+ {
281
+ method: "POST",
282
+ body: { ...body }
283
+ }
284
+ );
285
+ await this.storage.setTokens(response.tokens.accessToken, response.tokens.refreshToken);
286
+ const user = normalizeAuthUser(response.user);
287
+ this.setState("authenticated", user);
288
+ return user;
289
+ }
290
+ /**
291
+ * Create an OAuth authorization URL for linking another provider to the current user.
292
+ */
293
+ async getOAuthAuthorizationUrl(provider, options = {}) {
294
+ return this.createOAuthAuthorization(provider, { ...options, redirect: false });
295
+ }
296
+ /**
297
+ * Link an OAuth provider to the current authenticated user.
298
+ */
299
+ async linkOAuth(provider, params) {
300
+ const token = await this.requireAccessToken();
301
+ return this.request(`/auth/oauth/${encodeURIComponent(provider)}/link`, {
302
+ method: "POST",
303
+ body: {
304
+ code: params.code,
305
+ state: params.state
306
+ },
307
+ token
308
+ });
309
+ }
310
+ /**
311
+ * List OAuth accounts linked to the current authenticated user.
312
+ */
313
+ async listLinkedAccounts() {
314
+ const token = await this.requireAccessToken();
315
+ return this.request("/auth/oauth/links", {
316
+ method: "GET",
317
+ token
318
+ });
319
+ }
320
+ /**
321
+ * Unlink an OAuth provider from the current authenticated user.
322
+ */
323
+ async unlinkOAuth(provider) {
324
+ const token = await this.requireAccessToken();
325
+ await this.request(`/auth/oauth/${encodeURIComponent(provider)}/link`, {
326
+ method: "DELETE",
327
+ token
328
+ });
329
+ }
223
330
  /**
224
331
  * Sign out the current user.
225
332
  *
@@ -228,9 +335,9 @@ var AuthClient = class {
228
335
  * stolen refresh tokens cannot be used after the user explicitly signs out.
229
336
  */
230
337
  async signOut() {
231
- const accessToken = this.storage.getAccessToken();
232
- const refreshToken = this.storage.getRefreshToken();
233
- this.storage.clear();
338
+ const accessToken = await this.storage.getAccessToken();
339
+ const refreshToken = await this.storage.getRefreshToken();
340
+ await this.storage.clear();
234
341
  this._refreshPromise = null;
235
342
  this.setState("unauthenticated", null);
236
343
  if (accessToken) {
@@ -254,11 +361,11 @@ var AuthClient = class {
254
361
  * authenticated and refresh is not possible
255
362
  */
256
363
  async getAccessToken() {
257
- const accessToken = this.storage.getAccessToken();
364
+ const accessToken = await this.storage.getAccessToken();
258
365
  if (accessToken && !isTokenExpired(accessToken)) {
259
366
  return accessToken;
260
367
  }
261
- const refreshToken = this.storage.getRefreshToken();
368
+ const refreshToken = await this.storage.getRefreshToken();
262
369
  if (!refreshToken) {
263
370
  return null;
264
371
  }
@@ -341,7 +448,7 @@ var AuthClient = class {
341
448
  name: null
342
449
  });
343
450
  } else {
344
- this.storage.clear();
451
+ await this.storage.clear();
345
452
  this.setState("unauthenticated", null);
346
453
  }
347
454
  }
@@ -354,10 +461,47 @@ var AuthClient = class {
354
461
  method: "GET",
355
462
  token: accessToken
356
463
  });
464
+ return normalizeAuthUser(profile);
465
+ }
466
+ async createOAuthAuthorization(provider, options) {
467
+ const params = new URLSearchParams();
468
+ const withIdentity = await this.withDeviceIdentity({
469
+ deviceId: options.deviceId,
470
+ devicePublicKey: options.devicePublicKey
471
+ });
472
+ if (options.returnTo) {
473
+ params.set("returnTo", options.returnTo);
474
+ }
475
+ if (withIdentity.deviceId) {
476
+ params.set("deviceId", withIdentity.deviceId);
477
+ }
478
+ if (withIdentity.devicePublicKey) {
479
+ params.set("devicePublicKey", withIdentity.devicePublicKey);
480
+ }
481
+ if (options.metadata) {
482
+ for (const [key, value] of Object.entries(options.metadata)) {
483
+ if (value !== void 0 && value !== null) {
484
+ params.set(key, String(value));
485
+ }
486
+ }
487
+ }
488
+ const query = params.toString();
489
+ return this.request(
490
+ `/auth/oauth/${encodeURIComponent(provider)}${query ? `?${query}` : ""}`,
491
+ {
492
+ method: "GET"
493
+ }
494
+ );
495
+ }
496
+ async withDeviceIdentity(params) {
497
+ if (!this.deviceIdentity || params.deviceId && params.devicePublicKey) {
498
+ return params;
499
+ }
500
+ const identity = await this.deviceIdentity.getDeviceIdentity();
357
501
  return {
358
- id: profile.id,
359
- email: profile.email,
360
- name: profile.name ?? null
502
+ ...params,
503
+ deviceId: params.deviceId ?? identity.deviceId,
504
+ devicePublicKey: params.devicePublicKey ?? identity.devicePublicKey
361
505
  };
362
506
  }
363
507
  /**
@@ -376,6 +520,13 @@ var AuthClient = class {
376
520
  this._refreshPromise = null;
377
521
  }
378
522
  }
523
+ async requireAccessToken() {
524
+ const token = await this.getAccessToken();
525
+ if (!token) {
526
+ throw new AuthError("You must be signed in to perform this action.", "AUTH_REQUIRED");
527
+ }
528
+ return token;
529
+ }
379
530
  /**
380
531
  * Execute the token refresh network request.
381
532
  */
@@ -385,10 +536,10 @@ var AuthClient = class {
385
536
  method: "POST",
386
537
  body: { refreshToken }
387
538
  });
388
- this.storage.setTokens(response.accessToken, response.refreshToken);
539
+ await this.storage.setTokens(response.accessToken, response.refreshToken);
389
540
  return response.accessToken;
390
541
  } catch {
391
- this.storage.clear();
542
+ await this.storage.clear();
392
543
  this.setState("unauthenticated", null);
393
544
  return null;
394
545
  }
@@ -412,7 +563,7 @@ var AuthClient = class {
412
563
  }
413
564
  let response;
414
565
  try {
415
- response = await fetch(url, {
566
+ response = await this.fetchFn(url, {
416
567
  method: options.method,
417
568
  headers,
418
569
  body: options.body ? JSON.stringify(options.body) : void 0
@@ -424,35 +575,475 @@ var AuthClient = class {
424
575
  { path, cause: cause instanceof Error ? cause.message : String(cause) }
425
576
  );
426
577
  }
427
- if (!response.ok) {
428
- let errorMessage = `Auth server returned HTTP ${response.status}`;
429
- let serverError;
430
- try {
431
- const body = await response.json();
432
- if (typeof body.error === "string") {
433
- errorMessage = body.error;
434
- serverError = errorMessage;
435
- } else if (typeof body.message === "string") {
436
- errorMessage = body.message;
437
- serverError = errorMessage;
438
- }
439
- } catch {
578
+ if (!response.ok) {
579
+ let errorMessage = `Auth server returned HTTP ${response.status}`;
580
+ let serverError;
581
+ try {
582
+ const body = await response.json();
583
+ if (typeof body.error === "string") {
584
+ errorMessage = body.error;
585
+ serverError = errorMessage;
586
+ } else if (typeof body.message === "string") {
587
+ errorMessage = body.message;
588
+ serverError = errorMessage;
589
+ }
590
+ } catch {
591
+ }
592
+ throw new AuthError(errorMessage, "AUTH_SERVER_ERROR", {
593
+ path,
594
+ status: response.status,
595
+ serverError
596
+ });
597
+ }
598
+ const json = await response.json();
599
+ const data = json.data !== void 0 ? json.data : json;
600
+ return data;
601
+ }
602
+ };
603
+
604
+ // src/client/device-session.ts
605
+ import { KoraError as KoraError3 } from "@korajs/core";
606
+
607
+ // src/device/device-store.ts
608
+ import { KoraError as KoraError2 } from "@korajs/core";
609
+ var DeviceKeyStoreError = class extends KoraError2 {
610
+ constructor(message, context) {
611
+ super(message, "DEVICE_KEY_STORE_ERROR", context);
612
+ this.name = "DeviceKeyStoreError";
613
+ }
614
+ };
615
+ var IDB_DATABASE_NAME = "kora_device_keys";
616
+ var IDB_STORE_NAME = "keypairs";
617
+ var IDB_VERSION = 1;
618
+ var IndexedDBDeviceKeyStore = class {
619
+ dbPromise = null;
620
+ /**
621
+ * Opens (or creates) the IndexedDB database.
622
+ *
623
+ * The database connection is lazily initialized on first use and
624
+ * reused for subsequent operations. If the database does not exist,
625
+ * it is created with the `keypairs` object store.
626
+ */
627
+ openDatabase() {
628
+ if (this.dbPromise !== null) {
629
+ return this.dbPromise;
630
+ }
631
+ this.dbPromise = new Promise((resolve, reject) => {
632
+ let request;
633
+ try {
634
+ request = globalThis.indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION);
635
+ } catch (cause) {
636
+ this.dbPromise = null;
637
+ reject(
638
+ new DeviceKeyStoreError(
639
+ "Failed to open IndexedDB database for device key storage. IndexedDB may be unavailable or access may be denied.",
640
+ { cause: cause instanceof Error ? cause.message : String(cause) }
641
+ )
642
+ );
643
+ return;
644
+ }
645
+ request.onupgradeneeded = () => {
646
+ const db = request.result;
647
+ if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
648
+ db.createObjectStore(IDB_STORE_NAME);
649
+ }
650
+ };
651
+ request.onsuccess = () => {
652
+ resolve(request.result);
653
+ };
654
+ request.onerror = () => {
655
+ this.dbPromise = null;
656
+ reject(
657
+ new DeviceKeyStoreError("Failed to open IndexedDB database for device key storage.", {
658
+ error: request.error?.message
659
+ })
660
+ );
661
+ };
662
+ request.onblocked = () => {
663
+ this.dbPromise = null;
664
+ reject(
665
+ new DeviceKeyStoreError(
666
+ "IndexedDB database open was blocked. Another tab may have an older version of the database open. Close other tabs and try again."
667
+ )
668
+ );
669
+ };
670
+ });
671
+ return this.dbPromise;
672
+ }
673
+ /** @inheritdoc */
674
+ async saveKeyPair(deviceId, keyPair) {
675
+ const db = await this.openDatabase();
676
+ return new Promise((resolve, reject) => {
677
+ try {
678
+ const tx = db.transaction(IDB_STORE_NAME, "readwrite");
679
+ const store = tx.objectStore(IDB_STORE_NAME);
680
+ store.put(keyPair, deviceId);
681
+ tx.oncomplete = () => {
682
+ resolve();
683
+ };
684
+ tx.onerror = () => {
685
+ reject(
686
+ new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
687
+ deviceId,
688
+ error: tx.error?.message
689
+ })
690
+ );
691
+ };
692
+ } catch (cause) {
693
+ reject(
694
+ new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
695
+ deviceId,
696
+ cause: cause instanceof Error ? cause.message : String(cause)
697
+ })
698
+ );
699
+ }
700
+ });
701
+ }
702
+ /** @inheritdoc */
703
+ async loadKeyPair(deviceId) {
704
+ const db = await this.openDatabase();
705
+ return new Promise((resolve, reject) => {
706
+ try {
707
+ const tx = db.transaction(IDB_STORE_NAME, "readonly");
708
+ const store = tx.objectStore(IDB_STORE_NAME);
709
+ const request = store.get(deviceId);
710
+ request.onsuccess = () => {
711
+ const result = request.result;
712
+ resolve(result ?? null);
713
+ };
714
+ request.onerror = () => {
715
+ reject(
716
+ new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
717
+ deviceId,
718
+ error: request.error?.message
719
+ })
720
+ );
721
+ };
722
+ } catch (cause) {
723
+ reject(
724
+ new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
725
+ deviceId,
726
+ cause: cause instanceof Error ? cause.message : String(cause)
727
+ })
728
+ );
729
+ }
730
+ });
731
+ }
732
+ /** @inheritdoc */
733
+ async deleteKeyPair(deviceId) {
734
+ const db = await this.openDatabase();
735
+ return new Promise((resolve, reject) => {
736
+ try {
737
+ const tx = db.transaction(IDB_STORE_NAME, "readwrite");
738
+ const store = tx.objectStore(IDB_STORE_NAME);
739
+ store.delete(deviceId);
740
+ tx.oncomplete = () => {
741
+ resolve();
742
+ };
743
+ tx.onerror = () => {
744
+ reject(
745
+ new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
746
+ deviceId,
747
+ error: tx.error?.message
748
+ })
749
+ );
750
+ };
751
+ } catch (cause) {
752
+ reject(
753
+ new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
754
+ deviceId,
755
+ cause: cause instanceof Error ? cause.message : String(cause)
756
+ })
757
+ );
758
+ }
759
+ });
760
+ }
761
+ /** @inheritdoc */
762
+ async hasKeyPair(deviceId) {
763
+ const db = await this.openDatabase();
764
+ return new Promise((resolve, reject) => {
765
+ try {
766
+ const tx = db.transaction(IDB_STORE_NAME, "readonly");
767
+ const store = tx.objectStore(IDB_STORE_NAME);
768
+ const request = store.count(deviceId);
769
+ request.onsuccess = () => {
770
+ resolve(request.result > 0);
771
+ };
772
+ request.onerror = () => {
773
+ reject(
774
+ new DeviceKeyStoreError(
775
+ `Failed to check if key pair exists for device "${deviceId}".`,
776
+ { deviceId, error: request.error?.message }
777
+ )
778
+ );
779
+ };
780
+ } catch (cause) {
781
+ reject(
782
+ new DeviceKeyStoreError(`Failed to check if key pair exists for device "${deviceId}".`, {
783
+ deviceId,
784
+ cause: cause instanceof Error ? cause.message : String(cause)
785
+ })
786
+ );
787
+ }
788
+ });
789
+ }
790
+ };
791
+ var InMemoryDeviceKeyStore = class {
792
+ store = /* @__PURE__ */ new Map();
793
+ /** @inheritdoc */
794
+ async saveKeyPair(deviceId, keyPair) {
795
+ this.store.set(deviceId, keyPair);
796
+ }
797
+ /** @inheritdoc */
798
+ async loadKeyPair(deviceId) {
799
+ return this.store.get(deviceId) ?? null;
800
+ }
801
+ /** @inheritdoc */
802
+ async deleteKeyPair(deviceId) {
803
+ this.store.delete(deviceId);
804
+ }
805
+ /** @inheritdoc */
806
+ async hasKeyPair(deviceId) {
807
+ return this.store.has(deviceId);
808
+ }
809
+ };
810
+ function createDeviceKeyStore() {
811
+ if (typeof globalThis.indexedDB !== "undefined") {
812
+ return new IndexedDBDeviceKeyStore();
813
+ }
814
+ return new InMemoryDeviceKeyStore();
815
+ }
816
+
817
+ // src/client/device-session.ts
818
+ var DEFAULT_DEVICE_ID_KEY = "kora_auth_device_id";
819
+ var AuthDeviceIdentityError = class extends KoraError3 {
820
+ constructor(message, context) {
821
+ super(message, "AUTH_DEVICE_IDENTITY_ERROR", context);
822
+ this.name = "AuthDeviceIdentityError";
823
+ }
824
+ };
825
+ function createPersistentDeviceIdentity(options) {
826
+ const storage = options.storage;
827
+ const keyStore = options.keyStore ?? createDefaultPersistentKeyStore();
828
+ const deviceIdKey = options.deviceIdKey ?? DEFAULT_DEVICE_ID_KEY;
829
+ const generateDeviceId = options.generateDeviceId ?? defaultDeviceId;
830
+ return {
831
+ async getDeviceIdentity() {
832
+ let deviceId = await storage.getItem(deviceIdKey);
833
+ if (!deviceId) {
834
+ deviceId = generateDeviceId();
835
+ await storage.setItem(deviceIdKey, deviceId);
836
+ }
837
+ let keyPair = await keyStore.loadKeyPair(deviceId);
838
+ if (!keyPair) {
839
+ keyPair = await generateDeviceKeyPair();
840
+ await keyStore.saveKeyPair(deviceId, keyPair);
841
+ }
842
+ const publicKey = await exportPublicKeyJwk(keyPair);
843
+ return {
844
+ deviceId,
845
+ devicePublicKey: JSON.stringify(publicKey)
846
+ };
847
+ }
848
+ };
849
+ }
850
+ function createDefaultPersistentKeyStore() {
851
+ if (typeof globalThis.indexedDB !== "undefined") {
852
+ return createDeviceKeyStore();
853
+ }
854
+ throw new AuthDeviceIdentityError(
855
+ "No persistent device key store is available in this runtime. Pass `keyStore` to createPersistentDeviceIdentity()."
856
+ );
857
+ }
858
+ function defaultDeviceId() {
859
+ if (typeof globalThis.crypto?.randomUUID === "function") {
860
+ return globalThis.crypto.randomUUID();
861
+ }
862
+ const bytes = new Uint8Array(16);
863
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
864
+ globalThis.crypto.getRandomValues(bytes);
865
+ } else {
866
+ for (let i = 0; i < bytes.length; i++) {
867
+ bytes[i] = Math.floor(Math.random() * 256);
868
+ }
869
+ }
870
+ bytes[6] = bytes[6] & 15 | 64;
871
+ bytes[8] = bytes[8] & 63 | 128;
872
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
873
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
874
+ }
875
+
876
+ // src/client/storage.ts
877
+ function createAuthTokenStorage(options) {
878
+ const prefix = options.prefix ?? "kora_auth";
879
+ const accessKey = `${prefix}_access_token`;
880
+ const refreshKey = `${prefix}_refresh_token`;
881
+ const store = options.store;
882
+ return {
883
+ getAccessToken: () => store.getItem(accessKey),
884
+ getRefreshToken: () => store.getItem(refreshKey),
885
+ async setTokens(accessToken, refreshToken) {
886
+ await store.setItem(accessKey, accessToken);
887
+ await store.setItem(refreshKey, refreshToken);
888
+ },
889
+ async clear() {
890
+ await store.removeItem(accessKey);
891
+ await store.removeItem(refreshKey);
892
+ }
893
+ };
894
+ }
895
+ function createMemoryAuthTokenStorage() {
896
+ let accessToken = null;
897
+ let refreshToken = null;
898
+ return {
899
+ getAccessToken: () => accessToken,
900
+ getRefreshToken: () => refreshToken,
901
+ setTokens(access, refresh) {
902
+ accessToken = access;
903
+ refreshToken = refresh;
904
+ },
905
+ clear() {
906
+ accessToken = null;
907
+ refreshToken = null;
908
+ }
909
+ };
910
+ }
911
+ function createWebStorageAuthTokenStorage(storage, prefix) {
912
+ return createAuthTokenStorage({
913
+ prefix,
914
+ store: {
915
+ getItem: (key) => storage.getItem(key),
916
+ setItem: (key, value) => {
917
+ storage.setItem(key, value);
918
+ },
919
+ removeItem: (key) => {
920
+ storage.removeItem(key);
921
+ }
922
+ }
923
+ });
924
+ }
925
+
926
+ // src/client/quickstart.ts
927
+ function createKoraAuth(options) {
928
+ const storage = options.storage ?? (options.credentialStore ? createAuthTokenStorage({
929
+ store: options.credentialStore,
930
+ prefix: options.storageKey
931
+ }) : tryCreateDefaultTokenStorage(options.storageKey));
932
+ const deviceIdentity = options.deviceIdentity === false ? void 0 : options.deviceIdentity ?? tryCreateDefaultDeviceIdentity(options.credentialStore, options.deviceKeyStore);
933
+ return new AuthClient({
934
+ serverUrl: options.serverUrl,
935
+ storageKey: options.storageKey,
936
+ storage,
937
+ fetch: options.fetch,
938
+ deviceIdentity
939
+ });
940
+ }
941
+ function tryCreateDefaultTokenStorage(storageKey) {
942
+ const storage = tryGetBrowserStorage();
943
+ return storage ? createWebStorageAuthTokenStorage(storage, storageKey) : void 0;
944
+ }
945
+ function tryCreateDefaultDeviceIdentity(credentialStore, deviceKeyStore) {
946
+ const storage = credentialStore ?? tryGetBrowserStorage();
947
+ if (!storage) {
948
+ return void 0;
949
+ }
950
+ try {
951
+ return createPersistentDeviceIdentity({
952
+ storage,
953
+ keyStore: deviceKeyStore
954
+ });
955
+ } catch {
956
+ return void 0;
957
+ }
958
+ }
959
+ function tryGetBrowserStorage() {
960
+ try {
961
+ if (typeof globalThis.localStorage === "undefined") {
962
+ return null;
963
+ }
964
+ const key = "__kora_auth_quickstart_test__";
965
+ globalThis.localStorage.setItem(key, "1");
966
+ globalThis.localStorage.removeItem(key);
967
+ return globalThis.localStorage;
968
+ } catch {
969
+ return null;
970
+ }
971
+ }
972
+
973
+ // src/client/auth-sync.ts
974
+ import {
975
+ buildScopeMap,
976
+ extractScopeValuesFromClaims
977
+ } from "@korajs/core";
978
+ function decodeJwtPayload2(token) {
979
+ const parts = token.split(".");
980
+ if (parts.length !== 3) {
981
+ return null;
982
+ }
983
+ const payloadSegment = parts[1];
984
+ if (payloadSegment === void 0) {
985
+ return null;
986
+ }
987
+ try {
988
+ const base64 = payloadSegment.replace(/-/g, "+").replace(/_/g, "/");
989
+ const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
990
+ const json = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf-8");
991
+ const parsed = JSON.parse(json);
992
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
993
+ return null;
994
+ }
995
+ return parsed;
996
+ } catch {
997
+ return null;
998
+ }
999
+ }
1000
+ function readDeviceIdFromClaims(claims) {
1001
+ const dev = claims.dev;
1002
+ return typeof dev === "string" && dev.length > 0 ? dev : void 0;
1003
+ }
1004
+ function createKoraAuthSync(options) {
1005
+ const { authClient, schema, scopeFromClaims } = options;
1006
+ const binding = {
1007
+ auth: async () => {
1008
+ const token = await authClient.getAccessToken();
1009
+ return { token: token ?? "" };
1010
+ }
1011
+ };
1012
+ if (schema) {
1013
+ binding.resolveScopeMap = async () => {
1014
+ const token = await authClient.getAccessToken();
1015
+ if (!token) {
1016
+ return void 0;
440
1017
  }
441
- throw new AuthError(errorMessage, "AUTH_SERVER_ERROR", {
442
- path,
443
- status: response.status,
444
- serverError
445
- });
1018
+ const claims = decodeJwtPayload2(token);
1019
+ if (!claims) {
1020
+ return void 0;
1021
+ }
1022
+ const scopeValues = scopeFromClaims ? scopeFromClaims(claims) : extractScopeValuesFromClaims(schema, claims);
1023
+ return buildScopeMap(schema, scopeValues);
1024
+ };
1025
+ }
1026
+ binding.resolveNodeId = async () => {
1027
+ const token = await authClient.getAccessToken();
1028
+ if (!token) {
1029
+ return void 0;
446
1030
  }
447
- const json = await response.json();
448
- const data = json.data !== void 0 ? json.data : json;
449
- return data;
1031
+ const claims = decodeJwtPayload2(token);
1032
+ if (!claims) {
1033
+ return void 0;
1034
+ }
1035
+ return readDeviceIdFromClaims(claims);
1036
+ };
1037
+ if (authClient.onAuthChange) {
1038
+ binding.subscribe = (listener) => authClient.onAuthChange?.(() => listener()) ?? (() => {
1039
+ });
450
1040
  }
451
- };
1041
+ return binding;
1042
+ }
452
1043
 
453
1044
  // src/client/org-client.ts
454
- import { KoraError as KoraError2 } from "@korajs/core";
455
- var OrgClientError = class extends KoraError2 {
1045
+ import { KoraError as KoraError4 } from "@korajs/core";
1046
+ var OrgClientError = class extends KoraError4 {
456
1047
  constructor(message, code, context) {
457
1048
  super(message, code, context);
458
1049
  this.name = "OrgClientError";
@@ -711,216 +1302,6 @@ var OrgClient = class {
711
1302
  }
712
1303
  };
713
1304
 
714
- // src/device/device-store.ts
715
- import { KoraError as KoraError3 } from "@korajs/core";
716
- var DeviceKeyStoreError = class extends KoraError3 {
717
- constructor(message, context) {
718
- super(message, "DEVICE_KEY_STORE_ERROR", context);
719
- this.name = "DeviceKeyStoreError";
720
- }
721
- };
722
- var IDB_DATABASE_NAME = "kora_device_keys";
723
- var IDB_STORE_NAME = "keypairs";
724
- var IDB_VERSION = 1;
725
- var IndexedDBDeviceKeyStore = class {
726
- dbPromise = null;
727
- /**
728
- * Opens (or creates) the IndexedDB database.
729
- *
730
- * The database connection is lazily initialized on first use and
731
- * reused for subsequent operations. If the database does not exist,
732
- * it is created with the `keypairs` object store.
733
- */
734
- openDatabase() {
735
- if (this.dbPromise !== null) {
736
- return this.dbPromise;
737
- }
738
- this.dbPromise = new Promise((resolve, reject) => {
739
- let request;
740
- try {
741
- request = globalThis.indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION);
742
- } catch (cause) {
743
- this.dbPromise = null;
744
- reject(
745
- new DeviceKeyStoreError(
746
- "Failed to open IndexedDB database for device key storage. IndexedDB may be unavailable or access may be denied.",
747
- { cause: cause instanceof Error ? cause.message : String(cause) }
748
- )
749
- );
750
- return;
751
- }
752
- request.onupgradeneeded = () => {
753
- const db = request.result;
754
- if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
755
- db.createObjectStore(IDB_STORE_NAME);
756
- }
757
- };
758
- request.onsuccess = () => {
759
- resolve(request.result);
760
- };
761
- request.onerror = () => {
762
- this.dbPromise = null;
763
- reject(
764
- new DeviceKeyStoreError("Failed to open IndexedDB database for device key storage.", {
765
- error: request.error?.message
766
- })
767
- );
768
- };
769
- request.onblocked = () => {
770
- this.dbPromise = null;
771
- reject(
772
- new DeviceKeyStoreError(
773
- "IndexedDB database open was blocked. Another tab may have an older version of the database open. Close other tabs and try again."
774
- )
775
- );
776
- };
777
- });
778
- return this.dbPromise;
779
- }
780
- /** @inheritdoc */
781
- async saveKeyPair(deviceId, keyPair) {
782
- const db = await this.openDatabase();
783
- return new Promise((resolve, reject) => {
784
- try {
785
- const tx = db.transaction(IDB_STORE_NAME, "readwrite");
786
- const store = tx.objectStore(IDB_STORE_NAME);
787
- store.put(keyPair, deviceId);
788
- tx.oncomplete = () => {
789
- resolve();
790
- };
791
- tx.onerror = () => {
792
- reject(
793
- new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
794
- deviceId,
795
- error: tx.error?.message
796
- })
797
- );
798
- };
799
- } catch (cause) {
800
- reject(
801
- new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
802
- deviceId,
803
- cause: cause instanceof Error ? cause.message : String(cause)
804
- })
805
- );
806
- }
807
- });
808
- }
809
- /** @inheritdoc */
810
- async loadKeyPair(deviceId) {
811
- const db = await this.openDatabase();
812
- return new Promise((resolve, reject) => {
813
- try {
814
- const tx = db.transaction(IDB_STORE_NAME, "readonly");
815
- const store = tx.objectStore(IDB_STORE_NAME);
816
- const request = store.get(deviceId);
817
- request.onsuccess = () => {
818
- const result = request.result;
819
- resolve(result ?? null);
820
- };
821
- request.onerror = () => {
822
- reject(
823
- new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
824
- deviceId,
825
- error: request.error?.message
826
- })
827
- );
828
- };
829
- } catch (cause) {
830
- reject(
831
- new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
832
- deviceId,
833
- cause: cause instanceof Error ? cause.message : String(cause)
834
- })
835
- );
836
- }
837
- });
838
- }
839
- /** @inheritdoc */
840
- async deleteKeyPair(deviceId) {
841
- const db = await this.openDatabase();
842
- return new Promise((resolve, reject) => {
843
- try {
844
- const tx = db.transaction(IDB_STORE_NAME, "readwrite");
845
- const store = tx.objectStore(IDB_STORE_NAME);
846
- store.delete(deviceId);
847
- tx.oncomplete = () => {
848
- resolve();
849
- };
850
- tx.onerror = () => {
851
- reject(
852
- new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
853
- deviceId,
854
- error: tx.error?.message
855
- })
856
- );
857
- };
858
- } catch (cause) {
859
- reject(
860
- new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
861
- deviceId,
862
- cause: cause instanceof Error ? cause.message : String(cause)
863
- })
864
- );
865
- }
866
- });
867
- }
868
- /** @inheritdoc */
869
- async hasKeyPair(deviceId) {
870
- const db = await this.openDatabase();
871
- return new Promise((resolve, reject) => {
872
- try {
873
- const tx = db.transaction(IDB_STORE_NAME, "readonly");
874
- const store = tx.objectStore(IDB_STORE_NAME);
875
- const request = store.count(deviceId);
876
- request.onsuccess = () => {
877
- resolve(request.result > 0);
878
- };
879
- request.onerror = () => {
880
- reject(
881
- new DeviceKeyStoreError(
882
- `Failed to check if key pair exists for device "${deviceId}".`,
883
- { deviceId, error: request.error?.message }
884
- )
885
- );
886
- };
887
- } catch (cause) {
888
- reject(
889
- new DeviceKeyStoreError(`Failed to check if key pair exists for device "${deviceId}".`, {
890
- deviceId,
891
- cause: cause instanceof Error ? cause.message : String(cause)
892
- })
893
- );
894
- }
895
- });
896
- }
897
- };
898
- var InMemoryDeviceKeyStore = class {
899
- store = /* @__PURE__ */ new Map();
900
- /** @inheritdoc */
901
- async saveKeyPair(deviceId, keyPair) {
902
- this.store.set(deviceId, keyPair);
903
- }
904
- /** @inheritdoc */
905
- async loadKeyPair(deviceId) {
906
- return this.store.get(deviceId) ?? null;
907
- }
908
- /** @inheritdoc */
909
- async deleteKeyPair(deviceId) {
910
- this.store.delete(deviceId);
911
- }
912
- /** @inheritdoc */
913
- async hasKeyPair(deviceId) {
914
- return this.store.has(deviceId);
915
- }
916
- };
917
- function createDeviceKeyStore() {
918
- if (typeof globalThis.indexedDB !== "undefined") {
919
- return new IndexedDBDeviceKeyStore();
920
- }
921
- return new InMemoryDeviceKeyStore();
922
- }
923
-
924
1305
  // src/tokens/token-store.ts
925
1306
  var DEFAULT_STORAGE_KEY = "kora_auth_tokens";
926
1307
  var MemoryStorage = class {
@@ -1044,8 +1425,8 @@ var TokenStore = class {
1044
1425
  };
1045
1426
 
1046
1427
  // src/tokens/encrypted-token-store.ts
1047
- import { KoraError as KoraError4 } from "@korajs/core";
1048
- var EncryptedTokenStoreError = class extends KoraError4 {
1428
+ import { KoraError as KoraError5 } from "@korajs/core";
1429
+ var EncryptedTokenStoreError = class extends KoraError5 {
1049
1430
  constructor(message, context) {
1050
1431
  super(message, "ENCRYPTED_TOKEN_STORE_ERROR", context);
1051
1432
  this.name = "EncryptedTokenStoreError";
@@ -1240,8 +1621,8 @@ var EncryptedTokenStore = class {
1240
1621
  };
1241
1622
 
1242
1623
  // src/encryption/key-derivation.ts
1243
- import { KoraError as KoraError5 } from "@korajs/core";
1244
- var KeyDerivationError = class extends KoraError5 {
1624
+ import { KoraError as KoraError6 } from "@korajs/core";
1625
+ var KeyDerivationError = class extends KoraError6 {
1245
1626
  constructor(message, context) {
1246
1627
  super(message, "KEY_DERIVATION_ERROR", context);
1247
1628
  this.name = "KeyDerivationError";
@@ -1421,6 +1802,7 @@ var AutoLockManager = class {
1421
1802
  };
1422
1803
  export {
1423
1804
  AuthClient,
1805
+ AuthDeviceIdentityError,
1424
1806
  AuthError,
1425
1807
  AutoLockManager,
1426
1808
  CryptoUnavailableError,
@@ -1441,8 +1823,14 @@ export {
1441
1823
  TokenStore,
1442
1824
  authenticateWithPasskey,
1443
1825
  computePublicKeyThumbprint,
1826
+ createAuthTokenStorage,
1444
1827
  createDeviceKeyStore,
1828
+ createKoraAuth,
1829
+ createKoraAuthSync,
1830
+ createMemoryAuthTokenStorage,
1445
1831
  createPasskeyCredential,
1832
+ createPersistentDeviceIdentity,
1833
+ createWebStorageAuthTokenStorage,
1446
1834
  decryptData,
1447
1835
  deriveEncryptionKey,
1448
1836
  encryptData,