@everystack/auth 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/README.md +256 -0
- package/package.json +104 -0
- package/src/api.ts +65 -0
- package/src/cff/index.ts +157 -0
- package/src/client/index.tsx +139 -0
- package/src/context.ts +30 -0
- package/src/cookies.ts +93 -0
- package/src/index.ts +526 -0
- package/src/jwt/hmac-node.ts +11 -0
- package/src/jwt/hmac-web.ts +17 -0
- package/src/jwt/hmac.ts +10 -0
- package/src/jwt/index.ts +182 -0
- package/src/jwt/jwt.sql +177 -0
- package/src/oauth/index.ts +7 -0
- package/src/oauth/providers/apple.ts +43 -0
- package/src/oauth/providers/google.ts +49 -0
- package/src/oauth/types.ts +17 -0
- package/src/oauth/verify.ts +193 -0
- package/src/oauth.ts +6 -0
- package/src/passwords.ts +70 -0
- package/src/postgres.ts +504 -0
- package/src/refresh.ts +204 -0
- package/src/response.ts +50 -0
- package/src/schema.ts +224 -0
- package/src/sql.ts +261 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @everystack/auth/client — React auth context with localStorage persistence.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { AuthProvider, useAuth } from '@everystack/auth/client';
|
|
6
|
+
*
|
|
7
|
+
* <AuthProvider>
|
|
8
|
+
* <App />
|
|
9
|
+
* </AuthProvider>
|
|
10
|
+
*
|
|
11
|
+
* const { user, token, signIn, signUp, signOut, isLoading } = useAuth();
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
15
|
+
|
|
16
|
+
export interface AuthUser {
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
username: string;
|
|
20
|
+
displayName: string;
|
|
21
|
+
avatarUrl?: string | null;
|
|
22
|
+
role?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AuthContextValue {
|
|
26
|
+
user: AuthUser | null;
|
|
27
|
+
token: string | null;
|
|
28
|
+
isLoading: boolean;
|
|
29
|
+
signIn: (email: string, password: string) => Promise<void>;
|
|
30
|
+
signUp: (email: string, password: string, username: string, displayName: string) => Promise<void>;
|
|
31
|
+
signOut: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
35
|
+
|
|
36
|
+
const TOKEN_KEY = 'everystack-token';
|
|
37
|
+
const REFRESH_KEY = 'everystack-refresh';
|
|
38
|
+
const USER_KEY = 'everystack-user';
|
|
39
|
+
|
|
40
|
+
export const storage = {
|
|
41
|
+
getToken(): string | null {
|
|
42
|
+
try { return localStorage.getItem(TOKEN_KEY); } catch { return null; }
|
|
43
|
+
},
|
|
44
|
+
getRefresh(): string | null {
|
|
45
|
+
try { return localStorage.getItem(REFRESH_KEY); } catch { return null; }
|
|
46
|
+
},
|
|
47
|
+
getUser(): AuthUser | null {
|
|
48
|
+
try {
|
|
49
|
+
const s = localStorage.getItem(USER_KEY);
|
|
50
|
+
return s ? JSON.parse(s) : null;
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
},
|
|
53
|
+
save(token: string, refresh: string, user: AuthUser): void {
|
|
54
|
+
try {
|
|
55
|
+
localStorage.setItem(TOKEN_KEY, token);
|
|
56
|
+
localStorage.setItem(REFRESH_KEY, refresh);
|
|
57
|
+
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
|
58
|
+
} catch {}
|
|
59
|
+
},
|
|
60
|
+
clear(): void {
|
|
61
|
+
try {
|
|
62
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
63
|
+
localStorage.removeItem(REFRESH_KEY);
|
|
64
|
+
localStorage.removeItem(USER_KEY);
|
|
65
|
+
} catch {}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function AuthProvider({ children }: { children: React.ReactNode }): React.ReactElement {
|
|
70
|
+
const [user, setUser] = useState<AuthUser | null>(null);
|
|
71
|
+
const [token, setToken] = useState<string | null>(null);
|
|
72
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const savedToken = storage.getToken();
|
|
76
|
+
const savedUser = storage.getUser();
|
|
77
|
+
if (savedToken && savedUser) {
|
|
78
|
+
setToken(savedToken);
|
|
79
|
+
setUser(savedUser);
|
|
80
|
+
}
|
|
81
|
+
setIsLoading(false);
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const signIn = useCallback(async (email: string, password: string) => {
|
|
85
|
+
const res = await fetch('/api/auth/signin', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({ email, password }),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const body = await res.json();
|
|
92
|
+
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
throw new Error(body.error || 'Sign in failed');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
storage.save(body.token, body.refreshToken, body.user);
|
|
98
|
+
setToken(body.token);
|
|
99
|
+
setUser(body.user);
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
const signUp = useCallback(async (email: string, password: string, username: string, displayName: string) => {
|
|
103
|
+
const res = await fetch('/api/auth/signup', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify({ email, password, username, displayName }),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const body = await res.json();
|
|
110
|
+
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
throw new Error(body.error || 'Sign up failed');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
storage.save(body.token, body.refreshToken, body.user);
|
|
116
|
+
setToken(body.token);
|
|
117
|
+
setUser(body.user);
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const signOut = useCallback(() => {
|
|
121
|
+
// Clear server-side cookies (best-effort, don't block UI)
|
|
122
|
+
fetch('/api/auth/signout', { method: 'POST' }).catch(() => {});
|
|
123
|
+
storage.clear();
|
|
124
|
+
setToken(null);
|
|
125
|
+
setUser(null);
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<AuthContext.Provider value={{ user, token, isLoading, signIn, signUp, signOut }}>
|
|
130
|
+
{children}
|
|
131
|
+
</AuthContext.Provider>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function useAuth(): AuthContextValue {
|
|
136
|
+
const ctx = useContext(AuthContext);
|
|
137
|
+
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
138
|
+
return ctx;
|
|
139
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR auth context via AsyncLocalStorage.
|
|
3
|
+
*
|
|
4
|
+
* The SSR handler wraps each request in `runWithAuthContext(user, fn)`.
|
|
5
|
+
* Inside that scope, SSR loaders call `getAuthContext()` to read the
|
|
6
|
+
* authenticated user's claims without prop-drilling or global mutation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
10
|
+
import type { TokenPayload } from './index';
|
|
11
|
+
|
|
12
|
+
const authStore = new AsyncLocalStorage<TokenPayload | null>();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run a function with auth context available via getAuthContext().
|
|
16
|
+
*/
|
|
17
|
+
export function runWithAuthContext<T>(
|
|
18
|
+
user: TokenPayload | null,
|
|
19
|
+
fn: () => T | Promise<T>,
|
|
20
|
+
): T | Promise<T> {
|
|
21
|
+
return authStore.run(user, fn);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the current auth context from within an SSR loader.
|
|
26
|
+
* Returns null if no authenticated user or not within an SSR request.
|
|
27
|
+
*/
|
|
28
|
+
export function getAuthContext(): TokenPayload | null {
|
|
29
|
+
return authStore.getStore() ?? null;
|
|
30
|
+
}
|
package/src/cookies.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie serialization and parsing utilities for SSR authentication.
|
|
3
|
+
*
|
|
4
|
+
* Access token cookie is sent on all requests (Path=/).
|
|
5
|
+
* Refresh token cookie is scoped to the refresh endpoint only (Path=/api/auth/refresh).
|
|
6
|
+
* Both are HttpOnly + Secure + SameSite=Lax.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const COOKIE_NAMES = {
|
|
10
|
+
token: '__es_token',
|
|
11
|
+
refresh: '__es_refresh',
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export interface CookieOptions {
|
|
15
|
+
/** Custom domain for cookies. Omit for exact-origin match (most secure). */
|
|
16
|
+
domain?: string;
|
|
17
|
+
/** Base path for the token cookie. Default '/'. */
|
|
18
|
+
path?: string;
|
|
19
|
+
/** Whether to set Secure flag. Default true. Set false for localhost dev. */
|
|
20
|
+
secure?: boolean;
|
|
21
|
+
/** Access token max-age in seconds. Default 3600 (1 hour). */
|
|
22
|
+
tokenMaxAge?: number;
|
|
23
|
+
/** Refresh token max-age in seconds. Default 604800 (7 days). */
|
|
24
|
+
refreshMaxAge?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build Set-Cookie header values for access + refresh tokens.
|
|
29
|
+
* Returns an array of two Set-Cookie strings.
|
|
30
|
+
*/
|
|
31
|
+
export function serializeAuthCookies(
|
|
32
|
+
token: string,
|
|
33
|
+
refreshToken: string,
|
|
34
|
+
opts?: CookieOptions,
|
|
35
|
+
): string[] {
|
|
36
|
+
const secure = opts?.secure !== false;
|
|
37
|
+
const domain = opts?.domain ? `; Domain=${opts.domain}` : '';
|
|
38
|
+
const basePath = opts?.path ?? '/';
|
|
39
|
+
const tokenMaxAge = opts?.tokenMaxAge ?? 3600;
|
|
40
|
+
const refreshMaxAge = opts?.refreshMaxAge ?? 604800;
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
`${COOKIE_NAMES.token}=${token}; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=Lax; Path=${basePath}; Max-Age=${tokenMaxAge}${domain}`,
|
|
44
|
+
`${COOKIE_NAMES.refresh}=${refreshToken}; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=Lax; Path=/api/auth/refresh; Max-Age=${refreshMaxAge}${domain}`,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build Set-Cookie header values that clear auth cookies (Max-Age=0).
|
|
50
|
+
*/
|
|
51
|
+
export function clearAuthCookies(opts?: CookieOptions): string[] {
|
|
52
|
+
const secure = opts?.secure !== false;
|
|
53
|
+
const domain = opts?.domain ? `; Domain=${opts.domain}` : '';
|
|
54
|
+
const basePath = opts?.path ?? '/';
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
`${COOKIE_NAMES.token}=; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=Lax; Path=${basePath}; Max-Age=0${domain}`,
|
|
58
|
+
`${COOKIE_NAMES.refresh}=; HttpOnly; ${secure ? 'Secure; ' : ''}SameSite=Lax; Path=/api/auth/refresh; Max-Age=0${domain}`,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a Cookie header string into a key-value map.
|
|
64
|
+
*/
|
|
65
|
+
export function parseCookies(header: string): Record<string, string> {
|
|
66
|
+
const cookies: Record<string, string> = {};
|
|
67
|
+
for (const pair of header.split(';')) {
|
|
68
|
+
const idx = pair.indexOf('=');
|
|
69
|
+
if (idx === -1) continue;
|
|
70
|
+
const key = pair.slice(0, idx).trim();
|
|
71
|
+
const val = pair.slice(idx + 1).trim();
|
|
72
|
+
if (key) cookies[key] = val;
|
|
73
|
+
}
|
|
74
|
+
return cookies;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extract the access token from a Cookie header string.
|
|
79
|
+
*/
|
|
80
|
+
export function getTokenFromCookies(cookieHeader: string | null): string | null {
|
|
81
|
+
if (!cookieHeader) return null;
|
|
82
|
+
const cookies = parseCookies(cookieHeader);
|
|
83
|
+
return cookies[COOKIE_NAMES.token] ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract the refresh token from a Cookie header string.
|
|
88
|
+
*/
|
|
89
|
+
export function getRefreshFromCookies(cookieHeader: string | null): string | null {
|
|
90
|
+
if (!cookieHeader) return null;
|
|
91
|
+
const cookies = parseCookies(cookieHeader);
|
|
92
|
+
return cookies[COOKIE_NAMES.refresh] ?? null;
|
|
93
|
+
}
|