@authms/core 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/dist/index.d.mts +398 -0
- package/dist/index.d.ts +398 -0
- package/dist/index.js +1337 -0
- package/dist/index.mjs +1294 -0
- package/package.json +48 -0
- package/src/__tests__/api-client.test.ts +314 -0
- package/src/__tests__/auth-client.test.ts +412 -0
- package/src/__tests__/discovery.test.ts +129 -0
- package/src/__tests__/password-transmission.test.ts +131 -0
- package/src/__tests__/sync.test.ts +85 -0
- package/src/__tests__/token-manager.test.ts +104 -0
- package/src/api-client.ts +203 -0
- package/src/auth-client.ts +368 -0
- package/src/authms.ts +244 -0
- package/src/binding.ts +126 -0
- package/src/crypto/index.ts +6 -0
- package/src/crypto/password-transmission.ts +198 -0
- package/src/crypto/pow-solver.ts +41 -0
- package/src/discovery.ts +77 -0
- package/src/errors.ts +44 -0
- package/src/index.ts +39 -0
- package/src/platform/browser.ts +23 -0
- package/src/platform/index.ts +3 -0
- package/src/platform/memory.ts +19 -0
- package/src/platform/types.ts +21 -0
- package/src/plugin.ts +8 -0
- package/src/sync.ts +51 -0
- package/src/token-manager.ts +140 -0
- package/src/types.ts +113 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1337 @@
|
|
|
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
|
+
ApiClient: () => ApiClient,
|
|
24
|
+
AuthClient: () => AuthClient,
|
|
25
|
+
AuthMS: () => AuthMS,
|
|
26
|
+
AuthmsApiError: () => AuthmsApiError,
|
|
27
|
+
AuthmsAuthError: () => AuthmsAuthError,
|
|
28
|
+
AuthmsConfigError: () => AuthmsConfigError,
|
|
29
|
+
AuthmsError: () => AuthmsError,
|
|
30
|
+
AuthmsNetworkError: () => AuthmsNetworkError,
|
|
31
|
+
Discovery: () => Discovery,
|
|
32
|
+
ERROR_CODES: () => ERROR_CODES,
|
|
33
|
+
TabSync: () => TabSync,
|
|
34
|
+
TokenManager: () => TokenManager,
|
|
35
|
+
browserPlatform: () => browserPlatform,
|
|
36
|
+
createPlatformBinding: () => createPlatformBinding,
|
|
37
|
+
memoryPlatform: () => memoryPlatform,
|
|
38
|
+
processPasswordForTransmission: () => processPasswordForTransmission,
|
|
39
|
+
solveProofOfWork: () => solveProofOfWork
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// src/token-manager.ts
|
|
44
|
+
var STORAGE_PREFIX = "authms_";
|
|
45
|
+
var TokenManager = class {
|
|
46
|
+
constructor(storage, prefix) {
|
|
47
|
+
this.changeListeners = /* @__PURE__ */ new Set();
|
|
48
|
+
this.storage = storage;
|
|
49
|
+
this.prefix = prefix ?? STORAGE_PREFIX;
|
|
50
|
+
this.store = this.initialStore();
|
|
51
|
+
}
|
|
52
|
+
initialStore() {
|
|
53
|
+
return {
|
|
54
|
+
accessToken: null,
|
|
55
|
+
refreshToken: null,
|
|
56
|
+
user: null,
|
|
57
|
+
tenantId: null,
|
|
58
|
+
expiresAt: null
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
storageKey(key) {
|
|
62
|
+
return `${this.prefix}${key}`;
|
|
63
|
+
}
|
|
64
|
+
async load() {
|
|
65
|
+
try {
|
|
66
|
+
const raw = await this.storage.getItem(this.storageKey("tokens"));
|
|
67
|
+
if (raw) {
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
this.store = { ...this.initialStore(), ...parsed };
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
this.store = this.initialStore();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async persist() {
|
|
76
|
+
try {
|
|
77
|
+
await this.storage.setItem(this.storageKey("tokens"), JSON.stringify(this.store));
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
getAccessToken() {
|
|
82
|
+
if (this.store.accessToken && this.isTokenExpired(this.store.accessToken)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return this.store.accessToken;
|
|
86
|
+
}
|
|
87
|
+
getRefreshToken() {
|
|
88
|
+
return this.store.refreshToken;
|
|
89
|
+
}
|
|
90
|
+
getUser() {
|
|
91
|
+
return this.store.user;
|
|
92
|
+
}
|
|
93
|
+
getTenantId() {
|
|
94
|
+
return this.store.tenantId;
|
|
95
|
+
}
|
|
96
|
+
getExpiresAt() {
|
|
97
|
+
return this.store.expiresAt;
|
|
98
|
+
}
|
|
99
|
+
isAuthenticated() {
|
|
100
|
+
return !!this.store.accessToken && !this.isTokenExpired(this.store.accessToken);
|
|
101
|
+
}
|
|
102
|
+
setTokens(accessToken, refreshToken, expiresIn) {
|
|
103
|
+
this.store.accessToken = accessToken;
|
|
104
|
+
this.store.refreshToken = refreshToken;
|
|
105
|
+
this.store.expiresAt = Date.now() + expiresIn * 1e3;
|
|
106
|
+
this.notifyListeners(accessToken);
|
|
107
|
+
}
|
|
108
|
+
setUser(user) {
|
|
109
|
+
this.store.user = user;
|
|
110
|
+
}
|
|
111
|
+
setTenantId(tenantId) {
|
|
112
|
+
this.store.tenantId = tenantId;
|
|
113
|
+
}
|
|
114
|
+
clear() {
|
|
115
|
+
this.store = this.initialStore();
|
|
116
|
+
this.notifyListeners(null);
|
|
117
|
+
}
|
|
118
|
+
onTokenChange(listener) {
|
|
119
|
+
this.changeListeners.add(listener);
|
|
120
|
+
return () => this.changeListeners.delete(listener);
|
|
121
|
+
}
|
|
122
|
+
notifyListeners(token) {
|
|
123
|
+
this.changeListeners.forEach((fn) => {
|
|
124
|
+
try {
|
|
125
|
+
fn(token);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
decodeToken(token) {
|
|
131
|
+
try {
|
|
132
|
+
const base64Url = token.split(".")[1];
|
|
133
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
134
|
+
const json = decodeURIComponent(
|
|
135
|
+
atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
136
|
+
);
|
|
137
|
+
return JSON.parse(json);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
isTokenExpired(token) {
|
|
143
|
+
const claims = this.decodeToken(token);
|
|
144
|
+
if (!claims?.exp) return true;
|
|
145
|
+
return Date.now() >= claims.exp * 1e3;
|
|
146
|
+
}
|
|
147
|
+
getTokenRemainingTime(token) {
|
|
148
|
+
const claims = this.decodeToken(token);
|
|
149
|
+
if (!claims?.exp) return 0;
|
|
150
|
+
return Math.max(0, claims.exp * 1e3 - Date.now());
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/errors.ts
|
|
155
|
+
var AuthmsError = class extends Error {
|
|
156
|
+
constructor(code, message, status, detail) {
|
|
157
|
+
super(message);
|
|
158
|
+
this.name = "AuthmsError";
|
|
159
|
+
this.code = code;
|
|
160
|
+
this.status = status;
|
|
161
|
+
this.detail = detail;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var AuthmsAuthError = class extends AuthmsError {
|
|
165
|
+
constructor(code, message, status) {
|
|
166
|
+
super(code, message, status);
|
|
167
|
+
this.name = "AuthmsAuthError";
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var AuthmsNetworkError = class extends AuthmsError {
|
|
171
|
+
constructor(message) {
|
|
172
|
+
super("NETWORK_ERROR", message, 0);
|
|
173
|
+
this.name = "AuthmsNetworkError";
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var AuthmsApiError = class extends AuthmsError {
|
|
177
|
+
constructor(code, message, status, violations) {
|
|
178
|
+
super(code, message, status);
|
|
179
|
+
this.name = "AuthmsApiError";
|
|
180
|
+
this.violations = violations;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var AuthmsConfigError = class extends AuthmsError {
|
|
184
|
+
constructor(message) {
|
|
185
|
+
super("CONFIG_ERROR", message, 500);
|
|
186
|
+
this.name = "AuthmsConfigError";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/api-client.ts
|
|
191
|
+
function snakeToCamel(str) {
|
|
192
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
193
|
+
}
|
|
194
|
+
function camelToSnake(str) {
|
|
195
|
+
return str.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
|
|
196
|
+
}
|
|
197
|
+
function transformKeys(obj, transform) {
|
|
198
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
199
|
+
if (Array.isArray(obj)) return obj.map((v) => transformKeys(v, transform));
|
|
200
|
+
const result = {};
|
|
201
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
202
|
+
result[transform(k)] = transformKeys(v, transform);
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
function unwrapResponse(data) {
|
|
207
|
+
if ("data" in data && data.data !== void 0) {
|
|
208
|
+
return data.data;
|
|
209
|
+
}
|
|
210
|
+
if ("items" in data) {
|
|
211
|
+
return { items: data.items, total: data.total, pagination: data.pagination };
|
|
212
|
+
}
|
|
213
|
+
return data;
|
|
214
|
+
}
|
|
215
|
+
var ApiClient = class {
|
|
216
|
+
constructor(config) {
|
|
217
|
+
this.refreshPromise = null;
|
|
218
|
+
this.redirectingToLogin = false;
|
|
219
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
220
|
+
this.tokenManager = config.tokenManager;
|
|
221
|
+
this.http = config.http;
|
|
222
|
+
this.refreshTokenFn = config.refreshTokenFn;
|
|
223
|
+
this.onForceLogout = config.onForceLogout;
|
|
224
|
+
}
|
|
225
|
+
async get(path, params) {
|
|
226
|
+
const url = this.buildUrl(path, params);
|
|
227
|
+
return this.request(url, { method: "GET" });
|
|
228
|
+
}
|
|
229
|
+
async post(path, data) {
|
|
230
|
+
const url = `${this.baseUrl}${path}`;
|
|
231
|
+
return this.request(url, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { "Content-Type": "application/json" },
|
|
234
|
+
body: data ? JSON.stringify(transformKeys(data, camelToSnake)) : void 0
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async put(path, data) {
|
|
238
|
+
const url = `${this.baseUrl}${path}`;
|
|
239
|
+
return this.request(url, {
|
|
240
|
+
method: "PUT",
|
|
241
|
+
headers: { "Content-Type": "application/json" },
|
|
242
|
+
body: data ? JSON.stringify(transformKeys(data, camelToSnake)) : void 0
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async delete(path, params) {
|
|
246
|
+
const url = this.buildUrl(path, params);
|
|
247
|
+
return this.request(url, { method: "DELETE" });
|
|
248
|
+
}
|
|
249
|
+
buildUrl(path, params) {
|
|
250
|
+
let url = `${this.baseUrl}${path}`;
|
|
251
|
+
if (params && Object.keys(params).length > 0) {
|
|
252
|
+
const query = new URLSearchParams();
|
|
253
|
+
for (const [k, v] of Object.entries(params)) {
|
|
254
|
+
if (v !== void 0 && v !== null) {
|
|
255
|
+
query.append(camelToSnake(k), String(v));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
url += "?" + query.toString();
|
|
259
|
+
}
|
|
260
|
+
return url;
|
|
261
|
+
}
|
|
262
|
+
async request(url, init) {
|
|
263
|
+
const headers = {
|
|
264
|
+
...init.headers || {}
|
|
265
|
+
};
|
|
266
|
+
const token = this.tokenManager.getAccessToken();
|
|
267
|
+
if (token) {
|
|
268
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
269
|
+
}
|
|
270
|
+
const tenantId = this.tokenManager.getTenantId();
|
|
271
|
+
if (tenantId) {
|
|
272
|
+
headers["X-Tenant-ID"] = tenantId;
|
|
273
|
+
}
|
|
274
|
+
const requestInit = {
|
|
275
|
+
...init,
|
|
276
|
+
headers
|
|
277
|
+
};
|
|
278
|
+
let response;
|
|
279
|
+
try {
|
|
280
|
+
response = await this.http.request(url, requestInit);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
throw new AuthmsNetworkError(err instanceof Error ? err.message : "Network request failed");
|
|
283
|
+
}
|
|
284
|
+
if (response.status === 401 && !url.includes("/api/v1/auth/login") && !url.includes("/api/v1/auth/refresh")) {
|
|
285
|
+
return this.handle401(url, requestInit);
|
|
286
|
+
}
|
|
287
|
+
return this.handleResponse(response);
|
|
288
|
+
}
|
|
289
|
+
async handle401(url, requestInit) {
|
|
290
|
+
if (!this.refreshPromise) {
|
|
291
|
+
this.refreshPromise = (async () => {
|
|
292
|
+
try {
|
|
293
|
+
await this.refreshTokenFn();
|
|
294
|
+
} catch {
|
|
295
|
+
this.tokenManager.clear();
|
|
296
|
+
if (!this.redirectingToLogin) {
|
|
297
|
+
this.redirectingToLogin = true;
|
|
298
|
+
this.onForceLogout?.();
|
|
299
|
+
throw new AuthmsAuthError("SESSION_EXPIRED", "Session expired, please login again", 401);
|
|
300
|
+
}
|
|
301
|
+
throw new AuthmsAuthError("REFRESH_FAILED", "Token refresh failed", 401);
|
|
302
|
+
} finally {
|
|
303
|
+
this.refreshPromise = null;
|
|
304
|
+
}
|
|
305
|
+
})();
|
|
306
|
+
}
|
|
307
|
+
await this.refreshPromise;
|
|
308
|
+
const newToken = this.tokenManager.getAccessToken();
|
|
309
|
+
if (newToken) {
|
|
310
|
+
const headers = { ...requestInit.headers };
|
|
311
|
+
headers["Authorization"] = `Bearer ${newToken}`;
|
|
312
|
+
const response = await this.http.request(url, { ...requestInit, headers });
|
|
313
|
+
return this.handleResponse(response);
|
|
314
|
+
}
|
|
315
|
+
throw new AuthmsAuthError("NOT_AUTHENTICATED", "Not authenticated", 401);
|
|
316
|
+
}
|
|
317
|
+
async handleResponse(response) {
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
await this.handleErrorResponse(response);
|
|
320
|
+
}
|
|
321
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
322
|
+
return void 0;
|
|
323
|
+
}
|
|
324
|
+
const json = await response.json();
|
|
325
|
+
if (json && typeof json === "object" && "code" in json) {
|
|
326
|
+
const code = String(json.code);
|
|
327
|
+
if (json.code !== 0) {
|
|
328
|
+
throw new AuthmsApiError(
|
|
329
|
+
code,
|
|
330
|
+
json.message || "API error",
|
|
331
|
+
response.status
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return transformKeys(unwrapResponse(json), snakeToCamel);
|
|
335
|
+
}
|
|
336
|
+
return json;
|
|
337
|
+
}
|
|
338
|
+
async handleErrorResponse(response) {
|
|
339
|
+
try {
|
|
340
|
+
const json = await response.json();
|
|
341
|
+
const code = String(json.code ?? response.status);
|
|
342
|
+
const message = json.message || `HTTP ${response.status}`;
|
|
343
|
+
throw new AuthmsApiError(code, message, response.status);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
if (e instanceof AuthmsError) throw e;
|
|
346
|
+
throw new AuthmsNetworkError(`HTTP ${response.status}: ${response.statusText}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// src/crypto/password-transmission.ts
|
|
352
|
+
var ENCODER = new TextEncoder();
|
|
353
|
+
function base64ToBytes(base64) {
|
|
354
|
+
const binary = atob(base64);
|
|
355
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
356
|
+
}
|
|
357
|
+
function bytesToHex(bytes) {
|
|
358
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
359
|
+
}
|
|
360
|
+
async function processPasswordForTransmission(rawPassword, policy, keyExchangeFn) {
|
|
361
|
+
const mode = policy.mode || "plain";
|
|
362
|
+
const result = {
|
|
363
|
+
password: rawPassword,
|
|
364
|
+
passwordTransmission: mode
|
|
365
|
+
};
|
|
366
|
+
switch (mode) {
|
|
367
|
+
case "hash": {
|
|
368
|
+
const input = rawPassword + "|" + policy.tenantId;
|
|
369
|
+
try {
|
|
370
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", ENCODER.encode(input));
|
|
371
|
+
result.password = bytesToHex(new Uint8Array(hashBuffer));
|
|
372
|
+
} catch {
|
|
373
|
+
result.password = sha256HexPureJS(input);
|
|
374
|
+
}
|
|
375
|
+
result.passwordTransmission = "hash";
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case "symmetric": {
|
|
379
|
+
if (!keyExchangeFn) {
|
|
380
|
+
throw new Error("keyExchangeFn is required for symmetric mode");
|
|
381
|
+
}
|
|
382
|
+
const keResult = await keyExchangeFn();
|
|
383
|
+
const serverPubBytes = base64ToBytes(keResult.serverPubKey);
|
|
384
|
+
const clientKeyPair = await crypto.subtle.generateKey(
|
|
385
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
386
|
+
false,
|
|
387
|
+
["deriveBits"]
|
|
388
|
+
);
|
|
389
|
+
const serverPubKey = await crypto.subtle.importKey(
|
|
390
|
+
"raw",
|
|
391
|
+
serverPubBytes,
|
|
392
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
393
|
+
false,
|
|
394
|
+
[]
|
|
395
|
+
);
|
|
396
|
+
const sharedBits = await crypto.subtle.deriveBits(
|
|
397
|
+
{ name: "ECDH", public: serverPubKey },
|
|
398
|
+
clientKeyPair.privateKey,
|
|
399
|
+
256
|
|
400
|
+
);
|
|
401
|
+
const aesKey = await crypto.subtle.importKey(
|
|
402
|
+
"raw",
|
|
403
|
+
sharedBits,
|
|
404
|
+
"AES-GCM",
|
|
405
|
+
false,
|
|
406
|
+
["encrypt"]
|
|
407
|
+
);
|
|
408
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
409
|
+
const encoded = ENCODER.encode(rawPassword);
|
|
410
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
411
|
+
{ name: "AES-GCM", iv },
|
|
412
|
+
aesKey,
|
|
413
|
+
encoded
|
|
414
|
+
);
|
|
415
|
+
const clientPubRaw = await crypto.subtle.exportKey("raw", clientKeyPair.publicKey);
|
|
416
|
+
const clientPubBytes = new Uint8Array(clientPubRaw);
|
|
417
|
+
const combined = new Uint8Array(12 + ciphertext.byteLength + clientPubBytes.byteLength);
|
|
418
|
+
combined.set(iv, 0);
|
|
419
|
+
combined.set(new Uint8Array(ciphertext), 12);
|
|
420
|
+
combined.set(clientPubBytes, 12 + ciphertext.byteLength);
|
|
421
|
+
result.password = btoa(String.fromCharCode(...Array.from(combined)));
|
|
422
|
+
result.keyExchangeId = keResult.keyExchangeId;
|
|
423
|
+
result.clientPubKey = btoa(String.fromCharCode(...Array.from(new Uint8Array(clientPubRaw))));
|
|
424
|
+
result.passwordTransmission = "symmetric";
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
case "asymmetric": {
|
|
428
|
+
const publicKeyPem = policy.publicKey;
|
|
429
|
+
if (!publicKeyPem || publicKeyPem.length < 100) {
|
|
430
|
+
throw new Error("public_key is required for asymmetric mode");
|
|
431
|
+
}
|
|
432
|
+
const pemContents = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replace(/\s/g, "");
|
|
433
|
+
const derBytes = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
434
|
+
const publicKey = await crypto.subtle.importKey(
|
|
435
|
+
"spki",
|
|
436
|
+
derBytes,
|
|
437
|
+
{ name: "RSA-OAEP", hash: "SHA-256" },
|
|
438
|
+
false,
|
|
439
|
+
["encrypt"]
|
|
440
|
+
);
|
|
441
|
+
const encoded = ENCODER.encode(rawPassword);
|
|
442
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
443
|
+
{ name: "RSA-OAEP" },
|
|
444
|
+
publicKey,
|
|
445
|
+
encoded
|
|
446
|
+
);
|
|
447
|
+
result.password = btoa(String.fromCharCode(...Array.from(new Uint8Array(ciphertext))));
|
|
448
|
+
result.passwordTransmission = "asymmetric";
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
default:
|
|
452
|
+
result.passwordTransmission = "plain";
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
function ror(x, n) {
|
|
458
|
+
return x >>> n | x << 32 - n;
|
|
459
|
+
}
|
|
460
|
+
function sha256HexPureJS(input) {
|
|
461
|
+
const msg = new TextEncoder().encode(input);
|
|
462
|
+
const msgBits = msg.length * 8;
|
|
463
|
+
const buf = new Uint8Array(msg.length + 9 + 63 >>> 6 << 6);
|
|
464
|
+
buf.set(msg);
|
|
465
|
+
buf[msg.length] = 128;
|
|
466
|
+
const view = new DataView(buf.buffer);
|
|
467
|
+
view.setUint32(buf.length - 4, msgBits);
|
|
468
|
+
const K = new Uint32Array([
|
|
469
|
+
1116352408,
|
|
470
|
+
1899447441,
|
|
471
|
+
3049323471,
|
|
472
|
+
3921009573,
|
|
473
|
+
961987163,
|
|
474
|
+
1508970993,
|
|
475
|
+
2453635748,
|
|
476
|
+
2870763221,
|
|
477
|
+
3624381080,
|
|
478
|
+
310598401,
|
|
479
|
+
607225278,
|
|
480
|
+
1426881987,
|
|
481
|
+
1925078388,
|
|
482
|
+
2162078206,
|
|
483
|
+
2614888103,
|
|
484
|
+
3248222580,
|
|
485
|
+
3835390401,
|
|
486
|
+
4022224774,
|
|
487
|
+
264347078,
|
|
488
|
+
604807628,
|
|
489
|
+
770255983,
|
|
490
|
+
1249150122,
|
|
491
|
+
1555081692,
|
|
492
|
+
1996064986,
|
|
493
|
+
2554220882,
|
|
494
|
+
2821834349,
|
|
495
|
+
2952996808,
|
|
496
|
+
3210313671,
|
|
497
|
+
3336571891,
|
|
498
|
+
3584528711,
|
|
499
|
+
113926993,
|
|
500
|
+
338241895,
|
|
501
|
+
666307205,
|
|
502
|
+
773529912,
|
|
503
|
+
1294757372,
|
|
504
|
+
1396182291,
|
|
505
|
+
1695183700,
|
|
506
|
+
1986661051,
|
|
507
|
+
2177026350,
|
|
508
|
+
2456956037,
|
|
509
|
+
2730485921,
|
|
510
|
+
2820302411,
|
|
511
|
+
3259730800,
|
|
512
|
+
3345764771,
|
|
513
|
+
3516065817,
|
|
514
|
+
3600352804,
|
|
515
|
+
4094571909,
|
|
516
|
+
275423344,
|
|
517
|
+
430227734,
|
|
518
|
+
506948616,
|
|
519
|
+
659060556,
|
|
520
|
+
883997877,
|
|
521
|
+
958139571,
|
|
522
|
+
1322822218,
|
|
523
|
+
1537002063,
|
|
524
|
+
1747873779,
|
|
525
|
+
1955562222,
|
|
526
|
+
2024104815,
|
|
527
|
+
2227730452,
|
|
528
|
+
2361852424,
|
|
529
|
+
2428436474,
|
|
530
|
+
2756734187,
|
|
531
|
+
3204031479,
|
|
532
|
+
3329325298
|
|
533
|
+
]);
|
|
534
|
+
const H = new Uint32Array([1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]);
|
|
535
|
+
const W = new Uint32Array(64);
|
|
536
|
+
for (let off = 0; off < buf.length; off += 64) {
|
|
537
|
+
for (let i = 0; i < 16; i++) W[i] = view.getUint32(off + i * 4);
|
|
538
|
+
for (let i = 16; i < 64; i++) {
|
|
539
|
+
const s0 = ror(W[i - 15], 7) ^ ror(W[i - 15], 18) ^ W[i - 15] >>> 3;
|
|
540
|
+
const s1 = ror(W[i - 2], 17) ^ ror(W[i - 2], 19) ^ W[i - 2] >>> 10;
|
|
541
|
+
W[i] = W[i - 16] + s0 + W[i - 7] + s1 | 0;
|
|
542
|
+
}
|
|
543
|
+
let [a, b, c, d, e, f2, g, h] = H;
|
|
544
|
+
for (let i = 0; i < 64; i++) {
|
|
545
|
+
const S1 = ror(e, 6) ^ ror(e, 11) ^ ror(e, 25);
|
|
546
|
+
const ch = e & f2 ^ ~e & g;
|
|
547
|
+
const t1 = h + S1 + ch + K[i] + W[i] | 0;
|
|
548
|
+
const S0 = ror(a, 2) ^ ror(a, 13) ^ ror(a, 22);
|
|
549
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
550
|
+
const t2 = S0 + maj | 0;
|
|
551
|
+
h = g;
|
|
552
|
+
g = f2;
|
|
553
|
+
f2 = e;
|
|
554
|
+
e = d + t1 | 0;
|
|
555
|
+
d = c;
|
|
556
|
+
c = b;
|
|
557
|
+
b = a;
|
|
558
|
+
a = t1 + t2 | 0;
|
|
559
|
+
}
|
|
560
|
+
H[0] = H[0] + a | 0;
|
|
561
|
+
H[1] = H[1] + b | 0;
|
|
562
|
+
H[2] = H[2] + c | 0;
|
|
563
|
+
H[3] = H[3] + d | 0;
|
|
564
|
+
H[4] = H[4] + e | 0;
|
|
565
|
+
H[5] = H[5] + f2 | 0;
|
|
566
|
+
H[6] = H[6] + g | 0;
|
|
567
|
+
H[7] = H[7] + h | 0;
|
|
568
|
+
}
|
|
569
|
+
const digest = new Uint8Array(32);
|
|
570
|
+
const dv = new DataView(digest.buffer);
|
|
571
|
+
for (let i = 0; i < 8; i++) dv.setUint32(i * 4, H[i]);
|
|
572
|
+
return Array.from(digest).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/crypto/pow-solver.ts
|
|
576
|
+
var SOLVER_TIMEOUT_MS = 5e3;
|
|
577
|
+
async function solveProofOfWork(challenge, difficulty = 4, timeoutMs = SOLVER_TIMEOUT_MS) {
|
|
578
|
+
const startTime = Date.now();
|
|
579
|
+
const encoder = new TextEncoder();
|
|
580
|
+
let nonce = 0;
|
|
581
|
+
const prefix = "0".repeat(difficulty);
|
|
582
|
+
while (true) {
|
|
583
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
584
|
+
throw new Error("PoW solver timeout");
|
|
585
|
+
}
|
|
586
|
+
const data = encoder.encode(challenge + nonce);
|
|
587
|
+
try {
|
|
588
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
589
|
+
const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
590
|
+
if (hex.startsWith(prefix)) {
|
|
591
|
+
return "pow_" + nonce.toString(36);
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
nonce++;
|
|
596
|
+
if (nonce % 1e3 === 0) {
|
|
597
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/auth-client.ts
|
|
603
|
+
var MAX_CAPTCHA_RETRIES = 3;
|
|
604
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
605
|
+
var AuthClient = class {
|
|
606
|
+
constructor(config) {
|
|
607
|
+
this.configCache = /* @__PURE__ */ new Map();
|
|
608
|
+
this.tokenManager = config.tokenManager;
|
|
609
|
+
this.http = config.http;
|
|
610
|
+
this.baseUrl = config.baseUrl;
|
|
611
|
+
this.keyExchangeFn = config.keyExchangeFn;
|
|
612
|
+
}
|
|
613
|
+
async fetchAuthConfig(tenantId) {
|
|
614
|
+
const key = tenantId || "__default__";
|
|
615
|
+
const cached = this.configCache.get(key);
|
|
616
|
+
if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
617
|
+
return cached.data;
|
|
618
|
+
}
|
|
619
|
+
const path = tenantId ? `/identity/api/v1/public/auth-config/${tenantId}` : "/identity/api/v1/public/auth-config";
|
|
620
|
+
const response = await this.http.request(`${this.baseUrl}${path}`);
|
|
621
|
+
const json = await response.json();
|
|
622
|
+
const data = json.data ?? json;
|
|
623
|
+
const pp = data.password_policy ?? {};
|
|
624
|
+
const config = {
|
|
625
|
+
tenantId: data.tenant_id || "",
|
|
626
|
+
tenantName: data.tenant_name || "",
|
|
627
|
+
displayName: data.display_name || "",
|
|
628
|
+
membershipApproval: data.membership_approval || "open",
|
|
629
|
+
loginMethods: data.login_methods || [],
|
|
630
|
+
oauthProviders: data.oauth_providers || [],
|
|
631
|
+
passwordPolicy: {
|
|
632
|
+
mode: pp.password_transmission || data.password_transmission || "plain",
|
|
633
|
+
minLength: pp.min_length || 8,
|
|
634
|
+
maxLength: pp.max_length || 128,
|
|
635
|
+
requireUpper: pp.require_upper || false,
|
|
636
|
+
requireLower: pp.require_lower || false,
|
|
637
|
+
requireDigit: pp.require_digit || false,
|
|
638
|
+
requireSpecial: pp.require_special || false,
|
|
639
|
+
tenantId: data.tenant_id || "",
|
|
640
|
+
publicKey: data.transmission_public_key || ""
|
|
641
|
+
},
|
|
642
|
+
captchaEnabled: data.captcha_enabled || false,
|
|
643
|
+
captchaProvider: data.captcha_provider || "pow",
|
|
644
|
+
silentChallengeEnabled: data.silent_challenge_enabled || false,
|
|
645
|
+
transmissionPublicKey: data.transmission_public_key || "",
|
|
646
|
+
oauthClientId: data.oauth_client_id || "",
|
|
647
|
+
passkeyEnabled: data.passkey_enabled || false,
|
|
648
|
+
magicLinkEnabled: data.magic_link_enabled || false,
|
|
649
|
+
branding: data.branding ?? null
|
|
650
|
+
};
|
|
651
|
+
this.configCache.set(key, { data: config, at: Date.now() });
|
|
652
|
+
return config;
|
|
653
|
+
}
|
|
654
|
+
clearConfigCache() {
|
|
655
|
+
this.configCache.clear();
|
|
656
|
+
}
|
|
657
|
+
async login(credentials) {
|
|
658
|
+
const authConfig = await this.fetchAuthConfig(credentials.tenantId);
|
|
659
|
+
let captchaRetries = 0;
|
|
660
|
+
while (true) {
|
|
661
|
+
const body = await this.buildLoginBody(credentials, authConfig);
|
|
662
|
+
if (!body["captcha_token"] && authConfig.silentChallengeEnabled && authConfig.captchaProvider === "pow") {
|
|
663
|
+
try {
|
|
664
|
+
const token = await this.solveCaptchaChallenge();
|
|
665
|
+
body["captcha_token"] = token;
|
|
666
|
+
body["captcha_provider"] = "pow";
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const response = await this.http.request(`${this.baseUrl}/identity/api/v1/auth/login`, {
|
|
671
|
+
method: "POST",
|
|
672
|
+
headers: { "Content-Type": "application/json" },
|
|
673
|
+
body: JSON.stringify(body)
|
|
674
|
+
});
|
|
675
|
+
if (response.status === 401) {
|
|
676
|
+
const errJson = await response.json().catch(() => ({}));
|
|
677
|
+
const code = String(errJson.code ?? "");
|
|
678
|
+
if (code.includes("captcha") && captchaRetries < MAX_CAPTCHA_RETRIES) {
|
|
679
|
+
captchaRetries++;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
throw new AuthmsAuthError(code, errJson.message || `Login failed`, 401);
|
|
683
|
+
}
|
|
684
|
+
if (!response.ok) {
|
|
685
|
+
const errJson = await response.json().catch(() => ({}));
|
|
686
|
+
throw new AuthmsAuthError(
|
|
687
|
+
String(errJson.code ?? response.status),
|
|
688
|
+
errJson.message || `Login failed`,
|
|
689
|
+
response.status
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const json = await response.json();
|
|
693
|
+
return this.handleAuthResponse(json);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async register(data) {
|
|
697
|
+
const authConfig = await this.fetchAuthConfig(data.tenantId);
|
|
698
|
+
const processed = await processPasswordForTransmission(
|
|
699
|
+
data.password,
|
|
700
|
+
{
|
|
701
|
+
mode: authConfig.passwordPolicy.mode,
|
|
702
|
+
tenantId: authConfig.tenantId || data.tenantId || "",
|
|
703
|
+
requireUpper: false,
|
|
704
|
+
minLength: 0,
|
|
705
|
+
publicKey: authConfig.transmissionPublicKey || ""
|
|
706
|
+
},
|
|
707
|
+
this.keyExchangeFn
|
|
708
|
+
);
|
|
709
|
+
const body = {
|
|
710
|
+
...data,
|
|
711
|
+
password: processed.password,
|
|
712
|
+
password_transmission: processed.passwordTransmission
|
|
713
|
+
};
|
|
714
|
+
if (processed.keyExchangeId) body.key_exchange_id = processed.keyExchangeId;
|
|
715
|
+
if (processed.clientPubKey) body.client_pub_key = processed.clientPubKey;
|
|
716
|
+
const response = await this.http.request(`${this.baseUrl}/identity/api/v1/auth/register`, {
|
|
717
|
+
method: "POST",
|
|
718
|
+
headers: { "Content-Type": "application/json" },
|
|
719
|
+
body: JSON.stringify(body)
|
|
720
|
+
});
|
|
721
|
+
if (!response.ok) {
|
|
722
|
+
const errJson = await response.json().catch(() => ({}));
|
|
723
|
+
throw new AuthmsAuthError(
|
|
724
|
+
String(errJson.code ?? response.status),
|
|
725
|
+
errJson.message || `Registration failed`,
|
|
726
|
+
response.status
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
const json = await response.json();
|
|
730
|
+
const payload = json.data ?? json;
|
|
731
|
+
if (payload.access_token) {
|
|
732
|
+
const result = this.handleAuthResponse(json);
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
return { user: payload, accessToken: "", refreshToken: "", expiresIn: 0, tokenType: "" };
|
|
736
|
+
}
|
|
737
|
+
async changePassword(currentPassword, newPassword) {
|
|
738
|
+
const authConfig = await this.fetchAuthConfig();
|
|
739
|
+
const processed = await processPasswordForTransmission(
|
|
740
|
+
newPassword,
|
|
741
|
+
{
|
|
742
|
+
mode: authConfig.passwordPolicy.mode,
|
|
743
|
+
tenantId: authConfig.tenantId,
|
|
744
|
+
requireUpper: false,
|
|
745
|
+
minLength: 0,
|
|
746
|
+
publicKey: authConfig.transmissionPublicKey || ""
|
|
747
|
+
},
|
|
748
|
+
this.keyExchangeFn
|
|
749
|
+
);
|
|
750
|
+
const body = {
|
|
751
|
+
current_password: currentPassword,
|
|
752
|
+
password: processed.password,
|
|
753
|
+
password_transmission: processed.passwordTransmission
|
|
754
|
+
};
|
|
755
|
+
if (processed.keyExchangeId) body.key_exchange_id = processed.keyExchangeId;
|
|
756
|
+
if (processed.clientPubKey) body.client_pub_key = processed.clientPubKey;
|
|
757
|
+
const response = await this.http.request(`${this.baseUrl}/identity/api/v1/auth/me/password`, {
|
|
758
|
+
method: "PUT",
|
|
759
|
+
headers: { "Content-Type": "application/json" },
|
|
760
|
+
body: JSON.stringify(body)
|
|
761
|
+
});
|
|
762
|
+
if (!response.ok) {
|
|
763
|
+
const errJson = await response.json().catch(() => ({}));
|
|
764
|
+
throw new AuthmsAuthError(
|
|
765
|
+
String(errJson.code ?? response.status),
|
|
766
|
+
errJson.message || `Password change failed`,
|
|
767
|
+
response.status
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async loginWithOAuth(options) {
|
|
772
|
+
const redirectUri = options.redirectUri || (typeof window !== "undefined" ? window.location.origin + "/oauth/callback" : "");
|
|
773
|
+
const params = new URLSearchParams({ provider: options.provider, redirect_uri: redirectUri });
|
|
774
|
+
if (typeof window !== "undefined") {
|
|
775
|
+
window.location.href = `${this.baseUrl}/oauth/api/v1/oauth/${options.provider}/authorize?${params.toString()}`;
|
|
776
|
+
} else {
|
|
777
|
+
throw new AuthmsAuthError("NOT_BROWSER", "OAuth login requires a browser environment", 400);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async handleOAuthCallback(url) {
|
|
781
|
+
const urlObj = new URL(url);
|
|
782
|
+
const code = urlObj.searchParams.get("code");
|
|
783
|
+
const state = urlObj.searchParams.get("state");
|
|
784
|
+
if (!code) throw new AuthmsAuthError("OAUTH_FAILED", "No authorization code in callback URL", 400);
|
|
785
|
+
const response = await this.http.request(`${this.baseUrl}/oauth/api/v1/oauth/token`, {
|
|
786
|
+
method: "POST",
|
|
787
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
788
|
+
body: new URLSearchParams({
|
|
789
|
+
grant_type: "authorization_code",
|
|
790
|
+
code,
|
|
791
|
+
state: state ?? "",
|
|
792
|
+
redirect_uri: typeof window !== "undefined" ? window.location.origin + "/oauth/callback" : ""
|
|
793
|
+
}).toString()
|
|
794
|
+
});
|
|
795
|
+
if (!response.ok) throw new AuthmsNetworkError(`OAuth token exchange failed (${response.status})`);
|
|
796
|
+
const json = await response.json();
|
|
797
|
+
return this.handleAuthResponse(json);
|
|
798
|
+
}
|
|
799
|
+
async refreshToken() {
|
|
800
|
+
const refreshToken = this.tokenManager.getRefreshToken();
|
|
801
|
+
if (!refreshToken) throw new AuthmsAuthError("NO_REFRESH_TOKEN", "No refresh token available", 401);
|
|
802
|
+
const response = await this.http.request(`${this.baseUrl}/identity/api/v1/auth/refresh`, {
|
|
803
|
+
method: "POST",
|
|
804
|
+
headers: { "Content-Type": "application/json" },
|
|
805
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
806
|
+
});
|
|
807
|
+
if (!response.ok) {
|
|
808
|
+
const errJson = await response.json().catch(() => ({}));
|
|
809
|
+
const code = String(errJson.code ?? "");
|
|
810
|
+
if (code.startsWith("400002")) {
|
|
811
|
+
this.tokenManager.clear();
|
|
812
|
+
this.tokenManager.persist();
|
|
813
|
+
throw new AuthmsAuthError("TOKEN_REUSE", "Refresh token reused \u2014 all sessions revoked", 401);
|
|
814
|
+
}
|
|
815
|
+
this.tokenManager.clear();
|
|
816
|
+
this.tokenManager.persist();
|
|
817
|
+
throw new AuthmsAuthError("REFRESH_FAILED", "Token refresh failed", 401);
|
|
818
|
+
}
|
|
819
|
+
const json = await response.json();
|
|
820
|
+
const data = json.data ?? json;
|
|
821
|
+
this.tokenManager.setTokens(
|
|
822
|
+
data.access_token,
|
|
823
|
+
data.refresh_token || refreshToken,
|
|
824
|
+
data.expires_in || 900
|
|
825
|
+
);
|
|
826
|
+
if (data.user) this.tokenManager.setUser(data.user);
|
|
827
|
+
this.tokenManager.persist();
|
|
828
|
+
}
|
|
829
|
+
async logout() {
|
|
830
|
+
const accessToken = this.tokenManager.getAccessToken();
|
|
831
|
+
const refreshToken = this.tokenManager.getRefreshToken();
|
|
832
|
+
this.tokenManager.clear();
|
|
833
|
+
this.tokenManager.persist();
|
|
834
|
+
if (accessToken) {
|
|
835
|
+
this.http.request(`${this.baseUrl}/identity/api/v1/auth/logout`, {
|
|
836
|
+
method: "POST",
|
|
837
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${accessToken}` },
|
|
838
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
839
|
+
}).catch(() => {
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
if (typeof window !== "undefined") window.localStorage.removeItem("authms_auth_tokens");
|
|
843
|
+
}
|
|
844
|
+
async getProfile() {
|
|
845
|
+
const response = await this.http.request(`${this.baseUrl}/identity/api/v1/auth/me`, {
|
|
846
|
+
method: "GET",
|
|
847
|
+
headers: { "Authorization": `Bearer ${this.tokenManager.getAccessToken()}` }
|
|
848
|
+
});
|
|
849
|
+
if (response.status === 401) return null;
|
|
850
|
+
if (!response.ok) return null;
|
|
851
|
+
const json = await response.json();
|
|
852
|
+
const data = json.data ?? json;
|
|
853
|
+
this.tokenManager.setUser(data);
|
|
854
|
+
return data;
|
|
855
|
+
}
|
|
856
|
+
async buildLoginBody(credentials, authConfig) {
|
|
857
|
+
const processed = await processPasswordForTransmission(
|
|
858
|
+
credentials.password,
|
|
859
|
+
{
|
|
860
|
+
mode: authConfig.passwordPolicy.mode,
|
|
861
|
+
tenantId: authConfig.tenantId || credentials.tenantId || "",
|
|
862
|
+
requireUpper: false,
|
|
863
|
+
minLength: 0,
|
|
864
|
+
publicKey: authConfig.transmissionPublicKey || ""
|
|
865
|
+
},
|
|
866
|
+
this.keyExchangeFn
|
|
867
|
+
);
|
|
868
|
+
const body = {
|
|
869
|
+
identity: credentials.email || credentials.phone || credentials.username || "",
|
|
870
|
+
password: processed.password,
|
|
871
|
+
password_transmission: processed.passwordTransmission
|
|
872
|
+
};
|
|
873
|
+
if (credentials.tenantId) body["tenant_id"] = credentials.tenantId;
|
|
874
|
+
if (processed.keyExchangeId) body["key_exchange_id"] = processed.keyExchangeId;
|
|
875
|
+
if (processed.clientPubKey) body["client_pub_key"] = processed.clientPubKey;
|
|
876
|
+
if (credentials.captchaToken) body["captcha_token"] = credentials.captchaToken;
|
|
877
|
+
if (credentials.captchaProvider) body["captcha_provider"] = credentials.captchaProvider;
|
|
878
|
+
if (credentials.captchaChallengeId) body["captcha_challenge_id"] = credentials.captchaChallengeId;
|
|
879
|
+
return body;
|
|
880
|
+
}
|
|
881
|
+
async solveCaptchaChallenge() {
|
|
882
|
+
const challengeResp = await this.http.request(
|
|
883
|
+
`${this.baseUrl}/identity/api/v1/auth/captcha/challenge?provider=pow&difficulty=4`
|
|
884
|
+
);
|
|
885
|
+
const cJson = await challengeResp.json();
|
|
886
|
+
const cData = cJson.data ?? cJson;
|
|
887
|
+
const cd = cData.data ?? cData ?? cData;
|
|
888
|
+
const powChallenge = String(cd.challenge ?? cd["challenge"] ?? "");
|
|
889
|
+
const difficulty = Number(cd.difficulty ?? cd["difficulty"] ?? 4);
|
|
890
|
+
return solveProofOfWork(powChallenge, difficulty);
|
|
891
|
+
}
|
|
892
|
+
handleAuthResponse(json) {
|
|
893
|
+
const data = json.data ?? json;
|
|
894
|
+
const result = {
|
|
895
|
+
accessToken: data.access_token,
|
|
896
|
+
refreshToken: data.refresh_token,
|
|
897
|
+
expiresIn: data.expires_in || 900,
|
|
898
|
+
tokenType: data.token_type || "Bearer",
|
|
899
|
+
user: data.user || { id: data.user_id || "" }
|
|
900
|
+
};
|
|
901
|
+
this.tokenManager.setTokens(result.accessToken, result.refreshToken, result.expiresIn);
|
|
902
|
+
this.tokenManager.setUser(result.user);
|
|
903
|
+
this.tokenManager.persist();
|
|
904
|
+
return result;
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// src/discovery.ts
|
|
909
|
+
var Discovery = class {
|
|
910
|
+
/**
|
|
911
|
+
* @param http HTTP adapter
|
|
912
|
+
* @param discoveryBaseUrl discovery 请求使用的基础 URL(proxy 模式用 apiUrl,否则留空用 issuer)
|
|
913
|
+
*/
|
|
914
|
+
constructor(http, discoveryBaseUrl) {
|
|
915
|
+
this.metadata = null;
|
|
916
|
+
this.http = http;
|
|
917
|
+
this.discoveryBaseUrl = (discoveryBaseUrl || "").replace(/\/bff\/?$/, "").replace(/\/$/, "");
|
|
918
|
+
}
|
|
919
|
+
async discover(issuer) {
|
|
920
|
+
if (this.metadata) return this.metadata;
|
|
921
|
+
const base = this.discoveryBaseUrl || issuer.replace(/\/$/, "");
|
|
922
|
+
const url = `${base}/.well-known/openid-configuration`;
|
|
923
|
+
const response = await this.http.request(url);
|
|
924
|
+
if (!response.ok) {
|
|
925
|
+
throw new Error(`OIDC Discovery failed: HTTP ${response.status}`);
|
|
926
|
+
}
|
|
927
|
+
const json = await response.json();
|
|
928
|
+
const metadata = json.data ?? json;
|
|
929
|
+
if (!metadata.issuer || !metadata.authorization_endpoint || !metadata.token_endpoint) {
|
|
930
|
+
throw new Error("OIDC Discovery response missing required fields (issuer, authorization_endpoint, token_endpoint)");
|
|
931
|
+
}
|
|
932
|
+
this.metadata = metadata;
|
|
933
|
+
return metadata;
|
|
934
|
+
}
|
|
935
|
+
getAuthorizationEndpoint() {
|
|
936
|
+
return this.metadata?.authorization_endpoint ?? null;
|
|
937
|
+
}
|
|
938
|
+
getTokenEndpoint() {
|
|
939
|
+
return this.metadata?.token_endpoint ?? null;
|
|
940
|
+
}
|
|
941
|
+
getEndSessionEndpoint() {
|
|
942
|
+
return this.metadata?.end_session_endpoint ?? null;
|
|
943
|
+
}
|
|
944
|
+
getJWKSUri() {
|
|
945
|
+
return this.metadata?.jwks_uri ?? null;
|
|
946
|
+
}
|
|
947
|
+
getMetadata() {
|
|
948
|
+
return this.metadata;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/sync.ts
|
|
953
|
+
var TabSync = class {
|
|
954
|
+
constructor(onLogout, onTokenChange) {
|
|
955
|
+
this.channel = null;
|
|
956
|
+
this.onLogout = onLogout;
|
|
957
|
+
this.onTokenChange = onTokenChange;
|
|
958
|
+
}
|
|
959
|
+
listen() {
|
|
960
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
961
|
+
try {
|
|
962
|
+
this.channel = new BroadcastChannel("authms:sync");
|
|
963
|
+
this.channel.onmessage = (event) => {
|
|
964
|
+
const { type } = event.data;
|
|
965
|
+
switch (type) {
|
|
966
|
+
case "LOGOUT":
|
|
967
|
+
this.onLogout();
|
|
968
|
+
break;
|
|
969
|
+
case "TOKEN_REFRESHED":
|
|
970
|
+
case "LOGIN":
|
|
971
|
+
this.onTokenChange();
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
} catch {
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
broadcast(type) {
|
|
979
|
+
if (!this.channel) return;
|
|
980
|
+
try {
|
|
981
|
+
this.channel.postMessage({ type, timestamp: Date.now() });
|
|
982
|
+
} catch {
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
close() {
|
|
986
|
+
if (this.channel) {
|
|
987
|
+
this.channel.close();
|
|
988
|
+
this.channel = null;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
// src/authms.ts
|
|
994
|
+
var AuthMS = class {
|
|
995
|
+
constructor(config) {
|
|
996
|
+
this.tabSync = null;
|
|
997
|
+
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
998
|
+
this._ready = false;
|
|
999
|
+
this._userCache = null;
|
|
1000
|
+
this._authConfig = null;
|
|
1001
|
+
if (!config.appId) throw new AuthmsError("CONFIG_ERROR", "appId is required", 500);
|
|
1002
|
+
if (!config.issuer) throw new AuthmsError("CONFIG_ERROR", "issuer is required", 500);
|
|
1003
|
+
this.config = config;
|
|
1004
|
+
const apiUrl = config.apiUrl ?? config.issuer;
|
|
1005
|
+
this.tokenManager = new TokenManager(config.platform.storage, config.storagePrefix);
|
|
1006
|
+
this.discovery = new Discovery(config.platform.http, config.apiUrl);
|
|
1007
|
+
this.authClient = new AuthClient({
|
|
1008
|
+
tokenManager: this.tokenManager,
|
|
1009
|
+
http: config.platform.http,
|
|
1010
|
+
baseUrl: apiUrl
|
|
1011
|
+
});
|
|
1012
|
+
this.api = new ApiClient({
|
|
1013
|
+
baseUrl: apiUrl,
|
|
1014
|
+
tokenManager: this.tokenManager,
|
|
1015
|
+
http: config.platform.http,
|
|
1016
|
+
refreshTokenFn: () => this.authClient.refreshToken(),
|
|
1017
|
+
onForceLogout: () => {
|
|
1018
|
+
this.emit("LOGGED_OUT");
|
|
1019
|
+
this.tabSync?.broadcast("LOGOUT");
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
if (config.syncTabs !== false) {
|
|
1023
|
+
this.tabSync = new TabSync(
|
|
1024
|
+
() => {
|
|
1025
|
+
this.tokenManager.clear();
|
|
1026
|
+
this._userCache = null;
|
|
1027
|
+
this.emit("LOGGED_OUT");
|
|
1028
|
+
},
|
|
1029
|
+
async () => {
|
|
1030
|
+
await this.tokenManager.load();
|
|
1031
|
+
this._userCache = this.tokenManager.getUser();
|
|
1032
|
+
this.emit("TOKEN_CHANGED");
|
|
1033
|
+
this.emit("USER_CHANGED");
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
this.tabSync.listen();
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
async initialize() {
|
|
1040
|
+
await this.tokenManager.load();
|
|
1041
|
+
try {
|
|
1042
|
+
await this.discovery.discover(this.config.issuer);
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
const accessToken = this.tokenManager.getAccessToken();
|
|
1046
|
+
if (accessToken) {
|
|
1047
|
+
this._userCache = this.tokenManager.getUser();
|
|
1048
|
+
if (!this._userCache) {
|
|
1049
|
+
try {
|
|
1050
|
+
this._userCache = await this.authClient.getProfile();
|
|
1051
|
+
if (this._userCache) {
|
|
1052
|
+
this.tokenManager.setUser(this._userCache);
|
|
1053
|
+
this.tokenManager.persist();
|
|
1054
|
+
}
|
|
1055
|
+
} catch {
|
|
1056
|
+
if (!this.tokenManager.isTokenExpired(accessToken)) {
|
|
1057
|
+
const claims = this.tokenManager.decodeToken(accessToken);
|
|
1058
|
+
this._userCache = claims ?? {};
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
this.tokenManager.onTokenChange((token) => {
|
|
1063
|
+
if (!token) {
|
|
1064
|
+
this._userCache = null;
|
|
1065
|
+
this.emit("LOGGED_OUT");
|
|
1066
|
+
}
|
|
1067
|
+
this.emit("TOKEN_CHANGED", { token });
|
|
1068
|
+
this.emit("USER_CHANGED");
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
this._ready = true;
|
|
1072
|
+
this.emit("READY");
|
|
1073
|
+
if (this._userCache) {
|
|
1074
|
+
this.emit("USER_CHANGED");
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
isReady() {
|
|
1078
|
+
return this._ready;
|
|
1079
|
+
}
|
|
1080
|
+
get user() {
|
|
1081
|
+
return this._userCache ?? this.tokenManager.getUser();
|
|
1082
|
+
}
|
|
1083
|
+
get authConfig() {
|
|
1084
|
+
return this._authConfig;
|
|
1085
|
+
}
|
|
1086
|
+
async fetchAuthConfig(tenantId) {
|
|
1087
|
+
const config = await this.authClient.fetchAuthConfig(tenantId);
|
|
1088
|
+
this._authConfig = config;
|
|
1089
|
+
return config;
|
|
1090
|
+
}
|
|
1091
|
+
async getAccessToken() {
|
|
1092
|
+
const token = this.tokenManager.getAccessToken();
|
|
1093
|
+
if (token) return token;
|
|
1094
|
+
try {
|
|
1095
|
+
await this.authClient.refreshToken();
|
|
1096
|
+
return this.tokenManager.getAccessToken();
|
|
1097
|
+
} catch {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
getRefreshToken() {
|
|
1102
|
+
return this.tokenManager.getRefreshToken();
|
|
1103
|
+
}
|
|
1104
|
+
isAuthenticated() {
|
|
1105
|
+
return this.tokenManager.isAuthenticated();
|
|
1106
|
+
}
|
|
1107
|
+
async login(credentials) {
|
|
1108
|
+
const result = await this.authClient.login(credentials);
|
|
1109
|
+
this._userCache = result.user;
|
|
1110
|
+
this.emit("USER_CHANGED");
|
|
1111
|
+
this.emit("TOKEN_CHANGED");
|
|
1112
|
+
this.tabSync?.broadcast("LOGIN");
|
|
1113
|
+
return result;
|
|
1114
|
+
}
|
|
1115
|
+
async loginWithOAuth(options) {
|
|
1116
|
+
await this.authClient.loginWithOAuth(options);
|
|
1117
|
+
}
|
|
1118
|
+
async handleOAuthCallback(url) {
|
|
1119
|
+
const result = await this.authClient.handleOAuthCallback(url);
|
|
1120
|
+
this._userCache = result.user;
|
|
1121
|
+
this.emit("USER_CHANGED");
|
|
1122
|
+
this.tabSync?.broadcast("LOGIN");
|
|
1123
|
+
return result;
|
|
1124
|
+
}
|
|
1125
|
+
async register(data) {
|
|
1126
|
+
const result = await this.authClient.register(data);
|
|
1127
|
+
if (result.accessToken) {
|
|
1128
|
+
this._userCache = result.user;
|
|
1129
|
+
this.emit("USER_CHANGED");
|
|
1130
|
+
this.tabSync?.broadcast("LOGIN");
|
|
1131
|
+
}
|
|
1132
|
+
return result;
|
|
1133
|
+
}
|
|
1134
|
+
async logout() {
|
|
1135
|
+
await this.authClient.logout();
|
|
1136
|
+
this._userCache = null;
|
|
1137
|
+
this.emit("LOGGED_OUT");
|
|
1138
|
+
this.emit("USER_CHANGED");
|
|
1139
|
+
this.tabSync?.broadcast("LOGOUT");
|
|
1140
|
+
}
|
|
1141
|
+
async getProfile() {
|
|
1142
|
+
return this.authClient.getProfile();
|
|
1143
|
+
}
|
|
1144
|
+
setTenantId(tenantId) {
|
|
1145
|
+
this.tokenManager.setTenantId(tenantId);
|
|
1146
|
+
this.tokenManager.persist();
|
|
1147
|
+
this._authConfig = null;
|
|
1148
|
+
try {
|
|
1149
|
+
this.authClient.clearConfigCache();
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
getTenantId() {
|
|
1154
|
+
return this.tokenManager.getTenantId();
|
|
1155
|
+
}
|
|
1156
|
+
use(plugin) {
|
|
1157
|
+
plugin.install(this);
|
|
1158
|
+
}
|
|
1159
|
+
on(event, handler) {
|
|
1160
|
+
if (!this.eventHandlers.has(event)) {
|
|
1161
|
+
this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
1162
|
+
}
|
|
1163
|
+
this.eventHandlers.get(event).add(handler);
|
|
1164
|
+
return () => this.eventHandlers.get(event)?.delete(handler);
|
|
1165
|
+
}
|
|
1166
|
+
emit(event, ...args) {
|
|
1167
|
+
const handlers = this.eventHandlers.get(event);
|
|
1168
|
+
if (handlers) {
|
|
1169
|
+
handlers.forEach((fn) => {
|
|
1170
|
+
try {
|
|
1171
|
+
fn(...args);
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
dispose() {
|
|
1178
|
+
this.tabSync?.close();
|
|
1179
|
+
this.eventHandlers.clear();
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
// src/platform/browser.ts
|
|
1184
|
+
var browserPlatform = {
|
|
1185
|
+
storage: {
|
|
1186
|
+
getItem(key) {
|
|
1187
|
+
try {
|
|
1188
|
+
return localStorage.getItem(key);
|
|
1189
|
+
} catch {
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
setItem(key, value) {
|
|
1194
|
+
try {
|
|
1195
|
+
localStorage.setItem(key, value);
|
|
1196
|
+
} catch {
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
removeItem(key) {
|
|
1200
|
+
try {
|
|
1201
|
+
localStorage.removeItem(key);
|
|
1202
|
+
} catch {
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
keys() {
|
|
1206
|
+
try {
|
|
1207
|
+
return Object.keys(localStorage);
|
|
1208
|
+
} catch {
|
|
1209
|
+
return [];
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
http: {
|
|
1214
|
+
request(input, init) {
|
|
1215
|
+
return fetch(input, init);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
// src/binding.ts
|
|
1221
|
+
function createPlatformBinding(config) {
|
|
1222
|
+
const coreConfig = {
|
|
1223
|
+
appId: config.appId,
|
|
1224
|
+
issuer: config.issuer,
|
|
1225
|
+
apiUrl: config.apiUrl,
|
|
1226
|
+
platform: config.platform || browserPlatform,
|
|
1227
|
+
storagePrefix: config.storagePrefix,
|
|
1228
|
+
syncTabs: config.syncTabs
|
|
1229
|
+
};
|
|
1230
|
+
const authms = new AuthMS(coreConfig);
|
|
1231
|
+
let user = null;
|
|
1232
|
+
let authConfig = null;
|
|
1233
|
+
let ready = false;
|
|
1234
|
+
const changeHandlers = /* @__PURE__ */ new Set();
|
|
1235
|
+
const emitChange = () => {
|
|
1236
|
+
changeHandlers.forEach((fn) => {
|
|
1237
|
+
try {
|
|
1238
|
+
fn();
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
};
|
|
1243
|
+
authms.initialize().then(async () => {
|
|
1244
|
+
user = authms.user;
|
|
1245
|
+
try {
|
|
1246
|
+
authConfig = await authms.fetchAuthConfig();
|
|
1247
|
+
} catch {
|
|
1248
|
+
}
|
|
1249
|
+
ready = true;
|
|
1250
|
+
emitChange();
|
|
1251
|
+
}).catch(() => {
|
|
1252
|
+
ready = true;
|
|
1253
|
+
emitChange();
|
|
1254
|
+
});
|
|
1255
|
+
authms.on("USER_CHANGED", () => {
|
|
1256
|
+
user = authms.user;
|
|
1257
|
+
emitChange();
|
|
1258
|
+
});
|
|
1259
|
+
return {
|
|
1260
|
+
authms,
|
|
1261
|
+
getUser: () => user,
|
|
1262
|
+
getAuthConfig: () => authConfig,
|
|
1263
|
+
isReady: () => ready,
|
|
1264
|
+
isAuthenticated: () => authms.isAuthenticated(),
|
|
1265
|
+
onChange: (handler) => {
|
|
1266
|
+
changeHandlers.add(handler);
|
|
1267
|
+
return () => changeHandlers.delete(handler);
|
|
1268
|
+
},
|
|
1269
|
+
login: (c) => authms.login(c),
|
|
1270
|
+
loginWithOAuth: (o) => authms.loginWithOAuth(o),
|
|
1271
|
+
register: (d) => authms.register(d),
|
|
1272
|
+
logout: () => authms.logout(),
|
|
1273
|
+
getAccessToken: () => authms.getAccessToken(),
|
|
1274
|
+
setTenantId: (id) => authms.setTenantId(id),
|
|
1275
|
+
getTenantId: () => authms.getTenantId(),
|
|
1276
|
+
dispose: () => {
|
|
1277
|
+
changeHandlers.clear();
|
|
1278
|
+
authms.dispose();
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/platform/memory.ts
|
|
1284
|
+
function memoryPlatform() {
|
|
1285
|
+
const store = /* @__PURE__ */ new Map();
|
|
1286
|
+
return {
|
|
1287
|
+
storage: {
|
|
1288
|
+
getItem(key) {
|
|
1289
|
+
return store.get(key) ?? null;
|
|
1290
|
+
},
|
|
1291
|
+
setItem(key, value) {
|
|
1292
|
+
store.set(key, value);
|
|
1293
|
+
},
|
|
1294
|
+
removeItem(key) {
|
|
1295
|
+
store.delete(key);
|
|
1296
|
+
},
|
|
1297
|
+
keys() {
|
|
1298
|
+
return Array.from(store.keys());
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
http: {
|
|
1302
|
+
request(_input, _init) {
|
|
1303
|
+
throw new Error("HttpAdapter.request not implemented in memory platform");
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/types.ts
|
|
1310
|
+
var ERROR_CODES = {
|
|
1311
|
+
CAPTCHA_REQUIRED: "CAPTCHA_REQUIRED",
|
|
1312
|
+
INVALID_CAPTCHA: "INVALID_CAPTCHA",
|
|
1313
|
+
TOKEN_REUSE: "TOKEN_REUSE",
|
|
1314
|
+
SESSION_EXPIRED: "SESSION_EXPIRED",
|
|
1315
|
+
REFRESH_FAILED: "REFRESH_FAILED",
|
|
1316
|
+
NOT_AUTHENTICATED: "NOT_AUTHENTICATED"
|
|
1317
|
+
};
|
|
1318
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1319
|
+
0 && (module.exports = {
|
|
1320
|
+
ApiClient,
|
|
1321
|
+
AuthClient,
|
|
1322
|
+
AuthMS,
|
|
1323
|
+
AuthmsApiError,
|
|
1324
|
+
AuthmsAuthError,
|
|
1325
|
+
AuthmsConfigError,
|
|
1326
|
+
AuthmsError,
|
|
1327
|
+
AuthmsNetworkError,
|
|
1328
|
+
Discovery,
|
|
1329
|
+
ERROR_CODES,
|
|
1330
|
+
TabSync,
|
|
1331
|
+
TokenManager,
|
|
1332
|
+
browserPlatform,
|
|
1333
|
+
createPlatformBinding,
|
|
1334
|
+
memoryPlatform,
|
|
1335
|
+
processPasswordForTransmission,
|
|
1336
|
+
solveProofOfWork
|
|
1337
|
+
});
|