@ccatto/react-auth 1.0.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 +61 -0
- package/dist/config-CvzbPvtw.d.cts +88 -0
- package/dist/config-CvzbPvtw.d.ts +88 -0
- package/dist/index.cjs +325 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +296 -0
- package/dist/index.d.ts +296 -0
- package/dist/index.js +321 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +162 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +15 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +160 -0
- package/dist/server.js.map +1 -0
- package/package.json +91 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Preferences } from '@capacitor/preferences';
|
|
3
|
+
|
|
4
|
+
// src/client/session-store.ts
|
|
5
|
+
var currentSession = null;
|
|
6
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
var sessionStore = {
|
|
8
|
+
/** Get the current session (synchronous, no network call) */
|
|
9
|
+
getSession() {
|
|
10
|
+
return currentSession ? Object.freeze({ ...currentSession }) : null;
|
|
11
|
+
},
|
|
12
|
+
/** Update the session (called by SessionSync component) */
|
|
13
|
+
setSession(session) {
|
|
14
|
+
currentSession = session;
|
|
15
|
+
const snapshot = [...listeners];
|
|
16
|
+
snapshot.forEach((listener) => listener(session));
|
|
17
|
+
},
|
|
18
|
+
/** Subscribe to session changes */
|
|
19
|
+
subscribe(listener) {
|
|
20
|
+
listeners.add(listener);
|
|
21
|
+
return () => listeners.delete(listener);
|
|
22
|
+
},
|
|
23
|
+
/** Get the user ID from the current session (convenience method) */
|
|
24
|
+
getUserId() {
|
|
25
|
+
return currentSession?.user?.id ?? null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/services/jwt-auth.service.ts
|
|
30
|
+
var noopLogger = {
|
|
31
|
+
info: () => {
|
|
32
|
+
},
|
|
33
|
+
warn: () => {
|
|
34
|
+
},
|
|
35
|
+
error: () => {
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var _JwtAuthService = class _JwtAuthService {
|
|
39
|
+
// 5 second cooldown after failure
|
|
40
|
+
constructor(storage, api, logger, options) {
|
|
41
|
+
this.storage = storage;
|
|
42
|
+
this.api = api;
|
|
43
|
+
// Fix 7: Cache token expiry to avoid re-parsing JWT on every getAuthHeaders()
|
|
44
|
+
this.cachedTokenExp = null;
|
|
45
|
+
// Fix 5: Refresh deduplication + cooldown after failure
|
|
46
|
+
this.refreshPromise = null;
|
|
47
|
+
this.lastRefreshFailure = 0;
|
|
48
|
+
this.log = logger || noopLogger;
|
|
49
|
+
this.options = options || {};
|
|
50
|
+
}
|
|
51
|
+
/** Login with email and password */
|
|
52
|
+
async login(credentials) {
|
|
53
|
+
try {
|
|
54
|
+
const data = await this.api.login(credentials);
|
|
55
|
+
await this.storage.setAccessToken(data.accessToken);
|
|
56
|
+
await this.storage.setRefreshToken(data.refreshToken);
|
|
57
|
+
this.log.info("User logged in successfully", { userId: data.user.id });
|
|
58
|
+
return data;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
this.log.error("Login failed", { error });
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Register new user */
|
|
65
|
+
async register(data) {
|
|
66
|
+
try {
|
|
67
|
+
const result = await this.api.register(data);
|
|
68
|
+
await this.storage.setAccessToken(result.accessToken);
|
|
69
|
+
await this.storage.setRefreshToken(result.refreshToken);
|
|
70
|
+
this.log.info("User registered successfully", { userId: result.user.id });
|
|
71
|
+
return result;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
this.log.error("Registration failed", { error });
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Logout (clear tokens) */
|
|
78
|
+
async logout() {
|
|
79
|
+
try {
|
|
80
|
+
const refreshToken = await this.storage.getRefreshToken();
|
|
81
|
+
if (refreshToken) {
|
|
82
|
+
await this.api.logout(refreshToken).catch(() => {
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
await this.storage.clearTokens();
|
|
87
|
+
this.cachedTokenExp = null;
|
|
88
|
+
this.log.info("User logged out");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Refresh access token */
|
|
92
|
+
async refreshAccessToken() {
|
|
93
|
+
const refreshToken = await this.storage.getRefreshToken();
|
|
94
|
+
if (!refreshToken) {
|
|
95
|
+
throw new Error("No refresh token available");
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const result = await this.api.refreshToken(refreshToken);
|
|
99
|
+
await this.storage.setAccessToken(result.accessToken);
|
|
100
|
+
return result.accessToken;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
await this.storage.clearTokens();
|
|
103
|
+
this.cachedTokenExp = null;
|
|
104
|
+
this.log.error("Token refresh failed", { error });
|
|
105
|
+
this.options.onSessionExpired?.();
|
|
106
|
+
throw new Error("Session expired");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Get current access token */
|
|
110
|
+
async getAccessToken() {
|
|
111
|
+
return this.storage.getAccessToken();
|
|
112
|
+
}
|
|
113
|
+
/** Check if a JWT token is expired or about to expire */
|
|
114
|
+
isTokenExpiredOrExpiring(token, bufferSeconds = 120) {
|
|
115
|
+
try {
|
|
116
|
+
let exp;
|
|
117
|
+
if (this.cachedTokenExp?.token === token) {
|
|
118
|
+
exp = this.cachedTokenExp.exp;
|
|
119
|
+
} else {
|
|
120
|
+
const base64Url = token.split(".")[1];
|
|
121
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
122
|
+
const payload = JSON.parse(atob(base64));
|
|
123
|
+
exp = payload.exp;
|
|
124
|
+
if (!exp) return true;
|
|
125
|
+
this.cachedTokenExp = { token, exp };
|
|
126
|
+
}
|
|
127
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
128
|
+
return nowSeconds >= exp - bufferSeconds;
|
|
129
|
+
} catch {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get auth headers for API requests.
|
|
135
|
+
* Proactively refreshes the access token if it's expired or about to expire.
|
|
136
|
+
* Includes cooldown to prevent repeated refresh attempts after failure.
|
|
137
|
+
*/
|
|
138
|
+
async getAuthHeaders() {
|
|
139
|
+
let token = await this.getAccessToken();
|
|
140
|
+
if (!token) return {};
|
|
141
|
+
if (this.isTokenExpiredOrExpiring(token)) {
|
|
142
|
+
const timeSinceFailure = Date.now() - this.lastRefreshFailure;
|
|
143
|
+
if (this.lastRefreshFailure > 0 && timeSinceFailure < _JwtAuthService.REFRESH_COOLDOWN_MS) {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
if (!this.refreshPromise) {
|
|
148
|
+
this.refreshPromise = this.refreshAccessToken().finally(() => {
|
|
149
|
+
this.refreshPromise = null;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
token = await this.refreshPromise;
|
|
153
|
+
this.lastRefreshFailure = 0;
|
|
154
|
+
} catch {
|
|
155
|
+
this.lastRefreshFailure = Date.now();
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { Authorization: `Bearer ${token}` };
|
|
160
|
+
}
|
|
161
|
+
/** Check if user is authenticated */
|
|
162
|
+
async isAuthenticated() {
|
|
163
|
+
return this.storage.hasTokens();
|
|
164
|
+
}
|
|
165
|
+
/** Check if tokens exist in storage */
|
|
166
|
+
async hasTokens() {
|
|
167
|
+
return this.storage.hasTokens();
|
|
168
|
+
}
|
|
169
|
+
/** Decode JWT token (client-side only — for user info, NOT for security) */
|
|
170
|
+
decodeToken(token) {
|
|
171
|
+
try {
|
|
172
|
+
const base64Url = token.split(".")[1];
|
|
173
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
174
|
+
const jsonPayload = decodeURIComponent(
|
|
175
|
+
atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
176
|
+
);
|
|
177
|
+
const payload = JSON.parse(jsonPayload);
|
|
178
|
+
return {
|
|
179
|
+
id: payload.sub,
|
|
180
|
+
email: payload.email,
|
|
181
|
+
name: payload.name || null,
|
|
182
|
+
role: payload.role || "user",
|
|
183
|
+
playerID: payload.playerID,
|
|
184
|
+
organizationId: payload.organizationId
|
|
185
|
+
};
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Get current user from token (client-side decode) */
|
|
191
|
+
async getCurrentUser() {
|
|
192
|
+
const token = await this.getAccessToken();
|
|
193
|
+
if (!token) return null;
|
|
194
|
+
return this.decodeToken(token);
|
|
195
|
+
}
|
|
196
|
+
/** Login with passkey (WebAuthn/FIDO2) */
|
|
197
|
+
async loginWithPasskey() {
|
|
198
|
+
if (!this.api.generatePasskeyAuthenticationOptions || !this.api.verifyPasskeyAuthentication) {
|
|
199
|
+
throw new Error("Passkey authentication not configured");
|
|
200
|
+
}
|
|
201
|
+
const { startAuthentication, browserSupportsWebAuthn } = await import('@simplewebauthn/browser');
|
|
202
|
+
if (!browserSupportsWebAuthn()) {
|
|
203
|
+
throw new Error("WebAuthn is not supported on this device");
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const { options, sessionId } = await this.api.generatePasskeyAuthenticationOptions();
|
|
207
|
+
const authResponse = await startAuthentication({
|
|
208
|
+
optionsJSON: JSON.parse(options)
|
|
209
|
+
});
|
|
210
|
+
const result = await this.api.verifyPasskeyAuthentication(
|
|
211
|
+
sessionId,
|
|
212
|
+
JSON.stringify(authResponse)
|
|
213
|
+
);
|
|
214
|
+
await this.storage.setAccessToken(result.accessToken);
|
|
215
|
+
await this.storage.setRefreshToken(result.refreshToken);
|
|
216
|
+
this.log.info("Passkey auth successful", { userId: result.user.id });
|
|
217
|
+
return result;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error instanceof Error && error.name === "NotAllowedError") {
|
|
220
|
+
throw new Error("Passkey authentication was cancelled");
|
|
221
|
+
}
|
|
222
|
+
this.log.error("Passkey auth failed", { error });
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Send OTP to phone number for phone-based login */
|
|
227
|
+
async sendPhoneOtp(phoneNumber) {
|
|
228
|
+
if (!this.api.sendPhoneOtp) {
|
|
229
|
+
throw new Error("Phone OTP not configured");
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const result = await this.api.sendPhoneOtp(phoneNumber);
|
|
233
|
+
if (result.success) {
|
|
234
|
+
this.log.info("OTP sent", { phoneNumber: phoneNumber.slice(-4) });
|
|
235
|
+
} else {
|
|
236
|
+
this.log.warn("OTP send failed", {
|
|
237
|
+
phoneNumber: phoneNumber.slice(-4),
|
|
238
|
+
message: result.message
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.log.error("Failed to send OTP", { error });
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Verify OTP and login/register user */
|
|
248
|
+
async verifyPhoneOtp(phoneNumber, code) {
|
|
249
|
+
if (!this.api.verifyPhoneOtp) {
|
|
250
|
+
throw new Error("Phone OTP not configured");
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const result = await this.api.verifyPhoneOtp(phoneNumber, code);
|
|
254
|
+
if (!result.success || !result.accessToken || !result.refreshToken) {
|
|
255
|
+
throw new Error(result.message || "Verification failed");
|
|
256
|
+
}
|
|
257
|
+
await this.storage.setAccessToken(result.accessToken);
|
|
258
|
+
await this.storage.setRefreshToken(result.refreshToken);
|
|
259
|
+
const user = this.decodeToken(result.accessToken);
|
|
260
|
+
if (!user) {
|
|
261
|
+
throw new Error("Failed to decode user token");
|
|
262
|
+
}
|
|
263
|
+
this.log.info("Phone auth successful", {
|
|
264
|
+
userId: user.id,
|
|
265
|
+
isNewUser: result.isNewUser
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
accessToken: result.accessToken,
|
|
269
|
+
refreshToken: result.refreshToken,
|
|
270
|
+
user,
|
|
271
|
+
isNewUser: result.isNewUser ?? false
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
this.log.error("Phone OTP verification failed", { error });
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
_JwtAuthService.REFRESH_COOLDOWN_MS = 5e3;
|
|
280
|
+
var JwtAuthService = _JwtAuthService;
|
|
281
|
+
var CapacitorAuthStorage = class {
|
|
282
|
+
constructor(options) {
|
|
283
|
+
const prefix = options?.keyPrefix ?? "catto_auth";
|
|
284
|
+
this.ACCESS_TOKEN_KEY = `${prefix}_access_token`;
|
|
285
|
+
this.REFRESH_TOKEN_KEY = `${prefix}_refresh_token`;
|
|
286
|
+
}
|
|
287
|
+
async setAccessToken(token) {
|
|
288
|
+
await Preferences.set({
|
|
289
|
+
key: this.ACCESS_TOKEN_KEY,
|
|
290
|
+
value: token
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async getAccessToken() {
|
|
294
|
+
const { value } = await Preferences.get({ key: this.ACCESS_TOKEN_KEY });
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
async setRefreshToken(token) {
|
|
298
|
+
await Preferences.set({
|
|
299
|
+
key: this.REFRESH_TOKEN_KEY,
|
|
300
|
+
value: token
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async getRefreshToken() {
|
|
304
|
+
const { value } = await Preferences.get({ key: this.REFRESH_TOKEN_KEY });
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
async clearTokens() {
|
|
308
|
+
await Promise.all([
|
|
309
|
+
Preferences.remove({ key: this.ACCESS_TOKEN_KEY }),
|
|
310
|
+
Preferences.remove({ key: this.REFRESH_TOKEN_KEY })
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
async hasTokens() {
|
|
314
|
+
const accessToken = await this.getAccessToken();
|
|
315
|
+
return accessToken !== null;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export { CapacitorAuthStorage, JwtAuthService, sessionStore };
|
|
320
|
+
//# sourceMappingURL=index.js.map
|
|
321
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/session-store.ts","../src/services/jwt-auth.service.ts","../src/storage/capacitor-auth-storage.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @ccatto/react-auth - Session Store\n *\n * Synchronous session state store for sharing auth session with Apollo Client.\n * Avoids network calls in Apollo's authLink.\n *\n * @example\n * // Write (from SessionSync component in React tree)\n * sessionStore.setSession(session);\n *\n * // Read (from Apollo authLink — synchronous, no network call)\n * const session = sessionStore.getSession();\n */\n\nimport type { CompatSession } from '../types/session';\n\nlet currentSession: CompatSession | null = null;\nconst listeners: Set<(session: CompatSession | null) => void> = new Set();\n\nexport const sessionStore = {\n /** Get the current session (synchronous, no network call) */\n getSession(): CompatSession | null {\n return currentSession ? Object.freeze({ ...currentSession }) : null;\n },\n\n /** Update the session (called by SessionSync component) */\n setSession(session: CompatSession | null): void {\n currentSession = session;\n const snapshot = [...listeners];\n snapshot.forEach((listener) => listener(session));\n },\n\n /** Subscribe to session changes */\n subscribe(listener: (session: CompatSession | null) => void): () => void {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n\n /** Get the user ID from the current session (convenience method) */\n getUserId(): string | null {\n return currentSession?.user?.id ?? null;\n },\n};\n","/**\n * @ccatto/react-auth - JWT Auth Service\n *\n * Platform-agnostic JWT authentication service.\n * Handles token storage, login, register, refresh, and passkey auth.\n *\n * Uses IAuthStorage for token persistence and IAuthApiService for API calls.\n *\n * @example\n * ```typescript\n * import { JwtAuthService, CapacitorAuthStorage } from '@ccatto/react-auth';\n *\n * const storage = new CapacitorAuthStorage({ keyPrefix: 'myapp' });\n * const authService = new JwtAuthService(storage, myApiService, undefined, {\n * onSessionExpired: () => router.push('/login'),\n * });\n * await authService.login({ email, password });\n * ```\n */\nimport type { IAuthStorage } from '../storage/auth-storage.interface';\nimport type {\n AuthUser,\n IAuthApiService,\n IAuthLogger,\n LoginCredentials,\n LoginResponse,\n RegisterData,\n} from './auth-api.interface';\n\n// Re-export types for convenience\nexport type {\n AuthUser,\n LoginCredentials,\n RegisterData,\n LoginResponse,\n AuthTokens,\n} from './auth-api.interface';\n\nconst noopLogger: IAuthLogger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n};\n\nexport interface JwtAuthServiceOptions {\n /** Called when session expires (refresh token fails). Use to redirect to login. */\n onSessionExpired?: () => void;\n}\n\nexport class JwtAuthService {\n private log: IAuthLogger;\n private options: JwtAuthServiceOptions;\n\n // Fix 7: Cache token expiry to avoid re-parsing JWT on every getAuthHeaders()\n private cachedTokenExp: { token: string; exp: number } | null = null;\n\n // Fix 5: Refresh deduplication + cooldown after failure\n private refreshPromise: Promise<string> | null = null;\n private lastRefreshFailure: number = 0;\n private static REFRESH_COOLDOWN_MS = 5000; // 5 second cooldown after failure\n\n constructor(\n private storage: IAuthStorage,\n private api: IAuthApiService,\n logger?: IAuthLogger,\n options?: JwtAuthServiceOptions,\n ) {\n this.log = logger || noopLogger;\n this.options = options || {};\n }\n\n /** Login with email and password */\n async login(credentials: LoginCredentials): Promise<LoginResponse> {\n try {\n const data = await this.api.login(credentials);\n await this.storage.setAccessToken(data.accessToken);\n await this.storage.setRefreshToken(data.refreshToken);\n this.log.info('User logged in successfully', { userId: data.user.id });\n return data;\n } catch (error) {\n this.log.error('Login failed', { error });\n throw error;\n }\n }\n\n /** Register new user */\n async register(data: RegisterData): Promise<LoginResponse> {\n try {\n const result = await this.api.register(data);\n await this.storage.setAccessToken(result.accessToken);\n await this.storage.setRefreshToken(result.refreshToken);\n this.log.info('User registered successfully', { userId: result.user.id });\n return result;\n } catch (error) {\n this.log.error('Registration failed', { error });\n throw error;\n }\n }\n\n /** Logout (clear tokens) */\n async logout(): Promise<void> {\n try {\n const refreshToken = await this.storage.getRefreshToken();\n if (refreshToken) {\n await this.api.logout(refreshToken).catch(() => {});\n }\n } finally {\n await this.storage.clearTokens();\n this.cachedTokenExp = null;\n this.log.info('User logged out');\n }\n }\n\n /** Refresh access token */\n async refreshAccessToken(): Promise<string> {\n const refreshToken = await this.storage.getRefreshToken();\n if (!refreshToken) {\n throw new Error('No refresh token available');\n }\n\n try {\n const result = await this.api.refreshToken(refreshToken);\n await this.storage.setAccessToken(result.accessToken);\n return result.accessToken;\n } catch (error) {\n await this.storage.clearTokens();\n this.cachedTokenExp = null;\n this.log.error('Token refresh failed', { error });\n this.options.onSessionExpired?.();\n throw new Error('Session expired');\n }\n }\n\n /** Get current access token */\n async getAccessToken(): Promise<string | null> {\n return this.storage.getAccessToken();\n }\n\n /** Check if a JWT token is expired or about to expire */\n private isTokenExpiredOrExpiring(\n token: string,\n bufferSeconds = 120,\n ): boolean {\n try {\n let exp: number;\n\n // Fix 7: Use cached expiry if token hasn't changed\n if (this.cachedTokenExp?.token === token) {\n exp = this.cachedTokenExp.exp;\n } else {\n const base64Url = token.split('.')[1];\n const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');\n const payload = JSON.parse(atob(base64));\n exp = payload.exp;\n if (!exp) return true;\n this.cachedTokenExp = { token, exp };\n }\n\n const nowSeconds = Math.floor(Date.now() / 1000);\n return nowSeconds >= exp - bufferSeconds;\n } catch {\n return true;\n }\n }\n\n /**\n * Get auth headers for API requests.\n * Proactively refreshes the access token if it's expired or about to expire.\n * Includes cooldown to prevent repeated refresh attempts after failure.\n */\n async getAuthHeaders(): Promise<Record<string, string>> {\n let token = await this.getAccessToken();\n if (!token) return {};\n\n if (this.isTokenExpiredOrExpiring(token)) {\n // Fix 5: Skip refresh if we recently failed (cooldown)\n const timeSinceFailure = Date.now() - this.lastRefreshFailure;\n if (\n this.lastRefreshFailure > 0 &&\n timeSinceFailure < JwtAuthService.REFRESH_COOLDOWN_MS\n ) {\n return {};\n }\n\n try {\n if (!this.refreshPromise) {\n this.refreshPromise = this.refreshAccessToken().finally(() => {\n this.refreshPromise = null;\n });\n }\n token = await this.refreshPromise;\n this.lastRefreshFailure = 0;\n } catch {\n this.lastRefreshFailure = Date.now();\n return {};\n }\n }\n\n return { Authorization: `Bearer ${token}` };\n }\n\n /** Check if user is authenticated */\n async isAuthenticated(): Promise<boolean> {\n return this.storage.hasTokens();\n }\n\n /** Check if tokens exist in storage */\n async hasTokens(): Promise<boolean> {\n return this.storage.hasTokens();\n }\n\n /** Decode JWT token (client-side only — for user info, NOT for security) */\n decodeToken(token: string): AuthUser | null {\n try {\n const base64Url = token.split('.')[1];\n const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');\n const jsonPayload = decodeURIComponent(\n atob(base64)\n .split('')\n .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))\n .join(''),\n );\n const payload = JSON.parse(jsonPayload);\n return {\n id: payload.sub,\n email: payload.email,\n name: payload.name || null,\n role: payload.role || 'user',\n playerID: payload.playerID,\n organizationId: payload.organizationId,\n };\n } catch {\n return null;\n }\n }\n\n /** Get current user from token (client-side decode) */\n async getCurrentUser(): Promise<AuthUser | null> {\n const token = await this.getAccessToken();\n if (!token) return null;\n return this.decodeToken(token);\n }\n\n /** Login with passkey (WebAuthn/FIDO2) */\n async loginWithPasskey(): Promise<LoginResponse> {\n if (\n !this.api.generatePasskeyAuthenticationOptions ||\n !this.api.verifyPasskeyAuthentication\n ) {\n throw new Error('Passkey authentication not configured');\n }\n\n const { startAuthentication, browserSupportsWebAuthn } =\n await import('@simplewebauthn/browser');\n\n if (!browserSupportsWebAuthn()) {\n throw new Error('WebAuthn is not supported on this device');\n }\n\n try {\n const { options, sessionId } =\n await this.api.generatePasskeyAuthenticationOptions();\n\n const authResponse = await startAuthentication({\n optionsJSON: JSON.parse(options),\n });\n\n const result = await this.api.verifyPasskeyAuthentication(\n sessionId,\n JSON.stringify(authResponse),\n );\n\n await this.storage.setAccessToken(result.accessToken);\n await this.storage.setRefreshToken(result.refreshToken);\n\n this.log.info('Passkey auth successful', { userId: result.user.id });\n return result;\n } catch (error) {\n if (error instanceof Error && error.name === 'NotAllowedError') {\n throw new Error('Passkey authentication was cancelled');\n }\n this.log.error('Passkey auth failed', { error });\n throw error;\n }\n }\n\n /** Send OTP to phone number for phone-based login */\n async sendPhoneOtp(\n phoneNumber: string,\n ): Promise<{ success: boolean; message: string; expiresIn: number }> {\n if (!this.api.sendPhoneOtp) {\n throw new Error('Phone OTP not configured');\n }\n\n try {\n const result = await this.api.sendPhoneOtp(phoneNumber);\n if (result.success) {\n this.log.info('OTP sent', { phoneNumber: phoneNumber.slice(-4) });\n } else {\n this.log.warn('OTP send failed', {\n phoneNumber: phoneNumber.slice(-4),\n message: result.message,\n });\n }\n return result;\n } catch (error) {\n this.log.error('Failed to send OTP', { error });\n throw error;\n }\n }\n\n /** Verify OTP and login/register user */\n async verifyPhoneOtp(\n phoneNumber: string,\n code: string,\n ): Promise<LoginResponse & { isNewUser: boolean }> {\n if (!this.api.verifyPhoneOtp) {\n throw new Error('Phone OTP not configured');\n }\n\n try {\n const result = await this.api.verifyPhoneOtp(phoneNumber, code);\n\n if (!result.success || !result.accessToken || !result.refreshToken) {\n throw new Error(result.message || 'Verification failed');\n }\n\n await this.storage.setAccessToken(result.accessToken);\n await this.storage.setRefreshToken(result.refreshToken);\n\n const user = this.decodeToken(result.accessToken);\n if (!user) {\n throw new Error('Failed to decode user token');\n }\n\n this.log.info('Phone auth successful', {\n userId: user.id,\n isNewUser: result.isNewUser,\n });\n\n return {\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n user,\n isNewUser: result.isNewUser ?? false,\n };\n } catch (error) {\n this.log.error('Phone OTP verification failed', { error });\n throw error;\n }\n }\n}\n","/**\n * @ccatto/react-auth - Capacitor Auth Storage\n *\n * Capacitor-based auth storage using @capacitor/preferences.\n * Works on BOTH web and mobile (no platform detection needed!).\n *\n * - Web: Uses localStorage\n * - iOS: Uses UserDefaults/Keychain\n * - Android: Uses SharedPreferences\n */\nimport { Preferences } from '@capacitor/preferences';\nimport type { IAuthStorage } from './auth-storage.interface';\n\nexport interface CapacitorAuthStorageOptions {\n /** Key prefix for stored tokens (default: 'catto_auth') */\n keyPrefix?: string;\n}\n\nexport class CapacitorAuthStorage implements IAuthStorage {\n private readonly ACCESS_TOKEN_KEY: string;\n private readonly REFRESH_TOKEN_KEY: string;\n\n constructor(options?: CapacitorAuthStorageOptions) {\n const prefix = options?.keyPrefix ?? 'catto_auth';\n this.ACCESS_TOKEN_KEY = `${prefix}_access_token`;\n this.REFRESH_TOKEN_KEY = `${prefix}_refresh_token`;\n }\n\n async setAccessToken(token: string): Promise<void> {\n await Preferences.set({\n key: this.ACCESS_TOKEN_KEY,\n value: token,\n });\n }\n\n async getAccessToken(): Promise<string | null> {\n const { value } = await Preferences.get({ key: this.ACCESS_TOKEN_KEY });\n return value;\n }\n\n async setRefreshToken(token: string): Promise<void> {\n await Preferences.set({\n key: this.REFRESH_TOKEN_KEY,\n value: token,\n });\n }\n\n async getRefreshToken(): Promise<string | null> {\n const { value } = await Preferences.get({ key: this.REFRESH_TOKEN_KEY });\n return value;\n }\n\n async clearTokens(): Promise<void> {\n await Promise.all([\n Preferences.remove({ key: this.ACCESS_TOKEN_KEY }),\n Preferences.remove({ key: this.REFRESH_TOKEN_KEY }),\n ]);\n }\n\n async hasTokens(): Promise<boolean> {\n const accessToken = await this.getAccessToken();\n return accessToken !== null;\n }\n}\n"]}
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var betterAuth = require('better-auth');
|
|
4
|
+
var prisma = require('better-auth/adapters/prisma');
|
|
5
|
+
var api = require('better-auth/api');
|
|
6
|
+
var nextJs = require('better-auth/next-js');
|
|
7
|
+
|
|
8
|
+
// src/server/create-auth.ts
|
|
9
|
+
var DEFAULT_TOKEN_EXPIRY_SECONDS = 69 * 24 * 60 * 60;
|
|
10
|
+
function createCattoAuth(config) {
|
|
11
|
+
const expiresIn = config.session?.expiresInSeconds ?? DEFAULT_TOKEN_EXPIRY_SECONDS;
|
|
12
|
+
const socialProviders = {};
|
|
13
|
+
if (config.socialProviders?.google) {
|
|
14
|
+
socialProviders.google = config.socialProviders.google;
|
|
15
|
+
}
|
|
16
|
+
if (config.socialProviders?.github) {
|
|
17
|
+
socialProviders.github = config.socialProviders.github;
|
|
18
|
+
}
|
|
19
|
+
if (config.socialProviders?.facebook) {
|
|
20
|
+
socialProviders.facebook = {
|
|
21
|
+
...config.socialProviders.facebook,
|
|
22
|
+
scope: config.socialProviders.facebook.scope ?? [
|
|
23
|
+
"email",
|
|
24
|
+
"public_profile"
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const auth = betterAuth.betterAuth({
|
|
29
|
+
// Database
|
|
30
|
+
// Cast needed: prismaAdapter expects PrismaClient but we use a minimal interface for package portability
|
|
31
|
+
database: prisma.prismaAdapter(
|
|
32
|
+
config.database,
|
|
33
|
+
{
|
|
34
|
+
provider: config.databaseProvider
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
// Secret & URL
|
|
38
|
+
secret: config.secret,
|
|
39
|
+
baseURL: config.baseURL,
|
|
40
|
+
// Email/password
|
|
41
|
+
emailAndPassword: {
|
|
42
|
+
enabled: config.emailAndPassword?.enabled ?? true,
|
|
43
|
+
requireEmailVerification: config.emailAndPassword?.requireEmailVerification ?? false
|
|
44
|
+
},
|
|
45
|
+
// Social providers
|
|
46
|
+
socialProviders,
|
|
47
|
+
// Session
|
|
48
|
+
session: {
|
|
49
|
+
expiresIn,
|
|
50
|
+
cookieCache: {
|
|
51
|
+
enabled: true,
|
|
52
|
+
maxAge: expiresIn
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
// Account linking
|
|
56
|
+
account: {
|
|
57
|
+
accountLinking: {
|
|
58
|
+
enabled: true,
|
|
59
|
+
trustedProviders: Object.keys(socialProviders)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
// User fields
|
|
63
|
+
user: {
|
|
64
|
+
additionalFields: {
|
|
65
|
+
role: {
|
|
66
|
+
type: "string",
|
|
67
|
+
required: false,
|
|
68
|
+
defaultValue: "user"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
// Advanced
|
|
73
|
+
advanced: {
|
|
74
|
+
useSecureCookies: config.advanced?.useSecureCookies ?? process.env.NODE_ENV === "production",
|
|
75
|
+
crossSubDomainCookies: {
|
|
76
|
+
enabled: false
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
// Plugins
|
|
80
|
+
plugins: [nextJs.nextCookies()],
|
|
81
|
+
// Hooks
|
|
82
|
+
hooks: {
|
|
83
|
+
// Before hooks: email normalization
|
|
84
|
+
before: api.createAuthMiddleware(async (ctx) => {
|
|
85
|
+
if (ctx.path === "/sign-up/email") {
|
|
86
|
+
const body = ctx.body;
|
|
87
|
+
if (body?.email && typeof body.email === "string") {
|
|
88
|
+
ctx.body.email = body.email.toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}),
|
|
92
|
+
// After hooks: user lifecycle events
|
|
93
|
+
after: api.createAuthMiddleware(async (ctx) => {
|
|
94
|
+
if (ctx.path.startsWith("/sign-in") || ctx.path.startsWith("/sign-up")) {
|
|
95
|
+
const newSession = ctx.context.newSession;
|
|
96
|
+
if (newSession?.user) {
|
|
97
|
+
const user = newSession.user;
|
|
98
|
+
const normalizedEmail = user.email?.toLowerCase();
|
|
99
|
+
if (normalizedEmail && user.email !== normalizedEmail) {
|
|
100
|
+
try {
|
|
101
|
+
await config.database.user.update({
|
|
102
|
+
where: { id: user.id },
|
|
103
|
+
data: { email: normalizedEmail }
|
|
104
|
+
});
|
|
105
|
+
} catch (hookError) {
|
|
106
|
+
config.logger?.warn?.(
|
|
107
|
+
"[CattoAuth] Email normalization failed",
|
|
108
|
+
{
|
|
109
|
+
userId: user.id,
|
|
110
|
+
error: hookError instanceof Error ? hookError.message : String(hookError)
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (config.hooks?.onUserCreated) {
|
|
116
|
+
try {
|
|
117
|
+
await config.hooks.onUserCreated(
|
|
118
|
+
{
|
|
119
|
+
id: user.id,
|
|
120
|
+
email: normalizedEmail || user.email,
|
|
121
|
+
name: user.name
|
|
122
|
+
},
|
|
123
|
+
config.database
|
|
124
|
+
);
|
|
125
|
+
} catch (hookError) {
|
|
126
|
+
config.logger?.warn?.("[CattoAuth] onUserCreated hook failed", {
|
|
127
|
+
userId: user.id,
|
|
128
|
+
error: hookError instanceof Error ? hookError.message : String(hookError)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
async function getEnrichedSession(headers) {
|
|
138
|
+
try {
|
|
139
|
+
const baseSession = await auth.api.getSession({ headers });
|
|
140
|
+
if (!baseSession?.user) return null;
|
|
141
|
+
const userId = baseSession.user.id;
|
|
142
|
+
const enrichment = config.hooks?.enrichSession ? await config.hooks.enrichSession(userId, config.database) : {};
|
|
143
|
+
return {
|
|
144
|
+
user: {
|
|
145
|
+
id: baseSession.user.id,
|
|
146
|
+
email: baseSession.user.email,
|
|
147
|
+
name: baseSession.user.name,
|
|
148
|
+
image: baseSession.user.image,
|
|
149
|
+
...enrichment
|
|
150
|
+
},
|
|
151
|
+
session: baseSession.session
|
|
152
|
+
};
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { auth, getEnrichedSession };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
exports.createCattoAuth = createCattoAuth;
|
|
161
|
+
//# sourceMappingURL=server.cjs.map
|
|
162
|
+
//# sourceMappingURL=server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/create-auth.ts"],"names":["betterAuth","prismaAdapter","nextCookies","createAuthMiddleware"],"mappings":"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","file":"server.cjs","sourcesContent":["/**\n * @ccatto/react-auth/server - createCattoAuth Factory\n *\n * Creates a configured Better Auth instance with sensible defaults,\n * automatic email normalization, and pluggable hooks for\n * session enrichment and user lifecycle events.\n *\n * @example\n * ```typescript\n * import { createCattoAuth } from '@ccatto/react-auth/server';\n * import { prisma } from '@myapp/database';\n *\n * export const { auth, getEnrichedSession } = createCattoAuth({\n * database: prisma,\n * databaseProvider: 'postgresql',\n * secret: process.env.BETTER_AUTH_SECRET!,\n * baseURL: process.env.BETTER_AUTH_URL!,\n * hooks: {\n * enrichSession: async (userId, db) => {\n * // Return custom fields to merge into session.user\n * return { role: 'admin', customField: 'value' };\n * },\n * },\n * });\n * ```\n */\nimport { betterAuth } from 'better-auth';\nimport { prismaAdapter } from 'better-auth/adapters/prisma';\nimport { createAuthMiddleware } from 'better-auth/api';\nimport { nextCookies } from 'better-auth/next-js';\nimport type {\n CattoAuthServerConfig,\n CattoAuthSocialProvider,\n} from '../types/config';\n\n// Default: 69 days (matches common long-lived session configs)\nconst DEFAULT_TOKEN_EXPIRY_SECONDS = 69 * 24 * 60 * 60;\n\nexport interface CreateCattoAuthResult {\n /** The Better Auth instance (use for API routes) */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n auth: any;\n /**\n * Get enriched session with custom fields from hooks.enrichSession.\n * Call this instead of auth.api.getSession for full session data.\n */\n getEnrichedSession: (headers: Headers) => Promise<any | null>;\n}\n\nexport function createCattoAuth(\n config: CattoAuthServerConfig,\n): CreateCattoAuthResult {\n const expiresIn =\n config.session?.expiresInSeconds ?? DEFAULT_TOKEN_EXPIRY_SECONDS;\n\n // Build social providers config\n const socialProviders: Record<\n string,\n CattoAuthSocialProvider & { scope?: string[] }\n > = {};\n if (config.socialProviders?.google) {\n socialProviders.google = config.socialProviders.google;\n }\n if (config.socialProviders?.github) {\n socialProviders.github = config.socialProviders.github;\n }\n if (config.socialProviders?.facebook) {\n socialProviders.facebook = {\n ...config.socialProviders.facebook,\n scope: config.socialProviders.facebook.scope ?? [\n 'email',\n 'public_profile',\n ],\n };\n }\n\n const auth = betterAuth({\n // Database\n // Cast needed: prismaAdapter expects PrismaClient but we use a minimal interface for package portability\n database: prismaAdapter(\n config.database as Parameters<typeof prismaAdapter>[0],\n {\n provider: config.databaseProvider,\n },\n ),\n\n // Secret & URL\n secret: config.secret,\n baseURL: config.baseURL,\n\n // Email/password\n emailAndPassword: {\n enabled: config.emailAndPassword?.enabled ?? true,\n requireEmailVerification:\n config.emailAndPassword?.requireEmailVerification ?? false,\n },\n\n // Social providers\n socialProviders,\n\n // Session\n session: {\n expiresIn,\n cookieCache: {\n enabled: true,\n maxAge: expiresIn,\n },\n },\n\n // Account linking\n account: {\n accountLinking: {\n enabled: true,\n trustedProviders: Object.keys(socialProviders),\n },\n },\n\n // User fields\n user: {\n additionalFields: {\n role: {\n type: 'string' as const,\n required: false,\n defaultValue: 'user',\n },\n },\n },\n\n // Advanced\n advanced: {\n useSecureCookies:\n config.advanced?.useSecureCookies ??\n process.env.NODE_ENV === 'production',\n crossSubDomainCookies: {\n enabled: false,\n },\n },\n\n // Plugins\n plugins: [nextCookies()],\n\n // Hooks\n hooks: {\n // Before hooks: email normalization\n before: createAuthMiddleware(async (ctx) => {\n if (ctx.path === '/sign-up/email') {\n const body = ctx.body as { email?: string } | undefined;\n if (body?.email && typeof body.email === 'string') {\n (ctx.body as { email: string }).email = body.email.toLowerCase();\n }\n }\n }),\n\n // After hooks: user lifecycle events\n after: createAuthMiddleware(async (ctx) => {\n if (\n ctx.path.startsWith('/sign-in') ||\n ctx.path.startsWith('/sign-up')\n ) {\n const newSession = ctx.context.newSession;\n if (newSession?.user) {\n const user = newSession.user;\n\n // Normalize email in database if needed\n const normalizedEmail = user.email?.toLowerCase();\n if (normalizedEmail && user.email !== normalizedEmail) {\n try {\n await config.database.user.update({\n where: { id: user.id },\n data: { email: normalizedEmail },\n });\n } catch (hookError) {\n config.logger?.warn?.(\n '[CattoAuth] Email normalization failed',\n {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n },\n );\n }\n }\n\n // Call onUserCreated hook\n if (config.hooks?.onUserCreated) {\n try {\n await config.hooks.onUserCreated(\n {\n id: user.id,\n email: normalizedEmail || user.email,\n name: user.name,\n },\n config.database,\n );\n } catch (hookError) {\n config.logger?.warn?.('[CattoAuth] onUserCreated hook failed', {\n userId: user.id,\n error:\n hookError instanceof Error\n ? hookError.message\n : String(hookError),\n });\n }\n }\n }\n }\n }),\n },\n });\n\n // Enriched session factory\n async function getEnrichedSession(headers: Headers): Promise<any | null> {\n try {\n const baseSession = await auth.api.getSession({ headers });\n if (!baseSession?.user) return null;\n\n const userId = baseSession.user.id;\n\n // Call enrichSession hook if provided\n const enrichment = config.hooks?.enrichSession\n ? await config.hooks.enrichSession(userId, config.database)\n : {};\n\n return {\n user: {\n id: baseSession.user.id,\n email: baseSession.user.email,\n name: baseSession.user.name,\n image: baseSession.user.image,\n ...enrichment,\n },\n session: baseSession.session,\n };\n } catch {\n return null;\n }\n }\n\n return { auth, getEnrichedSession };\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { b as CattoAuthServerConfig } from './config-CvzbPvtw.cjs';
|
|
2
|
+
export { c as CattoAuthSocialProvider } from './config-CvzbPvtw.cjs';
|
|
3
|
+
|
|
4
|
+
interface CreateCattoAuthResult {
|
|
5
|
+
/** The Better Auth instance (use for API routes) */
|
|
6
|
+
auth: any;
|
|
7
|
+
/**
|
|
8
|
+
* Get enriched session with custom fields from hooks.enrichSession.
|
|
9
|
+
* Call this instead of auth.api.getSession for full session data.
|
|
10
|
+
*/
|
|
11
|
+
getEnrichedSession: (headers: Headers) => Promise<any | null>;
|
|
12
|
+
}
|
|
13
|
+
declare function createCattoAuth(config: CattoAuthServerConfig): CreateCattoAuthResult;
|
|
14
|
+
|
|
15
|
+
export { CattoAuthServerConfig, type CreateCattoAuthResult, createCattoAuth };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { b as CattoAuthServerConfig } from './config-CvzbPvtw.js';
|
|
2
|
+
export { c as CattoAuthSocialProvider } from './config-CvzbPvtw.js';
|
|
3
|
+
|
|
4
|
+
interface CreateCattoAuthResult {
|
|
5
|
+
/** The Better Auth instance (use for API routes) */
|
|
6
|
+
auth: any;
|
|
7
|
+
/**
|
|
8
|
+
* Get enriched session with custom fields from hooks.enrichSession.
|
|
9
|
+
* Call this instead of auth.api.getSession for full session data.
|
|
10
|
+
*/
|
|
11
|
+
getEnrichedSession: (headers: Headers) => Promise<any | null>;
|
|
12
|
+
}
|
|
13
|
+
declare function createCattoAuth(config: CattoAuthServerConfig): CreateCattoAuthResult;
|
|
14
|
+
|
|
15
|
+
export { CattoAuthServerConfig, type CreateCattoAuthResult, createCattoAuth };
|