@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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { StorageAdapter } from './platform/types';
|
|
2
|
+
import type { TokenClaims } from './types';
|
|
3
|
+
|
|
4
|
+
interface TokenStore {
|
|
5
|
+
accessToken: string | null;
|
|
6
|
+
refreshToken: string | null;
|
|
7
|
+
user: Record<string, unknown> | null;
|
|
8
|
+
tenantId: string | null;
|
|
9
|
+
expiresAt: number | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const STORAGE_PREFIX = 'authms_';
|
|
13
|
+
|
|
14
|
+
export class TokenManager {
|
|
15
|
+
private storage: StorageAdapter;
|
|
16
|
+
private prefix: string;
|
|
17
|
+
private store: TokenStore;
|
|
18
|
+
private changeListeners: Set<(token: string | null) => void> = new Set();
|
|
19
|
+
|
|
20
|
+
constructor(storage: StorageAdapter, prefix?: string) {
|
|
21
|
+
this.storage = storage;
|
|
22
|
+
this.prefix = prefix ?? STORAGE_PREFIX;
|
|
23
|
+
this.store = this.initialStore();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private initialStore(): TokenStore {
|
|
27
|
+
return {
|
|
28
|
+
accessToken: null,
|
|
29
|
+
refreshToken: null,
|
|
30
|
+
user: null,
|
|
31
|
+
tenantId: null,
|
|
32
|
+
expiresAt: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private storageKey(key: string): string {
|
|
37
|
+
return `${this.prefix}${key}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async load(): Promise<void> {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await this.storage.getItem(this.storageKey('tokens'));
|
|
43
|
+
if (raw) {
|
|
44
|
+
const parsed = JSON.parse(raw) as TokenStore;
|
|
45
|
+
this.store = { ...this.initialStore(), ...parsed };
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
this.store = this.initialStore();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async persist(): Promise<void> {
|
|
53
|
+
try {
|
|
54
|
+
await this.storage.setItem(this.storageKey('tokens'), JSON.stringify(this.store));
|
|
55
|
+
} catch { /* quota exceeded, non-critical */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getAccessToken(): string | null {
|
|
59
|
+
if (this.store.accessToken && this.isTokenExpired(this.store.accessToken)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return this.store.accessToken;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getRefreshToken(): string | null {
|
|
66
|
+
return this.store.refreshToken;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getUser(): Record<string, unknown> | null {
|
|
70
|
+
return this.store.user;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getTenantId(): string | null {
|
|
74
|
+
return this.store.tenantId;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getExpiresAt(): number | null {
|
|
78
|
+
return this.store.expiresAt;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isAuthenticated(): boolean {
|
|
82
|
+
return !!this.store.accessToken && !this.isTokenExpired(this.store.accessToken!);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setTokens(accessToken: string, refreshToken: string, expiresIn: number): void {
|
|
86
|
+
this.store.accessToken = accessToken;
|
|
87
|
+
this.store.refreshToken = refreshToken;
|
|
88
|
+
this.store.expiresAt = Date.now() + expiresIn * 1000;
|
|
89
|
+
this.notifyListeners(accessToken);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setUser(user: Record<string, unknown>): void {
|
|
93
|
+
this.store.user = user;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setTenantId(tenantId: string | null): void {
|
|
97
|
+
this.store.tenantId = tenantId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clear(): void {
|
|
101
|
+
this.store = this.initialStore();
|
|
102
|
+
this.notifyListeners(null);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onTokenChange(listener: (token: string | null) => void): () => void {
|
|
106
|
+
this.changeListeners.add(listener);
|
|
107
|
+
return () => this.changeListeners.delete(listener);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private notifyListeners(token: string | null): void {
|
|
111
|
+
this.changeListeners.forEach(fn => {
|
|
112
|
+
try { fn(token); } catch { /* listener error shouldn't break others */ }
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
decodeToken(token: string): TokenClaims | null {
|
|
117
|
+
try {
|
|
118
|
+
const base64Url = token.split('.')[1];
|
|
119
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
120
|
+
const json = decodeURIComponent(
|
|
121
|
+
atob(base64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
|
|
122
|
+
);
|
|
123
|
+
return JSON.parse(json) as TokenClaims;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
isTokenExpired(token: string): boolean {
|
|
130
|
+
const claims = this.decodeToken(token);
|
|
131
|
+
if (!claims?.exp) return true;
|
|
132
|
+
return Date.now() >= claims.exp * 1000;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getTokenRemainingTime(token: string): number {
|
|
136
|
+
const claims = this.decodeToken(token);
|
|
137
|
+
if (!claims?.exp) return 0;
|
|
138
|
+
return Math.max(0, claims.exp * 1000 - Date.now());
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export interface User {
|
|
2
|
+
id: string;
|
|
3
|
+
username?: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
phone?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
avatar?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AuthResult {
|
|
12
|
+
user: User;
|
|
13
|
+
accessToken: string;
|
|
14
|
+
refreshToken: string;
|
|
15
|
+
expiresIn: number;
|
|
16
|
+
tokenType: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LoginRequest {
|
|
20
|
+
email?: string;
|
|
21
|
+
phone?: string;
|
|
22
|
+
username?: string;
|
|
23
|
+
password: string;
|
|
24
|
+
tenantId?: string;
|
|
25
|
+
captchaToken?: string;
|
|
26
|
+
captchaProvider?: string;
|
|
27
|
+
captchaChallengeId?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RegisterRequest {
|
|
31
|
+
email?: string;
|
|
32
|
+
phone?: string;
|
|
33
|
+
username?: string;
|
|
34
|
+
password: string;
|
|
35
|
+
tenantId?: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OAuthOptions {
|
|
40
|
+
provider: string;
|
|
41
|
+
redirectUri?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TokenClaims {
|
|
45
|
+
sub?: string;
|
|
46
|
+
user_id?: string;
|
|
47
|
+
tenant_id?: string;
|
|
48
|
+
exp?: number;
|
|
49
|
+
iat?: number;
|
|
50
|
+
session_id?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
role?: string;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type AuthmsEvent =
|
|
57
|
+
| 'READY'
|
|
58
|
+
| 'USER_CHANGED'
|
|
59
|
+
| 'TOKEN_CHANGED'
|
|
60
|
+
| 'NOT_AUTHENTICATED'
|
|
61
|
+
| 'LOGGED_OUT'
|
|
62
|
+
| 'SECURITY_ALERT';
|
|
63
|
+
|
|
64
|
+
export interface SecurityAlert {
|
|
65
|
+
reason: 'token_reuse' | 'suspicious_activity' | 'session_revoked';
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PasswordPolicyConfig {
|
|
70
|
+
mode: string;
|
|
71
|
+
minLength: number;
|
|
72
|
+
maxLength?: number;
|
|
73
|
+
requireUpper: boolean;
|
|
74
|
+
requireLower?: boolean;
|
|
75
|
+
requireDigit?: boolean;
|
|
76
|
+
requireSpecial?: boolean;
|
|
77
|
+
tenantId: string;
|
|
78
|
+
publicKey: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface BrandingInfo {
|
|
82
|
+
logoUrl?: string;
|
|
83
|
+
primaryColor?: string;
|
|
84
|
+
companyName?: string;
|
|
85
|
+
loginPageTitle?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface TenantAuthConfig {
|
|
89
|
+
tenantId: string;
|
|
90
|
+
tenantName: string;
|
|
91
|
+
displayName: string;
|
|
92
|
+
membershipApproval: string;
|
|
93
|
+
loginMethods: string[];
|
|
94
|
+
oauthProviders: string[];
|
|
95
|
+
passwordPolicy: PasswordPolicyConfig;
|
|
96
|
+
captchaEnabled: boolean;
|
|
97
|
+
captchaProvider: string;
|
|
98
|
+
silentChallengeEnabled: boolean;
|
|
99
|
+
transmissionPublicKey: string;
|
|
100
|
+
oauthClientId: string;
|
|
101
|
+
passkeyEnabled: boolean;
|
|
102
|
+
magicLinkEnabled: boolean;
|
|
103
|
+
branding: BrandingInfo | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const ERROR_CODES = {
|
|
107
|
+
CAPTCHA_REQUIRED: 'CAPTCHA_REQUIRED',
|
|
108
|
+
INVALID_CAPTCHA: 'INVALID_CAPTCHA',
|
|
109
|
+
TOKEN_REUSE: 'TOKEN_REUSE',
|
|
110
|
+
SESSION_EXPIRED: 'SESSION_EXPIRED',
|
|
111
|
+
REFRESH_FAILED: 'REFRESH_FAILED',
|
|
112
|
+
NOT_AUTHENTICATED: 'NOT_AUTHENTICATED',
|
|
113
|
+
} as const;
|