@korajs/auth 0.1.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 +156 -0
- package/dist/auth-client-CrDNuh10.d.cts +181 -0
- package/dist/auth-client-CrDNuh10.d.ts +181 -0
- package/dist/chunk-L554ZDPY.js +174 -0
- package/dist/chunk-L554ZDPY.js.map +1 -0
- package/dist/device-identity-DiwdLsUB.d.cts +247 -0
- package/dist/device-identity-DiwdLsUB.d.ts +247 -0
- package/dist/index.cjs +740 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +552 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +206 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +189 -0
- package/dist/react.d.ts +189 -0
- package/dist/react.js +175 -0
- package/dist/react.js.map +1 -0
- package/dist/server.cjs +1040 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +780 -0
- package/dist/server.d.ts +780 -0
- package/dist/server.js +890 -0
- package/dist/server.js.map +1 -0
- package/package.json +74 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AuthClient: () => AuthClient,
|
|
24
|
+
AuthError: () => AuthError,
|
|
25
|
+
CryptoUnavailableError: () => CryptoUnavailableError,
|
|
26
|
+
DeviceIdentityError: () => DeviceIdentityError,
|
|
27
|
+
TokenStore: () => TokenStore,
|
|
28
|
+
computePublicKeyThumbprint: () => computePublicKeyThumbprint,
|
|
29
|
+
exportPublicKeyJwk: () => exportPublicKeyJwk,
|
|
30
|
+
fromBase64Url: () => fromBase64Url,
|
|
31
|
+
generateDeviceKeyPair: () => generateDeviceKeyPair,
|
|
32
|
+
signChallenge: () => signChallenge,
|
|
33
|
+
toBase64Url: () => toBase64Url,
|
|
34
|
+
verifyChallenge: () => verifyChallenge
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/client/auth-client.ts
|
|
39
|
+
var import_core = require("@korajs/core");
|
|
40
|
+
var AuthError = class extends import_core.KoraError {
|
|
41
|
+
constructor(message, code, context) {
|
|
42
|
+
super(message, code, context);
|
|
43
|
+
this.name = "AuthError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var EXPIRY_BUFFER_SECONDS = 30;
|
|
47
|
+
function decodeJwtPayload(token) {
|
|
48
|
+
const parts = token.split(".");
|
|
49
|
+
if (parts.length !== 3) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
54
|
+
const json = atob(base64);
|
|
55
|
+
return JSON.parse(json);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isTokenExpired(token) {
|
|
61
|
+
const payload = decodeJwtPayload(token);
|
|
62
|
+
if (!payload || typeof payload["exp"] !== "number") {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
66
|
+
return payload["exp"] <= nowSeconds + EXPIRY_BUFFER_SECONDS;
|
|
67
|
+
}
|
|
68
|
+
function getUserIdFromToken(token) {
|
|
69
|
+
const payload = decodeJwtPayload(token);
|
|
70
|
+
if (!payload || typeof payload["sub"] !== "string") {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return payload["sub"];
|
|
74
|
+
}
|
|
75
|
+
function createTokenStorage(prefix) {
|
|
76
|
+
let useLocalStorage = false;
|
|
77
|
+
try {
|
|
78
|
+
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
|
79
|
+
const testKey = `${prefix}_test`;
|
|
80
|
+
window.localStorage.setItem(testKey, "1");
|
|
81
|
+
window.localStorage.removeItem(testKey);
|
|
82
|
+
useLocalStorage = true;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
if (useLocalStorage) {
|
|
87
|
+
const accessKey = `${prefix}_access_token`;
|
|
88
|
+
const refreshKey = `${prefix}_refresh_token`;
|
|
89
|
+
return {
|
|
90
|
+
getAccessToken() {
|
|
91
|
+
return window.localStorage.getItem(accessKey);
|
|
92
|
+
},
|
|
93
|
+
getRefreshToken() {
|
|
94
|
+
return window.localStorage.getItem(refreshKey);
|
|
95
|
+
},
|
|
96
|
+
setTokens(access, refresh) {
|
|
97
|
+
window.localStorage.setItem(accessKey, access);
|
|
98
|
+
window.localStorage.setItem(refreshKey, refresh);
|
|
99
|
+
},
|
|
100
|
+
clear() {
|
|
101
|
+
window.localStorage.removeItem(accessKey);
|
|
102
|
+
window.localStorage.removeItem(refreshKey);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
let accessToken = null;
|
|
107
|
+
let refreshToken = null;
|
|
108
|
+
return {
|
|
109
|
+
getAccessToken() {
|
|
110
|
+
return accessToken;
|
|
111
|
+
},
|
|
112
|
+
getRefreshToken() {
|
|
113
|
+
return refreshToken;
|
|
114
|
+
},
|
|
115
|
+
setTokens(access, refresh) {
|
|
116
|
+
accessToken = access;
|
|
117
|
+
refreshToken = refresh;
|
|
118
|
+
},
|
|
119
|
+
clear() {
|
|
120
|
+
accessToken = null;
|
|
121
|
+
refreshToken = null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
var AuthClient = class {
|
|
126
|
+
serverUrl;
|
|
127
|
+
storage;
|
|
128
|
+
listeners = /* @__PURE__ */ new Set();
|
|
129
|
+
_state = "loading";
|
|
130
|
+
_user = null;
|
|
131
|
+
_refreshPromise = null;
|
|
132
|
+
/**
|
|
133
|
+
* Creates a new AuthClient.
|
|
134
|
+
*
|
|
135
|
+
* @param config - Auth client configuration
|
|
136
|
+
*/
|
|
137
|
+
constructor(config) {
|
|
138
|
+
this.serverUrl = config.serverUrl.replace(/\/+$/, "");
|
|
139
|
+
const prefix = config.storageKey ?? "kora_auth";
|
|
140
|
+
this.storage = createTokenStorage(prefix);
|
|
141
|
+
}
|
|
142
|
+
// -----------------------------------------------------------------------
|
|
143
|
+
// Public getters
|
|
144
|
+
// -----------------------------------------------------------------------
|
|
145
|
+
/** Current authentication state. */
|
|
146
|
+
get state() {
|
|
147
|
+
return this._state;
|
|
148
|
+
}
|
|
149
|
+
/** Current authenticated user, or null if not signed in. */
|
|
150
|
+
get currentUser() {
|
|
151
|
+
return this._user;
|
|
152
|
+
}
|
|
153
|
+
/** Whether the user is currently authenticated. */
|
|
154
|
+
get isAuthenticated() {
|
|
155
|
+
return this._state === "authenticated";
|
|
156
|
+
}
|
|
157
|
+
// -----------------------------------------------------------------------
|
|
158
|
+
// Initialization
|
|
159
|
+
// -----------------------------------------------------------------------
|
|
160
|
+
/**
|
|
161
|
+
* Initialize the auth client by restoring a session from stored tokens.
|
|
162
|
+
*
|
|
163
|
+
* Loads tokens from storage, validates the access token, and attempts a
|
|
164
|
+
* refresh if the access token is expired but a refresh token is available.
|
|
165
|
+
* Safe to call multiple times -- subsequent calls are no-ops once initialized.
|
|
166
|
+
*/
|
|
167
|
+
async initialize() {
|
|
168
|
+
const accessToken = this.storage.getAccessToken();
|
|
169
|
+
const refreshToken = this.storage.getRefreshToken();
|
|
170
|
+
if (!accessToken || !refreshToken) {
|
|
171
|
+
this.setState("unauthenticated", null);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!isTokenExpired(accessToken)) {
|
|
175
|
+
await this.restoreSession(accessToken);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const newAccessToken = await this.refreshAccessToken(refreshToken);
|
|
180
|
+
if (newAccessToken) {
|
|
181
|
+
await this.restoreSession(newAccessToken);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
this.storage.clear();
|
|
187
|
+
this.setState("unauthenticated", null);
|
|
188
|
+
}
|
|
189
|
+
// -----------------------------------------------------------------------
|
|
190
|
+
// Sign up / Sign in / Sign out
|
|
191
|
+
// -----------------------------------------------------------------------
|
|
192
|
+
/**
|
|
193
|
+
* Register a new user account.
|
|
194
|
+
*
|
|
195
|
+
* @param params - Sign-up credentials
|
|
196
|
+
* @returns The newly created AuthUser
|
|
197
|
+
* @throws {AuthError} If the request fails or the server returns an error
|
|
198
|
+
*/
|
|
199
|
+
async signUp(params) {
|
|
200
|
+
const response = await this.request("/auth/signup", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
body: params
|
|
203
|
+
});
|
|
204
|
+
this.storage.setTokens(response.accessToken, response.refreshToken);
|
|
205
|
+
const user = await this.fetchUserProfile(response.accessToken);
|
|
206
|
+
this.setState("authenticated", user);
|
|
207
|
+
return user;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Sign in with email and password.
|
|
211
|
+
*
|
|
212
|
+
* @param params - Sign-in credentials
|
|
213
|
+
* @returns The authenticated AuthUser
|
|
214
|
+
* @throws {AuthError} If the credentials are invalid or the request fails
|
|
215
|
+
*/
|
|
216
|
+
async signIn(params) {
|
|
217
|
+
const response = await this.request("/auth/signin", {
|
|
218
|
+
method: "POST",
|
|
219
|
+
body: params
|
|
220
|
+
});
|
|
221
|
+
this.storage.setTokens(response.accessToken, response.refreshToken);
|
|
222
|
+
const user = await this.fetchUserProfile(response.accessToken);
|
|
223
|
+
this.setState("authenticated", user);
|
|
224
|
+
return user;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Sign out the current user.
|
|
228
|
+
*
|
|
229
|
+
* Clears local tokens and state. Does not make a network request to the
|
|
230
|
+
* server -- tokens are simply discarded locally.
|
|
231
|
+
*/
|
|
232
|
+
async signOut() {
|
|
233
|
+
this.storage.clear();
|
|
234
|
+
this._refreshPromise = null;
|
|
235
|
+
this.setState("unauthenticated", null);
|
|
236
|
+
}
|
|
237
|
+
// -----------------------------------------------------------------------
|
|
238
|
+
// Token access
|
|
239
|
+
// -----------------------------------------------------------------------
|
|
240
|
+
/**
|
|
241
|
+
* Get a valid access token, automatically refreshing if expired.
|
|
242
|
+
*
|
|
243
|
+
* @returns A valid access token string, or null if the user is not
|
|
244
|
+
* authenticated and refresh is not possible
|
|
245
|
+
*/
|
|
246
|
+
async getAccessToken() {
|
|
247
|
+
const accessToken = this.storage.getAccessToken();
|
|
248
|
+
if (accessToken && !isTokenExpired(accessToken)) {
|
|
249
|
+
return accessToken;
|
|
250
|
+
}
|
|
251
|
+
const refreshToken = this.storage.getRefreshToken();
|
|
252
|
+
if (!refreshToken) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const newAccessToken = await this.refreshAccessToken(refreshToken);
|
|
257
|
+
return newAccessToken;
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get a valid token for the sync engine handshake.
|
|
264
|
+
* Alias for {@link getAccessToken}.
|
|
265
|
+
*
|
|
266
|
+
* @returns A valid access token string, or null if unavailable
|
|
267
|
+
*/
|
|
268
|
+
async getSyncToken() {
|
|
269
|
+
return this.getAccessToken();
|
|
270
|
+
}
|
|
271
|
+
// -----------------------------------------------------------------------
|
|
272
|
+
// State change subscriptions
|
|
273
|
+
// -----------------------------------------------------------------------
|
|
274
|
+
/**
|
|
275
|
+
* Subscribe to authentication state changes.
|
|
276
|
+
*
|
|
277
|
+
* The callback is invoked whenever the auth state transitions (e.g., from
|
|
278
|
+
* 'unauthenticated' to 'authenticated' on sign-in).
|
|
279
|
+
*
|
|
280
|
+
* @param callback - Function called with the new AuthState on each change
|
|
281
|
+
* @returns An unsubscribe function that removes the listener
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* const unsub = auth.onAuthChange((state) => {
|
|
286
|
+
* console.log('Auth state changed to:', state)
|
|
287
|
+
* })
|
|
288
|
+
* // Later: unsub()
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
onAuthChange(callback) {
|
|
292
|
+
this.listeners.add(callback);
|
|
293
|
+
return () => {
|
|
294
|
+
this.listeners.delete(callback);
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// -----------------------------------------------------------------------
|
|
298
|
+
// Internal helpers
|
|
299
|
+
// -----------------------------------------------------------------------
|
|
300
|
+
/**
|
|
301
|
+
* Update internal state and notify all listeners.
|
|
302
|
+
*/
|
|
303
|
+
setState(state, user) {
|
|
304
|
+
const changed = this._state !== state || this._user !== user;
|
|
305
|
+
this._state = state;
|
|
306
|
+
this._user = user;
|
|
307
|
+
if (changed) {
|
|
308
|
+
for (const listener of this.listeners) {
|
|
309
|
+
try {
|
|
310
|
+
listener(state);
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Restore a session from a valid access token by fetching the user profile.
|
|
318
|
+
* Falls back to extracting the user ID from the token payload if the
|
|
319
|
+
* /auth/me request fails (offline scenario).
|
|
320
|
+
*/
|
|
321
|
+
async restoreSession(accessToken) {
|
|
322
|
+
try {
|
|
323
|
+
const user = await this.fetchUserProfile(accessToken);
|
|
324
|
+
this.setState("authenticated", user);
|
|
325
|
+
} catch {
|
|
326
|
+
const userId = getUserIdFromToken(accessToken);
|
|
327
|
+
if (userId) {
|
|
328
|
+
this.setState("authenticated", {
|
|
329
|
+
id: userId,
|
|
330
|
+
email: "",
|
|
331
|
+
name: null
|
|
332
|
+
});
|
|
333
|
+
} else {
|
|
334
|
+
this.storage.clear();
|
|
335
|
+
this.setState("unauthenticated", null);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Fetch the current user profile from the server.
|
|
341
|
+
*/
|
|
342
|
+
async fetchUserProfile(accessToken) {
|
|
343
|
+
const profile = await this.request("/auth/me", {
|
|
344
|
+
method: "GET",
|
|
345
|
+
token: accessToken
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
id: profile.id,
|
|
349
|
+
email: profile.email,
|
|
350
|
+
name: profile.name ?? null
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Refresh the access token using a refresh token.
|
|
355
|
+
* De-duplicates concurrent refresh calls so only one network request is made.
|
|
356
|
+
*/
|
|
357
|
+
async refreshAccessToken(refreshToken) {
|
|
358
|
+
if (this._refreshPromise) {
|
|
359
|
+
return this._refreshPromise;
|
|
360
|
+
}
|
|
361
|
+
this._refreshPromise = this.performRefresh(refreshToken);
|
|
362
|
+
try {
|
|
363
|
+
const result = await this._refreshPromise;
|
|
364
|
+
return result;
|
|
365
|
+
} finally {
|
|
366
|
+
this._refreshPromise = null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Execute the token refresh network request.
|
|
371
|
+
*/
|
|
372
|
+
async performRefresh(refreshToken) {
|
|
373
|
+
try {
|
|
374
|
+
const response = await this.request("/auth/refresh", {
|
|
375
|
+
method: "POST",
|
|
376
|
+
body: { refreshToken }
|
|
377
|
+
});
|
|
378
|
+
this.storage.setTokens(response.accessToken, response.refreshToken);
|
|
379
|
+
return response.accessToken;
|
|
380
|
+
} catch {
|
|
381
|
+
this.storage.clear();
|
|
382
|
+
this.setState("unauthenticated", null);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Make an HTTP request to the auth server.
|
|
388
|
+
*
|
|
389
|
+
* @param path - URL path relative to serverUrl (e.g. '/auth/signin')
|
|
390
|
+
* @param options - Request options
|
|
391
|
+
* @returns Parsed JSON response body
|
|
392
|
+
* @throws {AuthError} On network failure or non-2xx response
|
|
393
|
+
*/
|
|
394
|
+
async request(path, options) {
|
|
395
|
+
const url = `${this.serverUrl}${path}`;
|
|
396
|
+
const headers = {};
|
|
397
|
+
if (options.body) {
|
|
398
|
+
headers["Content-Type"] = "application/json";
|
|
399
|
+
}
|
|
400
|
+
if (options.token) {
|
|
401
|
+
headers["Authorization"] = `Bearer ${options.token}`;
|
|
402
|
+
}
|
|
403
|
+
let response;
|
|
404
|
+
try {
|
|
405
|
+
response = await fetch(url, {
|
|
406
|
+
method: options.method,
|
|
407
|
+
headers,
|
|
408
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
409
|
+
});
|
|
410
|
+
} catch (cause) {
|
|
411
|
+
throw new AuthError(
|
|
412
|
+
`Network request to ${path} failed. The auth server at ${this.serverUrl} may be unreachable. Check your network connection and serverUrl configuration.`,
|
|
413
|
+
"AUTH_NETWORK_ERROR",
|
|
414
|
+
{ path, cause: cause instanceof Error ? cause.message : String(cause) }
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (!response.ok) {
|
|
418
|
+
let errorMessage = `Auth server returned HTTP ${response.status}`;
|
|
419
|
+
let serverError;
|
|
420
|
+
try {
|
|
421
|
+
const body = await response.json();
|
|
422
|
+
if (typeof body["error"] === "string") {
|
|
423
|
+
errorMessage = body["error"];
|
|
424
|
+
serverError = errorMessage;
|
|
425
|
+
} else if (typeof body["message"] === "string") {
|
|
426
|
+
errorMessage = body["message"];
|
|
427
|
+
serverError = errorMessage;
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
}
|
|
431
|
+
throw new AuthError(
|
|
432
|
+
errorMessage,
|
|
433
|
+
"AUTH_SERVER_ERROR",
|
|
434
|
+
{ path, status: response.status, serverError }
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
const data = await response.json();
|
|
438
|
+
return data;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// src/device/device-identity.ts
|
|
443
|
+
var import_core2 = require("@korajs/core");
|
|
444
|
+
var CryptoUnavailableError = class extends import_core2.KoraError {
|
|
445
|
+
constructor() {
|
|
446
|
+
super(
|
|
447
|
+
"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.",
|
|
448
|
+
"CRYPTO_UNAVAILABLE"
|
|
449
|
+
);
|
|
450
|
+
this.name = "CryptoUnavailableError";
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
var DeviceIdentityError = class extends import_core2.KoraError {
|
|
454
|
+
constructor(message, context) {
|
|
455
|
+
super(message, "DEVICE_IDENTITY_ERROR", context);
|
|
456
|
+
this.name = "DeviceIdentityError";
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
function toBase64Url(buffer) {
|
|
460
|
+
const bytes = new Uint8Array(buffer);
|
|
461
|
+
let binary = "";
|
|
462
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
463
|
+
binary += String.fromCharCode(bytes[i]);
|
|
464
|
+
}
|
|
465
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
466
|
+
}
|
|
467
|
+
function fromBase64Url(str) {
|
|
468
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
469
|
+
const paddingNeeded = (4 - base64.length % 4) % 4;
|
|
470
|
+
base64 += "=".repeat(paddingNeeded);
|
|
471
|
+
const binary = atob(base64);
|
|
472
|
+
const bytes = new Uint8Array(binary.length);
|
|
473
|
+
for (let i = 0; i < binary.length; i++) {
|
|
474
|
+
bytes[i] = binary.charCodeAt(i);
|
|
475
|
+
}
|
|
476
|
+
return bytes;
|
|
477
|
+
}
|
|
478
|
+
function assertCryptoAvailable() {
|
|
479
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
480
|
+
throw new CryptoUnavailableError();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
var ECDSA_ALGORITHM = {
|
|
484
|
+
name: "ECDSA",
|
|
485
|
+
namedCurve: "P-256"
|
|
486
|
+
};
|
|
487
|
+
var ECDSA_SIGN_ALGORITHM = {
|
|
488
|
+
name: "ECDSA",
|
|
489
|
+
hash: { name: "SHA-256" }
|
|
490
|
+
};
|
|
491
|
+
async function generateDeviceKeyPair() {
|
|
492
|
+
assertCryptoAvailable();
|
|
493
|
+
try {
|
|
494
|
+
const keyPair = await globalThis.crypto.subtle.generateKey(
|
|
495
|
+
ECDSA_ALGORITHM,
|
|
496
|
+
// extractable: false makes the private key non-extractable.
|
|
497
|
+
// The public key is always extractable regardless of this flag.
|
|
498
|
+
false,
|
|
499
|
+
["sign", "verify"]
|
|
500
|
+
);
|
|
501
|
+
return keyPair;
|
|
502
|
+
} catch (cause) {
|
|
503
|
+
throw new DeviceIdentityError(
|
|
504
|
+
"Failed to generate ECDSA P-256 device key pair. Ensure the runtime supports the ECDSA algorithm with the P-256 curve.",
|
|
505
|
+
{ cause: cause instanceof Error ? cause.message : String(cause) }
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function exportPublicKeyJwk(keyPair) {
|
|
510
|
+
assertCryptoAvailable();
|
|
511
|
+
try {
|
|
512
|
+
const jwk = await globalThis.crypto.subtle.exportKey("jwk", keyPair.publicKey);
|
|
513
|
+
return jwk;
|
|
514
|
+
} catch (cause) {
|
|
515
|
+
throw new DeviceIdentityError(
|
|
516
|
+
"Failed to export public key as JWK. The key pair may be invalid or the public key may not support JWK export.",
|
|
517
|
+
{ cause: cause instanceof Error ? cause.message : String(cause) }
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function signChallenge(privateKey, challenge) {
|
|
522
|
+
assertCryptoAvailable();
|
|
523
|
+
try {
|
|
524
|
+
const encoded = new TextEncoder().encode(challenge);
|
|
525
|
+
const signatureBuffer = await globalThis.crypto.subtle.sign(
|
|
526
|
+
ECDSA_SIGN_ALGORITHM,
|
|
527
|
+
privateKey,
|
|
528
|
+
encoded
|
|
529
|
+
);
|
|
530
|
+
return toBase64Url(signatureBuffer);
|
|
531
|
+
} catch (cause) {
|
|
532
|
+
throw new DeviceIdentityError(
|
|
533
|
+
'Failed to sign challenge. Ensure the key is a valid ECDSA P-256 private key with "sign" usage.',
|
|
534
|
+
{ cause: cause instanceof Error ? cause.message : String(cause) }
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async function verifyChallenge(publicKeyJwk, challenge, signature) {
|
|
539
|
+
assertCryptoAvailable();
|
|
540
|
+
try {
|
|
541
|
+
const publicKey = await globalThis.crypto.subtle.importKey(
|
|
542
|
+
"jwk",
|
|
543
|
+
publicKeyJwk,
|
|
544
|
+
ECDSA_ALGORITHM,
|
|
545
|
+
true,
|
|
546
|
+
["verify"]
|
|
547
|
+
);
|
|
548
|
+
const encoded = new TextEncoder().encode(challenge);
|
|
549
|
+
const signatureBytes = fromBase64Url(signature);
|
|
550
|
+
const isValid = await globalThis.crypto.subtle.verify(
|
|
551
|
+
ECDSA_SIGN_ALGORITHM,
|
|
552
|
+
publicKey,
|
|
553
|
+
signatureBytes,
|
|
554
|
+
encoded
|
|
555
|
+
);
|
|
556
|
+
return isValid;
|
|
557
|
+
} catch (cause) {
|
|
558
|
+
throw new DeviceIdentityError(
|
|
559
|
+
"Failed to verify challenge signature. The public key JWK or signature format may be invalid.",
|
|
560
|
+
{
|
|
561
|
+
cause: cause instanceof Error ? cause.message : String(cause),
|
|
562
|
+
publicKeyKty: publicKeyJwk.kty,
|
|
563
|
+
publicKeyCrv: publicKeyJwk.crv
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async function computePublicKeyThumbprint(publicKeyJwk) {
|
|
569
|
+
assertCryptoAvailable();
|
|
570
|
+
if (publicKeyJwk.kty !== "EC") {
|
|
571
|
+
throw new DeviceIdentityError(
|
|
572
|
+
`Expected JWK key type "EC" but received "${publicKeyJwk.kty ?? "undefined"}". Only ECDSA public keys are supported for device identity.`,
|
|
573
|
+
{ kty: publicKeyJwk.kty }
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
if (!publicKeyJwk.crv || !publicKeyJwk.x || !publicKeyJwk.y) {
|
|
577
|
+
throw new DeviceIdentityError(
|
|
578
|
+
'JWK is missing required EC fields. An EC public key JWK must include "crv", "x", and "y" members.',
|
|
579
|
+
{
|
|
580
|
+
hasCrv: Boolean(publicKeyJwk.crv),
|
|
581
|
+
hasX: Boolean(publicKeyJwk.x),
|
|
582
|
+
hasY: Boolean(publicKeyJwk.y)
|
|
583
|
+
}
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
const canonicalJson = JSON.stringify({
|
|
587
|
+
crv: publicKeyJwk.crv,
|
|
588
|
+
kty: publicKeyJwk.kty,
|
|
589
|
+
x: publicKeyJwk.x,
|
|
590
|
+
y: publicKeyJwk.y
|
|
591
|
+
});
|
|
592
|
+
try {
|
|
593
|
+
const encoded = new TextEncoder().encode(canonicalJson);
|
|
594
|
+
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
|
|
595
|
+
return toBase64Url(hashBuffer);
|
|
596
|
+
} catch (cause) {
|
|
597
|
+
throw new DeviceIdentityError(
|
|
598
|
+
"Failed to compute SHA-256 thumbprint of the public key JWK.",
|
|
599
|
+
{ cause: cause instanceof Error ? cause.message : String(cause) }
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/tokens/token-store.ts
|
|
605
|
+
var DEFAULT_STORAGE_KEY = "kora_auth_tokens";
|
|
606
|
+
var MemoryStorage = class {
|
|
607
|
+
store = /* @__PURE__ */ new Map();
|
|
608
|
+
getItem(key) {
|
|
609
|
+
return this.store.get(key) ?? null;
|
|
610
|
+
}
|
|
611
|
+
setItem(key, value) {
|
|
612
|
+
this.store.set(key, value);
|
|
613
|
+
}
|
|
614
|
+
removeItem(key) {
|
|
615
|
+
this.store.delete(key);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
function tryGetLocalStorage() {
|
|
619
|
+
try {
|
|
620
|
+
if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
|
|
621
|
+
const storage = globalThis.localStorage;
|
|
622
|
+
const testKey = "__kora_storage_test__";
|
|
623
|
+
storage.setItem(testKey, "1");
|
|
624
|
+
storage.removeItem(testKey);
|
|
625
|
+
return storage;
|
|
626
|
+
}
|
|
627
|
+
} catch {
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
var TokenStore = class {
|
|
632
|
+
storageKey;
|
|
633
|
+
storage;
|
|
634
|
+
/**
|
|
635
|
+
* Creates a new TokenStore instance.
|
|
636
|
+
*
|
|
637
|
+
* @param storageKey - The key under which tokens are stored. Defaults to 'kora_auth_tokens'.
|
|
638
|
+
* Use different keys if your app runs multiple Kora instances with separate auth.
|
|
639
|
+
*/
|
|
640
|
+
constructor(storageKey) {
|
|
641
|
+
this.storageKey = storageKey ?? DEFAULT_STORAGE_KEY;
|
|
642
|
+
this.storage = tryGetLocalStorage() ?? new MemoryStorage();
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Save tokens to persistent storage.
|
|
646
|
+
*
|
|
647
|
+
* Overwrites any previously stored tokens. The tokens are serialized
|
|
648
|
+
* as JSON. Only the `accessToken`, `refreshToken`, and optional
|
|
649
|
+
* `deviceCredential` fields are persisted.
|
|
650
|
+
*
|
|
651
|
+
* @param tokens - The token set to store
|
|
652
|
+
*/
|
|
653
|
+
saveTokens(tokens) {
|
|
654
|
+
const serialized = {
|
|
655
|
+
accessToken: tokens.accessToken,
|
|
656
|
+
refreshToken: tokens.refreshToken
|
|
657
|
+
};
|
|
658
|
+
if (tokens.deviceCredential !== void 0) {
|
|
659
|
+
serialized.deviceCredential = tokens.deviceCredential;
|
|
660
|
+
}
|
|
661
|
+
this.storage.setItem(this.storageKey, JSON.stringify(serialized));
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Load tokens from storage.
|
|
665
|
+
*
|
|
666
|
+
* @returns The stored {@link AuthTokens}, or null if no tokens have been saved
|
|
667
|
+
*/
|
|
668
|
+
loadTokens() {
|
|
669
|
+
const raw = this.storage.getItem(this.storageKey);
|
|
670
|
+
if (raw === null) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const parsed = JSON.parse(raw);
|
|
675
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
const record = parsed;
|
|
679
|
+
if (typeof record["accessToken"] !== "string" || typeof record["refreshToken"] !== "string") {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
const tokens = {
|
|
683
|
+
accessToken: record["accessToken"],
|
|
684
|
+
refreshToken: record["refreshToken"]
|
|
685
|
+
};
|
|
686
|
+
if (typeof record["deviceCredential"] === "string") {
|
|
687
|
+
tokens.deviceCredential = record["deviceCredential"];
|
|
688
|
+
}
|
|
689
|
+
return tokens;
|
|
690
|
+
} catch {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Clear all stored tokens.
|
|
696
|
+
*
|
|
697
|
+
* Call this on logout to remove credentials from persistent storage.
|
|
698
|
+
*/
|
|
699
|
+
clearTokens() {
|
|
700
|
+
this.storage.removeItem(this.storageKey);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Get the current access token.
|
|
704
|
+
*
|
|
705
|
+
* Returns the raw token string without validating expiration.
|
|
706
|
+
* The caller is responsible for checking whether the token is
|
|
707
|
+
* still valid and initiating a refresh if needed.
|
|
708
|
+
*
|
|
709
|
+
* @returns The access token string, or null if no tokens are stored
|
|
710
|
+
*/
|
|
711
|
+
getAccessToken() {
|
|
712
|
+
const tokens = this.loadTokens();
|
|
713
|
+
return tokens?.accessToken ?? null;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Get the current refresh token.
|
|
717
|
+
*
|
|
718
|
+
* @returns The refresh token string, or null if no tokens are stored
|
|
719
|
+
*/
|
|
720
|
+
getRefreshToken() {
|
|
721
|
+
const tokens = this.loadTokens();
|
|
722
|
+
return tokens?.refreshToken ?? null;
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
726
|
+
0 && (module.exports = {
|
|
727
|
+
AuthClient,
|
|
728
|
+
AuthError,
|
|
729
|
+
CryptoUnavailableError,
|
|
730
|
+
DeviceIdentityError,
|
|
731
|
+
TokenStore,
|
|
732
|
+
computePublicKeyThumbprint,
|
|
733
|
+
exportPublicKeyJwk,
|
|
734
|
+
fromBase64Url,
|
|
735
|
+
generateDeviceKeyPair,
|
|
736
|
+
signChallenge,
|
|
737
|
+
toBase64Url,
|
|
738
|
+
verifyChallenge
|
|
739
|
+
});
|
|
740
|
+
//# sourceMappingURL=index.cjs.map
|