@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.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthClient: () => AuthClient,
24
+ AuthDeviceIdentityError: () => AuthDeviceIdentityError,
24
25
  AuthError: () => AuthError,
25
26
  AutoLockManager: () => AutoLockManager,
26
27
  CryptoUnavailableError: () => CryptoUnavailableError,
@@ -41,8 +42,14 @@ __export(index_exports, {
41
42
  TokenStore: () => TokenStore,
42
43
  authenticateWithPasskey: () => authenticateWithPasskey,
43
44
  computePublicKeyThumbprint: () => computePublicKeyThumbprint,
45
+ createAuthTokenStorage: () => createAuthTokenStorage,
44
46
  createDeviceKeyStore: () => createDeviceKeyStore,
47
+ createKoraAuth: () => createKoraAuth,
48
+ createKoraAuthSync: () => createKoraAuthSync,
49
+ createMemoryAuthTokenStorage: () => createMemoryAuthTokenStorage,
45
50
  createPasskeyCredential: () => createPasskeyCredential,
51
+ createPersistentDeviceIdentity: () => createPersistentDeviceIdentity,
52
+ createWebStorageAuthTokenStorage: () => createWebStorageAuthTokenStorage,
46
53
  decryptData: () => decryptData,
47
54
  deriveEncryptionKey: () => deriveEncryptionKey,
48
55
  encryptData: () => encryptData,
@@ -99,6 +106,36 @@ function getUserIdFromToken(token) {
99
106
  }
100
107
  return payload.sub;
101
108
  }
109
+ function getDefaultFetch() {
110
+ if (typeof globalThis.fetch !== "function") {
111
+ return async () => {
112
+ throw new AuthError(
113
+ "No fetch implementation is available in this runtime. Pass `fetch` to AuthClientConfig.",
114
+ "AUTH_FETCH_UNAVAILABLE"
115
+ );
116
+ };
117
+ }
118
+ return globalThis.fetch.bind(globalThis);
119
+ }
120
+ function normalizeAuthUser(user) {
121
+ return {
122
+ id: user.id,
123
+ email: user.email,
124
+ name: user.name ?? null
125
+ };
126
+ }
127
+ function canRedirectCurrentWindow() {
128
+ return typeof globalThis.window !== "undefined" && typeof globalThis.window.location?.assign === "function";
129
+ }
130
+ function redirectCurrentWindow(url) {
131
+ if (!canRedirectCurrentWindow()) {
132
+ throw new AuthError(
133
+ "OAuth redirect is not available in this runtime. Pass redirect: false and open the returned URL with your platform browser API.",
134
+ "AUTH_OAUTH_REDIRECT_UNAVAILABLE"
135
+ );
136
+ }
137
+ globalThis.window.location.assign(url);
138
+ }
102
139
  function createTokenStorage(prefix) {
103
140
  let useLocalStorage = false;
104
141
  try {
@@ -152,6 +189,8 @@ function createTokenStorage(prefix) {
152
189
  var AuthClient = class {
153
190
  serverUrl;
154
191
  storage;
192
+ fetchFn;
193
+ deviceIdentity;
155
194
  listeners = /* @__PURE__ */ new Set();
156
195
  _state = "loading";
157
196
  _user = null;
@@ -165,7 +204,9 @@ var AuthClient = class {
165
204
  constructor(config) {
166
205
  this.serverUrl = config.serverUrl.replace(/\/+$/, "");
167
206
  const prefix = config.storageKey ?? "kora_auth";
168
- this.storage = createTokenStorage(prefix);
207
+ this.storage = config.storage ?? createTokenStorage(prefix);
208
+ this.fetchFn = config.fetch ?? getDefaultFetch();
209
+ this.deviceIdentity = config.deviceIdentity;
169
210
  }
170
211
  // -----------------------------------------------------------------------
171
212
  // Public getters
@@ -197,8 +238,8 @@ var AuthClient = class {
197
238
  return;
198
239
  }
199
240
  this._initialized = true;
200
- const accessToken = this.storage.getAccessToken();
201
- const refreshToken = this.storage.getRefreshToken();
241
+ const accessToken = await this.storage.getAccessToken();
242
+ const refreshToken = await this.storage.getRefreshToken();
202
243
  if (!accessToken || !refreshToken) {
203
244
  this.setState("unauthenticated", null);
204
245
  return;
@@ -215,7 +256,7 @@ var AuthClient = class {
215
256
  }
216
257
  } catch {
217
258
  }
218
- this.storage.clear();
259
+ await this.storage.clear();
219
260
  this.setState("unauthenticated", null);
220
261
  }
221
262
  // -----------------------------------------------------------------------
@@ -229,12 +270,13 @@ var AuthClient = class {
229
270
  * @throws {AuthError} If the request fails or the server returns an error
230
271
  */
231
272
  async signUp(params) {
273
+ const body = await this.withDeviceIdentity(params);
232
274
  const response = await this.request("/auth/signup", {
233
275
  method: "POST",
234
- body: params
276
+ body
235
277
  });
236
278
  const tokens = "tokens" in response ? response.tokens : response;
237
- this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
279
+ await this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
238
280
  const user = await this.fetchUserProfile(tokens.accessToken);
239
281
  this.setState("authenticated", user);
240
282
  return user;
@@ -247,16 +289,88 @@ var AuthClient = class {
247
289
  * @throws {AuthError} If the credentials are invalid or the request fails
248
290
  */
249
291
  async signIn(params) {
292
+ const body = await this.withDeviceIdentity(params);
250
293
  const response = await this.request("/auth/signin", {
251
294
  method: "POST",
252
- body: params
295
+ body
253
296
  });
254
297
  const tokens = "tokens" in response ? response.tokens : response;
255
- this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
298
+ await this.storage.setTokens(tokens.accessToken, tokens.refreshToken);
256
299
  const user = await this.fetchUserProfile(tokens.accessToken);
257
300
  this.setState("authenticated", user);
258
301
  return user;
259
302
  }
303
+ /**
304
+ * Create an OAuth authorization URL and optionally redirect the current window.
305
+ *
306
+ * For web apps, call this from a button click and keep the default redirect behavior.
307
+ * For desktop/mobile, pass `redirect: false`, open the returned URL with the runtime's
308
+ * browser API, then call `completeOAuthSignIn()` after receiving the callback.
309
+ */
310
+ async signInWithOAuth(provider, options = {}) {
311
+ const result = await this.createOAuthAuthorization(provider, options);
312
+ if (options.redirect ?? canRedirectCurrentWindow()) {
313
+ redirectCurrentWindow(result.url);
314
+ }
315
+ return result;
316
+ }
317
+ /**
318
+ * Complete an OAuth sign-in callback and store the issued Kora tokens.
319
+ */
320
+ async completeOAuthSignIn(provider, params) {
321
+ const body = await this.withDeviceIdentity(params);
322
+ const response = await this.request(
323
+ `/auth/oauth/${encodeURIComponent(provider)}/callback`,
324
+ {
325
+ method: "POST",
326
+ body: { ...body }
327
+ }
328
+ );
329
+ await this.storage.setTokens(response.tokens.accessToken, response.tokens.refreshToken);
330
+ const user = normalizeAuthUser(response.user);
331
+ this.setState("authenticated", user);
332
+ return user;
333
+ }
334
+ /**
335
+ * Create an OAuth authorization URL for linking another provider to the current user.
336
+ */
337
+ async getOAuthAuthorizationUrl(provider, options = {}) {
338
+ return this.createOAuthAuthorization(provider, { ...options, redirect: false });
339
+ }
340
+ /**
341
+ * Link an OAuth provider to the current authenticated user.
342
+ */
343
+ async linkOAuth(provider, params) {
344
+ const token = await this.requireAccessToken();
345
+ return this.request(`/auth/oauth/${encodeURIComponent(provider)}/link`, {
346
+ method: "POST",
347
+ body: {
348
+ code: params.code,
349
+ state: params.state
350
+ },
351
+ token
352
+ });
353
+ }
354
+ /**
355
+ * List OAuth accounts linked to the current authenticated user.
356
+ */
357
+ async listLinkedAccounts() {
358
+ const token = await this.requireAccessToken();
359
+ return this.request("/auth/oauth/links", {
360
+ method: "GET",
361
+ token
362
+ });
363
+ }
364
+ /**
365
+ * Unlink an OAuth provider from the current authenticated user.
366
+ */
367
+ async unlinkOAuth(provider) {
368
+ const token = await this.requireAccessToken();
369
+ await this.request(`/auth/oauth/${encodeURIComponent(provider)}/link`, {
370
+ method: "DELETE",
371
+ token
372
+ });
373
+ }
260
374
  /**
261
375
  * Sign out the current user.
262
376
  *
@@ -265,9 +379,9 @@ var AuthClient = class {
265
379
  * stolen refresh tokens cannot be used after the user explicitly signs out.
266
380
  */
267
381
  async signOut() {
268
- const accessToken = this.storage.getAccessToken();
269
- const refreshToken = this.storage.getRefreshToken();
270
- this.storage.clear();
382
+ const accessToken = await this.storage.getAccessToken();
383
+ const refreshToken = await this.storage.getRefreshToken();
384
+ await this.storage.clear();
271
385
  this._refreshPromise = null;
272
386
  this.setState("unauthenticated", null);
273
387
  if (accessToken) {
@@ -291,11 +405,11 @@ var AuthClient = class {
291
405
  * authenticated and refresh is not possible
292
406
  */
293
407
  async getAccessToken() {
294
- const accessToken = this.storage.getAccessToken();
408
+ const accessToken = await this.storage.getAccessToken();
295
409
  if (accessToken && !isTokenExpired(accessToken)) {
296
410
  return accessToken;
297
411
  }
298
- const refreshToken = this.storage.getRefreshToken();
412
+ const refreshToken = await this.storage.getRefreshToken();
299
413
  if (!refreshToken) {
300
414
  return null;
301
415
  }
@@ -378,7 +492,7 @@ var AuthClient = class {
378
492
  name: null
379
493
  });
380
494
  } else {
381
- this.storage.clear();
495
+ await this.storage.clear();
382
496
  this.setState("unauthenticated", null);
383
497
  }
384
498
  }
@@ -391,10 +505,47 @@ var AuthClient = class {
391
505
  method: "GET",
392
506
  token: accessToken
393
507
  });
508
+ return normalizeAuthUser(profile);
509
+ }
510
+ async createOAuthAuthorization(provider, options) {
511
+ const params = new URLSearchParams();
512
+ const withIdentity = await this.withDeviceIdentity({
513
+ deviceId: options.deviceId,
514
+ devicePublicKey: options.devicePublicKey
515
+ });
516
+ if (options.returnTo) {
517
+ params.set("returnTo", options.returnTo);
518
+ }
519
+ if (withIdentity.deviceId) {
520
+ params.set("deviceId", withIdentity.deviceId);
521
+ }
522
+ if (withIdentity.devicePublicKey) {
523
+ params.set("devicePublicKey", withIdentity.devicePublicKey);
524
+ }
525
+ if (options.metadata) {
526
+ for (const [key, value] of Object.entries(options.metadata)) {
527
+ if (value !== void 0 && value !== null) {
528
+ params.set(key, String(value));
529
+ }
530
+ }
531
+ }
532
+ const query = params.toString();
533
+ return this.request(
534
+ `/auth/oauth/${encodeURIComponent(provider)}${query ? `?${query}` : ""}`,
535
+ {
536
+ method: "GET"
537
+ }
538
+ );
539
+ }
540
+ async withDeviceIdentity(params) {
541
+ if (!this.deviceIdentity || params.deviceId && params.devicePublicKey) {
542
+ return params;
543
+ }
544
+ const identity = await this.deviceIdentity.getDeviceIdentity();
394
545
  return {
395
- id: profile.id,
396
- email: profile.email,
397
- name: profile.name ?? null
546
+ ...params,
547
+ deviceId: params.deviceId ?? identity.deviceId,
548
+ devicePublicKey: params.devicePublicKey ?? identity.devicePublicKey
398
549
  };
399
550
  }
400
551
  /**
@@ -413,6 +564,13 @@ var AuthClient = class {
413
564
  this._refreshPromise = null;
414
565
  }
415
566
  }
567
+ async requireAccessToken() {
568
+ const token = await this.getAccessToken();
569
+ if (!token) {
570
+ throw new AuthError("You must be signed in to perform this action.", "AUTH_REQUIRED");
571
+ }
572
+ return token;
573
+ }
416
574
  /**
417
575
  * Execute the token refresh network request.
418
576
  */
@@ -422,10 +580,10 @@ var AuthClient = class {
422
580
  method: "POST",
423
581
  body: { refreshToken }
424
582
  });
425
- this.storage.setTokens(response.accessToken, response.refreshToken);
583
+ await this.storage.setTokens(response.accessToken, response.refreshToken);
426
584
  return response.accessToken;
427
585
  } catch {
428
- this.storage.clear();
586
+ await this.storage.clear();
429
587
  this.setState("unauthenticated", null);
430
588
  return null;
431
589
  }
@@ -449,7 +607,7 @@ var AuthClient = class {
449
607
  }
450
608
  let response;
451
609
  try {
452
- response = await fetch(url, {
610
+ response = await this.fetchFn(url, {
453
611
  method: options.method,
454
612
  headers,
455
613
  body: options.body ? JSON.stringify(options.body) : void 0
@@ -487,400 +645,142 @@ var AuthClient = class {
487
645
  }
488
646
  };
489
647
 
490
- // src/client/org-client.ts
648
+ // src/client/device-session.ts
649
+ var import_core4 = require("@korajs/core");
650
+
651
+ // src/device/device-identity.ts
491
652
  var import_core2 = require("@korajs/core");
492
- var OrgClientError = class extends import_core2.KoraError {
493
- constructor(message, code, context) {
494
- super(message, code, context);
495
- this.name = "OrgClientError";
653
+ var CryptoUnavailableError = class extends import_core2.KoraError {
654
+ constructor() {
655
+ super(
656
+ "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.",
657
+ "CRYPTO_UNAVAILABLE"
658
+ );
659
+ this.name = "CryptoUnavailableError";
496
660
  }
497
661
  };
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;
662
+ var DeviceIdentityError = class extends import_core2.KoraError {
663
+ constructor(message, context) {
664
+ super(message, "DEVICE_IDENTITY_ERROR", context);
665
+ this.name = "DeviceIdentityError";
508
666
  }
509
- // --- Getters ---
510
- /** Currently active organization ID */
511
- get activeOrgId() {
512
- return this._activeOrgId;
667
+ };
668
+ function toBase64Url(buffer) {
669
+ const bytes = new Uint8Array(buffer);
670
+ let binary = "";
671
+ for (let i = 0; i < bytes.length; i++) {
672
+ binary += String.fromCharCode(bytes[i]);
513
673
  }
514
- /** Currently active organization */
515
- get activeOrg() {
516
- return this._activeOrg;
674
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
675
+ }
676
+ function fromBase64Url(str) {
677
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
678
+ const paddingNeeded = (4 - base64.length % 4) % 4;
679
+ base64 += "=".repeat(paddingNeeded);
680
+ const binary = atob(base64);
681
+ const bytes = new Uint8Array(binary.length);
682
+ for (let i = 0; i < binary.length; i++) {
683
+ bytes[i] = binary.charCodeAt(i);
517
684
  }
518
- /** Current user's role in the active organization */
519
- get activeRole() {
520
- return this._activeRole;
685
+ return bytes;
686
+ }
687
+ function assertCryptoAvailable() {
688
+ if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
689
+ throw new CryptoUnavailableError();
521
690
  }
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
- });
691
+ }
692
+ var ECDSA_ALGORITHM = {
693
+ name: "ECDSA",
694
+ namedCurve: "P-256"
695
+ };
696
+ var ECDSA_SIGN_ALGORITHM = {
697
+ name: "ECDSA",
698
+ hash: { name: "SHA-256" }
699
+ };
700
+ async function generateDeviceKeyPair() {
701
+ assertCryptoAvailable();
702
+ try {
703
+ const keyPair = await globalThis.crypto.subtle.generateKey(
704
+ ECDSA_ALGORITHM,
705
+ // extractable: false makes the private key non-extractable.
706
+ // The public key is always extractable regardless of this flag.
707
+ false,
708
+ ["sign", "verify"]
709
+ );
710
+ return keyPair;
711
+ } catch (cause) {
712
+ throw new DeviceIdentityError(
713
+ "Failed to generate ECDSA P-256 device key pair. Ensure the runtime supports the ECDSA algorithm with the P-256 curve.",
714
+ { cause: cause instanceof Error ? cause.message : String(cause) }
715
+ );
531
716
  }
532
- /**
533
- * List all organizations the current user belongs to.
534
- */
535
- async listOrgs() {
536
- return this.request("/orgs", { method: "GET" });
717
+ }
718
+ async function exportPublicKeyJwk(keyPair) {
719
+ assertCryptoAvailable();
720
+ try {
721
+ const jwk = await globalThis.crypto.subtle.exportKey("jwk", keyPair.publicKey);
722
+ return jwk;
723
+ } catch (cause) {
724
+ throw new DeviceIdentityError(
725
+ "Failed to export public key as JWK. The key pair may be invalid or the public key may not support JWK export.",
726
+ { cause: cause instanceof Error ? cause.message : String(cause) }
727
+ );
537
728
  }
538
- /**
539
- * Get an organization by ID.
540
- */
541
- async getOrg(orgId) {
542
- return this.request(`/orgs/${orgId}`, { method: "GET" });
729
+ }
730
+ async function signChallenge(privateKey, challenge) {
731
+ assertCryptoAvailable();
732
+ try {
733
+ const encoded = new TextEncoder().encode(challenge);
734
+ const signatureBuffer = await globalThis.crypto.subtle.sign(
735
+ ECDSA_SIGN_ALGORITHM,
736
+ privateKey,
737
+ encoded
738
+ );
739
+ return toBase64Url(signatureBuffer);
740
+ } catch (cause) {
741
+ throw new DeviceIdentityError(
742
+ 'Failed to sign challenge. Ensure the key is a valid ECDSA P-256 private key with "sign" usage.',
743
+ { cause: cause instanceof Error ? cause.message : String(cause) }
744
+ );
543
745
  }
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;
746
+ }
747
+ async function verifyChallenge(publicKeyJwk, challenge, signature) {
748
+ assertCryptoAvailable();
749
+ try {
750
+ const publicKey = await globalThis.crypto.subtle.importKey(
751
+ "jwk",
752
+ publicKeyJwk,
753
+ ECDSA_ALGORITHM,
754
+ true,
755
+ ["verify"]
756
+ );
757
+ const encoded = new TextEncoder().encode(challenge);
758
+ const signatureBytes = fromBase64Url(signature);
759
+ const isValid = await globalThis.crypto.subtle.verify(
760
+ ECDSA_SIGN_ALGORITHM,
761
+ publicKey,
762
+ signatureBytes,
763
+ encoded
764
+ );
765
+ return isValid;
766
+ } catch (cause) {
767
+ throw new DeviceIdentityError(
768
+ "Failed to verify challenge signature. The public key JWK or signature format may be invalid.",
769
+ {
770
+ cause: cause instanceof Error ? cause.message : String(cause),
771
+ publicKeyKty: publicKeyJwk.kty,
772
+ publicKeyCrv: publicKeyJwk.crv
773
+ }
774
+ );
556
775
  }
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(`/invitations?email=${encodeURIComponent(email)}`, {
672
- method: "GET"
673
- });
674
- }
675
- // --- Subscriptions ---
676
- /**
677
- * Subscribe to active org changes.
678
- * @returns Unsubscribe function
679
- */
680
- onOrgChange(callback) {
681
- this.listeners.add(callback);
682
- return () => {
683
- this.listeners.delete(callback);
684
- };
685
- }
686
- // --- Internal ---
687
- notifyListeners() {
688
- for (const listener of this.listeners) {
689
- try {
690
- listener(this._activeOrgId);
691
- } catch {
692
- }
693
- }
694
- }
695
- async request(path, options) {
696
- const token = await this.getAccessToken();
697
- if (!token) {
698
- throw new OrgClientError(
699
- "Not authenticated. Sign in before performing organization operations.",
700
- "ORG_NOT_AUTHENTICATED"
701
- );
702
- }
703
- const url = `${this.serverUrl}${path}`;
704
- const headers = {
705
- Authorization: `Bearer ${token}`
706
- };
707
- if (options.body) {
708
- headers["Content-Type"] = "application/json";
709
- }
710
- let response;
711
- try {
712
- response = await fetch(url, {
713
- method: options.method,
714
- headers,
715
- body: options.body ? JSON.stringify(options.body) : void 0
716
- });
717
- } catch (cause) {
718
- throw new OrgClientError(`Network request to ${path} failed.`, "ORG_NETWORK_ERROR", {
719
- path,
720
- cause: cause instanceof Error ? cause.message : String(cause)
721
- });
722
- }
723
- if (!response.ok) {
724
- let errorMessage = `Server returned HTTP ${response.status}`;
725
- try {
726
- const body = await response.json();
727
- if (typeof body.error === "string") {
728
- errorMessage = body.error;
729
- }
730
- } catch {
731
- }
732
- throw new OrgClientError(errorMessage, "ORG_SERVER_ERROR", {
733
- path,
734
- status: response.status
735
- });
736
- }
737
- const text = await response.text();
738
- if (text.length === 0) return void 0;
739
- try {
740
- const body = JSON.parse(text);
741
- if (body && typeof body === "object" && "data" in body) {
742
- return body.data;
743
- }
744
- return body;
745
- } catch {
746
- return void 0;
747
- }
748
- }
749
- };
750
-
751
- // src/device/device-identity.ts
752
- var import_core3 = require("@korajs/core");
753
- var CryptoUnavailableError = class extends import_core3.KoraError {
754
- constructor() {
755
- super(
756
- "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.",
757
- "CRYPTO_UNAVAILABLE"
758
- );
759
- this.name = "CryptoUnavailableError";
760
- }
761
- };
762
- var DeviceIdentityError = class extends import_core3.KoraError {
763
- constructor(message, context) {
764
- super(message, "DEVICE_IDENTITY_ERROR", context);
765
- this.name = "DeviceIdentityError";
766
- }
767
- };
768
- function toBase64Url(buffer) {
769
- const bytes = new Uint8Array(buffer);
770
- let binary = "";
771
- for (let i = 0; i < bytes.length; i++) {
772
- binary += String.fromCharCode(bytes[i]);
773
- }
774
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
775
- }
776
- function fromBase64Url(str) {
777
- let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
778
- const paddingNeeded = (4 - base64.length % 4) % 4;
779
- base64 += "=".repeat(paddingNeeded);
780
- const binary = atob(base64);
781
- const bytes = new Uint8Array(binary.length);
782
- for (let i = 0; i < binary.length; i++) {
783
- bytes[i] = binary.charCodeAt(i);
784
- }
785
- return bytes;
786
- }
787
- function assertCryptoAvailable() {
788
- if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
789
- throw new CryptoUnavailableError();
790
- }
791
- }
792
- var ECDSA_ALGORITHM = {
793
- name: "ECDSA",
794
- namedCurve: "P-256"
795
- };
796
- var ECDSA_SIGN_ALGORITHM = {
797
- name: "ECDSA",
798
- hash: { name: "SHA-256" }
799
- };
800
- async function generateDeviceKeyPair() {
801
- assertCryptoAvailable();
802
- try {
803
- const keyPair = await globalThis.crypto.subtle.generateKey(
804
- ECDSA_ALGORITHM,
805
- // extractable: false makes the private key non-extractable.
806
- // The public key is always extractable regardless of this flag.
807
- false,
808
- ["sign", "verify"]
809
- );
810
- return keyPair;
811
- } catch (cause) {
812
- throw new DeviceIdentityError(
813
- "Failed to generate ECDSA P-256 device key pair. Ensure the runtime supports the ECDSA algorithm with the P-256 curve.",
814
- { cause: cause instanceof Error ? cause.message : String(cause) }
815
- );
816
- }
817
- }
818
- async function exportPublicKeyJwk(keyPair) {
819
- assertCryptoAvailable();
820
- try {
821
- const jwk = await globalThis.crypto.subtle.exportKey("jwk", keyPair.publicKey);
822
- return jwk;
823
- } catch (cause) {
824
- throw new DeviceIdentityError(
825
- "Failed to export public key as JWK. The key pair may be invalid or the public key may not support JWK export.",
826
- { cause: cause instanceof Error ? cause.message : String(cause) }
827
- );
828
- }
829
- }
830
- async function signChallenge(privateKey, challenge) {
831
- assertCryptoAvailable();
832
- try {
833
- const encoded = new TextEncoder().encode(challenge);
834
- const signatureBuffer = await globalThis.crypto.subtle.sign(
835
- ECDSA_SIGN_ALGORITHM,
836
- privateKey,
837
- encoded
838
- );
839
- return toBase64Url(signatureBuffer);
840
- } catch (cause) {
841
- throw new DeviceIdentityError(
842
- 'Failed to sign challenge. Ensure the key is a valid ECDSA P-256 private key with "sign" usage.',
843
- { cause: cause instanceof Error ? cause.message : String(cause) }
844
- );
845
- }
846
- }
847
- async function verifyChallenge(publicKeyJwk, challenge, signature) {
848
- assertCryptoAvailable();
849
- try {
850
- const publicKey = await globalThis.crypto.subtle.importKey(
851
- "jwk",
852
- publicKeyJwk,
853
- ECDSA_ALGORITHM,
854
- true,
855
- ["verify"]
856
- );
857
- const encoded = new TextEncoder().encode(challenge);
858
- const signatureBytes = fromBase64Url(signature);
859
- const isValid = await globalThis.crypto.subtle.verify(
860
- ECDSA_SIGN_ALGORITHM,
861
- publicKey,
862
- signatureBytes,
863
- encoded
864
- );
865
- return isValid;
866
- } catch (cause) {
867
- throw new DeviceIdentityError(
868
- "Failed to verify challenge signature. The public key JWK or signature format may be invalid.",
869
- {
870
- cause: cause instanceof Error ? cause.message : String(cause),
871
- publicKeyKty: publicKeyJwk.kty,
872
- publicKeyCrv: publicKeyJwk.crv
873
- }
874
- );
875
- }
876
- }
877
- async function computePublicKeyThumbprint(publicKeyJwk) {
878
- assertCryptoAvailable();
879
- if (publicKeyJwk.kty !== "EC") {
880
- throw new DeviceIdentityError(
881
- `Expected JWK key type "EC" but received "${publicKeyJwk.kty ?? "undefined"}". Only ECDSA public keys are supported for device identity.`,
882
- { kty: publicKeyJwk.kty }
883
- );
776
+ }
777
+ async function computePublicKeyThumbprint(publicKeyJwk) {
778
+ assertCryptoAvailable();
779
+ if (publicKeyJwk.kty !== "EC") {
780
+ throw new DeviceIdentityError(
781
+ `Expected JWK key type "EC" but received "${publicKeyJwk.kty ?? "undefined"}". Only ECDSA public keys are supported for device identity.`,
782
+ { kty: publicKeyJwk.kty }
783
+ );
884
784
  }
885
785
  if (!publicKeyJwk.crv || !publicKeyJwk.x || !publicKeyJwk.y) {
886
786
  throw new DeviceIdentityError(
@@ -910,8 +810,8 @@ async function computePublicKeyThumbprint(publicKeyJwk) {
910
810
  }
911
811
 
912
812
  // src/device/device-store.ts
913
- var import_core4 = require("@korajs/core");
914
- var DeviceKeyStoreError = class extends import_core4.KoraError {
813
+ var import_core3 = require("@korajs/core");
814
+ var DeviceKeyStoreError = class extends import_core3.KoraError {
915
815
  constructor(message, context) {
916
816
  super(message, "DEVICE_KEY_STORE_ERROR", context);
917
817
  this.name = "DeviceKeyStoreError";
@@ -982,13 +882,72 @@ var IndexedDBDeviceKeyStore = class {
982
882
  try {
983
883
  const tx = db.transaction(IDB_STORE_NAME, "readwrite");
984
884
  const store = tx.objectStore(IDB_STORE_NAME);
985
- store.put(keyPair, deviceId);
885
+ store.put(keyPair, deviceId);
886
+ tx.oncomplete = () => {
887
+ resolve();
888
+ };
889
+ tx.onerror = () => {
890
+ reject(
891
+ new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
892
+ deviceId,
893
+ error: tx.error?.message
894
+ })
895
+ );
896
+ };
897
+ } catch (cause) {
898
+ reject(
899
+ new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
900
+ deviceId,
901
+ cause: cause instanceof Error ? cause.message : String(cause)
902
+ })
903
+ );
904
+ }
905
+ });
906
+ }
907
+ /** @inheritdoc */
908
+ async loadKeyPair(deviceId) {
909
+ const db = await this.openDatabase();
910
+ return new Promise((resolve, reject) => {
911
+ try {
912
+ const tx = db.transaction(IDB_STORE_NAME, "readonly");
913
+ const store = tx.objectStore(IDB_STORE_NAME);
914
+ const request = store.get(deviceId);
915
+ request.onsuccess = () => {
916
+ const result = request.result;
917
+ resolve(result ?? null);
918
+ };
919
+ request.onerror = () => {
920
+ reject(
921
+ new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
922
+ deviceId,
923
+ error: request.error?.message
924
+ })
925
+ );
926
+ };
927
+ } catch (cause) {
928
+ reject(
929
+ new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
930
+ deviceId,
931
+ cause: cause instanceof Error ? cause.message : String(cause)
932
+ })
933
+ );
934
+ }
935
+ });
936
+ }
937
+ /** @inheritdoc */
938
+ async deleteKeyPair(deviceId) {
939
+ const db = await this.openDatabase();
940
+ return new Promise((resolve, reject) => {
941
+ try {
942
+ const tx = db.transaction(IDB_STORE_NAME, "readwrite");
943
+ const store = tx.objectStore(IDB_STORE_NAME);
944
+ store.delete(deviceId);
986
945
  tx.oncomplete = () => {
987
946
  resolve();
988
947
  };
989
948
  tx.onerror = () => {
990
949
  reject(
991
- new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
950
+ new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
992
951
  deviceId,
993
952
  error: tx.error?.message
994
953
  })
@@ -996,7 +955,36 @@ var IndexedDBDeviceKeyStore = class {
996
955
  };
997
956
  } catch (cause) {
998
957
  reject(
999
- new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
958
+ new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
959
+ deviceId,
960
+ cause: cause instanceof Error ? cause.message : String(cause)
961
+ })
962
+ );
963
+ }
964
+ });
965
+ }
966
+ /** @inheritdoc */
967
+ async hasKeyPair(deviceId) {
968
+ const db = await this.openDatabase();
969
+ return new Promise((resolve, reject) => {
970
+ try {
971
+ const tx = db.transaction(IDB_STORE_NAME, "readonly");
972
+ const store = tx.objectStore(IDB_STORE_NAME);
973
+ const request = store.count(deviceId);
974
+ request.onsuccess = () => {
975
+ resolve(request.result > 0);
976
+ };
977
+ request.onerror = () => {
978
+ reject(
979
+ new DeviceKeyStoreError(
980
+ `Failed to check if key pair exists for device "${deviceId}".`,
981
+ { deviceId, error: request.error?.message }
982
+ )
983
+ );
984
+ };
985
+ } catch (cause) {
986
+ reject(
987
+ new DeviceKeyStoreError(`Failed to check if key pair exists for device "${deviceId}".`, {
1000
988
  deviceId,
1001
989
  cause: cause instanceof Error ? cause.message : String(cause)
1002
990
  })
@@ -1004,120 +992,517 @@ var IndexedDBDeviceKeyStore = class {
1004
992
  }
1005
993
  });
1006
994
  }
1007
- /** @inheritdoc */
1008
- async loadKeyPair(deviceId) {
1009
- const db = await this.openDatabase();
1010
- return new Promise((resolve, reject) => {
1011
- try {
1012
- const tx = db.transaction(IDB_STORE_NAME, "readonly");
1013
- const store = tx.objectStore(IDB_STORE_NAME);
1014
- const request = store.get(deviceId);
1015
- request.onsuccess = () => {
1016
- const result = request.result;
1017
- resolve(result ?? null);
1018
- };
1019
- request.onerror = () => {
1020
- reject(
1021
- new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
1022
- deviceId,
1023
- error: request.error?.message
1024
- })
1025
- );
1026
- };
1027
- } catch (cause) {
1028
- reject(
1029
- new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
1030
- deviceId,
1031
- cause: cause instanceof Error ? cause.message : String(cause)
1032
- })
1033
- );
1034
- }
995
+ };
996
+ var InMemoryDeviceKeyStore = class {
997
+ store = /* @__PURE__ */ new Map();
998
+ /** @inheritdoc */
999
+ async saveKeyPair(deviceId, keyPair) {
1000
+ this.store.set(deviceId, keyPair);
1001
+ }
1002
+ /** @inheritdoc */
1003
+ async loadKeyPair(deviceId) {
1004
+ return this.store.get(deviceId) ?? null;
1005
+ }
1006
+ /** @inheritdoc */
1007
+ async deleteKeyPair(deviceId) {
1008
+ this.store.delete(deviceId);
1009
+ }
1010
+ /** @inheritdoc */
1011
+ async hasKeyPair(deviceId) {
1012
+ return this.store.has(deviceId);
1013
+ }
1014
+ };
1015
+ function createDeviceKeyStore() {
1016
+ if (typeof globalThis.indexedDB !== "undefined") {
1017
+ return new IndexedDBDeviceKeyStore();
1018
+ }
1019
+ return new InMemoryDeviceKeyStore();
1020
+ }
1021
+
1022
+ // src/client/device-session.ts
1023
+ var DEFAULT_DEVICE_ID_KEY = "kora_auth_device_id";
1024
+ var AuthDeviceIdentityError = class extends import_core4.KoraError {
1025
+ constructor(message, context) {
1026
+ super(message, "AUTH_DEVICE_IDENTITY_ERROR", context);
1027
+ this.name = "AuthDeviceIdentityError";
1028
+ }
1029
+ };
1030
+ function createPersistentDeviceIdentity(options) {
1031
+ const storage = options.storage;
1032
+ const keyStore = options.keyStore ?? createDefaultPersistentKeyStore();
1033
+ const deviceIdKey = options.deviceIdKey ?? DEFAULT_DEVICE_ID_KEY;
1034
+ const generateDeviceId = options.generateDeviceId ?? defaultDeviceId;
1035
+ return {
1036
+ async getDeviceIdentity() {
1037
+ let deviceId = await storage.getItem(deviceIdKey);
1038
+ if (!deviceId) {
1039
+ deviceId = generateDeviceId();
1040
+ await storage.setItem(deviceIdKey, deviceId);
1041
+ }
1042
+ let keyPair = await keyStore.loadKeyPair(deviceId);
1043
+ if (!keyPair) {
1044
+ keyPair = await generateDeviceKeyPair();
1045
+ await keyStore.saveKeyPair(deviceId, keyPair);
1046
+ }
1047
+ const publicKey = await exportPublicKeyJwk(keyPair);
1048
+ return {
1049
+ deviceId,
1050
+ devicePublicKey: JSON.stringify(publicKey)
1051
+ };
1052
+ }
1053
+ };
1054
+ }
1055
+ function createDefaultPersistentKeyStore() {
1056
+ if (typeof globalThis.indexedDB !== "undefined") {
1057
+ return createDeviceKeyStore();
1058
+ }
1059
+ throw new AuthDeviceIdentityError(
1060
+ "No persistent device key store is available in this runtime. Pass `keyStore` to createPersistentDeviceIdentity()."
1061
+ );
1062
+ }
1063
+ function defaultDeviceId() {
1064
+ if (typeof globalThis.crypto?.randomUUID === "function") {
1065
+ return globalThis.crypto.randomUUID();
1066
+ }
1067
+ const bytes = new Uint8Array(16);
1068
+ if (typeof globalThis.crypto?.getRandomValues === "function") {
1069
+ globalThis.crypto.getRandomValues(bytes);
1070
+ } else {
1071
+ for (let i = 0; i < bytes.length; i++) {
1072
+ bytes[i] = Math.floor(Math.random() * 256);
1073
+ }
1074
+ }
1075
+ bytes[6] = bytes[6] & 15 | 64;
1076
+ bytes[8] = bytes[8] & 63 | 128;
1077
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1078
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1079
+ }
1080
+
1081
+ // src/client/storage.ts
1082
+ function createAuthTokenStorage(options) {
1083
+ const prefix = options.prefix ?? "kora_auth";
1084
+ const accessKey = `${prefix}_access_token`;
1085
+ const refreshKey = `${prefix}_refresh_token`;
1086
+ const store = options.store;
1087
+ return {
1088
+ getAccessToken: () => store.getItem(accessKey),
1089
+ getRefreshToken: () => store.getItem(refreshKey),
1090
+ async setTokens(accessToken, refreshToken) {
1091
+ await store.setItem(accessKey, accessToken);
1092
+ await store.setItem(refreshKey, refreshToken);
1093
+ },
1094
+ async clear() {
1095
+ await store.removeItem(accessKey);
1096
+ await store.removeItem(refreshKey);
1097
+ }
1098
+ };
1099
+ }
1100
+ function createMemoryAuthTokenStorage() {
1101
+ let accessToken = null;
1102
+ let refreshToken = null;
1103
+ return {
1104
+ getAccessToken: () => accessToken,
1105
+ getRefreshToken: () => refreshToken,
1106
+ setTokens(access, refresh) {
1107
+ accessToken = access;
1108
+ refreshToken = refresh;
1109
+ },
1110
+ clear() {
1111
+ accessToken = null;
1112
+ refreshToken = null;
1113
+ }
1114
+ };
1115
+ }
1116
+ function createWebStorageAuthTokenStorage(storage, prefix) {
1117
+ return createAuthTokenStorage({
1118
+ prefix,
1119
+ store: {
1120
+ getItem: (key) => storage.getItem(key),
1121
+ setItem: (key, value) => {
1122
+ storage.setItem(key, value);
1123
+ },
1124
+ removeItem: (key) => {
1125
+ storage.removeItem(key);
1126
+ }
1127
+ }
1128
+ });
1129
+ }
1130
+
1131
+ // src/client/quickstart.ts
1132
+ function createKoraAuth(options) {
1133
+ const storage = options.storage ?? (options.credentialStore ? createAuthTokenStorage({
1134
+ store: options.credentialStore,
1135
+ prefix: options.storageKey
1136
+ }) : tryCreateDefaultTokenStorage(options.storageKey));
1137
+ const deviceIdentity = options.deviceIdentity === false ? void 0 : options.deviceIdentity ?? tryCreateDefaultDeviceIdentity(options.credentialStore, options.deviceKeyStore);
1138
+ return new AuthClient({
1139
+ serverUrl: options.serverUrl,
1140
+ storageKey: options.storageKey,
1141
+ storage,
1142
+ fetch: options.fetch,
1143
+ deviceIdentity
1144
+ });
1145
+ }
1146
+ function tryCreateDefaultTokenStorage(storageKey) {
1147
+ const storage = tryGetBrowserStorage();
1148
+ return storage ? createWebStorageAuthTokenStorage(storage, storageKey) : void 0;
1149
+ }
1150
+ function tryCreateDefaultDeviceIdentity(credentialStore, deviceKeyStore) {
1151
+ const storage = credentialStore ?? tryGetBrowserStorage();
1152
+ if (!storage) {
1153
+ return void 0;
1154
+ }
1155
+ try {
1156
+ return createPersistentDeviceIdentity({
1157
+ storage,
1158
+ keyStore: deviceKeyStore
1159
+ });
1160
+ } catch {
1161
+ return void 0;
1162
+ }
1163
+ }
1164
+ function tryGetBrowserStorage() {
1165
+ try {
1166
+ if (typeof globalThis.localStorage === "undefined") {
1167
+ return null;
1168
+ }
1169
+ const key = "__kora_auth_quickstart_test__";
1170
+ globalThis.localStorage.setItem(key, "1");
1171
+ globalThis.localStorage.removeItem(key);
1172
+ return globalThis.localStorage;
1173
+ } catch {
1174
+ return null;
1175
+ }
1176
+ }
1177
+
1178
+ // src/client/auth-sync.ts
1179
+ var import_core5 = require("@korajs/core");
1180
+ function decodeJwtPayload2(token) {
1181
+ const parts = token.split(".");
1182
+ if (parts.length !== 3) {
1183
+ return null;
1184
+ }
1185
+ const payloadSegment = parts[1];
1186
+ if (payloadSegment === void 0) {
1187
+ return null;
1188
+ }
1189
+ try {
1190
+ const base64 = payloadSegment.replace(/-/g, "+").replace(/_/g, "/");
1191
+ const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
1192
+ const json = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf-8");
1193
+ const parsed = JSON.parse(json);
1194
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1195
+ return null;
1196
+ }
1197
+ return parsed;
1198
+ } catch {
1199
+ return null;
1200
+ }
1201
+ }
1202
+ function readDeviceIdFromClaims(claims) {
1203
+ const dev = claims.dev;
1204
+ return typeof dev === "string" && dev.length > 0 ? dev : void 0;
1205
+ }
1206
+ function createKoraAuthSync(options) {
1207
+ const { authClient, schema, scopeFromClaims } = options;
1208
+ const binding = {
1209
+ auth: async () => {
1210
+ const token = await authClient.getAccessToken();
1211
+ return { token: token ?? "" };
1212
+ }
1213
+ };
1214
+ if (schema) {
1215
+ binding.resolveScopeMap = async () => {
1216
+ const token = await authClient.getAccessToken();
1217
+ if (!token) {
1218
+ return void 0;
1219
+ }
1220
+ const claims = decodeJwtPayload2(token);
1221
+ if (!claims) {
1222
+ return void 0;
1223
+ }
1224
+ const scopeValues = scopeFromClaims ? scopeFromClaims(claims) : (0, import_core5.extractScopeValuesFromClaims)(schema, claims);
1225
+ return (0, import_core5.buildScopeMap)(schema, scopeValues);
1226
+ };
1227
+ }
1228
+ binding.resolveNodeId = async () => {
1229
+ const token = await authClient.getAccessToken();
1230
+ if (!token) {
1231
+ return void 0;
1232
+ }
1233
+ const claims = decodeJwtPayload2(token);
1234
+ if (!claims) {
1235
+ return void 0;
1236
+ }
1237
+ return readDeviceIdFromClaims(claims);
1238
+ };
1239
+ if (authClient.onAuthChange) {
1240
+ binding.subscribe = (listener) => authClient.onAuthChange?.(() => listener()) ?? (() => {
1241
+ });
1242
+ }
1243
+ return binding;
1244
+ }
1245
+
1246
+ // src/client/org-client.ts
1247
+ var import_core6 = require("@korajs/core");
1248
+ var OrgClientError = class extends import_core6.KoraError {
1249
+ constructor(message, code, context) {
1250
+ super(message, code, context);
1251
+ this.name = "OrgClientError";
1252
+ }
1253
+ };
1254
+ var OrgClient = class {
1255
+ serverUrl;
1256
+ getAccessToken;
1257
+ listeners = /* @__PURE__ */ new Set();
1258
+ _activeOrgId = null;
1259
+ _activeOrg = null;
1260
+ _activeRole = null;
1261
+ constructor(config) {
1262
+ this.serverUrl = config.serverUrl.replace(/\/+$/, "");
1263
+ this.getAccessToken = config.getAccessToken;
1264
+ }
1265
+ // --- Getters ---
1266
+ /** Currently active organization ID */
1267
+ get activeOrgId() {
1268
+ return this._activeOrgId;
1269
+ }
1270
+ /** Currently active organization */
1271
+ get activeOrg() {
1272
+ return this._activeOrg;
1273
+ }
1274
+ /** Current user's role in the active organization */
1275
+ get activeRole() {
1276
+ return this._activeRole;
1277
+ }
1278
+ // --- Organization Operations ---
1279
+ /**
1280
+ * Create a new organization.
1281
+ */
1282
+ async createOrg(params) {
1283
+ return this.request("/orgs", {
1284
+ method: "POST",
1285
+ body: params
1286
+ });
1287
+ }
1288
+ /**
1289
+ * List all organizations the current user belongs to.
1290
+ */
1291
+ async listOrgs() {
1292
+ return this.request("/orgs", { method: "GET" });
1293
+ }
1294
+ /**
1295
+ * Get an organization by ID.
1296
+ */
1297
+ async getOrg(orgId) {
1298
+ return this.request(`/orgs/${orgId}`, { method: "GET" });
1299
+ }
1300
+ /**
1301
+ * Update an organization.
1302
+ */
1303
+ async updateOrg(orgId, params) {
1304
+ const result = await this.request(`/orgs/${orgId}`, {
1305
+ method: "PATCH",
1306
+ body: params
1307
+ });
1308
+ if (this._activeOrgId === orgId) {
1309
+ this._activeOrg = result;
1310
+ }
1311
+ return result;
1312
+ }
1313
+ /**
1314
+ * Delete an organization.
1315
+ */
1316
+ async deleteOrg(orgId) {
1317
+ await this.request(`/orgs/${orgId}`, { method: "DELETE" });
1318
+ if (this._activeOrgId === orgId) {
1319
+ this._activeOrgId = null;
1320
+ this._activeOrg = null;
1321
+ this._activeRole = null;
1322
+ this.notifyListeners();
1323
+ }
1324
+ }
1325
+ // --- Org Switching ---
1326
+ /**
1327
+ * Switch the active organization context.
1328
+ * Fetches the org details and the user's membership/role.
1329
+ */
1330
+ async switchOrg(orgId) {
1331
+ const org = await this.request(`/orgs/${orgId}`, { method: "GET" });
1332
+ const membership = await this.request(`/orgs/${orgId}/membership`, {
1333
+ method: "GET"
1334
+ });
1335
+ this._activeOrgId = orgId;
1336
+ this._activeOrg = org;
1337
+ this._activeRole = membership.role;
1338
+ this.notifyListeners();
1339
+ }
1340
+ /**
1341
+ * Clear the active organization (no org selected).
1342
+ */
1343
+ clearActiveOrg() {
1344
+ this._activeOrgId = null;
1345
+ this._activeOrg = null;
1346
+ this._activeRole = null;
1347
+ this.notifyListeners();
1348
+ }
1349
+ // --- Member Management ---
1350
+ /**
1351
+ * List members of an organization.
1352
+ */
1353
+ async listMembers(orgId) {
1354
+ return this.request(`/orgs/${orgId}/members`, { method: "GET" });
1355
+ }
1356
+ /**
1357
+ * Remove a member from an organization.
1358
+ */
1359
+ async removeMember(orgId, userId) {
1360
+ await this.request(`/orgs/${orgId}/members/${userId}`, { method: "DELETE" });
1361
+ }
1362
+ /**
1363
+ * Update a member's role.
1364
+ */
1365
+ async updateMemberRole(orgId, userId, role) {
1366
+ return this.request(`/orgs/${orgId}/members/${userId}/role`, {
1367
+ method: "PATCH",
1368
+ body: { role }
1369
+ });
1370
+ }
1371
+ /**
1372
+ * Transfer ownership to another member.
1373
+ */
1374
+ async transferOwnership(orgId, newOwnerId) {
1375
+ await this.request(`/orgs/${orgId}/transfer`, {
1376
+ method: "POST",
1377
+ body: { newOwnerId }
1035
1378
  });
1036
1379
  }
1037
- /** @inheritdoc */
1038
- async deleteKeyPair(deviceId) {
1039
- const db = await this.openDatabase();
1040
- return new Promise((resolve, reject) => {
1041
- try {
1042
- const tx = db.transaction(IDB_STORE_NAME, "readwrite");
1043
- const store = tx.objectStore(IDB_STORE_NAME);
1044
- store.delete(deviceId);
1045
- tx.oncomplete = () => {
1046
- resolve();
1047
- };
1048
- tx.onerror = () => {
1049
- reject(
1050
- new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
1051
- deviceId,
1052
- error: tx.error?.message
1053
- })
1054
- );
1055
- };
1056
- } catch (cause) {
1057
- reject(
1058
- new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
1059
- deviceId,
1060
- cause: cause instanceof Error ? cause.message : String(cause)
1061
- })
1062
- );
1063
- }
1380
+ /**
1381
+ * Leave an organization (remove yourself).
1382
+ */
1383
+ async leaveOrg(orgId) {
1384
+ await this.request(`/orgs/${orgId}/leave`, { method: "POST" });
1385
+ if (this._activeOrgId === orgId) {
1386
+ this._activeOrgId = null;
1387
+ this._activeOrg = null;
1388
+ this._activeRole = null;
1389
+ this.notifyListeners();
1390
+ }
1391
+ }
1392
+ // --- Invitations ---
1393
+ /**
1394
+ * Invite a user to an organization by email.
1395
+ */
1396
+ async inviteMember(orgId, params) {
1397
+ return this.request(`/orgs/${orgId}/invitations`, {
1398
+ method: "POST",
1399
+ body: params
1064
1400
  });
1065
1401
  }
1066
- /** @inheritdoc */
1067
- async hasKeyPair(deviceId) {
1068
- const db = await this.openDatabase();
1069
- return new Promise((resolve, reject) => {
1070
- try {
1071
- const tx = db.transaction(IDB_STORE_NAME, "readonly");
1072
- const store = tx.objectStore(IDB_STORE_NAME);
1073
- const request = store.count(deviceId);
1074
- request.onsuccess = () => {
1075
- resolve(request.result > 0);
1076
- };
1077
- request.onerror = () => {
1078
- reject(
1079
- new DeviceKeyStoreError(
1080
- `Failed to check if key pair exists for device "${deviceId}".`,
1081
- { deviceId, error: request.error?.message }
1082
- )
1083
- );
1084
- };
1085
- } catch (cause) {
1086
- reject(
1087
- new DeviceKeyStoreError(`Failed to check if key pair exists for device "${deviceId}".`, {
1088
- deviceId,
1089
- cause: cause instanceof Error ? cause.message : String(cause)
1090
- })
1091
- );
1092
- }
1402
+ /**
1403
+ * Accept an invitation by token.
1404
+ */
1405
+ async acceptInvitation(token) {
1406
+ return this.request("/invitations/accept", {
1407
+ method: "POST",
1408
+ body: { token }
1093
1409
  });
1094
1410
  }
1095
- };
1096
- var InMemoryDeviceKeyStore = class {
1097
- store = /* @__PURE__ */ new Map();
1098
- /** @inheritdoc */
1099
- async saveKeyPair(deviceId, keyPair) {
1100
- this.store.set(deviceId, keyPair);
1411
+ /**
1412
+ * List pending invitations for an organization.
1413
+ */
1414
+ async listInvitations(orgId) {
1415
+ return this.request(`/orgs/${orgId}/invitations`, { method: "GET" });
1101
1416
  }
1102
- /** @inheritdoc */
1103
- async loadKeyPair(deviceId) {
1104
- return this.store.get(deviceId) ?? null;
1417
+ /**
1418
+ * Revoke a pending invitation.
1419
+ */
1420
+ async revokeInvitation(orgId, invitationId) {
1421
+ await this.request(`/orgs/${orgId}/invitations/${invitationId}`, { method: "DELETE" });
1105
1422
  }
1106
- /** @inheritdoc */
1107
- async deleteKeyPair(deviceId) {
1108
- this.store.delete(deviceId);
1423
+ /**
1424
+ * List pending invitations for the current user's email.
1425
+ */
1426
+ async listMyInvitations(email) {
1427
+ return this.request(`/invitations?email=${encodeURIComponent(email)}`, {
1428
+ method: "GET"
1429
+ });
1109
1430
  }
1110
- /** @inheritdoc */
1111
- async hasKeyPair(deviceId) {
1112
- return this.store.has(deviceId);
1431
+ // --- Subscriptions ---
1432
+ /**
1433
+ * Subscribe to active org changes.
1434
+ * @returns Unsubscribe function
1435
+ */
1436
+ onOrgChange(callback) {
1437
+ this.listeners.add(callback);
1438
+ return () => {
1439
+ this.listeners.delete(callback);
1440
+ };
1113
1441
  }
1114
- };
1115
- function createDeviceKeyStore() {
1116
- if (typeof globalThis.indexedDB !== "undefined") {
1117
- return new IndexedDBDeviceKeyStore();
1442
+ // --- Internal ---
1443
+ notifyListeners() {
1444
+ for (const listener of this.listeners) {
1445
+ try {
1446
+ listener(this._activeOrgId);
1447
+ } catch {
1448
+ }
1449
+ }
1118
1450
  }
1119
- return new InMemoryDeviceKeyStore();
1120
- }
1451
+ async request(path, options) {
1452
+ const token = await this.getAccessToken();
1453
+ if (!token) {
1454
+ throw new OrgClientError(
1455
+ "Not authenticated. Sign in before performing organization operations.",
1456
+ "ORG_NOT_AUTHENTICATED"
1457
+ );
1458
+ }
1459
+ const url = `${this.serverUrl}${path}`;
1460
+ const headers = {
1461
+ Authorization: `Bearer ${token}`
1462
+ };
1463
+ if (options.body) {
1464
+ headers["Content-Type"] = "application/json";
1465
+ }
1466
+ let response;
1467
+ try {
1468
+ response = await fetch(url, {
1469
+ method: options.method,
1470
+ headers,
1471
+ body: options.body ? JSON.stringify(options.body) : void 0
1472
+ });
1473
+ } catch (cause) {
1474
+ throw new OrgClientError(`Network request to ${path} failed.`, "ORG_NETWORK_ERROR", {
1475
+ path,
1476
+ cause: cause instanceof Error ? cause.message : String(cause)
1477
+ });
1478
+ }
1479
+ if (!response.ok) {
1480
+ let errorMessage = `Server returned HTTP ${response.status}`;
1481
+ try {
1482
+ const body = await response.json();
1483
+ if (typeof body.error === "string") {
1484
+ errorMessage = body.error;
1485
+ }
1486
+ } catch {
1487
+ }
1488
+ throw new OrgClientError(errorMessage, "ORG_SERVER_ERROR", {
1489
+ path,
1490
+ status: response.status
1491
+ });
1492
+ }
1493
+ const text = await response.text();
1494
+ if (text.length === 0) return void 0;
1495
+ try {
1496
+ const body = JSON.parse(text);
1497
+ if (body && typeof body === "object" && "data" in body) {
1498
+ return body.data;
1499
+ }
1500
+ return body;
1501
+ } catch {
1502
+ return void 0;
1503
+ }
1504
+ }
1505
+ };
1121
1506
 
1122
1507
  // src/tokens/token-store.ts
1123
1508
  var DEFAULT_STORAGE_KEY = "kora_auth_tokens";
@@ -1242,17 +1627,17 @@ var TokenStore = class {
1242
1627
  };
1243
1628
 
1244
1629
  // src/tokens/encrypted-token-store.ts
1245
- var import_core6 = require("@korajs/core");
1630
+ var import_core8 = require("@korajs/core");
1246
1631
 
1247
1632
  // src/encryption/database-encryption.ts
1248
- var import_core5 = require("@korajs/core");
1249
- var EncryptionError = class extends import_core5.KoraError {
1633
+ var import_core7 = require("@korajs/core");
1634
+ var EncryptionError = class extends import_core7.KoraError {
1250
1635
  constructor(message, context) {
1251
1636
  super(message, "ENCRYPTION_ERROR", context);
1252
1637
  this.name = "EncryptionError";
1253
1638
  }
1254
1639
  };
1255
- var CryptoUnavailableError2 = class extends import_core5.KoraError {
1640
+ var CryptoUnavailableError2 = class extends import_core7.KoraError {
1256
1641
  constructor() {
1257
1642
  super(
1258
1643
  "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.",
@@ -1360,7 +1745,7 @@ async function importKey(rawKey) {
1360
1745
  }
1361
1746
 
1362
1747
  // src/tokens/encrypted-token-store.ts
1363
- var EncryptedTokenStoreError = class extends import_core6.KoraError {
1748
+ var EncryptedTokenStoreError = class extends import_core8.KoraError {
1364
1749
  constructor(message, context) {
1365
1750
  super(message, "ENCRYPTED_TOKEN_STORE_ERROR", context);
1366
1751
  this.name = "EncryptedTokenStoreError";
@@ -1555,14 +1940,14 @@ var EncryptedTokenStore = class {
1555
1940
  };
1556
1941
 
1557
1942
  // src/passkey/passkey-client.ts
1558
- var import_core7 = require("@korajs/core");
1559
- var PasskeyError = class extends import_core7.KoraError {
1943
+ var import_core9 = require("@korajs/core");
1944
+ var PasskeyError = class extends import_core9.KoraError {
1560
1945
  constructor(message, context) {
1561
1946
  super(message, "PASSKEY_ERROR", context);
1562
1947
  this.name = "PasskeyError";
1563
1948
  }
1564
1949
  };
1565
- var PasskeyUnsupportedError = class extends import_core7.KoraError {
1950
+ var PasskeyUnsupportedError = class extends import_core9.KoraError {
1566
1951
  constructor() {
1567
1952
  super(
1568
1953
  "WebAuthn is not supported in this browser. Passkey authentication requires a modern browser with WebAuthn support.",
@@ -1834,8 +2219,8 @@ function decodeCbor(data, offset) {
1834
2219
  }
1835
2220
 
1836
2221
  // src/encryption/key-derivation.ts
1837
- var import_core8 = require("@korajs/core");
1838
- var KeyDerivationError = class extends import_core8.KoraError {
2222
+ var import_core10 = require("@korajs/core");
2223
+ var KeyDerivationError = class extends import_core10.KoraError {
1839
2224
  constructor(message, context) {
1840
2225
  super(message, "KEY_DERIVATION_ERROR", context);
1841
2226
  this.name = "KeyDerivationError";
@@ -2015,10 +2400,10 @@ var AutoLockManager = class {
2015
2400
  };
2016
2401
 
2017
2402
  // src/encryption/operation-encryptor.ts
2018
- var import_core9 = require("@korajs/core");
2403
+ var import_core11 = require("@korajs/core");
2019
2404
  var ENCRYPTED_MARKER = "__kora_encrypted";
2020
2405
  var ENCRYPTION_VERSION = 1;
2021
- var OperationEncryptionError = class extends import_core9.KoraError {
2406
+ var OperationEncryptionError = class extends import_core11.KoraError {
2022
2407
  constructor(message, context) {
2023
2408
  super(message, "OPERATION_ENCRYPTION_ERROR", context);
2024
2409
  this.name = "OperationEncryptionError";
@@ -2214,6 +2599,7 @@ function isEncryptedEnvelope(field) {
2214
2599
  // Annotate the CommonJS export names for ESM import in node:
2215
2600
  0 && (module.exports = {
2216
2601
  AuthClient,
2602
+ AuthDeviceIdentityError,
2217
2603
  AuthError,
2218
2604
  AutoLockManager,
2219
2605
  CryptoUnavailableError,
@@ -2234,8 +2620,14 @@ function isEncryptedEnvelope(field) {
2234
2620
  TokenStore,
2235
2621
  authenticateWithPasskey,
2236
2622
  computePublicKeyThumbprint,
2623
+ createAuthTokenStorage,
2237
2624
  createDeviceKeyStore,
2625
+ createKoraAuth,
2626
+ createKoraAuthSync,
2627
+ createMemoryAuthTokenStorage,
2238
2628
  createPasskeyCredential,
2629
+ createPersistentDeviceIdentity,
2630
+ createWebStorageAuthTokenStorage,
2239
2631
  decryptData,
2240
2632
  deriveEncryptionKey,
2241
2633
  encryptData,