@deeep-network/riptide 0.2.3 → 0.2.4

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/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var _=Object.create;var x=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var F=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var n=(o,e)=>x(o,"name",{value:e,configurable:!0});var L=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of A(e))!Y.call(o,i)&&i!==t&&x(o,i,{get:()=>e[i],enumerable:!(r=N(e,i))||r.enumerable});return o};var g=(o,e,t)=>(t=o!=null?_(F(o)):{},L(e||!o||!o.__esModule?x(t,"default",{value:o,enumerable:!0}):t,o));var D=g(require("pino"));function v(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}n(v,"isRiptideError");var k=g(require("pino"));function P(o){let{level:e="info",format:t="pretty",serviceName:r}=o,i={level:e,timestamp:k.default.stdTimeFunctions.isoTime,formatters:{log:n(s=>({service:r,...s}),"log"),bindings:n(s=>{let{pid:a,hostname:c,...d}=s;return d},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,k.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,k.default)(i)}n(P,"createLogger");var R=require("child_process"),f=require("fs"),U=g(require("http")),V=g(require("https")),b=g(require("path")),O=require("util");var W=(0,O.promisify)(R.exec);function I(){return{sleep:n(o=>new Promise(e=>setTimeout(e,o)),"sleep"),retry:n(async(o,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,a,c=r;for(let d=1;d<=t;d++)try{return await o()}catch(p){if(a=p instanceof Error?p:new Error(String(p)),d===t)throw a;await new Promise(m=>setTimeout(m,Math.min(c,s))),c*=i}throw a},"retry"),execCommand:n(async(o,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:a}=await W(o,{timeout:t,cwd:r,env:{...process.env,...i}});return{stdout:s.trim(),stderr:a.trim(),exitCode:0}}catch(s){if(s.killed&&s.signal==="SIGTERM"){let c=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${o}', { timeout: ${t*2} })`,d=s.stderr?.trim()||"",p=d?`${d}
2
+ "use strict";var N=Object.create;var x=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var A=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var n=(o,e)=>x(o,"name",{value:e,configurable:!0});var L=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of F(e))!Y.call(o,i)&&i!==t&&x(o,i,{get:()=>e[i],enumerable:!(r=_(e,i))||r.enumerable});return o};var g=(o,e,t)=>(t=o!=null?N(A(o)):{},L(e||!o||!o.__esModule?x(t,"default",{value:o,enumerable:!0}):t,o));var D=g(require("pino"));function v(o){return o instanceof Error&&"exitCode"in o&&typeof o.exitCode=="number"}n(v,"isRiptideError");var k=g(require("pino"));function P(o){let{level:e="info",format:t="pretty",serviceName:r}=o,i={level:e,timestamp:k.default.stdTimeFunctions.isoTime,formatters:{log:n(s=>({service:r,...s}),"log"),bindings:n(s=>{let{pid:a,hostname:c,...d}=s;return d},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,k.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,k.default)(i)}n(P,"createLogger");var O=require("child_process"),f=require("fs"),U=g(require("http")),V=g(require("https")),b=g(require("path")),R=require("util");var W=(0,R.promisify)(O.exec);function H(){return{sleep:n(o=>new Promise(e=>setTimeout(e,o)),"sleep"),retry:n(async(o,e={})=>{let{maxAttempts:t=3,delay:r=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,a,c=r;for(let d=1;d<=t;d++)try{return await o()}catch(p){if(a=p instanceof Error?p:new Error(String(p)),d===t)throw a;await new Promise(m=>setTimeout(m,Math.min(c,s))),c*=i}throw a},"retry"),execCommand:n(async(o,e={})=>{let{timeout:t=3e4,cwd:r=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:a}=await W(o,{timeout:t,cwd:r,env:{...process.env,...i}});return{stdout:s.trim(),stderr:a.trim(),exitCode:0}}catch(s){if(s.killed&&s.signal==="SIGTERM"){let c=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${o}', { timeout: ${t*2} })`,d=s.stderr?.trim()||"",p=d?`${d}
3
3
 
4
4
  ${c}`:c,m=[];s.stdout?.trim()&&m.push(`STDOUT: ${s.stdout.trim()}`),p&&m.push(`STDERR: ${p}`);let M=[`Command execution timed out: ${o}`,...m].join(`
5
5
 
6
- `),E=new Error(M);throw E.name="CommandTimeoutError",E}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:n(async(o,e,t={})=>{let{mode:r="0644",encoding:i="utf8"}=t;await f.promises.mkdir(b.dirname(o),{recursive:!0}),await f.promises.writeFile(o,e,{encoding:i}),await f.promises.chmod(o,r)},"writeFile"),readFile:n(async o=>await f.promises.readFile(o,"utf8"),"readFile"),fileExists:n(async o=>{try{return await f.promises.access(o),!0}catch{return!1}},"fileExists"),downloadFile:n(async function o(e,t){return new Promise((r,i)=>{let s=b.dirname(t);f.promises.mkdir(s,{recursive:!0}).then(()=>{let a=(0,f.createWriteStream)(t);(e.startsWith("https:")?V:U).get(e,p=>{if(p.statusCode===200)p.pipe(a),a.on("finish",()=>{a.close(),r(t)});else if(p.statusCode===301||p.statusCode===302){let m=p.headers.location;m?r(o(m,t)):i(new Error(`Redirect without location header: ${p.statusCode}`))}else i(new Error(`Failed to download: ${p.statusCode} ${p.statusMessage}`))}).on("error",p=>{f.promises.unlink(t).catch(()=>{}),i(p)}),a.on("error",p=>{f.promises.unlink(t).catch(()=>{}),i(p)})}).catch(i)})},"downloadFile")}}n(I,"createUtilityContext");var H=require("http");var w=class{constructor(e,t,r,i,s){this.getStatus=r;this.executeHealthCheck=i;this.getMetrics=s;this.port=e,this.logger=t.child({component:"web-server"})}static{n(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,H.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let r=await this.executeHealthCheck(),i=this.getStatus(),s={healthy:r,status:i.status,uptime:i.uptime,message:i.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(r){this.logger.error({error:r},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:r instanceof Error?r.message:"Internal server error"}))}else if(e.url==="/metrics")try{let r=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(r)}catch(r){this.logger.error({error:r},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:r instanceof Error?r.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",r=>{this.logger.error({error:r,port:this.port},"Failed to start web server"),t(r)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var S=class{static{n(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;heartbeatInterval;webServer;constructor(e,t,r={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.logger=P({serviceName:t.service.name,level:r.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},v(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),process.exit(e.exitCode)):process.exit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook, continuing...");return}this.logger.info("Installing secrets...");try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);let r=this.createHookContext(),i=Date.now();try{let s=await t(r),a=Date.now()-i;return this.logger.debug(`Hook ${e} completed in ${a}ms`),s}catch(s){let a=Date.now()-i;throw this.logger.error({hookName:e,duration:a,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},"Hook threw an exception, will not continue"),s}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(r){return this.logger.warn({hookName:e,error:r instanceof Error?r.message:String(r)},"Optional hook execution failed but continuing"),null}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:I()}}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),v(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),process.exit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),v(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),process.exit(1))})}setupSignalHandlers(){let e=n(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.webServer&&await this.webServer.stop(),await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(r){this.logger.error({error:r instanceof Error?r.message:String(r)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;try{let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat");return}await this.pingSonar(r)}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Heartbeat execution failed")}finally{t=!1}},e)}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.info({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor(Date.now()/1e3),a={entity_id:i,client_timestamp:s,metadata:e};try{this.logger.info({payload:a,sonarUrl:t,sonarApiKey:r,nomadJobId:i},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(a)});if(!c.ok){let d=await c.text();this.logger.warn({status:c.status,error:d,entity_id:i},"Sonar API heartbeat failed")}}catch(c){this.logger.error({error:c instanceof Error?c.message:String(c),errorType:c?.constructor?.name,errorCause:c instanceof Error&&"cause"in c?c.cause:void 0,stack:c instanceof Error?c.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:i},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new w(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>await this.executeHookSafely("health")===!0,async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=n(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var h=g(require("fs/promises")),l=g(require("path"));var C=class{constructor(e){this.logger=e}static{n(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:i="basic",description:s}=e,a=l.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${i}`),this.logger.info(`Location: ${a}`),await this.createDirectoryStructure(a),await this.createPackageJson(a,t,s),await this.createRiptideConfig(a,t,s),await this.createTsConfig(a),await this.createTsupConfig(a),await this.createHooks(a,i),await this.createDockerfile(a,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,a)}async createDirectoryStructure(e){await h.mkdir(l.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let i=await this.checkIfInWorkspace(e),s=i?"workspace:*":await this.getRiptideVersion(),a=i?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f reef/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,c={name:`reef-${t}`,version:"1.0.0",description:r||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":a,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":s},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await h.writeFile(l.join(e,"package.json"),JSON.stringify(c,null,2))}async createRiptideConfig(e,t,r){let i={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await h.writeFile(l.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let r=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await h.writeFile(l.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await h.writeFile(l.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
6
+ `),E=new Error(M);throw E.name="CommandTimeoutError",E}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:n(async(o,e,t={})=>{let{mode:r="0644",encoding:i="utf8"}=t;await f.promises.mkdir(b.dirname(o),{recursive:!0}),await f.promises.writeFile(o,e,{encoding:i}),await f.promises.chmod(o,r)},"writeFile"),readFile:n(async o=>await f.promises.readFile(o,"utf8"),"readFile"),fileExists:n(async o=>{try{return await f.promises.access(o),!0}catch{return!1}},"fileExists"),downloadFile:n(async function o(e,t){return new Promise((r,i)=>{let s=b.dirname(t);f.promises.mkdir(s,{recursive:!0}).then(()=>{let a=(0,f.createWriteStream)(t);(e.startsWith("https:")?V:U).get(e,p=>{if(p.statusCode===200)p.pipe(a),a.on("finish",()=>{a.close(),r(t)});else if(p.statusCode===301||p.statusCode===302){let m=p.headers.location;m?r(o(m,t)):i(new Error(`Redirect without location header: ${p.statusCode}`))}else i(new Error(`Failed to download: ${p.statusCode} ${p.statusMessage}`))}).on("error",p=>{f.promises.unlink(t).catch(()=>{}),i(p)}),a.on("error",p=>{f.promises.unlink(t).catch(()=>{}),i(p)})}).catch(i)})},"downloadFile")}}n(H,"createUtilityContext");var I=require("http");var w=class{constructor(e,t,r,i,s){this.getStatus=r;this.executeHealthCheck=i;this.getMetrics=s;this.port=e,this.logger=t.child({component:"web-server"})}static{n(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,I.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let r=await this.executeHealthCheck(),i=this.getStatus(),s={healthy:r,status:i.status,uptime:i.uptime,message:i.message};r?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(r){this.logger.error({error:r},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:r instanceof Error?r.message:"Internal server error"}))}else if(e.url==="/metrics")try{let r=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(r)}catch(r){this.logger.error({error:r},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:r instanceof Error?r.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",r=>{this.logger.error({error:r,port:this.port},"Failed to start web server"),t(r)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var S=class{static{n(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;heartbeatInterval;webServer;constructor(e,t,r={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.logger=P({serviceName:t.service.name,level:r.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},v(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),process.exit(e.exitCode)):process.exit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook, continuing...");return}this.logger.info("Installing secrets...");try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);let r=this.createHookContext(),i=Date.now();try{let s=await t(r),a=Date.now()-i;return this.logger.debug(`Hook ${e} completed in ${a}ms`),s}catch(s){let a=Date.now()-i;throw this.logger.error({hookName:e,duration:a,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},"Hook threw an exception, will not continue"),s}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(r){return this.logger.warn({hookName:e,error:r instanceof Error?r.message:String(r)},"Optional hook execution failed but continuing"),null}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:H()}}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),v(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),process.exit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),v(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),process.exit(1))})}setupSignalHandlers(){let e=n(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.webServer&&await this.webServer.stop(),await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(r){this.logger.error({error:r instanceof Error?r.message:String(r)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;try{let r=await this.executeHookSafely("heartbeat");if(r===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat");return}await this.pingSonar(r)}catch(r){this.logger.warn({error:r instanceof Error?r.message:String(r)},"Heartbeat execution failed")}finally{t=!1}},e)}async pingSonar(e){let t=process.env.SONAR_API_URL,r=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!r||!i){this.logger.info({hasUrl:!!t,hasKey:!!r,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor(Date.now()/1e3),a={entity_id:i,client_timestamp:s,metadata:e};try{this.logger.info({payload:a,sonarUrl:t,sonarApiKey:r,nomadJobId:i},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify(a)});if(!c.ok){let d=await c.text();this.logger.warn({status:c.status,error:d,entity_id:i},"Sonar API heartbeat failed")}}catch(c){this.logger.error({error:c instanceof Error?c.message:String(c),errorType:c?.constructor?.name,errorCause:c instanceof Error&&"cause"in c?c.cause:void 0,stack:c instanceof Error?c.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:i},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new w(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>await this.executeHookSafely("health")===!0,async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=n(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var h=g(require("fs/promises")),l=g(require("path"));var C=class{constructor(e){this.logger=e}static{n(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:r=".",template:i="basic",description:s}=e,a=l.join(r,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${i}`),this.logger.info(`Location: ${a}`),await this.createDirectoryStructure(a),await this.createPackageJson(a,t,s),await this.createRiptideConfig(a,t,s),await this.createTsConfig(a),await this.createTsupConfig(a),await this.createHooks(a,i),await this.createDockerfile(a,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,a)}async createDirectoryStructure(e){await h.mkdir(l.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,r){let i=await this.checkIfInWorkspace(e),s=i?"workspace:*":await this.getRiptideVersion(),a=i?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f reef/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,c={name:`reef-${t}`,version:"1.0.0",description:r||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":a,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":s},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await h.writeFile(l.join(e,"package.json"),JSON.stringify(c,null,2))}async createRiptideConfig(e,t,r){let i={service:{name:t,version:"1.0.0",description:r||`${t} service`},logging:{level:"info"}};await h.writeFile(l.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let r=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await h.writeFile(l.join(e,"tsconfig.json"),JSON.stringify(r,null,2))}async createTsupConfig(e){await h.writeFile(l.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
7
7
 
8
8
  export default defineConfig({
9
9
  entry: ['src/hooks.ts'],
@@ -246,7 +246,7 @@ module.exports = {
246
246
  logger.info('Service stopping')
247
247
  }
248
248
  }
249
- `}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# ${t} Service with Layered Riptide Security
249
+ `}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# DeEEP Network Service for ${t}
250
250
 
251
251
  # ----------------------------------------
252
252
  # Base
@@ -278,7 +278,7 @@ RUN pnpm turbo run build --filter=reef-${t}...
278
278
  RUN pnpm deploy --prod --filter reef-${t} deploy
279
279
 
280
280
  # ----------------------------------------
281
- # Third-party Base (customize as needed)
281
+ # Third-party Base
282
282
  # ----------------------------------------
283
283
  FROM node:22-alpine AS third-party-base
284
284
 
@@ -290,20 +290,14 @@ RUN apk add --no-cache \\
290
290
  ca-certificates
291
291
 
292
292
  # ----------------------------------------
293
- # Final Layer: Controlled Riptide Runtime
293
+ # Final Layer: Riptide Runtime
294
294
  # ----------------------------------------
295
295
  FROM third-party-base
296
-
297
- # \u{1F512} SECURITY LAYER: Inject controlled Riptide runtime
298
- # This layer comes from your signed, verified base image
299
296
  COPY --from=quay.io/nerdnode/riptide:latest /usr/local/bin/riptide /usr/local/bin/riptide
300
297
  COPY --from=quay.io/nerdnode/riptide:latest /riptide-runtime/ /riptide-runtime/
301
298
 
302
- # Verify Riptide binary permissions and ownership
303
299
  RUN chmod +x /usr/local/bin/riptide && \\
304
300
  chown root:root /usr/local/bin/riptide
305
-
306
- # Set up environment
307
301
  ENV NODE_ENV=production
308
302
  ENV PATH="/usr/local/bin:$PATH"
309
303
 
@@ -312,18 +306,14 @@ WORKDIR /riptide/reef/${t}
312
306
  COPY --from=builder /app/deploy/dist ./dist
313
307
  COPY --from=builder /app/deploy/node_modules ./node_modules
314
308
  COPY --from=builder /app/deploy/package.json ./package.json
315
-
316
- # Copy Riptide configuration directly from source
317
309
  COPY reef/${t}/riptide.config.json ./riptide.config.json
318
310
 
319
- # Expose health check port
311
+ # Expose riptide health check port
320
312
  EXPOSE 3000
321
313
 
322
- # \u{1F6E1}\uFE0F SECURITY: Use controlled Riptide binary directly (bypasses npm)
323
- # This ensures we use YOUR verified Riptide, not workspace dependencies
324
314
  ENTRYPOINT ["/usr/local/bin/riptide"]
325
315
  CMD ["start", "--config", "/riptide/reef/${t}/riptide.config.json", "--hooks", "/riptide/reef/${t}/dist/hooks.js"]
326
- `:`# ${t} Service with Layered Riptide Security
316
+ `:`# DeEEP Network Service for ${t}
327
317
 
328
318
  # ----------------------------------------
329
319
  # Builder Stage
@@ -365,20 +355,14 @@ RUN apk add --no-cache \\
365
355
  ca-certificates
366
356
 
367
357
  # ----------------------------------------
368
- # Final Layer: Controlled Riptide Runtime
358
+ # Final Layer: Riptide Runtime
369
359
  # ----------------------------------------
370
360
  FROM third-party-base
371
-
372
- # \u{1F512} SECURITY LAYER: Inject controlled Riptide runtime
373
- # This layer comes from your signed, verified base image
374
361
  COPY --from=quay.io/nerdnode/riptide:latest /usr/local/bin/riptide /usr/local/bin/riptide
375
362
  COPY --from=quay.io/nerdnode/riptide:latest /riptide-runtime/ /riptide-runtime/
376
363
 
377
- # Verify Riptide binary permissions and ownership
378
364
  RUN chmod +x /usr/local/bin/riptide && \\
379
365
  chown root:root /usr/local/bin/riptide
380
-
381
- # Set up environment
382
366
  ENV NODE_ENV=production
383
367
  ENV PATH="/usr/local/bin:$PATH"
384
368
 
@@ -387,11 +371,9 @@ WORKDIR /app
387
371
  COPY --from=builder /app/dist ./dist
388
372
  COPY --from=builder /app/node_modules ./node_modules
389
373
  COPY --from=builder /app/package.json ./package.json
390
-
391
- # Copy Riptide configuration directly from source
392
374
  COPY riptide.config.json ./riptide.config.json
393
375
 
394
- # Expose health check port
376
+ # Expose riptide health check port
395
377
  EXPOSE 3000
396
378
 
397
379
  # \u{1F6E1}\uFE0F SECURITY: Use controlled Riptide binary directly (bypasses npm)
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- "use strict";var Q=Object.create;var v=Object.defineProperty;var Z=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.getPrototypeOf,re=Object.prototype.hasOwnProperty;var a=(r,e)=>v(r,"name",{value:e,configurable:!0});var oe=(r,e)=>{for(var t in e)v(r,t,{get:e[t],enumerable:!0})},j=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ee(e))!re.call(r,i)&&i!==t&&v(r,i,{get:()=>e[i],enumerable:!(o=Z(e,i))||o.enumerable});return r};var m=(r,e,t)=>(t=r!=null?Q(te(r)):{},j(e||!r||!r.__esModule?v(t,"default",{value:r,enumerable:!0}):t,r)),ie=r=>j(v({},"__esModule",{value:!0}),r);var pe={};oe(pe,{AlreadyRunningError:()=>x,DiagnoseRequiredError:()=>w,InvalidSecretError:()=>C,MissingSecretError:()=>S,RiptideEntrypoint:()=>H,RiptideError:()=>h,ServiceScaffolder:()=>k,createChildLogger:()=>A,createLogger:()=>b,createUtilityContext:()=>R,expandEnvironmentVariables:()=>P,getDefaultConfig:()=>G,initService:()=>z,isAlreadyRunningError:()=>N,isDiagnoseRequiredError:()=>M,isInvalidSecretError:()=>_,isMissingSecretError:()=>D,isRiptideError:()=>y,loadConfig:()=>J,loadHooks:()=>K,mergeConfigs:()=>B,parseEnvironmentVariables:()=>U,redactSecret:()=>L,validateConfig:()=>I});module.exports=ie(pe);var h=class r extends Error{static{a(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,r.prototype)}},w=class r extends h{static{a(this,"DiagnoseRequiredError")}exitCode=3;constructor(e){super(e),this.name="DiagnoseRequiredError",Object.setPrototypeOf(this,r.prototype)}},S=class r extends h{static{a(this,"MissingSecretError")}exitCode=4;constructor(e){super(e),this.name="MissingSecretError",Object.setPrototypeOf(this,r.prototype)}},C=class r extends h{static{a(this,"InvalidSecretError")}exitCode=5;constructor(e){super(e),this.name="InvalidSecretError",Object.setPrototypeOf(this,r.prototype)}},x=class r extends h{static{a(this,"AlreadyRunningError")}exitCode=6;constructor(e){super(e),this.name="AlreadyRunningError",Object.setPrototypeOf(this,r.prototype)}};function y(r){return r instanceof Error&&"exitCode"in r&&typeof r.exitCode=="number"}a(y,"isRiptideError");function M(r){return r instanceof Error&&r.name==="DiagnoseRequiredError"&&r.exitCode===3}a(M,"isDiagnoseRequiredError");function D(r){return r instanceof Error&&r.name==="MissingSecretError"&&r.exitCode===4}a(D,"isMissingSecretError");function _(r){return r instanceof Error&&r.name==="InvalidSecretError"&&r.exitCode===5}a(_,"isInvalidSecretError");function N(r){return r instanceof Error&&r.name==="AlreadyRunningError"&&r.exitCode===6}a(N,"isAlreadyRunningError");var E=m(require("pino"));function b(r){let{level:e="info",format:t="pretty",serviceName:o}=r,i={level:e,timestamp:E.default.stdTimeFunctions.isoTime,formatters:{log:a(s=>({service:o,...s}),"log"),bindings:a(s=>{let{pid:n,hostname:c,...u}=s;return u},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,E.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,E.default)(i)}a(b,"createLogger");function A(r,e){return r.child(e)}a(A,"createChildLogger");var F=require("child_process"),g=require("fs"),se=m(require("http")),ne=m(require("https")),T=m(require("path")),Y=require("util");var ae=(0,Y.promisify)(F.exec);function R(){return{sleep:a(r=>new Promise(e=>setTimeout(e,r)),"sleep"),retry:a(async(r,e={})=>{let{maxAttempts:t=3,delay:o=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,n,c=o;for(let u=1;u<=t;u++)try{return await r()}catch(l){if(n=l instanceof Error?l:new Error(String(l)),u===t)throw n;await new Promise(f=>setTimeout(f,Math.min(c,s))),c*=i}throw n},"retry"),execCommand:a(async(r,e={})=>{let{timeout:t=3e4,cwd:o=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:n}=await ae(r,{timeout:t,cwd:o,env:{...process.env,...i}});return{stdout:s.trim(),stderr:n.trim(),exitCode:0}}catch(s){if(s.killed&&s.signal==="SIGTERM"){let c=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${r}', { timeout: ${t*2} })`,u=s.stderr?.trim()||"",l=u?`${u}
1
+ "use strict";var Q=Object.create;var v=Object.defineProperty;var Z=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.getPrototypeOf,re=Object.prototype.hasOwnProperty;var a=(r,e)=>v(r,"name",{value:e,configurable:!0});var oe=(r,e)=>{for(var t in e)v(r,t,{get:e[t],enumerable:!0})},D=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of ee(e))!re.call(r,i)&&i!==t&&v(r,i,{get:()=>e[i],enumerable:!(o=Z(e,i))||o.enumerable});return r};var m=(r,e,t)=>(t=r!=null?Q(te(r)):{},D(e||!r||!r.__esModule?v(t,"default",{value:r,enumerable:!0}):t,r)),ie=r=>D(v({},"__esModule",{value:!0}),r);var pe={};oe(pe,{AlreadyRunningError:()=>x,DiagnoseRequiredError:()=>w,InvalidSecretError:()=>C,MissingSecretError:()=>S,RiptideEntrypoint:()=>H,RiptideError:()=>h,ServiceScaffolder:()=>k,createChildLogger:()=>A,createLogger:()=>b,createUtilityContext:()=>P,expandEnvironmentVariables:()=>R,getDefaultConfig:()=>G,initService:()=>z,isAlreadyRunningError:()=>N,isDiagnoseRequiredError:()=>M,isInvalidSecretError:()=>_,isMissingSecretError:()=>j,isRiptideError:()=>y,loadConfig:()=>V,loadHooks:()=>K,mergeConfigs:()=>B,parseEnvironmentVariables:()=>L,redactSecret:()=>q,validateConfig:()=>I});module.exports=ie(pe);var h=class r extends Error{static{a(this,"RiptideError")}constructor(e){super(e),Object.setPrototypeOf(this,r.prototype)}},w=class r extends h{static{a(this,"DiagnoseRequiredError")}exitCode=3;constructor(e){super(e),this.name="DiagnoseRequiredError",Object.setPrototypeOf(this,r.prototype)}},S=class r extends h{static{a(this,"MissingSecretError")}exitCode=4;constructor(e){super(e),this.name="MissingSecretError",Object.setPrototypeOf(this,r.prototype)}},C=class r extends h{static{a(this,"InvalidSecretError")}exitCode=5;constructor(e){super(e),this.name="InvalidSecretError",Object.setPrototypeOf(this,r.prototype)}},x=class r extends h{static{a(this,"AlreadyRunningError")}exitCode=6;constructor(e){super(e),this.name="AlreadyRunningError",Object.setPrototypeOf(this,r.prototype)}};function y(r){return r instanceof Error&&"exitCode"in r&&typeof r.exitCode=="number"}a(y,"isRiptideError");function M(r){return r instanceof Error&&r.name==="DiagnoseRequiredError"&&r.exitCode===3}a(M,"isDiagnoseRequiredError");function j(r){return r instanceof Error&&r.name==="MissingSecretError"&&r.exitCode===4}a(j,"isMissingSecretError");function _(r){return r instanceof Error&&r.name==="InvalidSecretError"&&r.exitCode===5}a(_,"isInvalidSecretError");function N(r){return r instanceof Error&&r.name==="AlreadyRunningError"&&r.exitCode===6}a(N,"isAlreadyRunningError");var E=m(require("pino"));function b(r){let{level:e="info",format:t="pretty",serviceName:o}=r,i={level:e,timestamp:E.default.stdTimeFunctions.isoTime,formatters:{log:a(s=>({service:o,...s}),"log"),bindings:a(s=>{let{pid:n,hostname:c,...u}=s;return u},"bindings")}};return t==="pretty"&&!process.env.NODE_ENV?.includes("prod")?(0,E.default)({...i,transport:{target:"pino-pretty",options:{colorize:!0,translateTime:"SYS:standard",ignore:"pid,hostname"}}}):(0,E.default)(i)}a(b,"createLogger");function A(r,e){return r.child(e)}a(A,"createChildLogger");var F=require("child_process"),g=require("fs"),se=m(require("http")),ne=m(require("https")),T=m(require("path")),Y=require("util");var ae=(0,Y.promisify)(F.exec);function P(){return{sleep:a(r=>new Promise(e=>setTimeout(e,r)),"sleep"),retry:a(async(r,e={})=>{let{maxAttempts:t=3,delay:o=1e3,backoffMultiplier:i=2,maxDelay:s=3e4}=e,n,c=o;for(let u=1;u<=t;u++)try{return await r()}catch(l){if(n=l instanceof Error?l:new Error(String(l)),u===t)throw n;await new Promise(f=>setTimeout(f,Math.min(c,s))),c*=i}throw n},"retry"),execCommand:a(async(r,e={})=>{let{timeout:t=3e4,cwd:o=process.cwd(),env:i=process.env}=e;try{let{stdout:s,stderr:n}=await ae(r,{timeout:t,cwd:o,env:{...process.env,...i}});return{stdout:s.trim(),stderr:n.trim(),exitCode:0}}catch(s){if(s.killed&&s.signal==="SIGTERM"){let c=`Command timed out after ${t/1e3}s. Consider increasing timeout if command needs more time to complete: utils.execCommand('${r}', { timeout: ${t*2} })`,u=s.stderr?.trim()||"",l=u?`${u}
2
2
 
3
3
  ${c}`:c,f=[];s.stdout?.trim()&&f.push(`STDOUT: ${s.stdout.trim()}`),l&&f.push(`STDERR: ${l}`);let X=[`Command execution timed out: ${r}`,...f].join(`
4
4
 
5
- `),$=new Error(X);throw $.name="CommandTimeoutError",$}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:a(async(r,e,t={})=>{let{mode:o="0644",encoding:i="utf8"}=t;await g.promises.mkdir(T.dirname(r),{recursive:!0}),await g.promises.writeFile(r,e,{encoding:i}),await g.promises.chmod(r,o)},"writeFile"),readFile:a(async r=>await g.promises.readFile(r,"utf8"),"readFile"),fileExists:a(async r=>{try{return await g.promises.access(r),!0}catch{return!1}},"fileExists"),downloadFile:a(async function r(e,t){return new Promise((o,i)=>{let s=T.dirname(t);g.promises.mkdir(s,{recursive:!0}).then(()=>{let n=(0,g.createWriteStream)(t);(e.startsWith("https:")?ne:se).get(e,l=>{if(l.statusCode===200)l.pipe(n),n.on("finish",()=>{n.close(),o(t)});else if(l.statusCode===301||l.statusCode===302){let f=l.headers.location;f?o(r(f,t)):i(new Error(`Redirect without location header: ${l.statusCode}`))}else i(new Error(`Failed to download: ${l.statusCode} ${l.statusMessage}`))}).on("error",l=>{g.promises.unlink(t).catch(()=>{}),i(l)}),n.on("error",l=>{g.promises.unlink(t).catch(()=>{}),i(l)})}).catch(i)})},"downloadFile")}}a(R,"createUtilityContext");function L(r){return!r||r.length<=10?"[REDACTED]":`${r.slice(0,4)}...${r.slice(-4)}`}a(L,"redactSecret");function U(r){let e={},t=r.split(`
6
- `);for(let o of t){let i=o.trim();if(i&&!i.startsWith("#")){let[s,...n]=i.split("=");s&&n.length>0&&(e[s.trim()]=n.join("=").trim())}}return e}a(U,"parseEnvironmentVariables");function P(r,e=process.env){return r.replace(/\$\{([^}]+)\}/g,(t,o)=>e[o]||t)}a(P,"expandEnvironmentVariables");var q=require("http");var O=class{constructor(e,t,o,i,s){this.getStatus=o;this.executeHealthCheck=i;this.getMetrics=s;this.port=e,this.logger=t.child({component:"web-server"})}static{a(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,q.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let o=await this.executeHealthCheck(),i=this.getStatus(),s={healthy:o,status:i.status,uptime:i.uptime,message:i.message};o?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(o){this.logger.error({error:o},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:o instanceof Error?o.message:"Internal server error"}))}else if(e.url==="/metrics")try{let o=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(o)}catch(o){this.logger.error({error:o},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:o instanceof Error?o.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",o=>{this.logger.error({error:o,port:this.port},"Failed to start web server"),t(o)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var H=class{static{a(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;heartbeatInterval;webServer;constructor(e,t,o={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.logger=b({serviceName:t.service.name,level:o.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},y(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),process.exit(e.exitCode)):process.exit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook, continuing...");return}this.logger.info("Installing secrets...");try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);let o=this.createHookContext(),i=Date.now();try{let s=await t(o),n=Date.now()-i;return this.logger.debug(`Hook ${e} completed in ${n}ms`),s}catch(s){let n=Date.now()-i;throw this.logger.error({hookName:e,duration:n,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},"Hook threw an exception, will not continue"),s}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(o){return this.logger.warn({hookName:e,error:o instanceof Error?o.message:String(o)},"Optional hook execution failed but continuing"),null}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:R()}}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),process.exit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),process.exit(1))})}setupSignalHandlers(){let e=a(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.webServer&&await this.webServer.stop(),await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(o){this.logger.error({error:o instanceof Error?o.message:String(o)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;try{let o=await this.executeHookSafely("heartbeat");if(o===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat");return}await this.pingSonar(o)}catch(o){this.logger.warn({error:o instanceof Error?o.message:String(o)},"Heartbeat execution failed")}finally{t=!1}},e)}async pingSonar(e){let t=process.env.SONAR_API_URL,o=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!o||!i){this.logger.info({hasUrl:!!t,hasKey:!!o,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor(Date.now()/1e3),n={entity_id:i,client_timestamp:s,metadata:e};try{this.logger.info({payload:n,sonarUrl:t,sonarApiKey:o,nomadJobId:i},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify(n)});if(!c.ok){let u=await c.text();this.logger.warn({status:c.status,error:u,entity_id:i},"Sonar API heartbeat failed")}}catch(c){this.logger.error({error:c instanceof Error?c.message:String(c),errorType:c?.constructor?.name,errorCause:c instanceof Error&&"cause"in c?c.cause:void 0,stack:c instanceof Error?c.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:i},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new O(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>await this.executeHookSafely("health")===!0,async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=a(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var W=require("fs"),V=m(require("path"));async function J(r){try{let e=await W.promises.readFile(r,"utf8"),t=JSON.parse(e),o=ce(t);return I(o),o}catch(e){throw new Error(`Failed to load config from ${r}: ${e instanceof Error?e.message:String(e)}`)}}a(J,"loadConfig");async function K(r){try{let e=V.resolve(r);delete require.cache[e];let t=require(e);if(typeof t.start!="function")throw new Error('Hooks module must export a "start" function');return t}catch(e){throw new Error(`Failed to load hooks from ${r}: ${e instanceof Error?e.message:String(e)}`)}}a(K,"loadHooks");function I(r){if(!r.service?.name)throw new Error("Config must have service.name");if(r.health,r.heartbeat&&r.heartbeat.interval&&r.heartbeat.interval<1e3)throw new Error("heartbeat.interval must be at least 1000ms")}a(I,"validateConfig");function ce(r,e=process.env){let t=JSON.parse(JSON.stringify(r));function o(i){if(typeof i=="string")return P(i,e);if(Array.isArray(i))return i.map(o);if(i&&typeof i=="object"){let s={};for(let[n,c]of Object.entries(i))s[n]=o(c);return s}return i}return a(o,"expandObject"),o(t)}a(ce,"expandConfigVariables");function G(){return{health:{port:3e3},heartbeat:{interval:6e4,enabled:!1},logging:{level:"info",format:"pretty"}}}a(G,"getDefaultConfig");function B(r,e){function t(i,s){if(s&&typeof s=="object"&&!Array.isArray(s))for(let n in s)s.hasOwnProperty(n)&&(i[n]&&typeof i[n]=="object"&&!Array.isArray(i[n])?i[n]=t(i[n],s[n]):i[n]=s[n]);return i}a(t,"deepMerge");let o=t({...r},e);return I(o),o}a(B,"mergeConfigs");var d=m(require("fs/promises")),p=m(require("path"));var k=class{constructor(e){this.logger=e}static{a(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:o=".",template:i="basic",description:s}=e,n=p.join(o,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${i}`),this.logger.info(`Location: ${n}`),await this.createDirectoryStructure(n),await this.createPackageJson(n,t,s),await this.createRiptideConfig(n,t,s),await this.createTsConfig(n),await this.createTsupConfig(n),await this.createHooks(n,i),await this.createDockerfile(n,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,n)}async createDirectoryStructure(e){await d.mkdir(p.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,o){let i=await this.checkIfInWorkspace(e),s=i?"workspace:*":await this.getRiptideVersion(),n=i?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f reef/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,c={name:`reef-${t}`,version:"1.0.0",description:o||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":n,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":s},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await d.writeFile(p.join(e,"package.json"),JSON.stringify(c,null,2))}async createRiptideConfig(e,t,o){let i={service:{name:t,version:"1.0.0",description:o||`${t} service`},logging:{level:"info"}};await d.writeFile(p.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let o=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await d.writeFile(p.join(e,"tsconfig.json"),JSON.stringify(o,null,2))}async createTsupConfig(e){await d.writeFile(p.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
5
+ `),$=new Error(X);throw $.name="CommandTimeoutError",$}return{stdout:s.stdout?.trim()||"",stderr:s.stderr?.trim()||s.message,exitCode:s.code||1}}},"execCommand"),writeFile:a(async(r,e,t={})=>{let{mode:o="0644",encoding:i="utf8"}=t;await g.promises.mkdir(T.dirname(r),{recursive:!0}),await g.promises.writeFile(r,e,{encoding:i}),await g.promises.chmod(r,o)},"writeFile"),readFile:a(async r=>await g.promises.readFile(r,"utf8"),"readFile"),fileExists:a(async r=>{try{return await g.promises.access(r),!0}catch{return!1}},"fileExists"),downloadFile:a(async function r(e,t){return new Promise((o,i)=>{let s=T.dirname(t);g.promises.mkdir(s,{recursive:!0}).then(()=>{let n=(0,g.createWriteStream)(t);(e.startsWith("https:")?ne:se).get(e,l=>{if(l.statusCode===200)l.pipe(n),n.on("finish",()=>{n.close(),o(t)});else if(l.statusCode===301||l.statusCode===302){let f=l.headers.location;f?o(r(f,t)):i(new Error(`Redirect without location header: ${l.statusCode}`))}else i(new Error(`Failed to download: ${l.statusCode} ${l.statusMessage}`))}).on("error",l=>{g.promises.unlink(t).catch(()=>{}),i(l)}),n.on("error",l=>{g.promises.unlink(t).catch(()=>{}),i(l)})}).catch(i)})},"downloadFile")}}a(P,"createUtilityContext");function q(r){return!r||r.length<=10?"[REDACTED]":`${r.slice(0,4)}...${r.slice(-4)}`}a(q,"redactSecret");function L(r){let e={},t=r.split(`
6
+ `);for(let o of t){let i=o.trim();if(i&&!i.startsWith("#")){let[s,...n]=i.split("=");s&&n.length>0&&(e[s.trim()]=n.join("=").trim())}}return e}a(L,"parseEnvironmentVariables");function R(r,e=process.env){return r.replace(/\$\{([^}]+)\}/g,(t,o)=>e[o]||t)}a(R,"expandEnvironmentVariables");var W=require("http");var O=class{constructor(e,t,o,i,s){this.getStatus=o;this.executeHealthCheck=i;this.getMetrics=s;this.port=e,this.logger=t.child({component:"web-server"})}static{a(this,"WebServer")}server=null;port;logger;async start(){if(this.server){this.logger.warn("Web server already running");return}return this.server=(0,W.createServer)(async(e,t)=>{if(e.method!=="GET"){t.writeHead(405,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Method not allowed"}));return}if(e.url==="/health")try{let o=await this.executeHealthCheck(),i=this.getStatus(),s={healthy:o,status:i.status,uptime:i.uptime,message:i.message};o?t.writeHead(200,{"Content-Type":"application/json"}):t.writeHead(503,{"Content-Type":"application/json"}),t.end(JSON.stringify(s))}catch(o){this.logger.error({error:o},"Health check endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({healthy:!1,error:o instanceof Error?o.message:"Internal server error"}))}else if(e.url==="/metrics")try{let o=await this.getMetrics();t.writeHead(200,{"Content-Type":"text/plain; version=0.0.4"}),t.end(o)}catch(o){this.logger.error({error:o},"Metrics endpoint error"),t.writeHead(500,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:o instanceof Error?o.message:"Internal server error"}))}else t.writeHead(404,{"Content-Type":"application/json"}),t.end(JSON.stringify({error:"Not found"}))}),new Promise((e,t)=>{this.server.once("error",o=>{this.logger.error({error:o,port:this.port},"Failed to start web server"),t(o)}),this.server.listen(this.port,()=>{e()})})}async stop(){if(this.server)return new Promise(e=>{this.server.close(()=>{this.logger.info("Web server stopped"),this.server=null,e()})})}};var H=class{static{a(this,"RiptideEntrypoint")}logger;hooks;config;isShuttingDown=!1;status={status:"starting"};startTime;heartbeatInterval;webServer;constructor(e,t,o={}){this.hooks=e,this.config=t,this.startTime=Date.now(),this.logger=b({serviceName:t.service.name,level:o.logLevel||t.logging?.level||"info",format:process.env.NODE_ENV==="production"?"json":t.logging?.format||"pretty"}),this.setupGlobalErrorHandlers(),this.setupSignalHandlers()}async start(){try{let e=this.config.service.version||"unknown",t=process.env.NODE_ENV||"production";this.logger.info({version:e,environment:t},`Starting ${this.config.service.name}`),await this.processSecrets(),await this.executeHook("start"),await this.startWebServer(this.config.health?.port||3e3),this.startHeartbeat(),this.status={status:"healthy",uptime:Date.now()-this.startTime,message:"Service started successfully"},this.logger.info({service:this.config.service.name},`${this.config.service.name} service is ready`),await this.waitForShutdown()}catch(e){this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0},`Failed to start ${this.config.service.name} service`),this.status={status:"unhealthy",message:e instanceof Error?e.message:String(e)},y(e)?(this.logger.error(`Exiting with code ${e.exitCode} (${e.name})`),process.exit(e.exitCode)):process.exit(1)}}async processSecrets(){if(!this.hooks.installSecrets){this.logger.info("No installSecrets hook, continuing...");return}this.logger.info("Installing secrets...");try{await this.executeHook("installSecrets")}catch(e){throw this.logger.error({error:e instanceof Error?e.message:String(e),stack:e instanceof Error?e.stack:void 0,hookName:"installSecrets"},"Failed to install secrets"),e}}async executeHook(e){let t=this.hooks[e];if(typeof t!="function")throw new Error(`Required hook '${e}' not found`);let o=this.createHookContext(),i=Date.now();try{let s=await t(o),n=Date.now()-i;return this.logger.debug(`Hook ${e} completed in ${n}ms`),s}catch(s){let n=Date.now()-i;throw this.logger.error({hookName:e,duration:n,error:s instanceof Error?s.message:String(s),stack:s instanceof Error?s.stack:void 0},"Hook threw an exception, will not continue"),s}}async executeHookSafely(e){if(typeof this.hooks[e]!="function")return this.logger.debug(`Optional hook '${e}' not found, skipping`),null;try{return await this.executeHook(e)}catch(o){return this.logger.warn({hookName:e,error:o instanceof Error?o.message:String(o)},"Optional hook execution failed but continuing"),null}}createHookContext(){return{config:this.config,logger:this.logger,env:process.env,utils:P()}}setupGlobalErrorHandlers(){process.on("uncaughtException",e=>{this.logger.error({error:e.message,stack:e.stack},"Uncaught exception"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled exception`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled exception"),process.exit(1))}),process.on("unhandledRejection",(e,t)=>{this.logger.error({reason:e,promise:t},"Unhandled promise rejection"),y(e)?(this.logger.info(`Exiting with code ${e.exitCode} (${e.name}) from unhandled rejection`),process.exit(e.exitCode)):(this.logger.error("Exiting with code 1 due to unhandled promise rejection"),process.exit(1))})}setupSignalHandlers(){let e=a(async t=>{this.isShuttingDown&&(this.logger.warn("Force shutdown signal received"),process.exit(1)),this.isShuttingDown=!0,this.status={status:"stopping",message:`Received ${t} signal`},this.logger.info(`${t} signal received, starting graceful shutdown...`);try{this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.webServer&&await this.webServer.stop(),await this.executeHookSafely("stop"),this.status={status:"stopped",message:"Graceful shutdown completed"},this.logger.info("Graceful shutdown completed"),process.exit(0)}catch(o){this.logger.error({error:o instanceof Error?o.message:String(o)},"Error during graceful shutdown"),process.exit(1)}},"gracefulShutdown");process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}startHeartbeat(){if(!this.hooks.heartbeat){this.logger.info("No heartbeat hook defined, skipping heartbeat");return}if(!this.config.heartbeat?.enabled){this.logger.info("Heartbeat disabled in config, skipping...");return}let e=this.config.heartbeat?.interval||6e4;this.logger.info({interval:e},"Starting heartbeat");let t=!1;this.heartbeatInterval=setInterval(async()=>{if(t){this.logger.info("Heartbeat still executing, skipping this interval");return}t=!0;try{let o=await this.executeHookSafely("heartbeat");if(o===null){this.logger.info("Heartbeat hook returned null, skipping heartbeat");return}await this.pingSonar(o)}catch(o){this.logger.warn({error:o instanceof Error?o.message:String(o)},"Heartbeat execution failed")}finally{t=!1}},e)}async pingSonar(e){let t=process.env.SONAR_API_URL,o=process.env.SONAR_API_KEY,i=process.env.NOMAD_JOB_NAME;if(!t||!o||!i){this.logger.info({hasUrl:!!t,hasKey:!!o,hasJobId:!!i},"Sonar API configuration incomplete, skipping heartbeat send");return}let s=Math.floor(Date.now()/1e3),n={entity_id:i,client_timestamp:s,metadata:e};try{this.logger.info({payload:n,sonarUrl:t,sonarApiKey:o,nomadJobId:i},"Sending heartbeat to Sonar API");let c=await fetch(`${t}/api/v1/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify(n)});if(!c.ok){let u=await c.text();this.logger.warn({status:c.status,error:u,entity_id:i},"Sonar API heartbeat failed")}}catch(c){this.logger.error({error:c instanceof Error?c.message:String(c),errorType:c?.constructor?.name,errorCause:c instanceof Error&&"cause"in c?c.cause:void 0,stack:c instanceof Error?c.stack:void 0,url:`${t}/api/v1/heartbeat`,entity_id:i},"Failed to send heartbeat to Sonar API")}}async startWebServer(e){this.webServer=new O(e,this.logger,()=>({...this.status,uptime:Date.now()-this.startTime}),async()=>await this.executeHookSafely("health")===!0,async()=>await this.getMetrics());try{await this.webServer.start()}catch(t){this.logger.error({error:t,port:e},"Failed to start web server")}}async waitForShutdown(){return new Promise(e=>{let t=a(()=>{this.isShuttingDown?e():setTimeout(t,100)},"checkShutdown");t()})}async getMetrics(){if(!this.hooks.metrics)return{uptime:Date.now()-this.startTime,status:this.status.status};try{return await this.executeHookSafely("metrics")}catch{return{uptime:Date.now()-this.startTime,status:this.status.status}}}};var U=require("fs"),J=m(require("path"));async function V(r){try{let e=await U.promises.readFile(r,"utf8"),t=JSON.parse(e),o=ce(t);return I(o),o}catch(e){throw new Error(`Failed to load config from ${r}: ${e instanceof Error?e.message:String(e)}`)}}a(V,"loadConfig");async function K(r){try{let e=J.resolve(r);delete require.cache[e];let t=require(e);if(typeof t.start!="function")throw new Error('Hooks module must export a "start" function');return t}catch(e){throw new Error(`Failed to load hooks from ${r}: ${e instanceof Error?e.message:String(e)}`)}}a(K,"loadHooks");function I(r){if(!r.service?.name)throw new Error("Config must have service.name");if(r.health,r.heartbeat&&r.heartbeat.interval&&r.heartbeat.interval<1e3)throw new Error("heartbeat.interval must be at least 1000ms")}a(I,"validateConfig");function ce(r,e=process.env){let t=JSON.parse(JSON.stringify(r));function o(i){if(typeof i=="string")return R(i,e);if(Array.isArray(i))return i.map(o);if(i&&typeof i=="object"){let s={};for(let[n,c]of Object.entries(i))s[n]=o(c);return s}return i}return a(o,"expandObject"),o(t)}a(ce,"expandConfigVariables");function G(){return{health:{port:3e3},heartbeat:{interval:6e4,enabled:!1},logging:{level:"info",format:"pretty"}}}a(G,"getDefaultConfig");function B(r,e){function t(i,s){if(s&&typeof s=="object"&&!Array.isArray(s))for(let n in s)s.hasOwnProperty(n)&&(i[n]&&typeof i[n]=="object"&&!Array.isArray(i[n])?i[n]=t(i[n],s[n]):i[n]=s[n]);return i}a(t,"deepMerge");let o=t({...r},e);return I(o),o}a(B,"mergeConfigs");var d=m(require("fs/promises")),p=m(require("path"));var k=class{constructor(e){this.logger=e}static{a(this,"ServiceScaffolder")}async scaffold(e){let{serviceName:t,targetPath:o=".",template:i="basic",description:s}=e,n=p.join(o,t);this.logger.info(`Creating new Coral Reef service: ${t}`),this.logger.info(`Template: ${i}`),this.logger.info(`Location: ${n}`),await this.createDirectoryStructure(n),await this.createPackageJson(n,t,s),await this.createRiptideConfig(n,t,s),await this.createTsConfig(n),await this.createTsupConfig(n),await this.createHooks(n,i),await this.createDockerfile(n,t),this.logger.info("\u2705 Service scaffolding complete!"),this.showNextSteps(t,n)}async createDirectoryStructure(e){await d.mkdir(p.join(e,"src"),{recursive:!0})}async createPackageJson(e,t,o){let i=await this.checkIfInWorkspace(e),s=i?"workspace:*":await this.getRiptideVersion(),n=i?`cd ../.. && docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} -f reef/${t}/Dockerfile .`:`docker build --platform \${DOCKER_PLATFORM:-linux/amd64} --progress=plain -t reef-${t} .`,c={name:`reef-${t}`,version:"1.0.0",description:o||`${t} service for Coral Reef`,main:"dist/hooks.js",scripts:{build:"tsc --noEmit && tsup","build:docker":n,clean:"rm -rf dist",start:"npx @deeep-network/riptide start --hooks dist/hooks.js",validate:"pnpm run build && npx @deeep-network/riptide validate --hooks dist/hooks.js","type-check":"tsc --noEmit"},dependencies:{"@deeep-network/riptide":s},devDependencies:{typescript:"^5.8.3",tsup:"^8.5.0","@types/node":"^20.0.0"},engines:{node:">=22.0.0"}};await d.writeFile(p.join(e,"package.json"),JSON.stringify(c,null,2))}async createRiptideConfig(e,t,o){let i={service:{name:t,version:"1.0.0",description:o||`${t} service`},logging:{level:"info"}};await d.writeFile(p.join(e,"riptide.config.json"),JSON.stringify(i,null,2))}async createTsConfig(e){let o=await this.checkIfInWorkspace(e)?{extends:"../../tsconfig.json",compilerOptions:{outDir:"./dist",rootDir:"./src",declaration:!0,declarationMap:!0,sourceMap:!0},include:["src/**/*"],exclude:["dist","node_modules"]}:{compilerOptions:{target:"ES2022",module:"commonjs",lib:["ES2022"],outDir:"./dist",rootDir:"./src",strict:!0,esModuleInterop:!0,skipLibCheck:!0,forceConsistentCasingInFileNames:!0,declaration:!0,declarationMap:!0,sourceMap:!0,moduleResolution:"node"},include:["src/**/*"],exclude:["dist","node_modules"]};await d.writeFile(p.join(e,"tsconfig.json"),JSON.stringify(o,null,2))}async createTsupConfig(e){await d.writeFile(p.join(e,"tsup.config.ts"),`import { defineConfig } from 'tsup'
7
7
 
8
8
  export default defineConfig({
9
9
  entry: ['src/hooks.ts'],
@@ -246,7 +246,7 @@ module.exports = {
246
246
  logger.info('Service stopping')
247
247
  }
248
248
  }
249
- `}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# ${t} Service with Layered Riptide Security
249
+ `}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# DeEEP Network Service for ${t}
250
250
 
251
251
  # ----------------------------------------
252
252
  # Base
@@ -278,7 +278,7 @@ RUN pnpm turbo run build --filter=reef-${t}...
278
278
  RUN pnpm deploy --prod --filter reef-${t} deploy
279
279
 
280
280
  # ----------------------------------------
281
- # Third-party Base (customize as needed)
281
+ # Third-party Base
282
282
  # ----------------------------------------
283
283
  FROM node:22-alpine AS third-party-base
284
284
 
@@ -290,20 +290,14 @@ RUN apk add --no-cache \\
290
290
  ca-certificates
291
291
 
292
292
  # ----------------------------------------
293
- # Final Layer: Controlled Riptide Runtime
293
+ # Final Layer: Riptide Runtime
294
294
  # ----------------------------------------
295
295
  FROM third-party-base
296
-
297
- # \u{1F512} SECURITY LAYER: Inject controlled Riptide runtime
298
- # This layer comes from your signed, verified base image
299
296
  COPY --from=quay.io/nerdnode/riptide:latest /usr/local/bin/riptide /usr/local/bin/riptide
300
297
  COPY --from=quay.io/nerdnode/riptide:latest /riptide-runtime/ /riptide-runtime/
301
298
 
302
- # Verify Riptide binary permissions and ownership
303
299
  RUN chmod +x /usr/local/bin/riptide && \\
304
300
  chown root:root /usr/local/bin/riptide
305
-
306
- # Set up environment
307
301
  ENV NODE_ENV=production
308
302
  ENV PATH="/usr/local/bin:$PATH"
309
303
 
@@ -312,18 +306,14 @@ WORKDIR /riptide/reef/${t}
312
306
  COPY --from=builder /app/deploy/dist ./dist
313
307
  COPY --from=builder /app/deploy/node_modules ./node_modules
314
308
  COPY --from=builder /app/deploy/package.json ./package.json
315
-
316
- # Copy Riptide configuration directly from source
317
309
  COPY reef/${t}/riptide.config.json ./riptide.config.json
318
310
 
319
- # Expose health check port
311
+ # Expose riptide health check port
320
312
  EXPOSE 3000
321
313
 
322
- # \u{1F6E1}\uFE0F SECURITY: Use controlled Riptide binary directly (bypasses npm)
323
- # This ensures we use YOUR verified Riptide, not workspace dependencies
324
314
  ENTRYPOINT ["/usr/local/bin/riptide"]
325
315
  CMD ["start", "--config", "/riptide/reef/${t}/riptide.config.json", "--hooks", "/riptide/reef/${t}/dist/hooks.js"]
326
- `:`# ${t} Service with Layered Riptide Security
316
+ `:`# DeEEP Network Service for ${t}
327
317
 
328
318
  # ----------------------------------------
329
319
  # Builder Stage
@@ -365,20 +355,14 @@ RUN apk add --no-cache \\
365
355
  ca-certificates
366
356
 
367
357
  # ----------------------------------------
368
- # Final Layer: Controlled Riptide Runtime
358
+ # Final Layer: Riptide Runtime
369
359
  # ----------------------------------------
370
360
  FROM third-party-base
371
-
372
- # \u{1F512} SECURITY LAYER: Inject controlled Riptide runtime
373
- # This layer comes from your signed, verified base image
374
361
  COPY --from=quay.io/nerdnode/riptide:latest /usr/local/bin/riptide /usr/local/bin/riptide
375
362
  COPY --from=quay.io/nerdnode/riptide:latest /riptide-runtime/ /riptide-runtime/
376
363
 
377
- # Verify Riptide binary permissions and ownership
378
364
  RUN chmod +x /usr/local/bin/riptide && \\
379
365
  chown root:root /usr/local/bin/riptide
380
-
381
- # Set up environment
382
366
  ENV NODE_ENV=production
383
367
  ENV PATH="/usr/local/bin:$PATH"
384
368
 
@@ -387,11 +371,9 @@ WORKDIR /app
387
371
  COPY --from=builder /app/dist ./dist
388
372
  COPY --from=builder /app/node_modules ./node_modules
389
373
  COPY --from=builder /app/package.json ./package.json
390
-
391
- # Copy Riptide configuration directly from source
392
374
  COPY riptide.config.json ./riptide.config.json
393
375
 
394
- # Expose health check port
376
+ # Expose riptide health check port
395
377
  EXPOSE 3000
396
378
 
397
379
  # \u{1F6E1}\uFE0F SECURITY: Use controlled Riptide binary directly (bypasses npm)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeep-network/riptide",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Universal Riptide Runtime for the DeEEP Network",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,6 +14,10 @@
14
14
  "deploy:docker": "./scripts/deploy-docker.sh",
15
15
  "deploy:npm": "./scripts/deploy-npm.sh",
16
16
  "test:universal": "./tests/test-universal.sh",
17
+ "release": "./scripts/release.sh",
18
+ "release:minor": "./scripts/release.sh --type minor",
19
+ "release:major": "./scripts/release.sh --type major",
20
+ "release:dry-run": "./scripts/release.sh --dry-run",
17
21
  "dev": "tsup --watch",
18
22
  "clean": "rm -rf dist"
19
23
  },