@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
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almadar/shell-hono",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"private": true,
|
|
3
|
+
"version": "0.1.88",
|
|
5
4
|
"description": "Minimal full-stack shell template for Almadar applications (Hono server)",
|
|
6
5
|
"workspaces": [
|
|
7
6
|
"packages/*"
|
|
8
7
|
],
|
|
9
8
|
"scripts": {
|
|
10
|
-
"dev": "concurrently -n client,server -c blue,green \"bun --filter @
|
|
11
|
-
"build": "
|
|
9
|
+
"dev": "concurrently -n client,server -c blue,green \"bun --filter @almadar/shell-hono-client dev\" \"bun --filter @almadar/shell-hono-server dev\"",
|
|
10
|
+
"build": "turbo run build",
|
|
12
11
|
"serve": "bun run packages/server/src/serve.ts",
|
|
13
12
|
"typecheck": "turbo run typecheck",
|
|
14
13
|
"lint": "turbo run lint",
|
|
@@ -19,7 +18,13 @@
|
|
|
19
18
|
"turbo": "^2.8.17"
|
|
20
19
|
},
|
|
21
20
|
"files": [
|
|
22
|
-
"
|
|
21
|
+
"packages",
|
|
22
|
+
"locales",
|
|
23
|
+
"pnpm-workspace.yaml",
|
|
24
|
+
"turbo.json",
|
|
25
|
+
"pnpm-lock.yaml",
|
|
26
|
+
".env.example",
|
|
27
|
+
".gitignore"
|
|
23
28
|
],
|
|
24
29
|
"repository": {
|
|
25
30
|
"type": "git",
|
|
@@ -27,5 +32,6 @@
|
|
|
27
32
|
},
|
|
28
33
|
"publishConfig": {
|
|
29
34
|
"access": "public"
|
|
30
|
-
}
|
|
35
|
+
},
|
|
36
|
+
"packageManager": "pnpm@10.30.3"
|
|
31
37
|
}
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
"test:watch": "vitest"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@almadar/ui": "
|
|
17
|
-
"@almadar/syntax": "
|
|
18
|
-
"@almadar/evaluator": "
|
|
19
|
-
"@almadar/patterns": "
|
|
20
|
-
"@almadar/core": "
|
|
16
|
+
"@almadar/ui": "^6.31.0",
|
|
17
|
+
"@almadar/syntax": "^1.17.0",
|
|
18
|
+
"@almadar/evaluator": "^2.47.0",
|
|
19
|
+
"@almadar/patterns": "^2.101.0",
|
|
20
|
+
"@almadar/core": "^10.99.0",
|
|
21
21
|
"firebase": "^11.4.0",
|
|
22
22
|
"@tanstack/react-query": "^5.67.3",
|
|
23
23
|
"react": "^19.0.0",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"postprocessing": "^6.39.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@almadar/eslint-plugin": "
|
|
45
|
+
"@almadar/eslint-plugin": "^2.18.0",
|
|
46
46
|
"@typescript-eslint/parser": "8.56.0",
|
|
47
47
|
"eslint": "10.0.0",
|
|
48
48
|
"@testing-library/react": "^16.1.0",
|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
15
15
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
16
16
|
import { ThemeProvider, UISlotProvider } from '@almadar/ui/context';
|
|
17
|
-
import { UISlotComponent
|
|
17
|
+
import { UISlotComponent } from '@almadar/ui/components';
|
|
18
18
|
import {
|
|
19
|
-
EntityStoreProvider,
|
|
20
19
|
EventBusProvider,
|
|
21
20
|
VerificationProvider,
|
|
22
21
|
} from '@almadar/ui/providers';
|
|
@@ -49,7 +48,6 @@ function App() {
|
|
|
49
48
|
<I18nProvider value={i18nValue}>
|
|
50
49
|
<QueryClientProvider client={queryClient}>
|
|
51
50
|
<ThemeProvider>
|
|
52
|
-
<EntityStoreProvider>
|
|
53
51
|
<EventBusProvider>
|
|
54
52
|
<VerificationProvider>
|
|
55
53
|
<UISlotProvider>
|
|
@@ -68,13 +66,11 @@ function App() {
|
|
|
68
66
|
{/* Portal slots rendered by compiled trait views via CompiledPortal */}
|
|
69
67
|
{/* Toast notifications (non-overlapping, always safe to render here) */}
|
|
70
68
|
<UISlotComponent slot="toast" portal />
|
|
71
|
-
<NotifyListener />
|
|
72
69
|
</BrowserRouter>
|
|
73
70
|
</NavigationProvider>
|
|
74
71
|
</UISlotProvider>
|
|
75
72
|
</VerificationProvider>
|
|
76
73
|
</EventBusProvider>
|
|
77
|
-
</EntityStoreProvider>
|
|
78
74
|
</ThemeProvider>
|
|
79
75
|
</QueryClientProvider>
|
|
80
76
|
</I18nProvider>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { initializeApp, getApps, getApp, FirebaseApp } from 'firebase/app';
|
|
2
2
|
import { getAuth, Auth } from 'firebase/auth';
|
|
3
3
|
|
|
4
|
-
let app: FirebaseApp;
|
|
5
|
-
let auth: Auth;
|
|
4
|
+
let app: FirebaseApp | undefined;
|
|
5
|
+
let auth: Auth | undefined;
|
|
6
6
|
|
|
7
7
|
export async function initializeFirebase(): Promise<void> {
|
|
8
8
|
if (getApps().length > 0) {
|
|
@@ -30,8 +30,29 @@ export async function initializeFirebase(): Promise<void> {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// No credentials anywhere — auth stays disabled instead of crashing
|
|
34
|
+
// the app boot with auth/invalid-api-key.
|
|
35
|
+
if (!config.apiKey) {
|
|
36
|
+
console.warn('Firebase not configured — auth disabled (set VITE_APP_FIREBASE_* env vars)');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
app = initializeApp(config);
|
|
34
41
|
auth = getAuth(app);
|
|
35
42
|
}
|
|
36
43
|
|
|
44
|
+
export function requireAuth(): Auth {
|
|
45
|
+
if (!auth) {
|
|
46
|
+
throw new Error('Firebase auth is not configured — set VITE_APP_FIREBASE_* env vars');
|
|
47
|
+
}
|
|
48
|
+
return auth;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// True once Firebase initialized with real credentials. When false the
|
|
52
|
+
// app runs in auth-disabled mode: route guards pass through and sign-in
|
|
53
|
+
// actions report the missing config instead of crashing the boot.
|
|
54
|
+
export function isAuthEnabled(): boolean {
|
|
55
|
+
return auth !== undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
export { auth, app };
|
|
@@ -27,6 +27,11 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|
|
27
27
|
let unsubscribe: (() => void) | undefined;
|
|
28
28
|
|
|
29
29
|
initializeFirebase().then(() => {
|
|
30
|
+
// Auth disabled (no Firebase config) — render without a listener.
|
|
31
|
+
if (!auth) {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
30
35
|
unsubscribe = auth.onAuthStateChanged((firebaseUser) => {
|
|
31
36
|
setUser(firebaseUser);
|
|
32
37
|
setLoading(false);
|
|
@@ -43,7 +48,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|
|
43
48
|
setLoading(true);
|
|
44
49
|
clearError();
|
|
45
50
|
await authService.signInWithGoogle();
|
|
46
|
-
} catch (err
|
|
51
|
+
} catch (err) {
|
|
47
52
|
setLoading(false);
|
|
48
53
|
const firebaseErr = err as { code?: string; message?: string };
|
|
49
54
|
const isCancel =
|
|
@@ -61,7 +66,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|
|
61
66
|
setLoading(true);
|
|
62
67
|
clearError();
|
|
63
68
|
await authService.signInWithEmail(email, password);
|
|
64
|
-
} catch (err
|
|
69
|
+
} catch (err) {
|
|
65
70
|
setLoading(false);
|
|
66
71
|
setError((err as { message?: string }).message ?? 'Sign-in failed');
|
|
67
72
|
}
|
|
@@ -72,7 +77,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|
|
72
77
|
setLoading(true);
|
|
73
78
|
clearError();
|
|
74
79
|
await authService.signUpWithEmail(email, password, displayName);
|
|
75
|
-
} catch (err
|
|
80
|
+
} catch (err) {
|
|
76
81
|
setLoading(false);
|
|
77
82
|
setError((err as { message?: string }).message ?? 'Sign-up failed');
|
|
78
83
|
}
|
|
@@ -88,7 +93,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|
|
88
93
|
};
|
|
89
94
|
await authService.sendSignInLinkToEmail(email, actionCodeSettings);
|
|
90
95
|
setLoading(false);
|
|
91
|
-
} catch (err
|
|
96
|
+
} catch (err) {
|
|
92
97
|
setLoading(false);
|
|
93
98
|
setError((err as { message?: string }).message ?? 'Failed to send sign-in link');
|
|
94
99
|
}
|
|
@@ -99,7 +104,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|
|
99
104
|
setLoading(true);
|
|
100
105
|
clearError();
|
|
101
106
|
await authService.signInWithEmailLink(email, emailLink);
|
|
102
|
-
} catch (err
|
|
107
|
+
} catch (err) {
|
|
103
108
|
setLoading(false);
|
|
104
109
|
setError((err as { message?: string }).message ?? 'Email link sign-in failed');
|
|
105
110
|
}
|
|
@@ -115,7 +120,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|
|
115
120
|
await authService.signOut();
|
|
116
121
|
setUser(null);
|
|
117
122
|
setLoading(false);
|
|
118
|
-
} catch (err
|
|
123
|
+
} catch (err) {
|
|
119
124
|
setLoading(false);
|
|
120
125
|
setError((err as { message?: string }).message ?? 'Sign-out failed');
|
|
121
126
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
signInWithEmailLink as firebaseSignInWithEmailLink,
|
|
11
11
|
ActionCodeSettings,
|
|
12
12
|
} from 'firebase/auth';
|
|
13
|
-
import { auth } from '../../config/firebase';
|
|
13
|
+
import { auth, requireAuth } from '../../config/firebase';
|
|
14
14
|
|
|
15
15
|
const googleProvider = new GoogleAuthProvider();
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ export const authService = {
|
|
|
18
18
|
// Google Sign In
|
|
19
19
|
signInWithGoogle: async () => {
|
|
20
20
|
try {
|
|
21
|
-
const result = await signInWithPopup(
|
|
21
|
+
const result = await signInWithPopup(requireAuth(), googleProvider);
|
|
22
22
|
return result.user;
|
|
23
23
|
} catch (error) {
|
|
24
24
|
throw error;
|
|
@@ -28,7 +28,7 @@ export const authService = {
|
|
|
28
28
|
// Email/Password Sign In
|
|
29
29
|
signInWithEmail: async (email: string, password: string) => {
|
|
30
30
|
try {
|
|
31
|
-
const result = await signInWithEmailAndPassword(
|
|
31
|
+
const result = await signInWithEmailAndPassword(requireAuth(), email, password);
|
|
32
32
|
return result.user;
|
|
33
33
|
} catch (error) {
|
|
34
34
|
throw error;
|
|
@@ -38,7 +38,7 @@ export const authService = {
|
|
|
38
38
|
// Email/Password Sign Up
|
|
39
39
|
signUpWithEmail: async (email: string, password: string, displayName?: string) => {
|
|
40
40
|
try {
|
|
41
|
-
const result = await createUserWithEmailAndPassword(
|
|
41
|
+
const result = await createUserWithEmailAndPassword(requireAuth(), email, password);
|
|
42
42
|
|
|
43
43
|
if (displayName) {
|
|
44
44
|
await updateProfile(result.user, { displayName });
|
|
@@ -53,7 +53,7 @@ export const authService = {
|
|
|
53
53
|
// Sign Out
|
|
54
54
|
signOut: async () => {
|
|
55
55
|
try {
|
|
56
|
-
await firebaseSignOut(
|
|
56
|
+
await firebaseSignOut(requireAuth());
|
|
57
57
|
} catch (error) {
|
|
58
58
|
throw error;
|
|
59
59
|
}
|
|
@@ -62,19 +62,22 @@ export const authService = {
|
|
|
62
62
|
// Email Link Authentication
|
|
63
63
|
sendSignInLinkToEmail: async (email: string, actionCodeSettings: ActionCodeSettings) => {
|
|
64
64
|
try {
|
|
65
|
-
await firebaseSendSignInLinkToEmail(
|
|
65
|
+
await firebaseSendSignInLinkToEmail(requireAuth(), email, actionCodeSettings);
|
|
66
66
|
} catch (error) {
|
|
67
67
|
throw error;
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
|
|
71
71
|
isSignInWithEmailLink: (emailLink: string): boolean => {
|
|
72
|
+
// Predicate evaluated during Login render — degrade, don't throw,
|
|
73
|
+
// when auth is unconfigured: it simply isn't an email-link sign-in.
|
|
74
|
+
if (!auth) return false;
|
|
72
75
|
return firebaseIsSignInWithEmailLink(auth, emailLink);
|
|
73
76
|
},
|
|
74
77
|
|
|
75
78
|
signInWithEmailLink: async (email: string, emailLink: string) => {
|
|
76
79
|
try {
|
|
77
|
-
const result = await firebaseSignInWithEmailLink(
|
|
80
|
+
const result = await firebaseSignInWithEmailLink(requireAuth(), email, emailLink);
|
|
78
81
|
return result.user;
|
|
79
82
|
} catch (error) {
|
|
80
83
|
throw error;
|
package/shells/almadar-shell-hono/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
|
}
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"test:watch": "vitest"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@almadar/server-hono": "
|
|
17
|
-
"@almadar/evaluator": "
|
|
18
|
-
"@almadar/core": "
|
|
19
|
-
"@almadar/integrations": "
|
|
16
|
+
"@almadar/server-hono": "^0.4.13",
|
|
17
|
+
"@almadar/evaluator": "^2.47.0",
|
|
18
|
+
"@almadar/core": "^10.99.0",
|
|
19
|
+
"@almadar/integrations": "^2.36.0",
|
|
20
20
|
"hono": "^4.7.0",
|
|
21
21
|
"@hono/node-server": "^1.14.0",
|
|
22
22
|
"@hono/zod-validator": "^0.4.0",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"zod": "^3.22.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@almadar/eslint-plugin": "
|
|
28
|
+
"@almadar/eslint-plugin": "^2.18.0",
|
|
29
29
|
"@typescript-eslint/parser": "8.56.0",
|
|
30
30
|
"@types/bun": "^1.1.0",
|
|
31
31
|
"@types/node": "^20.17.19",
|
|
@@ -8,8 +8,15 @@ import {
|
|
|
8
8
|
errorHandler,
|
|
9
9
|
notFoundHandler,
|
|
10
10
|
debugEventsRouter,
|
|
11
|
+
pushRouter,
|
|
12
|
+
pushServiceWorkerHandler,
|
|
13
|
+
PUSH_SERVICE_WORKER_PATH,
|
|
14
|
+
reportsRouter,
|
|
15
|
+
createHooksRouter,
|
|
11
16
|
type AppEnv,
|
|
12
17
|
} from '@almadar/server-hono';
|
|
18
|
+
import { hookProviders } from './hooks-providers.js';
|
|
19
|
+
import { broadcastBusEvent } from './sse.js';
|
|
13
20
|
import { registerRoutes } from './routes.js';
|
|
14
21
|
|
|
15
22
|
export const app = new Hono<AppEnv>();
|
|
@@ -23,9 +30,41 @@ app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
|
23
30
|
// Debug event bus endpoints (dev-only, no-op in production)
|
|
24
31
|
app.route('/api/debug', debugEventsRouter());
|
|
25
32
|
|
|
33
|
+
// Web Push surface (browser/push-subscribe): public VAPID key + the shared
|
|
34
|
+
// service worker (root-scoped — a SW's max scope is its own directory).
|
|
35
|
+
app.route('/api/push', pushRouter);
|
|
36
|
+
app.get(PUSH_SERVICE_WORKER_PATH, pushServiceWorkerHandler);
|
|
37
|
+
|
|
38
|
+
// Inbound webhook ingress (I-19 decision): mounted BEFORE the authenticated
|
|
39
|
+
// /api routes — hook senders (Google Calendar watch channels, e-sign status,
|
|
40
|
+
// banking callbacks) cannot present a Firebase token, so each provider
|
|
41
|
+
// VERIFIES its own signature/channel token and unverified requests get 400.
|
|
42
|
+
// Dispatch = SSE bus broadcast to every connected client, so client circuits'
|
|
43
|
+
// `listens` fire; the cron pull cycle stays the no-client backstop.
|
|
44
|
+
app.route(
|
|
45
|
+
'/api/hooks',
|
|
46
|
+
createHooksRouter({
|
|
47
|
+
// Derived from this app's invoked services (generated hooks-providers.ts, I-26).
|
|
48
|
+
providers: hookProviders(),
|
|
49
|
+
dispatch: (event, payload) => {
|
|
50
|
+
broadcastBusEvent(undefined, {
|
|
51
|
+
type: 'bus',
|
|
52
|
+
event,
|
|
53
|
+
payload,
|
|
54
|
+
source: { orbital: 'webhook', trait: 'ingress' },
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
26
61
|
// Register generated routes
|
|
27
62
|
registerRoutes(app);
|
|
28
63
|
|
|
64
|
+
// Server-side report export (Excel/PDF/CSV). Mounted AFTER registerRoutes so
|
|
65
|
+
// the /api/* auth middleware it registers covers this route too.
|
|
66
|
+
app.route('/api/reports', reportsRouter);
|
|
67
|
+
|
|
29
68
|
// Error handling
|
|
30
69
|
app.notFound(notFoundHandler);
|
|
31
70
|
app.onError(errorHandler);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Providers
|
|
3
|
+
*
|
|
4
|
+
* Checked-in stub so the template builds standalone in CI; the compiler
|
|
5
|
+
* overwrites it with the provider map derived from the app's invoked
|
|
6
|
+
* services × the registry's hook declarations (I-26).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function hookProviders() {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
@@ -4,15 +4,54 @@
|
|
|
4
4
|
|
|
5
5
|
import { initializeFirebase, env, logger } from '@almadar/server-hono';
|
|
6
6
|
|
|
7
|
-
// Initialize Firebase before anything else uses it
|
|
8
|
-
|
|
7
|
+
// Initialize Firebase before anything else uses it. When no credentials
|
|
8
|
+
// are configured (local dev without FIREBASE_*/FIRESTORE_EMULATOR_HOST),
|
|
9
|
+
// boot anyway — a purely client-side app must still serve.
|
|
10
|
+
try {
|
|
11
|
+
initializeFirebase();
|
|
12
|
+
} catch (e) {
|
|
13
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
14
|
+
// In production the real data source IS Firebase (env.ts refuses to boot with
|
|
15
|
+
// USE_MOCK_DATA=true there), so swallowing this leaves a server that starts
|
|
16
|
+
// cleanly and then throws on the first request. Fail at boot instead.
|
|
17
|
+
if (env.NODE_ENV === 'production') {
|
|
18
|
+
logger.error(`Firebase is required when NODE_ENV=production — refusing to start: ${message}`);
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
logger.warn(`Firebase not configured — auth/db routes disabled: ${message}`);
|
|
22
|
+
}
|
|
9
23
|
|
|
10
24
|
import { serve } from '@hono/node-server';
|
|
25
|
+
import { validateIntegrationEnv, FirestoreCredentialPersistence } from '@almadar/server-hono';
|
|
11
26
|
import { app } from './app.js';
|
|
27
|
+
import { invokedServices, installTenantCredentialStore } from './services/clients.js';
|
|
28
|
+
|
|
29
|
+
// Fail fast in production when a required integration credential is missing
|
|
30
|
+
// (see SECRETS.md); registers the invoked services for /health reporting.
|
|
31
|
+
validateIntegrationEnv(invokedServices);
|
|
12
32
|
|
|
13
33
|
const PORT = env.PORT || 3030;
|
|
14
34
|
|
|
15
35
|
async function start(): Promise<void> {
|
|
36
|
+
// W4 tenant credential store: with a master key configured, stored
|
|
37
|
+
// credentials (Firestore rows, AES-256-GCM) layer over the env —
|
|
38
|
+
// store → env → unconfigured — and go live on change without restart.
|
|
39
|
+
if (process.env.ALMADAR_CREDENTIAL_MASTER_KEY) {
|
|
40
|
+
try {
|
|
41
|
+
await installTenantCredentialStore(new FirestoreCredentialPersistence());
|
|
42
|
+
logger.info('Tenant credential store installed');
|
|
43
|
+
} catch (e) {
|
|
44
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
45
|
+
// A configured master key IS the deployment's statement that the store
|
|
46
|
+
// is in use — failing to serve it in production is a boot failure.
|
|
47
|
+
if (env.NODE_ENV === 'production') {
|
|
48
|
+
logger.error(`Tenant credential store failed to initialize — refusing to start: ${message}`);
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
logger.warn(`Tenant credential store unavailable — falling back to env credentials: ${message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
16
55
|
// Seed mock data when USE_MOCK_DATA is enabled
|
|
17
56
|
if (env.USE_MOCK_DATA) {
|
|
18
57
|
try {
|
|
@@ -15,8 +15,10 @@ import { join } from 'path';
|
|
|
15
15
|
const PORT = Number(env.PORT) || 3030;
|
|
16
16
|
const clientDist = join(import.meta.dir, '../../client/dist');
|
|
17
17
|
|
|
18
|
-
// Seed mock data
|
|
19
|
-
|
|
18
|
+
// Seed mock data. Gated on USE_MOCK_DATA alone, matching the three other server
|
|
19
|
+
// entry points — "not production" is a different question from "is the data
|
|
20
|
+
// source mock", and conflating them seeds a store nothing reads.
|
|
21
|
+
if (env.USE_MOCK_DATA) {
|
|
20
22
|
try {
|
|
21
23
|
const { initializeMockData } = await import(/* @vite-ignore */ './seedMockData.js' as string);
|
|
22
24
|
await initializeMockData();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Clients — placeholder
|
|
3
|
+
*
|
|
4
|
+
* Compiler generates the real service clients here (delegating to
|
|
5
|
+
* @almadar/integrations; see orbital-shell-typescript codegen/service.rs).
|
|
6
|
+
* This stub only lets the template build standalone; an emitted app
|
|
7
|
+
* overwrites it wholesale.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const declaredServices = [] as const;
|
|
11
|
+
|
|
12
|
+
export const invokedServices = [] as const;
|
|
13
|
+
|
|
14
|
+
/** Overwritten at emit time with the RuntimeIntegrationManager-backed store install. */
|
|
15
|
+
export async function installTenantCredentialStore(_adapter: object): Promise<void> {
|
|
16
|
+
// {{GENERATED_SERVICE_CLIENTS}}
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-push SSE channel — placeholder
|
|
3
|
+
*
|
|
4
|
+
* Compiler generates the real SSE module here (connection registry +
|
|
5
|
+
* cross-client broadcast; see orbital-shell-typescript backend/server/sse.rs).
|
|
6
|
+
* This stub only lets the template build standalone; an emitted app
|
|
7
|
+
* overwrites it wholesale.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface BusBroadcastItem {
|
|
11
|
+
type: 'bus';
|
|
12
|
+
event: string;
|
|
13
|
+
payload?: object;
|
|
14
|
+
source: { orbital: string; trait: string };
|
|
15
|
+
timestamp: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function broadcastBusEvent(_originClientId: string | undefined, _item: BusBroadcastItem): void {
|
|
19
|
+
// {{GENERATED_SSE_BROADCAST}}
|
|
20
|
+
}
|