@almadar/orb 16.9.1 → 17.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 +5 -0
- package/package.json +9 -6
- package/scripts/postinstall.js +38 -0
- package/shells/almadar-shell/package.json +17 -3
- package/shells/almadar-shell/packages/client/package.json +7 -7
- package/shells/almadar-shell/packages/client/src/App.tsx +23 -2
- package/shells/almadar-shell/packages/client/src/config/firebase.ts +23 -2
- package/shells/almadar-shell/packages/client/src/config/mockAuth.ts +257 -0
- package/shells/almadar-shell/packages/client/src/features/auth/AuthContext.tsx +22 -9
- package/shells/almadar-shell/packages/client/src/features/auth/authService.ts +28 -7
- package/shells/almadar-shell/packages/client/src/features/auth/components/PersonaSwitcher.tsx +50 -0
- package/shells/almadar-shell/packages/client/src/features/auth/components/ProtectedRoute.tsx +9 -0
- package/shells/almadar-shell/packages/client/src/features/auth/index.ts +2 -1
- package/shells/almadar-shell/packages/client/src/features/auth/types.ts +18 -2
- package/shells/almadar-shell/packages/server/package.json +9 -7
- package/shells/almadar-shell/packages/server/pnpm-lock.yaml +4723 -0
- package/shells/almadar-shell/packages/server/src/app.ts +58 -3
- package/shells/almadar-shell/packages/server/src/hooks-providers.ts +11 -0
- package/shells/almadar-shell/packages/server/src/index.ts +41 -2
- package/shells/almadar-shell/packages/server/src/services/clients.ts +17 -0
- package/shells/almadar-shell/packages/server/src/sse.ts +20 -0
- package/shells/almadar-shell/packages/server/src/types/express.d.ts +2 -8
- package/shells/almadar-shell/packages/server/tsconfig.json +0 -1
- package/shells/almadar-shell/packages/shared/package.json +1 -1
- package/shells/almadar-shell/pnpm-lock.yaml +3963 -1119
- package/shells/almadar-shell-hono/package.json +12 -6
- package/shells/almadar-shell-hono/packages/client/package.json +6 -6
- package/shells/almadar-shell-hono/packages/client/src/App.tsx +1 -5
- package/shells/almadar-shell-hono/packages/client/src/config/firebase.ts +23 -2
- package/shells/almadar-shell-hono/packages/client/src/features/auth/AuthContext.tsx +11 -6
- package/shells/almadar-shell-hono/packages/client/src/features/auth/authService.ts +10 -7
- package/shells/almadar-shell-hono/packages/client/src/features/auth/components/ProtectedRoute.tsx +9 -0
- package/shells/almadar-shell-hono/packages/server/package.json +5 -5
- package/shells/almadar-shell-hono/packages/server/src/app.ts +39 -0
- package/shells/almadar-shell-hono/packages/server/src/hooks-providers.ts +11 -0
- package/shells/almadar-shell-hono/packages/server/src/index.ts +41 -2
- package/shells/almadar-shell-hono/packages/server/src/serve.ts +4 -2
- package/shells/almadar-shell-hono/packages/server/src/services/clients.ts +17 -0
- package/shells/almadar-shell-hono/packages/server/src/sse.ts +20 -0
- package/shells/almadar-shell-hono/packages/server/tsconfig.json +0 -1
- package/shells/almadar-shell-hono/pnpm-lock.yaml +4370 -1238
|
@@ -10,15 +10,24 @@ import {
|
|
|
10
10
|
signInWithEmailLink as firebaseSignInWithEmailLink,
|
|
11
11
|
ActionCodeSettings,
|
|
12
12
|
} from 'firebase/auth';
|
|
13
|
-
import { auth } from '../../config/firebase';
|
|
13
|
+
import { auth, isAuthEnabled, requireAuth } from '../../config/firebase';
|
|
14
|
+
import { mockAuth } from '../../config/mockAuth';
|
|
15
|
+
|
|
16
|
+
// With no Firebase credentials the real calls throw "not configured", which
|
|
17
|
+
// leaves a generated app impossible to sign into — and therefore impossible to
|
|
18
|
+
// view as anyone, since @user.id/@user.role drive ownership and role gates.
|
|
19
|
+
// Route to the persona-backed mock in exactly that case.
|
|
20
|
+
const useMock = (): boolean => !isAuthEnabled();
|
|
14
21
|
|
|
15
22
|
const googleProvider = new GoogleAuthProvider();
|
|
16
23
|
|
|
17
24
|
export const authService = {
|
|
18
25
|
// Google Sign In
|
|
19
26
|
signInWithGoogle: async () => {
|
|
27
|
+
// No popup to show without Firebase — sign in as the seeded end-user.
|
|
28
|
+
if (useMock()) return mockAuth.signInAsPersona('member');
|
|
20
29
|
try {
|
|
21
|
-
const result = await signInWithPopup(
|
|
30
|
+
const result = await signInWithPopup(requireAuth(), googleProvider);
|
|
22
31
|
return result.user;
|
|
23
32
|
} catch (error) {
|
|
24
33
|
throw error;
|
|
@@ -27,8 +36,9 @@ export const authService = {
|
|
|
27
36
|
|
|
28
37
|
// Email/Password Sign In
|
|
29
38
|
signInWithEmail: async (email: string, password: string) => {
|
|
39
|
+
if (useMock()) return mockAuth.signInWithEmail(email, password);
|
|
30
40
|
try {
|
|
31
|
-
const result = await signInWithEmailAndPassword(
|
|
41
|
+
const result = await signInWithEmailAndPassword(requireAuth(), email, password);
|
|
32
42
|
return result.user;
|
|
33
43
|
} catch (error) {
|
|
34
44
|
throw error;
|
|
@@ -37,8 +47,9 @@ export const authService = {
|
|
|
37
47
|
|
|
38
48
|
// Email/Password Sign Up
|
|
39
49
|
signUpWithEmail: async (email: string, password: string, displayName?: string) => {
|
|
50
|
+
if (useMock()) return mockAuth.signUpWithEmail(email, password, displayName);
|
|
40
51
|
try {
|
|
41
|
-
const result = await createUserWithEmailAndPassword(
|
|
52
|
+
const result = await createUserWithEmailAndPassword(requireAuth(), email, password);
|
|
42
53
|
|
|
43
54
|
if (displayName) {
|
|
44
55
|
await updateProfile(result.user, { displayName });
|
|
@@ -52,8 +63,9 @@ export const authService = {
|
|
|
52
63
|
|
|
53
64
|
// Sign Out
|
|
54
65
|
signOut: async () => {
|
|
66
|
+
if (useMock()) return mockAuth.signOut();
|
|
55
67
|
try {
|
|
56
|
-
await firebaseSignOut(
|
|
68
|
+
await firebaseSignOut(requireAuth());
|
|
57
69
|
} catch (error) {
|
|
58
70
|
throw error;
|
|
59
71
|
}
|
|
@@ -62,22 +74,31 @@ export const authService = {
|
|
|
62
74
|
// Email Link Authentication
|
|
63
75
|
sendSignInLinkToEmail: async (email: string, actionCodeSettings: ActionCodeSettings) => {
|
|
64
76
|
try {
|
|
65
|
-
await firebaseSendSignInLinkToEmail(
|
|
77
|
+
await firebaseSendSignInLinkToEmail(requireAuth(), email, actionCodeSettings);
|
|
66
78
|
} catch (error) {
|
|
67
79
|
throw error;
|
|
68
80
|
}
|
|
69
81
|
},
|
|
70
82
|
|
|
71
83
|
isSignInWithEmailLink: (emailLink: string): boolean => {
|
|
84
|
+
// Predicate evaluated during Login render — degrade, don't throw,
|
|
85
|
+
// when auth is unconfigured: it simply isn't an email-link sign-in.
|
|
86
|
+
if (!auth) return false;
|
|
72
87
|
return firebaseIsSignInWithEmailLink(auth, emailLink);
|
|
73
88
|
},
|
|
74
89
|
|
|
75
90
|
signInWithEmailLink: async (email: string, emailLink: string) => {
|
|
76
91
|
try {
|
|
77
|
-
const result = await firebaseSignInWithEmailLink(
|
|
92
|
+
const result = await firebaseSignInWithEmailLink(requireAuth(), email, emailLink);
|
|
78
93
|
return result.user;
|
|
79
94
|
} catch (error) {
|
|
80
95
|
throw error;
|
|
81
96
|
}
|
|
82
97
|
},
|
|
98
|
+
|
|
99
|
+
/** Sign in as a named seeded persona (dev only; no-op with real Firebase). */
|
|
100
|
+
signInAsPersona: async (idOrRole: string) => {
|
|
101
|
+
if (useMock()) return mockAuth.signInAsPersona(idOrRole);
|
|
102
|
+
throw new Error('Persona sign-in is a dev-only affordance; real auth is configured');
|
|
103
|
+
},
|
|
83
104
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev persona switch — view the app as each identity its domain implies.
|
|
3
|
+
*
|
|
4
|
+
* Renders only when auth is mocked (no Firebase credentials), so it cannot appear
|
|
5
|
+
* in a real deployment. Role gates and ownership-scoped lists are the parts of an
|
|
6
|
+
* app that look identical whether they work or are simply empty; switching viewer
|
|
7
|
+
* in one click is how you tell the difference.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { HStack, Select, Typography } from '@almadar/ui';
|
|
12
|
+
import { isAuthEnabled } from '../../../config/firebase';
|
|
13
|
+
import { listMockAccounts, onMockRosterChanged } from '../../../config/mockAuth';
|
|
14
|
+
import { authService } from '../authService';
|
|
15
|
+
import { useAuthContext } from '../AuthContext';
|
|
16
|
+
|
|
17
|
+
export function PersonaSwitcher(): React.ReactElement | null {
|
|
18
|
+
const { user } = useAuthContext();
|
|
19
|
+
// The roster arrives over HTTP after first paint. Without this the picker
|
|
20
|
+
// renders its empty pre-fetch state and never updates, because the signed-out
|
|
21
|
+
// viewer never changes and so the auth listener never fires.
|
|
22
|
+
const [accounts, setAccounts] = React.useState(() => listMockAccounts());
|
|
23
|
+
|
|
24
|
+
React.useEffect(() => onMockRosterChanged(() => setAccounts(listMockAccounts())), []);
|
|
25
|
+
|
|
26
|
+
if (isAuthEnabled()) return null;
|
|
27
|
+
|
|
28
|
+
const options = accounts.map((p) => ({
|
|
29
|
+
value: p.id,
|
|
30
|
+
label: `${String(p.name ?? p.id)} — ${String(p.role ?? 'no role')}`,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<HStack gap="sm" align="center">
|
|
35
|
+
<Typography variant="caption" color="muted">
|
|
36
|
+
Viewing as
|
|
37
|
+
</Typography>
|
|
38
|
+
<Select
|
|
39
|
+
options={options}
|
|
40
|
+
value={user?.uid ?? ''}
|
|
41
|
+
placeholder="Signed out"
|
|
42
|
+
onChange={(event) => {
|
|
43
|
+
void authService.signInAsPersona(event.target.value);
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
</HStack>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default PersonaSwitcher;
|
package/shells/almadar-shell/packages/client/src/features/auth/components/ProtectedRoute.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Navigate } from 'react-router-dom';
|
|
3
3
|
import { useAuthContext } from '../AuthContext';
|
|
4
|
+
import { isAuthEnabled } from '../../../config/firebase';
|
|
4
5
|
|
|
5
6
|
interface ProtectedRouteProps {
|
|
6
7
|
children: React.ReactNode;
|
|
@@ -17,6 +18,14 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
|
|
17
18
|
);
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// Auth-disabled mode (no Firebase config): nothing can sign in, so
|
|
22
|
+
// gating would strand every route on /login — pass straight through.
|
|
23
|
+
// Checked after `loading` so a configured-but-initializing Firebase
|
|
24
|
+
// still shows the spinner instead of flashing protected content.
|
|
25
|
+
if (!isAuthEnabled()) {
|
|
26
|
+
return <>{children}</>;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
if (!user) {
|
|
21
30
|
return <Navigate to="/login" replace />;
|
|
22
31
|
}
|
|
@@ -5,9 +5,10 @@ export { AuthProvider, useAuthContext } from './AuthContext';
|
|
|
5
5
|
export { default as Login } from './components/Login';
|
|
6
6
|
export { default as UserProfile } from './components/UserProfile';
|
|
7
7
|
export { default as ProtectedRoute } from './components/ProtectedRoute';
|
|
8
|
+
export { PersonaSwitcher } from './components/PersonaSwitcher';
|
|
8
9
|
|
|
9
10
|
// Service
|
|
10
11
|
export { authService } from './authService';
|
|
11
12
|
|
|
12
13
|
// Types
|
|
13
|
-
export type { AuthContextType, LoginCredentials, SignUpCredentials } from './types';
|
|
14
|
+
export type { AuthContextType, AuthViewer, LoginCredentials, SignUpCredentials } from './types';
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The signed-in viewer, as the auth UI reads it. Firebase's `User` and the mocked
|
|
3
|
+
* `MockUser` are both structurally assignable to this, so the provider carries
|
|
4
|
+
* either without a cast and without the UI knowing which one is live.
|
|
5
|
+
*
|
|
6
|
+
* A type alias, not an interface: only aliases get TS's implicit index signature,
|
|
7
|
+
* which is what lets this be passed to `normalizeUserContext(claims: RawUserClaims)`
|
|
8
|
+
* without a cast.
|
|
9
|
+
*/
|
|
10
|
+
export type AuthViewer = {
|
|
11
|
+
uid: string;
|
|
12
|
+
email: string | null;
|
|
13
|
+
displayName: string | null;
|
|
14
|
+
photoURL: string | null;
|
|
15
|
+
/** Present only for mocked personas; real Firebase claims carry no role. */
|
|
16
|
+
role?: string;
|
|
17
|
+
};
|
|
2
18
|
|
|
3
19
|
export interface LoginCredentials {
|
|
4
20
|
email: string;
|
|
@@ -10,7 +26,7 @@ export interface SignUpCredentials extends LoginCredentials {
|
|
|
10
26
|
}
|
|
11
27
|
|
|
12
28
|
export interface AuthContextType {
|
|
13
|
-
user:
|
|
29
|
+
user: AuthViewer | null;
|
|
14
30
|
loading: boolean;
|
|
15
31
|
error: string | null;
|
|
16
32
|
signInWithGoogle: () => Promise<void>;
|
|
@@ -13,20 +13,22 @@
|
|
|
13
13
|
"test:watch": "vitest"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@almadar/core": "^
|
|
17
|
-
"@almadar/evaluator": "^2.
|
|
18
|
-
"@almadar/integrations": "^2.
|
|
19
|
-
"@almadar/logger": "^1.
|
|
20
|
-
"@almadar/runtime": "^6.
|
|
21
|
-
"@almadar/server": "^2.
|
|
16
|
+
"@almadar/core": "^10.99.0",
|
|
17
|
+
"@almadar/evaluator": "^2.47.0",
|
|
18
|
+
"@almadar/integrations": "^2.36.0",
|
|
19
|
+
"@almadar/logger": "^1.12.0",
|
|
20
|
+
"@almadar/runtime": "^6.84.0",
|
|
21
|
+
"@almadar/server": "^2.43.0",
|
|
22
22
|
"cors": "^2.8.5",
|
|
23
23
|
"dotenv": "^16.4.7",
|
|
24
24
|
"express": "^4.21.2",
|
|
25
|
+
"express-rate-limit": "^7.4.0",
|
|
25
26
|
"firebase-admin": "^12.7.0",
|
|
27
|
+
"helmet": "^8.0.0",
|
|
26
28
|
"zod": "^3.22.0"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"@almadar/eslint-plugin": "^2.
|
|
31
|
+
"@almadar/eslint-plugin": "^2.18.0",
|
|
30
32
|
"@types/cors": "^2.8.17",
|
|
31
33
|
"@types/express": "^5.0.0",
|
|
32
34
|
"@types/node": "^20.17.19",
|