@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/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @ccatto/react-auth
|
|
2
|
+
|
|
3
|
+
React/Next.js authentication package with Better Auth integration, JWT auth, session management, and mobile (Capacitor) support.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ccatto/react-auth
|
|
9
|
+
# or
|
|
10
|
+
yarn add @ccatto/react-auth
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
// Client-side
|
|
17
|
+
import { useSession, signIn, signOut } from '@ccatto/react-auth';
|
|
18
|
+
|
|
19
|
+
function Profile() {
|
|
20
|
+
const session = useSession();
|
|
21
|
+
|
|
22
|
+
if (!session) return <button onClick={() => signIn()}>Sign In</button>;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<p>Welcome, {session.user.name}</p>
|
|
27
|
+
<button onClick={() => signOut()}>Sign Out</button>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// Server-side
|
|
35
|
+
import { createCattoAuth } from '@ccatto/react-auth/server';
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Sub-exports
|
|
39
|
+
|
|
40
|
+
| Entry Point | Description |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `@ccatto/react-auth` | Client-side hooks, providers, types |
|
|
43
|
+
| `@ccatto/react-auth/server` | Server-side auth config, session enrichment |
|
|
44
|
+
|
|
45
|
+
## Peer Dependencies
|
|
46
|
+
|
|
47
|
+
| Package | Version | Required |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `better-auth` | `>=1.0.0` | Yes |
|
|
50
|
+
| `next` | `>=14.0.0` | Yes |
|
|
51
|
+
| `react` | `>=18.0.0` | Yes |
|
|
52
|
+
| `zustand` | `>=4.0.0` | Yes |
|
|
53
|
+
| `@apollo/client` | `*` | No |
|
|
54
|
+
| `@capacitor/core` | `*` | No |
|
|
55
|
+
| `@capacitor/preferences` | `*` | No |
|
|
56
|
+
| `@simplewebauthn/browser` | `*` | No |
|
|
57
|
+
| `graphql` | `*` | No |
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ccatto/react-auth - Configuration Types
|
|
3
|
+
*
|
|
4
|
+
* Interfaces for configuring the auth system.
|
|
5
|
+
* Consumer apps pass these configs to factory functions.
|
|
6
|
+
*/
|
|
7
|
+
interface CattoAuthSocialProvider {
|
|
8
|
+
clientId: string;
|
|
9
|
+
clientSecret: string;
|
|
10
|
+
scope?: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Minimal database client interface — compatible with PrismaClient without version coupling */
|
|
13
|
+
interface CattoAuthDatabaseClient {
|
|
14
|
+
user: {
|
|
15
|
+
findUnique(args: Record<string, unknown>): Promise<unknown>;
|
|
16
|
+
update(args: {
|
|
17
|
+
where: {
|
|
18
|
+
id: string;
|
|
19
|
+
};
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
}): Promise<unknown>;
|
|
22
|
+
};
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
interface CattoAuthServerConfig {
|
|
26
|
+
/** Prisma client instance (or any compatible ORM client) */
|
|
27
|
+
database: CattoAuthDatabaseClient;
|
|
28
|
+
/** Database provider type */
|
|
29
|
+
databaseProvider: 'postgresql' | 'mysql' | 'sqlite';
|
|
30
|
+
/** Secret for signing tokens (BETTER_AUTH_SECRET) */
|
|
31
|
+
secret: string;
|
|
32
|
+
/** Base URL for auth callbacks (BETTER_AUTH_URL) */
|
|
33
|
+
baseURL: string;
|
|
34
|
+
/** OAuth social providers */
|
|
35
|
+
socialProviders?: {
|
|
36
|
+
google?: CattoAuthSocialProvider;
|
|
37
|
+
facebook?: CattoAuthSocialProvider;
|
|
38
|
+
github?: CattoAuthSocialProvider;
|
|
39
|
+
};
|
|
40
|
+
/** Session configuration */
|
|
41
|
+
session?: {
|
|
42
|
+
/** Session expiry in seconds (default: 69 days = 5961600) */
|
|
43
|
+
expiresInSeconds?: number;
|
|
44
|
+
};
|
|
45
|
+
/** Email/password authentication */
|
|
46
|
+
emailAndPassword?: {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
requireEmailVerification?: boolean;
|
|
49
|
+
};
|
|
50
|
+
/** Advanced options */
|
|
51
|
+
advanced?: {
|
|
52
|
+
useSecureCookies?: boolean;
|
|
53
|
+
};
|
|
54
|
+
/** Pluggable hooks for domain-specific logic */
|
|
55
|
+
hooks?: {
|
|
56
|
+
/**
|
|
57
|
+
* Called to enrich the session with custom fields (e.g., playerID, organizations).
|
|
58
|
+
* Return an object that gets merged into session.user.
|
|
59
|
+
*/
|
|
60
|
+
enrichSession?: (userId: string, db: unknown) => Promise<Record<string, unknown>>;
|
|
61
|
+
/**
|
|
62
|
+
* Called after a new user is created (e.g., create domain-specific records).
|
|
63
|
+
*/
|
|
64
|
+
onUserCreated?: (user: {
|
|
65
|
+
id: string;
|
|
66
|
+
email: string;
|
|
67
|
+
name?: string | null;
|
|
68
|
+
}, db: unknown) => Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
/** Optional logger for auth events (hook errors, warnings) */
|
|
71
|
+
logger?: {
|
|
72
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
interface CattoAuthClientConfig {
|
|
76
|
+
/** Base URL for auth API calls (default: NEXT_PUBLIC_BASE_URL or http://localhost:3000) */
|
|
77
|
+
baseURL?: string;
|
|
78
|
+
/** Endpoint for enriched session data (default: '/api/auth/session/enriched') */
|
|
79
|
+
enrichedSessionEndpoint?: string;
|
|
80
|
+
}
|
|
81
|
+
interface CattoSessionProviderConfig {
|
|
82
|
+
/** Seconds between session refetches (default: 300 = 5 min) */
|
|
83
|
+
refetchInterval?: number;
|
|
84
|
+
/** Endpoint for enriched session data (default: '/api/auth/session/enriched') */
|
|
85
|
+
enrichedSessionEndpoint?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type { CattoAuthClientConfig as C, CattoAuthDatabaseClient as a, CattoAuthServerConfig as b, CattoAuthSocialProvider as c, CattoSessionProviderConfig as d };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ccatto/react-auth - Configuration Types
|
|
3
|
+
*
|
|
4
|
+
* Interfaces for configuring the auth system.
|
|
5
|
+
* Consumer apps pass these configs to factory functions.
|
|
6
|
+
*/
|
|
7
|
+
interface CattoAuthSocialProvider {
|
|
8
|
+
clientId: string;
|
|
9
|
+
clientSecret: string;
|
|
10
|
+
scope?: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Minimal database client interface — compatible with PrismaClient without version coupling */
|
|
13
|
+
interface CattoAuthDatabaseClient {
|
|
14
|
+
user: {
|
|
15
|
+
findUnique(args: Record<string, unknown>): Promise<unknown>;
|
|
16
|
+
update(args: {
|
|
17
|
+
where: {
|
|
18
|
+
id: string;
|
|
19
|
+
};
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
}): Promise<unknown>;
|
|
22
|
+
};
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
interface CattoAuthServerConfig {
|
|
26
|
+
/** Prisma client instance (or any compatible ORM client) */
|
|
27
|
+
database: CattoAuthDatabaseClient;
|
|
28
|
+
/** Database provider type */
|
|
29
|
+
databaseProvider: 'postgresql' | 'mysql' | 'sqlite';
|
|
30
|
+
/** Secret for signing tokens (BETTER_AUTH_SECRET) */
|
|
31
|
+
secret: string;
|
|
32
|
+
/** Base URL for auth callbacks (BETTER_AUTH_URL) */
|
|
33
|
+
baseURL: string;
|
|
34
|
+
/** OAuth social providers */
|
|
35
|
+
socialProviders?: {
|
|
36
|
+
google?: CattoAuthSocialProvider;
|
|
37
|
+
facebook?: CattoAuthSocialProvider;
|
|
38
|
+
github?: CattoAuthSocialProvider;
|
|
39
|
+
};
|
|
40
|
+
/** Session configuration */
|
|
41
|
+
session?: {
|
|
42
|
+
/** Session expiry in seconds (default: 69 days = 5961600) */
|
|
43
|
+
expiresInSeconds?: number;
|
|
44
|
+
};
|
|
45
|
+
/** Email/password authentication */
|
|
46
|
+
emailAndPassword?: {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
requireEmailVerification?: boolean;
|
|
49
|
+
};
|
|
50
|
+
/** Advanced options */
|
|
51
|
+
advanced?: {
|
|
52
|
+
useSecureCookies?: boolean;
|
|
53
|
+
};
|
|
54
|
+
/** Pluggable hooks for domain-specific logic */
|
|
55
|
+
hooks?: {
|
|
56
|
+
/**
|
|
57
|
+
* Called to enrich the session with custom fields (e.g., playerID, organizations).
|
|
58
|
+
* Return an object that gets merged into session.user.
|
|
59
|
+
*/
|
|
60
|
+
enrichSession?: (userId: string, db: unknown) => Promise<Record<string, unknown>>;
|
|
61
|
+
/**
|
|
62
|
+
* Called after a new user is created (e.g., create domain-specific records).
|
|
63
|
+
*/
|
|
64
|
+
onUserCreated?: (user: {
|
|
65
|
+
id: string;
|
|
66
|
+
email: string;
|
|
67
|
+
name?: string | null;
|
|
68
|
+
}, db: unknown) => Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
/** Optional logger for auth events (hook errors, warnings) */
|
|
71
|
+
logger?: {
|
|
72
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
interface CattoAuthClientConfig {
|
|
76
|
+
/** Base URL for auth API calls (default: NEXT_PUBLIC_BASE_URL or http://localhost:3000) */
|
|
77
|
+
baseURL?: string;
|
|
78
|
+
/** Endpoint for enriched session data (default: '/api/auth/session/enriched') */
|
|
79
|
+
enrichedSessionEndpoint?: string;
|
|
80
|
+
}
|
|
81
|
+
interface CattoSessionProviderConfig {
|
|
82
|
+
/** Seconds between session refetches (default: 300 = 5 min) */
|
|
83
|
+
refetchInterval?: number;
|
|
84
|
+
/** Endpoint for enriched session data (default: '/api/auth/session/enriched') */
|
|
85
|
+
enrichedSessionEndpoint?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type { CattoAuthClientConfig as C, CattoAuthDatabaseClient as a, CattoAuthServerConfig as b, CattoAuthSocialProvider as c, CattoSessionProviderConfig as d };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var preferences = require('@capacitor/preferences');
|
|
5
|
+
|
|
6
|
+
// src/client/session-store.ts
|
|
7
|
+
var currentSession = null;
|
|
8
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
9
|
+
var sessionStore = {
|
|
10
|
+
/** Get the current session (synchronous, no network call) */
|
|
11
|
+
getSession() {
|
|
12
|
+
return currentSession ? Object.freeze({ ...currentSession }) : null;
|
|
13
|
+
},
|
|
14
|
+
/** Update the session (called by SessionSync component) */
|
|
15
|
+
setSession(session) {
|
|
16
|
+
currentSession = session;
|
|
17
|
+
const snapshot = [...listeners];
|
|
18
|
+
snapshot.forEach((listener) => listener(session));
|
|
19
|
+
},
|
|
20
|
+
/** Subscribe to session changes */
|
|
21
|
+
subscribe(listener) {
|
|
22
|
+
listeners.add(listener);
|
|
23
|
+
return () => listeners.delete(listener);
|
|
24
|
+
},
|
|
25
|
+
/** Get the user ID from the current session (convenience method) */
|
|
26
|
+
getUserId() {
|
|
27
|
+
return currentSession?.user?.id ?? null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/services/jwt-auth.service.ts
|
|
32
|
+
var noopLogger = {
|
|
33
|
+
info: () => {
|
|
34
|
+
},
|
|
35
|
+
warn: () => {
|
|
36
|
+
},
|
|
37
|
+
error: () => {
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var _JwtAuthService = class _JwtAuthService {
|
|
41
|
+
// 5 second cooldown after failure
|
|
42
|
+
constructor(storage, api, logger, options) {
|
|
43
|
+
this.storage = storage;
|
|
44
|
+
this.api = api;
|
|
45
|
+
// Fix 7: Cache token expiry to avoid re-parsing JWT on every getAuthHeaders()
|
|
46
|
+
this.cachedTokenExp = null;
|
|
47
|
+
// Fix 5: Refresh deduplication + cooldown after failure
|
|
48
|
+
this.refreshPromise = null;
|
|
49
|
+
this.lastRefreshFailure = 0;
|
|
50
|
+
this.log = logger || noopLogger;
|
|
51
|
+
this.options = options || {};
|
|
52
|
+
}
|
|
53
|
+
/** Login with email and password */
|
|
54
|
+
async login(credentials) {
|
|
55
|
+
try {
|
|
56
|
+
const data = await this.api.login(credentials);
|
|
57
|
+
await this.storage.setAccessToken(data.accessToken);
|
|
58
|
+
await this.storage.setRefreshToken(data.refreshToken);
|
|
59
|
+
this.log.info("User logged in successfully", { userId: data.user.id });
|
|
60
|
+
return data;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
this.log.error("Login failed", { error });
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Register new user */
|
|
67
|
+
async register(data) {
|
|
68
|
+
try {
|
|
69
|
+
const result = await this.api.register(data);
|
|
70
|
+
await this.storage.setAccessToken(result.accessToken);
|
|
71
|
+
await this.storage.setRefreshToken(result.refreshToken);
|
|
72
|
+
this.log.info("User registered successfully", { userId: result.user.id });
|
|
73
|
+
return result;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.log.error("Registration failed", { error });
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Logout (clear tokens) */
|
|
80
|
+
async logout() {
|
|
81
|
+
try {
|
|
82
|
+
const refreshToken = await this.storage.getRefreshToken();
|
|
83
|
+
if (refreshToken) {
|
|
84
|
+
await this.api.logout(refreshToken).catch(() => {
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
await this.storage.clearTokens();
|
|
89
|
+
this.cachedTokenExp = null;
|
|
90
|
+
this.log.info("User logged out");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Refresh access token */
|
|
94
|
+
async refreshAccessToken() {
|
|
95
|
+
const refreshToken = await this.storage.getRefreshToken();
|
|
96
|
+
if (!refreshToken) {
|
|
97
|
+
throw new Error("No refresh token available");
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const result = await this.api.refreshToken(refreshToken);
|
|
101
|
+
await this.storage.setAccessToken(result.accessToken);
|
|
102
|
+
return result.accessToken;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
await this.storage.clearTokens();
|
|
105
|
+
this.cachedTokenExp = null;
|
|
106
|
+
this.log.error("Token refresh failed", { error });
|
|
107
|
+
this.options.onSessionExpired?.();
|
|
108
|
+
throw new Error("Session expired");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Get current access token */
|
|
112
|
+
async getAccessToken() {
|
|
113
|
+
return this.storage.getAccessToken();
|
|
114
|
+
}
|
|
115
|
+
/** Check if a JWT token is expired or about to expire */
|
|
116
|
+
isTokenExpiredOrExpiring(token, bufferSeconds = 120) {
|
|
117
|
+
try {
|
|
118
|
+
let exp;
|
|
119
|
+
if (this.cachedTokenExp?.token === token) {
|
|
120
|
+
exp = this.cachedTokenExp.exp;
|
|
121
|
+
} else {
|
|
122
|
+
const base64Url = token.split(".")[1];
|
|
123
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
124
|
+
const payload = JSON.parse(atob(base64));
|
|
125
|
+
exp = payload.exp;
|
|
126
|
+
if (!exp) return true;
|
|
127
|
+
this.cachedTokenExp = { token, exp };
|
|
128
|
+
}
|
|
129
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
130
|
+
return nowSeconds >= exp - bufferSeconds;
|
|
131
|
+
} catch {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get auth headers for API requests.
|
|
137
|
+
* Proactively refreshes the access token if it's expired or about to expire.
|
|
138
|
+
* Includes cooldown to prevent repeated refresh attempts after failure.
|
|
139
|
+
*/
|
|
140
|
+
async getAuthHeaders() {
|
|
141
|
+
let token = await this.getAccessToken();
|
|
142
|
+
if (!token) return {};
|
|
143
|
+
if (this.isTokenExpiredOrExpiring(token)) {
|
|
144
|
+
const timeSinceFailure = Date.now() - this.lastRefreshFailure;
|
|
145
|
+
if (this.lastRefreshFailure > 0 && timeSinceFailure < _JwtAuthService.REFRESH_COOLDOWN_MS) {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
if (!this.refreshPromise) {
|
|
150
|
+
this.refreshPromise = this.refreshAccessToken().finally(() => {
|
|
151
|
+
this.refreshPromise = null;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
token = await this.refreshPromise;
|
|
155
|
+
this.lastRefreshFailure = 0;
|
|
156
|
+
} catch {
|
|
157
|
+
this.lastRefreshFailure = Date.now();
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { Authorization: `Bearer ${token}` };
|
|
162
|
+
}
|
|
163
|
+
/** Check if user is authenticated */
|
|
164
|
+
async isAuthenticated() {
|
|
165
|
+
return this.storage.hasTokens();
|
|
166
|
+
}
|
|
167
|
+
/** Check if tokens exist in storage */
|
|
168
|
+
async hasTokens() {
|
|
169
|
+
return this.storage.hasTokens();
|
|
170
|
+
}
|
|
171
|
+
/** Decode JWT token (client-side only — for user info, NOT for security) */
|
|
172
|
+
decodeToken(token) {
|
|
173
|
+
try {
|
|
174
|
+
const base64Url = token.split(".")[1];
|
|
175
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
176
|
+
const jsonPayload = decodeURIComponent(
|
|
177
|
+
atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
178
|
+
);
|
|
179
|
+
const payload = JSON.parse(jsonPayload);
|
|
180
|
+
return {
|
|
181
|
+
id: payload.sub,
|
|
182
|
+
email: payload.email,
|
|
183
|
+
name: payload.name || null,
|
|
184
|
+
role: payload.role || "user",
|
|
185
|
+
playerID: payload.playerID,
|
|
186
|
+
organizationId: payload.organizationId
|
|
187
|
+
};
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Get current user from token (client-side decode) */
|
|
193
|
+
async getCurrentUser() {
|
|
194
|
+
const token = await this.getAccessToken();
|
|
195
|
+
if (!token) return null;
|
|
196
|
+
return this.decodeToken(token);
|
|
197
|
+
}
|
|
198
|
+
/** Login with passkey (WebAuthn/FIDO2) */
|
|
199
|
+
async loginWithPasskey() {
|
|
200
|
+
if (!this.api.generatePasskeyAuthenticationOptions || !this.api.verifyPasskeyAuthentication) {
|
|
201
|
+
throw new Error("Passkey authentication not configured");
|
|
202
|
+
}
|
|
203
|
+
const { startAuthentication, browserSupportsWebAuthn } = await import('@simplewebauthn/browser');
|
|
204
|
+
if (!browserSupportsWebAuthn()) {
|
|
205
|
+
throw new Error("WebAuthn is not supported on this device");
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const { options, sessionId } = await this.api.generatePasskeyAuthenticationOptions();
|
|
209
|
+
const authResponse = await startAuthentication({
|
|
210
|
+
optionsJSON: JSON.parse(options)
|
|
211
|
+
});
|
|
212
|
+
const result = await this.api.verifyPasskeyAuthentication(
|
|
213
|
+
sessionId,
|
|
214
|
+
JSON.stringify(authResponse)
|
|
215
|
+
);
|
|
216
|
+
await this.storage.setAccessToken(result.accessToken);
|
|
217
|
+
await this.storage.setRefreshToken(result.refreshToken);
|
|
218
|
+
this.log.info("Passkey auth successful", { userId: result.user.id });
|
|
219
|
+
return result;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error instanceof Error && error.name === "NotAllowedError") {
|
|
222
|
+
throw new Error("Passkey authentication was cancelled");
|
|
223
|
+
}
|
|
224
|
+
this.log.error("Passkey auth failed", { error });
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Send OTP to phone number for phone-based login */
|
|
229
|
+
async sendPhoneOtp(phoneNumber) {
|
|
230
|
+
if (!this.api.sendPhoneOtp) {
|
|
231
|
+
throw new Error("Phone OTP not configured");
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const result = await this.api.sendPhoneOtp(phoneNumber);
|
|
235
|
+
if (result.success) {
|
|
236
|
+
this.log.info("OTP sent", { phoneNumber: phoneNumber.slice(-4) });
|
|
237
|
+
} else {
|
|
238
|
+
this.log.warn("OTP send failed", {
|
|
239
|
+
phoneNumber: phoneNumber.slice(-4),
|
|
240
|
+
message: result.message
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
this.log.error("Failed to send OTP", { error });
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Verify OTP and login/register user */
|
|
250
|
+
async verifyPhoneOtp(phoneNumber, code) {
|
|
251
|
+
if (!this.api.verifyPhoneOtp) {
|
|
252
|
+
throw new Error("Phone OTP not configured");
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const result = await this.api.verifyPhoneOtp(phoneNumber, code);
|
|
256
|
+
if (!result.success || !result.accessToken || !result.refreshToken) {
|
|
257
|
+
throw new Error(result.message || "Verification failed");
|
|
258
|
+
}
|
|
259
|
+
await this.storage.setAccessToken(result.accessToken);
|
|
260
|
+
await this.storage.setRefreshToken(result.refreshToken);
|
|
261
|
+
const user = this.decodeToken(result.accessToken);
|
|
262
|
+
if (!user) {
|
|
263
|
+
throw new Error("Failed to decode user token");
|
|
264
|
+
}
|
|
265
|
+
this.log.info("Phone auth successful", {
|
|
266
|
+
userId: user.id,
|
|
267
|
+
isNewUser: result.isNewUser
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
accessToken: result.accessToken,
|
|
271
|
+
refreshToken: result.refreshToken,
|
|
272
|
+
user,
|
|
273
|
+
isNewUser: result.isNewUser ?? false
|
|
274
|
+
};
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.log.error("Phone OTP verification failed", { error });
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
_JwtAuthService.REFRESH_COOLDOWN_MS = 5e3;
|
|
282
|
+
var JwtAuthService = _JwtAuthService;
|
|
283
|
+
var CapacitorAuthStorage = class {
|
|
284
|
+
constructor(options) {
|
|
285
|
+
const prefix = options?.keyPrefix ?? "catto_auth";
|
|
286
|
+
this.ACCESS_TOKEN_KEY = `${prefix}_access_token`;
|
|
287
|
+
this.REFRESH_TOKEN_KEY = `${prefix}_refresh_token`;
|
|
288
|
+
}
|
|
289
|
+
async setAccessToken(token) {
|
|
290
|
+
await preferences.Preferences.set({
|
|
291
|
+
key: this.ACCESS_TOKEN_KEY,
|
|
292
|
+
value: token
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async getAccessToken() {
|
|
296
|
+
const { value } = await preferences.Preferences.get({ key: this.ACCESS_TOKEN_KEY });
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
async setRefreshToken(token) {
|
|
300
|
+
await preferences.Preferences.set({
|
|
301
|
+
key: this.REFRESH_TOKEN_KEY,
|
|
302
|
+
value: token
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
async getRefreshToken() {
|
|
306
|
+
const { value } = await preferences.Preferences.get({ key: this.REFRESH_TOKEN_KEY });
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
async clearTokens() {
|
|
310
|
+
await Promise.all([
|
|
311
|
+
preferences.Preferences.remove({ key: this.ACCESS_TOKEN_KEY }),
|
|
312
|
+
preferences.Preferences.remove({ key: this.REFRESH_TOKEN_KEY })
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
async hasTokens() {
|
|
316
|
+
const accessToken = await this.getAccessToken();
|
|
317
|
+
return accessToken !== null;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
exports.CapacitorAuthStorage = CapacitorAuthStorage;
|
|
322
|
+
exports.JwtAuthService = JwtAuthService;
|
|
323
|
+
exports.sessionStore = sessionStore;
|
|
324
|
+
//# sourceMappingURL=index.cjs.map
|
|
325
|
+
//# sourceMappingURL=index.cjs.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":["Preferences"],"mappings":"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","file":"index.cjs","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"]}
|