@ttt-productions/auth-core 0.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/package.json +31 -0
- package/src/auth.ts +21 -0
- package/src/claims.ts +54 -0
- package/src/errors.ts +126 -0
- package/src/index.ts +3 -0
- package/src/react/index.ts +1 -0
- package/src/react/useAuthState.ts +34 -0
- package/tsconfig.json +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ttt-productions/auth-core",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/ttt-productions/ttt-packages.git",
|
|
7
|
+
"directory": "packages/auth-core"
|
|
8
|
+
},
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"main": "dist/index.cjs",
|
|
11
|
+
"module": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.cjs"
|
|
18
|
+
},
|
|
19
|
+
"./react": {
|
|
20
|
+
"types": "./dist/react/index.d.ts",
|
|
21
|
+
"import": "./dist/react/index.js",
|
|
22
|
+
"require": "./dist/react/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"firebase": "^10.0.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependenciesMeta": {
|
|
29
|
+
"react": { "optional": true }
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Auth, User } from "firebase/auth";
|
|
2
|
+
import { onAuthStateChanged as fbOnAuthStateChanged } from "firebase/auth";
|
|
3
|
+
|
|
4
|
+
export type Unsubscribe = () => void;
|
|
5
|
+
|
|
6
|
+
export function getAuthUser(auth: Auth): User | null {
|
|
7
|
+
return auth.currentUser;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thin wrapper around Firebase onAuthStateChanged.
|
|
12
|
+
* - always returns the unsubscribe function
|
|
13
|
+
* - preserves Firebase callback signature
|
|
14
|
+
*/
|
|
15
|
+
export function onAuthStateChanged(
|
|
16
|
+
auth: Auth,
|
|
17
|
+
cb: (user: User | null) => void,
|
|
18
|
+
onError?: (error: unknown) => void
|
|
19
|
+
): Unsubscribe {
|
|
20
|
+
return fbOnAuthStateChanged(auth, cb, onError);
|
|
21
|
+
}
|
package/src/claims.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Auth, User, IdTokenResult } from "firebase/auth";
|
|
2
|
+
|
|
3
|
+
export type IdTokenClaims = IdTokenResult["claims"];
|
|
4
|
+
|
|
5
|
+
export type ParsedClaims = {
|
|
6
|
+
// common defaults (kept generic)
|
|
7
|
+
roles: string[];
|
|
8
|
+
// raw claims for app-level decisions
|
|
9
|
+
raw: IdTokenClaims;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the current user's ID token claims.
|
|
14
|
+
* Returns null if no current user.
|
|
15
|
+
*/
|
|
16
|
+
export async function getIdTokenClaims(user: User | null): Promise<IdTokenClaims | null> {
|
|
17
|
+
if (!user) return null;
|
|
18
|
+
const res = await user.getIdTokenResult(false);
|
|
19
|
+
return res.claims;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Forces refresh of ID token + claims.
|
|
24
|
+
* Returns null if no current user.
|
|
25
|
+
*/
|
|
26
|
+
export async function refreshClaims(user: User | null): Promise<IdTokenClaims | null> {
|
|
27
|
+
if (!user) return null;
|
|
28
|
+
const res = await user.getIdTokenResult(true);
|
|
29
|
+
return res.claims;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convenience helper when you only have Auth.
|
|
34
|
+
*/
|
|
35
|
+
export async function refreshClaimsFromAuth(auth: Auth): Promise<IdTokenClaims | null> {
|
|
36
|
+
return refreshClaims(auth.currentUser);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Keep parsing intentionally minimal and generic:
|
|
41
|
+
* - normalize "roles" into string[]
|
|
42
|
+
* - keep raw claims attached
|
|
43
|
+
*/
|
|
44
|
+
export function parseClaims(claims: IdTokenClaims | null | undefined): ParsedClaims {
|
|
45
|
+
const raw = (claims ?? {}) as IdTokenClaims;
|
|
46
|
+
|
|
47
|
+
const rolesValue = (raw as any).roles;
|
|
48
|
+
const roles =
|
|
49
|
+
Array.isArray(rolesValue) ? rolesValue.filter((x) => typeof x === "string") :
|
|
50
|
+
typeof rolesValue === "string" ? [rolesValue] :
|
|
51
|
+
[];
|
|
52
|
+
|
|
53
|
+
return { roles, raw };
|
|
54
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export type AuthErrorCode =
|
|
2
|
+
| "AUTH_UNKNOWN"
|
|
3
|
+
| "AUTH_NETWORK_ERROR"
|
|
4
|
+
| "AUTH_TOO_MANY_REQUESTS"
|
|
5
|
+
| "AUTH_POPUP_CLOSED"
|
|
6
|
+
| "AUTH_POPUP_BLOCKED"
|
|
7
|
+
| "AUTH_CANCELLED"
|
|
8
|
+
| "AUTH_USER_DISABLED"
|
|
9
|
+
| "AUTH_USER_NOT_FOUND"
|
|
10
|
+
| "AUTH_WRONG_PASSWORD"
|
|
11
|
+
| "AUTH_INVALID_EMAIL"
|
|
12
|
+
| "AUTH_EMAIL_ALREADY_IN_USE"
|
|
13
|
+
| "AUTH_WEAK_PASSWORD"
|
|
14
|
+
| "AUTH_REQUIRES_RECENT_LOGIN"
|
|
15
|
+
| "AUTH_INVALID_CREDENTIAL"
|
|
16
|
+
| "AUTH_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL"
|
|
17
|
+
| "AUTH_PROVIDER_ALREADY_LINKED"
|
|
18
|
+
| "AUTH_CREDENTIAL_ALREADY_IN_USE";
|
|
19
|
+
|
|
20
|
+
export type NormalizedAuthError = {
|
|
21
|
+
code: AuthErrorCode;
|
|
22
|
+
message: string;
|
|
23
|
+
firebaseCode?: string;
|
|
24
|
+
details?: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function pickFirebaseCode(err: unknown): string | undefined {
|
|
28
|
+
if (!err || typeof err !== "object") return undefined;
|
|
29
|
+
const anyErr = err as any;
|
|
30
|
+
const c = anyErr.code;
|
|
31
|
+
return typeof c === "string" ? c : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MAP: Record<string, { code: AuthErrorCode; message: string }> = {
|
|
35
|
+
"auth/network-request-failed": {
|
|
36
|
+
code: "AUTH_NETWORK_ERROR",
|
|
37
|
+
message: "Network error. Please check your connection and try again."
|
|
38
|
+
},
|
|
39
|
+
"auth/too-many-requests": {
|
|
40
|
+
code: "AUTH_TOO_MANY_REQUESTS",
|
|
41
|
+
message: "Too many attempts. Please wait a moment and try again."
|
|
42
|
+
},
|
|
43
|
+
"auth/popup-closed-by-user": {
|
|
44
|
+
code: "AUTH_POPUP_CLOSED",
|
|
45
|
+
message: "Sign-in popup was closed."
|
|
46
|
+
},
|
|
47
|
+
"auth/popup-blocked": {
|
|
48
|
+
code: "AUTH_POPUP_BLOCKED",
|
|
49
|
+
message: "Sign-in popup was blocked by the browser."
|
|
50
|
+
},
|
|
51
|
+
"auth/cancelled-popup-request": {
|
|
52
|
+
code: "AUTH_CANCELLED",
|
|
53
|
+
message: "Sign-in was cancelled."
|
|
54
|
+
},
|
|
55
|
+
"auth/user-disabled": {
|
|
56
|
+
code: "AUTH_USER_DISABLED",
|
|
57
|
+
message: "This account has been disabled."
|
|
58
|
+
},
|
|
59
|
+
"auth/user-not-found": {
|
|
60
|
+
code: "AUTH_USER_NOT_FOUND",
|
|
61
|
+
message: "No account found with that email."
|
|
62
|
+
},
|
|
63
|
+
"auth/wrong-password": {
|
|
64
|
+
code: "AUTH_WRONG_PASSWORD",
|
|
65
|
+
message: "Incorrect password."
|
|
66
|
+
},
|
|
67
|
+
"auth/invalid-email": {
|
|
68
|
+
code: "AUTH_INVALID_EMAIL",
|
|
69
|
+
message: "Invalid email address."
|
|
70
|
+
},
|
|
71
|
+
"auth/email-already-in-use": {
|
|
72
|
+
code: "AUTH_EMAIL_ALREADY_IN_USE",
|
|
73
|
+
message: "That email is already in use."
|
|
74
|
+
},
|
|
75
|
+
"auth/weak-password": {
|
|
76
|
+
code: "AUTH_WEAK_PASSWORD",
|
|
77
|
+
message: "Password is too weak."
|
|
78
|
+
},
|
|
79
|
+
"auth/requires-recent-login": {
|
|
80
|
+
code: "AUTH_REQUIRES_RECENT_LOGIN",
|
|
81
|
+
message: "Please re-authenticate and try again."
|
|
82
|
+
},
|
|
83
|
+
"auth/invalid-credential": {
|
|
84
|
+
code: "AUTH_INVALID_CREDENTIAL",
|
|
85
|
+
message: "Invalid credentials. Please try again."
|
|
86
|
+
},
|
|
87
|
+
"auth/account-exists-with-different-credential": {
|
|
88
|
+
code: "AUTH_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL",
|
|
89
|
+
message: "An account already exists with a different sign-in method."
|
|
90
|
+
},
|
|
91
|
+
"auth/provider-already-linked": {
|
|
92
|
+
code: "AUTH_PROVIDER_ALREADY_LINKED",
|
|
93
|
+
message: "That provider is already linked to this account."
|
|
94
|
+
},
|
|
95
|
+
"auth/credential-already-in-use": {
|
|
96
|
+
code: "AUTH_CREDENTIAL_ALREADY_IN_USE",
|
|
97
|
+
message: "Those credentials are already associated with another account."
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function normalizeAuthError(err: unknown, details?: Record<string, unknown>): NormalizedAuthError {
|
|
102
|
+
const firebaseCode = pickFirebaseCode(err);
|
|
103
|
+
|
|
104
|
+
const mapped = firebaseCode ? MAP[firebaseCode] : undefined;
|
|
105
|
+
if (mapped) {
|
|
106
|
+
return {
|
|
107
|
+
code: mapped.code,
|
|
108
|
+
message: mapped.message,
|
|
109
|
+
firebaseCode,
|
|
110
|
+
details
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// best-effort message (don’t leak weird objects)
|
|
115
|
+
const msg =
|
|
116
|
+
typeof (err as any)?.message === "string"
|
|
117
|
+
? (err as any).message
|
|
118
|
+
: "Authentication error. Please try again.";
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
code: "AUTH_UNKNOWN",
|
|
122
|
+
message: msg,
|
|
123
|
+
firebaseCode,
|
|
124
|
+
details
|
|
125
|
+
};
|
|
126
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useAuthState";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { Auth, User } from "firebase/auth";
|
|
3
|
+
import { onAuthStateChanged } from "../auth";
|
|
4
|
+
|
|
5
|
+
export type AuthState = {
|
|
6
|
+
user: User | null;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error: unknown | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function useAuthState(auth: Auth): AuthState {
|
|
12
|
+
const [user, setUser] = useState<User | null>(() => auth.currentUser);
|
|
13
|
+
const [loading, setLoading] = useState<boolean>(true);
|
|
14
|
+
const [error, setError] = useState<unknown | null>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setLoading(true);
|
|
18
|
+
const unsub = onAuthStateChanged(
|
|
19
|
+
auth,
|
|
20
|
+
(u) => {
|
|
21
|
+
setUser(u);
|
|
22
|
+
setError(null);
|
|
23
|
+
setLoading(false);
|
|
24
|
+
},
|
|
25
|
+
(e) => {
|
|
26
|
+
setError(e);
|
|
27
|
+
setLoading(false);
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
return unsub;
|
|
31
|
+
}, [auth]);
|
|
32
|
+
|
|
33
|
+
return { user, loading, error };
|
|
34
|
+
}
|