@almadar/server 2.0.4 → 2.0.5
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/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import dotenv from 'dotenv';
|
|
|
3
3
|
import { Router } from 'express';
|
|
4
4
|
import admin from 'firebase-admin';
|
|
5
5
|
export { default as admin } from 'firebase-admin';
|
|
6
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
6
|
import { faker } from '@faker-js/faker';
|
|
7
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
8
8
|
import { diffSchemas, categorizeRemovals, detectPageContentReduction, isDestructiveChange, hasSignificantPageReduction, requiresConfirmation } from '@almadar/core';
|
|
9
9
|
import { getObservabilityCollector, MemoryManager, SessionManager, getMultiUserManager, createWorkflowToolWrapper, createSkillAgent, createUserContext, getStateSyncManager } from '@almadar/agent';
|
|
10
10
|
|
|
@@ -452,27 +452,6 @@ var EventPersistence = class {
|
|
|
452
452
|
return this.store;
|
|
453
453
|
}
|
|
454
454
|
};
|
|
455
|
-
function debugEventsRouter() {
|
|
456
|
-
const router2 = Router();
|
|
457
|
-
if (process.env.NODE_ENV !== "development") {
|
|
458
|
-
return router2;
|
|
459
|
-
}
|
|
460
|
-
router2.get("/event-log", (_req, res) => {
|
|
461
|
-
const limit = parseInt(String(_req.query.limit) || "50", 10);
|
|
462
|
-
const events = getServerEventBus().getRecentEvents(limit);
|
|
463
|
-
res.json({ count: events.length, events });
|
|
464
|
-
});
|
|
465
|
-
router2.delete("/event-log", (_req, res) => {
|
|
466
|
-
getServerEventBus().clearEventLog();
|
|
467
|
-
res.json({ cleared: true });
|
|
468
|
-
});
|
|
469
|
-
router2.get("/listeners", (_req, res) => {
|
|
470
|
-
const counts = getServerEventBus().getListenerCounts();
|
|
471
|
-
const total = Object.values(counts).reduce((sum, n) => sum + n, 0);
|
|
472
|
-
res.json({ total, events: counts });
|
|
473
|
-
});
|
|
474
|
-
return router2;
|
|
475
|
-
}
|
|
476
455
|
function initializeFirebase() {
|
|
477
456
|
if (admin.apps.length > 0) {
|
|
478
457
|
return admin.app();
|
|
@@ -537,380 +516,117 @@ var db = new Proxy({}, {
|
|
|
537
516
|
return typeof value === "function" ? value.bind(firestore) : value;
|
|
538
517
|
}
|
|
539
518
|
});
|
|
540
|
-
var
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
wss.on("connection", (ws, req) => {
|
|
549
|
-
const clientId = req.headers["sec-websocket-key"] || "unknown";
|
|
550
|
-
logger.debug(`[WebSocket] Client connected: ${clientId}`);
|
|
551
|
-
ws.send(
|
|
552
|
-
JSON.stringify({
|
|
553
|
-
type: "CONNECTED",
|
|
554
|
-
timestamp: Date.now(),
|
|
555
|
-
message: "Connected to event stream"
|
|
556
|
-
})
|
|
557
|
-
);
|
|
558
|
-
ws.on("message", (data) => {
|
|
559
|
-
try {
|
|
560
|
-
const message = JSON.parse(data.toString());
|
|
561
|
-
logger.debug(`[WebSocket] Received from ${clientId}:`, message);
|
|
562
|
-
if (message.type && message.payload) {
|
|
563
|
-
getServerEventBus().emit(message.type, message.payload, {
|
|
564
|
-
orbital: "client",
|
|
565
|
-
entity: clientId
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
} catch (error) {
|
|
569
|
-
logger.error(`[WebSocket] Failed to parse message:`, error);
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
ws.on("close", () => {
|
|
573
|
-
logger.debug(`[WebSocket] Client disconnected: ${clientId}`);
|
|
574
|
-
});
|
|
575
|
-
ws.on("error", (error) => {
|
|
576
|
-
logger.error(`[WebSocket] Client error:`, error);
|
|
577
|
-
});
|
|
578
|
-
});
|
|
579
|
-
getServerEventBus().on("*", (event) => {
|
|
580
|
-
if (!wss) return;
|
|
581
|
-
const typedEvent = event;
|
|
582
|
-
const message = JSON.stringify({
|
|
583
|
-
type: typedEvent.type,
|
|
584
|
-
payload: typedEvent.payload,
|
|
585
|
-
timestamp: typedEvent.timestamp,
|
|
586
|
-
source: typedEvent.source
|
|
587
|
-
});
|
|
588
|
-
let broadcastCount = 0;
|
|
589
|
-
wss.clients.forEach((client) => {
|
|
590
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
591
|
-
client.send(message);
|
|
592
|
-
broadcastCount++;
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
if (broadcastCount > 0) {
|
|
596
|
-
logger.debug(`[WebSocket] Broadcast ${typedEvent.type} to ${broadcastCount} client(s)`);
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
return wss;
|
|
600
|
-
}
|
|
601
|
-
function getWebSocketServer() {
|
|
602
|
-
return wss;
|
|
603
|
-
}
|
|
604
|
-
function closeWebSocketServer() {
|
|
605
|
-
return new Promise((resolve, reject) => {
|
|
606
|
-
if (!wss) {
|
|
607
|
-
resolve();
|
|
608
|
-
return;
|
|
519
|
+
var MockDataService = class {
|
|
520
|
+
stores = /* @__PURE__ */ new Map();
|
|
521
|
+
schemas = /* @__PURE__ */ new Map();
|
|
522
|
+
idCounters = /* @__PURE__ */ new Map();
|
|
523
|
+
constructor() {
|
|
524
|
+
if (env.MOCK_SEED !== void 0) {
|
|
525
|
+
faker.seed(env.MOCK_SEED);
|
|
526
|
+
logger.info(`[Mock] Using seed: ${env.MOCK_SEED}`);
|
|
609
527
|
}
|
|
610
|
-
wss.close((err) => {
|
|
611
|
-
if (err) {
|
|
612
|
-
reject(err);
|
|
613
|
-
} else {
|
|
614
|
-
wss = null;
|
|
615
|
-
resolve();
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
function getConnectedClientCount() {
|
|
621
|
-
if (!wss) return 0;
|
|
622
|
-
return wss.clients.size;
|
|
623
|
-
}
|
|
624
|
-
var AppError = class extends Error {
|
|
625
|
-
constructor(statusCode, message, code) {
|
|
626
|
-
super(message);
|
|
627
|
-
this.statusCode = statusCode;
|
|
628
|
-
this.message = message;
|
|
629
|
-
this.code = code;
|
|
630
|
-
this.name = "AppError";
|
|
631
528
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
529
|
+
// ============================================================================
|
|
530
|
+
// Store Management
|
|
531
|
+
// ============================================================================
|
|
532
|
+
/**
|
|
533
|
+
* Initialize store for an entity.
|
|
534
|
+
*/
|
|
535
|
+
getStore(entityName) {
|
|
536
|
+
const normalized = entityName.toLowerCase();
|
|
537
|
+
if (!this.stores.has(normalized)) {
|
|
538
|
+
this.stores.set(normalized, /* @__PURE__ */ new Map());
|
|
539
|
+
this.idCounters.set(normalized, 0);
|
|
540
|
+
}
|
|
541
|
+
return this.stores.get(normalized);
|
|
636
542
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Generate next ID for an entity.
|
|
545
|
+
*/
|
|
546
|
+
nextId(entityName) {
|
|
547
|
+
const normalized = entityName.toLowerCase();
|
|
548
|
+
const counter = (this.idCounters.get(normalized) ?? 0) + 1;
|
|
549
|
+
this.idCounters.set(normalized, counter);
|
|
550
|
+
return `mock-${normalized}-${counter}`;
|
|
641
551
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
552
|
+
// ============================================================================
|
|
553
|
+
// Schema & Seeding
|
|
554
|
+
// ============================================================================
|
|
555
|
+
/**
|
|
556
|
+
* Register an entity schema.
|
|
557
|
+
*/
|
|
558
|
+
registerSchema(entityName, schema) {
|
|
559
|
+
this.schemas.set(entityName.toLowerCase(), schema);
|
|
646
560
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
561
|
+
/**
|
|
562
|
+
* Seed an entity with mock data.
|
|
563
|
+
*/
|
|
564
|
+
seed(entityName, fields, count = 10) {
|
|
565
|
+
const store = this.getStore(entityName);
|
|
566
|
+
const normalized = entityName.toLowerCase();
|
|
567
|
+
logger.info(`[Mock] Seeding ${count} ${entityName}...`);
|
|
568
|
+
for (let i = 0; i < count; i++) {
|
|
569
|
+
const item = this.generateMockItem(normalized, fields, i + 1);
|
|
570
|
+
store.set(item.id, item);
|
|
571
|
+
}
|
|
651
572
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Generate a single mock item based on field schemas.
|
|
575
|
+
*/
|
|
576
|
+
generateMockItem(entityName, fields, index) {
|
|
577
|
+
const id = this.nextId(entityName);
|
|
578
|
+
const now = /* @__PURE__ */ new Date();
|
|
579
|
+
const item = {
|
|
580
|
+
id,
|
|
581
|
+
createdAt: faker.date.past({ years: 1 }),
|
|
582
|
+
updatedAt: now
|
|
583
|
+
};
|
|
584
|
+
for (const field of fields) {
|
|
585
|
+
if (field.name === "id" || field.name === "createdAt" || field.name === "updatedAt") {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
item[field.name] = this.generateFieldValue(entityName, field, index);
|
|
589
|
+
}
|
|
590
|
+
return item;
|
|
656
591
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
Promise.resolve(fn(req, res, next)).catch(next);
|
|
696
|
-
};
|
|
697
|
-
var notFoundHandler = (req, res) => {
|
|
698
|
-
res.status(404).json({
|
|
699
|
-
success: false,
|
|
700
|
-
error: `Route ${req.method} ${req.path} not found`,
|
|
701
|
-
code: "ROUTE_NOT_FOUND"
|
|
702
|
-
});
|
|
703
|
-
};
|
|
704
|
-
var validateBody = (schema) => async (req, res, next) => {
|
|
705
|
-
try {
|
|
706
|
-
req.body = await schema.parseAsync(req.body);
|
|
707
|
-
next();
|
|
708
|
-
} catch (error) {
|
|
709
|
-
if (error instanceof ZodError) {
|
|
710
|
-
res.status(400).json({
|
|
711
|
-
success: false,
|
|
712
|
-
error: "Validation failed",
|
|
713
|
-
code: "VALIDATION_ERROR",
|
|
714
|
-
details: error.errors.map((e) => ({
|
|
715
|
-
path: e.path.join("."),
|
|
716
|
-
message: e.message
|
|
717
|
-
}))
|
|
718
|
-
});
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
next(error);
|
|
722
|
-
}
|
|
723
|
-
};
|
|
724
|
-
var validateQuery = (schema) => async (req, res, next) => {
|
|
725
|
-
try {
|
|
726
|
-
req.query = await schema.parseAsync(req.query);
|
|
727
|
-
next();
|
|
728
|
-
} catch (error) {
|
|
729
|
-
if (error instanceof ZodError) {
|
|
730
|
-
res.status(400).json({
|
|
731
|
-
success: false,
|
|
732
|
-
error: "Invalid query parameters",
|
|
733
|
-
code: "VALIDATION_ERROR",
|
|
734
|
-
details: error.errors.map((e) => ({
|
|
735
|
-
path: e.path.join("."),
|
|
736
|
-
message: e.message
|
|
737
|
-
}))
|
|
738
|
-
});
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
next(error);
|
|
742
|
-
}
|
|
743
|
-
};
|
|
744
|
-
var validateParams = (schema) => async (req, res, next) => {
|
|
745
|
-
try {
|
|
746
|
-
req.params = await schema.parseAsync(req.params);
|
|
747
|
-
next();
|
|
748
|
-
} catch (error) {
|
|
749
|
-
if (error instanceof ZodError) {
|
|
750
|
-
res.status(400).json({
|
|
751
|
-
success: false,
|
|
752
|
-
error: "Invalid path parameters",
|
|
753
|
-
code: "VALIDATION_ERROR",
|
|
754
|
-
details: error.errors.map((e) => ({
|
|
755
|
-
path: e.path.join("."),
|
|
756
|
-
message: e.message
|
|
757
|
-
}))
|
|
758
|
-
});
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
next(error);
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
// src/middleware/authenticateFirebase.ts
|
|
766
|
-
var BEARER_PREFIX = "Bearer ";
|
|
767
|
-
var DEV_USER = {
|
|
768
|
-
uid: "dev-user-001",
|
|
769
|
-
email: "dev@localhost",
|
|
770
|
-
email_verified: true,
|
|
771
|
-
aud: "dev-project",
|
|
772
|
-
auth_time: Math.floor(Date.now() / 1e3),
|
|
773
|
-
exp: Math.floor(Date.now() / 1e3) + 3600,
|
|
774
|
-
iat: Math.floor(Date.now() / 1e3),
|
|
775
|
-
iss: "https://securetoken.google.com/dev-project",
|
|
776
|
-
sub: "dev-user-001",
|
|
777
|
-
firebase: {
|
|
778
|
-
identities: {},
|
|
779
|
-
sign_in_provider: "custom"
|
|
780
|
-
}
|
|
781
|
-
};
|
|
782
|
-
async function authenticateFirebase(req, res, next) {
|
|
783
|
-
const authorization = req.headers.authorization;
|
|
784
|
-
if (env.NODE_ENV === "development" && (!authorization || !authorization.startsWith(BEARER_PREFIX))) {
|
|
785
|
-
req.firebaseUser = DEV_USER;
|
|
786
|
-
res.locals.firebaseUser = DEV_USER;
|
|
787
|
-
return next();
|
|
788
|
-
}
|
|
789
|
-
try {
|
|
790
|
-
if (!authorization || !authorization.startsWith(BEARER_PREFIX)) {
|
|
791
|
-
return res.status(401).json({ error: "Authorization header missing or malformed" });
|
|
792
|
-
}
|
|
793
|
-
const token = authorization.slice(BEARER_PREFIX.length);
|
|
794
|
-
const decodedToken = await getAuth().verifyIdToken(token);
|
|
795
|
-
req.firebaseUser = decodedToken;
|
|
796
|
-
res.locals.firebaseUser = decodedToken;
|
|
797
|
-
return next();
|
|
798
|
-
} catch (error) {
|
|
799
|
-
console.error("Firebase authentication failed:", error);
|
|
800
|
-
return res.status(401).json({ error: "Unauthorized" });
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
var MockDataService = class {
|
|
804
|
-
stores = /* @__PURE__ */ new Map();
|
|
805
|
-
schemas = /* @__PURE__ */ new Map();
|
|
806
|
-
idCounters = /* @__PURE__ */ new Map();
|
|
807
|
-
constructor() {
|
|
808
|
-
if (env.MOCK_SEED !== void 0) {
|
|
809
|
-
faker.seed(env.MOCK_SEED);
|
|
810
|
-
logger.info(`[Mock] Using seed: ${env.MOCK_SEED}`);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
// ============================================================================
|
|
814
|
-
// Store Management
|
|
815
|
-
// ============================================================================
|
|
816
|
-
/**
|
|
817
|
-
* Initialize store for an entity.
|
|
818
|
-
*/
|
|
819
|
-
getStore(entityName) {
|
|
820
|
-
const normalized = entityName.toLowerCase();
|
|
821
|
-
if (!this.stores.has(normalized)) {
|
|
822
|
-
this.stores.set(normalized, /* @__PURE__ */ new Map());
|
|
823
|
-
this.idCounters.set(normalized, 0);
|
|
824
|
-
}
|
|
825
|
-
return this.stores.get(normalized);
|
|
826
|
-
}
|
|
827
|
-
/**
|
|
828
|
-
* Generate next ID for an entity.
|
|
829
|
-
*/
|
|
830
|
-
nextId(entityName) {
|
|
831
|
-
const normalized = entityName.toLowerCase();
|
|
832
|
-
const counter = (this.idCounters.get(normalized) ?? 0) + 1;
|
|
833
|
-
this.idCounters.set(normalized, counter);
|
|
834
|
-
return `mock-${normalized}-${counter}`;
|
|
835
|
-
}
|
|
836
|
-
// ============================================================================
|
|
837
|
-
// Schema & Seeding
|
|
838
|
-
// ============================================================================
|
|
839
|
-
/**
|
|
840
|
-
* Register an entity schema.
|
|
841
|
-
*/
|
|
842
|
-
registerSchema(entityName, schema) {
|
|
843
|
-
this.schemas.set(entityName.toLowerCase(), schema);
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Seed an entity with mock data.
|
|
847
|
-
*/
|
|
848
|
-
seed(entityName, fields, count = 10) {
|
|
849
|
-
const store = this.getStore(entityName);
|
|
850
|
-
const normalized = entityName.toLowerCase();
|
|
851
|
-
logger.info(`[Mock] Seeding ${count} ${entityName}...`);
|
|
852
|
-
for (let i = 0; i < count; i++) {
|
|
853
|
-
const item = this.generateMockItem(normalized, fields, i + 1);
|
|
854
|
-
store.set(item.id, item);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Generate a single mock item based on field schemas.
|
|
859
|
-
*/
|
|
860
|
-
generateMockItem(entityName, fields, index) {
|
|
861
|
-
const id = this.nextId(entityName);
|
|
862
|
-
const now = /* @__PURE__ */ new Date();
|
|
863
|
-
const item = {
|
|
864
|
-
id,
|
|
865
|
-
createdAt: faker.date.past({ years: 1 }),
|
|
866
|
-
updatedAt: now
|
|
867
|
-
};
|
|
868
|
-
for (const field of fields) {
|
|
869
|
-
if (field.name === "id" || field.name === "createdAt" || field.name === "updatedAt") {
|
|
870
|
-
continue;
|
|
871
|
-
}
|
|
872
|
-
item[field.name] = this.generateFieldValue(entityName, field, index);
|
|
873
|
-
}
|
|
874
|
-
return item;
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Generate a mock value for a field based on its schema.
|
|
878
|
-
*/
|
|
879
|
-
generateFieldValue(entityName, field, index) {
|
|
880
|
-
if (!field.required && Math.random() > 0.8) {
|
|
881
|
-
return void 0;
|
|
882
|
-
}
|
|
883
|
-
switch (field.type) {
|
|
884
|
-
case "string":
|
|
885
|
-
return this.generateStringValue(entityName, field, index);
|
|
886
|
-
case "number":
|
|
887
|
-
return faker.number.int({
|
|
888
|
-
min: field.min ?? 0,
|
|
889
|
-
max: field.max ?? 1e3
|
|
890
|
-
});
|
|
891
|
-
case "boolean":
|
|
892
|
-
return faker.datatype.boolean();
|
|
893
|
-
case "date":
|
|
894
|
-
return this.generateDateValue(field);
|
|
895
|
-
case "enum":
|
|
896
|
-
if (field.enumValues && field.enumValues.length > 0) {
|
|
897
|
-
return faker.helpers.arrayElement(field.enumValues);
|
|
898
|
-
}
|
|
899
|
-
return null;
|
|
900
|
-
case "relation":
|
|
901
|
-
if (field.relatedEntity) {
|
|
902
|
-
const relatedStore = this.stores.get(field.relatedEntity.toLowerCase());
|
|
903
|
-
if (relatedStore && relatedStore.size > 0) {
|
|
904
|
-
const ids = Array.from(relatedStore.keys());
|
|
905
|
-
return faker.helpers.arrayElement(ids);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
return null;
|
|
909
|
-
case "array":
|
|
910
|
-
return [];
|
|
911
|
-
default:
|
|
912
|
-
return null;
|
|
913
|
-
}
|
|
592
|
+
/**
|
|
593
|
+
* Generate a mock value for a field based on its schema.
|
|
594
|
+
*/
|
|
595
|
+
generateFieldValue(entityName, field, index) {
|
|
596
|
+
if (!field.required && Math.random() > 0.8) {
|
|
597
|
+
return void 0;
|
|
598
|
+
}
|
|
599
|
+
switch (field.type) {
|
|
600
|
+
case "string":
|
|
601
|
+
return this.generateStringValue(entityName, field, index);
|
|
602
|
+
case "number":
|
|
603
|
+
return faker.number.int({
|
|
604
|
+
min: field.min ?? 0,
|
|
605
|
+
max: field.max ?? 1e3
|
|
606
|
+
});
|
|
607
|
+
case "boolean":
|
|
608
|
+
return faker.datatype.boolean();
|
|
609
|
+
case "date":
|
|
610
|
+
return this.generateDateValue(field);
|
|
611
|
+
case "enum":
|
|
612
|
+
if (field.enumValues && field.enumValues.length > 0) {
|
|
613
|
+
return faker.helpers.arrayElement(field.enumValues);
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
case "relation":
|
|
617
|
+
if (field.relatedEntity) {
|
|
618
|
+
const relatedStore = this.stores.get(field.relatedEntity.toLowerCase());
|
|
619
|
+
if (relatedStore && relatedStore.size > 0) {
|
|
620
|
+
const ids = Array.from(relatedStore.keys());
|
|
621
|
+
return faker.helpers.arrayElement(ids);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
case "array":
|
|
626
|
+
return [];
|
|
627
|
+
default:
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
914
630
|
}
|
|
915
631
|
/**
|
|
916
632
|
* Generate a string value based on field name heuristics.
|
|
@@ -1473,6 +1189,307 @@ function seedMockData(entities) {
|
|
|
1473
1189
|
logger.info("[DataService] Mock data seeding complete");
|
|
1474
1190
|
}
|
|
1475
1191
|
|
|
1192
|
+
// src/lib/debugRouter.ts
|
|
1193
|
+
function debugEventsRouter() {
|
|
1194
|
+
const router2 = Router();
|
|
1195
|
+
if (process.env.NODE_ENV !== "development") {
|
|
1196
|
+
return router2;
|
|
1197
|
+
}
|
|
1198
|
+
router2.get("/event-log", (_req, res) => {
|
|
1199
|
+
const limit = parseInt(String(_req.query.limit) || "50", 10);
|
|
1200
|
+
const events = getServerEventBus().getRecentEvents(limit);
|
|
1201
|
+
res.json({ count: events.length, events });
|
|
1202
|
+
});
|
|
1203
|
+
router2.delete("/event-log", (_req, res) => {
|
|
1204
|
+
getServerEventBus().clearEventLog();
|
|
1205
|
+
res.json({ cleared: true });
|
|
1206
|
+
});
|
|
1207
|
+
router2.get("/listeners", (_req, res) => {
|
|
1208
|
+
const counts = getServerEventBus().getListenerCounts();
|
|
1209
|
+
const total = Object.values(counts).reduce((sum, n) => sum + n, 0);
|
|
1210
|
+
res.json({ total, events: counts });
|
|
1211
|
+
});
|
|
1212
|
+
router2.post("/seed", (req, res) => {
|
|
1213
|
+
const { entities } = req.body;
|
|
1214
|
+
if (!entities || !Array.isArray(entities)) {
|
|
1215
|
+
res.status(400).json({ error: 'Body must have "entities" array' });
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const configs = entities.map((e) => ({
|
|
1219
|
+
name: e.name,
|
|
1220
|
+
fields: e.fields,
|
|
1221
|
+
seedCount: e.seedCount ?? 5
|
|
1222
|
+
}));
|
|
1223
|
+
seedMockData(configs);
|
|
1224
|
+
const summary = configs.map((c) => `${c.name}(${c.seedCount})`).join(", ");
|
|
1225
|
+
res.json({ seeded: true, summary });
|
|
1226
|
+
});
|
|
1227
|
+
return router2;
|
|
1228
|
+
}
|
|
1229
|
+
var wss = null;
|
|
1230
|
+
function setupEventBroadcast(server, path = "/ws/events") {
|
|
1231
|
+
if (wss) {
|
|
1232
|
+
logger.warn("[WebSocket] Server already initialized");
|
|
1233
|
+
return wss;
|
|
1234
|
+
}
|
|
1235
|
+
wss = new WebSocketServer({ server, path });
|
|
1236
|
+
logger.info(`[WebSocket] Server listening at ${path}`);
|
|
1237
|
+
wss.on("connection", (ws, req) => {
|
|
1238
|
+
const clientId = req.headers["sec-websocket-key"] || "unknown";
|
|
1239
|
+
logger.debug(`[WebSocket] Client connected: ${clientId}`);
|
|
1240
|
+
ws.send(
|
|
1241
|
+
JSON.stringify({
|
|
1242
|
+
type: "CONNECTED",
|
|
1243
|
+
timestamp: Date.now(),
|
|
1244
|
+
message: "Connected to event stream"
|
|
1245
|
+
})
|
|
1246
|
+
);
|
|
1247
|
+
ws.on("message", (data) => {
|
|
1248
|
+
try {
|
|
1249
|
+
const message = JSON.parse(data.toString());
|
|
1250
|
+
logger.debug(`[WebSocket] Received from ${clientId}:`, message);
|
|
1251
|
+
if (message.type && message.payload) {
|
|
1252
|
+
getServerEventBus().emit(message.type, message.payload, {
|
|
1253
|
+
orbital: "client",
|
|
1254
|
+
entity: clientId
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
logger.error(`[WebSocket] Failed to parse message:`, error);
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
ws.on("close", () => {
|
|
1262
|
+
logger.debug(`[WebSocket] Client disconnected: ${clientId}`);
|
|
1263
|
+
});
|
|
1264
|
+
ws.on("error", (error) => {
|
|
1265
|
+
logger.error(`[WebSocket] Client error:`, error);
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
getServerEventBus().on("*", (event) => {
|
|
1269
|
+
if (!wss) return;
|
|
1270
|
+
const typedEvent = event;
|
|
1271
|
+
const message = JSON.stringify({
|
|
1272
|
+
type: typedEvent.type,
|
|
1273
|
+
payload: typedEvent.payload,
|
|
1274
|
+
timestamp: typedEvent.timestamp,
|
|
1275
|
+
source: typedEvent.source
|
|
1276
|
+
});
|
|
1277
|
+
let broadcastCount = 0;
|
|
1278
|
+
wss.clients.forEach((client) => {
|
|
1279
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1280
|
+
client.send(message);
|
|
1281
|
+
broadcastCount++;
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
if (broadcastCount > 0) {
|
|
1285
|
+
logger.debug(`[WebSocket] Broadcast ${typedEvent.type} to ${broadcastCount} client(s)`);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
return wss;
|
|
1289
|
+
}
|
|
1290
|
+
function getWebSocketServer() {
|
|
1291
|
+
return wss;
|
|
1292
|
+
}
|
|
1293
|
+
function closeWebSocketServer() {
|
|
1294
|
+
return new Promise((resolve, reject) => {
|
|
1295
|
+
if (!wss) {
|
|
1296
|
+
resolve();
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
wss.close((err) => {
|
|
1300
|
+
if (err) {
|
|
1301
|
+
reject(err);
|
|
1302
|
+
} else {
|
|
1303
|
+
wss = null;
|
|
1304
|
+
resolve();
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
function getConnectedClientCount() {
|
|
1310
|
+
if (!wss) return 0;
|
|
1311
|
+
return wss.clients.size;
|
|
1312
|
+
}
|
|
1313
|
+
var AppError = class extends Error {
|
|
1314
|
+
constructor(statusCode, message, code) {
|
|
1315
|
+
super(message);
|
|
1316
|
+
this.statusCode = statusCode;
|
|
1317
|
+
this.message = message;
|
|
1318
|
+
this.code = code;
|
|
1319
|
+
this.name = "AppError";
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
var NotFoundError = class extends AppError {
|
|
1323
|
+
constructor(message = "Resource not found") {
|
|
1324
|
+
super(404, message, "NOT_FOUND");
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
var ValidationError = class extends AppError {
|
|
1328
|
+
constructor(message = "Validation failed") {
|
|
1329
|
+
super(400, message, "VALIDATION_ERROR");
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
var UnauthorizedError = class extends AppError {
|
|
1333
|
+
constructor(message = "Unauthorized") {
|
|
1334
|
+
super(401, message, "UNAUTHORIZED");
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
var ForbiddenError = class extends AppError {
|
|
1338
|
+
constructor(message = "Forbidden") {
|
|
1339
|
+
super(403, message, "FORBIDDEN");
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
var ConflictError = class extends AppError {
|
|
1343
|
+
constructor(message = "Resource conflict") {
|
|
1344
|
+
super(409, message, "CONFLICT");
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
var errorHandler = (err, _req, res, _next) => {
|
|
1348
|
+
logger.error("Error:", { name: err.name, message: err.message, stack: err.stack });
|
|
1349
|
+
if (err instanceof ZodError) {
|
|
1350
|
+
res.status(400).json({
|
|
1351
|
+
success: false,
|
|
1352
|
+
error: "Validation failed",
|
|
1353
|
+
code: "VALIDATION_ERROR",
|
|
1354
|
+
details: err.errors.map((e) => ({
|
|
1355
|
+
path: e.path.join("."),
|
|
1356
|
+
message: e.message
|
|
1357
|
+
}))
|
|
1358
|
+
});
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (err instanceof AppError) {
|
|
1362
|
+
res.status(err.statusCode).json({
|
|
1363
|
+
success: false,
|
|
1364
|
+
error: err.message,
|
|
1365
|
+
code: err.code
|
|
1366
|
+
});
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
if (err.name === "FirebaseError" || err.name === "FirestoreError") {
|
|
1370
|
+
res.status(500).json({
|
|
1371
|
+
success: false,
|
|
1372
|
+
error: "Database error",
|
|
1373
|
+
code: "DATABASE_ERROR"
|
|
1374
|
+
});
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
res.status(500).json({
|
|
1378
|
+
success: false,
|
|
1379
|
+
error: "Internal server error",
|
|
1380
|
+
code: "INTERNAL_ERROR"
|
|
1381
|
+
});
|
|
1382
|
+
};
|
|
1383
|
+
var asyncHandler = (fn) => (req, res, next) => {
|
|
1384
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
1385
|
+
};
|
|
1386
|
+
var notFoundHandler = (req, res) => {
|
|
1387
|
+
res.status(404).json({
|
|
1388
|
+
success: false,
|
|
1389
|
+
error: `Route ${req.method} ${req.path} not found`,
|
|
1390
|
+
code: "ROUTE_NOT_FOUND"
|
|
1391
|
+
});
|
|
1392
|
+
};
|
|
1393
|
+
var validateBody = (schema) => async (req, res, next) => {
|
|
1394
|
+
try {
|
|
1395
|
+
req.body = await schema.parseAsync(req.body);
|
|
1396
|
+
next();
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
if (error instanceof ZodError) {
|
|
1399
|
+
res.status(400).json({
|
|
1400
|
+
success: false,
|
|
1401
|
+
error: "Validation failed",
|
|
1402
|
+
code: "VALIDATION_ERROR",
|
|
1403
|
+
details: error.errors.map((e) => ({
|
|
1404
|
+
path: e.path.join("."),
|
|
1405
|
+
message: e.message
|
|
1406
|
+
}))
|
|
1407
|
+
});
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
next(error);
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
var validateQuery = (schema) => async (req, res, next) => {
|
|
1414
|
+
try {
|
|
1415
|
+
req.query = await schema.parseAsync(req.query);
|
|
1416
|
+
next();
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
if (error instanceof ZodError) {
|
|
1419
|
+
res.status(400).json({
|
|
1420
|
+
success: false,
|
|
1421
|
+
error: "Invalid query parameters",
|
|
1422
|
+
code: "VALIDATION_ERROR",
|
|
1423
|
+
details: error.errors.map((e) => ({
|
|
1424
|
+
path: e.path.join("."),
|
|
1425
|
+
message: e.message
|
|
1426
|
+
}))
|
|
1427
|
+
});
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
next(error);
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
var validateParams = (schema) => async (req, res, next) => {
|
|
1434
|
+
try {
|
|
1435
|
+
req.params = await schema.parseAsync(req.params);
|
|
1436
|
+
next();
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
if (error instanceof ZodError) {
|
|
1439
|
+
res.status(400).json({
|
|
1440
|
+
success: false,
|
|
1441
|
+
error: "Invalid path parameters",
|
|
1442
|
+
code: "VALIDATION_ERROR",
|
|
1443
|
+
details: error.errors.map((e) => ({
|
|
1444
|
+
path: e.path.join("."),
|
|
1445
|
+
message: e.message
|
|
1446
|
+
}))
|
|
1447
|
+
});
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
next(error);
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
// src/middleware/authenticateFirebase.ts
|
|
1455
|
+
var BEARER_PREFIX = "Bearer ";
|
|
1456
|
+
var DEV_USER = {
|
|
1457
|
+
uid: "dev-user-001",
|
|
1458
|
+
email: "dev@localhost",
|
|
1459
|
+
email_verified: true,
|
|
1460
|
+
aud: "dev-project",
|
|
1461
|
+
auth_time: Math.floor(Date.now() / 1e3),
|
|
1462
|
+
exp: Math.floor(Date.now() / 1e3) + 3600,
|
|
1463
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
1464
|
+
iss: "https://securetoken.google.com/dev-project",
|
|
1465
|
+
sub: "dev-user-001",
|
|
1466
|
+
firebase: {
|
|
1467
|
+
identities: {},
|
|
1468
|
+
sign_in_provider: "custom"
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
async function authenticateFirebase(req, res, next) {
|
|
1472
|
+
const authorization = req.headers.authorization;
|
|
1473
|
+
if (env.NODE_ENV === "development" && (!authorization || !authorization.startsWith(BEARER_PREFIX))) {
|
|
1474
|
+
req.firebaseUser = DEV_USER;
|
|
1475
|
+
res.locals.firebaseUser = DEV_USER;
|
|
1476
|
+
return next();
|
|
1477
|
+
}
|
|
1478
|
+
try {
|
|
1479
|
+
if (!authorization || !authorization.startsWith(BEARER_PREFIX)) {
|
|
1480
|
+
return res.status(401).json({ error: "Authorization header missing or malformed" });
|
|
1481
|
+
}
|
|
1482
|
+
const token = authorization.slice(BEARER_PREFIX.length);
|
|
1483
|
+
const decodedToken = await getAuth().verifyIdToken(token);
|
|
1484
|
+
req.firebaseUser = decodedToken;
|
|
1485
|
+
res.locals.firebaseUser = decodedToken;
|
|
1486
|
+
return next();
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
console.error("Firebase authentication failed:", error);
|
|
1489
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1476
1493
|
// src/stores/firestoreFormat.ts
|
|
1477
1494
|
function toFirestoreFormat(schema) {
|
|
1478
1495
|
const data = { ...schema };
|