@ttt-productions/auth-core 1.6.16 → 1.6.17
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/dist/acceptance.d.ts +0 -20
- package/dist/acceptance.d.ts.map +1 -1
- package/dist/acceptance.js +0 -20
- package/dist/acceptance.js.map +1 -1
- package/dist/client/auth.d.ts +0 -5
- package/dist/client/auth.d.ts.map +1 -1
- package/dist/client/auth.js +0 -5
- package/dist/client/auth.js.map +1 -1
- package/dist/client/claims.d.ts +0 -27
- package/dist/client/claims.d.ts.map +1 -1
- package/dist/client/claims.js +0 -27
- package/dist/client/claims.js.map +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +0 -3
- package/dist/client/index.js.map +1 -1
- package/dist/email-verification.d.ts +0 -13
- package/dist/email-verification.d.ts.map +1 -1
- package/dist/email-verification.js +0 -16
- package/dist/email-verification.js.map +1 -1
- package/dist/errors.d.ts +0 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -8
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -7
- package/dist/index.js.map +1 -1
- package/dist/react/AuthProvider.d.ts.map +1 -1
- package/dist/react/AuthProvider.js +1 -25
- package/dist/react/AuthProvider.js.map +1 -1
- package/dist/react/redirect-storage.d.ts.map +1 -1
- package/dist/react/redirect-storage.js +4 -13
- package/dist/react/redirect-storage.js.map +1 -1
- package/dist/react/types.d.ts +0 -21
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/types.js.map +1 -1
- package/dist/react/useAuth.d.ts +0 -4
- package/dist/react/useAuth.d.ts.map +1 -1
- package/dist/react/useAuth.js +0 -4
- package/dist/react/useAuth.js.map +1 -1
- package/dist/react/useAuthGuard.d.ts +0 -33
- package/dist/react/useAuthGuard.d.ts.map +1 -1
- package/dist/react/useAuthGuard.js +2 -32
- package/dist/react/useAuthGuard.js.map +1 -1
- package/dist/react/useAuthState.d.ts.map +1 -1
- package/dist/react/useAuthState.js +0 -4
- package/dist/react/useAuthState.js.map +1 -1
- package/dist/refusal.d.ts +0 -6
- package/dist/refusal.d.ts.map +1 -1
- package/dist/refusal.js +0 -12
- package/dist/refusal.js.map +1 -1
- package/dist/server/assertAuth.d.ts +1 -1
- package/dist/server/assertAuth.d.ts.map +1 -1
- package/dist/server/assertAuth.js +0 -22
- package/dist/server/assertAuth.js.map +1 -1
- package/dist/server/authError.d.ts +0 -48
- package/dist/server/authError.d.ts.map +1 -1
- package/dist/server/authError.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/types.d.ts +5 -105
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +0 -2
- package/dist/server/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// on the storage access itself (FRONTEND-010), so every access here is guarded: a blocked read
|
|
3
|
-
// is "nothing saved", and a blocked write keeps the value in memory for the page's life so a
|
|
4
|
-
// same-page sign-in still lands on it. Outside a browser every call is a no-op.
|
|
1
|
+
// A browser that blocks site data throws on the storage access itself (RULE-STD-FRONTEND-010).
|
|
5
2
|
const memory = new Map();
|
|
6
3
|
function inBrowser() {
|
|
7
4
|
return typeof window !== 'undefined';
|
|
@@ -22,9 +19,7 @@ export function readRedirectPath(key) {
|
|
|
22
19
|
if (saved !== null)
|
|
23
20
|
return saved;
|
|
24
21
|
}
|
|
25
|
-
catch {
|
|
26
|
-
// Blocked store: fall through to the page-life copy.
|
|
27
|
-
}
|
|
22
|
+
catch { }
|
|
28
23
|
return memory.get(key) ?? null;
|
|
29
24
|
}
|
|
30
25
|
export function saveRedirectPath(key, path) {
|
|
@@ -37,9 +32,7 @@ export function saveRedirectPath(key, path) {
|
|
|
37
32
|
memory.delete(key);
|
|
38
33
|
return;
|
|
39
34
|
}
|
|
40
|
-
catch {
|
|
41
|
-
// Blocked store: keep the page-life copy below.
|
|
42
|
-
}
|
|
35
|
+
catch { }
|
|
43
36
|
}
|
|
44
37
|
memory.set(key, path);
|
|
45
38
|
}
|
|
@@ -50,8 +43,6 @@ export function clearRedirectPath(key) {
|
|
|
50
43
|
try {
|
|
51
44
|
storage()?.removeItem(key);
|
|
52
45
|
}
|
|
53
|
-
catch {
|
|
54
|
-
// Blocked store: nothing was persisted there.
|
|
55
|
-
}
|
|
46
|
+
catch { }
|
|
56
47
|
}
|
|
57
48
|
//# sourceMappingURL=redirect-storage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect-storage.js","sourceRoot":"","sources":["../../src/react/redirect-storage.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"redirect-storage.js","sourceRoot":"","sources":["../../src/react/redirect-storage.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAE/F,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEzC,SAAS,SAAS;IAChB,OAAO,OAAO,MAAM,KAAK,WAAW,CAAC;AACvC,CAAC;AAED,SAAS,OAAO;IACd,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,YAAY,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,CAAC,SAAS,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,OAAO,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;QAC9C,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,OAAO,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,IAAY;IACxD,IAAI,CAAC,SAAS,EAAE;QAAE,OAAO;IACzB,MAAM,KAAK,GAAG,OAAO,EAAE,CAAC;IACxB,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,CAAC;YACH,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACZ,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,IAAI,CAAC,SAAS,EAAE;QAAE,OAAO;IACzB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACnB,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;AACZ,CAAC","sourcesContent":["// A browser that blocks site data throws on the storage access itself (RULE-STD-FRONTEND-010).\n\nconst memory = new Map<string, string>();\n\nfunction inBrowser(): boolean {\n return typeof window !== 'undefined';\n}\n\nfunction storage(): Storage | null {\n try {\n return window.localStorage;\n } catch {\n return null;\n }\n}\n\nexport function readRedirectPath(key: string): string | null {\n if (!inBrowser()) return null;\n try {\n const saved = storage()?.getItem(key) ?? null;\n if (saved !== null) return saved;\n } catch {}\n return memory.get(key) ?? null;\n}\n\nexport function saveRedirectPath(key: string, path: string): void {\n if (!inBrowser()) return;\n const store = storage();\n if (store) {\n try {\n store.setItem(key, path);\n memory.delete(key);\n return;\n } catch {}\n }\n memory.set(key, path);\n}\n\nexport function clearRedirectPath(key: string): void {\n if (!inBrowser()) return;\n memory.delete(key);\n try {\n storage()?.removeItem(key);\n } catch {}\n}\n"]}
|
package/dist/react/types.d.ts
CHANGED
|
@@ -1,40 +1,19 @@
|
|
|
1
1
|
import type { Auth, User } from 'firebase/auth';
|
|
2
2
|
export interface AuthProviderConfig<TClaims = Record<string, unknown>> {
|
|
3
3
|
auth: Auth;
|
|
4
|
-
/** Transform raw ID token claims into app claims shape */
|
|
5
4
|
parseClaims: (raw: Record<string, unknown>) => TClaims;
|
|
6
|
-
/** Default claims when no user or claims unavailable */
|
|
7
5
|
defaultClaims: TClaims;
|
|
8
|
-
/** Called on Firebase auth state change (for monitoring, analytics) */
|
|
9
6
|
onAuthStateChange?: (user: User | null) => void;
|
|
10
|
-
/** Called on errors (for Sentry) */
|
|
11
7
|
onError?: (error: unknown, context: string) => void;
|
|
12
|
-
/**
|
|
13
|
-
* Optional readiness gate awaited before the provider reports `loading: false`.
|
|
14
|
-
* Use when data consumers keyed on `user` must not mount until some async
|
|
15
|
-
* client readiness completes first — e.g. an App Check / attestation token,
|
|
16
|
-
* without which every data subscription opened at mount is rejected once and
|
|
17
|
-
* dies. While it is closed the context exposes `user: null`,
|
|
18
|
-
* `isAuthenticated: false` and the default claims, so no uid-keyed consumer can
|
|
19
|
-
* mount early. The gate FAILS OPEN: a rejection is reported via `onError('readyGate')`
|
|
20
|
-
* and the provider proceeds, so a broken gate degrades to today's behavior
|
|
21
|
-
* rather than bricking the app. Called once on mount (client only).
|
|
22
|
-
*/
|
|
23
8
|
readyGate?: () => Promise<unknown>;
|
|
24
9
|
}
|
|
25
10
|
export interface AuthContextValue<TClaims = Record<string, unknown>> {
|
|
26
|
-
/** Firebase User */
|
|
27
11
|
user: User | null;
|
|
28
|
-
/** Parsed custom claims */
|
|
29
12
|
claims: TClaims;
|
|
30
|
-
/** True until auth + claims resolved */
|
|
31
13
|
loading: boolean;
|
|
32
|
-
/** Individual loading states */
|
|
33
14
|
authLoading: boolean;
|
|
34
15
|
claimsLoading: boolean;
|
|
35
|
-
/** Convenience: !!user */
|
|
36
16
|
isAuthenticated: boolean;
|
|
37
|
-
/** Force refresh claims from Firebase */
|
|
38
17
|
refreshClaims: () => Promise<void>;
|
|
39
18
|
}
|
|
40
19
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/react/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,WAAW,kBAAkB,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACnE,IAAI,EAAE,IAAI,CAAC;IAEX,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/react/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,WAAW,kBAAkB,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACnE,IAAI,EAAE,IAAI,CAAC;IAEX,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAEvD,aAAa,EAAE,OAAO,CAAC;IAEvB,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IAEhD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAEpD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAElB,MAAM,EAAE,OAAO,CAAC;IAEhB,OAAO,EAAE,OAAO,CAAC;IAEjB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IAEvB,eAAe,EAAE,OAAO,CAAC;IAEzB,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC"}
|
package/dist/react/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/react/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Auth, User } from 'firebase/auth';\n\nexport interface AuthProviderConfig<TClaims = Record<string, unknown>> {\n auth: Auth;\n\n
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/react/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Auth, User } from 'firebase/auth';\n\nexport interface AuthProviderConfig<TClaims = Record<string, unknown>> {\n auth: Auth;\n\n parseClaims: (raw: Record<string, unknown>) => TClaims;\n\n defaultClaims: TClaims;\n\n onAuthStateChange?: (user: User | null) => void;\n\n onError?: (error: unknown, context: string) => void;\n\n readyGate?: () => Promise<unknown>;\n}\n\nexport interface AuthContextValue<TClaims = Record<string, unknown>> {\n user: User | null;\n\n claims: TClaims;\n\n loading: boolean;\n\n authLoading: boolean;\n claimsLoading: boolean;\n\n isAuthenticated: boolean;\n\n refreshClaims: () => Promise<void>;\n}\n"]}
|
package/dist/react/useAuth.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
1
|
import type { AuthContextValue } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Access the auth context. Must be used within an AuthProvider.
|
|
4
|
-
* Generic param lets each app specify its own claims type.
|
|
5
|
-
*/
|
|
6
2
|
export declare function useAuth<TClaims = Record<string, unknown>>(): AuthContextValue<TClaims>;
|
|
7
3
|
//# sourceMappingURL=useAuth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../src/react/useAuth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD
|
|
1
|
+
{"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../src/react/useAuth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,wBAAgB,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,gBAAgB,CAAC,OAAO,CAAC,CAMtF"}
|
package/dist/react/useAuth.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useContext } from 'react';
|
|
3
3
|
import { AuthContext } from './AuthProvider.js';
|
|
4
|
-
/**
|
|
5
|
-
* Access the auth context. Must be used within an AuthProvider.
|
|
6
|
-
* Generic param lets each app specify its own claims type.
|
|
7
|
-
*/
|
|
8
4
|
export function useAuth() {
|
|
9
5
|
const ctx = useContext(AuthContext);
|
|
10
6
|
if (!ctx) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuth.js","sourceRoot":"","sources":["../../src/react/useAuth.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD
|
|
1
|
+
{"version":3,"file":"useAuth.js","sourceRoot":"","sources":["../../src/react/useAuth.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,MAAM,UAAU,OAAO;IACrB,MAAM,GAAG,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,GAAgC,CAAC;AAC1C,CAAC","sourcesContent":["'use client';\n\nimport { useContext } from 'react';\nimport { AuthContext } from './AuthProvider.js';\nimport type { AuthContextValue } from './types.js';\n\nexport function useAuth<TClaims = Record<string, unknown>>(): AuthContextValue<TClaims> {\n const ctx = useContext(AuthContext);\n if (!ctx) {\n throw new Error('useAuth must be used within an AuthProvider');\n }\n return ctx as AuthContextValue<TClaims>;\n}\n"]}
|
|
@@ -1,50 +1,17 @@
|
|
|
1
1
|
export interface AuthGuardConfig {
|
|
2
|
-
/**
|
|
3
|
-
* Routes accessible without authentication, matched by PREFIX
|
|
4
|
-
* (`pathname.startsWith(entry)`), so `'/share/'` covers every `/share/:id`
|
|
5
|
-
* beneath it and `'/terms'` covers `/terms/anything`. This is the SOLE source of public
|
|
6
|
-
* routes — the hook has no built-in defaults and no app-specific literals, so
|
|
7
|
-
* a consumer that needs a dynamic public section must list its prefix here.
|
|
8
|
-
*/
|
|
9
2
|
publicRoutes: string[];
|
|
10
|
-
/** Routes that redirect authenticated users away (login, register) */
|
|
11
3
|
authRedirectRoutes: string[];
|
|
12
|
-
/** Where unauthenticated users go */
|
|
13
4
|
loginRoute: string;
|
|
14
|
-
/** Where authenticated users go from authRedirectRoutes */
|
|
15
5
|
defaultRoute: string;
|
|
16
|
-
/** Special root path handling */
|
|
17
6
|
rootRedirect?: {
|
|
18
7
|
authenticated: string;
|
|
19
8
|
unauthenticated: string;
|
|
20
9
|
};
|
|
21
|
-
/** localStorage key for post-login redirect */
|
|
22
10
|
redirectKey?: string;
|
|
23
|
-
/** Navigation function (e.g. router.replace) */
|
|
24
11
|
replace: (path: string) => void;
|
|
25
|
-
/** Current path */
|
|
26
12
|
pathname: string;
|
|
27
|
-
/** From useAuth() */
|
|
28
13
|
loading: boolean;
|
|
29
|
-
/** From useAuth() */
|
|
30
14
|
isAuthenticated: boolean;
|
|
31
15
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Handles route protection. Call in app shell component.
|
|
34
|
-
*
|
|
35
|
-
* - Does nothing while loading.
|
|
36
|
-
* - Redirects unauthenticated users from protected routes to loginRoute.
|
|
37
|
-
* - Redirects authenticated users from authRedirectRoutes to defaultRoute.
|
|
38
|
-
* - Saves attempted path for post-login redirect — a visitor's deep link only. When a
|
|
39
|
-
* session ends (an observed signed-in → signed-out transition: a sign-out, a forced
|
|
40
|
-
* sign-out, an expiry, another tab), the path it ended on is NOT saved and any saved
|
|
41
|
-
* path is cleared, so the next account to sign in on this device never lands on the
|
|
42
|
-
* previous account's page.
|
|
43
|
-
*
|
|
44
|
-
* "Public" is decided ENTIRELY by `config.publicRoutes` (prefix match). The hook
|
|
45
|
-
* carries no built-in or implicit public route: every route a consumer wants
|
|
46
|
-
* reachable while signed out — including a dynamic section like `/share/:id` — is
|
|
47
|
-
* listed by that consumer as a prefix.
|
|
48
|
-
*/
|
|
49
16
|
export declare function useAuthGuard(config: AuthGuardConfig): void;
|
|
50
17
|
//# sourceMappingURL=useAuthGuard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuthGuard.d.ts","sourceRoot":"","sources":["../../src/react/useAuthGuard.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B
|
|
1
|
+
{"version":3,"file":"useAuthGuard.d.ts","sourceRoot":"","sources":["../../src/react/useAuthGuard.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAID,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAyE1D"}
|
|
@@ -2,38 +2,11 @@
|
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { clearRedirectPath, readRedirectPath, saveRedirectPath } from './redirect-storage.js';
|
|
4
4
|
const DEFAULT_REDIRECT_KEY = 'auth_redirect_path';
|
|
5
|
-
/**
|
|
6
|
-
* Handles route protection. Call in app shell component.
|
|
7
|
-
*
|
|
8
|
-
* - Does nothing while loading.
|
|
9
|
-
* - Redirects unauthenticated users from protected routes to loginRoute.
|
|
10
|
-
* - Redirects authenticated users from authRedirectRoutes to defaultRoute.
|
|
11
|
-
* - Saves attempted path for post-login redirect — a visitor's deep link only. When a
|
|
12
|
-
* session ends (an observed signed-in → signed-out transition: a sign-out, a forced
|
|
13
|
-
* sign-out, an expiry, another tab), the path it ended on is NOT saved and any saved
|
|
14
|
-
* path is cleared, so the next account to sign in on this device never lands on the
|
|
15
|
-
* previous account's page.
|
|
16
|
-
*
|
|
17
|
-
* "Public" is decided ENTIRELY by `config.publicRoutes` (prefix match). The hook
|
|
18
|
-
* carries no built-in or implicit public route: every route a consumer wants
|
|
19
|
-
* reachable while signed out — including a dynamic section like `/share/:id` — is
|
|
20
|
-
* listed by that consumer as a prefix.
|
|
21
|
-
*/
|
|
22
5
|
export function useAuthGuard(config) {
|
|
23
6
|
const { publicRoutes, authRedirectRoutes, loginRoute, defaultRoute, rootRedirect, redirectKey = DEFAULT_REDIRECT_KEY, replace, pathname, loading, isAuthenticated, } = config;
|
|
24
|
-
// The
|
|
25
|
-
//
|
|
26
|
-
// asynchronously (the router fetches the target route first), so this effect can
|
|
27
|
-
// legitimately re-run while pathname still reads the auth route (an auth-state
|
|
28
|
-
// re-render, a loading flap). A re-run then finds the key already gone and
|
|
29
|
-
// re-replaces to defaultRoute, CLOBBERING the in-flight saved-path navigation —
|
|
30
|
-
// a deep link that routed through login then landed on defaultRoute instead of
|
|
31
|
-
// the saved path. The latch resets whenever the pathname is off the auth-route set.
|
|
7
|
+
// The router commits a navigation asynchronously, so this effect can re-run on the auth route
|
|
8
|
+
// after the key is consumed; the latch stops a second replace clobbering it.
|
|
32
9
|
const authRouteRedirected = useRef(false);
|
|
33
|
-
// The last settled authentication state this guard observed (null before the first), and
|
|
34
|
-
// the pathname a session ended on. The effect can re-run on that same pathname before the
|
|
35
|
-
// login navigation commits, so the "do not save" decision is held for that pathname rather
|
|
36
|
-
// than for one run.
|
|
37
10
|
const lastAuthenticated = useRef(null);
|
|
38
11
|
const sessionEndedOn = useRef(null);
|
|
39
12
|
useEffect(() => {
|
|
@@ -50,21 +23,18 @@ export function useAuthGuard(config) {
|
|
|
50
23
|
if (!authRedirectRoutes.includes(pathname)) {
|
|
51
24
|
authRouteRedirected.current = false;
|
|
52
25
|
}
|
|
53
|
-
// Root path special handling
|
|
54
26
|
if (rootRedirect && pathname === '/') {
|
|
55
27
|
replace(isAuthenticated ? rootRedirect.authenticated : rootRedirect.unauthenticated);
|
|
56
28
|
return;
|
|
57
29
|
}
|
|
58
30
|
const isPublic = publicRoutes.some((r) => pathname.startsWith(r));
|
|
59
31
|
const isAuthRedirect = authRedirectRoutes.includes(pathname);
|
|
60
|
-
// Unauthenticated on protected route -> save a visitor's path, go to login
|
|
61
32
|
if (!isAuthenticated && !isPublic) {
|
|
62
33
|
if (sessionEndedOn.current === null)
|
|
63
34
|
saveRedirectPath(redirectKey, pathname);
|
|
64
35
|
replace(loginRoute);
|
|
65
36
|
return;
|
|
66
37
|
}
|
|
67
|
-
// Authenticated user on auth-only route (login, register, etc.)
|
|
68
38
|
if (isAuthenticated && isAuthRedirect) {
|
|
69
39
|
if (authRouteRedirected.current)
|
|
70
40
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuthGuard.js","sourceRoot":"","sources":["../../src/react/useAuthGuard.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"useAuthGuard.js","sourceRoot":"","sources":["../../src/react/useAuthGuard.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAe9F,MAAM,oBAAoB,GAAG,oBAAoB,CAAC;AAElD,MAAM,UAAU,YAAY,CAAC,MAAuB;IAClD,MAAM,EACJ,YAAY,EACZ,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,WAAW,GAAG,oBAAoB,EAClC,OAAO,EACP,QAAQ,EACR,OAAO,EACP,eAAe,GAChB,GAAG,MAAM,CAAC;IAEX,8FAA8F;IAC9F,6EAA6E;IAC7E,MAAM,mBAAmB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAE1C,MAAM,iBAAiB,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IACvD,MAAM,cAAc,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IAEnD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO;YAAE,OAAO;QAEpB,IAAI,iBAAiB,CAAC,OAAO,KAAK,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC3D,cAAc,CAAC,OAAO,GAAG,QAAQ,CAAC;YAClC,iBAAiB,CAAC,WAAW,CAAC,CAAC;QACjC,CAAC;aAAM,IAAI,eAAe,IAAI,cAAc,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YAClE,cAAc,CAAC,OAAO,GAAG,IAAI,CAAC;QAChC,CAAC;QACD,iBAAiB,CAAC,OAAO,GAAG,eAAe,CAAC;QAE5C,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3C,mBAAmB,CAAC,OAAO,GAAG,KAAK,CAAC;QACtC,CAAC;QAED,IAAI,YAAY,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;YACrC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;YACrF,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAClE,MAAM,cAAc,GAAG,kBAAkB,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAE7D,IAAI,CAAC,eAAe,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClC,IAAI,cAAc,CAAC,OAAO,KAAK,IAAI;gBAAE,gBAAgB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;YAC7E,OAAO,CAAC,UAAU,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QAED,IAAI,eAAe,IAAI,cAAc,EAAE,CAAC;YACtC,IAAI,mBAAmB,CAAC,OAAO;gBAAE,OAAO;YACxC,mBAAmB,CAAC,OAAO,GAAG,IAAI,CAAC;YACnC,IAAI,MAAM,GAAG,YAAY,CAAC;YAC1B,MAAM,KAAK,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;YAC5C,IAAI,KAAK,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACjD,MAAM,GAAG,KAAK,CAAC;YACjB,CAAC;YACD,iBAAiB,CAAC,WAAW,CAAC,CAAC;YAC/B,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,EAAE;QACD,OAAO;QACP,eAAe;QACf,QAAQ;QACR,YAAY;QACZ,kBAAkB;QAClB,UAAU;QACV,YAAY;QACZ,YAAY;QACZ,WAAW;QACX,OAAO;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useEffect, useRef } from 'react';\nimport { clearRedirectPath, readRedirectPath, saveRedirectPath } from './redirect-storage.js';\n\nexport interface AuthGuardConfig {\n publicRoutes: string[];\n authRedirectRoutes: string[];\n loginRoute: string;\n defaultRoute: string;\n rootRedirect?: { authenticated: string; unauthenticated: string };\n redirectKey?: string;\n replace: (path: string) => void;\n pathname: string;\n loading: boolean;\n isAuthenticated: boolean;\n}\n\nconst DEFAULT_REDIRECT_KEY = 'auth_redirect_path';\n\nexport function useAuthGuard(config: AuthGuardConfig): void {\n const {\n publicRoutes,\n authRedirectRoutes,\n loginRoute,\n defaultRoute,\n rootRedirect,\n redirectKey = DEFAULT_REDIRECT_KEY,\n replace,\n pathname,\n loading,\n isAuthenticated,\n } = config;\n\n // The router commits a navigation asynchronously, so this effect can re-run on the auth route\n // after the key is consumed; the latch stops a second replace clobbering it.\n const authRouteRedirected = useRef(false);\n\n const lastAuthenticated = useRef<boolean | null>(null);\n const sessionEndedOn = useRef<string | null>(null);\n\n useEffect(() => {\n if (loading) return;\n\n if (lastAuthenticated.current === true && !isAuthenticated) {\n sessionEndedOn.current = pathname;\n clearRedirectPath(redirectKey);\n } else if (isAuthenticated || sessionEndedOn.current !== pathname) {\n sessionEndedOn.current = null;\n }\n lastAuthenticated.current = isAuthenticated;\n\n if (!authRedirectRoutes.includes(pathname)) {\n authRouteRedirected.current = false;\n }\n\n if (rootRedirect && pathname === '/') {\n replace(isAuthenticated ? rootRedirect.authenticated : rootRedirect.unauthenticated);\n return;\n }\n\n const isPublic = publicRoutes.some((r) => pathname.startsWith(r));\n const isAuthRedirect = authRedirectRoutes.includes(pathname);\n\n if (!isAuthenticated && !isPublic) {\n if (sessionEndedOn.current === null) saveRedirectPath(redirectKey, pathname);\n replace(loginRoute);\n return;\n }\n\n if (isAuthenticated && isAuthRedirect) {\n if (authRouteRedirected.current) return;\n authRouteRedirected.current = true;\n let target = defaultRoute;\n const saved = readRedirectPath(redirectKey);\n if (saved && !authRedirectRoutes.includes(saved)) {\n target = saved;\n }\n clearRedirectPath(redirectKey);\n replace(target);\n }\n }, [\n loading,\n isAuthenticated,\n pathname,\n publicRoutes,\n authRedirectRoutes,\n loginRoute,\n defaultRoute,\n rootRedirect,\n redirectKey,\n replace,\n ]);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuthState.d.ts","sourceRoot":"","sources":["../../src/react/useAuthState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAGhD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"useAuthState.d.ts","sourceRoot":"","sources":["../../src/react/useAuthState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAGhD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CA0BlD"}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { onAuthStateChanged } from '../client/auth.js';
|
|
3
3
|
export function useAuthState(auth) {
|
|
4
|
-
// Initialize state based on current auth status to avoid initial flash
|
|
5
4
|
const [user, setUser] = useState(() => auth.currentUser);
|
|
6
5
|
const [loading, setLoading] = useState(() => auth.currentUser === null);
|
|
7
6
|
const [error, setError] = useState(null);
|
|
8
7
|
useEffect(() => {
|
|
9
|
-
// Only set loading if we don't have a user and haven't loaded yet
|
|
10
|
-
// This prevents the "flash" when a user is already signed in (e.g. hydration)
|
|
11
|
-
// or when simply refreshing the token.
|
|
12
8
|
if (auth.currentUser === null) {
|
|
13
9
|
setLoading(true);
|
|
14
10
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAuthState.js","sourceRoot":"","sources":["../../src/react/useAuthState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAQvD,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,
|
|
1
|
+
{"version":3,"file":"useAuthState.js","sourceRoot":"","sources":["../../src/react/useAuthState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAQvD,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAc,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACtE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAU,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC;IACjF,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAiB,IAAI,CAAC,CAAC;IAEzD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;YAC9B,UAAU,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,KAAK,GAAG,kBAAkB,CAC9B,IAAI,EACJ,CAAC,CAAC,EAAE,EAAE;YACJ,OAAO,CAAC,CAAC,CAAC,CAAC;YACX,QAAQ,CAAC,IAAI,CAAC,CAAC;YACf,UAAU,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC,EACD,CAAC,CAAC,EAAE,EAAE;YACJ,QAAQ,CAAC,CAAC,CAAC,CAAC;YACZ,UAAU,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC,CACF,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAClC,CAAC","sourcesContent":["import { useEffect, useState } from 'react';\nimport type { Auth, User } from 'firebase/auth';\nimport { onAuthStateChanged } from '../client/auth.js';\n\nexport type AuthState = {\n user: User | null;\n loading: boolean;\n error: unknown | null;\n};\n\nexport function useAuthState(auth: Auth): AuthState {\n const [user, setUser] = useState<User | null>(() => auth.currentUser);\n const [loading, setLoading] = useState<boolean>(() => auth.currentUser === null);\n const [error, setError] = useState<unknown | null>(null);\n\n useEffect(() => {\n if (auth.currentUser === null) {\n setLoading(true);\n }\n\n const unsub = onAuthStateChanged(\n auth,\n (u) => {\n setUser(u);\n setError(null);\n setLoading(false);\n },\n (e) => {\n setError(e);\n setLoading(false);\n },\n );\n return unsub;\n }, [auth]);\n\n return { user, loading, error };\n}\n"]}
|
package/dist/refusal.d.ts
CHANGED
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `details` of `err` when it is a `failed-precondition` error in either spelling —
|
|
3
|
-
* server-side or client-side callable error alike — otherwise `undefined`. Each recognizer then
|
|
4
|
-
* asks whether those details are its own refusal's, so refusals are told apart by
|
|
5
|
-
* `details.reason`, never by message text.
|
|
6
|
-
*/
|
|
7
1
|
export declare function failedPreconditionDetails(err: unknown): unknown;
|
|
8
2
|
//# sourceMappingURL=refusal.d.ts.map
|
package/dist/refusal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refusal.d.ts","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"refusal.d.ts","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAKA,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAM/D"}
|
package/dist/refusal.js
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
// The code both structured `assertAuth` refusals carry — the acceptance refusal
|
|
2
|
-
// (./acceptance.ts) and the email-verification refusal (./email-verification.ts) — and the one
|
|
3
|
-
// place their recognizers read it. Internal: the root exports the recognizers, not this.
|
|
4
|
-
// Pure: no Firebase runtime.
|
|
5
|
-
// The HttpsError code, as the server (`failed-precondition`) and the Firebase client SDK
|
|
6
|
-
// (`functions/failed-precondition`) spell it.
|
|
7
1
|
const FAILED_PRECONDITION_CODES = new Set([
|
|
8
2
|
'failed-precondition',
|
|
9
3
|
'functions/failed-precondition',
|
|
10
4
|
]);
|
|
11
|
-
/**
|
|
12
|
-
* The `details` of `err` when it is a `failed-precondition` error in either spelling —
|
|
13
|
-
* server-side or client-side callable error alike — otherwise `undefined`. Each recognizer then
|
|
14
|
-
* asks whether those details are its own refusal's, so refusals are told apart by
|
|
15
|
-
* `details.reason`, never by message text.
|
|
16
|
-
*/
|
|
17
5
|
export function failedPreconditionDetails(err) {
|
|
18
6
|
if (!err || typeof err !== 'object')
|
|
19
7
|
return undefined;
|
package/dist/refusal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refusal.js","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"refusal.js","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAAA,MAAM,yBAAyB,GAAwB,IAAI,GAAG,CAAC;IAC7D,qBAAqB;IACrB,+BAA+B;CAChC,CAAC,CAAC;AAEH,MAAM,UAAU,yBAAyB,CAAC,GAAY;IACpD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACtD,MAAM,CAAC,GAAG,GAA4C,CAAC;IACvD,OAAO,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;QACxE,CAAC,CAAC,CAAC,CAAC,OAAO;QACX,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC","sourcesContent":["const FAILED_PRECONDITION_CODES: ReadonlySet<string> = new Set([\n 'failed-precondition',\n 'functions/failed-precondition',\n]);\n\nexport function failedPreconditionDetails(err: unknown): unknown {\n if (!err || typeof err !== 'object') return undefined;\n const e = err as { code?: unknown; details?: unknown };\n return typeof e.code === 'string' && FAILED_PRECONDITION_CODES.has(e.code)\n ? e.details\n : undefined;\n}\n"]}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { AssertAuthConfig, AssertAuthFn } from './types.js';
|
|
2
|
-
export declare function createAssertAuth<TUser, TAdmin = void>(config: AssertAuthConfig<TUser, TAdmin>): AssertAuthFn<TUser, TAdmin>;
|
|
2
|
+
export declare function createAssertAuth<TUser, TAdmin = void, TAdminOptions = never>(config: AssertAuthConfig<TUser, TAdmin, TAdminOptions>): AssertAuthFn<TUser, TAdmin, TAdminOptions>;
|
|
3
3
|
//# sourceMappingURL=assertAuth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertAuth.d.ts","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"assertAuth.d.ts","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAiC,MAAM,YAAY,CAAC;AAEhG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,aAAa,GAAG,KAAK,EAC1E,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,GACrD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,CAuE5C"}
|
|
@@ -1,27 +1,13 @@
|
|
|
1
|
-
// Factory that produces an assertAuth function bound to a consuming app's
|
|
2
|
-
// Firestore paths, user shape, and admin check.
|
|
3
|
-
//
|
|
4
|
-
// Every Cloud Functions callable in the consuming app should invoke the
|
|
5
|
-
// returned assertAuth as its first executable line. The check order is:
|
|
6
|
-
// 1. Authentication
|
|
7
|
-
// 2. Email verification
|
|
8
|
-
// 3. User doc Firestore read (when status check is needed)
|
|
9
|
-
// 4. Status check (suspended / banned)
|
|
10
|
-
// 5. Acceptance-level gate (only when config.acceptance is set)
|
|
11
|
-
// 6. Admin check (delegated to config.requireAdmin)
|
|
12
1
|
import { ACCEPTANCE_REQUIRED_REASON, normalizeAcceptanceLevel } from '../acceptance.js';
|
|
13
2
|
import { EMAIL_VERIFICATION_REQUIRED_REASON } from '../email-verification.js';
|
|
14
3
|
import { AuthAssertionError } from './authError.js';
|
|
15
4
|
export function createAssertAuth(config) {
|
|
16
5
|
return async function assertAuth(request, requirements = {}) {
|
|
17
|
-
// 1. Authentication check (always first)
|
|
18
6
|
if (!request.auth) {
|
|
19
7
|
throw new AuthAssertionError('unauthenticated', 'You must be authenticated');
|
|
20
8
|
}
|
|
21
9
|
const uid = request.auth.uid;
|
|
22
10
|
const token = request.auth.token;
|
|
23
|
-
// 2. Email verification check. The refusal carries its reason in `details`, so the client
|
|
24
|
-
// recognizes it structurally (`isEmailVerificationRequiredError`), never by the message.
|
|
25
11
|
if (requirements.emailVerified === true && requirements.allowUnverified !== true) {
|
|
26
12
|
if (token.email_verified !== true) {
|
|
27
13
|
throw new AuthAssertionError('failed-precondition', 'Email must be verified', {
|
|
@@ -29,11 +15,9 @@ export function createAssertAuth(config) {
|
|
|
29
15
|
});
|
|
30
16
|
}
|
|
31
17
|
}
|
|
32
|
-
// 3. User doc Firestore read (skipped when allowAnyStatus is set)
|
|
33
18
|
const db = config.firestore();
|
|
34
19
|
const userSnap = requirements.allowAnyStatus !== true ? await db.doc(config.userProfilePath(uid)).get() : null;
|
|
35
20
|
let userDoc;
|
|
36
|
-
// 4. Status check (default: block suspended + banned; skipped if allowAnyStatus)
|
|
37
21
|
if (requirements.allowAnyStatus !== true) {
|
|
38
22
|
if (!userSnap || !userSnap.exists) {
|
|
39
23
|
throw new AuthAssertionError('not-found', 'User profile not found');
|
|
@@ -48,10 +32,6 @@ export function createAssertAuth(config) {
|
|
|
48
32
|
}
|
|
49
33
|
userDoc = userData;
|
|
50
34
|
}
|
|
51
|
-
// 5. Acceptance-level gate. Runs only when the app configured one and the callable did
|
|
52
|
-
// not opt out. The required level comes from the app's cheap (cached) reader and the
|
|
53
|
-
// accepted level from the verified token, so the gate adds no per-call Firestore read.
|
|
54
|
-
// It sits after the status check so a banned caller still gets the standing refusal.
|
|
55
35
|
if (config.acceptance !== undefined && requirements.allowUnaccepted !== true) {
|
|
56
36
|
const requiredLevel = normalizeAcceptanceLevel(await config.acceptance.requiredLevel());
|
|
57
37
|
if (requiredLevel > 0) {
|
|
@@ -61,12 +41,10 @@ export function createAssertAuth(config) {
|
|
|
61
41
|
}
|
|
62
42
|
}
|
|
63
43
|
}
|
|
64
|
-
// 6. Admin check (delegates to config.requireAdmin)
|
|
65
44
|
let adminResult;
|
|
66
45
|
if (requirements.admin !== undefined) {
|
|
67
46
|
adminResult = await config.requireAdmin(uid, token, requirements.admin);
|
|
68
47
|
}
|
|
69
|
-
// 7. Return context with whatever docs were fetched
|
|
70
48
|
const ctx = { uid, token };
|
|
71
49
|
if (userDoc !== undefined) {
|
|
72
50
|
ctx.userDoc = userDoc;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertAuth.js","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"assertAuth.js","sourceRoot":"","sources":["../../src/server/assertAuth.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CallableRequest } from 'firebase-functions/v2/https';\nimport { ACCEPTANCE_REQUIRED_REASON, normalizeAcceptanceLevel } from '../acceptance.js';\nimport { EMAIL_VERIFICATION_REQUIRED_REASON } from '../email-verification.js';\nimport { AuthAssertionError } from './authError.js';\nimport type { AssertAuthConfig, AssertAuthFn, AuthContext, AuthRequirements } from './types.js';\n\nexport function createAssertAuth<TUser, TAdmin = void, TAdminOptions = never>(\n config: AssertAuthConfig<TUser, TAdmin, TAdminOptions>,\n): AssertAuthFn<TUser, TAdmin, TAdminOptions> {\n return async function assertAuth(\n request: CallableRequest,\n requirements: AuthRequirements<TAdminOptions> = {},\n ): Promise<AuthContext<TUser, TAdmin>> {\n if (!request.auth) {\n throw new AuthAssertionError('unauthenticated', 'You must be authenticated');\n }\n\n const uid = request.auth.uid;\n const token = request.auth.token;\n\n if (requirements.emailVerified === true && requirements.allowUnverified !== true) {\n if (token.email_verified !== true) {\n throw new AuthAssertionError('failed-precondition', 'Email must be verified', {\n reason: EMAIL_VERIFICATION_REQUIRED_REASON,\n });\n }\n }\n\n const db = config.firestore();\n const userSnap =\n requirements.allowAnyStatus !== true ? await db.doc(config.userProfilePath(uid)).get() : null;\n\n let userDoc: TUser | undefined;\n\n if (requirements.allowAnyStatus !== true) {\n if (!userSnap || !userSnap.exists) {\n throw new AuthAssertionError('not-found', 'User profile not found');\n }\n const userData = userSnap.data() as TUser;\n const status = config.getUserStatus(userData);\n if (status === 'banned') {\n throw new AuthAssertionError('permission-denied', 'Account is not in good standing');\n }\n if (status === 'suspended' && requirements.allowSuspended !== true) {\n throw new AuthAssertionError('permission-denied', 'Account is not in good standing');\n }\n userDoc = userData;\n }\n\n if (config.acceptance !== undefined && requirements.allowUnaccepted !== true) {\n const requiredLevel = normalizeAcceptanceLevel(await config.acceptance.requiredLevel());\n if (requiredLevel > 0) {\n const acceptedLevel = normalizeAcceptanceLevel(\n (token as Record<string, unknown>)[config.acceptance.claimKey],\n );\n if (acceptedLevel < requiredLevel) {\n throw new AuthAssertionError(\n 'failed-precondition',\n 'The current documents must be accepted before continuing',\n { reason: ACCEPTANCE_REQUIRED_REASON, requiredLevel, acceptedLevel },\n );\n }\n }\n }\n\n let adminResult: TAdmin | undefined;\n if (requirements.admin !== undefined) {\n adminResult = await config.requireAdmin(uid, token, requirements.admin);\n }\n\n const ctx: AuthContext<TUser, TAdmin> = { uid, token };\n if (userDoc !== undefined) {\n ctx.userDoc = userDoc;\n }\n if (requirements.admin !== undefined) {\n ctx.admin = adminResult as TAdmin;\n }\n return ctx;\n };\n}\n"]}
|
|
@@ -1,54 +1,6 @@
|
|
|
1
1
|
import type { AcceptanceRequiredDetails } from '../acceptance.js';
|
|
2
2
|
import type { EmailVerificationRequiredDetails } from '../email-verification.js';
|
|
3
|
-
/**
|
|
4
|
-
* Typed failure channel for `createAssertAuth`.
|
|
5
|
-
*
|
|
6
|
-
* Every EXPECTED auth outcome (not signed in, email unverified, missing profile,
|
|
7
|
-
* account not in good standing) throws an `AuthAssertionError` whose `code` is
|
|
8
|
-
* deliberately the matching `HttpsError` code string, so a consuming Cloud
|
|
9
|
-
* Function maps it 1:1 at the app boundary:
|
|
10
|
-
*
|
|
11
|
-
* catch (err) {
|
|
12
|
-
* if (err instanceof AuthAssertionError) throw new HttpsError(err.code, err.message);
|
|
13
|
-
* throw err;
|
|
14
|
-
* }
|
|
15
|
-
*
|
|
16
|
-
* WHY THE PACKAGE MUST NOT BUILD THE HttpsError ITSELF: firebase-functions 7.2.5
|
|
17
|
-
* ships TWO distinct `HttpsError` classes — the one
|
|
18
|
-
* exported from the ESM entry `firebase-functions/v2/https`, and the one the CJS
|
|
19
|
-
* callable runtime closes over and `instanceof`-checks at
|
|
20
|
-
* `lib/common/providers/https.js`. An `HttpsError` constructed by this package's
|
|
21
|
-
* ESM `dist` fails that identity check, so `onCall` logged "Unhandled error" and
|
|
22
|
-
* replaced a correct 400 with `500 INTERNAL`. Measured:
|
|
23
|
-
*
|
|
24
|
-
* ESM class === CJS class ? false
|
|
25
|
-
* esm-built error instanceof CJS class ? false
|
|
26
|
-
* httpErrorCode.status: 400 (the error itself was correct)
|
|
27
|
-
*
|
|
28
|
-
* Because `assertAuth` is the first line of every callable, that turned EVERY auth
|
|
29
|
-
* rejection — unauthenticated, unverified email, suspended, banned — into a generic
|
|
30
|
-
* 500: users saw a generic error instead of the verify-email dialog, and monitoring
|
|
31
|
-
* filled with 500s that were really expected answers. Constructing the HttpsError in
|
|
32
|
-
* APP module space fixes the identity permanently and cannot regress when
|
|
33
|
-
* firebase-functions repackages.
|
|
34
|
-
*
|
|
35
|
-
* This also brings auth-core in line with engineering-rules QUALITY-102 (generic
|
|
36
|
-
* packages stay framework-agnostic; the consuming app's adapter binds the framework) and
|
|
37
|
-
* mirrors `report-core`'s `ReportCoreTaskError`, which already works this way.
|
|
38
|
-
*/
|
|
39
3
|
export type AuthAssertionErrorCode = 'unauthenticated' | 'failed-precondition' | 'not-found' | 'permission-denied';
|
|
40
|
-
/**
|
|
41
|
-
* Structured details an `AuthAssertionError` may carry — one per refusal the client answers
|
|
42
|
-
* with a surface of its own, each told apart by `reason`: the email-verification refusal
|
|
43
|
-
* (`reason: 'email-verification-required'`, recognized by `isEmailVerificationRequiredError`)
|
|
44
|
-
* and the acceptance refusal (`reason: 'acceptance-required'`, recognized by
|
|
45
|
-
* `isAcceptanceRequiredError`). The app forwards `details` as the `HttpsError` details so the
|
|
46
|
-
* client can recognize them:
|
|
47
|
-
*
|
|
48
|
-
* throw new HttpsError(err.code, err.message, err.details);
|
|
49
|
-
*
|
|
50
|
-
* Every other rejection leaves `details` undefined.
|
|
51
|
-
*/
|
|
52
4
|
export type AuthAssertionErrorDetails = AcceptanceRequiredDetails | EmailVerificationRequiredDetails;
|
|
53
5
|
export declare class AuthAssertionError extends Error {
|
|
54
6
|
readonly code: AuthAssertionErrorCode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authError.d.ts","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"authError.d.ts","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,0BAA0B,CAAC;AAIjF,MAAM,MAAM,sBAAsB,GAChC,iBAAiB,GAAG,qBAAqB,GAAG,WAAW,GAAG,mBAAmB,CAAC;AAEhF,MAAM,MAAM,yBAAyB,GACnC,yBAAyB,GAAG,gCAAgC,CAAC;AAE/D,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,yBAAyB,CAAC;gBAEjC,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,yBAAyB;CAM/F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authError.js","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"authError.js","sourceRoot":"","sources":["../../src/server/authError.ts"],"names":[],"mappings":"AAWA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAClC,IAAI,CAAyB;IAC7B,OAAO,CAA6B;IAE7C,YAAY,IAA4B,EAAE,OAAe,EAAE,OAAmC;QAC5F,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACpD,CAAC;CACF","sourcesContent":["import type { AcceptanceRequiredDetails } from '../acceptance.js';\nimport type { EmailVerificationRequiredDetails } from '../email-verification.js';\n\n// firebase-functions' callable runtime instanceof-checks its own HttpsError class, which one built\n// in this package's ESM dist fails (a 500 INTERNAL), so the consuming app builds the HttpsError.\nexport type AuthAssertionErrorCode =\n 'unauthenticated' | 'failed-precondition' | 'not-found' | 'permission-denied';\n\nexport type AuthAssertionErrorDetails =\n AcceptanceRequiredDetails | EmailVerificationRequiredDetails;\n\nexport class AuthAssertionError extends Error {\n readonly code: AuthAssertionErrorCode;\n readonly details?: AuthAssertionErrorDetails;\n\n constructor(code: AuthAssertionErrorCode, message: string, details?: AuthAssertionErrorDetails) {\n super(message);\n this.name = 'AuthAssertionError';\n this.code = code;\n if (details !== undefined) this.details = details;\n }\n}\n"]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createAssertAuth } from './assertAuth.js';
|
|
2
2
|
export { AuthAssertionError } from './authError.js';
|
|
3
3
|
export type { AuthAssertionErrorCode, AuthAssertionErrorDetails } from './authError.js';
|
|
4
|
-
export type { AcceptanceGateConfig, AssertAuthConfig, AssertAuthFn, AuthContext, AuthRequirements,
|
|
4
|
+
export type { AcceptanceGateConfig, AssertAuthConfig, AssertAuthFn, AuthContext, AuthRequirements, UserStatus, } from './types.js';
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,YAAY,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AACxF,YAAY,EACV,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,UAAU,GACX,MAAM,YAAY,CAAC"}
|
package/dist/server/index.js
CHANGED
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["export { createAssertAuth } from './assertAuth.js';\nexport { AuthAssertionError } from './authError.js';\nexport type { AuthAssertionErrorCode, AuthAssertionErrorDetails } from './authError.js';\nexport type {\n AcceptanceGateConfig,\n AssertAuthConfig,\n AssertAuthFn,\n AuthContext,\n AuthRequirements,\n UserStatus,\n} from './types.js';\n"]}
|