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