@gnar-engine/core 1.0.22 → 1.0.23

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 (2) hide show
  1. package/dist/core.js +1 -1
  2. package/package.json +1 -1
package/dist/core.js CHANGED
@@ -1 +1 @@
1
- import ae from"fastify";import ie from"@fastify/cors";import ce from"@fastify/rate-limit";import"pino";var n={cloudProjectId:"",serviceName:"",logs:[],transports:[],flushIntervalMs:5e3,timer:null,init:({cloudProjectId:e,serviceName:t,transports:o,flushIntervalMs:r})=>{if(n.cloudProjectId=e||"",!t)throw new Error("Service name is required for logger initialization");n.serviceName=t,n.transports=o||[],n.flushIntervalMs=r||5e3,n.startFlushTimer()},info:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"info"})},warning:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"warning"})},error:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"error"})},table:(e,t,o,r)=>{n.addLog({args1:e,args2:t,args3:o,args4:r,level:"table"})},testResult:(e,t,o)=>{n.addLog({args1:o,level:"test_result",testResult:t,test:e})},addTransport:e=>{},addLog:({args1:e,args2:t,args3:o,args4:r,level:s,testResult:a,test:i})=>{let d=[e,t,o,r].filter(h=>h!==void 0).map(h=>{if(typeof h=="object")try{return JSON.stringify(h)}catch{return"[Object]"}else return String(h)}).join(" "),m={cloudProjectId:n.cloudProjectId||null,service:n.serviceName,timestamp:Date.now(),level:s||"info",message:d,testResult:a||null,test:i||null};if(n.transports.length==0)if(m.level==="table"){try{let h=JSON.parse(m.message);console.table(h)}catch{console.log("[table] unable to parse table data:",m.message)}return}else console.log(n.formatForConsole(m));else n.logs.push(m)},startFlushTimer(){n.timer&&clearInterval(n.timer),n.timer=setInterval(()=>n.flush(),n.flushIntervalMs)},flush:()=>{n.transports.forEach(e=>{}),n.logs=[]},formatForConsole:e=>{let o={error:"\x1B[31m",warning:"\x1B[33m",reset:"\x1B[0m"},r=new Date(e.timestamp).toISOString(),a=(e.level||"info").toUpperCase().padEnd(12),i=`${r} ${a} \u2014 ${e.message}`,c=o[e.level]||"",d=c?o.reset:"";return`${c}${i}${d}`}};var E=null,B=3e3,N={serviceName:"Unnamed",init:({config:e,serviceName:t})=>{E=ae({}),B=e.port||3e3,N.serviceName=t||N.serviceName,E.register(ie,{origin:e.allowedOrigins||[],methods:e.allowedMethods||["GET","POST","PUT","DELETE"],allowedHeaders:e.allowedHeaders||["Content-Type","Authorization"],preflight:!0,optionsSuccessStatus:204,credentials:!0}),e?.rateLimiting?.max&&e?.rateLimiting?.timeWindow&&E.register(ce,e.rateLimiting)},registerRoutes:({controllers:e})=>{e.forEach(t=>{Object.values(t).forEach(o=>{E.route(o)})}),n.info(`${N.serviceName} service registered http routes`+E.printRoutes())},addHook:(e,t)=>{E.addHook(e,t)},start:async()=>{try{await E.listen({port:B,host:"0.0.0.0"}),n.info(`HTTP server started on port ${B}`)}catch(e){n.error("Error starting HTTP server: "+e),process.exit(1)}},setErrorHandler:e=>{E.setErrorHandler(e)}};import le from"amqplib";var D="",C=null,M=null,U={arguments:{"x-queue-type":"quorum"}},j=e=>{D=e},k=async()=>{if(C)return C;try{console.log("Connecting to RabbitMQ:",D),C=await le.connect(D)}catch(e){return console.log("Error connecting to RabbitMQ:",e),C=null,await new Promise(t=>setTimeout(t,3e3)),k({rabbitConnectionUrl:D})}return C},S=async()=>{if(M)return M;C||await k();try{M=await C.createChannel()}catch(e){return console.error("Error creating RabbitMQ channel:",e),M=null,await new Promise(t=>setTimeout(t,3e3)),S()}return M},K=async(e,t,o)=>{let r=await S();return await r.assertQueue(e,U),await r.prefetch(t),(await r.consume(e,async a=>{a&&(async()=>{try{await o(a,r)}catch(i){console.error("Error handling message:",i),r.nack(a,!1,!1)}})()})).consumerTag},de=async(e,t,o,r)=>{await k(),await S();let s=await S();return await s.assertQueue(e,U),await s.prefetch(t),r&&await s.cancel(r),K(e,t,o)},Q=async(e,t,o)=>{await k();let r=await S();await r.assertQueue(e,U),await r.prefetch(t);let s=await K(e,t,o);return C.on("close",async()=>{console.log("RabbitMQ connection closed, attempting to reconnect..."),await z(e,t,o,s)}),console.log("RabbitMQ connection established and consumer registered:",s),s},z=async(e,t,o,r)=>{console.log("Reconnecting to RabbitMQ...");let s=de(e,t,o,r);return console.log("RabbitMQ connection re-established and consumer registered:",s),s},J={getChannel:S,getConnection:k,setConnectionUrl:j,initialize:Q,reinitialize:z};var G=async(e,t)=>{let o=`${e}Queue`,r;try{r=await S(),await r.assertQueue(o,{arguments:{"x-queue-type":"quorum"}});let s=me(),a=s,i=await r.assertQueue("",{exclusive:!0});return new Promise((c,d)=>{let m=setTimeout(()=>{h(),d(new Error(`No response from ${e} service`))},5e3),h=()=>{r.cancel(a).catch(()=>{}),r.deleteQueue(i.queue).catch(()=>{}),clearTimeout(m)};r.consume(i.queue,$=>{$.properties.correlationId===s&&(h(),c(JSON.parse($.content.toString())),r.ack($))},{noAck:!1,consumerTag:a}).catch(d),r.sendToQueue(o,Buffer.from(JSON.stringify(t)),{replyTo:i.queue,correlationId:s})})}catch(s){if(r)try{await r.close()}catch(a){console.error("Failed to close channel:",a)}throw new Error(`Failed to send message to ${e}: ${s.message}`)}},L=async(e,t)=>{let o=`${e}Queue`,r;try{r=await S(),await r.assertQueue(o,{arguments:{"x-queue-type":"quorum"}}),r.sendToQueue(o,Buffer.from(JSON.stringify(t)))}catch(s){if(r)try{await r.close()}catch(a){console.error("Failed to close channel:",a)}throw new Error(`Failed to send message to ${e}: ${s.message}`)}},me=()=>Math.random().toString(36).substring(7);import R,{WebSocketServer as ue}from"ws";import{v4 as he}from"uuid";import V from"os";var F={wsConnections:{},pendingCalls:[],async init(e,t){let o=V.hostname();if(e.serviceName=t,e.replicaSlot=o.split(".")[1],e.replicaId=o.split(".")[2],e.hostname=o,e.ip=(await V.networkInterfaces().eth0.find(r=>r.family==="IPv4")).address,this.startServer(e),e.serviceName!=="controlService"){let r,s={};for(;!r;)try{r=await this.connectToControlService(e)}catch(a){n.error("Failed to connect to control service, retrying...",a.message),await new Promise(i=>setTimeout(i,2e3))}(async()=>{for(;;){await new Promise(i=>r.once("close",i));let a;for(;!a;)try{a=await this.connectToControlService(e),setTimeout(async()=>{await w.execute("controlService.registerServiceAndReplica",{serviceName:e.serviceName,replicaSlot:e.replicaSlot,replicaHostname:e.hostname,replicaIp:e.ip})},1e3)}catch(i){n.error("Failed to reconnect to control service, retrying...",i.message),await new Promise(c=>setTimeout(c,2e3))}r=a}})();try{n.info("registering with",e.serviceName,e.replicaSlot,e.hostname,e.ip),await w.execute("controlService.registerServiceAndReplica",{serviceName:e.serviceName,replicaSlot:e.replicaSlot,replicaHostname:e.hostname,replicaIp:e.ip})}catch(a){throw n.error(`Failed to register service ${e.serviceName} with control service: ${a.message}`),a}try{s=await w.execute("controlService.newPeerSet",{requestingHostname:e.hostname}),n.info("Peers",s)}catch(a){throw n.error("Failed to get peer set. "+a.message),a}if(s&&Object.keys(s).length>0){await Promise.all(Object.entries(s).map(async([i,c])=>{!c||c?.serviceName===e.serviceName||await this.connect({peer:c,config:e})}));let a=!1;for(;!a;){await new Promise(c=>setTimeout(c,2e3)),n.info("Checking for missing peer connections...");for(let[c,d]of Object.entries(s))if(!(d.hostname||c==e.serviceName||this.wsConnections[c]&&Object.keys(this.wsConnections[c]).length>0))try{n.info("Missing "+c+" peer connection - requesting new peer from control service.",c+" / ",e.serviceName);let m=await w.execute("controlService.newPeerForService",{requestingHostname:e.hostname,serviceName:c});s[c]=m,await this.connect({peer:m,config:e})}catch(m){n.error(`Failed to get and connect to new peer for ${c}: ${m.message}`)}if(Object.entries(s).every(([c,d])=>{if(d.hostname||c==e.serviceName||this.wsConnections[c]&&Object.keys(this.wsConnections[c]).length>0)return!0;if(d.hostname)n.info(`Still missing peer connection for ${c}, due to ${d}`);else return n.error("Peer information is missing for failed connection. This can occur if a service is down for a prolonged period. This service will now wait for an inbound connection."),!0})){a=!0;break}}n.info("Initial peer connections established to: ",Object.keys(s))}}},startServer(e,t=5e3){new ue({port:t}).on("connection",(r,s)=>{let{serviceName:a,serviceHostname:i}=this.identifyPeer(s);this.wsConnections[a]||(this.wsConnections[a]={}),this.wsConnections[a][i]=r,n.info(`Peer connected (inbound): ${a} (${i})`),r.on("message",c=>{this.handleMessage({serviceName:a,hostname:i,ws:r,raw:c})}),r.on("close",()=>{n.info(`Peer disconnected: ${a}`),this.wsConnections[a]&&delete this.wsConnections[a][i],e.serviceName=="controlService"&&w.execute("controlService.removeServiceReplica",{serviceName:a,replicaHostname:i})})})},async connectToControlService(e){return new Promise((t,o)=>{try{let r="ws://control-service:5000",s=new R(r,{headers:{"x-api-key":"my-secret-key","x-service-name":e.serviceName,"x-service-hostname":e.hostname}});s.on("open",()=>{n.info("Connected to control service"),this.wsConnections.controlService||(this.wsConnections.controlService={}),this.wsConnections.controlService.controlService=s,s.on("message",a=>{this.handleMessage({serviceName:"controlService",hostname:"controlService",ws:s,raw:a})}),t(s)}),s.on("close",()=>{n.error("Control service disconnected"),delete this.wsConnections.controlService?.controlService,o(new Error("Control service connection closed"))}),s.on("error",a=>{n.error(`WS error with control service: ${a.message}`),delete this.wsConnections.controlService?.controlService,o(a)})}catch(r){o(r)}})},async connect({peer:e,config:t}){if(!(e.name==t.serviceName||e.name==="controlService")){this.wsConnections[e.name]||(this.wsConnections[e.name]={}),this.wsConnections[e.name][e.hostname]=null;try{let o=`ws://${e.ip}:5000`,r=new R(o,{headers:{"x-api-key":"my-secret-key","x-service-name":t.serviceName,"x-service-hostname":t.hostname}});r.on("open",()=>{n.info(`Connected to peer ${e.hostname} at ${o}`),this.wsConnections[e.name][e.hostname]=r,r.on("message",s=>{this.handleMessage({serviceName:e.name,hostname:e.hostname,ws:r,raw:s})})}),r.on("close",()=>{n.info(`Peer ${e.name} at ${e.hostname} disconnected`),delete this.wsConnections[e.name][e.hostname],this.handlePeerFailure(e,t)}),r.on("error",s=>{n.error(`WS error with ${e.hostname}: ${s.message}`),delete this.wsConnections[e.name][e.hostname],this.handlePeerFailure(e,t)})}catch(o){n.error(`Error during WS connection to ${e.hostname}: ${o.message}`)}}},async handlePeerFailure(e,t){try{if(t.serviceName==="controlService")return;let o=!1;this.wsConnections[e.name]?.length<1&&(o=!0),setTimeout(async()=>{try{if(n.info("Reporting peer connection failure to control service",e),!e.hostname)throw new Error("Peer information is missing for failed connection. This can occur if a service is down for a prolonged period. This service will now wait for an inbound connection.");let r=await w.execute("controlService.peerConnectionDropped",{requestingHostname:t.hostname,requestingServicename:t.serviceName,droppedPeerHostname:e.hostname,getNewPeer:o});r?.hostname&&this.connect({peer:r,config:t})}catch(r){n.error(`Failed to report peer connection failure to control service for ${e.hostname}: ${r.message}`)}},3e3)}catch(o){n.error(`Failed to report peer connection failure to control service for ${e.hostname}: ${o.message}`)}},async handleMessage({serviceName:e,hostname:t,ws:o,raw:r}){let s;try{s=JSON.parse(r)}catch{n.error("Invalid JSON from peer:",r);return}if(s.type==="request"&&s.commandName)try{let a=await w.execute(s.commandName,s.payload);this.sendResponse({ws:o,messageId:s.messageId,response:a})}catch(a){this.sendResponse({ws:o,messageId:s.messageId,error:a.message})}else if(s.type==="response"){let a=this.pendingCalls.findIndex(c=>c.messageId===s.messageId);if(a===-1){n.error(`No pending call for messageId ${s.messageId}`);return}let i=this.pendingCalls.splice(a,1)[0];clearTimeout(i.timeout),s.error?i.reject(new Error("RPC rejected: "+s.error)):i.resolve(s.response)}else n.error("Unknown message type",s)},async send(e,t,o,r=1e4){let s=this.wsConnections[e];if(!s||!Object.keys(s)||Object.keys(s).length===0)throw new Error(`No replicas connected for service ${e}`);let a=Object.keys(s)[Math.floor(Math.random()*Object.keys(s).length)],i=s[a];if((!i||i.readyState!==R.OPEN)&&(i=await this.waitForWsReady({ws:i,serviceName:e})),!i||i.readyState!==R.OPEN)throw new Error(`WebSocket not connected to ${e}`);let c=he(),d={type:"request",messageId:c,commandName:t,payload:o};return i.send(JSON.stringify(d)),new Promise((m,h)=>{let $=setTimeout(()=>{this.pendingCalls=this.pendingCalls.filter(ne=>ne.messageId!==c),h(new Error(`Timeout waiting for response from ${e}`)),n.error(`Timeout waiting for response on call: ${JSON.stringify(d)}`)},r);this.pendingCalls.push({messageId:c,resolve:m,reject:h,timeout:$})})},sendResponse({ws:e,messageId:t,response:o=null,error:r=null}){!e||e.readyState!==R.OPEN||e.send(JSON.stringify({type:"response",messageId:t,response:o,error:r}))},identifyPeer(e){return{serviceHostname:e.headers["x-service-hostname"]||`unknown-${Date.now()}`,serviceName:e.headers["x-service-name"]||`unknown-${Date.now()}`}},async waitForWsReady({ws:e,serviceName:t,waitInterval:o=50,maxWait:r=5e3}){let s=Date.now();for(;;){if(e&&e.readyState===R.OPEN)return e;if(Date.now()-s>r)throw new Error(`WebSocket not connected to ${t} after ${r}ms`);await new Promise(a=>setTimeout(a,o))}}};var y={manifest:{commandList:[],commandImplementations:{},schemas:{}},addCommand({commandName:e,handlerFunction:t}){y.manifest.commandImplementations[e]={function:t.toString()},y.manifest.commandList.push(e)},addSchema({schemaName:e,schema:t}){y.manifest.schemas[e]=t}};var w={config:{},handlers:new Map,init(e){this.config=e},register(e,t){this.handlers.set(e,t),y.addCommand({commandName:e,handlerFunction:t})},async execute(e,t,o={}){let{fireAndForget:r=!1}=o;if(this.config.architecture=="modular-monolith"){let s=this.handlers.get(e);if(!s)throw new Error(`Command "${e}" not registered`);return await s(t)}else{let[s,a]=e.split(".");if(!s||!a){let c=this.config.serviceName+"."+e,d=this.handlers.get(c);if(!d)throw new Error(`Command "${c}" not registered`);return await d(t)}if(s===this.config.serviceName){let c=this.handlers.get(e);if(!c)throw new Error(`Command "${e}" not registered`);return await c(t)}if(r){L(s,{method:e,data:t});return}return await F.send(s,e,t)}}};var Y={routeHandlers:{},defaultHandlers:{runMigrations:async e=>{let t=e.data?.migration;await w.execute("runMigrations",{migration:t})},runSeeders:async e=>{let t=e.data?.seeder;await w.execute("runSeeders",{seeder:t})},healthCheck:async()=>({status:"ok"})},async init({config:e,handlers:t={}}){if(!e?.queueName)throw new Error("Queue name is required for message controller initialization");this.routeHandlers={...this.defaultHandlers,...t},await Q(e.queueName,e.prefetch||20,this.handleMessage.bind(this))},async handleMessage(e,t){if(!e)return;let o;try{o=JSON.parse(e.content.toString())}catch(i){return n.error("Invalid JSON message received:",i),t.ack(e)}let r=o.method;r.includes(".")&&(r=r.split(".").slice(1).join("."));let s={correlationId:e.properties.correlationId},a=this.routeHandlers[r];if(!a)return t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify({error:"Method not found"})),s),t.ack(e);try{let i=await a(o,e,t);e.properties.replyTo&&t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify(i??{ok:!0})),s)}catch(i){e.properties.replyTo&&t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify({error:i.message})),s)}finally{t.ack(e)}}};import{MongoClient as pe}from"mongodb";import X from"mysql2/promise";var u=null,_="",fe=5e3,H=async e=>{if(u)return u;_=e.type;try{switch(e.type){case"mongodb":return await we({host:e.host,user:e.user,password:e.password,database:e.database,port:e.port,connectionArgs:e.connectionArgs});case"mysql":return await ge({host:e.host,user:e.user,password:e.password,database:e.database,connectionLimit:e.connectionLimit,queueLimit:e.queueLimit});default:throw new Error("Unsupported database type: "+e.type)}}catch(t){throw n.error("Error initializing database connection: "+t.message),t}},we=async({host:e,user:t,password:o,database:r,port:s=27017,connectionArgs:a={}})=>{try{n.info("Connecting to mongo..");let i=`mongodb://${t}:${encodeURIComponent(o)}@${e}:${s}/${r}`;return n.info(`MongoDB connection URL: ${i}`),u=(await pe.connect(i,a)).db(),n.info("MongoDB connected successfully"),u}catch(i){throw n.error("MongoDB connection error: "+i),i}},ge=async({host:e,user:t,password:o,database:r,connectionLimit:s=10,queueLimit:a=20,maxRetries:i=5})=>{if(u)return u;let c=0;for(;c<i;)try{return await ye({host:e,user:t,password:o,database:r}),n.info("Establishing new MySQL pool..."),u=await X.createPool({host:e,user:t,password:o,database:r,waitForConnections:!0,connectionLimit:s,queueLimit:a,enableKeepAlive:!0,keepAliveInitialDelay:0}),setInterval(async()=>{try{await u.query("SELECT 1")}catch(d){n.error("MySQL keep-alive query failed: "+d.message),u=null,c=0}},6e4),n.info("MySQL pool established successfully"),u}catch(d){if(n.error(`MySQL connection attempt ${c+1} failed: ${d.message}`),c++,c>=i)throw n.error("Max retries reached. Could not connect to MySQL."),new Error("Could not connect to the database after multiple attempts");await new Promise(m=>setTimeout(m,fe))}},ye=async({host:e,user:t,password:o,database:r})=>{if(n.info(`[assertDbExists] Attempting to connect to MySQL on host ${e} as ${t}`),n.info(`[assertDbExists] Target DB: ${r}`),!r||!e||!t||!o)throw n.error("[assertDbExists] Missing required variables: database, host, user, or password"),new Error("Missing required variables for DB connection");try{let s=await X.createConnection({host:e,user:t,password:o});n.info(`[assertDbExists] Connected to MySQL, asserting database '${r}'`);let[a]=await s.query(`CREATE DATABASE IF NOT EXISTS \`${r}\``);await s.end()}catch(s){throw n.error("[assertDbExists] \u274C Error asserting database existence"),n.error(s.stack||s.message||s),s}},Z=async()=>{if(!u)throw new Error("Database connection not initialized");try{return!0}catch(e){throw e}},ee=async()=>{if(!u)throw new Error("Database connection not initialized");try{_==="mongodb"?await u.dropDatabase():_==="mysql"&&await W()}catch(e){console.error("Error dropping database data: "+e.message)}};var W=async()=>{if(!u)throw new Error("Database connection not initialized");if(P.environment!=="test"&&P.environment!=="development")throw new Error("Cannot reset mysql database outside of test or development mode!");try{let[e]=await u.query("SHOW TABLES");await u.query("SET FOREIGN_KEY_CHECKS = 0");for(let t of e){let o=Object.values(t)[0];await u.query(`DROP TABLE \`${o}\``)}await u.query("SET FOREIGN_KEY_CHECKS = 1"),console.log("MySQL database reset successfully")}catch(e){throw console.error("Error resetting MySQL database: "+e.message),e}};import ve from"fs";import Se from"path";var b={config:null,runSeeders:async({config:e,seeder:t})=>{try{if(e.environment=="test"&&!t)return;n.info("Running seeders"),b.config=e;let o,r;try{if(t?o=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/seeders/"+t:o=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/seeders/"+e.environment,r=(await ve.promises.readdir(o)).sort(),r.length==0){n.info("No seeders found for environment: "+e.environment);return}}catch{n.info("No seeders found for environment: "+e.environment);return}for(let s of r){let i=await import(Se.join(o,s));if(await b.checkSeederAlreadyRun(s))n.info("Seeder already run: "+s);else try{n.info("Running seeder: "+s),await i.up(),await b.markSeederAsRun(s)}catch(c){throw n.error("Error running seeder "+s+": "+c),c}}n.info("Seeders completed successfully")}catch(o){throw n.error("Error running seeders: "+o),o}},checkSeederAlreadyRun:async e=>{let t=[];if(b.config.db.type=="mysql"){let[o]=await u.query('SHOW TABLES LIKE "seeders"');if(o.length==0)return!1;let[r]=await u.query("SELECT * FROM seeders WHERE name = ?",[e]);t=r}else t=await u.collection("seeders").find({name:e}).toArray();return t.length!=0},markSeederAsRun:async e=>{b.config.db.type=="mysql"?await u.execute("INSERT INTO seeders (name) VALUES (?)",[e]):await u.collection("seeders").insertOne({name:e,runAt:new Date})}};var te=async({config:e})=>{if(e&&e.db&&e.db.type)try{await H()}catch(t){n.error("[Internal health check] Failed - Exiting. Error connecting to MongoDB: "+t),process.exit(1)}};process.env.NODE_ENV!=="development"&&(process.on("uncaughtException",e=>{n.error("Uncaught Exception: "+e),process.exit(1)}),process.on("unhandledRejection",(e,t)=>{n.error("Unhandled Rejection at: "+t+". Reason: "+e),process.exit(1)}));var re=e=>{e.setErrorHandler((t,o,r)=>{if(t.validation){if(r&&typeof r.send=="function")return r.code(400).send({statusCode:400,error:"Bad Request",message:t.message});throw new Error("Bad Request: "+t.message)}if(t instanceof q){if(r&&typeof r.send=="function")return r.code(404).send({statusCode:404,error:"Not Found",message:t.message});throw new Error("Not found: "+t.message)}if(t instanceof O){if(r&&typeof r.code=="function"&&typeof r.send=="function")return r.code(400).send({statusCode:400,error:"Bad Request",message:t.message});throw new Error("Bad Request: "+t.message)}if(t instanceof A){if(r)return r.code(401).send({statusCode:401,error:"Unauthorized",message:t.message});throw new Error("Unauthorized: "+t.message)}if(t.statusCode===429){if(r)return r.code(429).send({statusCode:429,error:"Too Many Requests",message:"You have exceeded the request limit."});throw new Error("Too Many Requests: You have exceeded the request limit.")}if(t instanceof I){if(r)throw new Error("Failed Health Check: "+t.message);return r.code(500).send({statusCode:500,error:"Failed Health Check",message:t.message})}if(t instanceof Error||(t=new Error(typeof t=="string"?t:JSON.stringify(t))),n.error(t.stack||t.toString()),r)return r.code(500).send({statusCode:500,error:"Internal Server Error",message:"Something went wrong",details:t?.message||t?.toString()||"No additional error information available"});throw new Error("Internal Server Error: "+t.message)})},q=class extends Error{constructor(t="Resource not found"){super(t),this.name="NotFoundError",this.statusCode=404}},O=class extends Error{constructor(t="Bad request"){super(t),this.name="BadRequestError",this.statusCode=400}},A=class extends Error{constructor(t="Unauthorised"){super(t),this.name="UnauthorisedError",this.statusCode=401}},I=class extends Error{constructor(t="Failed health check"){super(t),this.name="FailedHealthCheckError",this.statusCode=500}};var v={db:{},init(e){v.db=e},async syncManyToMany({table:e,parentId:t,childIds:o,parentColumn:r,childColumn:s}){try{o=Array.from(new Set(o||[])).map(String);let[a]=await v.db.query(`SELECT ${s} FROM ${e} WHERE ${r} = ?`,[t]),i=a.map(m=>{let h=m[s];return Buffer.isBuffer(h)?h.toString("hex"):String(h)}),c=o.filter(m=>!i.includes(m)),d=i.filter(m=>!o.includes(m));if(d.length&&await v.db.query(`DELETE FROM ${e} WHERE ${r} = ? AND ${s} IN (?)`,[t,d]),c.length){let m=c.map(h=>[t,h]);await v.db.query(`INSERT IGNORE INTO ${e} (${r}, ${s}) VALUES ?`,[m])}return{added:c,removed:d}}catch(a){throw n.error("Error syncing many-to-many relationship: "+a.message),a}},async syncOneToMany({table:e,parentId:t,childIds:o,parentColumn:r,childIdColumn:s="id"}){try{Array.isArray(o)||(o=[]),o=Array.from(new Set(o)).map(String);let[a]=await v.db.query(`SELECT ${s} FROM ${e} WHERE ${r} = ?`,[t]),i=a.map(m=>{let h=m[s];return Buffer.isBuffer(h)?h.toString("hex"):String(h)});console.log("currentIds:",i),console.log("childIds:",o);let c=o.filter(m=>!i.includes(m)),d=i.filter(m=>!o.includes(m));return d.length&&await v.db.query(`UPDATE ${e} SET ${r} = NULL WHERE ${s} IN (?)`,[d]),c.length&&await v.db.query(`UPDATE ${e} SET ${r} = ? WHERE ${s} IN (?)`,[t,c]),{linked:c,unlinked:d}}catch(a){throw n.error("Error syncing one-to-many relationship: "+a.message),a}},toSnake(e){return e.replace(/[A-Z]/g,t=>`_${t.toLowerCase()}`)},objectToCamelCase(e){let t={};for(let o in e){let r=o.replace(/_([a-z])/g,s=>s[1].toUpperCase());t[r]=e[o]}return t}};import be from"fs";import Ee from"path";var x={config:null,runMigrations:async({config:e})=>{try{if(e.db.type!=="mysql"){n.info("Migrations only supported for MySQL");return}(e.resetDatabase&&e.environment=="development"||e.environment=="test")&&(n.info("Resetting database..."),await W()),n.info("Running migrations"),x.config=e;let t=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/migrations",o=(await be.promises.readdir(t)).sort();for(let r of o){let a=await import(Ee.join(t,r));await x.checkMigrationAlreadyRun(r)?n.info("Migration already run: "+r):(n.info("Running migration: "+r),await a.up(),await x.markMigrationAsRun(r))}n.info("Migrations completed successfully")}catch(t){throw n.error("Error running migrations: "+t),t}},checkMigrationAlreadyRun:async e=>{let[t]=await u.query('SHOW TABLES LIKE "migrations"');if(t.length==0)return!1;if(e=="01-init.js")return!0;let[o]=await u.query("SELECT * FROM migrations WHERE name = ?",[e]);return o.length!=0},markMigrationAsRun:async e=>{await u.execute("INSERT INTO migrations (name) VALUES (?)",[e])}};import Ce from"ajv";import $e from"ajv-formats";var T=new Ce({allErrors:!0,useDefaults:!0});$e(T);T.addFormat("mysql-date",/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);var Re={compile:e=>{y.addSchema(e);let t=T.compile(e.schema);return o=>{if(!t(o)){let s=[];return t.errors.map(a=>{s.push(a.instancePath+" "+a.message)}),{errors:s.join(", ")}}return!1}},addFormat:(e,t)=>{T.addFormat(e,t)},addKeyword:(e,t)=>{T.addKeyword(e,t)},addSchema:e=>{y.addSchema(e),T.addSchema(e.schema,e.$id)}},oe=Re;import xe from"assert";import Te from"fs";import Ne from"path";var p={testsDirectory:`${process.env.GLOBAL_SERVICE_BASE_DIR}/tests`,failed:0,beforeEachFns:[],afterEachFns:[],tests:[],testResults:[],beforeEach:e=>{p.beforeEachFns.push(e)},run:(e,t)=>{p.tests.push({name:e,fn:t})},afterEach:e=>{p.afterEachFns.push(e)},assert:xe,runCommandTests:async({config:e})=>{console.log("=============================="),console.log("Running command tests..."),p.testsDirectory+=`/${e.runTests||"ephemeral"}`;let t=Te.readdirSync(p.testsDirectory).filter(o=>o.endsWith(".js")).sort();for(let o of t){p.prepFns=[],p.tests=[];let r=Ne.join(p.testsDirectory,o);try{await import(r)}catch(a){p.failed++,console.error(`\u274C Failed to load ${o}: ${a.message}`);continue}let s=0;for(let a of p.tests){s++;for(let i of p.beforeEachFns)try{await i()}catch(c){p.failed++,console.error(`\u274C Test preparation failed - ${c.message}`);break}try{await a.fn(),p.testResults.push({name:a.name,error:null})}catch(i){p.failed++,p.testResults.push({name:a.name,error:i})}for(let i of p.afterEachFns)try{await i()}catch(c){p.failed++,console.error(`\u274C Test tidy up failed - ${c.message}`);break}}}console.table(p.testResults.map(o=>({Test:o.name,Result:o.error?`\u274C Failed - ${o.error.message}`:"\u2705 Passed"}))),p.failed>0?console.error(`\u274C Some Integration tests failed: ${p.failed} error(s)`):console.log("\u2705 All integration tests passed!")}};var f={client:null,bucket:"",uploadsUrl:"",init:async e=>{if(!e.bucket)throw new Error("S3 storage driver requires a bucket name.");if(!e.region)throw new Error("S3 storage driver requires a region.");if(!e.uploadsUrl)throw new Error("S3 storage driver requires an uploadsUrl.");let{S3Client:t,PutObjectCommand:o,GetObjectCommand:r,DeleteObjectCommand:s}=await import("@aws-sdk/client-s3");f.S3Client=t,f.PutObjectCommand=o,f.GetObjectCommand=r,f.DeleteObjectCommand=s,f.bucket=e.bucket,f.uploadsUrl=e.uploadsUrl,f.client=new t({region:e.region,credentials:{accessKeyId:e.accessKeyId,secretAccessKey:e.secretAccessKey}})},upload:async({file:e,key:t,contentType:o})=>{let r=new f.PutObjectCommand({Bucket:f.bucket,Key:t,Body:e,ContentType:o});return await f.client.send(r),t},download:async({key:e,stream:t})=>{let o=new f.GetObjectCommand({Bucket:f.bucket,Key:e}),r=await f.client.send(o);if(t)return r.Body;{let s=[];for await(let a of r.Body)s.push(a);return Buffer.concat(s)}},delete:async({key:e})=>{let t=new f.DeleteObjectCommand({Bucket:f.bucket,Key:e});await f.client.send(t)},getUrl:async({key:e})=>`${f.uploadsUrl}/${e}`};var g={driverName:"",driver:null,uploadsUrl:"",init:async e=>{try{if(!e.driver){n.info("No storage driver specified, skipping storage initialization.");return}switch(e.driver){case"s3":g.driverName="s3",g.driver=f;break;default:throw new Error(`Unsupported storage type: ${e.driver}`)}await g.driver.init(e),g.uploadsUrl=e.uploadsUrl||""}catch(t){throw n.error(`Storage initialization error: ${t.message}`),t}},upload:async({file:e,key:t,contentType:o,metadata:r})=>{try{let s=await g.driver.upload({file:e,key:t,contentType:o,metadata:r});return`${g.uploadsUrl}/${s}`}catch(s){throw n.error(`Storage upload error: ${s.message}`),s}},download:async({key:e,stream:t})=>{try{return await g.driver.download({key:e,stream:t})}catch(o){throw n.error(`Storage download error: ${o.message}`),o}},delete:async({key:e})=>{try{return await g.driver.delete({key:e})}catch(t){throw n.error(`Storage delete error: ${t.message}`),t}},getUrl:async({key:e,expiresIn:t})=>{try{return await g.driver.getUrl({key:e,expiresIn:t})}catch(o){throw n.error(`Storage getUrl error: ${o.message}`),o}}};import{v4 as Me}from"uuid";import{v5 as ke}from"uuid";import{CronExpressionParser as qe}from"cron-parser";import se from"bcrypt";var Oe=await import(process.env.GLOBAL_SERVICE_BASE_DIR+"config.js"),P=Oe.config,l={init:async e=>{if(l.config=e,e.http&&e.http.allowedMethods&&(l.http=N,await l.http.init({config:e.http,serviceName:e.serviceName})),l.commands=w,l.commands.init(e),e.db&&e.db.type)try{l.db=await H(e.db)}catch(t){n.error("Error connecting to database: "+t),process.exit(1)}if(l.db||(l.db={}),l.db.checkConnection=Z,l.db.migrations=x,l.db.seeders=b,v.init(l.db),l.db.sql={},l.db.sql.helpers=v,l.http&&l.http.addHook("onReady",async()=>{setInterval(()=>{w.execute("internalHealthCheck",{config:e})},6e4)}),l.http&&re(l.http),j(e.message?.url||""),l.message=Y,l.message.sendAwaitResponse=G,l.message.sendAndForget=L,l.webSockets=F,l.commands.register(`${e.serviceName}.runMigrations`,async()=>await x.runMigrations({config:e})),l.commands.register(`${e.serviceName}.runSeeders`,async({seeder:t})=>await b.runSeeders({config:e,seeder:t})),l.commands.register(`${e.serviceName}.internalHealthCheck`,async()=>await te({config:e})),l.commands.register(`${e.serviceName}.dropDatabaseData`,ee),l.schema=oe,l.logger=n,l.logger.init({cloudProjectId:e.cloud?.projectId||"",serviceName:e.serviceName,flushIntervalMs:e.cloud?.logger.flushIntervalMs||5e3,transports:e.cloud?.logger||[]}),l.error={notFound:q,badRequest:O,unauthorised:A,failedHealthCheck:I},l.utils={uuid:()=>Me(),hash:async t=>await se.hash(t,10),verifyHash:async(t,o,r="")=>typeof o=="string"&&o.startsWith("$2")?await se.compare(t,o):ke(t,r)===o,cronExpressionParser:qe},l.http&&l.http.addHook("onRequest",async(t,o)=>{let{url:r,method:s}=t;if(!r.endsWith("/")&&!r.includes(".")&&r!=="/"){let c=new URL(t.raw.url,`http://${t.headers.host}`);c.pathname+="/",t.raw.url=c.pathname+(c.search||"")}let a=t.raw.headers.authorization||"",i=a?a.split(" ")[1]:"";if(i){let c=await l.commands.execute("userService.getAuthenticatedUser",{token:i});c&&(t.user=c)}}),l.registerService=async()=>{if(e.serviceName!=="controlService")try{await l.commands.execute("controlService.registerService",{service:{name:e.serviceName,manifest:y.manifest}})}catch(t){l.logger.error(`Failed to register service ${e.serviceName} with control service: ${t.message}`)}},l.test=p,e.storage&&e.storage.driver)g.init(e.storage),l.storage=g;else{let t=()=>{throw new Error("Storage service not configured - please configure storage in config.js")};l.storage={upload:t,download:t,getUrl:t}}l.rabbit=J,l.http||(l.http={})}};await l.init(P);var yr=l,{commands:vr,http:Sr,message:br,db:Er,schema:Cr,logger:$r,error:Rr,utils:xr,registerService:Tr,webSockets:Nr,test:Mr,storage:kr,rabbit:qr}=l;export{vr as commands,P as config,Er as db,yr as default,Rr as error,Sr as http,$r as logger,br as message,qr as rabbit,Tr as registerService,Cr as schema,kr as storage,Mr as test,xr as utils,Nr as webSockets};
1
+ import ae from"fastify";import ie from"@fastify/cors";import ce from"@fastify/rate-limit";import"pino";var n={cloudProjectId:"",serviceName:"",logs:[],transports:[],flushIntervalMs:5e3,timer:null,init:({cloudProjectId:e,serviceName:t,transports:s,flushIntervalMs:r})=>{if(n.cloudProjectId=e||"",!t)throw new Error("Service name is required for logger initialization");n.serviceName=t,n.transports=s||[],n.flushIntervalMs=r||5e3,n.startFlushTimer()},info:(e,t,s,r)=>{n.addLog({args1:e,args2:t,args3:s,args4:r,level:"info"})},warning:(e,t,s,r)=>{n.addLog({args1:e,args2:t,args3:s,args4:r,level:"warning"})},error:(e,t,s,r)=>{n.addLog({args1:e,args2:t,args3:s,args4:r,level:"error"})},table:(e,t,s,r)=>{n.addLog({args1:e,args2:t,args3:s,args4:r,level:"table"})},testResult:(e,t,s)=>{n.addLog({args1:s,level:"test_result",testResult:t,test:e})},addTransport:e=>{},addLog:({args1:e,args2:t,args3:s,args4:r,level:o,testResult:a,test:i})=>{let d=[e,t,s,r].filter(h=>h!==void 0).map(h=>{if(typeof h=="object")try{return JSON.stringify(h)}catch{return"[Object]"}else return String(h)}).join(" "),m={cloudProjectId:n.cloudProjectId||null,service:n.serviceName,timestamp:Date.now(),level:o||"info",message:d,testResult:a||null,test:i||null};if(n.transports.length==0)if(m.level==="table"){try{let h=JSON.parse(m.message);console.table(h)}catch{console.log("[table] unable to parse table data:",m.message)}return}else console.log(n.formatForConsole(m));else n.logs.push(m)},startFlushTimer(){n.timer&&clearInterval(n.timer),n.timer=setInterval(()=>n.flush(),n.flushIntervalMs)},flush:()=>{n.transports.forEach(e=>{}),n.logs=[]},formatForConsole:e=>{let s={error:"\x1B[31m",warning:"\x1B[33m",reset:"\x1B[0m"},r=new Date(e.timestamp).toISOString(),a=(e.level||"info").toUpperCase().padEnd(12),i=`${r} ${a} \u2014 ${e.message}`,c=s[e.level]||"",d=c?s.reset:"";return`${c}${i}${d}`}};var E=null,B=3e3,N={serviceName:"Unnamed",init:({config:e,serviceName:t})=>{E=ae({}),B=e.port||3e3,N.serviceName=t||N.serviceName,E.register(ie,{origin:e.allowedOrigins||[],methods:e.allowedMethods||["GET","POST","PUT","DELETE"],allowedHeaders:e.allowedHeaders||["Content-Type","Authorization"],preflight:!0,optionsSuccessStatus:204,credentials:!0}),e?.rateLimiting?.max&&e?.rateLimiting?.timeWindow&&E.register(ce,e.rateLimiting)},registerRoutes:({controllers:e})=>{e.forEach(t=>{Object.values(t).forEach(s=>{E.route(s)})}),n.info(`${N.serviceName} service registered http routes`+E.printRoutes())},addHook:(e,t)=>{E.addHook(e,t)},start:async()=>{try{await E.listen({port:B,host:"0.0.0.0"}),n.info(`HTTP server started on port ${B}`)}catch(e){n.error("Error starting HTTP server: "+e),process.exit(1)}},setErrorHandler:e=>{E.setErrorHandler(e)}};import le from"amqplib";var I="",C=null,M=null,U={arguments:{"x-queue-type":"quorum"}},j=e=>{I=e},k=async()=>{if(C)return C;try{console.log("Connecting to RabbitMQ:",I),C=await le.connect(I)}catch(e){return console.log("Error connecting to RabbitMQ:",e),C=null,await new Promise(t=>setTimeout(t,3e3)),k({rabbitConnectionUrl:I})}return C},b=async()=>{if(M)return M;C||await k();try{M=await C.createChannel()}catch(e){return console.error("Error creating RabbitMQ channel:",e),M=null,await new Promise(t=>setTimeout(t,3e3)),b()}return M},K=async(e,t,s)=>{let r=await b();return await r.assertQueue(e,U),await r.prefetch(t),(await r.consume(e,async a=>{a&&(async()=>{try{await s(a,r)}catch(i){console.error("Error handling message:",i),r.nack(a,!1,!1)}})()})).consumerTag},de=async(e,t,s,r)=>{await k(),await b();let o=await b();return await o.assertQueue(e,U),await o.prefetch(t),r&&await o.cancel(r),K(e,t,s)},Q=async(e,t,s)=>{await k();let r=await b();await r.assertQueue(e,U),await r.prefetch(t);let o=await K(e,t,s);return C.on("close",async()=>{console.log("RabbitMQ connection closed, attempting to reconnect..."),await z(e,t,s,o)}),console.log("RabbitMQ connection established and consumer registered:",o),o},z=async(e,t,s,r)=>{console.log("Reconnecting to RabbitMQ...");let o=de(e,t,s,r);return console.log("RabbitMQ connection re-established and consumer registered:",o),o},J={getChannel:b,getConnection:k,setConnectionUrl:j,initialize:Q,reinitialize:z};var G=async(e,t)=>{let s=`${e}Queue`,r;try{r=await b(),await r.assertQueue(s,{arguments:{"x-queue-type":"quorum"}});let o=me(),a=o,i=await r.assertQueue("",{exclusive:!0});return new Promise((c,d)=>{let m=setTimeout(()=>{h(),d(new Error(`No response from ${e} service`))},5e3),h=()=>{r.cancel(a).catch(()=>{}),r.deleteQueue(i.queue).catch(()=>{}),clearTimeout(m)};r.consume(i.queue,R=>{R.properties.correlationId===o&&(h(),c(JSON.parse(R.content.toString())),r.ack(R))},{noAck:!1,consumerTag:a}).catch(d),r.sendToQueue(s,Buffer.from(JSON.stringify(t)),{replyTo:i.queue,correlationId:o})})}catch(o){if(r)try{await r.close()}catch(a){console.error("Failed to close channel:",a)}throw new Error(`Failed to send message to ${e}: ${o.message}`)}},P=async(e,t)=>{let s=`${e}Queue`,r;try{r=await b(),await r.assertQueue(s,{arguments:{"x-queue-type":"quorum"}}),r.sendToQueue(s,Buffer.from(JSON.stringify(t)))}catch(o){if(r)try{await r.close()}catch(a){console.error("Failed to close channel:",a)}throw new Error(`Failed to send message to ${e}: ${o.message}`)}},me=()=>Math.random().toString(36).substring(7);import $,{WebSocketServer as ue}from"ws";import{v4 as he}from"uuid";import V from"os";var L={wsConnections:{},pendingCalls:[],async init(e,t){let s=V.hostname();if(e.serviceName=t,e.replicaSlot=s.split(".")[1],e.replicaId=s.split(".")[2],e.hostname=s,e.ip=(await V.networkInterfaces().eth0.find(r=>r.family==="IPv4")).address,this.startServer(e),e.serviceName!=="controlService"){let r,o={};for(;!r;)try{r=await this.connectToControlService(e)}catch(a){n.error("Failed to connect to control service, retrying...",a.message),await new Promise(i=>setTimeout(i,2e3))}(async()=>{for(;;){await new Promise(i=>r.once("close",i));let a;for(;!a;)try{a=await this.connectToControlService(e),setTimeout(async()=>{await w.execute("controlService.registerServiceAndReplica",{serviceName:e.serviceName,replicaSlot:e.replicaSlot,replicaHostname:e.hostname,replicaIp:e.ip})},1e3)}catch(i){n.error("Failed to reconnect to control service, retrying...",i.message),await new Promise(c=>setTimeout(c,2e3))}r=a}})();try{n.info("registering with",e.serviceName,e.replicaSlot,e.hostname,e.ip),await w.execute("controlService.registerServiceAndReplica",{serviceName:e.serviceName,replicaSlot:e.replicaSlot,replicaHostname:e.hostname,replicaIp:e.ip})}catch(a){throw n.error(`Failed to register service ${e.serviceName} with control service: ${a.message}`),a}try{o=await w.execute("controlService.newPeerSet",{requestingHostname:e.hostname}),n.info("Peers",o)}catch(a){throw n.error("Failed to get peer set. "+a.message),a}if(o&&Object.keys(o).length>0){await Promise.all(Object.entries(o).map(async([i,c])=>{!c||c?.serviceName===e.serviceName||await this.connect({peer:c,config:e})}));let a=!1;for(;!a;){await new Promise(c=>setTimeout(c,2e3)),n.info("Checking for missing peer connections...");for(let[c,d]of Object.entries(o))if(!(d.hostname||c==e.serviceName||this.wsConnections[c]&&Object.keys(this.wsConnections[c]).length>0))try{n.info("Missing "+c+" peer connection - requesting new peer from control service.",c+" / ",e.serviceName);let m=await w.execute("controlService.newPeerForService",{requestingHostname:e.hostname,serviceName:c});o[c]=m,await this.connect({peer:m,config:e})}catch(m){n.error(`Failed to get and connect to new peer for ${c}: ${m.message}`)}if(Object.entries(o).every(([c,d])=>{if(d.hostname||c==e.serviceName||this.wsConnections[c]&&Object.keys(this.wsConnections[c]).length>0)return!0;if(d.hostname)n.info(`Still missing peer connection for ${c}, due to ${d}`);else return n.error("Peer information is missing for failed connection. This can occur if a service is down for a prolonged period. This service will now wait for an inbound connection."),!0})){a=!0;break}}n.info("Initial peer connections established to: ",Object.keys(o))}}setInterval(()=>{Object.values(this.wsConnections).forEach(r=>{Object.values(r).forEach(o=>{o.readyState===$.OPEN&&o.ping(),o.lastPong&&Date.now()-o.lastPong>3e4&&(n.warn("No pong received, terminating connection"),o.terminate())})})},1e4)},startServer(e,t=5e3){new ue({port:t}).on("connection",(r,o)=>{let{serviceName:a,serviceHostname:i}=this.identifyPeer(o);this.wsConnections[a]||(this.wsConnections[a]={}),this.wsConnections[a][i]=r,n.info(`Peer connected (inbound): ${a} (${i})`),r.on("pong",()=>{r.lastPong=Date.now()}),r.on("message",c=>{this.handleMessage({serviceName:a,hostname:i,ws:r,raw:c})}),r.on("close",()=>{n.info(`Peer disconnected: ${a}`),this.wsConnections[a]&&delete this.wsConnections[a][i],e.serviceName=="controlService"&&w.execute("controlService.removeServiceReplica",{serviceName:a,replicaHostname:i})})})},async connectToControlService(e){return new Promise((t,s)=>{try{let r="ws://control-service:5000",o=new $(r,{headers:{"x-api-key":"my-secret-key","x-service-name":e.serviceName,"x-service-hostname":e.hostname}});o.on("open",()=>{n.info("Connected to control service"),o.lastPong=Date.now(),o.on("pong",()=>{o.lastPong=Date.now()}),this.wsConnections.controlService||(this.wsConnections.controlService={}),this.wsConnections.controlService.controlService=o,o.on("message",a=>{this.handleMessage({serviceName:"controlService",hostname:"controlService",ws:o,raw:a})}),t(o)}),o.on("close",()=>{n.error("Control service disconnected"),delete this.wsConnections.controlService?.controlService,s(new Error("Control service connection closed"))}),o.on("error",a=>{n.error(`WS error with control service: ${a.message}`),delete this.wsConnections.controlService?.controlService,s(a)})}catch(r){s(r)}})},async connect({peer:e,config:t}){if(!(e.name==t.serviceName||e.name==="controlService")){this.wsConnections[e.name]||(this.wsConnections[e.name]={}),this.wsConnections[e.name][e.hostname]=null;try{let s=`ws://${e.ip}:5000`,r=new $(s,{headers:{"x-api-key":"my-secret-key","x-service-name":t.serviceName,"x-service-hostname":t.hostname}});r.on("open",()=>{n.info(`Connected to peer ${e.hostname} at ${s}`),r.isAlive=!0,r.lastPong=Date.now(),r.on("pong",()=>{r.lastPong=Date.now()}),this.wsConnections[e.name][e.hostname]=r,r.on("message",o=>{this.handleMessage({serviceName:e.name,hostname:e.hostname,ws:r,raw:o})})}),r.on("close",()=>{n.info(`Peer ${e.name} at ${e.hostname} disconnected`),delete this.wsConnections[e.name][e.hostname],this.handlePeerFailure(e,t)}),r.on("error",o=>{n.error(`WS error with ${e.hostname}: ${o.message}`),delete this.wsConnections[e.name][e.hostname],this.handlePeerFailure(e,t)})}catch(s){n.error(`Error during WS connection to ${e.hostname}: ${s.message}`)}}},async handlePeerFailure(e,t){try{if(t.serviceName==="controlService")return;let s=!1;this.wsConnections[e.name]?.length<1&&(s=!0),setTimeout(async()=>{try{if(n.info("Reporting peer connection failure to control service",e),!e.hostname)throw new Error("Peer information is missing for failed connection. This can occur if a service is down for a prolonged period. This service will now wait for an inbound connection.");let r=await w.execute("controlService.peerConnectionDropped",{requestingHostname:t.hostname,requestingServicename:t.serviceName,droppedPeerHostname:e.hostname,getNewPeer:s});r?.hostname&&this.connect({peer:r,config:t})}catch(r){n.error(`Failed to report peer connection failure to control service for ${e.hostname}: ${r.message}`)}},3e3)}catch(s){n.error(`Failed to report peer connection failure to control service for ${e.hostname}: ${s.message}`)}},async handleMessage({serviceName:e,hostname:t,ws:s,raw:r}){let o;try{o=JSON.parse(r)}catch{n.error("Invalid JSON from peer:",r);return}if(o.type==="request"&&o.commandName)try{let a=await w.execute(o.commandName,o.payload);this.sendResponse({ws:s,messageId:o.messageId,response:a})}catch(a){this.sendResponse({ws:s,messageId:o.messageId,error:a.message})}else if(o.type==="response"){let a=this.pendingCalls.findIndex(c=>c.messageId===o.messageId);if(a===-1){n.error(`No pending call for messageId ${o.messageId}`);return}let i=this.pendingCalls.splice(a,1)[0];clearTimeout(i.timeout),o.error?i.reject(new Error("RPC rejected: "+o.error)):i.resolve(o.response)}else n.error("Unknown message type",o)},async send(e,t,s,r=1e4){let o=this.wsConnections[e];if(!o||!Object.keys(o)||Object.keys(o).length===0)throw new Error(`No replicas connected for service ${e}`);let a=Object.keys(o)[Math.floor(Math.random()*Object.keys(o).length)],i=o[a];if((!i||i.readyState!==$.OPEN)&&(i=await this.waitForWsReady({ws:i,serviceName:e})),!i||i.readyState!==$.OPEN)throw new Error(`WebSocket not connected to ${e}`);let c=he(),d={type:"request",messageId:c,commandName:t,payload:s};return i.send(JSON.stringify(d)),new Promise((m,h)=>{let R=setTimeout(()=>{this.pendingCalls=this.pendingCalls.filter(ne=>ne.messageId!==c),h(new Error(`Timeout waiting for response from ${e}`)),n.error(`Timeout waiting for response on call: ${JSON.stringify(d)}`)},r);this.pendingCalls.push({messageId:c,resolve:m,reject:h,timeout:R})})},sendResponse({ws:e,messageId:t,response:s=null,error:r=null}){!e||e.readyState!==$.OPEN||e.send(JSON.stringify({type:"response",messageId:t,response:s,error:r}))},identifyPeer(e){return{serviceHostname:e.headers["x-service-hostname"]||`unknown-${Date.now()}`,serviceName:e.headers["x-service-name"]||`unknown-${Date.now()}`}},async waitForWsReady({ws:e,serviceName:t,waitInterval:s=50,maxWait:r=5e3}){let o=Date.now();for(;;){if(e&&e.readyState===$.OPEN)return e;if(Date.now()-o>r)throw new Error(`WebSocket not connected to ${t} after ${r}ms`);await new Promise(a=>setTimeout(a,s))}}};var y={manifest:{commandList:[],commandImplementations:{},schemas:{}},addCommand({commandName:e,handlerFunction:t}){y.manifest.commandImplementations[e]={function:t.toString()},y.manifest.commandList.push(e)},addSchema({schemaName:e,schema:t}){y.manifest.schemas[e]=t}};var w={config:{},handlers:new Map,init(e){this.config=e},register(e,t){this.handlers.set(e,t),y.addCommand({commandName:e,handlerFunction:t})},async execute(e,t,s={}){let{fireAndForget:r=!1}=s;if(this.config.architecture=="modular-monolith"){let o=this.handlers.get(e);if(!o)throw new Error(`Command "${e}" not registered`);return await o(t)}else{let[o,a]=e.split(".");if(!o||!a){let c=this.config.serviceName+"."+e,d=this.handlers.get(c);if(!d)throw new Error(`Command "${c}" not registered`);return await d(t)}if(o===this.config.serviceName){let c=this.handlers.get(e);if(!c)throw new Error(`Command "${e}" not registered`);return await c(t)}if(r){P(o,{method:e,data:t});return}return await L.send(o,e,t)}}};var Y={routeHandlers:{},defaultHandlers:{runMigrations:async e=>{let t=e.data?.migration;await w.execute("runMigrations",{migration:t})},runSeeders:async e=>{let t=e.data?.seeder;await w.execute("runSeeders",{seeder:t})},healthCheck:async()=>({status:"ok"})},async init({config:e,handlers:t={}}){if(!e?.queueName)throw new Error("Queue name is required for message controller initialization");this.routeHandlers={...this.defaultHandlers,...t},await Q(e.queueName,e.prefetch||20,this.handleMessage.bind(this))},async handleMessage(e,t){if(!e)return;let s;try{s=JSON.parse(e.content.toString())}catch(i){return n.error("Invalid JSON message received:",i),t.ack(e)}let r=s.method;r.includes(".")&&(r=r.split(".").slice(1).join("."));let o={correlationId:e.properties.correlationId},a=this.routeHandlers[r];if(!a)return t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify({error:"Method not found"})),o),t.ack(e);try{let i=await a(s,e,t);e.properties.replyTo&&t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify(i??{ok:!0})),o)}catch(i){e.properties.replyTo&&t.sendToQueue(e.properties.replyTo,Buffer.from(JSON.stringify({error:i.message})),o)}finally{t.ack(e)}}};import{MongoClient as pe}from"mongodb";import X from"mysql2/promise";var u=null,_="",fe=5e3,H=async e=>{if(u)return u;_=e.type;try{switch(e.type){case"mongodb":return await we({host:e.host,user:e.user,password:e.password,database:e.database,port:e.port,connectionArgs:e.connectionArgs});case"mysql":return await ge({host:e.host,user:e.user,password:e.password,database:e.database,connectionLimit:e.connectionLimit,queueLimit:e.queueLimit});default:throw new Error("Unsupported database type: "+e.type)}}catch(t){throw n.error("Error initializing database connection: "+t.message),t}},we=async({host:e,user:t,password:s,database:r,port:o=27017,connectionArgs:a={}})=>{try{n.info("Connecting to mongo..");let i=`mongodb://${t}:${encodeURIComponent(s)}@${e}:${o}/${r}`;return n.info(`MongoDB connection URL: ${i}`),u=(await pe.connect(i,a)).db(),n.info("MongoDB connected successfully"),u}catch(i){throw n.error("MongoDB connection error: "+i),i}},ge=async({host:e,user:t,password:s,database:r,connectionLimit:o=10,queueLimit:a=20,maxRetries:i=5})=>{if(u)return u;let c=0;for(;c<i;)try{return await ye({host:e,user:t,password:s,database:r}),n.info("Establishing new MySQL pool..."),u=await X.createPool({host:e,user:t,password:s,database:r,waitForConnections:!0,connectionLimit:o,queueLimit:a,enableKeepAlive:!0,keepAliveInitialDelay:0}),setInterval(async()=>{try{await u.query("SELECT 1")}catch(d){n.error("MySQL keep-alive query failed: "+d.message),u=null,c=0}},6e4),n.info("MySQL pool established successfully"),u}catch(d){if(n.error(`MySQL connection attempt ${c+1} failed: ${d.message}`),c++,c>=i)throw n.error("Max retries reached. Could not connect to MySQL."),new Error("Could not connect to the database after multiple attempts");await new Promise(m=>setTimeout(m,fe))}},ye=async({host:e,user:t,password:s,database:r})=>{if(n.info(`[assertDbExists] Attempting to connect to MySQL on host ${e} as ${t}`),n.info(`[assertDbExists] Target DB: ${r}`),!r||!e||!t||!s)throw n.error("[assertDbExists] Missing required variables: database, host, user, or password"),new Error("Missing required variables for DB connection");try{let o=await X.createConnection({host:e,user:t,password:s});n.info(`[assertDbExists] Connected to MySQL, asserting database '${r}'`);let[a]=await o.query(`CREATE DATABASE IF NOT EXISTS \`${r}\``);await o.end()}catch(o){throw n.error("[assertDbExists] \u274C Error asserting database existence"),n.error(o.stack||o.message||o),o}},Z=async()=>{if(!u)throw new Error("Database connection not initialized");try{return!0}catch(e){throw e}},ee=async()=>{if(!u)throw new Error("Database connection not initialized");try{_==="mongodb"?await u.dropDatabase():_==="mysql"&&await W()}catch(e){console.error("Error dropping database data: "+e.message)}};var W=async()=>{if(!u)throw new Error("Database connection not initialized");if(F.environment!=="test"&&F.environment!=="development")throw new Error("Cannot reset mysql database outside of test or development mode!");try{let[e]=await u.query("SHOW TABLES");await u.query("SET FOREIGN_KEY_CHECKS = 0");for(let t of e){let s=Object.values(t)[0];await u.query(`DROP TABLE \`${s}\``)}await u.query("SET FOREIGN_KEY_CHECKS = 1"),console.log("MySQL database reset successfully")}catch(e){throw console.error("Error resetting MySQL database: "+e.message),e}};import ve from"fs";import be from"path";var S={config:null,runSeeders:async({config:e,seeder:t})=>{try{if(e.environment=="test"&&!t)return;n.info("Running seeders"),S.config=e;let s,r;try{if(t?s=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/seeders/"+t:s=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/seeders/"+e.environment,r=(await ve.promises.readdir(s)).sort(),r.length==0){n.info("No seeders found for environment: "+e.environment);return}}catch{n.info("No seeders found for environment: "+e.environment);return}for(let o of r){let i=await import(be.join(s,o));if(await S.checkSeederAlreadyRun(o))n.info("Seeder already run: "+o);else try{n.info("Running seeder: "+o),await i.up(),await S.markSeederAsRun(o)}catch(c){throw n.error("Error running seeder "+o+": "+c),c}}n.info("Seeders completed successfully")}catch(s){throw n.error("Error running seeders: "+s),s}},checkSeederAlreadyRun:async e=>{let t=[];if(S.config.db.type=="mysql"){let[s]=await u.query('SHOW TABLES LIKE "seeders"');if(s.length==0)return!1;let[r]=await u.query("SELECT * FROM seeders WHERE name = ?",[e]);t=r}else t=await u.collection("seeders").find({name:e}).toArray();return t.length!=0},markSeederAsRun:async e=>{S.config.db.type=="mysql"?await u.execute("INSERT INTO seeders (name) VALUES (?)",[e]):await u.collection("seeders").insertOne({name:e,runAt:new Date})}};var te=async({config:e})=>{if(e&&e.db&&e.db.type)try{await H()}catch(t){n.error("[Internal health check] Failed - Exiting. Error connecting to MongoDB: "+t),process.exit(1)}};process.env.NODE_ENV!=="development"&&(process.on("uncaughtException",e=>{n.error("Uncaught Exception: "+e),process.exit(1)}),process.on("unhandledRejection",(e,t)=>{n.error("Unhandled Rejection at: "+t+". Reason: "+e),process.exit(1)}));var re=e=>{e.setErrorHandler((t,s,r)=>{if(t.validation){if(r&&typeof r.send=="function")return r.code(400).send({statusCode:400,error:"Bad Request",message:t.message});throw new Error("Bad Request: "+t.message)}if(t instanceof D){if(r&&typeof r.send=="function")return r.code(404).send({statusCode:404,error:"Not Found",message:t.message});throw new Error("Not found: "+t.message)}if(t instanceof O){if(r&&typeof r.code=="function"&&typeof r.send=="function")return r.code(400).send({statusCode:400,error:"Bad Request",message:t.message});throw new Error("Bad Request: "+t.message)}if(t instanceof q){if(r)return r.code(401).send({statusCode:401,error:"Unauthorized",message:t.message});throw new Error("Unauthorized: "+t.message)}if(t.statusCode===429){if(r)return r.code(429).send({statusCode:429,error:"Too Many Requests",message:"You have exceeded the request limit."});throw new Error("Too Many Requests: You have exceeded the request limit.")}if(t instanceof A){if(r)throw new Error("Failed Health Check: "+t.message);return r.code(500).send({statusCode:500,error:"Failed Health Check",message:t.message})}if(t instanceof Error||(t=new Error(typeof t=="string"?t:JSON.stringify(t))),n.error(t.stack||t.toString()),r)return r.code(500).send({statusCode:500,error:"Internal Server Error",message:"Something went wrong",details:t?.message||t?.toString()||"No additional error information available"});throw new Error("Internal Server Error: "+t.message)})},D=class extends Error{constructor(t="Resource not found"){super(t),this.name="NotFoundError",this.statusCode=404}},O=class extends Error{constructor(t="Bad request"){super(t),this.name="BadRequestError",this.statusCode=400}},q=class extends Error{constructor(t="Unauthorised"){super(t),this.name="UnauthorisedError",this.statusCode=401}},A=class extends Error{constructor(t="Failed health check"){super(t),this.name="FailedHealthCheckError",this.statusCode=500}};var v={db:{},init(e){v.db=e},async syncManyToMany({table:e,parentId:t,childIds:s,parentColumn:r,childColumn:o}){try{s=Array.from(new Set(s||[])).map(String);let[a]=await v.db.query(`SELECT ${o} FROM ${e} WHERE ${r} = ?`,[t]),i=a.map(m=>{let h=m[o];return Buffer.isBuffer(h)?h.toString("hex"):String(h)}),c=s.filter(m=>!i.includes(m)),d=i.filter(m=>!s.includes(m));if(d.length&&await v.db.query(`DELETE FROM ${e} WHERE ${r} = ? AND ${o} IN (?)`,[t,d]),c.length){let m=c.map(h=>[t,h]);await v.db.query(`INSERT IGNORE INTO ${e} (${r}, ${o}) VALUES ?`,[m])}return{added:c,removed:d}}catch(a){throw n.error("Error syncing many-to-many relationship: "+a.message),a}},async syncOneToMany({table:e,parentId:t,childIds:s,parentColumn:r,childIdColumn:o="id"}){try{Array.isArray(s)||(s=[]),s=Array.from(new Set(s)).map(String);let[a]=await v.db.query(`SELECT ${o} FROM ${e} WHERE ${r} = ?`,[t]),i=a.map(m=>{let h=m[o];return Buffer.isBuffer(h)?h.toString("hex"):String(h)});console.log("currentIds:",i),console.log("childIds:",s);let c=s.filter(m=>!i.includes(m)),d=i.filter(m=>!s.includes(m));return d.length&&await v.db.query(`UPDATE ${e} SET ${r} = NULL WHERE ${o} IN (?)`,[d]),c.length&&await v.db.query(`UPDATE ${e} SET ${r} = ? WHERE ${o} IN (?)`,[t,c]),{linked:c,unlinked:d}}catch(a){throw n.error("Error syncing one-to-many relationship: "+a.message),a}},toSnake(e){return e.replace(/[A-Z]/g,t=>`_${t.toLowerCase()}`)},objectToCamelCase(e){let t={};for(let s in e){let r=s.replace(/_([a-z])/g,o=>o[1].toUpperCase());t[r]=e[s]}return t}};import Se from"fs";import Ee from"path";var x={config:null,runMigrations:async({config:e})=>{try{if(e.db.type!=="mysql"){n.info("Migrations only supported for MySQL");return}(e.resetDatabase&&e.environment=="development"||e.environment=="test")&&(n.info("Resetting database..."),await W()),n.info("Running migrations"),x.config=e;let t=process.env.GLOBAL_SERVICE_BASE_DIR+"/db/migrations",s=(await Se.promises.readdir(t)).sort();for(let r of s){let a=await import(Ee.join(t,r));await x.checkMigrationAlreadyRun(r)?n.info("Migration already run: "+r):(n.info("Running migration: "+r),await a.up(),await x.markMigrationAsRun(r))}n.info("Migrations completed successfully")}catch(t){throw n.error("Error running migrations: "+t),t}},checkMigrationAlreadyRun:async e=>{let[t]=await u.query('SHOW TABLES LIKE "migrations"');if(t.length==0)return!1;if(e=="01-init.js")return!0;let[s]=await u.query("SELECT * FROM migrations WHERE name = ?",[e]);return s.length!=0},markMigrationAsRun:async e=>{await u.execute("INSERT INTO migrations (name) VALUES (?)",[e])}};import Ce from"ajv";import $e from"ajv-formats";var T=new Ce({allErrors:!0,useDefaults:!0});$e(T);T.addFormat("mysql-date",/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);var Re={compile:e=>{y.addSchema(e);let t=T.compile(e.schema);return s=>{if(!t(s)){let o=[];return t.errors.map(a=>{o.push(a.instancePath+" "+a.message)}),{errors:o.join(", ")}}return!1}},addFormat:(e,t)=>{T.addFormat(e,t)},addKeyword:(e,t)=>{T.addKeyword(e,t)},addSchema:e=>{y.addSchema(e),T.addSchema(e.schema,e.$id)}},oe=Re;import xe from"assert";import Te from"fs";import Ne from"path";var p={testsDirectory:`${process.env.GLOBAL_SERVICE_BASE_DIR}/tests`,failed:0,beforeEachFns:[],afterEachFns:[],tests:[],testResults:[],beforeEach:e=>{p.beforeEachFns.push(e)},run:(e,t)=>{p.tests.push({name:e,fn:t})},afterEach:e=>{p.afterEachFns.push(e)},assert:xe,runCommandTests:async({config:e})=>{console.log("=============================="),console.log("Running command tests..."),p.testsDirectory+=`/${e.runTests||"ephemeral"}`;let t=Te.readdirSync(p.testsDirectory).filter(s=>s.endsWith(".js")).sort();for(let s of t){p.prepFns=[],p.tests=[];let r=Ne.join(p.testsDirectory,s);try{await import(r)}catch(a){p.failed++,console.error(`\u274C Failed to load ${s}: ${a.message}`);continue}let o=0;for(let a of p.tests){o++;for(let i of p.beforeEachFns)try{await i()}catch(c){p.failed++,console.error(`\u274C Test preparation failed - ${c.message}`);break}try{await a.fn(),p.testResults.push({name:a.name,error:null})}catch(i){p.failed++,p.testResults.push({name:a.name,error:i})}for(let i of p.afterEachFns)try{await i()}catch(c){p.failed++,console.error(`\u274C Test tidy up failed - ${c.message}`);break}}}console.table(p.testResults.map(s=>({Test:s.name,Result:s.error?`\u274C Failed - ${s.error.message}`:"\u2705 Passed"}))),p.failed>0?console.error(`\u274C Some Integration tests failed: ${p.failed} error(s)`):console.log("\u2705 All integration tests passed!")}};var f={client:null,bucket:"",uploadsUrl:"",init:async e=>{if(!e.bucket)throw new Error("S3 storage driver requires a bucket name.");if(!e.region)throw new Error("S3 storage driver requires a region.");if(!e.uploadsUrl)throw new Error("S3 storage driver requires an uploadsUrl.");let{S3Client:t,PutObjectCommand:s,GetObjectCommand:r,DeleteObjectCommand:o}=await import("@aws-sdk/client-s3");f.S3Client=t,f.PutObjectCommand=s,f.GetObjectCommand=r,f.DeleteObjectCommand=o,f.bucket=e.bucket,f.uploadsUrl=e.uploadsUrl,f.client=new t({region:e.region,credentials:{accessKeyId:e.accessKeyId,secretAccessKey:e.secretAccessKey}})},upload:async({file:e,key:t,contentType:s})=>{let r=new f.PutObjectCommand({Bucket:f.bucket,Key:t,Body:e,ContentType:s});return await f.client.send(r),t},download:async({key:e,stream:t})=>{let s=new f.GetObjectCommand({Bucket:f.bucket,Key:e}),r=await f.client.send(s);if(t)return r.Body;{let o=[];for await(let a of r.Body)o.push(a);return Buffer.concat(o)}},delete:async({key:e})=>{let t=new f.DeleteObjectCommand({Bucket:f.bucket,Key:e});await f.client.send(t)},getUrl:async({key:e})=>`${f.uploadsUrl}/${e}`};var g={driverName:"",driver:null,uploadsUrl:"",init:async e=>{try{if(!e.driver){n.info("No storage driver specified, skipping storage initialization.");return}switch(e.driver){case"s3":g.driverName="s3",g.driver=f;break;default:throw new Error(`Unsupported storage type: ${e.driver}`)}await g.driver.init(e),g.uploadsUrl=e.uploadsUrl||""}catch(t){throw n.error(`Storage initialization error: ${t.message}`),t}},upload:async({file:e,key:t,contentType:s,metadata:r})=>{try{let o=await g.driver.upload({file:e,key:t,contentType:s,metadata:r});return`${g.uploadsUrl}/${o}`}catch(o){throw n.error(`Storage upload error: ${o.message}`),o}},download:async({key:e,stream:t})=>{try{return await g.driver.download({key:e,stream:t})}catch(s){throw n.error(`Storage download error: ${s.message}`),s}},delete:async({key:e})=>{try{return await g.driver.delete({key:e})}catch(t){throw n.error(`Storage delete error: ${t.message}`),t}},getUrl:async({key:e,expiresIn:t})=>{try{return await g.driver.getUrl({key:e,expiresIn:t})}catch(s){throw n.error(`Storage getUrl error: ${s.message}`),s}}};import{v4 as Me}from"uuid";import{v5 as ke}from"uuid";import{CronExpressionParser as De}from"cron-parser";import se from"bcrypt";var Oe=await import(process.env.GLOBAL_SERVICE_BASE_DIR+"config.js"),F=Oe.config,l={init:async e=>{if(l.config=e,e.http&&e.http.allowedMethods&&(l.http=N,await l.http.init({config:e.http,serviceName:e.serviceName})),l.commands=w,l.commands.init(e),e.db&&e.db.type)try{l.db=await H(e.db)}catch(t){n.error("Error connecting to database: "+t),process.exit(1)}if(l.db||(l.db={}),l.db.checkConnection=Z,l.db.migrations=x,l.db.seeders=S,v.init(l.db),l.db.sql={},l.db.sql.helpers=v,l.http&&l.http.addHook("onReady",async()=>{setInterval(()=>{w.execute("internalHealthCheck",{config:e})},6e4)}),l.http&&re(l.http),j(e.message?.url||""),l.message=Y,l.message.sendAwaitResponse=G,l.message.sendAndForget=P,l.webSockets=L,l.commands.register(`${e.serviceName}.runMigrations`,async()=>await x.runMigrations({config:e})),l.commands.register(`${e.serviceName}.runSeeders`,async({seeder:t})=>await S.runSeeders({config:e,seeder:t})),l.commands.register(`${e.serviceName}.internalHealthCheck`,async()=>await te({config:e})),l.commands.register(`${e.serviceName}.dropDatabaseData`,ee),l.schema=oe,l.logger=n,l.logger.init({cloudProjectId:e.cloud?.projectId||"",serviceName:e.serviceName,flushIntervalMs:e.cloud?.logger.flushIntervalMs||5e3,transports:e.cloud?.logger||[]}),l.error={notFound:D,badRequest:O,unauthorised:q,failedHealthCheck:A},l.utils={uuid:()=>Me(),hash:async t=>await se.hash(t,10),verifyHash:async(t,s,r="")=>typeof s=="string"&&s.startsWith("$2")?await se.compare(t,s):ke(t,r)===s,cronExpressionParser:De},l.http&&l.http.addHook("onRequest",async(t,s)=>{let{url:r,method:o}=t;if(!r.endsWith("/")&&!r.includes(".")&&r!=="/"){let c=new URL(t.raw.url,`http://${t.headers.host}`);c.pathname+="/",t.raw.url=c.pathname+(c.search||"")}let a=t.raw.headers.authorization||"",i=a?a.split(" ")[1]:"";if(i){let c=await l.commands.execute("userService.getAuthenticatedUser",{token:i});c&&(t.user=c)}}),l.registerService=async()=>{if(e.serviceName!=="controlService")try{await l.commands.execute("controlService.registerService",{service:{name:e.serviceName,manifest:y.manifest}})}catch(t){l.logger.error(`Failed to register service ${e.serviceName} with control service: ${t.message}`)}},l.test=p,e.storage&&e.storage.driver)g.init(e.storage),l.storage=g;else{let t=()=>{throw new Error("Storage service not configured - please configure storage in config.js")};l.storage={upload:t,download:t,getUrl:t}}l.rabbit=J,l.http||(l.http={})}};await l.init(F);var yr=l,{commands:vr,http:br,message:Sr,db:Er,schema:Cr,logger:$r,error:Rr,utils:xr,registerService:Tr,webSockets:Nr,test:Mr,storage:kr,rabbit:Dr}=l;export{vr as commands,F as config,Er as db,yr as default,Rr as error,br as http,$r as logger,Sr as message,Dr as rabbit,Tr as registerService,Cr as schema,kr as storage,Mr as test,xr as utils,Nr as webSockets};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnar-engine/core",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "Gnar Engine - Service Core",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",