@almadar/server 1.0.17 → 1.2.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.
@@ -0,0 +1,37 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+
3
+ /**
4
+ * Multi-User Middleware
5
+ *
6
+ * Provides user isolation and session ownership using Firebase Auth.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ declare global {
12
+ namespace Express {
13
+ interface Request {
14
+ user?: {
15
+ uid: string;
16
+ email?: string;
17
+ roles?: string[];
18
+ orgId?: string;
19
+ };
20
+ userContext?: {
21
+ userId: string;
22
+ orgId?: string;
23
+ roles?: string[];
24
+ };
25
+ }
26
+ }
27
+ }
28
+ /**
29
+ * Middleware to set up user context from Firebase Auth
30
+ */
31
+ declare function multiUserMiddleware(req: Request, res: Response, next: NextFunction): Promise<void>;
32
+ /**
33
+ * Verify Firebase Auth token from Authorization header
34
+ */
35
+ declare function verifyFirebaseAuth(req: Request, res: Response, next: NextFunction): Promise<void>;
36
+
37
+ export { multiUserMiddleware, verifyFirebaseAuth };
@@ -0,0 +1,150 @@
1
+ import { createUserContext, getMultiUserManager } from '@almadar/agent';
2
+ import admin from 'firebase-admin';
3
+ import { z } from 'zod';
4
+ import dotenv from 'dotenv';
5
+
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+ dotenv.config();
13
+ var envSchema = z.object({
14
+ NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
15
+ PORT: z.string().default("3030").transform((val) => parseInt(val, 10)),
16
+ CORS_ORIGIN: z.string().default("http://localhost:5173").transform((val) => val.includes(",") ? val.split(",").map((s) => s.trim()) : val),
17
+ // Database (Prisma/SQL) - optional
18
+ DATABASE_URL: z.string().optional(),
19
+ // Firebase/Firestore configuration
20
+ FIREBASE_PROJECT_ID: z.string().optional(),
21
+ FIREBASE_CLIENT_EMAIL: z.string().optional(),
22
+ FIREBASE_PRIVATE_KEY: z.string().optional(),
23
+ FIREBASE_SERVICE_ACCOUNT_PATH: z.string().optional(),
24
+ FIRESTORE_EMULATOR_HOST: z.string().optional(),
25
+ FIREBASE_AUTH_EMULATOR_HOST: z.string().optional(),
26
+ // API configuration
27
+ API_PREFIX: z.string().default("/api"),
28
+ // Mock data configuration
29
+ USE_MOCK_DATA: z.string().default("true").transform((v) => v === "true"),
30
+ MOCK_SEED: z.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
31
+ });
32
+ var parsed = envSchema.safeParse(process.env);
33
+ if (!parsed.success) {
34
+ console.error("\u274C Invalid environment variables:", parsed.error.flatten().fieldErrors);
35
+ throw new Error("Invalid environment variables");
36
+ }
37
+ var env = parsed.data;
38
+
39
+ // src/lib/db.ts
40
+ var firebaseApp = null;
41
+ function initializeFirebase() {
42
+ if (firebaseApp) {
43
+ return firebaseApp;
44
+ }
45
+ if (admin.apps.length > 0) {
46
+ firebaseApp = admin.apps[0];
47
+ return firebaseApp;
48
+ }
49
+ if (env.FIRESTORE_EMULATOR_HOST) {
50
+ firebaseApp = admin.initializeApp({
51
+ projectId: env.FIREBASE_PROJECT_ID || "demo-project"
52
+ });
53
+ console.log(`\u{1F527} Firebase Admin initialized for emulator: ${env.FIRESTORE_EMULATOR_HOST}`);
54
+ return firebaseApp;
55
+ }
56
+ const serviceAccountPath = env.FIREBASE_SERVICE_ACCOUNT_PATH;
57
+ if (serviceAccountPath) {
58
+ const serviceAccount = __require(serviceAccountPath);
59
+ firebaseApp = admin.initializeApp({
60
+ credential: admin.credential.cert(serviceAccount),
61
+ projectId: env.FIREBASE_PROJECT_ID
62
+ });
63
+ } else if (env.FIREBASE_PROJECT_ID && env.FIREBASE_CLIENT_EMAIL && env.FIREBASE_PRIVATE_KEY) {
64
+ firebaseApp = admin.initializeApp({
65
+ credential: admin.credential.cert({
66
+ projectId: env.FIREBASE_PROJECT_ID,
67
+ clientEmail: env.FIREBASE_CLIENT_EMAIL,
68
+ privateKey: env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n")
69
+ }),
70
+ projectId: env.FIREBASE_PROJECT_ID
71
+ });
72
+ } else if (env.FIREBASE_PROJECT_ID) {
73
+ firebaseApp = admin.initializeApp({
74
+ credential: admin.credential.applicationDefault(),
75
+ projectId: env.FIREBASE_PROJECT_ID
76
+ });
77
+ } else {
78
+ firebaseApp = admin.initializeApp({
79
+ projectId: "demo-project"
80
+ });
81
+ }
82
+ return firebaseApp;
83
+ }
84
+ function getFirestore() {
85
+ const app = initializeFirebase();
86
+ const db2 = app.firestore();
87
+ if (env.FIRESTORE_EMULATOR_HOST) {
88
+ db2.settings({
89
+ host: env.FIRESTORE_EMULATOR_HOST,
90
+ ssl: false
91
+ });
92
+ }
93
+ return db2;
94
+ }
95
+ function getAuth() {
96
+ const app = initializeFirebase();
97
+ return app.auth();
98
+ }
99
+ getFirestore();
100
+
101
+ // src/middleware/multi-user.ts
102
+ async function multiUserMiddleware(req, res, next) {
103
+ const userId = req.user?.uid;
104
+ if (!userId) {
105
+ res.status(401).json({ error: "Authentication required" });
106
+ return;
107
+ }
108
+ req.userContext = createUserContext(userId, {
109
+ orgId: req.user?.orgId,
110
+ roles: req.user?.roles ?? ["user"]
111
+ });
112
+ const originalJson = res.json.bind(res);
113
+ res.json = (body) => {
114
+ if (body && typeof body === "object" && "threadId" in body) {
115
+ const multiUser = getMultiUserManager();
116
+ multiUser.assignSessionOwnership(
117
+ body.threadId,
118
+ userId
119
+ );
120
+ }
121
+ return originalJson(body);
122
+ };
123
+ next();
124
+ }
125
+ async function verifyFirebaseAuth(req, res, next) {
126
+ const authHeader = req.headers.authorization;
127
+ if (!authHeader?.startsWith("Bearer ")) {
128
+ res.status(401).json({ error: "No token provided" });
129
+ return;
130
+ }
131
+ const token = authHeader.split("Bearer ")[1];
132
+ try {
133
+ const decodedToken = await getAuth().verifyIdToken(token);
134
+ const user = await getAuth().getUser(decodedToken.uid);
135
+ req.user = {
136
+ uid: decodedToken.uid,
137
+ email: decodedToken.email,
138
+ roles: user.customClaims?.roles ?? ["user"],
139
+ orgId: user.customClaims?.orgId
140
+ };
141
+ next();
142
+ } catch (error) {
143
+ console.error("Token verification failed:", error);
144
+ res.status(401).json({ error: "Invalid token" });
145
+ }
146
+ }
147
+
148
+ export { multiUserMiddleware, verifyFirebaseAuth };
149
+ //# sourceMappingURL=multi-user.js.map
150
+ //# sourceMappingURL=multi-user.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/env.ts","../../src/lib/db.ts","../../src/middleware/multi-user.ts"],"names":["db"],"mappings":"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","file":"multi-user.js","sourcesContent":["import { z } from 'zod';\nimport dotenv from 'dotenv';\n\n// Load environment variables\ndotenv.config();\n\nconst envSchema = z.object({\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n PORT: z\n .string()\n .default('3030')\n .transform((val) => parseInt(val, 10)),\n CORS_ORIGIN: z\n .string()\n .default('http://localhost:5173')\n .transform((val) => (val.includes(',') ? val.split(',').map((s) => s.trim()) : val)),\n \n // Database (Prisma/SQL) - optional\n DATABASE_URL: z.string().optional(),\n \n // Firebase/Firestore configuration\n FIREBASE_PROJECT_ID: z.string().optional(),\n FIREBASE_CLIENT_EMAIL: z.string().optional(),\n FIREBASE_PRIVATE_KEY: z.string().optional(),\n FIREBASE_SERVICE_ACCOUNT_PATH: z.string().optional(),\n FIRESTORE_EMULATOR_HOST: z.string().optional(),\n FIREBASE_AUTH_EMULATOR_HOST: z.string().optional(),\n \n // API configuration\n API_PREFIX: z.string().default('/api'),\n\n // Mock data configuration\n USE_MOCK_DATA: z.string().default('true').transform((v) => v === 'true'),\n MOCK_SEED: z\n .string()\n .optional()\n .transform((v) => (v ? parseInt(v, 10) : undefined)),\n});\n\nconst parsed = envSchema.safeParse(process.env);\n\nif (!parsed.success) {\n console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors);\n throw new Error('Invalid environment variables');\n}\n\nexport const env = parsed.data;\n","/**\n * Database Configuration\n * \n * Supports both Prisma (SQL) and Firebase Admin (Firestore)\n * Generated apps can use either depending on the database option\n */\n\nimport admin from 'firebase-admin';\nimport { env } from './env.js';\n\n// ============ Firebase Admin / Firestore ============\n\nlet firebaseApp: admin.app.App | null = null;\n\n/**\n * Initialize Firebase Admin SDK\n */\nfunction initializeFirebase(): admin.app.App {\n if (firebaseApp) {\n return firebaseApp;\n }\n\n // Check if already initialized\n if (admin.apps.length > 0) {\n firebaseApp = admin.apps[0]!;\n return firebaseApp;\n }\n\n // Check for emulator mode FIRST (no credentials needed)\n if (env.FIRESTORE_EMULATOR_HOST) {\n // Emulator mode - no credentials needed\n firebaseApp = admin.initializeApp({\n projectId: env.FIREBASE_PROJECT_ID || 'demo-project',\n });\n console.log(`🔧 Firebase Admin initialized for emulator: ${env.FIRESTORE_EMULATOR_HOST}`);\n return firebaseApp;\n }\n\n // Production mode - need credentials\n const serviceAccountPath = env.FIREBASE_SERVICE_ACCOUNT_PATH;\n \n if (serviceAccountPath) {\n // Use service account file\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const serviceAccount = require(serviceAccountPath);\n firebaseApp = admin.initializeApp({\n credential: admin.credential.cert(serviceAccount),\n projectId: env.FIREBASE_PROJECT_ID,\n });\n } else if (env.FIREBASE_PROJECT_ID && env.FIREBASE_CLIENT_EMAIL && env.FIREBASE_PRIVATE_KEY) {\n // Use inline service account credentials\n firebaseApp = admin.initializeApp({\n credential: admin.credential.cert({\n projectId: env.FIREBASE_PROJECT_ID,\n clientEmail: env.FIREBASE_CLIENT_EMAIL,\n privateKey: env.FIREBASE_PRIVATE_KEY.replace(/\\\\n/g, '\\n'),\n }),\n projectId: env.FIREBASE_PROJECT_ID,\n });\n } else if (env.FIREBASE_PROJECT_ID) {\n // Use application default credentials (for Cloud Run, etc.)\n firebaseApp = admin.initializeApp({\n credential: admin.credential.applicationDefault(),\n projectId: env.FIREBASE_PROJECT_ID,\n });\n } else {\n // Emulator mode - use default credentials\n firebaseApp = admin.initializeApp({\n projectId: 'demo-project',\n });\n }\n\n return firebaseApp;\n}\n\n/**\n * Get Firestore instance\n */\nexport function getFirestore(): admin.firestore.Firestore {\n const app = initializeFirebase();\n const db = app.firestore();\n \n // Connect to emulator if configured\n if (env.FIRESTORE_EMULATOR_HOST) {\n db.settings({\n host: env.FIRESTORE_EMULATOR_HOST,\n ssl: false,\n });\n }\n \n return db;\n}\n\n/**\n * Get Firebase Auth instance\n */\nexport function getAuth(): admin.auth.Auth {\n const app = initializeFirebase();\n return app.auth();\n}\n\n// ============ Prisma (Optional - for SQL databases) ============\n\n// Uncomment if using Prisma with SQL database\n/*\nimport { PrismaClient } from '@prisma/client';\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __db: PrismaClient | undefined;\n}\n\nconst createPrismaClient = () => {\n return new PrismaClient({\n log: env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],\n });\n};\n\nexport const db = globalThis.__db ?? createPrismaClient();\n\nif (env.NODE_ENV !== 'production') {\n globalThis.__db = db;\n}\n*/\n\n// Re-export admin for convenience\nexport { admin };\n\n// Export db instance for handler convenience\nexport const db = getFirestore();\n","/**\n * Multi-User Middleware\n *\n * Provides user isolation and session ownership using Firebase Auth.\n *\n * @packageDocumentation\n */\n\nimport { getMultiUserManager, createUserContext } from '@almadar/agent';\nimport { getAuth } from '../lib/db.js';\nimport type { Request, Response, NextFunction } from 'express';\n\n// Extend Express Request to include Firebase user\ndeclare global {\n namespace Express {\n interface Request {\n user?: {\n uid: string;\n email?: string;\n roles?: string[];\n orgId?: string;\n };\n userContext?: {\n userId: string;\n orgId?: string;\n roles?: string[];\n };\n }\n }\n}\n\n/**\n * Middleware to set up user context from Firebase Auth\n */\nexport async function multiUserMiddleware(\n req: Request,\n res: Response,\n next: NextFunction,\n): Promise<void> {\n const userId = req.user?.uid;\n\n if (!userId) {\n res.status(401).json({ error: 'Authentication required' });\n return;\n }\n\n // Create user context\n req.userContext = createUserContext(userId, {\n orgId: req.user?.orgId,\n roles: req.user?.roles ?? ['user'],\n });\n\n // Assign session ownership when creating new sessions\n const originalJson = res.json.bind(res);\n res.json = (body: unknown) => {\n if (body && typeof body === 'object' && 'threadId' in body) {\n const multiUser = getMultiUserManager();\n multiUser.assignSessionOwnership(\n (body as { threadId: string }).threadId,\n userId,\n );\n }\n return originalJson(body);\n };\n\n next();\n}\n\n/**\n * Verify Firebase Auth token from Authorization header\n */\nexport async function verifyFirebaseAuth(\n req: Request,\n res: Response,\n next: NextFunction,\n): Promise<void> {\n const authHeader = req.headers.authorization;\n\n if (!authHeader?.startsWith('Bearer ')) {\n res.status(401).json({ error: 'No token provided' });\n return;\n }\n\n const token = authHeader.split('Bearer ')[1];\n\n try {\n const decodedToken = await getAuth().verifyIdToken(token);\n\n // Get custom claims for roles/org\n const user = await getAuth().getUser(decodedToken.uid);\n\n req.user = {\n uid: decodedToken.uid,\n email: decodedToken.email,\n roles: (user.customClaims?.roles as string[]) ?? ['user'],\n orgId: user.customClaims?.orgId as string,\n };\n\n next();\n } catch (error) {\n console.error('Token verification failed:', error);\n res.status(401).json({ error: 'Invalid token' });\n }\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import * as express_serve_static_core from 'express-serve-static-core';
2
+
3
+ /**
4
+ * Observability Routes
5
+ *
6
+ * Provides endpoints for metrics, health checks, and telemetry.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ declare const router: express_serve_static_core.Router;
11
+
12
+ export { router as default };
@@ -0,0 +1,62 @@
1
+ import { Router } from 'express';
2
+ import { getObservabilityCollector } from '@almadar/agent';
3
+
4
+ // src/routes/observability.ts
5
+ var router = Router();
6
+ router.get("/metrics", async (req, res) => {
7
+ try {
8
+ const collector = getObservabilityCollector();
9
+ const snapshot = collector.getPerformanceSnapshot();
10
+ res.json(snapshot);
11
+ } catch (error) {
12
+ console.error("Metrics error:", error);
13
+ res.status(500).json({ error: "Failed to get metrics" });
14
+ }
15
+ });
16
+ router.get("/health", async (req, res) => {
17
+ try {
18
+ const collector = getObservabilityCollector();
19
+ const health = await collector.healthCheck();
20
+ const allHealthy = health.every((h) => h.status === "healthy");
21
+ res.status(allHealthy ? 200 : 503).json({
22
+ status: allHealthy ? "healthy" : "degraded",
23
+ timestamp: Date.now(),
24
+ checks: health
25
+ });
26
+ } catch (error) {
27
+ console.error("Health check error:", error);
28
+ res.status(500).json({
29
+ status: "unhealthy",
30
+ error: "Health check failed"
31
+ });
32
+ }
33
+ });
34
+ router.get("/sessions/:threadId/telemetry", async (req, res) => {
35
+ try {
36
+ const collector = getObservabilityCollector();
37
+ const telemetry = collector.getSessionTelemetry(req.params.threadId);
38
+ if (!telemetry) {
39
+ res.status(404).json({ error: "Session not found" });
40
+ return;
41
+ }
42
+ res.json(telemetry);
43
+ } catch (error) {
44
+ console.error("Telemetry error:", error);
45
+ res.status(500).json({ error: "Failed to get telemetry" });
46
+ }
47
+ });
48
+ router.get("/active-sessions", async (req, res) => {
49
+ try {
50
+ const collector = getObservabilityCollector();
51
+ const sessions = collector.getActiveSessions();
52
+ res.json(sessions);
53
+ } catch (error) {
54
+ console.error("Active sessions error:", error);
55
+ res.status(500).json({ error: "Failed to get active sessions" });
56
+ }
57
+ });
58
+ var observability_default = router;
59
+
60
+ export { observability_default as default };
61
+ //# sourceMappingURL=observability.js.map
62
+ //# sourceMappingURL=observability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/routes/observability.ts"],"names":[],"mappings":";;;;AAWA,IAAM,SAAS,MAAA,EAAO;AAKtB,MAAA,CAAO,GAAA,CAAI,UAAA,EAAY,OAAO,GAAA,EAAK,GAAA,KAAQ;AACzC,EAAA,IAAI;AACF,IAAA,MAAM,YAAY,yBAAA,EAA0B;AAC5C,IAAA,MAAM,QAAA,GAAW,UAAU,sBAAA,EAAuB;AAClD,IAAA,GAAA,CAAI,KAAK,QAAQ,CAAA;AAAA,EACnB,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,kBAAkB,KAAK,CAAA;AACrC,IAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,yBAAyB,CAAA;AAAA,EACzD;AACF,CAAC,CAAA;AAKD,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,OAAO,GAAA,EAAK,GAAA,KAAQ;AACxC,EAAA,IAAI;AACF,IAAA,MAAM,YAAY,yBAAA,EAA0B;AAC5C,IAAA,MAAM,MAAA,GAAS,MAAM,SAAA,CAAU,WAAA,EAAY;AAC3C,IAAA,MAAM,aAAa,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA,KAAM,CAAA,CAAE,WAAW,SAAS,CAAA;AAE7D,IAAA,GAAA,CAAI,MAAA,CAAO,UAAA,GAAa,GAAA,GAAM,GAAG,EAAE,IAAA,CAAK;AAAA,MACtC,MAAA,EAAQ,aAAa,SAAA,GAAY,UAAA;AAAA,MACjC,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,MACpB,MAAA,EAAQ;AAAA,KACT,CAAA;AAAA,EACH,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,uBAAuB,KAAK,CAAA;AAC1C,IAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK;AAAA,MACnB,MAAA,EAAQ,WAAA;AAAA,MACR,KAAA,EAAO;AAAA,KACR,CAAA;AAAA,EACH;AACF,CAAC,CAAA;AAKD,MAAA,CAAO,GAAA,CAAI,+BAAA,EAAiC,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC9D,EAAA,IAAI;AACF,IAAA,MAAM,YAAY,yBAAA,EAA0B;AAC5C,IAAA,MAAM,SAAA,GAAY,SAAA,CAAU,mBAAA,CAAoB,GAAA,CAAI,OAAO,QAAQ,CAAA;AAEnE,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,qBAAqB,CAAA;AACnD,MAAA;AAAA,IACF;AAEA,IAAA,GAAA,CAAI,KAAK,SAAS,CAAA;AAAA,EACpB,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,oBAAoB,KAAK,CAAA;AACvC,IAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,2BAA2B,CAAA;AAAA,EAC3D;AACF,CAAC,CAAA;AAKD,MAAA,CAAO,GAAA,CAAI,kBAAA,EAAoB,OAAO,GAAA,EAAK,GAAA,KAAQ;AACjD,EAAA,IAAI;AACF,IAAA,MAAM,YAAY,yBAAA,EAA0B;AAC5C,IAAA,MAAM,QAAA,GAAW,UAAU,iBAAA,EAAkB;AAC7C,IAAA,GAAA,CAAI,KAAK,QAAQ,CAAA;AAAA,EACnB,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,0BAA0B,KAAK,CAAA;AAC7C,IAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,KAAK,EAAE,KAAA,EAAO,iCAAiC,CAAA;AAAA,EACjE;AACF,CAAC,CAAA;AAED,IAAO,qBAAA,GAAQ","file":"observability.js","sourcesContent":["/**\n * Observability Routes\n *\n * Provides endpoints for metrics, health checks, and telemetry.\n *\n * @packageDocumentation\n */\n\nimport { Router } from 'express';\nimport { getObservabilityCollector } from '@almadar/agent';\n\nconst router = Router();\n\n/**\n * GET /metrics - Get performance snapshot\n */\nrouter.get('/metrics', async (req, res) => {\n try {\n const collector = getObservabilityCollector();\n const snapshot = collector.getPerformanceSnapshot();\n res.json(snapshot);\n } catch (error) {\n console.error('Metrics error:', error);\n res.status(500).json({ error: 'Failed to get metrics' });\n }\n});\n\n/**\n * GET /health - Get health check\n */\nrouter.get('/health', async (req, res) => {\n try {\n const collector = getObservabilityCollector();\n const health = await collector.healthCheck();\n const allHealthy = health.every((h) => h.status === 'healthy');\n\n res.status(allHealthy ? 200 : 503).json({\n status: allHealthy ? 'healthy' : 'degraded',\n timestamp: Date.now(),\n checks: health,\n });\n } catch (error) {\n console.error('Health check error:', error);\n res.status(500).json({\n status: 'unhealthy',\n error: 'Health check failed',\n });\n }\n});\n\n/**\n * GET /sessions/:threadId/telemetry - Get session telemetry\n */\nrouter.get('/sessions/:threadId/telemetry', async (req, res) => {\n try {\n const collector = getObservabilityCollector();\n const telemetry = collector.getSessionTelemetry(req.params.threadId);\n\n if (!telemetry) {\n res.status(404).json({ error: 'Session not found' });\n return;\n }\n\n res.json(telemetry);\n } catch (error) {\n console.error('Telemetry error:', error);\n res.status(500).json({ error: 'Failed to get telemetry' });\n }\n});\n\n/**\n * GET /active-sessions - Get active sessions\n */\nrouter.get('/active-sessions', async (req, res) => {\n try {\n const collector = getObservabilityCollector();\n const sessions = collector.getActiveSessions();\n res.json(sessions);\n } catch (error) {\n console.error('Active sessions error:', error);\n res.status(500).json({ error: 'Failed to get active sessions' });\n }\n});\n\nexport default router;\n"]}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * State Sync WebSocket Handler
3
+ *
4
+ * Provides real-time state synchronization using Socket.IO.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ interface Socket {
9
+ data: {
10
+ user: {
11
+ uid: string;
12
+ roles: string[];
13
+ orgId?: string;
14
+ };
15
+ };
16
+ handshake: {
17
+ auth: {
18
+ token: string;
19
+ clientId: string;
20
+ };
21
+ };
22
+ join: (room: string) => void;
23
+ leave: (room: string) => void;
24
+ on: (event: string, handler: (...args: unknown[]) => void) => void;
25
+ emit: (event: string, ...args: unknown[]) => void;
26
+ to: (room: string) => {
27
+ emit: (event: string, ...args: unknown[]) => void;
28
+ };
29
+ }
30
+ interface SocketServer {
31
+ use: (middleware: (socket: Socket, next: (err?: Error) => void) => void) => void;
32
+ on: (event: string, handler: (socket: Socket) => void) => void;
33
+ }
34
+ /**
35
+ * Set up state sync WebSocket with Firebase Auth
36
+ */
37
+ declare function setupStateSyncWebSocket(io: SocketServer): void;
38
+
39
+ export { setupStateSyncWebSocket };
@@ -0,0 +1,151 @@
1
+ import { getStateSyncManager, getMultiUserManager } from '@almadar/agent';
2
+ import admin from 'firebase-admin';
3
+ import { z } from 'zod';
4
+ import dotenv from 'dotenv';
5
+
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
12
+ dotenv.config();
13
+ var envSchema = z.object({
14
+ NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
15
+ PORT: z.string().default("3030").transform((val) => parseInt(val, 10)),
16
+ CORS_ORIGIN: z.string().default("http://localhost:5173").transform((val) => val.includes(",") ? val.split(",").map((s) => s.trim()) : val),
17
+ // Database (Prisma/SQL) - optional
18
+ DATABASE_URL: z.string().optional(),
19
+ // Firebase/Firestore configuration
20
+ FIREBASE_PROJECT_ID: z.string().optional(),
21
+ FIREBASE_CLIENT_EMAIL: z.string().optional(),
22
+ FIREBASE_PRIVATE_KEY: z.string().optional(),
23
+ FIREBASE_SERVICE_ACCOUNT_PATH: z.string().optional(),
24
+ FIRESTORE_EMULATOR_HOST: z.string().optional(),
25
+ FIREBASE_AUTH_EMULATOR_HOST: z.string().optional(),
26
+ // API configuration
27
+ API_PREFIX: z.string().default("/api"),
28
+ // Mock data configuration
29
+ USE_MOCK_DATA: z.string().default("true").transform((v) => v === "true"),
30
+ MOCK_SEED: z.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
31
+ });
32
+ var parsed = envSchema.safeParse(process.env);
33
+ if (!parsed.success) {
34
+ console.error("\u274C Invalid environment variables:", parsed.error.flatten().fieldErrors);
35
+ throw new Error("Invalid environment variables");
36
+ }
37
+ var env = parsed.data;
38
+
39
+ // src/lib/db.ts
40
+ var firebaseApp = null;
41
+ function initializeFirebase() {
42
+ if (firebaseApp) {
43
+ return firebaseApp;
44
+ }
45
+ if (admin.apps.length > 0) {
46
+ firebaseApp = admin.apps[0];
47
+ return firebaseApp;
48
+ }
49
+ if (env.FIRESTORE_EMULATOR_HOST) {
50
+ firebaseApp = admin.initializeApp({
51
+ projectId: env.FIREBASE_PROJECT_ID || "demo-project"
52
+ });
53
+ console.log(`\u{1F527} Firebase Admin initialized for emulator: ${env.FIRESTORE_EMULATOR_HOST}`);
54
+ return firebaseApp;
55
+ }
56
+ const serviceAccountPath = env.FIREBASE_SERVICE_ACCOUNT_PATH;
57
+ if (serviceAccountPath) {
58
+ const serviceAccount = __require(serviceAccountPath);
59
+ firebaseApp = admin.initializeApp({
60
+ credential: admin.credential.cert(serviceAccount),
61
+ projectId: env.FIREBASE_PROJECT_ID
62
+ });
63
+ } else if (env.FIREBASE_PROJECT_ID && env.FIREBASE_CLIENT_EMAIL && env.FIREBASE_PRIVATE_KEY) {
64
+ firebaseApp = admin.initializeApp({
65
+ credential: admin.credential.cert({
66
+ projectId: env.FIREBASE_PROJECT_ID,
67
+ clientEmail: env.FIREBASE_CLIENT_EMAIL,
68
+ privateKey: env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n")
69
+ }),
70
+ projectId: env.FIREBASE_PROJECT_ID
71
+ });
72
+ } else if (env.FIREBASE_PROJECT_ID) {
73
+ firebaseApp = admin.initializeApp({
74
+ credential: admin.credential.applicationDefault(),
75
+ projectId: env.FIREBASE_PROJECT_ID
76
+ });
77
+ } else {
78
+ firebaseApp = admin.initializeApp({
79
+ projectId: "demo-project"
80
+ });
81
+ }
82
+ return firebaseApp;
83
+ }
84
+ function getFirestore() {
85
+ const app = initializeFirebase();
86
+ const db2 = app.firestore();
87
+ if (env.FIRESTORE_EMULATOR_HOST) {
88
+ db2.settings({
89
+ host: env.FIRESTORE_EMULATOR_HOST,
90
+ ssl: false
91
+ });
92
+ }
93
+ return db2;
94
+ }
95
+ function getAuth() {
96
+ const app = initializeFirebase();
97
+ return app.auth();
98
+ }
99
+ getFirestore();
100
+
101
+ // src/websocket/state-sync.ts
102
+ function setupStateSyncWebSocket(io) {
103
+ const stateSync = getStateSyncManager();
104
+ io.use(async (socket, next) => {
105
+ try {
106
+ const token = socket.handshake.auth.token;
107
+ if (!token) {
108
+ return next(new Error("Authentication required"));
109
+ }
110
+ const decodedToken = await getAuth().verifyIdToken(token);
111
+ const user = await getAuth().getUser(decodedToken.uid);
112
+ socket.data.user = {
113
+ uid: decodedToken.uid,
114
+ roles: user.customClaims?.roles ?? ["user"],
115
+ orgId: user.customClaims?.orgId
116
+ };
117
+ next();
118
+ } catch (error) {
119
+ console.error("Socket auth failed:", error);
120
+ next(new Error("Invalid token"));
121
+ }
122
+ });
123
+ io.on("connection", (socket) => {
124
+ const userId = socket.data.user.uid;
125
+ const clientId = socket.handshake.auth.clientId;
126
+ console.log(`[StateSync] Client ${clientId} connected for user ${userId}`);
127
+ stateSync.updateConfig({ clientId });
128
+ socket.join(`user:${userId}`);
129
+ socket.on("stateChange", (...args) => {
130
+ const event = args[0];
131
+ const multiUser = getMultiUserManager();
132
+ if (!multiUser.isSessionOwner(event.threadId, userId)) {
133
+ socket.emit("error", { message: "Not session owner" });
134
+ return;
135
+ }
136
+ stateSync.receiveRemoteChange(event);
137
+ socket.to(`user:${userId}`).emit("remoteChange", event);
138
+ });
139
+ stateSync.on("syncRequired", (changes) => {
140
+ socket.emit("syncBatch", changes);
141
+ });
142
+ socket.on("disconnect", () => {
143
+ console.log(`[StateSync] Client ${clientId} disconnected`);
144
+ socket.leave(`user:${userId}`);
145
+ });
146
+ });
147
+ }
148
+
149
+ export { setupStateSyncWebSocket };
150
+ //# sourceMappingURL=state-sync.js.map
151
+ //# sourceMappingURL=state-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/env.ts","../../src/lib/db.ts","../../src/websocket/state-sync.ts"],"names":["db"],"mappings":"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","file":"state-sync.js","sourcesContent":["import { z } from 'zod';\nimport dotenv from 'dotenv';\n\n// Load environment variables\ndotenv.config();\n\nconst envSchema = z.object({\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n PORT: z\n .string()\n .default('3030')\n .transform((val) => parseInt(val, 10)),\n CORS_ORIGIN: z\n .string()\n .default('http://localhost:5173')\n .transform((val) => (val.includes(',') ? val.split(',').map((s) => s.trim()) : val)),\n \n // Database (Prisma/SQL) - optional\n DATABASE_URL: z.string().optional(),\n \n // Firebase/Firestore configuration\n FIREBASE_PROJECT_ID: z.string().optional(),\n FIREBASE_CLIENT_EMAIL: z.string().optional(),\n FIREBASE_PRIVATE_KEY: z.string().optional(),\n FIREBASE_SERVICE_ACCOUNT_PATH: z.string().optional(),\n FIRESTORE_EMULATOR_HOST: z.string().optional(),\n FIREBASE_AUTH_EMULATOR_HOST: z.string().optional(),\n \n // API configuration\n API_PREFIX: z.string().default('/api'),\n\n // Mock data configuration\n USE_MOCK_DATA: z.string().default('true').transform((v) => v === 'true'),\n MOCK_SEED: z\n .string()\n .optional()\n .transform((v) => (v ? parseInt(v, 10) : undefined)),\n});\n\nconst parsed = envSchema.safeParse(process.env);\n\nif (!parsed.success) {\n console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors);\n throw new Error('Invalid environment variables');\n}\n\nexport const env = parsed.data;\n","/**\n * Database Configuration\n * \n * Supports both Prisma (SQL) and Firebase Admin (Firestore)\n * Generated apps can use either depending on the database option\n */\n\nimport admin from 'firebase-admin';\nimport { env } from './env.js';\n\n// ============ Firebase Admin / Firestore ============\n\nlet firebaseApp: admin.app.App | null = null;\n\n/**\n * Initialize Firebase Admin SDK\n */\nfunction initializeFirebase(): admin.app.App {\n if (firebaseApp) {\n return firebaseApp;\n }\n\n // Check if already initialized\n if (admin.apps.length > 0) {\n firebaseApp = admin.apps[0]!;\n return firebaseApp;\n }\n\n // Check for emulator mode FIRST (no credentials needed)\n if (env.FIRESTORE_EMULATOR_HOST) {\n // Emulator mode - no credentials needed\n firebaseApp = admin.initializeApp({\n projectId: env.FIREBASE_PROJECT_ID || 'demo-project',\n });\n console.log(`🔧 Firebase Admin initialized for emulator: ${env.FIRESTORE_EMULATOR_HOST}`);\n return firebaseApp;\n }\n\n // Production mode - need credentials\n const serviceAccountPath = env.FIREBASE_SERVICE_ACCOUNT_PATH;\n \n if (serviceAccountPath) {\n // Use service account file\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const serviceAccount = require(serviceAccountPath);\n firebaseApp = admin.initializeApp({\n credential: admin.credential.cert(serviceAccount),\n projectId: env.FIREBASE_PROJECT_ID,\n });\n } else if (env.FIREBASE_PROJECT_ID && env.FIREBASE_CLIENT_EMAIL && env.FIREBASE_PRIVATE_KEY) {\n // Use inline service account credentials\n firebaseApp = admin.initializeApp({\n credential: admin.credential.cert({\n projectId: env.FIREBASE_PROJECT_ID,\n clientEmail: env.FIREBASE_CLIENT_EMAIL,\n privateKey: env.FIREBASE_PRIVATE_KEY.replace(/\\\\n/g, '\\n'),\n }),\n projectId: env.FIREBASE_PROJECT_ID,\n });\n } else if (env.FIREBASE_PROJECT_ID) {\n // Use application default credentials (for Cloud Run, etc.)\n firebaseApp = admin.initializeApp({\n credential: admin.credential.applicationDefault(),\n projectId: env.FIREBASE_PROJECT_ID,\n });\n } else {\n // Emulator mode - use default credentials\n firebaseApp = admin.initializeApp({\n projectId: 'demo-project',\n });\n }\n\n return firebaseApp;\n}\n\n/**\n * Get Firestore instance\n */\nexport function getFirestore(): admin.firestore.Firestore {\n const app = initializeFirebase();\n const db = app.firestore();\n \n // Connect to emulator if configured\n if (env.FIRESTORE_EMULATOR_HOST) {\n db.settings({\n host: env.FIRESTORE_EMULATOR_HOST,\n ssl: false,\n });\n }\n \n return db;\n}\n\n/**\n * Get Firebase Auth instance\n */\nexport function getAuth(): admin.auth.Auth {\n const app = initializeFirebase();\n return app.auth();\n}\n\n// ============ Prisma (Optional - for SQL databases) ============\n\n// Uncomment if using Prisma with SQL database\n/*\nimport { PrismaClient } from '@prisma/client';\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __db: PrismaClient | undefined;\n}\n\nconst createPrismaClient = () => {\n return new PrismaClient({\n log: env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],\n });\n};\n\nexport const db = globalThis.__db ?? createPrismaClient();\n\nif (env.NODE_ENV !== 'production') {\n globalThis.__db = db;\n}\n*/\n\n// Re-export admin for convenience\nexport { admin };\n\n// Export db instance for handler convenience\nexport const db = getFirestore();\n","/**\n * State Sync WebSocket Handler\n *\n * Provides real-time state synchronization using Socket.IO.\n *\n * @packageDocumentation\n */\n\nimport { getStateSyncManager } from '@almadar/agent';\nimport { getMultiUserManager } from '@almadar/agent';\nimport { getAuth } from '../lib/db.js';\n\n// Type definitions for Socket.IO (to avoid dependency)\ninterface Socket {\n data: { user: { uid: string; roles: string[]; orgId?: string } };\n handshake: { auth: { token: string; clientId: string } };\n join: (room: string) => void;\n leave: (room: string) => void;\n on: (event: string, handler: (...args: unknown[]) => void) => void;\n emit: (event: string, ...args: unknown[]) => void;\n to: (room: string) => { emit: (event: string, ...args: unknown[]) => void };\n}\n\ninterface SocketServer {\n use: (middleware: (socket: Socket, next: (err?: Error) => void) => void) => void;\n on: (event: string, handler: (socket: Socket) => void) => void;\n}\n\n/**\n * Set up state sync WebSocket with Firebase Auth\n */\nexport function setupStateSyncWebSocket(io: SocketServer): void {\n const stateSync = getStateSyncManager();\n\n // Firebase Auth middleware for Socket.IO\n io.use(async (socket, next) => {\n try {\n const token = socket.handshake.auth.token as string;\n if (!token) {\n return next(new Error('Authentication required'));\n }\n\n const decodedToken = await getAuth().verifyIdToken(token);\n const user = await getAuth().getUser(decodedToken.uid);\n\n socket.data.user = {\n uid: decodedToken.uid,\n roles: (user.customClaims?.roles as string[]) ?? ['user'],\n orgId: user.customClaims?.orgId as string,\n };\n\n next();\n } catch (error) {\n console.error('Socket auth failed:', error);\n next(new Error('Invalid token'));\n }\n });\n\n io.on('connection', (socket: Socket) => {\n const userId = socket.data.user.uid;\n const clientId = socket.handshake.auth.clientId;\n\n console.log(`[StateSync] Client ${clientId} connected for user ${userId}`);\n\n // Update client ID in sync manager\n stateSync.updateConfig({ clientId });\n\n // Join user's room for targeted updates\n socket.join(`user:${userId}`);\n\n // Listen for state changes from client\n socket.on('stateChange', (...args: unknown[]) => {\n const event = args[0] as { threadId: string };\n \n // Verify ownership\n const multiUser = getMultiUserManager();\n if (!multiUser.isSessionOwner(event.threadId, userId)) {\n socket.emit('error', { message: 'Not session owner' });\n return;\n }\n\n // Process through sync manager\n stateSync.receiveRemoteChange(event as unknown as import('@almadar/agent').StateChangeEvent);\n\n // Broadcast to other clients of same user\n socket.to(`user:${userId}`).emit('remoteChange', event);\n });\n\n // Handle sync required events from agent\n stateSync.on('syncRequired', (changes: unknown[]) => {\n socket.emit('syncBatch', changes);\n });\n\n socket.on('disconnect', () => {\n console.log(`[StateSync] Client ${clientId} disconnected`);\n socket.leave(`user:${userId}`);\n });\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/server",
3
- "version": "1.0.17",
3
+ "version": "1.2.0",
4
4
  "description": "Shared server infrastructure for Almadar applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,6 +45,7 @@
45
45
  "helmet": "^8.0.0",
46
46
  "ws": "^8.18.0",
47
47
  "zod": "^3.24.0",
48
+ "@almadar/agent": "1.2.0",
48
49
  "@almadar/core": "1.0.17"
49
50
  },
50
51
  "peerDependencies": {