@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.
Files changed (41) hide show
  1. package/README.md +5 -0
  2. package/package.json +9 -6
  3. package/scripts/postinstall.js +38 -0
  4. package/shells/almadar-shell/package.json +17 -3
  5. package/shells/almadar-shell/packages/client/package.json +7 -7
  6. package/shells/almadar-shell/packages/client/src/App.tsx +23 -2
  7. package/shells/almadar-shell/packages/client/src/config/firebase.ts +23 -2
  8. package/shells/almadar-shell/packages/client/src/config/mockAuth.ts +257 -0
  9. package/shells/almadar-shell/packages/client/src/features/auth/AuthContext.tsx +22 -9
  10. package/shells/almadar-shell/packages/client/src/features/auth/authService.ts +28 -7
  11. package/shells/almadar-shell/packages/client/src/features/auth/components/PersonaSwitcher.tsx +50 -0
  12. package/shells/almadar-shell/packages/client/src/features/auth/components/ProtectedRoute.tsx +9 -0
  13. package/shells/almadar-shell/packages/client/src/features/auth/index.ts +2 -1
  14. package/shells/almadar-shell/packages/client/src/features/auth/types.ts +18 -2
  15. package/shells/almadar-shell/packages/server/package.json +9 -7
  16. package/shells/almadar-shell/packages/server/pnpm-lock.yaml +4723 -0
  17. package/shells/almadar-shell/packages/server/src/app.ts +58 -3
  18. package/shells/almadar-shell/packages/server/src/hooks-providers.ts +11 -0
  19. package/shells/almadar-shell/packages/server/src/index.ts +41 -2
  20. package/shells/almadar-shell/packages/server/src/services/clients.ts +17 -0
  21. package/shells/almadar-shell/packages/server/src/sse.ts +20 -0
  22. package/shells/almadar-shell/packages/server/src/types/express.d.ts +2 -8
  23. package/shells/almadar-shell/packages/server/tsconfig.json +0 -1
  24. package/shells/almadar-shell/packages/shared/package.json +1 -1
  25. package/shells/almadar-shell/pnpm-lock.yaml +3963 -1119
  26. package/shells/almadar-shell-hono/package.json +12 -6
  27. package/shells/almadar-shell-hono/packages/client/package.json +6 -6
  28. package/shells/almadar-shell-hono/packages/client/src/App.tsx +1 -5
  29. package/shells/almadar-shell-hono/packages/client/src/config/firebase.ts +23 -2
  30. package/shells/almadar-shell-hono/packages/client/src/features/auth/AuthContext.tsx +11 -6
  31. package/shells/almadar-shell-hono/packages/client/src/features/auth/authService.ts +10 -7
  32. package/shells/almadar-shell-hono/packages/client/src/features/auth/components/ProtectedRoute.tsx +9 -0
  33. package/shells/almadar-shell-hono/packages/server/package.json +5 -5
  34. package/shells/almadar-shell-hono/packages/server/src/app.ts +39 -0
  35. package/shells/almadar-shell-hono/packages/server/src/hooks-providers.ts +11 -0
  36. package/shells/almadar-shell-hono/packages/server/src/index.ts +41 -2
  37. package/shells/almadar-shell-hono/packages/server/src/serve.ts +4 -2
  38. package/shells/almadar-shell-hono/packages/server/src/services/clients.ts +17 -0
  39. package/shells/almadar-shell-hono/packages/server/src/sse.ts +20 -0
  40. package/shells/almadar-shell-hono/packages/server/tsconfig.json +0 -1
  41. 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(auth, googleProvider);
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(auth, email, password);
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(auth, email, password);
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(auth);
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(auth, email, actionCodeSettings);
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(auth, email, emailLink);
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;
@@ -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
- import { User } from 'firebase/auth';
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: User | null;
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": "^7.14.3",
17
- "@almadar/evaluator": "^2.11.2",
18
- "@almadar/integrations": "^2.7.0",
19
- "@almadar/logger": "^1.3.0",
20
- "@almadar/runtime": "^6.5.1",
21
- "@almadar/server": "^2.11.0",
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.8.1",
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",