@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/src/binding.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * createPlatformBinding — 共享的 Core 绑定逻辑
3
+ *
4
+ * 用于各框架适配器,消除重复的 AuthMS 初始化/状态管理/事件订阅代码。
5
+ * 适配器只需将此 binding 包装成框架特定的组件/hook。
6
+ */
7
+ import { AuthMS, type AuthmsConfig } from './authms';
8
+ import type { AuthResult, LoginRequest, OAuthOptions } from './types';
9
+ import type { AuthmsPlatform } from './platform/types';
10
+ import { browserPlatform } from './platform/browser';
11
+
12
+ export interface BindingConfig {
13
+ appId: string;
14
+ issuer: string;
15
+ apiUrl?: string;
16
+ platform?: AuthmsPlatform;
17
+ storagePrefix?: string;
18
+ syncTabs?: boolean;
19
+ }
20
+
21
+ export interface PlatformBinding {
22
+ authms: AuthMS;
23
+
24
+ /** 获取当前用户(由适配器调用) */
25
+ getUser(): Record<string, unknown> | null;
26
+
27
+ /** 获取 auth config(由适配器调用) */
28
+ getAuthConfig(): Record<string, unknown> | null;
29
+
30
+ /** 是否已就绪 */
31
+ isReady(): boolean;
32
+
33
+ /** 是否已认证 */
34
+ isAuthenticated(): boolean;
35
+
36
+ /** 注册状态变更回调,返回取消订阅函数 */
37
+ onChange(handler: () => void): () => void;
38
+
39
+ /** 登录 */
40
+ login(credentials: LoginRequest): Promise<AuthResult>;
41
+
42
+ /** OAuth 登录 */
43
+ loginWithOAuth(options: OAuthOptions): Promise<void>;
44
+
45
+ /** 注册 */
46
+ register(data: LoginRequest): Promise<AuthResult>;
47
+
48
+ /** 登出 */
49
+ logout(): Promise<void>;
50
+
51
+ /** 获取 access token */
52
+ getAccessToken(): Promise<string | null>;
53
+
54
+ /** 切换租户 */
55
+ setTenantId(tenantId: string | null): void;
56
+
57
+ /** 获取当前租户 */
58
+ getTenantId(): string | null;
59
+
60
+ /** 销毁 */
61
+ dispose(): void;
62
+ }
63
+
64
+ export function createPlatformBinding(config: BindingConfig): PlatformBinding {
65
+ const coreConfig: AuthmsConfig = {
66
+ appId: config.appId,
67
+ issuer: config.issuer,
68
+ apiUrl: config.apiUrl,
69
+ platform: config.platform || browserPlatform,
70
+ storagePrefix: config.storagePrefix,
71
+ syncTabs: config.syncTabs,
72
+ };
73
+
74
+ const authms = new AuthMS(coreConfig);
75
+
76
+ let user: Record<string, unknown> | null = null;
77
+ let authConfig: Record<string, unknown> | null = null;
78
+ let ready = false;
79
+ const changeHandlers = new Set<() => void>();
80
+
81
+ const emitChange = () => {
82
+ changeHandlers.forEach((fn) => { try { fn(); } catch {} });
83
+ };
84
+
85
+ // 初始化
86
+ authms.initialize().then(async () => {
87
+ user = authms.user as unknown as Record<string, unknown> | null;
88
+ try {
89
+ authConfig = await authms.fetchAuthConfig();
90
+ } catch {}
91
+ ready = true;
92
+ emitChange();
93
+ }).catch(() => {
94
+ ready = true;
95
+ emitChange();
96
+ });
97
+
98
+ // 订阅事件
99
+ authms.on('USER_CHANGED', () => {
100
+ user = authms.user as unknown as Record<string, unknown> | null;
101
+ emitChange();
102
+ });
103
+
104
+ return {
105
+ authms,
106
+ getUser: () => user,
107
+ getAuthConfig: () => authConfig,
108
+ isReady: () => ready,
109
+ isAuthenticated: () => authms.isAuthenticated(),
110
+ onChange: (handler) => {
111
+ changeHandlers.add(handler);
112
+ return () => changeHandlers.delete(handler);
113
+ },
114
+ login: (c) => authms.login(c),
115
+ loginWithOAuth: (o) => authms.loginWithOAuth(o),
116
+ register: (d) => authms.register(d),
117
+ logout: () => authms.logout(),
118
+ getAccessToken: () => authms.getAccessToken(),
119
+ setTenantId: (id) => authms.setTenantId(id),
120
+ getTenantId: () => authms.getTenantId(),
121
+ dispose: () => {
122
+ changeHandlers.clear();
123
+ authms.dispose();
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ processPasswordForTransmission,
3
+ type TransmissionResult,
4
+ type KeyExchangeFn,
5
+ } from './password-transmission';
6
+ export { solveProofOfWork } from './pow-solver';
@@ -0,0 +1,198 @@
1
+ /**
2
+ * 密码传输安全模块
3
+ *
4
+ * 移植自 web/packages/shared/src/utils/password-transmission.ts
5
+ * 根据租户 password_transmission 策略在前端对密码进行预处理:
6
+ * plain — 不做处理,原始密码传输
7
+ * hash — SHA-256(password + tenantId)
8
+ * symmetric — ECDH 密钥交换 + AES-256-GCM
9
+ * asymmetric — RSA-OAEP 公钥加密
10
+ */
11
+ import type { PasswordPolicyConfig } from '../types';
12
+
13
+ const ENCODER = new TextEncoder();
14
+
15
+ function base64ToBytes(base64: string): Uint8Array {
16
+ const binary = atob(base64);
17
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0));
18
+ }
19
+
20
+ function bytesToHex(bytes: Uint8Array): string {
21
+ return Array.from(bytes)
22
+ .map((b) => b.toString(16).padStart(2, '0'))
23
+ .join('');
24
+ }
25
+
26
+ export interface TransmissionResult {
27
+ password: string;
28
+ passwordTransmission: string;
29
+ clientNonce?: string;
30
+ keyExchangeId?: string;
31
+ clientPubKey?: string;
32
+ }
33
+
34
+ export interface KeyExchangeResult {
35
+ serverPubKey: string;
36
+ keyExchangeId: string;
37
+ }
38
+
39
+ export type KeyExchangeFn = () => Promise<KeyExchangeResult>;
40
+
41
+ /**
42
+ * 根据传输模式预处理密码。
43
+ */
44
+ export async function processPasswordForTransmission(
45
+ rawPassword: string,
46
+ policy: PasswordPolicyConfig,
47
+ keyExchangeFn?: KeyExchangeFn,
48
+ ): Promise<TransmissionResult> {
49
+ const mode = policy.mode || 'plain';
50
+ const result: TransmissionResult = {
51
+ password: rawPassword,
52
+ passwordTransmission: mode,
53
+ };
54
+
55
+ switch (mode) {
56
+ case 'hash': {
57
+ const input = rawPassword + '|' + policy.tenantId;
58
+ try {
59
+ const hashBuffer = await crypto.subtle.digest('SHA-256', ENCODER.encode(input));
60
+ result.password = bytesToHex(new Uint8Array(hashBuffer));
61
+ } catch {
62
+ result.password = sha256HexPureJS(input);
63
+ }
64
+ result.passwordTransmission = 'hash';
65
+ break;
66
+ }
67
+
68
+ case 'symmetric': {
69
+ if (!keyExchangeFn) {
70
+ throw new Error('keyExchangeFn is required for symmetric mode');
71
+ }
72
+
73
+ const keResult = await keyExchangeFn();
74
+ const serverPubBytes = base64ToBytes(keResult.serverPubKey);
75
+
76
+ const clientKeyPair = await crypto.subtle.generateKey(
77
+ { name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits'],
78
+ );
79
+
80
+ const serverPubKey = await crypto.subtle.importKey(
81
+ 'raw', serverPubBytes as BufferSource, { name: 'ECDH', namedCurve: 'P-256' }, false, [],
82
+ );
83
+
84
+ const sharedBits = await crypto.subtle.deriveBits(
85
+ { name: 'ECDH', public: serverPubKey },
86
+ clientKeyPair.privateKey, 256,
87
+ );
88
+ const aesKey = await crypto.subtle.importKey(
89
+ 'raw', sharedBits as BufferSource, 'AES-GCM', false, ['encrypt'],
90
+ );
91
+
92
+ const iv = crypto.getRandomValues(new Uint8Array(12));
93
+ const encoded = ENCODER.encode(rawPassword);
94
+ const ciphertext = await crypto.subtle.encrypt(
95
+ { name: 'AES-GCM', iv: iv as BufferSource }, aesKey, encoded,
96
+ );
97
+
98
+ const clientPubRaw = await crypto.subtle.exportKey('raw', clientKeyPair.publicKey);
99
+ const clientPubBytes = new Uint8Array(clientPubRaw);
100
+
101
+ const combined = new Uint8Array(12 + ciphertext.byteLength + clientPubBytes.byteLength);
102
+ combined.set(iv, 0);
103
+ combined.set(new Uint8Array(ciphertext as ArrayBuffer), 12);
104
+ combined.set(clientPubBytes, 12 + ciphertext.byteLength);
105
+
106
+ result.password = btoa(String.fromCharCode(...Array.from(combined)));
107
+ result.keyExchangeId = keResult.keyExchangeId;
108
+ result.clientPubKey = btoa(String.fromCharCode(...Array.from(new Uint8Array(clientPubRaw as ArrayBuffer))));
109
+ result.passwordTransmission = 'symmetric';
110
+ break;
111
+ }
112
+
113
+ case 'asymmetric': {
114
+ const publicKeyPem = policy.publicKey;
115
+ if (!publicKeyPem || publicKeyPem.length < 100) {
116
+ throw new Error('public_key is required for asymmetric mode');
117
+ }
118
+ const pemContents = publicKeyPem
119
+ .replace('-----BEGIN PUBLIC KEY-----', '')
120
+ .replace('-----END PUBLIC KEY-----', '')
121
+ .replace(/\s/g, '');
122
+ const derBytes = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
123
+ const publicKey = await crypto.subtle.importKey(
124
+ 'spki', derBytes, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt'],
125
+ );
126
+ const encoded = ENCODER.encode(rawPassword);
127
+ const ciphertext = await crypto.subtle.encrypt(
128
+ { name: 'RSA-OAEP' }, publicKey, encoded,
129
+ );
130
+ result.password = btoa(String.fromCharCode(...Array.from(new Uint8Array(ciphertext as ArrayBuffer))));
131
+ result.passwordTransmission = 'asymmetric';
132
+ break;
133
+ }
134
+
135
+ default:
136
+ result.passwordTransmission = 'plain';
137
+ break;
138
+ }
139
+
140
+ return result;
141
+ }
142
+
143
+ function ror(x: number, n: number): number {
144
+ return (x >>> n) | (x << (32 - n));
145
+ }
146
+
147
+ function sha256HexPureJS(input: string): string {
148
+ const msg = new TextEncoder().encode(input);
149
+ const msgBits = msg.length * 8;
150
+ const buf = new Uint8Array(((msg.length + 9 + 63) >>> 6) << 6);
151
+ buf.set(msg);
152
+ buf[msg.length] = 0x80;
153
+ const view = new DataView(buf.buffer);
154
+ view.setUint32(buf.length - 4, msgBits);
155
+
156
+ const K = new Uint32Array([
157
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
158
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
159
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
160
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
161
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
162
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
163
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
164
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
165
+ ]);
166
+
167
+ const H = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
168
+ const W = new Uint32Array(64);
169
+
170
+ for (let off = 0; off < buf.length; off += 64) {
171
+ for (let i = 0; i < 16; i++) W[i] = view.getUint32(off + i * 4);
172
+ for (let i = 16; i < 64; i++) {
173
+ const s0 = (ror(W[i - 15], 7) ^ ror(W[i - 15], 18) ^ (W[i - 15] >>> 3));
174
+ const s1 = (ror(W[i - 2], 17) ^ ror(W[i - 2], 19) ^ (W[i - 2] >>> 10));
175
+ W[i] = (W[i - 16] + s0 + W[i - 7] + s1) | 0;
176
+ }
177
+ let [a, b, c, d, e, f2, g, h] = H;
178
+ for (let i = 0; i < 64; i++) {
179
+ const S1 = (ror(e, 6) ^ ror(e, 11) ^ ror(e, 25));
180
+ const ch = (e & f2) ^ (~e & g);
181
+ const t1 = (h + S1 + ch + K[i] + W[i]) | 0;
182
+ const S0 = (ror(a, 2) ^ ror(a, 13) ^ ror(a, 22));
183
+ const maj = (a & b) ^ (a & c) ^ (b & c);
184
+ const t2 = (S0 + maj) | 0;
185
+ h = g; g = f2; f2 = e; e = (d + t1) | 0;
186
+ d = c; c = b; b = a; a = (t1 + t2) | 0;
187
+ }
188
+ H[0] = (H[0] + a) | 0; H[1] = (H[1] + b) | 0;
189
+ H[2] = (H[2] + c) | 0; H[3] = (H[3] + d) | 0;
190
+ H[4] = (H[4] + e) | 0; H[5] = (H[5] + f2) | 0;
191
+ H[6] = (H[6] + g) | 0; H[7] = (H[7] + h) | 0;
192
+ }
193
+
194
+ const digest = new Uint8Array(32);
195
+ const dv = new DataView(digest.buffer);
196
+ for (let i = 0; i < 8; i++) dv.setUint32(i * 4, H[i]);
197
+ return Array.from(digest).map((b) => b.toString(16).padStart(2, '0')).join('');
198
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * PoW (Proof-of-Work) 求解器
3
+ * 当 silentChallengeEnabled 时自动获取并求解 challenge
4
+ *
5
+ * 移植自 web/apps/auth-pages/src/lib/silent-challenge.ts
6
+ */
7
+ const SOLVER_TIMEOUT_MS = 5000;
8
+
9
+ export async function solveProofOfWork(
10
+ challenge: string,
11
+ difficulty: number = 4,
12
+ timeoutMs: number = SOLVER_TIMEOUT_MS,
13
+ ): Promise<string> {
14
+ const startTime = Date.now();
15
+ const encoder = new TextEncoder();
16
+ let nonce = 0;
17
+ const prefix = '0'.repeat(difficulty);
18
+
19
+ while (true) {
20
+ if (Date.now() - startTime > timeoutMs) {
21
+ throw new Error('PoW solver timeout');
22
+ }
23
+
24
+ const data = encoder.encode(challenge + nonce);
25
+ try {
26
+ const hash = await crypto.subtle.digest('SHA-256', data);
27
+ const hex = Array.from(new Uint8Array(hash))
28
+ .map((b) => b.toString(16).padStart(2, '0'))
29
+ .join('');
30
+ if (hex.startsWith(prefix)) {
31
+ return 'pow_' + nonce.toString(36);
32
+ }
33
+ } catch {
34
+ // crypto.subtle not available — non-critical
35
+ }
36
+ nonce++;
37
+ if (nonce % 1000 === 0) {
38
+ await new Promise((r) => setTimeout(r, 0));
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,77 @@
1
+ import type { HttpAdapter } from './platform/types';
2
+
3
+ interface OIDCDiscoveryMetadata {
4
+ issuer: string;
5
+ authorization_endpoint: string;
6
+ token_endpoint: string;
7
+ userinfo_endpoint?: string;
8
+ jwks_uri: string;
9
+ end_session_endpoint?: string;
10
+ scopes_supported?: string[];
11
+ response_types_supported: string[];
12
+ grant_types_supported?: string[];
13
+ token_endpoint_auth_methods_supported?: string[];
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ export class Discovery {
18
+ private http: HttpAdapter;
19
+ private metadata: OIDCDiscoveryMetadata | null = null;
20
+ private discoveryBaseUrl: string;
21
+
22
+ /**
23
+ * @param http HTTP adapter
24
+ * @param discoveryBaseUrl discovery 请求使用的基础 URL(proxy 模式用 apiUrl,否则留空用 issuer)
25
+ */
26
+ constructor(http: HttpAdapter, discoveryBaseUrl?: string) {
27
+ this.http = http;
28
+ // 去掉 /bff 等 BFF 路径前缀,回到根路径访问 /.well-known
29
+ this.discoveryBaseUrl = (discoveryBaseUrl || '')
30
+ .replace(/\/bff\/?$/, '')
31
+ .replace(/\/$/, '');
32
+ }
33
+
34
+ async discover(issuer: string): Promise<OIDCDiscoveryMetadata> {
35
+ if (this.metadata) return this.metadata;
36
+
37
+ // proxy 模式:使用 discoveryBaseUrl(同源),否则直连 issuer
38
+ const base = this.discoveryBaseUrl || issuer.replace(/\/$/, '');
39
+ const url = `${base}/.well-known/openid-configuration`;
40
+
41
+ const response = await this.http.request(url);
42
+ if (!response.ok) {
43
+ throw new Error(`OIDC Discovery failed: HTTP ${response.status}`);
44
+ }
45
+
46
+ const json = await response.json() as Record<string, unknown>;
47
+
48
+ const metadata = (json.data ?? json) as OIDCDiscoveryMetadata;
49
+
50
+ if (!metadata.issuer || !metadata.authorization_endpoint || !metadata.token_endpoint) {
51
+ throw new Error('OIDC Discovery response missing required fields (issuer, authorization_endpoint, token_endpoint)');
52
+ }
53
+
54
+ this.metadata = metadata;
55
+ return metadata;
56
+ }
57
+
58
+ getAuthorizationEndpoint(): string | null {
59
+ return this.metadata?.authorization_endpoint ?? null;
60
+ }
61
+
62
+ getTokenEndpoint(): string | null {
63
+ return this.metadata?.token_endpoint ?? null;
64
+ }
65
+
66
+ getEndSessionEndpoint(): string | null {
67
+ return this.metadata?.end_session_endpoint ?? null;
68
+ }
69
+
70
+ getJWKSUri(): string | null {
71
+ return this.metadata?.jwks_uri ?? null;
72
+ }
73
+
74
+ getMetadata(): OIDCDiscoveryMetadata | null {
75
+ return this.metadata;
76
+ }
77
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,44 @@
1
+ export class AuthmsError extends Error {
2
+ code: string;
3
+ status: number;
4
+ detail?: string;
5
+
6
+ constructor(code: string, message: string, status: number, detail?: string) {
7
+ super(message);
8
+ this.name = 'AuthmsError';
9
+ this.code = code;
10
+ this.status = status;
11
+ this.detail = detail;
12
+ }
13
+ }
14
+
15
+ export class AuthmsAuthError extends AuthmsError {
16
+ constructor(code: string, message: string, status: number) {
17
+ super(code, message, status);
18
+ this.name = 'AuthmsAuthError';
19
+ }
20
+ }
21
+
22
+ export class AuthmsNetworkError extends AuthmsError {
23
+ constructor(message: string) {
24
+ super('NETWORK_ERROR', message, 0);
25
+ this.name = 'AuthmsNetworkError';
26
+ }
27
+ }
28
+
29
+ export class AuthmsApiError extends AuthmsError {
30
+ violations?: Array<{ field: string; message: string }>;
31
+
32
+ constructor(code: string, message: string, status: number, violations?: Array<{ field: string; message: string }>) {
33
+ super(code, message, status);
34
+ this.name = 'AuthmsApiError';
35
+ this.violations = violations;
36
+ }
37
+ }
38
+
39
+ export class AuthmsConfigError extends AuthmsError {
40
+ constructor(message: string) {
41
+ super('CONFIG_ERROR', message, 500);
42
+ this.name = 'AuthmsConfigError';
43
+ }
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ export { AuthMS } from './authms';
2
+ export type { AuthmsConfig } from './authms';
3
+ export { createPlatformBinding } from './binding';
4
+ export type { BindingConfig, PlatformBinding } from './binding';
5
+ export { TokenManager } from './token-manager';
6
+ export { ApiClient } from './api-client';
7
+ export { AuthClient } from './auth-client';
8
+ export { Discovery } from './discovery';
9
+ export { TabSync } from './sync';
10
+ export type { AuthmsPlugin } from './plugin';
11
+ export { browserPlatform, memoryPlatform } from './platform';
12
+ export type { AuthmsPlatform, StorageAdapter, HttpAdapter, CryptoAdapter } from './platform';
13
+ export {
14
+ processPasswordForTransmission,
15
+ solveProofOfWork,
16
+ type TransmissionResult,
17
+ type KeyExchangeFn,
18
+ } from './crypto';
19
+ export {
20
+ AuthmsError,
21
+ AuthmsAuthError,
22
+ AuthmsNetworkError,
23
+ AuthmsApiError,
24
+ AuthmsConfigError,
25
+ } from './errors';
26
+ export type {
27
+ User,
28
+ AuthResult,
29
+ LoginRequest,
30
+ RegisterRequest,
31
+ OAuthOptions,
32
+ TokenClaims,
33
+ AuthmsEvent,
34
+ SecurityAlert,
35
+ PasswordPolicyConfig,
36
+ TenantAuthConfig,
37
+ BrandingInfo,
38
+ } from './types';
39
+ export { ERROR_CODES } from './types';
@@ -0,0 +1,23 @@
1
+ import type { AuthmsPlatform } from './types';
2
+
3
+ export const browserPlatform: AuthmsPlatform = {
4
+ storage: {
5
+ getItem(key) {
6
+ try { return localStorage.getItem(key); } catch { return null; }
7
+ },
8
+ setItem(key, value) {
9
+ try { localStorage.setItem(key, value); } catch { /* quota exceeded */ }
10
+ },
11
+ removeItem(key) {
12
+ try { localStorage.removeItem(key); } catch { }
13
+ },
14
+ keys() {
15
+ try { return Object.keys(localStorage); } catch { return []; }
16
+ },
17
+ },
18
+ http: {
19
+ request(input, init) {
20
+ return fetch(input, init);
21
+ },
22
+ },
23
+ };
@@ -0,0 +1,3 @@
1
+ export type { AuthmsPlatform, StorageAdapter, HttpAdapter, CryptoAdapter } from './types';
2
+ export { browserPlatform } from './browser';
3
+ export { memoryPlatform } from './memory';
@@ -0,0 +1,19 @@
1
+ import type { AuthmsPlatform } from './types';
2
+
3
+ export function memoryPlatform(): AuthmsPlatform {
4
+ const store = new Map<string, string>();
5
+
6
+ return {
7
+ storage: {
8
+ getItem(key) { return store.get(key) ?? null; },
9
+ setItem(key, value) { store.set(key, value); },
10
+ removeItem(key) { store.delete(key); },
11
+ keys() { return Array.from(store.keys()); },
12
+ },
13
+ http: {
14
+ request(_input, _init) {
15
+ throw new Error('HttpAdapter.request not implemented in memory platform');
16
+ },
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,21 @@
1
+ export interface StorageAdapter {
2
+ getItem(key: string): string | null | Promise<string | null>;
3
+ setItem(key: string, value: string): void | Promise<void>;
4
+ removeItem(key: string): void | Promise<void>;
5
+ keys?(): string[] | Promise<string[]>;
6
+ }
7
+
8
+ export interface HttpAdapter {
9
+ request(input: string, init?: RequestInit): Promise<Response>;
10
+ }
11
+
12
+ export interface CryptoAdapter {
13
+ generateCodeChallenge(verifier: string): Promise<string>;
14
+ generateRandomString(length: number): string;
15
+ }
16
+
17
+ export interface AuthmsPlatform {
18
+ storage: StorageAdapter;
19
+ http: HttpAdapter;
20
+ crypto?: CryptoAdapter;
21
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { AuthMS } from './authms';
2
+
3
+ export interface AuthmsPlugin {
4
+ name: string;
5
+ version: string;
6
+ install(core: AuthMS): void | Promise<void>;
7
+ uninstall?(): void | Promise<void>;
8
+ }
package/src/sync.ts ADDED
@@ -0,0 +1,51 @@
1
+ type MessageType = 'LOGOUT' | 'TOKEN_REFRESHED' | 'LOGIN';
2
+
3
+ interface SyncMessage {
4
+ type: MessageType;
5
+ timestamp: number;
6
+ }
7
+
8
+ export class TabSync {
9
+ private channel: BroadcastChannel | null = null;
10
+ private onLogout: () => void;
11
+ private onTokenChange: () => void;
12
+
13
+ constructor(onLogout: () => void, onTokenChange: () => void) {
14
+ this.onLogout = onLogout;
15
+ this.onTokenChange = onTokenChange;
16
+ }
17
+
18
+ listen(): void {
19
+ if (typeof BroadcastChannel === 'undefined') return;
20
+
21
+ try {
22
+ this.channel = new BroadcastChannel('authms:sync');
23
+ this.channel.onmessage = (event: MessageEvent<SyncMessage>) => {
24
+ const { type } = event.data;
25
+ switch (type) {
26
+ case 'LOGOUT':
27
+ this.onLogout();
28
+ break;
29
+ case 'TOKEN_REFRESHED':
30
+ case 'LOGIN':
31
+ this.onTokenChange();
32
+ break;
33
+ }
34
+ };
35
+ } catch { /* BroadcastChannel not supported */ }
36
+ }
37
+
38
+ broadcast(type: MessageType): void {
39
+ if (!this.channel) return;
40
+ try {
41
+ this.channel.postMessage({ type, timestamp: Date.now() });
42
+ } catch { /* ignore broadcast failures */ }
43
+ }
44
+
45
+ close(): void {
46
+ if (this.channel) {
47
+ this.channel.close();
48
+ this.channel = null;
49
+ }
50
+ }
51
+ }