@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/README.md +81 -28
- package/dist/index.cjs +915 -523
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -118
- package/dist/index.d.ts +77 -118
- package/dist/index.js +646 -258
- package/dist/index.js.map +1 -1
- package/dist/org-client-q2u55qod.d.cts +665 -0
- package/dist/org-client-q2u55qod.d.ts +665 -0
- package/dist/react.cjs +79 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +17 -1
- package/dist/react.d.ts +17 -1
- package/dist/react.js +79 -0
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +1611 -730
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +415 -224
- package/dist/server.d.ts +415 -224
- package/dist/server.js +1565 -704
- package/dist/server.js.map +1 -1
- package/package.json +5 -5
- package/dist/org-client-BVTLKcIk.d.cts +0 -355
- package/dist/org-client-BVTLKcIk.d.ts +0 -355
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
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
|
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/
|
|
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
|
|
493
|
-
constructor(
|
|
494
|
-
super(
|
|
495
|
-
|
|
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
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
914
|
-
var DeviceKeyStoreError = class extends
|
|
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
|
|
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
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
-
/**
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
-
/**
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
-
/**
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
/**
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1442
|
+
// --- Internal ---
|
|
1443
|
+
notifyListeners() {
|
|
1444
|
+
for (const listener of this.listeners) {
|
|
1445
|
+
try {
|
|
1446
|
+
listener(this._activeOrgId);
|
|
1447
|
+
} catch {
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1118
1450
|
}
|
|
1119
|
-
|
|
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
|
|
1630
|
+
var import_core8 = require("@korajs/core");
|
|
1246
1631
|
|
|
1247
1632
|
// src/encryption/database-encryption.ts
|
|
1248
|
-
var
|
|
1249
|
-
var EncryptionError = class extends
|
|
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
|
|
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
|
|
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
|
|
1559
|
-
var PasskeyError = class extends
|
|
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
|
|
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
|
|
1838
|
-
var KeyDerivationError = class extends
|
|
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
|
|
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
|
|
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,
|