@everystack/auth 0.2.2 → 0.2.4
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/package.json +1 -1
- package/src/client/index.tsx +20 -2
- package/src/plugin.ts +31 -20
package/package.json
CHANGED
package/src/client/index.tsx
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* const { user, token, signIn, signUp, signOut, isLoading } = useAuth();
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
14
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
|
|
15
15
|
import { refreshAuthToken } from '../api';
|
|
16
16
|
|
|
17
17
|
export interface AuthUser {
|
|
@@ -81,11 +81,26 @@ export function isTokenExpired(token: string): boolean {
|
|
|
81
81
|
} catch { return true; }
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export
|
|
84
|
+
export interface AuthProviderProps {
|
|
85
|
+
children: React.ReactNode;
|
|
86
|
+
/**
|
|
87
|
+
* Called after a successful auth transition — the seam for product-analytics
|
|
88
|
+
* emitters (e.g. createAnalyticsClient().track('login')). Optional; kept generic
|
|
89
|
+
* so the auth package never depends on the analytics client.
|
|
90
|
+
*/
|
|
91
|
+
onAuthEvent?: (event: 'login' | 'signup' | 'logout', user: AuthUser | null) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function AuthProvider({ children, onAuthEvent }: AuthProviderProps): React.ReactElement {
|
|
85
95
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
86
96
|
const [token, setToken] = useState<string | null>(null);
|
|
87
97
|
const [isLoading, setIsLoading] = useState(true);
|
|
88
98
|
|
|
99
|
+
// Ref-backed so signIn/signUp/signOut stay stable (no dep churn) yet always
|
|
100
|
+
// call the latest handler.
|
|
101
|
+
const onAuthEventRef = useRef(onAuthEvent);
|
|
102
|
+
onAuthEventRef.current = onAuthEvent;
|
|
103
|
+
|
|
89
104
|
useEffect(() => {
|
|
90
105
|
const init = async () => {
|
|
91
106
|
const savedToken = storage.getToken();
|
|
@@ -129,6 +144,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }): React
|
|
|
129
144
|
storage.save(body.token, body.refreshToken, body.user);
|
|
130
145
|
setToken(body.token);
|
|
131
146
|
setUser(body.user);
|
|
147
|
+
onAuthEventRef.current?.('login', body.user);
|
|
132
148
|
}, []);
|
|
133
149
|
|
|
134
150
|
const signUp = useCallback(async (email: string, password: string, username: string, displayName: string) => {
|
|
@@ -147,11 +163,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }): React
|
|
|
147
163
|
storage.save(body.token, body.refreshToken, body.user);
|
|
148
164
|
setToken(body.token);
|
|
149
165
|
setUser(body.user);
|
|
166
|
+
onAuthEventRef.current?.('signup', body.user);
|
|
150
167
|
}, []);
|
|
151
168
|
|
|
152
169
|
const signOut = useCallback(() => {
|
|
153
170
|
// Clear server-side cookies (best-effort, don't block UI)
|
|
154
171
|
fetch('/api/auth/signout', { method: 'POST' }).catch(() => {});
|
|
172
|
+
onAuthEventRef.current?.('logout', storage.getUser());
|
|
155
173
|
storage.clear();
|
|
156
174
|
setToken(null);
|
|
157
175
|
setUser(null);
|
package/src/plugin.ts
CHANGED
|
@@ -176,33 +176,44 @@ export function authPlugin(options: AuthPluginOptions = {}): Plugin {
|
|
|
176
176
|
|
|
177
177
|
// --- Wire enrichClaims ref ---
|
|
178
178
|
const authCtx = ctx as AuthContext;
|
|
179
|
-
if (authCtx.enrichClaimsRef && clientCallbacks) {
|
|
179
|
+
if (authCtx.enrichClaimsRef && (clientCallbacks || options.audience)) {
|
|
180
180
|
authCtx.enrichClaimsRef.current = async (
|
|
181
181
|
baseClaims: Record<string, unknown>,
|
|
182
182
|
request: Request,
|
|
183
183
|
): Promise<Record<string, unknown>> => {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
184
|
+
if (clientCallbacks) {
|
|
185
|
+
// Check baseClaims first (refresh path — client_id from session data)
|
|
186
|
+
let clientId = baseClaims.client_id as string | undefined;
|
|
187
|
+
|
|
188
|
+
// Fall back to request body (signin/signup path)
|
|
189
|
+
if (!clientId) {
|
|
190
|
+
try {
|
|
191
|
+
const body = await request.clone().json();
|
|
192
|
+
clientId = body?.client_id;
|
|
193
|
+
} catch {
|
|
194
|
+
// No JSON body or no client_id
|
|
195
|
+
}
|
|
194
196
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!clientId) return {};
|
|
198
197
|
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
if (clientId) {
|
|
199
|
+
// An explicit client_id is an assertion of client-credential auth:
|
|
200
|
+
// unknown or revoked clients get no claims, so under an audience
|
|
201
|
+
// config their tokens fail verification — never a silent downgrade
|
|
202
|
+
// to first-party.
|
|
203
|
+
const client = await clientCallbacks.resolve(clientId);
|
|
204
|
+
if (!client || !client.active) return {};
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
client_id: clientId,
|
|
208
|
+
aud: client.audience ?? options.audience,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
201
212
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
};
|
|
213
|
+
// First-party signin/refresh: tokens this app issues for its own
|
|
214
|
+
// users carry the app audience — the audience layer rejects any
|
|
215
|
+
// token without it, including the ones we mint ourselves.
|
|
216
|
+
return options.audience ? { aud: options.audience } : {};
|
|
206
217
|
};
|
|
207
218
|
}
|
|
208
219
|
|