@deeep-network/riptide 0.2.0 → 0.2.2
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 +95 -54
- package/dist/index.js +97 -57
- package/package.json +5 -2
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,
|
|
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}
|
|
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(C.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=C.dirname(t);f.promises.mkdir(s,{recursive:!0}).then(()=>{let a=(0,f.createWriteStream)(t);(e.startsWith("https:")?J:W).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 k=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)},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 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"),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=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 k(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 b=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(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'
|
|
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
|
|
249
|
+
`}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# ${t} Service with Layered Riptide Security
|
|
250
250
|
|
|
251
251
|
# ----------------------------------------
|
|
252
252
|
# Base
|
|
@@ -273,38 +273,57 @@ RUN pnpm install
|
|
|
273
273
|
COPY turbo.json tsconfig.json .
|
|
274
274
|
RUN pnpm turbo run build --filter=reef-${t}...
|
|
275
275
|
|
|
276
|
-
# Create pnpm deployment
|
|
276
|
+
# Create pnpm deployment (requires .npmrc to have inject-workspace-packages=true)
|
|
277
|
+
# This "materializes" the workspace packages into the node_modules directory
|
|
277
278
|
RUN pnpm deploy --prod --filter reef-${t} deploy
|
|
278
279
|
|
|
279
280
|
# ----------------------------------------
|
|
280
|
-
#
|
|
281
|
+
# Third-party Base (customize as needed)
|
|
281
282
|
# ----------------------------------------
|
|
282
|
-
FROM node:22-alpine AS
|
|
283
|
+
FROM node:22-alpine AS third-party-base
|
|
283
284
|
|
|
284
|
-
#
|
|
285
|
-
RUN
|
|
286
|
-
|
|
285
|
+
# Install additional tools needed for your service
|
|
286
|
+
RUN apk add --no-cache \\
|
|
287
|
+
curl \\
|
|
288
|
+
bash \\
|
|
289
|
+
procps \\
|
|
290
|
+
ca-certificates
|
|
287
291
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
COPY --from=builder --chown=app:app /app/deploy/node_modules ./node_modules
|
|
293
|
-
COPY --from=builder --chown=app:app /app/deploy/package.json ./package.json
|
|
294
|
-
COPY --from=builder --chown=app:app /app/deploy/riptide.config.json ./riptide.config.json
|
|
292
|
+
# ----------------------------------------
|
|
293
|
+
# Final Layer: Controlled Riptide Runtime
|
|
294
|
+
# ----------------------------------------
|
|
295
|
+
FROM third-party-base
|
|
295
296
|
|
|
296
|
-
#
|
|
297
|
-
|
|
297
|
+
# \u{1F512} SECURITY LAYER: Inject controlled Riptide runtime
|
|
298
|
+
# This layer comes from your signed, verified base image
|
|
299
|
+
COPY --from=quay.io/nerdnode/riptide:latest /usr/local/bin/riptide /usr/local/bin/riptide
|
|
300
|
+
COPY --from=quay.io/nerdnode/riptide:latest /riptide-runtime/ /riptide-runtime/
|
|
298
301
|
|
|
299
|
-
#
|
|
300
|
-
|
|
302
|
+
# Verify Riptide binary permissions and ownership
|
|
303
|
+
RUN chmod +x /usr/local/bin/riptide && \\
|
|
304
|
+
chown root:root /usr/local/bin/riptide
|
|
301
305
|
|
|
302
|
-
# Set environment
|
|
306
|
+
# Set up environment
|
|
303
307
|
ENV NODE_ENV=production
|
|
308
|
+
ENV PATH="/usr/local/bin:$PATH"
|
|
304
309
|
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
# Copy the built application (your hooks that manage the service)
|
|
311
|
+
WORKDIR /riptide/reef/${t}
|
|
312
|
+
COPY --from=builder /app/deploy/dist ./dist
|
|
313
|
+
COPY --from=builder /app/deploy/node_modules ./node_modules
|
|
314
|
+
COPY --from=builder /app/deploy/package.json ./package.json
|
|
315
|
+
|
|
316
|
+
# Copy Riptide configuration directly from source
|
|
317
|
+
COPY reef/${t}/riptide.config.json ./riptide.config.json
|
|
318
|
+
|
|
319
|
+
# Expose health check port
|
|
320
|
+
EXPOSE 3000
|
|
321
|
+
|
|
322
|
+
# \u{1F6E1}\uFE0F SECURITY: Use controlled Riptide binary directly (bypasses npm)
|
|
323
|
+
# This ensures we use YOUR verified Riptide, not workspace dependencies
|
|
324
|
+
ENTRYPOINT ["/usr/local/bin/riptide"]
|
|
325
|
+
CMD ["start", "--config", "/riptide/reef/${t}/riptide.config.json", "--hooks", "/riptide/reef/${t}/dist/hooks.js"]
|
|
326
|
+
`:`# ${t} Service with Layered Riptide Security
|
|
308
327
|
|
|
309
328
|
# ----------------------------------------
|
|
310
329
|
# Builder Stage
|
|
@@ -315,7 +334,8 @@ RUN apk add --no-cache libc6-compat
|
|
|
315
334
|
WORKDIR /app
|
|
316
335
|
|
|
317
336
|
# Copy package files
|
|
318
|
-
COPY package.json
|
|
337
|
+
COPY package.json ./
|
|
338
|
+
COPY pnpm-lock.yaml ./
|
|
319
339
|
|
|
320
340
|
# Install dependencies
|
|
321
341
|
RUN npm install -g pnpm@10.8.0 && \\
|
|
@@ -324,7 +344,6 @@ RUN npm install -g pnpm@10.8.0 && \\
|
|
|
324
344
|
# Copy source code and config
|
|
325
345
|
COPY tsconfig.json ./
|
|
326
346
|
COPY tsup.config.ts ./
|
|
327
|
-
COPY riptide.config.json ./
|
|
328
347
|
COPY src ./src
|
|
329
348
|
|
|
330
349
|
# Build the application
|
|
@@ -334,37 +353,51 @@ RUN pnpm run build
|
|
|
334
353
|
RUN pnpm prune --prod
|
|
335
354
|
|
|
336
355
|
# ----------------------------------------
|
|
337
|
-
#
|
|
356
|
+
# Third-party Base (customize as needed)
|
|
338
357
|
# ----------------------------------------
|
|
339
|
-
FROM node:22-alpine AS
|
|
340
|
-
|
|
341
|
-
# Create application user
|
|
342
|
-
RUN addgroup -g 1001 app && \\
|
|
343
|
-
adduser -u 1001 -G app -D app
|
|
358
|
+
FROM node:22-alpine AS third-party-base
|
|
344
359
|
|
|
345
|
-
|
|
360
|
+
# Install additional tools needed for your service
|
|
361
|
+
RUN apk add --no-cache \\
|
|
362
|
+
curl \\
|
|
363
|
+
bash \\
|
|
364
|
+
procps \\
|
|
365
|
+
ca-certificates
|
|
346
366
|
|
|
347
|
-
#
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
COPY --from=builder --chown=app:app /app/riptide.config.json ./riptide.config.json
|
|
367
|
+
# ----------------------------------------
|
|
368
|
+
# Final Layer: Controlled Riptide Runtime
|
|
369
|
+
# ----------------------------------------
|
|
370
|
+
FROM third-party-base
|
|
352
371
|
|
|
353
|
-
#
|
|
354
|
-
|
|
372
|
+
# \u{1F512} SECURITY LAYER: Inject controlled Riptide runtime
|
|
373
|
+
# This layer comes from your signed, verified base image
|
|
374
|
+
COPY --from=quay.io/nerdnode/riptide:latest /usr/local/bin/riptide /usr/local/bin/riptide
|
|
375
|
+
COPY --from=quay.io/nerdnode/riptide:latest /riptide-runtime/ /riptide-runtime/
|
|
355
376
|
|
|
356
|
-
#
|
|
357
|
-
|
|
377
|
+
# Verify Riptide binary permissions and ownership
|
|
378
|
+
RUN chmod +x /usr/local/bin/riptide && \\
|
|
379
|
+
chown root:root /usr/local/bin/riptide
|
|
358
380
|
|
|
359
|
-
# Set environment
|
|
381
|
+
# Set up environment
|
|
360
382
|
ENV NODE_ENV=production
|
|
383
|
+
ENV PATH="/usr/local/bin:$PATH"
|
|
384
|
+
|
|
385
|
+
# Copy the built application
|
|
386
|
+
WORKDIR /app
|
|
387
|
+
COPY --from=builder /app/dist ./dist
|
|
388
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
389
|
+
COPY --from=builder /app/package.json ./package.json
|
|
390
|
+
|
|
391
|
+
# Copy Riptide configuration directly from source
|
|
392
|
+
COPY riptide.config.json ./riptide.config.json
|
|
361
393
|
|
|
362
|
-
#
|
|
363
|
-
|
|
364
|
-
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})" || exit 1
|
|
394
|
+
# Expose health check port
|
|
395
|
+
EXPOSE 3000
|
|
365
396
|
|
|
366
|
-
#
|
|
367
|
-
|
|
397
|
+
# \u{1F6E1}\uFE0F SECURITY: Use controlled Riptide binary directly (bypasses npm)
|
|
398
|
+
# This ensures we use YOUR verified Riptide, not workspace dependencies
|
|
399
|
+
ENTRYPOINT ["/usr/local/bin/riptide"]
|
|
400
|
+
CMD ["start", "--config", "/app/riptide.config.json", "--hooks", "/app/dist/hooks.js"]
|
|
368
401
|
`;await h.writeFile(l.join(e,"Dockerfile"),i)}showNextSteps(e,t){console.log(`
|
|
369
402
|
Next steps:
|
|
370
403
|
-----------
|
|
@@ -380,16 +413,23 @@ Next steps:
|
|
|
380
413
|
4. Validate the hooks:
|
|
381
414
|
pnpm run validate
|
|
382
415
|
|
|
383
|
-
5. Build
|
|
416
|
+
5. Build the Riptide runtime (required for layered security):
|
|
417
|
+
cd ../../packages/riptide && npm run build:docker
|
|
418
|
+
|
|
419
|
+
6. Build your service Docker image:
|
|
384
420
|
pnpm run build:docker
|
|
385
421
|
|
|
386
|
-
|
|
422
|
+
7. Run locally:
|
|
387
423
|
docker run -e API_KEY=your-key reef-${e}
|
|
388
424
|
|
|
389
|
-
|
|
425
|
+
8. Add to turbo.json if needed for monorepo builds
|
|
426
|
+
|
|
427
|
+
9. Customize the hooks in src/hooks.ts for your specific requirements
|
|
390
428
|
|
|
391
|
-
|
|
392
|
-
|
|
429
|
+
\u{1F512} Security Note:
|
|
430
|
+
Your service now uses layered Riptide security! The Dockerfile injects a controlled
|
|
431
|
+
Riptide runtime on top of your base image, ensuring integrity and preventing tampering.
|
|
432
|
+
`)}async checkIfInWorkspace(e){let t=l.resolve(e),r=l.parse(t).root;for(;t!==r;){try{return await h.access(l.join(t,"pnpm-workspace.yaml")),!0}catch{}t=l.dirname(t)}return!1}async getRiptideVersion(){try{let e=[l.join(__dirname,"..","package.json"),l.join(__dirname,"..","..","package.json"),l.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),l.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let r=await h.readFile(t,"utf-8"),i=JSON.parse(r);if(i.name==="@deeep-network/riptide")return`^${i.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function T(o,e,t={}){await new C(o).scaffold({serviceName:e,...t})}n(T,"initService");var u=(0,D.default)({level:process.env.LOG_LEVEL||"info",transport:{target:"pino-pretty"}});async function q(){try{let o=process.argv[2];if(o==="--help"||o==="-h"||o==="help"){j();return}if(o==="--version"||o==="-v"||o==="version"){await J();return}let e=y("--config")||y("-c")||"./riptide.config.json",t=y("--hooks")||y("-h")||"./hooks.js";switch(o){case"init":await z();break;case"start":await $(e,t);break;case"validate":await K(e,t);break;case"health":await G();break;case"status":await B();break;case"verify":break;default:o?(u.error(`Unknown command: ${o}`),j(),process.exit(1)):await $(e,t)}}catch(o){u.error({error:o},"CLI command failed"),process.exit(1)}}n(q,"main");function y(o){let e=process.argv.indexOf(o);if(e>=0&&e+1<process.argv.length)return process.argv[e+1]}n(y,"getArgValue");function j(){console.log(`
|
|
393
433
|
Riptide - Self-contained service lifecycle management
|
|
394
434
|
|
|
395
435
|
USAGE:
|
|
@@ -401,6 +441,7 @@ COMMANDS:
|
|
|
401
441
|
validate Validate configuration and hooks
|
|
402
442
|
health Check service health
|
|
403
443
|
status Show service status
|
|
444
|
+
verify Verify Dockerfile security compliance
|
|
404
445
|
help, --help, -h Show this help message
|
|
405
446
|
version, --version Show version information
|
|
406
447
|
|
|
@@ -419,4 +460,4 @@ EXAMPLES:
|
|
|
419
460
|
npx riptide start # Start service
|
|
420
461
|
npx riptide validate # Validate config and hooks
|
|
421
462
|
npx riptide health # Check service health
|
|
422
|
-
`)}n(j,"showHelp");async function
|
|
463
|
+
`)}n(j,"showHelp");async function J(){try{let o=await import("fs/promises"),e=await import("path"),t=[e.resolve(__dirname,"../package.json"),e.resolve(__dirname,"./package.json"),"/riptide-runtime/package.json",e.resolve(process.cwd(),"package.json")];for(let r of t)try{let i=await o.readFile(r,"utf-8"),s=JSON.parse(i);if(s.name==="@deeep-network/riptide"){console.log(`@deeep-network/riptide v${s.version}`);return}}catch{continue}console.log("@deeep-network/riptide (version unknown)")}catch{console.log("@deeep-network/riptide (version unknown)")}}n(J,"showVersion");async function $(o,e){try{let t=await import("fs/promises"),r=await import("path"),i=await t.readFile(o,"utf-8"),s=JSON.parse(i),c=await import(r.resolve(process.cwd(),e)),d=c.default||c;await new S(d,s,{}).start()}catch(t){u.error({error:t},"Failed to start service"),process.exit(1)}}n($,"startService");async function K(o,e){u.info("Validating riptide configuration and hooks...");try{let t=await import("fs/promises"),r=await import("path"),i=await t.readFile(o,"utf-8"),s=JSON.parse(i);u.info({config:s},"Configuration loaded successfully");let c=await import(r.resolve(process.cwd(),e)),d=c.default||c;u.info({availableHooks:Object.keys(d)},"Hooks loaded successfully"),u.info("\u2705 Validation completed successfully"),process.exit(0)}catch(t){u.error({error:t},"\u274C Validation failed"),process.exit(1)}}n(K,"validateService");async function G(){try{let e=await(await fetch("http://localhost:3000/health")).json();console.log("Health Status:",JSON.stringify(e,null,2)),e.status==="healthy"?process.exit(0):process.exit(1)}catch(o){u.error({error:o},"Failed to check health"),process.exit(1)}}n(G,"checkHealth");async function B(){try{let e=await(await fetch("http://localhost:3000/status")).json();console.log("Service Status:",JSON.stringify(e,null,2))}catch(o){u.error({error:o},"Failed to get status"),process.exit(1)}}n(B,"showStatus");async function z(){let o=process.argv[3];o||(u.error("Service name is required"),console.log("Usage: npx riptide init <service-name>"),process.exit(1)),/^[a-z0-9-]+$/.test(o)||(u.error("Service name must contain only lowercase letters, numbers, and hyphens"),process.exit(1));let e=y("--template")||"basic",t=y("--path")||"reef/",r=y("--description"),i=["basic","with-secrets","with-process","with-metrics"];i.includes(e)||(u.error(`Invalid template: ${e}`),console.log(`Valid templates: ${i.join(", ")}`),process.exit(1));try{await T(u,o,{targetPath:t,template:e,description:r}),process.exit(0)}catch(s){u.error({error:s},"Failed to initialize service"),process.exit(1)}}n(z,"initServiceCommand");q();
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"use strict";var
|
|
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}
|
|
2
2
|
|
|
3
|
-
${c}`:c,f=[];s.stdout?.trim()&&f.push(`STDOUT: ${s.stdout.trim()}`),l&&f.push(`STDERR: ${l}`);let
|
|
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
|
-
`)
|
|
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 R(r,e=process.env){return r.replace(/\$\{([^}]+)\}/g,(t,o)=>e[o]||t)}a(R,"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)},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 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"),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=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 g=await c.text();this.logger.warn({status:c.status,error:g,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 J=require("fs"),Y=m(require("path"));async function V(r){try{let e=await J.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=Y.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 u=m(require("fs/promises")),p=m(require("path"));var w=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 u.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 u.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 u.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 u.writeFile(p.join(e,"tsconfig.json"),JSON.stringify(o,null,2))}async createTsupConfig(e){await u.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(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'
|
|
7
7
|
|
|
8
8
|
export default defineConfig({
|
|
9
9
|
entry: ['src/hooks.ts'],
|
|
@@ -14,7 +14,7 @@ export default defineConfig({
|
|
|
14
14
|
minify: false,
|
|
15
15
|
sourcemap: true
|
|
16
16
|
})
|
|
17
|
-
`)}async createHooks(e,t){let o="";switch(t){case"with-secrets":o=this.getHooksWithSecrets();break;case"with-process":o=this.getHooksWithProcess();break;case"with-metrics":o=this.getHooksWithMetrics();break;default:o=this.getBasicHooks()}await
|
|
17
|
+
`)}async createHooks(e,t){let o="";switch(t){case"with-secrets":o=this.getHooksWithSecrets();break;case"with-process":o=this.getHooksWithProcess();break;case"with-metrics":o=this.getHooksWithMetrics();break;default:o=this.getBasicHooks()}await d.writeFile(p.join(e,"src","hooks.ts"),o)}getBasicHooks(){return`import type { HookContext } from '@deeep-network/riptide'
|
|
18
18
|
|
|
19
19
|
module.exports = {
|
|
20
20
|
installSecrets: async ({ logger }: HookContext) => {
|
|
@@ -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
|
|
249
|
+
`}async createDockerfile(e,t){let i=await this.checkIfInWorkspace(e)?`# ${t} Service with Layered Riptide Security
|
|
250
250
|
|
|
251
251
|
# ----------------------------------------
|
|
252
252
|
# Base
|
|
@@ -273,38 +273,57 @@ RUN pnpm install
|
|
|
273
273
|
COPY turbo.json tsconfig.json .
|
|
274
274
|
RUN pnpm turbo run build --filter=reef-${t}...
|
|
275
275
|
|
|
276
|
-
# Create pnpm deployment
|
|
276
|
+
# Create pnpm deployment (requires .npmrc to have inject-workspace-packages=true)
|
|
277
|
+
# This "materializes" the workspace packages into the node_modules directory
|
|
277
278
|
RUN pnpm deploy --prod --filter reef-${t} deploy
|
|
278
279
|
|
|
279
280
|
# ----------------------------------------
|
|
280
|
-
#
|
|
281
|
+
# Third-party Base (customize as needed)
|
|
281
282
|
# ----------------------------------------
|
|
282
|
-
FROM node:22-alpine AS
|
|
283
|
+
FROM node:22-alpine AS third-party-base
|
|
283
284
|
|
|
284
|
-
#
|
|
285
|
-
RUN
|
|
286
|
-
|
|
285
|
+
# Install additional tools needed for your service
|
|
286
|
+
RUN apk add --no-cache \\
|
|
287
|
+
curl \\
|
|
288
|
+
bash \\
|
|
289
|
+
procps \\
|
|
290
|
+
ca-certificates
|
|
287
291
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
COPY --from=builder --chown=app:app /app/deploy/node_modules ./node_modules
|
|
293
|
-
COPY --from=builder --chown=app:app /app/deploy/package.json ./package.json
|
|
294
|
-
COPY --from=builder --chown=app:app /app/deploy/riptide.config.json ./riptide.config.json
|
|
292
|
+
# ----------------------------------------
|
|
293
|
+
# Final Layer: Controlled Riptide Runtime
|
|
294
|
+
# ----------------------------------------
|
|
295
|
+
FROM third-party-base
|
|
295
296
|
|
|
296
|
-
#
|
|
297
|
-
|
|
297
|
+
# \u{1F512} SECURITY LAYER: Inject controlled Riptide runtime
|
|
298
|
+
# This layer comes from your signed, verified base image
|
|
299
|
+
COPY --from=quay.io/nerdnode/riptide:latest /usr/local/bin/riptide /usr/local/bin/riptide
|
|
300
|
+
COPY --from=quay.io/nerdnode/riptide:latest /riptide-runtime/ /riptide-runtime/
|
|
298
301
|
|
|
299
|
-
#
|
|
300
|
-
|
|
302
|
+
# Verify Riptide binary permissions and ownership
|
|
303
|
+
RUN chmod +x /usr/local/bin/riptide && \\
|
|
304
|
+
chown root:root /usr/local/bin/riptide
|
|
301
305
|
|
|
302
|
-
# Set environment
|
|
306
|
+
# Set up environment
|
|
303
307
|
ENV NODE_ENV=production
|
|
308
|
+
ENV PATH="/usr/local/bin:$PATH"
|
|
304
309
|
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
# Copy the built application (your hooks that manage the service)
|
|
311
|
+
WORKDIR /riptide/reef/${t}
|
|
312
|
+
COPY --from=builder /app/deploy/dist ./dist
|
|
313
|
+
COPY --from=builder /app/deploy/node_modules ./node_modules
|
|
314
|
+
COPY --from=builder /app/deploy/package.json ./package.json
|
|
315
|
+
|
|
316
|
+
# Copy Riptide configuration directly from source
|
|
317
|
+
COPY reef/${t}/riptide.config.json ./riptide.config.json
|
|
318
|
+
|
|
319
|
+
# Expose health check port
|
|
320
|
+
EXPOSE 3000
|
|
321
|
+
|
|
322
|
+
# \u{1F6E1}\uFE0F SECURITY: Use controlled Riptide binary directly (bypasses npm)
|
|
323
|
+
# This ensures we use YOUR verified Riptide, not workspace dependencies
|
|
324
|
+
ENTRYPOINT ["/usr/local/bin/riptide"]
|
|
325
|
+
CMD ["start", "--config", "/riptide/reef/${t}/riptide.config.json", "--hooks", "/riptide/reef/${t}/dist/hooks.js"]
|
|
326
|
+
`:`# ${t} Service with Layered Riptide Security
|
|
308
327
|
|
|
309
328
|
# ----------------------------------------
|
|
310
329
|
# Builder Stage
|
|
@@ -315,7 +334,8 @@ RUN apk add --no-cache libc6-compat
|
|
|
315
334
|
WORKDIR /app
|
|
316
335
|
|
|
317
336
|
# Copy package files
|
|
318
|
-
COPY package.json
|
|
337
|
+
COPY package.json ./
|
|
338
|
+
COPY pnpm-lock.yaml ./
|
|
319
339
|
|
|
320
340
|
# Install dependencies
|
|
321
341
|
RUN npm install -g pnpm@10.8.0 && \\
|
|
@@ -324,7 +344,6 @@ RUN npm install -g pnpm@10.8.0 && \\
|
|
|
324
344
|
# Copy source code and config
|
|
325
345
|
COPY tsconfig.json ./
|
|
326
346
|
COPY tsup.config.ts ./
|
|
327
|
-
COPY riptide.config.json ./
|
|
328
347
|
COPY src ./src
|
|
329
348
|
|
|
330
349
|
# Build the application
|
|
@@ -334,38 +353,52 @@ RUN pnpm run build
|
|
|
334
353
|
RUN pnpm prune --prod
|
|
335
354
|
|
|
336
355
|
# ----------------------------------------
|
|
337
|
-
#
|
|
356
|
+
# Third-party Base (customize as needed)
|
|
338
357
|
# ----------------------------------------
|
|
339
|
-
FROM node:22-alpine AS
|
|
340
|
-
|
|
341
|
-
# Create application user
|
|
342
|
-
RUN addgroup -g 1001 app && \\
|
|
343
|
-
adduser -u 1001 -G app -D app
|
|
358
|
+
FROM node:22-alpine AS third-party-base
|
|
344
359
|
|
|
345
|
-
|
|
360
|
+
# Install additional tools needed for your service
|
|
361
|
+
RUN apk add --no-cache \\
|
|
362
|
+
curl \\
|
|
363
|
+
bash \\
|
|
364
|
+
procps \\
|
|
365
|
+
ca-certificates
|
|
346
366
|
|
|
347
|
-
#
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
COPY --from=builder --chown=app:app /app/riptide.config.json ./riptide.config.json
|
|
367
|
+
# ----------------------------------------
|
|
368
|
+
# Final Layer: Controlled Riptide Runtime
|
|
369
|
+
# ----------------------------------------
|
|
370
|
+
FROM third-party-base
|
|
352
371
|
|
|
353
|
-
#
|
|
354
|
-
|
|
372
|
+
# \u{1F512} SECURITY LAYER: Inject controlled Riptide runtime
|
|
373
|
+
# This layer comes from your signed, verified base image
|
|
374
|
+
COPY --from=quay.io/nerdnode/riptide:latest /usr/local/bin/riptide /usr/local/bin/riptide
|
|
375
|
+
COPY --from=quay.io/nerdnode/riptide:latest /riptide-runtime/ /riptide-runtime/
|
|
355
376
|
|
|
356
|
-
#
|
|
357
|
-
|
|
377
|
+
# Verify Riptide binary permissions and ownership
|
|
378
|
+
RUN chmod +x /usr/local/bin/riptide && \\
|
|
379
|
+
chown root:root /usr/local/bin/riptide
|
|
358
380
|
|
|
359
|
-
# Set environment
|
|
381
|
+
# Set up environment
|
|
360
382
|
ENV NODE_ENV=production
|
|
383
|
+
ENV PATH="/usr/local/bin:$PATH"
|
|
384
|
+
|
|
385
|
+
# Copy the built application
|
|
386
|
+
WORKDIR /app
|
|
387
|
+
COPY --from=builder /app/dist ./dist
|
|
388
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
389
|
+
COPY --from=builder /app/package.json ./package.json
|
|
390
|
+
|
|
391
|
+
# Copy Riptide configuration directly from source
|
|
392
|
+
COPY riptide.config.json ./riptide.config.json
|
|
361
393
|
|
|
362
|
-
#
|
|
363
|
-
|
|
364
|
-
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})" || exit 1
|
|
394
|
+
# Expose health check port
|
|
395
|
+
EXPOSE 3000
|
|
365
396
|
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
397
|
+
# \u{1F6E1}\uFE0F SECURITY: Use controlled Riptide binary directly (bypasses npm)
|
|
398
|
+
# This ensures we use YOUR verified Riptide, not workspace dependencies
|
|
399
|
+
ENTRYPOINT ["/usr/local/bin/riptide"]
|
|
400
|
+
CMD ["start", "--config", "/app/riptide.config.json", "--hooks", "/app/dist/hooks.js"]
|
|
401
|
+
`;await d.writeFile(p.join(e,"Dockerfile"),i)}showNextSteps(e,t){console.log(`
|
|
369
402
|
Next steps:
|
|
370
403
|
-----------
|
|
371
404
|
1. Navigate to your service:
|
|
@@ -380,13 +413,20 @@ Next steps:
|
|
|
380
413
|
4. Validate the hooks:
|
|
381
414
|
pnpm run validate
|
|
382
415
|
|
|
383
|
-
5. Build
|
|
416
|
+
5. Build the Riptide runtime (required for layered security):
|
|
417
|
+
cd ../../packages/riptide && npm run build:docker
|
|
418
|
+
|
|
419
|
+
6. Build your service Docker image:
|
|
384
420
|
pnpm run build:docker
|
|
385
421
|
|
|
386
|
-
|
|
422
|
+
7. Run locally:
|
|
387
423
|
docker run -e API_KEY=your-key reef-${e}
|
|
388
424
|
|
|
389
|
-
|
|
425
|
+
8. Add to turbo.json if needed for monorepo builds
|
|
426
|
+
|
|
427
|
+
9. Customize the hooks in src/hooks.ts for your specific requirements
|
|
390
428
|
|
|
391
|
-
|
|
392
|
-
|
|
429
|
+
\u{1F512} Security Note:
|
|
430
|
+
Your service now uses layered Riptide security! The Dockerfile injects a controlled
|
|
431
|
+
Riptide runtime on top of your base image, ensuring integrity and preventing tampering.
|
|
432
|
+
`)}async checkIfInWorkspace(e){let t=p.resolve(e),o=p.parse(t).root;for(;t!==o;){try{return await d.access(p.join(t,"pnpm-workspace.yaml")),!0}catch{}t=p.dirname(t)}return!1}async getRiptideVersion(){try{let e=[p.join(__dirname,"..","package.json"),p.join(__dirname,"..","..","package.json"),p.join(__dirname,"..","..","..","@deeep-network","riptide","package.json"),p.join(__dirname,"..","..","node_modules","@deeep-network","riptide","package.json")];for(let t of e)try{let o=await d.readFile(t,"utf-8"),i=JSON.parse(o);if(i.name==="@deeep-network/riptide")return`^${i.version}`}catch{continue}try{return`^${require("@deeep-network/riptide/package.json").version}`}catch{}return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}catch{return this.logger.warn("Could not read riptide package.json, falling back to default version"),"^0.1.3"}}};async function z(r,e,t={}){await new k(r).scaffold({serviceName:e,...t})}a(z,"initService");0&&(module.exports={AlreadyRunningError,DiagnoseRequiredError,InvalidSecretError,MissingSecretError,RiptideEntrypoint,RiptideError,ServiceScaffolder,createChildLogger,createLogger,createUtilityContext,expandEnvironmentVariables,getDefaultConfig,initService,isAlreadyRunningError,isDiagnoseRequiredError,isInvalidSecretError,isMissingSecretError,isRiptideError,loadConfig,loadHooks,mergeConfigs,parseEnvironmentVariables,redactSecret,validateConfig});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeep-network/riptide",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Self-contained container orchestration library with lifecycle hooks",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc --noEmit && tsup",
|
|
12
12
|
"build:publish": "tsc --noEmit && tsup --minify",
|
|
13
|
+
"build:docker": "./scripts/build-docker.sh",
|
|
14
|
+
"deploy:docker": "./scripts/deploy-docker.sh",
|
|
15
|
+
"deploy:npm": "./scripts/deploy-npm.sh",
|
|
13
16
|
"dev": "tsup --watch",
|
|
14
17
|
"clean": "rm -rf dist",
|
|
15
18
|
"prepublishOnly": "npm run clean && npm run build:publish"
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
"coral-reef",
|
|
46
49
|
"riptide"
|
|
47
50
|
],
|
|
48
|
-
"author": "
|
|
51
|
+
"author": "DeEEP Network",
|
|
49
52
|
"license": "MIT",
|
|
50
53
|
"engines": {
|
|
51
54
|
"node": ">=22.0.0"
|