@stacksjs/ts-cloud 0.16.75 → 0.16.76
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/bin/{chunk-72kpcqps.js → chunk-cvbass8r.js} +1 -1
- package/dist/bin/{chunk-26vbw2m6.js → chunk-fp9zajws.js} +1 -1
- package/dist/bin/{chunk-gndp57b6.js → chunk-t3hsn9tf.js} +25 -25
- package/dist/bin/cli.js +4 -4
- package/dist/bin/dashboard-server.js +1 -1
- package/dist/{chunk-ne8btf6v.js → chunk-4a1qcgpb.js} +11 -11
- package/dist/{chunk-fnc88v79.js → chunk-ga00dvdb.js} +1 -1
- package/dist/deploy/index.js +1 -1
- package/dist/drivers/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/ui/404.html +1 -1
- package/dist/ui/accept-invitation.html +1 -1
- package/dist/ui/access-denied.html +1 -1
- package/dist/ui/account/automation.html +6 -6
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +6 -6
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +5 -5
- package/dist/ui/data/services.html +6 -6
- package/dist/ui/data/volumes.html +6 -6
- package/dist/ui/index.html +6 -6
- package/dist/ui/infrastructure/topology.html +5 -5
- package/dist/ui/integrations.html +2 -2
- package/dist/ui/operations/alerts.html +6 -6
- package/dist/ui/operations/configuration.html +6 -6
- package/dist/ui/operations/jobs.html +5 -5
- package/dist/ui/operations/maintenance.html +5 -5
- package/dist/ui/operations/observability.html +5 -5
- package/dist/ui/operations/previews.html +6 -6
- package/dist/ui/operations/queue.html +6 -6
- package/dist/ui/operations/regions.html +5 -5
- package/dist/ui/operations/releases.html +6 -6
- package/dist/ui/operations/spend.html +6 -6
- package/dist/ui/operations/workloads.html +6 -6
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +6 -6
- package/dist/ui/server/activity.html +1 -1
- package/dist/ui/server/capacity.html +6 -6
- package/dist/ui/server/database.html +6 -6
- package/dist/ui/server/deployments.html +5 -5
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +6 -6
- package/dist/ui/server/fleet.html +6 -6
- package/dist/ui/server/logs.html +6 -6
- package/dist/ui/server/metrics.html +5 -5
- package/dist/ui/server/security.html +1 -1
- package/dist/ui/server/services.html +1 -1
- package/dist/ui/server/sites.html +5 -5
- package/dist/ui/server/ssh-keys.html +6 -6
- package/dist/ui/server/team.html +6 -6
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +6 -6
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/cost.html +2 -2
- package/dist/ui/serverless/data.html +6 -6
- package/dist/ui/serverless/deployments.html +2 -2
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +6 -6
- package/dist/ui/serverless/logs.html +6 -6
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +6 -6
- package/dist/ui/serverless/secrets.html +6 -6
- package/dist/ui/serverless/traces.html +6 -6
- package/dist/ui/serverless.html +6 -6
- package/package.json +3 -3
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import{p}from"./chunk-540wkkhs.js";import{pa,I,A,ne,jr,ga,Jo,le,U,T,er,Vr}from"./chunk-wayhpmn7.js";import{v}from"./chunk-4vtyjj7c.js";import{Q}from"./chunk-9ktyfegk.js";import{g}from"./chunk-gqstb8zn.js";import{W,ki,Us,w,R,Tt,Ve,it,Io,Ni,Do,wt,Kc,ko,No,_i,xo,Oi,Ht,Vs,Jc,$i,zs,Hs,Gs,qs,_o,hn,Ws,yn,Ks,Oo}from"./chunk-ps0pzqm0.js";import{zo,Ho,Ys}from"./chunk-0r8k7qth.js";import{Ms}from"./chunk-k3cyh23p.js";import{Pr}from"./chunk-hhbmtgkq.js";import{bn}from"./chunk-ma0d990n.js";import{el}from"./chunk-hyrvmjq0.js";class Ze{client;region;constructor(e="us-east-1",t){this.region=e,this.client=new p}async putParameter(e){let t={Name:e.Name,Value:e.Value};if(e.Type)t.Type=e.Type;if(e.Description)t.Description=e.Description;if(e.KeyId)t.KeyId=e.KeyId;if(e.Overwrite!==void 0)t.Overwrite=e.Overwrite;if(e.AllowedPattern)t.AllowedPattern=e.AllowedPattern;if(e.Tags&&e.Tags.length>0)t.Tags=e.Tags;if(e.Tier)t.Tier=e.Tier;if(e.DataType)t.DataType=e.DataType;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.PutParameter","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Version:r.Version,Tier:r.Tier}}async getParameter(e){let t={Name:e.Name};if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParameter","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameter:r.Parameter?this.parseParameter(r.Parameter):void 0}}async getParameters(e){let t={Names:e.Names};if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParameters","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>this.parseParameter(n)),InvalidParameters:r.InvalidParameters}}async getParametersByPath(e){let t={Path:e.Path};if(e.Recursive!==void 0)t.Recursive=e.Recursive;if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;if(e.MaxResults)t.MaxResults=e.MaxResults;if(e.NextToken)t.NextToken=e.NextToken;if(e.ParameterFilters&&e.ParameterFilters.length>0)t.ParameterFilters=e.ParameterFilters;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParametersByPath","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>this.parseParameter(n)),NextToken:r.NextToken}}async deleteParameter(e){let t={Name:e.Name};await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.DeleteParameter","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})}async deleteParameters(e){let t={Names:e},r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.DeleteParameters","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{DeletedParameters:r.DeletedParameters,InvalidParameters:r.InvalidParameters}}async describeParameters(e){let t={};if(e?.Filters&&e.Filters.length>0)t.Filters=e.Filters;if(e?.ParameterFilters&&e.ParameterFilters.length>0)t.ParameterFilters=e.ParameterFilters;if(e?.MaxResults)t.MaxResults=e.MaxResults;if(e?.NextToken)t.NextToken=e.NextToken;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.DescribeParameters","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>this.parseParameter(n)),NextToken:r.NextToken}}async getParameterHistory(e){let t={Name:e.Name};if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;if(e.MaxResults)t.MaxResults=e.MaxResults;if(e.NextToken)t.NextToken=e.NextToken;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParameterHistory","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>({Name:n.Name,Type:n.Type,KeyId:n.KeyId,LastModifiedDate:n.LastModifiedDate,LastModifiedUser:n.LastModifiedUser,Description:n.Description,Value:n.Value,Version:n.Version,Labels:n.Labels,Tier:n.Tier})),NextToken:r.NextToken}}async labelParameterVersion(e){let t={Name:e.Name,Labels:e.Labels};if(e.ParameterVersion!==void 0)t.ParameterVersion=e.ParameterVersion;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.LabelParameterVersion","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{InvalidLabels:r.InvalidLabels,ParameterVersion:r.ParameterVersion}}async addTagsToResource(e){let t={ResourceType:e.ResourceType,ResourceId:e.ResourceId,Tags:e.Tags};await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.AddTagsToResource","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})}async removeTagsFromResource(e){let t={ResourceType:e.ResourceType,ResourceId:e.ResourceId,TagKeys:e.TagKeys};await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.RemoveTagsFromResource","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})}async listTagsForResource(e){let t={ResourceType:e.ResourceType,ResourceId:e.ResourceId};return{TagList:(await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.ListTagsForResource","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})).TagList}}async setString(e,t,r){return this.putParameter({Name:e,Value:t,Type:"String",Description:r?.description,Overwrite:r?.overwrite??!0,Tags:r?.tags})}async setSecureString(e,t,r){return this.putParameter({Name:e,Value:t,Type:"SecureString",Description:r?.description,Overwrite:r?.overwrite??!0,KeyId:r?.kmsKeyId,Tags:r?.tags})}async getValue(e){return(await this.getParameter({Name:e,WithDecryption:!0})).Parameter?.Value}async getAllByPath(e,t=!0){let r=[],n;do{let s=await this.getParametersByPath({Path:e,Recursive:t,WithDecryption:!0,NextToken:n});if(s.Parameters)r.push(...s.Parameters);n=s.NextToken}while(n);return r}parseParameter(e){return{Name:e.Name,Type:e.Type,Value:e.Value,Version:e.Version,LastModifiedDate:e.LastModifiedDate,ARN:e.ARN,DataType:e.DataType,Description:e.Description,AllowedPattern:e.AllowedPattern,KeyId:e.KeyId,Tier:e.Tier}}async sendCommand(e){if(!e.InstanceIds&&!e.Targets)throw Error("SendCommand requires either InstanceIds or Targets");let t={DocumentName:e.DocumentName};if(e.InstanceIds)t.InstanceIds=e.InstanceIds;if(e.Targets)t.Targets=e.Targets;if(e.Parameters)t.Parameters=e.Parameters;if(e.TimeoutSeconds)t.TimeoutSeconds=e.TimeoutSeconds;if(e.Comment)t.Comment=e.Comment;if(e.OutputS3BucketName)t.OutputS3BucketName=e.OutputS3BucketName;if(e.OutputS3KeyPrefix)t.OutputS3KeyPrefix=e.OutputS3KeyPrefix;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.SendCommand","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{CommandId:r.Command?.CommandId,Status:r.Command?.Status,StatusDetails:r.Command?.StatusDetails}}async getCommandInvocation(e){let t={CommandId:e.CommandId,InstanceId:e.InstanceId},r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetCommandInvocation","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Status:r.Status,StatusDetails:r.StatusDetails,StandardOutputContent:r.StandardOutputContent,StandardErrorContent:r.StandardErrorContent,ResponseCode:r.ResponseCode}}async runShellCommand(e,t,r){let n=await this.sendCommand({InstanceIds:[e],DocumentName:"AWS-RunShellScript",Parameters:{commands:t},TimeoutSeconds:r?.timeoutSeconds||600});if(!n.CommandId)return{success:!1,error:"Failed to send command"};if(r?.waitForCompletion===!1)return{success:!0,status:"Pending"};let s=r?.pollIntervalMs||2000,i=r?.maxWaitMs||300000,o=Date.now();while(Date.now()-o<i){await new Promise((a)=>setTimeout(a,s));try{let a=await this.getCommandInvocation({CommandId:n.CommandId,InstanceId:e});if(a.Status==="Success")return{success:!0,output:a.StandardOutputContent,error:a.StandardErrorContent,status:a.Status};if(a.Status==="Failed"||a.Status==="Cancelled"||a.Status==="TimedOut")return{success:!1,output:a.StandardOutputContent,error:a.StandardErrorContent||a.StatusDetails,status:a.Status}}catch(a){if(!a.message?.includes("InvocationDoesNotExist"))return{success:!1,error:a.message}}}return{success:!1,error:"Command timed out waiting for completion"}}async listCommandInvocations(e){let t={CommandId:e.CommandId};if(e.Details)t.Details=!0;return((await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.ListCommandInvocations","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})).CommandInvocations||[]).map((s)=>({InstanceId:s.InstanceId,Status:s.Status,StatusDetails:s.StatusDetails,StandardOutputContent:s.CommandPlugins?.[0]?.Output,StandardErrorContent:s.CommandPlugins?.[0]?.StandardErrorUrl}))}async sendCommandByTags(e){let t=Object.entries(e.tags).map(([c,l])=>({Key:`tag:${c}`,Values:[l]})),r=await this.sendCommand({Targets:t,DocumentName:"AWS-RunShellScript",Parameters:{commands:e.commands},TimeoutSeconds:e.timeoutSeconds||600,Comment:e.comment});if(!r.CommandId)return{success:!1,instanceCount:0,perInstance:[],error:"Failed to send command"};let n=e.pollIntervalMs||3000,s=e.maxWaitMs||600000,i=Date.now(),o=new Set(["Success","Failed","Cancelled","TimedOut"]),a=[];while(Date.now()-i<s){await new Promise((c)=>setTimeout(c,n));try{if(a=await this.listCommandInvocations({CommandId:r.CommandId,Details:!0}),a.length===0)continue;if(a.every((l)=>o.has(l.Status||""))){let l=a.map((d)=>({instanceId:d.InstanceId,status:d.Status||"Unknown",output:d.StandardOutputContent,error:d.StandardErrorContent||d.StatusDetails})),u=a.every((d)=>d.Status==="Success");return{success:u,instanceCount:a.length,perInstance:l,error:u?void 0:"One or more instances reported a non-success status"}}}catch(c){if(!c.message?.includes("InvocationDoesNotExist"))return{success:!1,instanceCount:0,perInstance:[],error:c.message}}}return{success:!1,instanceCount:a.length,perInstance:a.map((c)=>({instanceId:c.InstanceId,status:c.Status||"Unknown"})),error:"Command timed out waiting for all instances to complete"}}}function ks(e={}){let t=new Set;for(let r of Object.values(e)){if(!r.domain)continue;if(!(r.redirect||r.deploy==="server"||r.start))continue;t.add(r.domain);for(let n of r.aliases??[]){let s=String(n).trim().toLowerCase();if(s)t.add(s)}}return t}function Dr(e){return e.replace(/\.$/,"").toLowerCase()}function La(e,t,r){let n=Dr(e.name),s=Dr(t),i=Dr(r),o=i===s?"@":i.endsWith(`.${s}`)?i.slice(0,-(s.length+1)):i;return n===i||n===o||o==="@"&&(n===""||n===s)}async function ka(e,t,r,n,s="A"){let i=await e.listRecords(t);if(!i.success)return[`could not list ${s} records: ${i.message||"unknown provider error"}`];let o=i.records.filter((l)=>l.type===s&&La(l,t,r)),a=o.findIndex((l)=>l.content===n);if(o.length<=1||a===-1)return[];let c=[];for(let[l,u]of o.entries()){if(l===a)continue;let d=await e.deleteRecord(t,u);if(!d.success)c.push(`could not remove stale ${u.name} ${s} ${u.content}: ${d.message||"unknown provider error"}`)}return c}function me(e){if(!e)return;let t=e.trim();if(!t)return;if(!t.includes("/"))return t;let[r]=t.split("/");if(!r.endsWith("::"))return r||void 0;return`${r}1`}var Na=new Set(["mail","smtp","imap","mx"]);function po(e){return!Na.has(e.split(".")[0]??"")}async function Ei(e){let{provider:t,zone:r,fqdn:n,ipv4:s,ipv6:i,ttl:o=600}=e,a={published:[],warnings:[]},c=[{type:"A",content:s}];if(i&&po(n))c.push({type:"AAAA",content:i});for(let{type:l,content:u}of c){let d=await t.upsertRecord(r,{name:n,type:l,content:u,ttl:o});if(d?.success===!1){a.warnings.push(`${n} → ${u} failed: ${d.error||d.message||"unknown error"}`);continue}if(a.warnings.push(...(await ka(t,r,n,u,l)).map((m)=>`${n} cleanup: ${m}`)),await mo(t,r,n,u,l))a.published.push({fqdn:n,type:l,content:u});else a.warnings.push(`${n} → ${u} reported success at ${t.name} but no matching ${l} record exists — create it manually: ${l} ${n} → ${u}`)}return a}async function mo(e,t,r,n,s){try{if(typeof e?.listRecords!=="function")return!0;let i=await e.listRecords(t);if(!i?.success||!Array.isArray(i.records))return!0;return i.records.some((o)=>{let a=typeof o?.name==="string"?o.name.replace(/\.$/,""):"";return o?.type===s&&a===r&&o?.content===n})}catch{return!0}}import{readFileSync as Bs}from"node:fs";import{join as Rc}from"node:path";function Lr(e){return(Buffer.isBuffer(e)?e.toString("utf8"):typeof e==="string"?e:"").replace(/(^|\n)(\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*=).*$/gm,"$1$2[redacted]").replace(/encrypted:[A-Za-z0-9+/=]+/g,"encrypted:[redacted]").trim().slice(-8000)}function Pt(e,t){let r=Lr(e?.stderr),n=/line (\d+): syntax error/.exec(r);if(!n)return"";let s=Number(n[1]),i=t.split(`
|
|
2
2
|
`),o=Math.max(1,s-12),a=Math.min(i.length,s+2),c=i.slice(o-1,a).map((l,u)=>{let d=o+u,m=/^[A-Z_][A-Z0-9_]*=/.test(l)?l.replace(/=.*/,"=<redacted>"):l;return` ${String(d).padStart(4)}${d===s?" >":" "} ${m}`});return["",`The deploy script bash rejected is ${i.length} lines; it stopped at ${s}.`,...c].join(`
|
|
3
|
-
`)}function
|
|
3
|
+
`)}function Ue(e){let t=e,r=typeof t?.status==="number"?` (exit ${t.status})`:"",n=t?.signal?` (signal ${t.signal})`:"",s=[Lr(t?.stderr),Lr(t?.stdout)].filter(Boolean).join(`
|
|
4
4
|
`);return`Remote SSH command failed${r}${n}${s?`
|
|
5
5
|
${s}`:""}`}function qe(e){for(let t of e.split(`
|
|
6
6
|
`)){let r=t.trim();if(!r.startsWith("[ts-cloud]"))continue;console.warn(r)}}var is=4000,Ha=3;function Ua(e){return e.length>is?`…
|
|
7
|
-
${e.slice(-is)}`:e}function
|
|
7
|
+
${e.slice(-is)}`:e}function Me(e,t){let r=e.filter((o)=>o.status!=="Success");if(r.length===0)return t;let n=r.slice(0,Ha).map((o)=>{let a=(o.error||o.output||"").trim(),c=`${o.instanceId}: ${o.status}`;return a?`${c}
|
|
8
8
|
${Ua(a)}`:c}),s=r.length-n.length,i=s>0?[`(+${s} more instance${s===1?"":"s"} failed)`]:[];return[t,...n,...i].join(`
|
|
9
9
|
`)}function Ma(e){return`/etc/ts-cloud/${e}-upgrade.env`}function za(e){return`${e}-upgrade`}function go(e){if(!e||e.length===0)return[];let t=["mkdir -p /etc/ts-cloud"],r=[];for(let n of e){if(n.enabled===!1)continue;if(!n.service||!n.binary)continue;let{service:s,binary:i,command:o="upgrade",channel:a="stable",schedule:c="daily",randomizedDelay:l="4h",args:u=[]}=n,d=za(s),m=Ma(s),h=[`--path ${i}`,`--service ${s}`,...a==="canary"?["--canary"]:[],...u].join(" ");t.push(`if [ ! -f ${m} ]; then`,` cat > ${m} <<'TS_CLOUD_APPUPD_ENV_EOF'`,"# Set to false to pause automatic updates without disabling the timer.","ENABLED=true","# Extra flags appended to the update command.","ARGS=","TS_CLOUD_APPUPD_ENV_EOF","fi",`chmod 644 ${m}`,`cat > /etc/systemd/system/${d}.service <<'TS_CLOUD_APPUPD_SVC_EOF'`,"[Unit]",`Description=Check for and install ${s} updates`,"After=network-online.target","Wants=network-online.target","","[Service]","Type=oneshot",`EnvironmentFile=-${m}`,`ExecStart=/bin/sh -c 'if [ "\${ENABLED:-true}" != "true" ]; then echo "auto-update disabled via ${m}"; exit 0; fi; exec ${i} ${o} ${h} $ARGS'`,"TS_CLOUD_APPUPD_SVC_EOF",`cat > /etc/systemd/system/${d}.timer <<'TS_CLOUD_APPUPD_TMR_EOF'`,"[Unit]",`Description=Scheduled ${s} update check`,"","[Timer]",`OnCalendar=${c}`,`RandomizedDelaySec=${l}`,"Persistent=true","","[Install]","WantedBy=timers.target","TS_CLOUD_APPUPD_TMR_EOF"),r.push(`systemctl enable --now ${d}.timer`)}if(r.length===0)return[];return[...t,"systemctl daemon-reload",...r]}function Ee(e=!0){if(!e)return[];return["export DEBIAN_FRONTEND=noninteractive","dpkg-query -W -f='${Status}' unattended-upgrades 2>/dev/null | grep -q 'ok installed' || apt-get install -y unattended-upgrades","cat > /etc/apt/apt.conf.d/20auto-upgrades <<'TS_CLOUD_AUTOUPD_EOF'",'APT::Periodic::Update-Package-Lists "1";','APT::Periodic::Unattended-Upgrade "1";','APT::Periodic::Download-Upgradeable-Packages "1";','APT::Periodic::AutocleanInterval "7";',"TS_CLOUD_AUTOUPD_EOF","systemctl enable --now unattended-upgrades 2>/dev/null || true"]}var os=[["nginx",80],["https",443],["phpFpm",9074],["mysql",3306],["postgres",5432],["redis",6379],["meilisearch",7700],["typesense",8108],["smtp",25]];function Fa(e=!0){let t=typeof e==="object"?e:{},r=typeof e==="boolean"?e:e.enabled!==!1,n=Number(t.alerts?.bandwidthTb??0);return{enabled:r,cpuLoadPerCore:t.alerts?.cpuLoadPerCore??2,memPercent:t.alerts?.memPercent??90,diskPercent:t.alerts?.diskPercent??90,bandwidthTb:Number.isFinite(n)&&n>0?n:0,bandwidthPercent:t.alerts?.bandwidthPercent??80}}function Pe(e=!0){let{enabled:t,cpuLoadPerCore:r,memPercent:n,diskPercent:s,bandwidthTb:i,bandwidthPercent:o}=Fa(e);if(!t)return[];let a=Math.round(i*1000000000000),c=os.map(([f,S])=>`SVC_${f.toUpperCase()}=$(probe ${S})`),l=os.map(([f])=>`"${f}":"$SVC_${f.toUpperCase()}"`).join(","),u=(typeof e==="object"?e.filesystems:void 0)??["/"];if(!u.length)throw Error("monitoring.filesystems must include at least one path");let d=u.map((f)=>typeof f==="string"?{path:f,requiredMount:!1}:f);for(let f of d)if(f.path!=="/"&&(!f.path.startsWith("/")||!f.path.slice(1).split("/").every((S)=>/^[\w-]+$/.test(S))))throw Error(`Invalid monitoring filesystem path: ${f.path}`);let m=d.flatMap((f,S)=>[`DISK_${S}_MOUNTED=true; DISK_${S}_PCT=null`,...f.requiredMount?[`if ! mountpoint -q '${f.path}'; then DISK_${S}_MOUNTED=false; DISK_ALERTS="$DISK_ALERTS missing-mount=${f.path}"; ALERT_KEY="$ALERT_KEY mount:${f.path}"; fi`]:[],`if [ "$DISK_${S}_MOUNTED" = true ]; then`,` DISK_${S}_PCT=$(df -Pk '${f.path}' 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5}')`,` if [[ "$DISK_${S}_PCT" =~ ^[0-9]+$ ]]; then`,` [ "$DISK_${S}_PCT" -le "$DISK_PCT" ] || DISK_PCT=$DISK_${S}_PCT`,` if [ "$DISK_${S}_PCT" -ge ${s} ]; then DISK_ALERTS="$DISK_ALERTS disk=${f.path}:$DISK_${S}_PCT%"; ALERT_KEY="$ALERT_KEY disk:${f.path}"; fi`,` else DISK_${S}_PCT=null; DISK_ALERTS="$DISK_ALERTS unreadable-disk=${f.path}"; ALERT_KEY="$ALERT_KEY unreadable:${f.path}"; fi`,"fi"]),h=d.map((f,S)=>`{"path":"${f.path}","usedPercent":$DISK_${S}_PCT,"mounted":$DISK_${S}_MOUNTED}`).join(",");return["if ! command -v sadf >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive; apt-get update -qq; apt-get install -y -qq sysstat; fi","systemctl enable --now sysstat.service 2>/dev/null || true","mkdir -p /var/lib/ts-cloud","cat > /usr/local/bin/ts-cloud-metrics.sh <<'TS_CLOUD_METRICS_EOF'","#!/bin/bash","set -uo pipefail","LOAD=$(cut -d' ' -f1 /proc/loadavg)","CPUS=$(nproc)",`read MEM_TOTAL MEM_USED SWAP_TOTAL SWAP_USED <<EOF
|
|
10
10
|
$(free -m | awk '/^Mem:/{total=$2; used=$3} /^Swap:/{print total, used, $2, $3}')
|
|
@@ -12,7 +12,7 @@ EOF`,'DISK_PCT=0; DISK_ALERTS=""; ALERT_KEY=""',...m,"UPTIME_SEC=$(cut -d' ' -f1
|
|
|
12
12
|
`)}
|
|
13
13
|
`}return`${["server {"," listen 80;"," listen [::]:80;",` server_name ${r};`,...n,"}"].join(`
|
|
14
14
|
`)}
|
|
15
|
-
`}function Nr(e){let t=`/etc/nginx/sites-available/${e.siteName}`,r=`/etc/nginx/sites-enabled/${e.siteName}`,n=ec(e),s=[...Hr()];if(e.auth){let i=cs(e.siteName),o=(l)=>l.split("'").join("'\\''"),a=o(e.auth.password),c=o(e.auth.username);s.push(`TS_CLOUD_HTPASS=$(openssl passwd -apr1 '${a}')`,`printf '%s:%s\\n' '${c}' "$TS_CLOUD_HTPASS" > ${i}`,`chmod 640 ${i}`,`chown root:www-data ${i} 2>/dev/null || true`)}return s.push(`cat > ${t} <<'TS_CLOUD_NGINX_EOF'`,n.replace(/\n$/,""),"TS_CLOUD_NGINX_EOF",`ln -sf ${t} ${r}`,"rm -f /etc/nginx/sites-enabled/default",`${xe} -t`,"systemctl reload ts-cloud-nginx 2>/dev/null || systemctl restart ts-cloud-nginx 2>/dev/null || true"),s}var xe="/usr/local/bin/ts-cloud-nginx";function Hr(e="/opt/pantry"){return[...Tt(),...it(["nginx.org"]),"mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled /var/log/nginx /var/lib/nginx/body /var/lib/nginx/proxy /var/lib/nginx/fastcgi /var/lib/nginx/uwsgi /var/lib/nginx/scgi","getent passwd www-data >/dev/null || useradd --system --no-create-home --shell /usr/sbin/nologin www-data","chown -R www-data:www-data /var/lib/nginx /var/log/nginx",`cat > ${xe} <<'TS_CLOUD_NGINXBIN_EOF'`,"#!/bin/sh",`eval "$(cd ${e} && pantry env 2>/dev/null)"`,"NGINX_BIN=/opt/pantry/pantry/.bin/nginx",'if [ ! -x "$NGINX_BIN" ]; then NGINX_BIN=$(command -v nginx || true); fi','if [ -z "$NGINX_BIN" ]; then echo "nginx binary not found; run pantry install nginx.org or install nginx explicitly" >&2; exit 1; fi','exec "$NGINX_BIN" -c /etc/nginx/nginx.conf "$@"',"TS_CLOUD_NGINXBIN_EOF",`chmod +x ${xe}`,"cat > /etc/nginx/fastcgi_params <<'TS_CLOUD_FCGI_EOF'","fastcgi_param QUERY_STRING $query_string;","fastcgi_param REQUEST_METHOD $request_method;","fastcgi_param CONTENT_TYPE $content_type;","fastcgi_param CONTENT_LENGTH $content_length;","fastcgi_param SCRIPT_NAME $fastcgi_script_name;","fastcgi_param REQUEST_URI $request_uri;","fastcgi_param DOCUMENT_URI $document_uri;","fastcgi_param DOCUMENT_ROOT $document_root;","fastcgi_param SERVER_PROTOCOL $server_protocol;","fastcgi_param REQUEST_SCHEME $scheme;","fastcgi_param HTTPS $https if_not_empty;","fastcgi_param GATEWAY_INTERFACE CGI/1.1;","fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;","fastcgi_param REMOTE_ADDR $remote_addr;","fastcgi_param REMOTE_PORT $remote_port;","fastcgi_param SERVER_ADDR $server_addr;","fastcgi_param SERVER_PORT $server_port;","fastcgi_param SERVER_NAME $server_name;","fastcgi_param REDIRECT_STATUS 200;","TS_CLOUD_FCGI_EOF","cat > /etc/nginx/nginx.conf <<'TS_CLOUD_NGINXCONF_EOF'","user www-data;","worker_processes auto;","pid /run/nginx.pid;","error_log /var/log/nginx/error.log;","events { worker_connections 1024; }","http {"," default_type application/octet-stream;"," types {"," text/html html htm;"," text/css css;"," application/javascript js;"," application/json json;"," image/svg+xml svg;"," image/png png;"," image/jpeg jpg jpeg;"," image/gif gif;"," image/x-icon ico;"," image/webp webp;"," font/woff2 woff2;"," font/woff woff;"," text/plain txt;"," }"," access_log /var/log/nginx/access.log;"," sendfile on;"," tcp_nopush on;"," keepalive_timeout 65;"," server_tokens off;"," client_max_body_size 100m;"," client_body_temp_path /var/lib/nginx/body;"," proxy_temp_path /var/lib/nginx/proxy;"," fastcgi_temp_path /var/lib/nginx/fastcgi;"," uwsgi_temp_path /var/lib/nginx/uwsgi;"," scgi_temp_path /var/lib/nginx/scgi;"," server {"," listen 80 default_server;"," listen [::]:80 default_server;"," server_name _;"," return 444;"," }"," include /etc/nginx/sites-enabled/*;","}","TS_CLOUD_NGINXCONF_EOF","cat > /etc/systemd/system/ts-cloud-nginx.service <<'TS_CLOUD_NGINXUNIT_EOF'","[Unit]","Description=ts-cloud nginx (pantry)","After=network.target","","[Service]","Type=simple",`ExecStartPre=${xe} -t`,`ExecStart=${xe} -g 'daemon off;'`,`ExecReload=${xe} -s reload`,"Restart=always","RestartSec=3","","[Install]","WantedBy=multi-user.target","TS_CLOUD_NGINXUNIT_EOF","systemctl daemon-reload","systemctl enable ts-cloud-nginx","ls /etc/nginx/sites-enabled/* >/dev/null 2>&1 && systemctl restart ts-cloud-nginx || true"]}import{readFileSync as tc}from"node:fs";function rc(e){if(typeof e==="string")return e;if(!/^\/[\w./-]+$/.test(e.envFile)||!/^[A-Za-z_][A-Za-z0-9_]*$/.test(e.variable))throw Error("Invalid notification env-file reference");let r=tc(e.envFile,"utf8").split(/\r?\n/).map((n)=>n.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/)).find((n)=>n?.[1]===e.variable)?.[2];if(!r)throw Error(`Notification variable ${e.variable} is missing`);return r.replace(/^(['"])(.*)\1$/,"$2")}function Rt(e,t){return!e.events||e.events.includes(t)}async function Ur(e,t,r,n={}){if(!e)return[];let s=n.fetchImpl??globalThis.fetch,i=[],o=[],a=async(c,l)=>{try{return await s(rc(c),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)})}catch{return}};if(e.slack?.webhookUrl&&Rt(e,t))i.push("slack"),o.push(a(e.slack.webhookUrl,{text:r}));if(e.discord?.webhookUrl&&Rt(e,t))i.push("discord"),o.push(a(e.discord.webhookUrl,{content:r}));if(e.telegram?.botToken&&e.telegram.chatId&&Rt(e,t)){i.push("telegram");let c=`https://api.telegram.org/bot${e.telegram.botToken}/sendMessage`;o.push(a(c,{chat_id:e.telegram.chatId,text:r}))}if(e.webhook?.url&&Rt(e,t))if(i.push("webhook"),(e.webhook.method||"POST")==="GET"){let c=e.webhook.url.includes("?")?"&":"?",l=`${e.webhook.url}${c}event=${encodeURIComponent(t)}&message=${encodeURIComponent(r)}`;o.push(s(l,{method:"GET"}).catch(()=>{return}))}else o.push(a(e.webhook.url,{event:t,message:r}));if(e.email?.to&&Rt(e,t))i.push("email"),o.push(nc(e.email,t,r));return await Promise.all(o),i}async function nc(e,t,r){try{let n=await import("./chunk-tx56y5qk.js");await n.email.send({to:e.to,from:e.from,subject:`[ts-cloud] ${t}`,text:r})}catch{}}function ls(e,t){return t??e}function Me(e){if(!e)return[];let t=(s)=>"'"+s.replaceAll("'",`'"'"'`)+"'",r=(s)=>{if(typeof s==="string")return t(s);if(!/^\/[\w./-]+$/.test(s.envFile)||!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s.variable))throw Error("Invalid notification env-file reference");return'"$(read_env '+t(s.envFile)+" "+t(s.variable)+')"'},n=[];for(let[s,i]of[["slack","text"],["discord","content"]]){let o=e[s]?.webhookUrl;if(o)n.push("URL="+r(o),'post "$URL" '+t(i))}if(e.telegram?.botToken&&e.telegram.chatId)n.push("curl -fsS --max-time 30 -X POST "+t("https://api.telegram.org/bot"+e.telegram.botToken+"/sendMessage")+" --data-urlencode "+t("chat_id="+e.telegram.chatId)+' --data-urlencode "text=$MSG" >/dev/null || FAILED=1');if(e.webhook?.url)if(e.webhook.method==="GET")n.push("curl -fsS --max-time 30 -G "+t(e.webhook.url)+' --data-urlencode "message=$MSG" >/dev/null || FAILED=1');else n.push("post "+t(e.webhook.url)+" message");if(!n.length)return[];return["cat > /usr/local/bin/ts-cloud-notify.new <<'TS_CLOUD_NOTIFY_EOF'","#!/bin/bash","set -uo pipefail",'MSG="${1:-}"; FAILED=0',String.raw`read_env(){ awk -v key="$2" '$0 ~ "^[[:space:]]*(export[[:space:]]+)?" key "[[:space:]]*=" { sub("^[^=]*=[[:space:]]*", ""); sub(/[\r[:space:]]+$/, ""); if (($0 ~ /^".*"$/) || ($0 ~ /^\047.*\047$/)) $0=substr($0,2,length($0)-2); print; exit }' "$1"; }`,String.raw`ESCAPED=$(printf %s "$MSG" | LC_ALL=C awk 'BEGIN{ORS=""; for(i=1;i<32;i++) esc[sprintf("%c",i)]=sprintf("\\u%04x",i)} {if(NR>1) printf "\\n"; for(i=1;i<=length($0);i++){ c=substr($0,i,1); if(c=="\\") printf "\\\\"; else if(c=="\"") printf "\\\""; else if(c in esc) printf "%s",esc[c]; else printf "%s",c }}')`,String.raw`post(){ if [ -z "$1" ]; then FAILED=1; return; fi; curl -fsS --max-time 30 -X POST -H 'Content-Type: application/json' --data "{\"$2\":\"$ESCAPED\"}" "$1" >/dev/null || FAILED=1; }`,...n,'exit "$FAILED"',"TS_CLOUD_NOTIFY_EOF","chmod 0700 /usr/local/bin/ts-cloud-notify.new","mv -f /usr/local/bin/ts-cloud-notify.new /usr/local/bin/ts-cloud-notify"]}import{isIP as ps}from"node:net";var cn="/etc/rpx/certs",ds="/etc/rpx/local-ca",sc="rpx-root-ca.crt";function Gt(e=ds){return`${e.replace(/\/+$/,"")}/${sc}`}function Fr(e){if(e.toLowerCase().endsWith(".local"))return!1;return e.split(".").length===2}function fo(e,t={}){let r=new Set;for(let s of Object.values(e)){if(!s?.domain)continue;if(w(s)==="bucket")continue;r.add(s.domain)}let n=new Set;for(let s of Object.values(e)){if(!s?.domain||w(s)==="bucket")continue;for(let i of s.aliases??[]){let o=String(i).trim().toLowerCase();if(o&&o!==s.domain)n.add(o)}}if(t.autoWww!==!1)for(let s of[...r]){if(!Fr(s))continue;r.add(`www.${s}`)}for(let s of n)r.add(s);return[...r]}var ic=["1.1.1.1","8.8.8.8"],oc="/var/www/acme-challenge";function ac(e){if(typeof e==="string")return{to:e};let t={to:e.to};if(e.status!=null)t.status=e.status;if(e.preservePath!=null)t.preservePath=e.preservePath;return t}function cc(e){let t=e.auth;if(!t||t.enabled===!1||!t.password)return;return{username:t.username||"admin",password:t.password,...t.realm?{realm:t.realm}:{}}}function Br(e){if(!e)return;return e.tls??"local-ca"}function ho(e,t){if(e.cloud?.provider!=="ssh")return;let r=e.ssh?.lan;if(!r)return;return{...r.hostname?{hostname:r.hostname}:{},tls:r.tls??"local-ca",...t?{ip:t}:{}}}function ms(e){if(e.cloud?.provider!=="ssh")return;return e.ssh?.profile}function lc(e){let t=e.trim().toLowerCase();if(!t||t.startsWith("*")||t.includes(":")||t.includes("/")||t.includes(" "))return!1;if(ps(t)!==0)return!1;return t.endsWith(".local")||!t.includes(".")}function uc(e,t,r){let n=[],s=new Set,i=(l,u)=>{let d=l.trim().toLowerCase();if(!d||s.has(d))return;s.add(d),n.push({host:d,source:u})},o=t.hostname?.trim(),a=(o||`${r}.local`).toLowerCase();if(i(a,o?"ssh.lan.hostname":`the ssh provider's default LAN name for project "${r}"`),Object.entries(e).some(([l,u])=>!!u&&Vr(l)))i(`dashboard.${a}`,"the management dashboard site");for(let[l,u]of Object.entries(e)){if(!u?.domain)continue;if(w(u)==="bucket")continue;if(!lc(u.domain))continue;i(u.domain,`site "${l}"`)}return n}function pc(e,t){if(!t?.length)return;return t.find((r)=>{let n=(r.startsWith(".")?r.slice(1):r).toLowerCase();return e===n||e.endsWith(`.${n}`)})}function dc(e,t){if(!t?.enabled)return;for(let{host:r,source:n}of e){let s=pc(r,t.allowedSuffixes);if(!s)continue;throw Error(`The LAN certificate authority and public on-demand TLS both claim "${r}". LAN source: ${n}. Public source: the on-demand TLS allowed suffix "${s}", which is derived from the site domains. A host is either LAN-only, with a certificate signed by the box's own CA, or public, with one issued by Let's Encrypt, never both. Give the LAN a name no site domain covers (ssh.lan.hostname), or take that domain out of the sites this deploy routes.`)}}function mc(e){let t=e.split(".");return t.length>=3&&t[0]==="*"&&t.slice(1).every((r)=>/^[a-z0-9-]+$/i.test(r))}function fs(e,t={}){let{wildcardCerts:r,...n}=e;return JSON.stringify({...t,...n},null,2)}function gs(e){if(!e||e==="/")return;let t=`/${e}`.replace(/\/+/g,"/").replace(/\/+$/,"");if(!t.startsWith("/"))t=`/${t}`;return t===""||t==="/"?void 0:t}function jt(e,t){let n=(t?`${e}${t}`:e).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return n.length>0?n:"rpx"}function fc(e,t){if(!t||t.length===0)return`localhost:${e}`;return t.map((r)=>`${r.privateIp??r.publicIp}:${e}`)}function hs(e,t,r){let n=(t.wwwRoot??"/var/www").replace(/\/+$/,""),s=t.slug??"app",i=t.proxy.certsDir??cn,o=t.proxy.loadBalancer,a=[],c=new Set,l=new Map;for(let[h,f]of Object.entries(e)){if(!f||!f.domain)continue;let S=w(f);if(S==="bucket")continue;let x=a.length,B=[...new Set((f.aliases??[]).map((M)=>String(M).trim().toLowerCase()).filter((M)=>M&&M!==f.domain))],L=()=>{let M=a.slice(x).filter((H)=>H.to===f.domain);for(let H of B){for(let z of M)a.push({...z,to:H,id:jt(H,z.path)});if(mc(H)){if(!l.has(H))l.set(H,{domain:H,dnsEnvFile:`${n}/${s}-${h}/shared/.env`})}else c.add(H)}},N=gs(f.path),D=jt(f.domain,N),E=cc(f);if(S==="redirect"){a.push({to:f.domain,path:N,redirect:ac(f.redirect),id:D,...E?{auth:E}:{}}),c.add(f.domain),L();continue}if(S==="proxy"){let M=Us(f);if(M.length===0)continue;let H=M.length===1?M[0]:M;a.push({to:f.domain,path:N,from:H,id:D,...E?{auth:E}:{},...Array.isArray(H)&&o?{loadBalancer:o}:{}}),c.add(f.domain),L();continue}if(S==="server-app"){if(typeof f.port!=="number")continue;let M=fc(f.port,r);a.push({to:f.domain,path:N,from:M,id:D,...E?{auth:E}:{},...Array.isArray(M)&&o?{loadBalancer:o}:{}})}else a.push({to:f.domain,path:N,static:{dir:`${n}/${s}-${h}/current`,spa:f.spa??!1,pathRewriteStyle:f.pathRewriteStyle??"directory"},cleanUrls:f.pathRewriteStyle!=="flat",...E?{auth:E}:{},id:D});c.add(f.domain),L()}if(t.proxy.autoWww!==!1)for(let h of[...c]){if(!Fr(h))continue;let f=`www.${h}`;if(c.has(f))continue;a.push({to:f,redirect:{to:`https://${h}`},id:jt(f)}),c.add(f)}a.sort((h,f)=>{if(h.to!==f.to)return h.to.localeCompare(f.to);return(f.path?.length??0)-(h.path?.length??0)});let u={proxies:a,productionCerts:{certsDir:i,certsDirServerNames:[...c,...l.keys()]},https:!0,hostsManagement:!1,cleanup:{hosts:!1,certs:!1},...l.size>0?{wildcardCerts:[...l.values()]}:{}};if(t.proxy.onDemandTls&&c.size>0)u.onDemandTls={enabled:!0,allowedSuffixes:[...c],email:t.proxy.onDemandTlsEmail,certsDir:i,staging:t.proxy.onDemandTlsStaging??!1};if(t.proxy.onDemandTls)u.acmeChallengeWebroot=t.proxy.acmeWebroot??oc;let d=t.proxy.cdn,m=d?.frontedHosts?.length?d.frontedHosts:[...c];if(d?.secret&&m.length>0)u.originGuard={header:d.secretHeader??"X-Origin-Verify",value:d.secret,hosts:m};if(t.lan)if(Br(t.lan)==="off")u.https=!1;else{let h=uc(e,t.lan,s);dc(h,u.onDemandTls);let f=t.lan.ip&&ps(t.lan.ip)!==0?[t.lan.ip]:[];u.localCa={dir:ds,hosts:h.map((S)=>S.host),...f.length>0?{ips:f}:{},installTrust:!0}}return u}function Te(e,t){return hs({...e,...t.mail?Jo(t.mail):{}},t)}function Je(e,t,r){return hs({...e,...r.mail?Jo(r.mail):{}},r,t)}function ee(e){return e?.webServer==="rpx"||e?.proxy?.engine==="rpx"}var Et="/etc/rpx",pe="/opt/rpx-gateway",Mr="/etc/rpx/gateway.ts",Xe="/etc/rpx/gateway",we="rpx-gateway.service",he="/etc/rpx/sites.d";function gc(e=he,t=cn){return`// Generated by ts-cloud — rpx gateway assembler.
|
|
15
|
+
`}function Nr(e){let t=`/etc/nginx/sites-available/${e.siteName}`,r=`/etc/nginx/sites-enabled/${e.siteName}`,n=ec(e),s=[...Hr()];if(e.auth){let i=cs(e.siteName),o=(l)=>l.split("'").join("'\\''"),a=o(e.auth.password),c=o(e.auth.username);s.push(`TS_CLOUD_HTPASS=$(openssl passwd -apr1 '${a}')`,`printf '%s:%s\\n' '${c}' "$TS_CLOUD_HTPASS" > ${i}`,`chmod 640 ${i}`,`chown root:www-data ${i} 2>/dev/null || true`)}return s.push(`cat > ${t} <<'TS_CLOUD_NGINX_EOF'`,n.replace(/\n$/,""),"TS_CLOUD_NGINX_EOF",`ln -sf ${t} ${r}`,"rm -f /etc/nginx/sites-enabled/default",`${xe} -t`,"systemctl reload ts-cloud-nginx 2>/dev/null || systemctl restart ts-cloud-nginx 2>/dev/null || true"),s}var xe="/usr/local/bin/ts-cloud-nginx";function Hr(e="/opt/pantry"){return[...Tt(),...it(["nginx.org"]),"mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled /var/log/nginx /var/lib/nginx/body /var/lib/nginx/proxy /var/lib/nginx/fastcgi /var/lib/nginx/uwsgi /var/lib/nginx/scgi","getent passwd www-data >/dev/null || useradd --system --no-create-home --shell /usr/sbin/nologin www-data","chown -R www-data:www-data /var/lib/nginx /var/log/nginx",`cat > ${xe} <<'TS_CLOUD_NGINXBIN_EOF'`,"#!/bin/sh",`eval "$(cd ${e} && pantry env 2>/dev/null)"`,"NGINX_BIN=/opt/pantry/pantry/.bin/nginx",'if [ ! -x "$NGINX_BIN" ]; then NGINX_BIN=$(command -v nginx || true); fi','if [ -z "$NGINX_BIN" ]; then echo "nginx binary not found; run pantry install nginx.org or install nginx explicitly" >&2; exit 1; fi','exec "$NGINX_BIN" -c /etc/nginx/nginx.conf "$@"',"TS_CLOUD_NGINXBIN_EOF",`chmod +x ${xe}`,"cat > /etc/nginx/fastcgi_params <<'TS_CLOUD_FCGI_EOF'","fastcgi_param QUERY_STRING $query_string;","fastcgi_param REQUEST_METHOD $request_method;","fastcgi_param CONTENT_TYPE $content_type;","fastcgi_param CONTENT_LENGTH $content_length;","fastcgi_param SCRIPT_NAME $fastcgi_script_name;","fastcgi_param REQUEST_URI $request_uri;","fastcgi_param DOCUMENT_URI $document_uri;","fastcgi_param DOCUMENT_ROOT $document_root;","fastcgi_param SERVER_PROTOCOL $server_protocol;","fastcgi_param REQUEST_SCHEME $scheme;","fastcgi_param HTTPS $https if_not_empty;","fastcgi_param GATEWAY_INTERFACE CGI/1.1;","fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;","fastcgi_param REMOTE_ADDR $remote_addr;","fastcgi_param REMOTE_PORT $remote_port;","fastcgi_param SERVER_ADDR $server_addr;","fastcgi_param SERVER_PORT $server_port;","fastcgi_param SERVER_NAME $server_name;","fastcgi_param REDIRECT_STATUS 200;","TS_CLOUD_FCGI_EOF","cat > /etc/nginx/nginx.conf <<'TS_CLOUD_NGINXCONF_EOF'","user www-data;","worker_processes auto;","pid /run/nginx.pid;","error_log /var/log/nginx/error.log;","events { worker_connections 1024; }","http {"," default_type application/octet-stream;"," types {"," text/html html htm;"," text/css css;"," application/javascript js;"," application/json json;"," image/svg+xml svg;"," image/png png;"," image/jpeg jpg jpeg;"," image/gif gif;"," image/x-icon ico;"," image/webp webp;"," font/woff2 woff2;"," font/woff woff;"," text/plain txt;"," }"," access_log /var/log/nginx/access.log;"," sendfile on;"," tcp_nopush on;"," keepalive_timeout 65;"," server_tokens off;"," client_max_body_size 100m;"," client_body_temp_path /var/lib/nginx/body;"," proxy_temp_path /var/lib/nginx/proxy;"," fastcgi_temp_path /var/lib/nginx/fastcgi;"," uwsgi_temp_path /var/lib/nginx/uwsgi;"," scgi_temp_path /var/lib/nginx/scgi;"," server {"," listen 80 default_server;"," listen [::]:80 default_server;"," server_name _;"," return 444;"," }"," include /etc/nginx/sites-enabled/*;","}","TS_CLOUD_NGINXCONF_EOF","cat > /etc/systemd/system/ts-cloud-nginx.service <<'TS_CLOUD_NGINXUNIT_EOF'","[Unit]","Description=ts-cloud nginx (pantry)","After=network.target","","[Service]","Type=simple",`ExecStartPre=${xe} -t`,`ExecStart=${xe} -g 'daemon off;'`,`ExecReload=${xe} -s reload`,"Restart=always","RestartSec=3","","[Install]","WantedBy=multi-user.target","TS_CLOUD_NGINXUNIT_EOF","systemctl daemon-reload","systemctl enable ts-cloud-nginx","ls /etc/nginx/sites-enabled/* >/dev/null 2>&1 && systemctl restart ts-cloud-nginx || true"]}import{readFileSync as tc}from"node:fs";function rc(e){if(typeof e==="string")return e;if(!/^\/[\w./-]+$/.test(e.envFile)||!/^[A-Za-z_][A-Za-z0-9_]*$/.test(e.variable))throw Error("Invalid notification env-file reference");let r=tc(e.envFile,"utf8").split(/\r?\n/).map((n)=>n.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/)).find((n)=>n?.[1]===e.variable)?.[2];if(!r)throw Error(`Notification variable ${e.variable} is missing`);return r.replace(/^(['"])(.*)\1$/,"$2")}function Rt(e,t){return!e.events||e.events.includes(t)}async function Ur(e,t,r,n={}){if(!e)return[];let s=n.fetchImpl??globalThis.fetch,i=[],o=[],a=async(c,l)=>{try{return await s(rc(c),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)})}catch{return}};if(e.slack?.webhookUrl&&Rt(e,t))i.push("slack"),o.push(a(e.slack.webhookUrl,{text:r}));if(e.discord?.webhookUrl&&Rt(e,t))i.push("discord"),o.push(a(e.discord.webhookUrl,{content:r}));if(e.telegram?.botToken&&e.telegram.chatId&&Rt(e,t)){i.push("telegram");let c=`https://api.telegram.org/bot${e.telegram.botToken}/sendMessage`;o.push(a(c,{chat_id:e.telegram.chatId,text:r}))}if(e.webhook?.url&&Rt(e,t))if(i.push("webhook"),(e.webhook.method||"POST")==="GET"){let c=e.webhook.url.includes("?")?"&":"?",l=`${e.webhook.url}${c}event=${encodeURIComponent(t)}&message=${encodeURIComponent(r)}`;o.push(s(l,{method:"GET"}).catch(()=>{return}))}else o.push(a(e.webhook.url,{event:t,message:r}));if(e.email?.to&&Rt(e,t))i.push("email"),o.push(nc(e.email,t,r));return await Promise.all(o),i}async function nc(e,t,r){try{let n=await import("./chunk-tx56y5qk.js");await n.email.send({to:e.to,from:e.from,subject:`[ts-cloud] ${t}`,text:r})}catch{}}function ls(e,t){return t??e}function ze(e){if(!e)return[];let t=(s)=>"'"+s.replaceAll("'",`'"'"'`)+"'",r=(s)=>{if(typeof s==="string")return t(s);if(!/^\/[\w./-]+$/.test(s.envFile)||!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s.variable))throw Error("Invalid notification env-file reference");return'"$(read_env '+t(s.envFile)+" "+t(s.variable)+')"'},n=[];for(let[s,i]of[["slack","text"],["discord","content"]]){let o=e[s]?.webhookUrl;if(o)n.push("URL="+r(o),'post "$URL" '+t(i))}if(e.telegram?.botToken&&e.telegram.chatId)n.push("curl -fsS --max-time 30 -X POST "+t("https://api.telegram.org/bot"+e.telegram.botToken+"/sendMessage")+" --data-urlencode "+t("chat_id="+e.telegram.chatId)+' --data-urlencode "text=$MSG" >/dev/null || FAILED=1');if(e.webhook?.url)if(e.webhook.method==="GET")n.push("curl -fsS --max-time 30 -G "+t(e.webhook.url)+' --data-urlencode "message=$MSG" >/dev/null || FAILED=1');else n.push("post "+t(e.webhook.url)+" message");if(!n.length)return[];return["cat > /usr/local/bin/ts-cloud-notify.new <<'TS_CLOUD_NOTIFY_EOF'","#!/bin/bash","set -uo pipefail",'MSG="${1:-}"; FAILED=0',String.raw`read_env(){ awk -v key="$2" '$0 ~ "^[[:space:]]*(export[[:space:]]+)?" key "[[:space:]]*=" { sub("^[^=]*=[[:space:]]*", ""); sub(/[\r[:space:]]+$/, ""); if (($0 ~ /^".*"$/) || ($0 ~ /^\047.*\047$/)) $0=substr($0,2,length($0)-2); print; exit }' "$1"; }`,String.raw`ESCAPED=$(printf %s "$MSG" | LC_ALL=C awk 'BEGIN{ORS=""; for(i=1;i<32;i++) esc[sprintf("%c",i)]=sprintf("\\u%04x",i)} {if(NR>1) printf "\\n"; for(i=1;i<=length($0);i++){ c=substr($0,i,1); if(c=="\\") printf "\\\\"; else if(c=="\"") printf "\\\""; else if(c in esc) printf "%s",esc[c]; else printf "%s",c }}')`,String.raw`post(){ if [ -z "$1" ]; then FAILED=1; return; fi; curl -fsS --max-time 30 -X POST -H 'Content-Type: application/json' --data "{\"$2\":\"$ESCAPED\"}" "$1" >/dev/null || FAILED=1; }`,...n,'exit "$FAILED"',"TS_CLOUD_NOTIFY_EOF","chmod 0700 /usr/local/bin/ts-cloud-notify.new","mv -f /usr/local/bin/ts-cloud-notify.new /usr/local/bin/ts-cloud-notify"]}import{isIP as ps}from"node:net";var cn="/etc/rpx/certs",ds="/etc/rpx/local-ca",sc="rpx-root-ca.crt";function Gt(e=ds){return`${e.replace(/\/+$/,"")}/${sc}`}function Fr(e){if(e.toLowerCase().endsWith(".local"))return!1;return e.split(".").length===2}function fo(e,t={}){let r=new Set;for(let s of Object.values(e)){if(!s?.domain)continue;if(w(s)==="bucket")continue;r.add(s.domain)}let n=new Set;for(let s of Object.values(e)){if(!s?.domain||w(s)==="bucket")continue;for(let i of s.aliases??[]){let o=String(i).trim().toLowerCase();if(o&&o!==s.domain)n.add(o)}}if(t.autoWww!==!1)for(let s of[...r]){if(!Fr(s))continue;r.add(`www.${s}`)}for(let s of n)r.add(s);return[...r]}var ic=["1.1.1.1","8.8.8.8"],oc="/var/www/acme-challenge";function ac(e){if(typeof e==="string")return{to:e};let t={to:e.to};if(e.status!=null)t.status=e.status;if(e.preservePath!=null)t.preservePath=e.preservePath;return t}function cc(e){let t=e.auth;if(!t||t.enabled===!1||!t.password)return;return{username:t.username||"admin",password:t.password,...t.realm?{realm:t.realm}:{}}}function Br(e){if(!e)return;return e.tls??"local-ca"}function ho(e,t){if(e.cloud?.provider!=="ssh")return;let r=e.ssh?.lan;if(!r)return;return{...r.hostname?{hostname:r.hostname}:{},tls:r.tls??"local-ca",...t?{ip:t}:{}}}function ms(e){if(e.cloud?.provider!=="ssh")return;return e.ssh?.profile}function lc(e){let t=e.trim().toLowerCase();if(!t||t.startsWith("*")||t.includes(":")||t.includes("/")||t.includes(" "))return!1;if(ps(t)!==0)return!1;return t.endsWith(".local")||!t.includes(".")}function uc(e,t,r){let n=[],s=new Set,i=(l,u)=>{let d=l.trim().toLowerCase();if(!d||s.has(d))return;s.add(d),n.push({host:d,source:u})},o=t.hostname?.trim(),a=(o||`${r}.local`).toLowerCase();if(i(a,o?"ssh.lan.hostname":`the ssh provider's default LAN name for project "${r}"`),Object.entries(e).some(([l,u])=>!!u&&Vr(l)))i(`dashboard.${a}`,"the management dashboard site");for(let[l,u]of Object.entries(e)){if(!u?.domain)continue;if(w(u)==="bucket")continue;if(!lc(u.domain))continue;i(u.domain,`site "${l}"`)}return n}function pc(e,t){if(!t?.length)return;return t.find((r)=>{let n=(r.startsWith(".")?r.slice(1):r).toLowerCase();return e===n||e.endsWith(`.${n}`)})}function dc(e,t){if(!t?.enabled)return;for(let{host:r,source:n}of e){let s=pc(r,t.allowedSuffixes);if(!s)continue;throw Error(`The LAN certificate authority and public on-demand TLS both claim "${r}". LAN source: ${n}. Public source: the on-demand TLS allowed suffix "${s}", which is derived from the site domains. A host is either LAN-only, with a certificate signed by the box's own CA, or public, with one issued by Let's Encrypt, never both. Give the LAN a name no site domain covers (ssh.lan.hostname), or take that domain out of the sites this deploy routes.`)}}function mc(e){let t=e.split(".");return t.length>=3&&t[0]==="*"&&t.slice(1).every((r)=>/^[a-z0-9-]+$/i.test(r))}function fs(e,t={}){let{wildcardCerts:r,...n}=e;return JSON.stringify({...t,...n},null,2)}function gs(e){if(!e||e==="/")return;let t=`/${e}`.replace(/\/+/g,"/").replace(/\/+$/,"");if(!t.startsWith("/"))t=`/${t}`;return t===""||t==="/"?void 0:t}function jt(e,t){let n=(t?`${e}${t}`:e).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return n.length>0?n:"rpx"}function fc(e,t){if(!t||t.length===0)return`localhost:${e}`;return t.map((r)=>`${r.privateIp??r.publicIp}:${e}`)}function hs(e,t,r){let n=(t.wwwRoot??"/var/www").replace(/\/+$/,""),s=t.slug??"app",i=t.proxy.certsDir??cn,o=t.proxy.loadBalancer,a=[],c=new Set,l=new Map;for(let[h,f]of Object.entries(e)){if(!f||!f.domain)continue;let S=w(f);if(S==="bucket")continue;let x=a.length,B=[...new Set((f.aliases??[]).map((M)=>String(M).trim().toLowerCase()).filter((M)=>M&&M!==f.domain))],L=()=>{let M=a.slice(x).filter((H)=>H.to===f.domain);for(let H of B){for(let z of M)a.push({...z,to:H,id:jt(H,z.path)});if(mc(H)){if(!l.has(H))l.set(H,{domain:H,dnsEnvFile:`${n}/${s}-${h}/shared/.env`})}else c.add(H)}},N=gs(f.path),D=jt(f.domain,N),E=cc(f);if(S==="redirect"){a.push({to:f.domain,path:N,redirect:ac(f.redirect),id:D,...E?{auth:E}:{}}),c.add(f.domain),L();continue}if(S==="proxy"){let M=Us(f);if(M.length===0)continue;let H=M.length===1?M[0]:M;a.push({to:f.domain,path:N,from:H,id:D,...E?{auth:E}:{},...Array.isArray(H)&&o?{loadBalancer:o}:{}}),c.add(f.domain),L();continue}if(S==="server-app"){if(typeof f.port!=="number")continue;let M=fc(f.port,r);a.push({to:f.domain,path:N,from:M,id:D,...E?{auth:E}:{},...Array.isArray(M)&&o?{loadBalancer:o}:{}})}else a.push({to:f.domain,path:N,static:{dir:`${n}/${s}-${h}/current`,spa:f.spa??!1,pathRewriteStyle:f.pathRewriteStyle??"directory"},cleanUrls:f.pathRewriteStyle!=="flat",...E?{auth:E}:{},id:D});c.add(f.domain),L()}if(t.proxy.autoWww!==!1)for(let h of[...c]){if(!Fr(h))continue;let f=`www.${h}`;if(c.has(f))continue;a.push({to:f,redirect:{to:`https://${h}`},id:jt(f)}),c.add(f)}a.sort((h,f)=>{if(h.to!==f.to)return h.to.localeCompare(f.to);return(f.path?.length??0)-(h.path?.length??0)});let u={proxies:a,productionCerts:{certsDir:i,certsDirServerNames:[...c,...l.keys()]},https:!0,hostsManagement:!1,cleanup:{hosts:!1,certs:!1},...l.size>0?{wildcardCerts:[...l.values()]}:{}};if(t.proxy.onDemandTls&&c.size>0)u.onDemandTls={enabled:!0,allowedSuffixes:[...c],email:t.proxy.onDemandTlsEmail,certsDir:i,staging:t.proxy.onDemandTlsStaging??!1};if(t.proxy.onDemandTls)u.acmeChallengeWebroot=t.proxy.acmeWebroot??oc;let d=t.proxy.cdn,m=d?.frontedHosts?.length?d.frontedHosts:[...c];if(d?.secret&&m.length>0)u.originGuard={header:d.secretHeader??"X-Origin-Verify",value:d.secret,hosts:m};if(t.lan)if(Br(t.lan)==="off")u.https=!1;else{let h=uc(e,t.lan,s);dc(h,u.onDemandTls);let f=t.lan.ip&&ps(t.lan.ip)!==0?[t.lan.ip]:[];u.localCa={dir:ds,hosts:h.map((S)=>S.host),...f.length>0?{ips:f}:{},installTrust:!0}}return u}function Te(e,t){return hs({...e,...t.mail?Jo(t.mail):{}},t)}function Je(e,t,r){return hs({...e,...r.mail?Jo(r.mail):{}},r,t)}function ee(e){return e?.webServer==="rpx"||e?.proxy?.engine==="rpx"}var Et="/etc/rpx",pe="/opt/rpx-gateway",Mr="/etc/rpx/gateway.ts",Xe="/etc/rpx/gateway",we="rpx-gateway.service",he="/etc/rpx/sites.d";function gc(e=he,t=cn){return`// Generated by ts-cloud — rpx gateway assembler.
|
|
16
16
|
// Merges every app's fragment in ${e} so independent deploys compose
|
|
17
17
|
// without clobbering each other. Each deploy writes only its own <slug>.json.
|
|
18
18
|
import { startProxies } from '@stacksjs/rpx'
|
|
@@ -141,7 +141,7 @@ writeFileSync(candidatePath, JSON.stringify(candidate, null, 2) + '\\n', { mode:
|
|
|
141
141
|
`);return[`mkdir -p ${n}`,...e.tenant?[]:[`(cd ${pe} && ${i} add @stacksjs/tlsx@${o}) || true`],...Ye(h,L,"TS_CLOUD_RENEW_EOF"),`chmod +x ${h}`,...Ye(`/etc/systemd/system/${f}`,N,"TS_CLOUD_RENEW_SVC_EOF"),...Ye(`/etc/systemd/system/${S}`,D,"TS_CLOUD_RENEW_TIMER_EOF"),"systemctl daemon-reload",`systemctl enable --now ${S} || true`,`${h} || true`]}function $e(e){let{config:t,proxy:r}=e,n=e.bunBin??"/usr/local/bin/bun",s=r.version??"latest",i=t.productionCerts.certsDir,o=(e.slug||"app").replace(/[^a-z0-9._-]+/gi,"-"),a=fs(t,{slug:o}),c=gc(he,i);if(e.tenant)return["set -euo pipefail",`if [ ! -x ${Xe} ] || [ ! -d ${pe}/node_modules/@stacksjs/rpx ]; then`,` echo "ts-cloud: no rpx gateway on this box. This project attaches to another project's compute, so it does not provision one - deploy the owning project first." >&2`," exit 1","fi",`mkdir -p ${he} ${i}`,`__tsc_rpx_before="$(sha256sum ${he}/${o}.json 2>/dev/null | cut -d' ' -f1 || true)"`,...zr(`${he}/${o}.json`,a,"TS_CLOUD_RPX_FRAGMENT_EOF",{bunBin:n,preserveManagementDashboardRoutes:e.preserveManagementDashboardRoutes}),`__tsc_rpx_after="$(sha256sum ${he}/${o}.json 2>/dev/null | cut -d' ' -f1 || true)"`,`if [ "$__tsc_rpx_before" != "$__tsc_rpx_after" ] || ! systemctl is-active --quiet ${we}; then`,` systemctl restart ${we}`,"else",` echo "[ts-cloud] rpx routes for ${o} unchanged - leaving the shared gateway alone"`,"fi",...us(e)];let u=[`Environment=RPX_UPSTREAM_TIMEOUT=${r.upstreamTimeout??60}`];if(typeof r.maxUpstreamConns==="number")u.push(`Environment=RPX_MAX_UPSTREAM_CONNS=${r.maxUpstreamConns}`);let d=e.profile==="raspberry-pi"?yc:Sc,m=r.memoryHigh??d.high,h=r.memoryMax??d.max;return["set -euo pipefail",`mkdir -p ${Et} ${he} ${i} ${pe}`,`rm -rf ${pe}.next ${pe}.prev`,`mkdir -p ${pe}.next`,`(cd ${pe}.next && ${n} add @stacksjs/rpx@${s})`,`mv ${pe} ${pe}.prev`,`mv ${pe}.next ${pe}`,`rm -rf ${pe}.prev`,`ln -sfn ${pe}/node_modules ${Et}/node_modules`,...zr(`${he}/${o}.json`,a,"TS_CLOUD_RPX_FRAGMENT_EOF",{bunBin:n,preserveManagementDashboardRoutes:e.preserveManagementDashboardRoutes}),...Ye(Mr,c,"TS_CLOUD_RPX_EOF"),`${n} build --production --compile --outfile ${Xe}.next ${Mr}`,`chmod 0755 ${Xe}.next`,`mv -f ${Xe}.next ${Xe}`,...Ye(`/etc/systemd/system/${we}`,["[Unit]","Description=rpx reverse-proxy gateway (managed by ts-cloud)","After=network.target network-online.target","Wants=network-online.target","","[Service]","Type=simple",`ExecStart=${Xe}`,`WorkingDirectory=${pe}`,"Environment=BUN_INSTALL=/root/.bun","Environment=APP_ENV=production","Environment=NODE_ENV=production",...u,"MemoryAccounting=true",`MemoryHigh=${m}`,`MemoryMax=${h}`,"CPUWeight=500","IOWeight=500","OOMPolicy=stop","Restart=always","RestartSec=5","StartLimitIntervalSec=120","StartLimitBurst=5",`ExecStartPre=/bin/sh -c 'for p in 80 443; do fuser -k -n tcp "$p" 2>/dev/null || true; done; sleep 1'`,"LimitNOFILE=1048576","AmbientCapabilities=CAP_NET_BIND_SERVICE","","[Install]","WantedBy=multi-user.target"].join(`
|
|
142
142
|
`),"TS_CLOUD_RPX_UNIT_EOF"),"systemctl daemon-reload","systemctl disable --now bun-gateway.service 2>/dev/null || true","systemctl disable --now bun-gateway-renew.timer bun-gateway-renew.service 2>/dev/null || true","rm -f /etc/systemd/system/bun-gateway-renew.timer /etc/systemd/system/bun-gateway-renew.service","systemctl disable --now ts-cloud-nginx.service 2>/dev/null || true",`for timer in /etc/systemd/system/rpx-cert-renew-*.timer; do [ -e "$timer" ] || continue; unit="$(basename "$timer")"; tenant="\${unit#rpx-cert-renew-}"; tenant="\${tenant%.timer}"; [ -f "${he}/$tenant.json" ] && continue; systemctl disable --now "$unit" 2>/dev/null || true; rm -f "$timer" "/etc/systemd/system/rpx-cert-renew-$tenant.service" "${Et}/renew-certs-$tenant.sh"; done`,`systemctl enable ${we}`,`systemctl restart ${we}`,...us(e)]}function et(e){let t=(e.slug||"app").replace(/[^a-z0-9._-]+/gi,"-"),r=fs(e.config,{slug:t});return["set -euo pipefail",`mkdir -p ${he}`,...zr(`${he}/${t}.json`,r,"TS_CLOUD_RPX_FRAGMENT_EOF",{bunBin:"/usr/local/bin/bun",preserveManagementDashboardRoutes:e.preserveManagementDashboardRoutes}),`systemctl restart ${we}`]}function Qe(e=[],t={}){let r=t.path??"/root/.ssh/authorized_keys",s=[`mkdir -p ${r.replace(/\/[^/]*$/,"")}`,`touch ${r}`,`sed -i '/^${ys("# >>> ts-cloud managed keys >>>")}$/,/^${ys("# <<< ts-cloud managed keys <<<")}$/d' ${r}`];if(e.length>0){let i=["# >>> ts-cloud managed keys >>>",...e.map((o)=>`${o.publicKey.trim()} ${o.name}`),"# <<< ts-cloud managed keys <<<"].join(`
|
|
143
143
|
`);s.push(`cat >> ${r} <<'TS_CLOUD_KEYS_EOF'`,i,"TS_CLOUD_KEYS_EOF",`chmod 600 ${r}`)}return s}function ys(e){return e.replace(/[.*[\]\\/^$]/g,"\\$&")}function vs(e,t){let r=`/tmp/ts-cloud-${e}.sh`,n=`TS_CLOUD_${e.toUpperCase().replace(/[^A-Z0-9]/g,"_")}_EOF`;return[`cat > ${r} <<'${n}'`,t.replace(/\n$/,""),n,`chmod +x ${r}`,`bash ${r} || echo "ts-cloud: ${e} setup did not complete; continuing" >&2`,`rm -f ${r}`]}function bs(e,t){if(e===!1)return;if(e===!0||e===void 0)return t;return e}var Ss={enabled:!0,ports:[80,443]},bc={mode:"detection",paranoiaLevel:1};function wc(e=!0,t=[]){let r=bs(e,Ss);if(!r||r.enabled===!1)return[];let n=[...new Set([...r.ports??Ss.ports??[],...t])].sort((s,i)=>s-i);return vs("ddos",Pr({...r,ports:n}))}function $c(e=!0){let t=bs(e,bc);if(!t||t.mode==="off")return[];return vs("waf",Ms(t))}function ws(e={},t=[]){return[...wc(e.ddos,t),...$c(e.waf)]}function _c(e){return`${e}-sftp`}function Ts(e){return e.port??2222}function Tc(e){let t=e.sftp.storage;if(t?.type==="efs"&&t.path)return t.path;return`/var/sftp/${e.slug}`}function nt(e){return`'${e.split("'").join("'\\''")}'`}function Cs(e,t){if(t==="aws")return;let r=e.storage;if(r?.type==="s3"||!r&&!!e.bucket)throw Error(`sftp: bucket-backed storage is only available on the aws provider (Transfer Family). On ${t}, use storage: { type: 'efs' } to serve a directory on the server.`);for(let[s,i]of Object.entries(e.users))if(!i.sshPublicKeys?.length)throw Error(`sftp: user ${s} needs at least one SSH public key`)}function Ps(e){let{slug:t,sftp:r}=e,n=_c(t),s=Tc(e),i=Ts(r),o="/etc/ts-sftp/host_key",a="/etc/ts-sftp/users",c=r.serviceUser??"ts-sftp",l=[`# --- ts-sftp (${n}) ---`,`id -u ${c} >/dev/null 2>&1 || useradd --system --home-dir ${nt(s)} --shell /usr/sbin/nologin ${c}`,'export HOME="${HOME:-/root}"','command -v bun >/dev/null 2>&1 || (curl -fsSL https://bun.sh/install | bash && ln -sf "$HOME/.bun/bin/bun" /usr/local/bin/bun)',`mkdir -p ${nt(s)} /etc/ts-sftp /etc/ts-sftp/users /opt/ts-sftp`,"chmod 750 /etc/ts-sftp",`cd /opt/ts-sftp && [ -f package.json ] || printf '{"name":"ts-sftp-host","private":true}' > package.json`,`cd /opt/ts-sftp && bun add ts-sftp@${r.version??"latest"} --no-save --production || bun add ts-sftp@${r.version??"latest"}`,`[ -f /etc/ts-sftp/host_key ] || bun /opt/ts-sftp/node_modules/ts-sftp/dist/bin/cli.js keygen --out /etc/ts-sftp/host_key --comment ${nt(`${t}-sftp`)}`,"chmod 600 /etc/ts-sftp/host_key",'BUN_REAL=$(readlink -f "$(command -v bun)")','case "$BUN_REAL" in /root/*) install -m 0755 "$BUN_REAL" /usr/local/bin/ts-sftp-bun; BUN_BIN=/usr/local/bin/ts-sftp-bun;; *) BUN_BIN="$BUN_REAL";; esac'],u=[];for(let[d,m]of Object.entries(r.users)){let h=(m.homeDirectory??d).replace(/^\/+|\/+$/g,"");if(!h||h.split("/").includes(".."))throw Error(`sftp: invalid homeDirectory for user ${d}`);let f=`/etc/ts-sftp/users/${d}.pub`;l.push(`mkdir -p ${nt(`${s}/${h}`)}`,`printf '%s\\n' ${nt(m.sshPublicKeys.join(`
|
|
144
|
-
`))} > ${f}`,`chmod 644 ${f}`),u.push(`--user ${d}:${f}`)}return l.push(`chown -R ${c}:${c} ${nt(s)} /etc/ts-sftp`,`cat > /etc/systemd/system/${n}.service <<TSCLOUD_SFTP_UNIT`,"[Unit]","Description=ts-sftp file transfer server","After=network.target","","[Service]",`User=${c}`,`ExecStart=$BUN_BIN /opt/ts-sftp/node_modules/ts-sftp/dist/bin/cli.js serve --root ${s} --host-key /etc/ts-sftp/host_key --port ${i}${r.readOnly?" --read-only":""} ${u.join(" ")}`,"Restart=always","RestartSec=2","NoNewPrivileges=true","PrivateTmp=true","ProtectSystem=strict",`ReadWritePaths=${s}`,"","[Install]","WantedBy=multi-user.target","TSCLOUD_SFTP_UNIT","systemctl daemon-reload",`systemctl enable ${n}.service`,`systemctl restart ${n}.service`),l}function Rs(e){return e?[Ts(e)]:[]}var Cc=[80,443];function Wt(e={}){if(e.enabled===!1)return[];let t=[...new Set([...Cc,...e.allowedPorts||[]])].filter((n)=>Number.isInteger(n)&&n>=1&&n<=65535).sort((n,s)=>n-s),r=["export DEBIAN_FRONTEND=noninteractive","command -v ufw >/dev/null 2>&1 || apt-get install -y ufw","ufw --force reset","ufw default deny incoming","ufw default allow outgoing","ufw allow OpenSSH"];for(let n of t)r.push(`ufw allow ${n}/tcp`);return r.push("ufw --force enable"),r}function
|
|
144
|
+
`))} > ${f}`,`chmod 644 ${f}`),u.push(`--user ${d}:${f}`)}return l.push(`chown -R ${c}:${c} ${nt(s)} /etc/ts-sftp`,`cat > /etc/systemd/system/${n}.service <<TSCLOUD_SFTP_UNIT`,"[Unit]","Description=ts-sftp file transfer server","After=network.target","","[Service]",`User=${c}`,`ExecStart=$BUN_BIN /opt/ts-sftp/node_modules/ts-sftp/dist/bin/cli.js serve --root ${s} --host-key /etc/ts-sftp/host_key --port ${i}${r.readOnly?" --read-only":""} ${u.join(" ")}`,"Restart=always","RestartSec=2","NoNewPrivileges=true","PrivateTmp=true","ProtectSystem=strict",`ReadWritePaths=${s}`,"","[Install]","WantedBy=multi-user.target","TSCLOUD_SFTP_UNIT","systemctl daemon-reload",`systemctl enable ${n}.service`,`systemctl restart ${n}.service`),l}function Rs(e){return e?[Ts(e)]:[]}var Cc=[80,443];function Wt(e={}){if(e.enabled===!1)return[];let t=[...new Set([...Cc,...e.allowedPorts||[]])].filter((n)=>Number.isInteger(n)&&n>=1&&n<=65535).sort((n,s)=>n-s),r=["export DEBIAN_FRONTEND=noninteractive","command -v ufw >/dev/null 2>&1 || apt-get install -y ufw","ufw --force reset","ufw default deny incoming","ufw default allow outgoing","ufw allow OpenSSH"];for(let n of t)r.push(`ufw allow ${n}/tcp`);return r.push("ufw --force enable"),r}function Fe(e,t){let r=e.infrastructure?.compute??{},n=r.runtime==="php"||!!r.php,s=ne(e,{environment:t?.environment}),i=n||!!r.managedServices,o=i?Tt():[],a=!ee({...r,proxy:le(e,{environment:t?.environment})}),c=n?[...o,...Kt({versions:r.php?.versions,default:r.php?.default,extensions:r.php?.extensions,installNginx:a,optimizeForProduction:r.php?.optimizeForProduction,ini:r.php?.ini}),...a?Hr():[]]:void 0,l=[],u=A(e);if(!n&&i)l.push(...o);if(l.push(...ze(e.notifications)),r.managedServices)l.push(...Do(r.managedServices,{mail:s.enabled?s:void 0}),...wt(u,r.managedServices));let d=e.infrastructure?.sftp;if(d)Cs(d,U(e)),l.push(...Ps({slug:e.project.slug,sftp:d}));let m=r.firewall??(n?{enabled:!0}:{enabled:!1}),h=jr(s);if(l.push(...Wt(h.length>0?{...m,allowedPorts:[...m.allowedPorts??[],...h]}:m)),l.push(...ws({ddos:r.ddos,waf:r.waf},[...r.firewall?.allowedPorts??[],...h])),l.push(...Ee(r.autoUpdates??n)),l.push(...go(r.appUpdates)),l.push(...Pe(r.monitoring??n)),l.push(...Qe(r.sshKeys,{path:t?.authorizedKeysPath})),r.backups?.enabled)l.push(...ko({database:u,backups:r.backups}));return{runtime:r.runtime||"bun",runtimeVersion:r.runtimeVersion||"latest",phpBox:n,phpProvision:c,servicesProvision:l.length>0?l:void 0}}function ye(e={}){let{runtime:t="bun",runtimeVersion:r="latest",systemPackages:n=[],database:s,phpProvision:i,servicesProvision:o,caddyfile:a,rpxProvision:c,baked:l=!1,swapGb:u=2}=e,d=new Set(n);if(s==="sqlite")d.add("sqlite3");else if(s==="mysql")d.add("mysql-client");else if(s==="postgres")d.add("postgresql-client");let m=`#!/bin/bash
|
|
145
145
|
set -euo pipefail
|
|
146
146
|
`,h=Math.floor(u);if(h>0)m+=`
|
|
147
147
|
# Swap: a swapless box under memory pressure lets the kernel OOM killer choose
|
|
@@ -251,8 +251,8 @@ ${f.join(`
|
|
|
251
251
|
`)}
|
|
252
252
|
`}return m+=`
|
|
253
253
|
echo "ts-cloud bootstrap complete — instance is ready for site deploys"
|
|
254
|
-
`,m}var Es="/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id";function Ds(e,t){let r=[{port:22,protocol:"tcp",cidr:"0.0.0.0/0"},{port:80,protocol:"tcp",cidr:"0.0.0.0/0"},{port:443,protocol:"tcp",cidr:"0.0.0.0/0"}],n=new Set;for(let s of Object.values(e.sites||{}))if(s&&typeof s.port==="number"&&![22,80,443].includes(s.port))n.add(s.port);for(let s of jr(ne(e,{environment:t})))n.add(s);for(let s of n)r.push({port:s,protocol:"tcp",cidr:"0.0.0.0/0"});return r}function Is(e,t){let r={...e.infrastructure?.compute,proxy:le(e,{environment:t})},n=
|
|
255
|
-
`),"TS_CLOUD_BASH_EOF"]}async runRemoteDeploy(e){let t=this.region,r=new Ze(t);if(e.targets.length>0){let s=await r.sendCommand({InstanceIds:e.targets.map((i)=>i.id),DocumentName:"AWS-RunShellScript",Parameters:{commands:this.bashWrap(e.commands)},TimeoutSeconds:e.timeoutSeconds||600,Comment:e.comment});if(!s.CommandId)return{success:!1,instanceCount:0,perInstance:[],error:"Failed to send SSM command"};return this.pollSsmCommand(r,s.CommandId,e.targets.length,((e.timeoutSeconds||600)+30)*1000)}if(!e.tags||Object.keys(e.tags).length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets or tags provided for AWS deploy"};let n=await r.sendCommandByTags({tags:e.tags,commands:this.bashWrap(e.commands),timeoutSeconds:e.timeoutSeconds||600,comment:e.comment});return{success:n.success,instanceCount:n.instanceCount,perInstance:n.perInstance.map((s)=>({instanceId:s.instanceId,status:s.status,output:s.output,error:s.error})),error:n.error}}async pollSsmCommand(e,t,r,n=600000){let i=Date.now(),o=new Set(["Success","Failed","Cancelled","TimedOut"]),a=[];while(Date.now()-i<n){await new Promise((u)=>setTimeout(u,3000));try{if(a=await e.listCommandInvocations({CommandId:t,Details:!0}),a.length>=r&&a.every((d)=>o.has(d.Status||"")))break}catch{}}let c=a.map((u)=>({instanceId:u.InstanceId,status:u.Status||"Unknown",output:u.StandardOutputContent,error:u.StandardErrorContent})),l=c.length>0&&c.every((u)=>u.status==="Success");return{success:l,instanceCount:c.length,perInstance:c,error:l?void 0:
|
|
254
|
+
`,m}var Es="/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id";function Ds(e,t){let r=[{port:22,protocol:"tcp",cidr:"0.0.0.0/0"},{port:80,protocol:"tcp",cidr:"0.0.0.0/0"},{port:443,protocol:"tcp",cidr:"0.0.0.0/0"}],n=new Set;for(let s of Object.values(e.sites||{}))if(s&&typeof s.port==="number"&&![22,80,443].includes(s.port))n.add(s.port);for(let s of jr(ne(e,{environment:t})))n.add(s);for(let s of n)r.push({port:s,protocol:"tcp",cidr:"0.0.0.0/0"});return r}function Is(e,t){let r={...e.infrastructure?.compute,proxy:le(e,{environment:t})},n=Fe(e,{environment:t}),s=r.proxy?.engine==="rpx"?$e({proxy:r.proxy,config:Te(e.sites??{},{proxy:r.proxy,slug:e.project.slug,mail:ne(e,{environment:t})}),slug:e.project.slug,bunBin:r.runtime==="node"||r.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0;return ye({runtime:n.runtime,runtimeVersion:n.runtimeVersion,systemPackages:r.systemPackages,database:e.infrastructure?.database,phpProvision:n.phpProvision,servicesProvision:n.servicesProvision,rpxProvision:s,baked:r.bakedImage===!0,swapGb:r.swapGb})}function As(e){return Buffer.from(e,"utf8").toString("base64")}function Ls(e){return e.infrastructure?.compute?.image??null}function Vt(e,t){return`${e}-${t}-app-sg`}function Wr(e,t,r="app"){return[{Key:"Project",Value:e},{Key:"Environment",Value:t},{Key:"Role",Value:r},{Key:"ManagedBy",Value:"ts-cloud"}]}function Pc(e,t,r){if(!e?.length)return!1;let n=(s)=>e.find((i)=>i.Key===s)?.Value;return n("Project")===t&&n("Environment")===r}function Fs(e,t){let r=Vt(t.slug,t.environment);return e.find((n)=>{if(n.GroupName!==r)return!1;if(t.vpcId&&n.VpcId&&n.VpcId!==t.vpcId)return!1;if(!!n.Tags?.some((i)=>i.Key==="Project"))return Pc(n.Tags,t.slug,t.environment);return!!t.vpcId&&n.VpcId===t.vpcId})}function Ec(e){try{let t=Bs(Rc(process.cwd(),"storage/cloud/state",`${e}.json`),"utf8"),r=JSON.parse(t);return typeof r.instanceId==="string"&&r.instanceId.length>0?r.instanceId:null}catch{return null}}class qt{name="aws";usesCloudFormation=!0;region;ec2Client;cfnClient;constructor(e={}){this.region=e.region||"us-east-1",this.ec2Client=e.ec2Client??((t)=>new Q(t)),this.cfnClient=e.cfnClient??((t)=>new v(t))}resolveRegion(e){return e.project.region||this.region}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=this.resolveRegion(t),s=t.project.slug,i=t.infrastructure?.compute;if(!i)throw Error("infrastructure.compute is required to provision AWS compute");let o=this.ec2Client(n),a=await this.findComputeTargets({slug:s,environment:r,role:"app",region:n});if(a.length>0){let H=a[0];return{appInstanceId:H.id,appPublicIp:H.publicIp,appPublicIpv6:H.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}let c=Ls(t);if(!c){if(c=(await new Ze(n).getParameter({Name:Es})).Parameter?.Value??null,!c)throw Error("Could not resolve the Ubuntu 24.04 AMI from SSM")}let l=await o.describeVpcs(),u=(l.Vpcs||[]).find((H)=>H.IsDefault)||(l.Vpcs||[])[0];if(!u?.VpcId)throw Error("No VPC found to launch the instance into");let d=(await o.describeSubnets({Filters:[{Name:"vpc-id",Values:[u.VpcId]}]})).Subnets||[],m=d.find((H)=>H.MapPublicIpOnLaunch)||d[0],h=m?.SubnetId;if(!m?.MapPublicIpOnLaunch)console.warn("ts-cloud: no public subnet found; the instance may not get a public IP (deploys/SSL need one).");let f=Vt(s,r),x=(await o.describeSecurityGroups({Filters:[{Name:"group-name",Values:[f]},{Name:"vpc-id",Values:[u.VpcId]}]})).SecurityGroups?.[0]?.GroupId;if(!x)x=(await o.createSecurityGroup({GroupName:f,Description:`ts-cloud ${s}/${r} app`,VpcId:u.VpcId,TagSpecifications:[{ResourceType:"security-group",Tags:Wr(s,r)}]})).GroupId;let B=Ds(t,r);await o.authorizeSecurityGroupIngress({GroupId:x,IpPermissions:B.map((H)=>({IpProtocol:H.protocol,FromPort:H.port,ToPort:H.port,IpRanges:[{CidrIp:H.cidr}]}))}).catch((H)=>{if(!/InvalidPermission\.Duplicate/.test(H instanceof Error?H.message:""))throw H});let L=As(Is(t,r)),N=i.server?.instanceType||"t3.small",E=(await o.runInstances({ImageId:c,InstanceType:N,MinCount:1,MaxCount:1,SecurityGroupIds:x?[x]:void 0,SubnetId:h,UserData:L,IamInstanceProfile:i.server?.iamInstanceProfile?{Name:i.server.iamInstanceProfile}:void 0,TagSpecifications:[{ResourceType:"instance",Tags:[{Key:"Name",Value:`${s}-${r}-app`},...Wr(s,r)]}]})).Instances?.[0]?.InstanceId;if(!E)throw Error("RunInstances did not return an instance id");let M=await o.waitForInstanceState(E,"running");if(!M)throw Error(`Instance ${E} did not reach 'running' before timeout`);return{appInstanceId:E,appPublicIp:M.PublicIpAddress,appPublicIpv6:me(M.Ipv6Address),sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=this.resolveRegion(t),i=this.ec2Client(s),o=[],c=(await this.findComputeTargets({slug:n,environment:r,role:"app",region:s})).map((f)=>f.id),l;if(c.length>0)l=(await i.describeInstances({InstanceIds:c}).catch(()=>({Reservations:[]}))).Reservations?.flatMap((S)=>S.Instances??[]).find((S)=>S.VpcId)?.VpcId;if(c.length>0)await i.terminateInstances(c),o.push(...c.map((f)=>`instance ${f}`)),await Promise.all(c.map((f)=>i.waitForInstanceState(f,"terminated").catch(()=>{return})));let u=Vt(n,r),d=[{Name:"group-name",Values:[u]}];if(l)d.push({Name:"vpc-id",Values:[l]});let m=await i.describeSecurityGroups({Filters:d}).catch(()=>({SecurityGroups:[]})),h=Fs(m.SecurityGroups??[],{slug:n,environment:r,vpcId:l});if(h?.GroupId)for(let f=0;f<10;f++)try{await i.deleteSecurityGroup(h.GroupId),o.push(`security group ${u}`);break}catch{await new Promise((S)=>setTimeout(S,3000))}return{destroyed:o}}async getComputeOutputs(e){let t=this.resolveRegion(e.config),r=T(e.config,e.environment),n=this.cfnClient(t);try{let s=await n.getStackOutputs(r);return{deployBucketName:s.deployBucketName,appInstanceId:s.appInstanceId,appPublicIp:s.appPublicIp,sshUser:"ec2-user"}}catch(s){if(!/does not exist|ValidationError/i.test(s instanceof Error?s.message:""))throw s;let o=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",region:t}))[0];return{appInstanceId:o?.id,appPublicIp:o?.publicIp,appPublicIpv6:o?.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}}async uploadRelease(e){let t=this.resolveRegion(e.config),n=(await this.getComputeOutputs({config:e.config,environment:e.environment})).deployBucketName;if(!n)throw Error("No deployBucketName in stack outputs. Re-deploy infrastructure to add the staging bucket.");return await new g(t).putObject({bucket:n,key:e.remoteKey,body:Bs(e.localPath),contentType:"application/gzip"}),{artifactRef:`s3://${n}/${e.remoteKey}`}}async findComputeTargets(e){let t=e.region??this.region,r=this.ec2Client(t),n=[{Name:"tag:Project",Values:[e.slug]},{Name:"tag:Environment",Values:[e.environment]},{Name:"tag:Role",Values:[e.role||"app"]},{Name:"instance-state-name",Values:["running","pending"]}],s=await r.describeInstances({Filters:n}),i=this.reservationsToTargets(s.Reservations);if(i.length>0||(e.role||"app")!=="app")return i;let o=e.stackName??`${e.slug}-${e.environment}`,a=Ec(o);if(!a)try{a=(await this.cfnClient(t).getStackOutputs(o)).appInstanceId??null}catch{a=null}if(!a)return[];try{let c=await r.describeInstances({InstanceIds:[a],Filters:[{Name:"instance-state-name",Values:["running","pending"]}]});return this.reservationsToTargets(c.Reservations)}catch{return[]}}reservationsToTargets(e){let t=[];for(let r of e||[])for(let n of r.Instances||[]){if(!n.InstanceId)continue;let s=n.Tags?.find((i)=>i.Key==="Name")?.Value;t.push({id:n.InstanceId,name:s,publicIp:n.PublicIpAddress,publicIpv6:me(n.Ipv6Address),privateIp:n.PrivateIpAddress,status:n.State?.Name})}return t}bashWrap(e){return["bash <<'TS_CLOUD_BASH_EOF'",e.join(`
|
|
255
|
+
`),"TS_CLOUD_BASH_EOF"]}async runRemoteDeploy(e){let t=this.region,r=new Ze(t);if(e.targets.length>0){let s=await r.sendCommand({InstanceIds:e.targets.map((i)=>i.id),DocumentName:"AWS-RunShellScript",Parameters:{commands:this.bashWrap(e.commands)},TimeoutSeconds:e.timeoutSeconds||600,Comment:e.comment});if(!s.CommandId)return{success:!1,instanceCount:0,perInstance:[],error:"Failed to send SSM command"};return this.pollSsmCommand(r,s.CommandId,e.targets.length,((e.timeoutSeconds||600)+30)*1000)}if(!e.tags||Object.keys(e.tags).length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets or tags provided for AWS deploy"};let n=await r.sendCommandByTags({tags:e.tags,commands:this.bashWrap(e.commands),timeoutSeconds:e.timeoutSeconds||600,comment:e.comment});return{success:n.success,instanceCount:n.instanceCount,perInstance:n.perInstance.map((s)=>({instanceId:s.instanceId,status:s.status,output:s.output,error:s.error})),error:n.error}}async pollSsmCommand(e,t,r,n=600000){let i=Date.now(),o=new Set(["Success","Failed","Cancelled","TimedOut"]),a=[];while(Date.now()-i<n){await new Promise((u)=>setTimeout(u,3000));try{if(a=await e.listCommandInvocations({CommandId:t,Details:!0}),a.length>=r&&a.every((d)=>o.has(d.Status||"")))break}catch{}}let c=a.map((u)=>({instanceId:u.InstanceId,status:u.Status||"Unknown",output:u.StandardOutputContent,error:u.StandardErrorContent})),l=c.length>0&&c.every((u)=>u.status==="Success");return{success:l,instanceCount:c.length,perInstance:c,error:l?void 0:Me(c,"One or more SSM command invocations failed")}}}import{execFileSync as ml,execSync as vi}from"node:child_process";import{createHash as fl,randomUUID as bi}from"node:crypto";import{createReadStream as gl,existsSync as hl,readFileSync as yl}from"node:fs";async function js(e,t){let r=await import("node:fs"),n=await import("node:path"),s=await import("node:child_process"),i="/etc/ts-cloud/compute-storage.json",o="/var/lib/ts-cloud/compute-storage-state.json",a="/run/ts-cloud-root-web",c=t[0]??"status";if(!["plan","status","check","apply","ensure-site","reclaim"].includes(c))throw Error(`Unknown compute storage command: ${c}`);let l=(y,k)=>{try{return JSON.parse(r.readFileSync(y,"utf8"))}catch(b){if(b.code==="ENOENT")return k;throw b}},u=(y,k)=>{r.mkdirSync(n.dirname(y),{recursive:!0});let b=`${y}.${process.pid}.tmp`;r.writeFileSync(b,`${JSON.stringify(k,null,2)}
|
|
256
256
|
`,{mode:420}),r.renameSync(b,y)},d=(y,k)=>s.execFileSync(y,k,{encoding:"utf8",maxBuffer:16777216}),m=(y,k)=>{let b=s.spawnSync(y,k,{encoding:"utf8"});return b.status===0?b.stdout.trim():""},h=(y)=>!!m("findmnt",["-n","--mountpoint",y]),f=(y,k)=>{try{let b=r.statSync(y),C=r.statSync(k);return b.dev===C.dev&&b.ino===C.ino}catch{return!1}},S=l("/var/lib/ts-cloud/compute-storage-state.json",{entries:[]}),x=e.deploymentStorage,B=e.volumes.find((y)=>y.key===x?.volume),L=(y,k)=>{if(!B)return[];let b=[];for(let C of r.readdirSync(e.webRoot,{withFileTypes:!0})){if(!C.isDirectory()||C.isSymbolicLink())continue;let P=n.join(e.webRoot,C.name);if(y&&P!==y&&P!==n.join(e.webRoot,`${k}-shared`))continue;for(let G of["releases",...x?.includeBackups===!1?[]:["backups"]]){let K=n.join(P,G);if(!r.existsSync(K)||!r.lstatSync(K).isDirectory())continue;b.push({source:K,destination:n.join(B.mountPath,G,C.name)})}}return b},N=(y)=>{try{return h(y.mountPath)&&r.statSync(y.mountPath).dev===r.statSync(y.device).rdev}catch{return!1}},D=(y)=>{let k=r.statfsSync(y),b=(k.blocks-k.bfree)*k.bsize,C=k.bavail*k.bsize;return{path:y,sizeBytes:k.blocks*k.bsize,usedBytes:b,availableBytes:C,usedPercent:Math.ceil(100*b/(b+C))}},E=()=>{let y=[],k=[D("/")];for(let C of e.volumes)if(!N(C))y.push(`missing or incorrect volume mount: ${C.mountPath}`);else k.push(D(C.mountPath));for(let C of S.entries)if(!h(C.source)||!f(C.source,C.destination))y.push(`missing deployment mount: ${C.source}`);let b=new Set(S.entries.map((C)=>C.source));for(let C of L())if(!b.has(C.source))y.push(`deployment needs volume placement: ${C.source}`);return{timestamp:new Date().toISOString(),volumes:e.volumes.map((C)=>({key:C.key,name:C.name,id:C.id,size:C.size,mountPath:C.mountPath})),disks:k,mounts:S.entries.length,problems:y}};if(c==="plan")return console.log(JSON.stringify({volumes:e.volumes,entries:L(),state:S},null,2));if(c==="status"||c==="check"){let y=E();if(c==="check"){u("/var/lib/ts-cloud/compute-storage-status.json",y);let k="/var/lib/ts-cloud/compute-storage-alert.json",b=l(k,{problems:[]});if(JSON.stringify(b.problems)!==JSON.stringify(y.problems)){let C=y.problems.join("; ")||"all configured mounts are healthy",P=`${y.problems.length?"Storage alert":"Storage recovered"} on ${m("hostname",[])}: ${C}`;d("/usr/local/bin/ts-cloud-notify",[P.length>1800?`${P.slice(0,1600)}... Full report: /var/lib/ts-cloud/compute-storage-status.json`:P])}u(k,{problems:y.problems,timestamp:y.timestamp})}if(console.log(JSON.stringify(y)),y.problems.length)process.exitCode=1;return y}if(process.getuid?.()!==0)throw Error("Compute storage changes require root");let M=()=>{let C=r.readFileSync("/etc/fstab","utf8");if(!r.existsSync("/etc/fstab.before-ts-cloud-storage"))r.copyFileSync("/etc/fstab","/etc/fstab.before-ts-cloud-storage");let P=[];for(let j of e.volumes){let V=d("blkid",["-s","UUID","-o","value",j.device]).trim();if(!V)throw Error(`Volume ${j.name} has no filesystem UUID`);P.push(`UUID=${V} ${j.mountPath} ext4 defaults,noatime,nodev,nosuid,nofail,x-systemd.device-timeout=30s 0 2`)}for(let j of S.entries)P.push(`${j.destination} ${j.source} none bind,nofail,x-systemd.requires-mounts-for=${B.mountPath} 0 0`);let G=`# BEGIN ts-cloud compute storage
|
|
257
257
|
${P.join(`
|
|
258
258
|
`)}
|
|
@@ -268,13 +268,13 @@ RequiresMountsFor=${y.join(" ")}
|
|
|
268
268
|
`).at(-1)?.split(/\s+/)[0],F=r.statfsSync(B.mountPath);if(Number(_)>F.bavail*F.bsize)throw Error("Insufficient free volume capacity for a verified deployment copy")}if(!r.existsSync("/run/ts-cloud-root-web"))r.mkdirSync("/run/ts-cloud-root-web");if(!h("/run/ts-cloud-root-web"))d("mount",["--bind",e.webRoot,"/run/ts-cloud-root-web"]);d("mount",["--make-private","/run/ts-cloud-root-web"]);let C=r.statSync(e.webRoot).dev;for(let _ of S.entries){let F=n.join("/run/ts-cloud-root-web",n.relative(e.webRoot,_.source));if(r.existsSync(F)&&(r.statSync(F).dev!==C||f(F,_.destination)))throw Error(`Root recovery view contains a deployment mount: ${F}`)}for(let _=0;_<b.length;_++){if(h(b[_].source)&&f(b[_].source,b[_].destination))continue;console.log(`copy ${_+1}/${b.length}: ${b[_].source}`),await Y(b[_],!1)}if(c!=="reclaim")for(let _ of y)te(_);for(let _ of r.readdirSync(e.webRoot,{withFileTypes:!0}).filter((F)=>F.isDirectory()).sort((F,ue)=>F.name.localeCompare(ue.name)))te(n.join(e.webRoot,_.name,".ts-cloud/deploy.lock"));for(let _ of c==="reclaim"?[]:L(c==="ensure-site"?t[1]:void 0,t[2]))if(!S.entries.some((F)=>F.source===_.source)&&!b.some((F)=>F.source===_.source))b.push(_);if(b.length)console.log("Checking application services and HTTPS routes before activation");let P=b.length?await ie():void 0;if(P)u("/var/lib/ts-cloud/compute-storage-health-before.json",P);let G=Z().filter((_)=>_.write);if(G.length)throw Error(`Live writable files must be shared before migration: ${G.map((_)=>_.file).join(", ")}`);let K={entries:[...S.entries],fstab:r.readFileSync("/etc/fstab","utf8")};if(b.length)d("sync",[]);let j=[];try{for(let _=0;_<b.length;_++){let F=b[_];if(h(F.source)){if(!f(F.source,F.destination))throw Error(`Unrecorded mount occupies ${F.source}`);S.entries.push(F),u("/var/lib/ts-cloud/compute-storage-state.json",S);continue}if(console.log(`verify and mount ${_+1}/${b.length}: ${F.source}`),await Y(F,!0),d("mount",["--bind",F.destination,F.source]),!f(F.source,F.destination))throw Error(`Bind verification failed: ${F.source}`);j.push(F),S.entries.push(F),u("/var/lib/ts-cloud/compute-storage-state.json",S),M()}if(M(),d("findmnt",["--verify"]),S.activatedAt=new Date().toISOString(),u("/var/lib/ts-cloud/compute-storage-state.json",S),ce(),P){console.log("Checking application services and HTTPS routes after activation");let _=await ie();u("/var/lib/ts-cloud/compute-storage-health-after.json",_);let F=P.routes.filter((re)=>re.ok&&!_.routes.some((fe)=>fe.domain===re.domain&&fe.path===re.path&&fe.ok)),ue=Object.keys(P.services).filter((re)=>!_.services[re]);if(F.length||ue.length)throw Error(`Application verification failed: ${F.map((re)=>re.domain+re.path).concat(ue).join(", ")}`);console.log(`Verified ${_.routes.length} routes and ${Object.keys(_.services).length} services`)}}catch(_){let F=[];for(let ue of j.reverse())try{d("umount",[ue.source])}catch{F.push(ue),console.error(`Rollback mount busy, original data retained: ${ue.source}`)}if(S.entries=[...K.entries,...F],r.writeFileSync("/etc/fstab",K.fstab),F.length)M();throw u("/var/lib/ts-cloud/compute-storage-state.json",S),d("systemctl",["daemon-reload"]),_}let V=Z().filter((_)=>_.device===C).map((_)=>_.file);for(let _ of S.entries){if(!h(_.source)||!f(_.source,_.destination))throw Error(`Lost deployment mount: ${_.source}`);let F=n.join("/run/ts-cloud-root-web",n.relative(e.webRoot,_.source));if(!r.existsSync(F))continue;if(r.statSync(F).dev!==C||f(F,_.destination))throw Error(`Refusing to clean a deployment volume through the root recovery view: ${F}`);let ue="";try{ue=n.basename(r.realpathSync(n.join(n.dirname(_.source),"current")))}catch{}for(let re of r.readdirSync(F,{withFileTypes:!0})){let fe=n.join(F,re.name);if(n.basename(_.source)==="backups"){if(re.isFile()&&r.existsSync(n.join(_.destination,re.name)))r.unlinkSync(fe)}else if(re.isDirectory()&&!re.isSymbolicLink()&&re.name!==ue&&!V.some((_t)=>_t===n.join(_.source,re.name)||_t.startsWith(`${n.join(_.source,re.name)}/`)))r.rmSync(fe,{recursive:!0})}}d("sync",[]);let se=E();return u("/var/lib/ts-cloud/compute-storage-status.json",se),console.log(JSON.stringify(se)),se}finally{for(let y of J.reverse())X.symbols.flock(y,8),r.closeSync(y);X.close()}}var qr="/etc/ts-cloud/compute-storage.json",De="/usr/local/lib/ts-cloud/compute-storage.mjs";function Xr(e,t){let r=Object.entries(e.infrastructure?.compute?.volumes??{}).map(([s,i])=>{if(!/^[a-z][a-z0-9-]*$/.test(s))throw Error(`Invalid compute volume key: ${s}`);if(!Number.isInteger(i.size)||i.size<10||i.size>10240)throw Error(`compute.volumes.${s}.size must be a whole number from 10 to 10240 GB`);if(!i.mountPath.startsWith("/")||!i.mountPath.slice(1).split("/").every((a)=>/^[\w-]+$/.test(a))||i.mountPath==="/var"||i.mountPath==="/var/lib"||["/etc","/usr","/var/www","/root","/boot","/dev","/proc","/sys","/run","/bin","/sbin","/lib"].some((a)=>i.mountPath===a||i.mountPath.startsWith(`${a}/`)))throw Error(`Unsafe compute volume mount path: ${i.mountPath}`);let o=i.name??`${e.project.slug}-${t}-${s}`;if(!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(o))throw Error(`Invalid compute volume name: ${o}`);return{...i,key:s,name:o,mountPath:i.mountPath.replace(/\/+$/,"")}});for(let s of r)if(r.some((i)=>i!==s&&(i.name===s.name||i.mountPath===s.mountPath||i.mountPath.startsWith(`${s.mountPath}/`))))throw Error("Compute volumes must have distinct names and non-overlapping mount paths");let n=e.infrastructure?.compute?.deploymentStorage;for(let s of n?.locks??[])if(!s.startsWith("/run/lock/")||!s.slice(10).split("/").every((i)=>/^[\w.-]+$/.test(i)&&i!=="."&&i!=="..")||s==="/run/lock/ts-cloud-compute-storage.lock")throw Error(`Invalid deployment storage lock: ${s}`);for(let s of n?.dependentUnits??[])if(!/^[\w@.-]+\.service$/.test(s))throw Error(`Invalid deployment storage dependent unit: ${s}`);if(n&&!r.some((s)=>s.key===n.volume))throw Error(`deploymentStorage.volume references an undeclared compute volume: ${n.volume}`);return r}function Xt(e){return`'${e.replaceAll("'",`'"'"'`)}'`}function Yr(e){let t=`${qr}.candidate`,r=`#!/usr/bin/env bun
|
|
269
269
|
await (${js.toString()})(JSON.parse(await Bun.file(process.env.TS_CLOUD_STORAGE_POLICY || ${JSON.stringify(qr)}).text()), process.argv.slice(2));
|
|
270
270
|
`;return['command -v bun >/dev/null 2>&1 || { echo "Configured compute storage requires Bun on the host" >&2; exit 1; }',"install -d -m 0755 /etc/ts-cloud /usr/local/lib/ts-cloud /var/lib/ts-cloud",`printf %s ${Xt(Buffer.from(r).toString("base64"))} | base64 -d > ${De}.new`,`chmod 0755 ${De}.new && mv ${De}.new ${De}`,`printf %s ${Xt(JSON.stringify(e))} > ${t}`,`chmod 0644 ${t}`,`TS_CLOUD_STORAGE_POLICY=${t} bun ${De} apply 2>&1 | tee /var/log/ts-cloud-compute-storage.log`,`rm -f ${t}`,"cat > /etc/systemd/system/ts-cloud-storage-check.service <<'TS_CLOUD_STORAGE_SERVICE'","[Unit]","Description=Check configured compute storage and deployment mounts","[Service]","Type=oneshot",`ExecStart=/usr/local/bin/bun ${De} check`,"TS_CLOUD_STORAGE_SERVICE","cat > /etc/systemd/system/ts-cloud-storage-check.timer <<'TS_CLOUD_STORAGE_TIMER'","[Unit]","Description=Check compute storage every minute","[Timer]","OnBootSec=2min","OnUnitActiveSec=1min","[Install]","WantedBy=timers.target","TS_CLOUD_STORAGE_TIMER","systemctl daemon-reload","systemctl enable --now ts-cloud-storage-check.timer","cat > /etc/systemd/system/ts-cloud-storage-reclaim.service <<'TS_CLOUD_RECLAIM_SERVICE'","[Unit]","Description=Reclaim inactive root copies of volume-backed deployments","[Service]","Type=oneshot",`ExecStart=/usr/local/bin/bun ${De} reclaim`,"Nice=19","IOSchedulingClass=idle","TS_CLOUD_RECLAIM_SERVICE","cat > /etc/systemd/system/ts-cloud-storage-reclaim.timer <<'TS_CLOUD_RECLAIM_TIMER'","[Unit]","Description=Reclaim retired deployment copies hourly","[Timer]","OnCalendar=hourly","RandomizedDelaySec=5min","Persistent=true","[Install]","WantedBy=timers.target","TS_CLOUD_RECLAIM_TIMER","systemctl daemon-reload","systemctl enable --now ts-cloud-storage-reclaim.timer"]}function Yt(e,t){return[`if [ -f ${qr} ]; then bun ${De} ensure-site ${Xt(e)} ${Xt(t)}; fi`]}import{createHash as xc}from"node:crypto";function st(e){if(!/^[a-z0-9][a-z0-9-]*$/.test(e))throw Error("Runtime isolation requires a safe project slug");return`tsc-${e.length<=24?e:`${e.slice(0,15)}-${xc("sha256").update(e).digest("hex").slice(0,8)}`}`}function xt(e,t,r){return[`User=${e}`,`Group=${e}`,"NoNewPrivileges=true","PrivateTmp=true","ProtectHome=true","ProtectProc=invisible","InaccessiblePaths=-/run/postgresql -/var/run/postgresql","TemporaryFileSystem=/var/www:ro",`BindPaths=${t} -/var/www/${r}-shared`,"UMask=0077",`Environment=HOME=/var/lib/ts-cloud/${e}`]}function Xs(e,t,r){let n=st(e);if(!t.startsWith(`/var/www/${e}-`)||t.slice(9).includes("/"))throw Error("Runtime isolation requires a project-namespaced application directory");let s=`/var/lib/ts-cloud/${n}`,i=`/var/www/${e}-shared`,o=[...new Set([t,...r])];for(let a of o)if(!/^\/[a-zA-Z0-9_./-]+$/.test(a)||a.split("/").includes("..")||!(a===t||a.startsWith(`${t}/`)||a===i||a.startsWith(`${i}/`)))throw Error(`Runtime isolation cannot take ownership outside project ${e}`);return[`if ! id -u ${n} >/dev/null 2>&1; then useradd --system --user-group --create-home --home-dir ${s} --shell /usr/sbin/nologin ${n}; fi`,`[ "$(id -u ${n})" != 0 ] && [ "$(getent passwd ${n} | cut -d: -f6)" = ${s} ] || { echo 'Unexpected runtime isolation identity' >&2; exit 1; }`,`install -d -m 700 -o ${n} -g ${n} ${s}`,...o.map((a)=>`chown -hR ${n}:${n} ${a}`),`chmod 750 ${t}`]}function Jt(e){return Object.entries(e).map(([t,r])=>`${t}=${Oc(String(r))}`).join(`
|
|
271
|
-
`)}function Oc(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t")}"`}var Dc=new Set(["sqlite","sqlite3"]),Ic=new Set([":memory:","memory"]);function Ac(e){let t=e?.DB_CONNECTION?.trim().toLowerCase();return t!=null&&Dc.has(t)}function Lc(e){let t=e.trim().replace(/^\.\//,"");if(t===""||t.startsWith("/")||t.startsWith("~"))return;if(t.split("/").includes(".."))return;return t.replace(/\/+$/,"")}function Jr(e,t=[]){if(!Ac(e))return{};let r=e?.DB_DATABASE_PATH?.trim()||e?.DB_DATABASE?.trim();if(r&&Ic.has(r.toLowerCase()))return{};if(!r)return{warning:"DB_CONNECTION is sqlite but neither DB_DATABASE_PATH nor DB_DATABASE names a file, so ts-cloud cannot tell where the database lives. If it is written inside the release directory, the next deploy discards it. Set DB_DATABASE_PATH, or list the file in the site's `sharedPaths`."};let n=Lc(r);if(!n)return{};if(t.some((s)=>Vs(s)===n))return{};return{path:n}}function Zt(e,t=[]){let{path:r}=Jr(e,t);return r?[r]:[]}var ot="/var/ts-cloud/artifacts";var ni=Object.freeze({stagingMaxAgeMinutes:60,artifactMaxAgeDays:2,artifactUploadMaxAgeMinutes:60,bunCacheMaxAgeDays:7,journalMaxAgeDays:14,journalMaxSizeMb:512,containerImageMaxAgeHours:168}),si=Object.freeze({artifactMaxAgeDays:0,bunCacheMaxAgeDays:1,journalMaxAgeDays:3,journalMaxSizeMb:256,containerImageMaxAgeHours:24});function Js(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Fe(e,t,r=0){if(!Number.isInteger(t)||t<r)throw Error(`hostCleanup: ${e} must be a whole number >= ${r} (got ${t})`)}function Zs(e,t){for(let[r,n]of Object.entries(t))Fe(`${e}.${r}`,n);Fe(`${e}.stagingMaxAgeMinutes`,t.stagingMaxAgeMinutes,15),Fe(`${e}.artifactUploadMaxAgeMinutes`,t.artifactUploadMaxAgeMinutes,15),Fe(`${e}.journalMaxSizeMb`,t.journalMaxSizeMb,1)}function kc(e,t){let r=`hostCleanup.paths[${t}]`,n=e.path?.replace(/\/+$/,"")??"";if(!n.startsWith("/")||n===""||/[\s'"`$\\]/.test(n)||n.split("/").includes(".."))throw Error(`${r}.path must be an absolute directory with no spaces, quotes or '..' (got ${JSON.stringify(e.path)})`);if(n==="/var/www"||n.startsWith("/var/www/"))throw Error(`${r}.path must not be under /var/www: releases are pruned by keepReleases`);if(!e.pattern||/^[*?]+$/.test(e.pattern)||e.pattern.includes("/")||/[\n'"`]/.test(e.pattern))throw Error(`${r}.pattern must be a file-name glob naming something, not a bare wildcard (got ${JSON.stringify(e.pattern)})`);if(Fe(`${r}.maxAgeDays`,e.maxAgeDays),e.type!==void 0&&e.type!=="file"&&e.type!=="directory")throw Error(`${r}.type must be 'file' or 'directory'`)}function Qt(e){let t=typeof e==="object"&&e!==null?e:{},r=e!==!1,n={...ni,...Qs(t.retention)};Zs("hostCleanup.retention",n);let s=null;if(t.pressure!==!1){let a=typeof t.pressure==="object"?t.pressure:{},c=a.lowWaterPercent??50,l=a.highWaterPercent??85;if(Fe("hostCleanup.pressure.lowWaterPercent",c),Fe("hostCleanup.pressure.highWaterPercent",l,1),l>100||c>=l)throw Error(`hostCleanup.pressure: need 0 <= lowWaterPercent < highWaterPercent <= 100 (got ${c} and ${l})`);let u={...si,...Qs(a.escalated)},d=Object.fromEntries(Object.keys(n).map((m)=>[m,Math.min(n[m],u[m]??n[m])]));Zs("hostCleanup.pressure.escalated",d),s={lowWaterPercent:c,highWaterPercent:l,escalated:d}}let i=t.schedule??"daily";if(!/^[\w*:,./~ -]+$/.test(i))throw Error(`hostCleanup.schedule must be a systemd OnCalendar expression (got ${JSON.stringify(i)})`);let o=t.paths??[];return o.forEach(kc),{enabled:r,timer:r&&t.timer!==!1,schedule:i,retention:n,pressure:s,paths:o}}function Qs(e){return Object.fromEntries(Object.entries(e??{}).filter(([,t])=>t!==void 0))}var Ot={stagingMaxAgeMinutes:"TS_CLOUD_HC_STAGING_MIN",artifactMaxAgeDays:"TS_CLOUD_HC_ARTIFACT_DAYS",artifactUploadMaxAgeMinutes:"TS_CLOUD_HC_UPLOAD_MIN",bunCacheMaxAgeDays:"TS_CLOUD_HC_BUN_DAYS",journalMaxAgeDays:"TS_CLOUD_HC_JOURNAL_DAYS",journalMaxSizeMb:"TS_CLOUD_HC_JOURNAL_MB",containerImageMaxAgeHours:"TS_CLOUD_HC_IMAGE_HOURS"};function Zr(e){return Object.keys(Ot).map((t)=>`${Ot[t]}=${e[t]}`).join(" ")}var Ie=(e)=>`"$${Ot[e]}"`;function ei(e){return[`find /var/ts-cloud/staging -xdev -maxdepth 1 -type f -mmin +${Ie("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /tmp -xdev -maxdepth 1 -type f -name "*-release.tar.gz" -mmin +${Ie("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name "*.tar.gz" -mtime +${Ie("artifactMaxAgeDays")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name ".*.tmp" -mmin +${Ie("artifactUploadMaxAgeMinutes")} -delete 2>/dev/null || true`,...e.map(Nc),`journalctl --vacuum-time=${Ie("journalMaxAgeDays")}d --vacuum-size=${Ie("journalMaxSizeMb")}M >/dev/null 2>&1 || true`]}function Nc(e){let t=Js(e.path.replace(/\/+$/,"")),r=Js(e.pattern);return e.type==="directory"?`find ${t} -xdev -mindepth 1 -maxdepth 1 -type d -name ${r} -mtime +${e.maxAgeDays} -exec rm -rf -- {} + 2>/dev/null || true`:`find ${t} -xdev -mindepth 1 -maxdepth 1 -type f -name ${r} -mtime +${e.maxAgeDays} -delete 2>/dev/null || true`}function ti(){return[`find /root/.bun/install/cache -xdev -type f -mtime +${Ie("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`find /root/.bun/install/cache -xdev -depth -type d -empty -mtime +${Ie("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`if command -v docker >/dev/null 2>&1; then docker image prune --all --force --filter "until=\${${Ot.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,`if command -v podman >/dev/null 2>&1; then podman image prune --all --force --filter "until=\${${Ot.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,"if command -v apt-get >/dev/null 2>&1; then apt-get clean >/dev/null 2>&1 || true; fi"]}function ri(e){return[`${e}="$(df -P / 2>/dev/null | awk 'NR==2 {sub(/%/, "", $5); print $5}' || true)"`,`case "$${e}" in ''|*[!0-9]*) ${e}=-1 ;; esac`]}function en(e){let{retention:t,pressure:r,paths:n}=Qt(e),s='echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',i='echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"';if(!r)return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',Zr(t),...ei(n),...ti(),'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"'];let{lowWaterPercent:o,highWaterPercent:a,escalated:c}=r;return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',...ri("TS_CLOUD_HC_BEFORE"),"TS_CLOUD_HC_TIER=normal",Zr(t),`if [ "$TS_CLOUD_HC_BEFORE" -ge 0 ] && [ "$TS_CLOUD_HC_BEFORE" -lt ${o} ]; then`," TS_CLOUD_HC_TIER=light",` echo "[ts-cloud] host cleanup: / at \${TS_CLOUD_HC_BEFORE}% (under ${o}%), skipping cache and image pruning"`,`elif [ "$TS_CLOUD_HC_BEFORE" -ge ${a} ]; then`," TS_CLOUD_HC_TIER=pressure",` ${Zr(c)}`,` echo "[ts-cloud] warning: / at \${TS_CLOUD_HC_BEFORE}% (at or above ${a}%), host cleanup is using shortened retention windows" >&2`,"fi",...ei(n),'if [ "$TS_CLOUD_HC_TIER" != light ]; then',...ti().map((l)=>` ${l}`),"fi",'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"',...ri("TS_CLOUD_HC_AFTER"),`echo "[ts-cloud] host-cleanup {\\"event\\":\\"host-cleanup\\",\\"tier\\":\\"$TS_CLOUD_HC_TIER\\",\\"diskBeforePercent\\":$TS_CLOUD_HC_BEFORE,\\"diskAfterPercent\\":$TS_CLOUD_HC_AFTER,\\"lowWaterPercent\\":${o},\\"highWaterPercent\\":${a}}"`,"mkdir -p /var/lib/ts-cloud 2>/dev/null || true",'TS_CLOUD_HC_PREV="$(cat /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || echo ok)"',`if [ "$TS_CLOUD_HC_AFTER" -ge ${a} ]; then`,` echo "[ts-cloud] warning: / still at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%); something outside ts-cloud's retention rules is filling the disk" >&2`,` if [ "$TS_CLOUD_HC_PREV" != alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): / at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%)" >/dev/null 2>&1 || true; fi`," echo alert > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","else",` if [ "$TS_CLOUD_HC_PREV" = alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "✅ $(hostname): / back under ${a}% after host cleanup (\${TS_CLOUD_HC_AFTER}%)" >/dev/null 2>&1 || true; fi`," echo ok > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","fi"]}function ii(e){return["#!/usr/bin/env bash","# Managed by ts-cloud: rewritten on every deploy. Configure compute.hostCleanup instead of editing.","set -uo pipefail","TS_CLOUD_HC_LOCK=/run/lock/ts-cloud-host-cleanup.lock","[ -d /run/lock ] || TS_CLOUD_HC_LOCK=/tmp/ts-cloud-host-cleanup.lock",'if command -v flock >/dev/null 2>&1 && exec 9>"$TS_CLOUD_HC_LOCK"; then',' if ! flock -n 9; then echo "[ts-cloud] host cleanup already running; skipping"; exit 0; fi',"fi",...en(e),"exit 0"]}function Qr(e,t,r,n,s){return[`cat > ${e}.ts-cloud-new <<'${r}' || true`,...t,r,`if [ -s ${e}.ts-cloud-new ] && ! cmp -s ${e}.ts-cloud-new ${e}; then chmod ${n} ${e}.ts-cloud-new && mv -f ${e}.ts-cloud-new ${e}${s?` && ${s}=1`:""} || true; else rm -f ${e}.ts-cloud-new; fi`]}function oi(e){let t=Qt(e),r="/etc/systemd/system/ts-cloud-host-cleanup",n=["if [ -f /etc/systemd/system/ts-cloud-host-cleanup.timer ]; then"," systemctl disable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true"," rm -f /etc/systemd/system/ts-cloud-host-cleanup.timer /etc/systemd/system/ts-cloud-host-cleanup.service"," systemctl daemon-reload >/dev/null 2>&1 || true","fi"];if(!t.enabled)return[...n,"rm -f /usr/local/bin/ts-cloud-host-cleanup.sh"];let s=Qr("/usr/local/bin/ts-cloud-host-cleanup.sh",ii(e),"TS_CLOUD_HOST_CLEANUP_EOF","755");if(!t.timer)return[...s,...n];return[...s,"TS_CLOUD_HC_UNITS_CHANGED=0","if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then",...Qr("/etc/systemd/system/ts-cloud-host-cleanup.service",["[Unit]","Description=ts-cloud host cleanup (bounded disk retention)","After=local-fs.target","","[Service]","Type=oneshot","ExecStart=/usr/local/bin/ts-cloud-host-cleanup.sh","Nice=19","IOSchedulingClass=idle"],"TS_CLOUD_HOST_CLEANUP_SVC_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),...Qr("/etc/systemd/system/ts-cloud-host-cleanup.timer",["[Unit]","Description=Run ts-cloud host cleanup on a schedule, independent of deploys","","[Timer]",`OnCalendar=${t.schedule}`,"RandomizedDelaySec=1h","Persistent=true","","[Install]","WantedBy=timers.target"],"TS_CLOUD_HOST_CLEANUP_TMR_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),' if [ "$TS_CLOUD_HC_UNITS_CHANGED" -eq 1 ]; then systemctl daemon-reload >/dev/null 2>&1 || true; fi'," systemctl enable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true","fi"]}function tn(e){let t=oi(e);if(!Qt(e).enabled)return t;return[...t,'if [ -x /usr/local/bin/ts-cloud-host-cleanup.sh ]; then /usr/local/bin/ts-cloud-host-cleanup.sh < /dev/null || echo "[ts-cloud] warning: host cleanup exited $?" >&2; fi']}function ui(e,t){if(!t.length)return[];return[`cd ${e}`,`if [ -d ${R} ] && command -v pantry >/dev/null 2>&1; then`,` ${Ve()}`,"fi",...t]}var Uc=/\.(?:[cm]?[jt]sx?)$/;function sn(e,t,r){let n=t==="bun"?"/usr/local/bin/bun":t==="deno"?"/usr/local/bin/deno":"/usr/local/bin/node",s=e.trim();if(/^(?:bun|node|deno)\s+/.test(s))return`${n} ${s.replace(/^(?:bun|node|deno)\s+/,"")}`;let[i="",...o]=s.split(/\s+/);if(i&&!Uc.test(i)){let a=i.startsWith("/")?i:r&&i.startsWith("./")?`${r.replace(/\/+$/,"")}/${i.replace(/^\.\//,"")}`:void 0;if(a)return[a,...o].join(" ")}return`${n} ${s}`}function ai(e){return["TS_CLOUD_MEM_MB=$(awk '/^MemTotal:/{print int($2/1024)}' /proc/meminfo)","TS_CLOUD_HIGH_MB=$((TS_CLOUD_MEM_MB / 8))",'[ "$TS_CLOUD_HIGH_MB" -lt 512 ] && TS_CLOUD_HIGH_MB=512','[ "$TS_CLOUD_HIGH_MB" -gt 4096 ] && TS_CLOUD_HIGH_MB=4096',`mkdir -p /etc/systemd/system/${e}.d`,`printf '# Written by ts-cloud: memoryHigh resolved from this box.
|
|
271
|
+
`)}function Oc(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t")}"`}var Dc=new Set(["sqlite","sqlite3"]),Ic=new Set([":memory:","memory"]);function Ac(e){let t=e?.DB_CONNECTION?.trim().toLowerCase();return t!=null&&Dc.has(t)}function Lc(e){let t=e.trim().replace(/^\.\//,"");if(t===""||t.startsWith("/")||t.startsWith("~"))return;if(t.split("/").includes(".."))return;return t.replace(/\/+$/,"")}function Jr(e,t=[]){if(!Ac(e))return{};let r=e?.DB_DATABASE_PATH?.trim()||e?.DB_DATABASE?.trim();if(r&&Ic.has(r.toLowerCase()))return{};if(!r)return{warning:"DB_CONNECTION is sqlite but neither DB_DATABASE_PATH nor DB_DATABASE names a file, so ts-cloud cannot tell where the database lives. If it is written inside the release directory, the next deploy discards it. Set DB_DATABASE_PATH, or list the file in the site's `sharedPaths`."};let n=Lc(r);if(!n)return{};if(t.some((s)=>Vs(s)===n))return{};return{path:n}}function Zt(e,t=[]){let{path:r}=Jr(e,t);return r?[r]:[]}var ot="/var/ts-cloud/artifacts";var ni=Object.freeze({stagingMaxAgeMinutes:60,artifactMaxAgeDays:2,artifactUploadMaxAgeMinutes:60,bunCacheMaxAgeDays:7,journalMaxAgeDays:14,journalMaxSizeMb:512,containerImageMaxAgeHours:168}),si=Object.freeze({artifactMaxAgeDays:0,bunCacheMaxAgeDays:1,journalMaxAgeDays:3,journalMaxSizeMb:256,containerImageMaxAgeHours:24});function Js(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Be(e,t,r=0){if(!Number.isInteger(t)||t<r)throw Error(`hostCleanup: ${e} must be a whole number >= ${r} (got ${t})`)}function Zs(e,t){for(let[r,n]of Object.entries(t))Be(`${e}.${r}`,n);Be(`${e}.stagingMaxAgeMinutes`,t.stagingMaxAgeMinutes,15),Be(`${e}.artifactUploadMaxAgeMinutes`,t.artifactUploadMaxAgeMinutes,15),Be(`${e}.journalMaxSizeMb`,t.journalMaxSizeMb,1)}function kc(e,t){let r=`hostCleanup.paths[${t}]`,n=e.path?.replace(/\/+$/,"")??"";if(!n.startsWith("/")||n===""||/[\s'"`$\\]/.test(n)||n.split("/").includes(".."))throw Error(`${r}.path must be an absolute directory with no spaces, quotes or '..' (got ${JSON.stringify(e.path)})`);if(n==="/var/www"||n.startsWith("/var/www/"))throw Error(`${r}.path must not be under /var/www: releases are pruned by keepReleases`);if(!e.pattern||/^[*?]+$/.test(e.pattern)||e.pattern.includes("/")||/[\n'"`]/.test(e.pattern))throw Error(`${r}.pattern must be a file-name glob naming something, not a bare wildcard (got ${JSON.stringify(e.pattern)})`);if(Be(`${r}.maxAgeDays`,e.maxAgeDays),e.type!==void 0&&e.type!=="file"&&e.type!=="directory")throw Error(`${r}.type must be 'file' or 'directory'`)}function Qt(e){let t=typeof e==="object"&&e!==null?e:{},r=e!==!1,n={...ni,...Qs(t.retention)};Zs("hostCleanup.retention",n);let s=null;if(t.pressure!==!1){let a=typeof t.pressure==="object"?t.pressure:{},c=a.lowWaterPercent??50,l=a.highWaterPercent??85;if(Be("hostCleanup.pressure.lowWaterPercent",c),Be("hostCleanup.pressure.highWaterPercent",l,1),l>100||c>=l)throw Error(`hostCleanup.pressure: need 0 <= lowWaterPercent < highWaterPercent <= 100 (got ${c} and ${l})`);let u={...si,...Qs(a.escalated)},d=Object.fromEntries(Object.keys(n).map((m)=>[m,Math.min(n[m],u[m]??n[m])]));Zs("hostCleanup.pressure.escalated",d),s={lowWaterPercent:c,highWaterPercent:l,escalated:d}}let i=t.schedule??"daily";if(!/^[\w*:,./~ -]+$/.test(i))throw Error(`hostCleanup.schedule must be a systemd OnCalendar expression (got ${JSON.stringify(i)})`);let o=t.paths??[];return o.forEach(kc),{enabled:r,timer:r&&t.timer!==!1,schedule:i,retention:n,pressure:s,paths:o}}function Qs(e){return Object.fromEntries(Object.entries(e??{}).filter(([,t])=>t!==void 0))}var Ot={stagingMaxAgeMinutes:"TS_CLOUD_HC_STAGING_MIN",artifactMaxAgeDays:"TS_CLOUD_HC_ARTIFACT_DAYS",artifactUploadMaxAgeMinutes:"TS_CLOUD_HC_UPLOAD_MIN",bunCacheMaxAgeDays:"TS_CLOUD_HC_BUN_DAYS",journalMaxAgeDays:"TS_CLOUD_HC_JOURNAL_DAYS",journalMaxSizeMb:"TS_CLOUD_HC_JOURNAL_MB",containerImageMaxAgeHours:"TS_CLOUD_HC_IMAGE_HOURS"};function Zr(e){return Object.keys(Ot).map((t)=>`${Ot[t]}=${e[t]}`).join(" ")}var Ie=(e)=>`"$${Ot[e]}"`;function ei(e){return[`find /var/ts-cloud/staging -xdev -maxdepth 1 -type f -mmin +${Ie("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /tmp -xdev -maxdepth 1 -type f -name "*-release.tar.gz" -mmin +${Ie("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name "*.tar.gz" -mtime +${Ie("artifactMaxAgeDays")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name ".*.tmp" -mmin +${Ie("artifactUploadMaxAgeMinutes")} -delete 2>/dev/null || true`,...e.map(Nc),`journalctl --vacuum-time=${Ie("journalMaxAgeDays")}d --vacuum-size=${Ie("journalMaxSizeMb")}M >/dev/null 2>&1 || true`]}function Nc(e){let t=Js(e.path.replace(/\/+$/,"")),r=Js(e.pattern);return e.type==="directory"?`find ${t} -xdev -mindepth 1 -maxdepth 1 -type d -name ${r} -mtime +${e.maxAgeDays} -exec rm -rf -- {} + 2>/dev/null || true`:`find ${t} -xdev -mindepth 1 -maxdepth 1 -type f -name ${r} -mtime +${e.maxAgeDays} -delete 2>/dev/null || true`}function ti(){return[`find /root/.bun/install/cache -xdev -type f -mtime +${Ie("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`find /root/.bun/install/cache -xdev -depth -type d -empty -mtime +${Ie("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`if command -v docker >/dev/null 2>&1; then docker image prune --all --force --filter "until=\${${Ot.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,`if command -v podman >/dev/null 2>&1; then podman image prune --all --force --filter "until=\${${Ot.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,"if command -v apt-get >/dev/null 2>&1; then apt-get clean >/dev/null 2>&1 || true; fi"]}function ri(e){return[`${e}="$(df -P / 2>/dev/null | awk 'NR==2 {sub(/%/, "", $5); print $5}' || true)"`,`case "$${e}" in ''|*[!0-9]*) ${e}=-1 ;; esac`]}function en(e){let{retention:t,pressure:r,paths:n}=Qt(e),s='echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',i='echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"';if(!r)return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',Zr(t),...ei(n),...ti(),'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"'];let{lowWaterPercent:o,highWaterPercent:a,escalated:c}=r;return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',...ri("TS_CLOUD_HC_BEFORE"),"TS_CLOUD_HC_TIER=normal",Zr(t),`if [ "$TS_CLOUD_HC_BEFORE" -ge 0 ] && [ "$TS_CLOUD_HC_BEFORE" -lt ${o} ]; then`," TS_CLOUD_HC_TIER=light",` echo "[ts-cloud] host cleanup: / at \${TS_CLOUD_HC_BEFORE}% (under ${o}%), skipping cache and image pruning"`,`elif [ "$TS_CLOUD_HC_BEFORE" -ge ${a} ]; then`," TS_CLOUD_HC_TIER=pressure",` ${Zr(c)}`,` echo "[ts-cloud] warning: / at \${TS_CLOUD_HC_BEFORE}% (at or above ${a}%), host cleanup is using shortened retention windows" >&2`,"fi",...ei(n),'if [ "$TS_CLOUD_HC_TIER" != light ]; then',...ti().map((l)=>` ${l}`),"fi",'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"',...ri("TS_CLOUD_HC_AFTER"),`echo "[ts-cloud] host-cleanup {\\"event\\":\\"host-cleanup\\",\\"tier\\":\\"$TS_CLOUD_HC_TIER\\",\\"diskBeforePercent\\":$TS_CLOUD_HC_BEFORE,\\"diskAfterPercent\\":$TS_CLOUD_HC_AFTER,\\"lowWaterPercent\\":${o},\\"highWaterPercent\\":${a}}"`,"mkdir -p /var/lib/ts-cloud 2>/dev/null || true",'TS_CLOUD_HC_PREV="$(cat /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || echo ok)"',`if [ "$TS_CLOUD_HC_AFTER" -ge ${a} ]; then`,` echo "[ts-cloud] warning: / still at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%); something outside ts-cloud's retention rules is filling the disk" >&2`,` if [ "$TS_CLOUD_HC_PREV" != alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): / at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%)" >/dev/null 2>&1 || true; fi`," echo alert > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","else",` if [ "$TS_CLOUD_HC_PREV" = alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "✅ $(hostname): / back under ${a}% after host cleanup (\${TS_CLOUD_HC_AFTER}%)" >/dev/null 2>&1 || true; fi`," echo ok > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","fi"]}function ii(e){return["#!/usr/bin/env bash","# Managed by ts-cloud: rewritten on every deploy. Configure compute.hostCleanup instead of editing.","set -uo pipefail","TS_CLOUD_HC_LOCK=/run/lock/ts-cloud-host-cleanup.lock","[ -d /run/lock ] || TS_CLOUD_HC_LOCK=/tmp/ts-cloud-host-cleanup.lock",'if command -v flock >/dev/null 2>&1 && exec 9>"$TS_CLOUD_HC_LOCK"; then',' if ! flock -n 9; then echo "[ts-cloud] host cleanup already running; skipping"; exit 0; fi',"fi",...en(e),"exit 0"]}function Qr(e,t,r,n,s){return[`cat > ${e}.ts-cloud-new <<'${r}' || true`,...t,r,`if [ -s ${e}.ts-cloud-new ] && ! cmp -s ${e}.ts-cloud-new ${e}; then chmod ${n} ${e}.ts-cloud-new && mv -f ${e}.ts-cloud-new ${e}${s?` && ${s}=1`:""} || true; else rm -f ${e}.ts-cloud-new; fi`]}function oi(e){let t=Qt(e),r="/etc/systemd/system/ts-cloud-host-cleanup",n=["if [ -f /etc/systemd/system/ts-cloud-host-cleanup.timer ]; then"," systemctl disable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true"," rm -f /etc/systemd/system/ts-cloud-host-cleanup.timer /etc/systemd/system/ts-cloud-host-cleanup.service"," systemctl daemon-reload >/dev/null 2>&1 || true","fi"];if(!t.enabled)return[...n,"rm -f /usr/local/bin/ts-cloud-host-cleanup.sh"];let s=Qr("/usr/local/bin/ts-cloud-host-cleanup.sh",ii(e),"TS_CLOUD_HOST_CLEANUP_EOF","755");if(!t.timer)return[...s,...n];return[...s,"TS_CLOUD_HC_UNITS_CHANGED=0","if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then",...Qr("/etc/systemd/system/ts-cloud-host-cleanup.service",["[Unit]","Description=ts-cloud host cleanup (bounded disk retention)","After=local-fs.target","","[Service]","Type=oneshot","ExecStart=/usr/local/bin/ts-cloud-host-cleanup.sh","Nice=19","IOSchedulingClass=idle"],"TS_CLOUD_HOST_CLEANUP_SVC_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),...Qr("/etc/systemd/system/ts-cloud-host-cleanup.timer",["[Unit]","Description=Run ts-cloud host cleanup on a schedule, independent of deploys","","[Timer]",`OnCalendar=${t.schedule}`,"RandomizedDelaySec=1h","Persistent=true","","[Install]","WantedBy=timers.target"],"TS_CLOUD_HOST_CLEANUP_TMR_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),' if [ "$TS_CLOUD_HC_UNITS_CHANGED" -eq 1 ]; then systemctl daemon-reload >/dev/null 2>&1 || true; fi'," systemctl enable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true","fi"]}function tn(e){let t=oi(e);if(!Qt(e).enabled)return t;return[...t,'if [ -x /usr/local/bin/ts-cloud-host-cleanup.sh ]; then /usr/local/bin/ts-cloud-host-cleanup.sh < /dev/null || echo "[ts-cloud] warning: host cleanup exited $?" >&2; fi']}function ui(e,t,r={}){if(!t.length)return[];let n=Object.entries(r).flatMap(([s,i])=>{if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s))throw Error("Invalid pre-start environment variable name");if(["PORT","PORT_BACKEND","PORT_ADMIN","PORT_FRONTEND"].includes(s))return[];return[`export ${s}=${Ae(String(i))}`]});return[`cd ${e}`,`if [ -d ${R} ] && command -v pantry >/dev/null 2>&1; then`,` ${Ve()}`,"fi",...n,...t]}var Uc=/\.(?:[cm]?[jt]sx?)$/;function sn(e,t,r){let n=t==="bun"?"/usr/local/bin/bun":t==="deno"?"/usr/local/bin/deno":"/usr/local/bin/node",s=e.trim();if(/^(?:bun|node|deno)\s+/.test(s))return`${n} ${s.replace(/^(?:bun|node|deno)\s+/,"")}`;let[i="",...o]=s.split(/\s+/);if(i&&!Uc.test(i)){let a=i.startsWith("/")?i:r&&i.startsWith("./")?`${r.replace(/\/+$/,"")}/${i.replace(/^\.\//,"")}`:void 0;if(a)return[a,...o].join(" ")}return`${n} ${s}`}function ai(e){return["TS_CLOUD_MEM_MB=$(awk '/^MemTotal:/{print int($2/1024)}' /proc/meminfo)","TS_CLOUD_HIGH_MB=$((TS_CLOUD_MEM_MB / 8))",'[ "$TS_CLOUD_HIGH_MB" -lt 512 ] && TS_CLOUD_HIGH_MB=512','[ "$TS_CLOUD_HIGH_MB" -gt 4096 ] && TS_CLOUD_HIGH_MB=4096',`mkdir -p /etc/systemd/system/${e}.d`,`printf '# Written by ts-cloud: memoryHigh resolved from this box.
|
|
272
272
|
# An eighth of %sMB RAM. Set memoryHigh in config to override.
|
|
273
273
|
[Service]
|
|
274
274
|
MemoryAccounting=true
|
|
275
275
|
MemoryHigh=%sM
|
|
276
|
-
' "$TS_CLOUD_MEM_MB" "$TS_CLOUD_HIGH_MB" > /etc/systemd/system/${e}.d/50-ts-cloud-memory.conf`,"systemctl daemon-reload",'echo "[ts-cloud] memoryHigh=auto resolved to ${TS_CLOUD_HIGH_MB}M (one eighth of ${TS_CLOUD_MEM_MB}M)"',...nn()]}function tr(e,t={auto:!0}){let r=t.auto===!1?[]:[`/etc/systemd/system/${e}.d/50-ts-cloud-memory.conf`];for(let n of["/etc/systemd/system.control","/run/systemd/system.control"]){if(t.high)r.push(`${n}/${e}.d/50-MemoryHigh.conf`);if(t.max)r.push(`${n}/${e}.d/50-MemoryMax.conf`)}if(r.length===0)return[];return["TS_CLOUD_MEM_CLEARED=0",...r.map((n)=>`if [ -f ${n} ]; then rm -f ${n}; TS_CLOUD_MEM_CLEARED=1; echo "[ts-cloud] removed ${n}: memory limits now come from config"; fi`),'if [ "$TS_CLOUD_MEM_CLEARED" = 1 ]; then systemctl daemon-reload; fi']}function nn(){return[`TS_CLOUD_COMMIT=$(for f in /sys/fs/cgroup/system.slice/*.service/memory.high /sys/fs/cgroup/system.slice/*.slice/*.service/memory.high; do [ -f "$f" ] || continue; v=$(cat "$f" 2>/dev/null); [ "$v" = "max" ] && continue; echo "$v"; done | awk '{s+=$1} END {print int(s/1048576)}')`,"TS_CLOUD_RAM=$(awk '/^MemTotal:/{print int($2/1024)}' /proc/meminfo)",'if [ -n "$TS_CLOUD_COMMIT" ] && [ "$TS_CLOUD_COMMIT" -gt 0 ] && [ "$TS_CLOUD_RAM" -gt 0 ]; then TS_CLOUD_PCT=$((TS_CLOUD_COMMIT * 100 / TS_CLOUD_RAM)); if [ "$TS_CLOUD_PCT" -gt 300 ]; then echo "[ts-cloud] WARNING: services on this box declare ${TS_CLOUD_COMMIT}M of MemoryHigh against ${TS_CLOUD_RAM}M of RAM (${TS_CLOUD_PCT}% committed). Soft ceilings, so this is not fatal - but no ceiling can protect the box once enough of them are drawn on at once."; else echo "[ts-cloud] memory commitment: ${TS_CLOUD_COMMIT}M declared against ${TS_CLOUD_RAM}M RAM (${TS_CLOUD_PCT}%)"; fi; fi']}function Mc(e){return[`TS_CLOUD_SLICE=$(systemctl show ${e} -p Slice --value 2>/dev/null || true)`,'if [ -n "$TS_CLOUD_SLICE" ] && [ "$TS_CLOUD_SLICE" != "system.slice" ]; then systemctl set-property --runtime "$TS_CLOUD_SLICE" MemoryHigh=infinity MemoryMax=infinity 2>/dev/null || true; systemctl set-property "$TS_CLOUD_SLICE" MemoryHigh=infinity MemoryMax=infinity 2>/dev/null || true; fi']}var Be={path:"/",intervalSeconds:60,failuresBeforeRestart:3,timeoutSeconds:10,startupGraceSeconds:600,maxBackoffSeconds:3600,backoffBaseSeconds:300};function Dt(e,t,r){return typeof e==="number"&&Number.isFinite(e)?Math.max(r,Math.floor(e)):t}function ci(e){let{unitBase:t,unitPattern:r,port:n}=e,s=e.path&&e.path.startsWith("/")?e.path:Be.path,i=Dt(e.intervalSeconds,Be.intervalSeconds,10),o=Dt(e.failuresBeforeRestart,Be.failuresBeforeRestart,1),a=Dt(e.timeoutSeconds,Be.timeoutSeconds,1),c=Dt(e.startupGraceSeconds,Be.startupGraceSeconds,0),l=Dt(e.maxBackoffSeconds,Be.maxBackoffSeconds,0),u=Math.min(Be.backoffBaseSeconds,l),d=`/usr/local/sbin/${t}-liveness`,m=`/run/${t}-liveness`,h=`${t}-liveness`,f=`127.0.0.1:${n}${s}`,x=r.includes("@*")?`TS_CLOUD_UNIT=$(systemctl list-units --plain --no-legend --type=service '${r}' 2>/dev/null | awk '{print $1}' | head -1)`:`TS_CLOUD_UNIT=${r}`;return[`cat > ${d} <<'TS_CLOUD_LIVENESS_EOF'`,"#!/bin/sh",`# Liveness check for ${t}, written by ts-cloud on every deploy.`,`# Settings: interval=${i}s timeout=${a}s failures=${o} startup-grace=${c}s max-backoff=${l}s`,"set -eu",`TS_CLOUD_STATE=${m}`,'exec 9>"$TS_CLOUD_STATE.lock"',"flock -n 9 || exit 0",x,'[ -n "$TS_CLOUD_UNIT" ] || exit 0','systemctl is-active --quiet "$TS_CLOUD_UNIT" || exit 0',"TS_CLOUD_RC=0",`curl -s -o /dev/null --max-time ${a} "http://${f}" || TS_CLOUD_RC=$?`,'if [ "$TS_CLOUD_RC" -eq 0 ]; then',' rm -f "$TS_CLOUD_STATE.fail"',' if [ -f "$TS_CLOUD_STATE.restarts" ]; then',` TS_CLOUD_LAST=$(awk '{print $2}' "$TS_CLOUD_STATE.restarts" 2>/dev/null || true)`,` case "$TS_CLOUD_LAST" in ''|*[!0-9]*) TS_CLOUD_LAST=0 ;; esac`,` [ $(($(date +%s) - TS_CLOUD_LAST)) -lt ${l} ] || rm -f "$TS_CLOUD_STATE.restarts"`," fi"," exit 0","fi",'case "$TS_CLOUD_RC" in'," 7) TS_CLOUD_WHY='connection refused' ;;",` 28) TS_CLOUD_WHY='no response within ${a}s' ;;`,' *) TS_CLOUD_WHY="curl exit $TS_CLOUD_RC" ;;',"esac",'TS_CLOUD_SINCE=$(systemctl show "$TS_CLOUD_UNIT" -p ActiveEnterTimestampMonotonic --value 2>/dev/null || true)',"TS_CLOUD_UPTIME=$(cut -d' ' -f1 /proc/uptime 2>/dev/null || true)",`TS_CLOUD_AGE=$(awk -v up="$TS_CLOUD_UPTIME" -v since="$TS_CLOUD_SINCE" 'BEGIN { if (since + 0 > 0 && up + 0 > 0) print int(up - since / 1000000); else print -1 }')`,`case "$TS_CLOUD_AGE" in ''|*[!0-9-]*) TS_CLOUD_AGE=-1 ;; esac`,'if [ "$TS_CLOUD_AGE" -lt 0 ]; then',' TS_CLOUD_PID=$(systemctl show "$TS_CLOUD_UNIT" -p MainPID --value 2>/dev/null || true)',` case "$TS_CLOUD_PID" in ''|0|*[!0-9]*) ;; *) TS_CLOUD_AGE=$(ps -o etimes= -p "$TS_CLOUD_PID" 2>/dev/null | tr -d ' ' || true) ;; esac`,` case "$TS_CLOUD_AGE" in ''|*[!0-9]*) TS_CLOUD_AGE=-1 ;; esac`,"fi",`if [ "$TS_CLOUD_AGE" -ge 0 ] && [ "$TS_CLOUD_AGE" -lt ${c} ]; then`,' rm -f "$TS_CLOUD_STATE.fail"',` logger -t ${h} "$TS_CLOUD_UNIT is \${TS_CLOUD_AGE}s old, inside its ${c}s startup grace; not counting this failed check on ${f} ($TS_CLOUD_WHY)"`," exit 0","fi",'TS_CLOUD_FAILS=$(cat "$TS_CLOUD_STATE.fail" 2>/dev/null || echo 0)',`case "$TS_CLOUD_FAILS" in ''|*[!0-9]*) TS_CLOUD_FAILS=0 ;; esac`,"TS_CLOUD_FAILS=$((TS_CLOUD_FAILS + 1))",'echo "$TS_CLOUD_FAILS" > "$TS_CLOUD_STATE.fail"',`if [ "$TS_CLOUD_FAILS" -lt ${o} ]; then`,` logger -t ${h} "check $TS_CLOUD_FAILS of ${o} failed on ${f} ($TS_CLOUD_WHY)"`," exit 0","fi","TS_CLOUD_NOW=$(date +%s)","TS_CLOUD_STREAK=0","TS_CLOUD_LAST=0",'[ ! -f "$TS_CLOUD_STATE.restarts" ] || read -r TS_CLOUD_STREAK TS_CLOUD_LAST < "$TS_CLOUD_STATE.restarts" || true',`case "$TS_CLOUD_STREAK" in ''|*[!0-9]*) TS_CLOUD_STREAK=0 ;; esac`,`case "$TS_CLOUD_LAST" in ''|*[!0-9]*) TS_CLOUD_LAST=0 ;; esac`,'if [ "$TS_CLOUD_STREAK" -gt 0 ]; then',` TS_CLOUD_WAIT=${u}`," TS_CLOUD_I=1",` while [ "$TS_CLOUD_I" -lt "$TS_CLOUD_STREAK" ] && [ "$TS_CLOUD_WAIT" -lt ${l} ]; do TS_CLOUD_WAIT=$((TS_CLOUD_WAIT * 2)); TS_CLOUD_I=$((TS_CLOUD_I + 1)); done`,` [ "$TS_CLOUD_WAIT" -le ${l} ] || TS_CLOUD_WAIT=${l}`," TS_CLOUD_ELAPSED=$((TS_CLOUD_NOW - TS_CLOUD_LAST))",' if [ "$TS_CLOUD_ELAPSED" -lt "$TS_CLOUD_WAIT" ]; then',` logger -t ${h} "holding off: $TS_CLOUD_STREAK liveness restart(s) in a row have not brought $TS_CLOUD_UNIT back; next restart allowed in $((TS_CLOUD_WAIT - TS_CLOUD_ELAPSED))s ($TS_CLOUD_WHY on ${f})"`," exit 0"," fi","fi","TS_CLOUD_STREAK=$((TS_CLOUD_STREAK + 1))",`logger -t ${h} "no HTTP response on ${f} after $TS_CLOUD_FAILS checks ($TS_CLOUD_WHY); restarting $TS_CLOUD_UNIT (liveness restart $TS_CLOUD_STREAK in a row)"`,'echo "$TS_CLOUD_STREAK $TS_CLOUD_NOW" > "$TS_CLOUD_STATE.restarts"','rm -f "$TS_CLOUD_STATE.fail"','systemctl restart "$TS_CLOUD_UNIT"',"TS_CLOUD_LIVENESS_EOF",`chmod +x ${d}`,`rm -f ${m}.fail ${m}.restarts`,`cat > /etc/systemd/system/${t}-liveness.service <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=Liveness check for ${t} (managed by ts-cloud)`,"","[Service]","Type=oneshot",`ExecStart=${d}`,"TS_CLOUD_UNIT_EOF",`cat > /etc/systemd/system/${t}-liveness.timer <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=Run the ${t} liveness check every ${i}s`,"","[Timer]",`OnUnitActiveSec=${i}s`,`OnBootSec=${i}s`,"Persistent=true","","[Install]","WantedBy=timers.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",`systemctl enable --now ${t}-liveness.timer >/dev/null 2>&1 || true`]}var li=20,rn=3,Fc=5,Bc=60;function jc(e){let{paths:t,unitBase:r,releaseId:n,port:s}=e,i=`${r}@${n}.service`,o=`${r}.service`,a=Math.max(1,e.healthGateSeconds??5),c=e.healthCheckPath?e.healthCheckPath.startsWith("/")?e.healthCheckPath:`/${e.healthCheckPath}`:null,l=c?`curl -sf -o /dev/null --max-time ${Fc} "http://127.0.0.1:${s}${c}"`:null,u=(d)=>`{ echo "[ts-cloud] release ${n} ${d}" >&2; ts_cloud_restore_previous; exit 1; }`;return[`TS_CLOUD_OLD_UNITS=$(systemctl list-units --plain --no-legend --type=service "${r}@*.service" 2>/dev/null | awk '{print $1}' | grep -v "^${i}$" || true)`,`TS_CLOUD_PREV_CURRENT="$(readlink ${t.current} 2>/dev/null || true)"`,"TS_CLOUD_LEGACY_STOPPED=0","ts_cloud_restore_previous() {",` journalctl -u ${i} -n 50 --no-pager >&2 || true`,` systemctl stop ${i} 2>/dev/null || true`,` systemctl disable ${i} 2>/dev/null || true`,` if [ -n "$TS_CLOUD_PREV_CURRENT" ] && [ -d "$TS_CLOUD_PREV_CURRENT" ]; then ln -sfn "$TS_CLOUD_PREV_CURRENT" ${t.current}.tmp && mv -Tf ${t.current}.tmp ${t.current}; fi`,' for TS_CLOUD_RU in ${TS_CLOUD_OLD_UNITS}; do systemctl enable "$TS_CLOUD_RU" 2>/dev/null || true; systemctl start "$TS_CLOUD_RU" 2>/dev/null || true; done',` if [ "$TS_CLOUD_LEGACY_STOPPED" -eq 1 ]; then systemctl start ${o} 2>/dev/null || true; fi`,' if [ -n "$TS_CLOUD_OLD_UNITS" ] || [ "$TS_CLOUD_LEGACY_STOPPED" -eq 1 ]; then echo "[ts-cloud] previous release restored and serving" >&2; else echo "[ts-cloud] there was no previous release to restore" >&2; fi',"}",`if [ -f /etc/systemd/system/${o} ] && systemctl is-active --quiet ${o}; then echo "[ts-cloud] retiring pre-zero-downtime unit ${o} (one-time restart cutover)"; systemctl stop ${o}; TS_CLOUD_LEGACY_STOPPED=1; fi`,'TS_CLOUD_STARTED_AT="@$(date +%s)"',`systemctl restart ${i}`,`TS_CLOUD_GATE_OK=1; for TS_CLOUD_I in $(seq 1 ${a}); do sleep 1; systemctl is-active --quiet ${i} || { TS_CLOUD_GATE_OK=0; break; }; done`,'if [ "$TS_CLOUD_GATE_OK" -ne 1 ]; then',` if journalctl -u ${i} --since "$TS_CLOUD_STARTED_AT" -o cat --no-pager 2>/dev/null | grep -iE 'EADDRINUSE|address already in use|port [0-9]+ (is )?in use|Failed to start server' >/dev/null; then`,` echo "[ts-cloud] release ${n} could not share :${s} with the previous release (no SO_REUSEPORT?) — retiring it and retrying" >&2`,' for TS_CLOUD_RU in ${TS_CLOUD_OLD_UNITS}; do systemctl stop "$TS_CLOUD_RU" 2>/dev/null || true; done',` systemctl restart ${i}`,` for TS_CLOUD_I in $(seq 1 ${a}); do sleep 1; systemctl is-active --quiet ${i} || ${u("failed its health gate on the free port")}; done`," else",` ${u("crashed on start — the previous release was never stopped")}`," fi","fi",`if ss -ltnpH "sport = :${s}" >/dev/null 2>&1; then`," TS_CLOUD_BOUND=0",` for TS_CLOUD_I in $(seq 1 ${Bc}); do`,` TS_CLOUD_NEW_PID="$(systemctl show -p MainPID --value ${i} 2>/dev/null || true)"`,` if [ -n "$TS_CLOUD_NEW_PID" ] && [ "$TS_CLOUD_NEW_PID" != "0" ] && ss -ltnpH "sport = :${s}" 2>/dev/null | grep "pid=$TS_CLOUD_NEW_PID," >/dev/null; then TS_CLOUD_BOUND=1; break; fi`,` systemctl is-active --quiet ${i} || break`,` sleep ${rn}`," done",` [ "$TS_CLOUD_BOUND" -eq 1 ] || ${u(`never took :${s}`)}`,"else",' echo "[ts-cloud] cannot read listener PIDs on this host — the response probes run without the bind check" >&2',"fi",...l?[`TS_CLOUD_HEALTHY=0; for TS_CLOUD_I in $(seq 1 ${li}); do if ${l}; then TS_CLOUD_HEALTHY=1; break; fi; sleep ${rn}; done`,`[ "$TS_CLOUD_HEALTHY" -eq 1 ] || ${u(`never answered ${c}`)}`]:[],...hn(t),`systemctl enable ${i} 2>/dev/null || true`,'for TS_CLOUD_U in ${TS_CLOUD_OLD_UNITS}; do systemctl stop "$TS_CLOUD_U" 2>/dev/null || true; systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done',`TS_CLOUD_LISTENING=0; for TS_CLOUD_I in $(seq 1 ${a}); do if ss -ltnH "sport = :${s}" 2>/dev/null | grep . >/dev/null; then TS_CLOUD_LISTENING=1; break; fi; sleep 1; done`,`if [ "$TS_CLOUD_LISTENING" -ne 1 ]; then echo "[ts-cloud] nothing is listening on ${s} after retiring the previous release — restarting ${i} on the now-free port" >&2; systemctl restart ${i}; for TS_CLOUD_I in $(seq 1 ${a}); do if ss -ltnH "sport = :${s}" 2>/dev/null | grep . >/dev/null; then TS_CLOUD_LISTENING=1; break; fi; sleep 1; done; fi`,`[ "$TS_CLOUD_LISTENING" -eq 1 ] || ${u(`is active but never bound :${s}`)}`,...l?[`TS_CLOUD_HEALTHY=0; for TS_CLOUD_I in $(seq 1 ${li}); do if ${l}; then TS_CLOUD_HEALTHY=1; break; fi; sleep ${rn}; done`,`[ "$TS_CLOUD_HEALTHY" -eq 1 ] || ${u(`stopped answering ${c} once it served alone`)}`]:[],...Ws(t),`systemctl list-unit-files --plain --no-legend "${r}@*.service" 2>/dev/null | awk '{print $1}' | { grep -v -e "^${i}$" -e "^${r}@\\.service$" || true; } | while read -r TS_CLOUD_U; do systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,`if [ -f /etc/systemd/system/${o} ]; then systemctl disable ${o} 2>/dev/null || true; rm -f /etc/systemd/system/${o}; systemctl daemon-reload; fi`]}function pi(e,t){if(!t.length)return[];let r=Ke(e.release);return[`if [ -e ${r} ] || [ -L ${r} ]; then`,...t,"fi"]}function on(e){let{siteName:t,slug:r,artifactFetch:n,releaseId:s,execStart:i,envEntries:o,port:a,keepReleases:c=_i,preStartCommands:l=[],postActivateCommands:u=[],beforeResetCommands:d=[],healthCheckPath:m,healthGateSeconds:h=5,liveness:f,memoryHigh:S="auto",memoryMax:x,cpuWeight:B,ioWeight:L,tasksMax:N,stopTimeout:D}=e,E=[...B!=null?[`CPUWeight=${B}`]:[],...L!=null?[`IOWeight=${L}`]:[],...N!=null?[`TasksMax=${N}`]:[],...D?[`TimeoutStopSec=${D}`]:[]],M=e.zeroDowntime??a!=null,H=e.appDir??`/var/www/${t}`,z=Ht(H,s),q=`${r}-${t}`,X=`${q}.service`,J=It(r,t,s),te=e.sharedPaths??[],Z=Jc([".env",...te,...Zt(o,te)]),Y=Jt(o),ie=ui(z.release,l),ce=["set -euo pipefail",...zs(z),...n,...$i(z,Z),...pi(z,d),Gs(z),...Hs(z),`tar xzf ${J} -C "$TS_CLOUD_STAGED"`,`rm -f ${J}`,`cat > ${z.shared}/.env <<'TS_CLOUD_ENV_EOF'`,Y,"TS_CLOUD_ENV_EOF",`chmod 600 ${z.shared}/.env`,...qs(z),`find ${z.release} -maxdepth 1 \\( -type f -o -type l \\) -name '.env*' ! -name '.env.example' ! -name '*.example' -delete`,`sed -i -E '/^[[:space:]]*(PORT|PORT_BACKEND|PORT_ADMIN|PORT_FRONTEND)[[:space:]]*=/d' ${z.shared}/.env 2>/dev/null || true`,..._o(z,Z),...ie,...e.isolation?Xs(e.slug||t,z.base,Z.flatMap((y)=>typeof y==="string"||!y.target?[]:[y.target])):[]];if(M&&a!=null){let y=`${q}@${s}.service`,k=m?m.startsWith("/")?m:`/${m}`:null;return[...ce,`cat > /etc/systemd/system/${q}@.service <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=${t} release %i (managed by ts-cloud)`,"After=network.target","","[Service]","Type=simple",...e.isolation?xt(st(e.slug||t),z.base,e.slug||t):[],`WorkingDirectory=${z.releases}/%i`,`ExecStart=${i}`,"Restart=always","RestartSec=5","MemoryAccounting=true",...S&&S!=="auto"?[`MemoryHigh=${S}`]:[],...x?[`MemoryMax=${x}`]:[],...E,`EnvironmentFile=${z.releases}/%i/.env`,`Environment=PORT=${a}`,`Environment=STX_IMAGE_CACHE_DIR=${xo(z)}`,`Environment=STX_BUNDLE_CACHE_DIR=${Oi(z)}`,`Environment=STX_BUNDLE_CACHE_DIR=${Oi(z)}`,"","[Install]","WantedBy=multi-user.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",...S==="auto"?ai(`${q}@.service`):[...tr(`${q}@.service`),...nn()],...Mc(y),...jc({paths:z,unitBase:q,releaseId:s,port:a,healthCheckPath:k,healthGateSeconds:h}),...u,...yn(z,c),...f===!1?[]:ci({...f,unitBase:q,unitPattern:`${q}@*.service`,port:a,path:f?.path??m})]}return[...ce,`cat > /etc/systemd/system/${X} <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=${t} (managed by ts-cloud)`,"After=network.target","","[Service]","Type=simple",...e.isolation?xt(st(e.slug||t),z.base,e.slug||t):[],`WorkingDirectory=${z.current}`,`ExecStart=${i}`,"Restart=always","RestartSec=5","MemoryAccounting=true",...S&&S!=="auto"?[`MemoryHigh=${S}`]:[],...x?[`MemoryMax=${x}`]:[],...E,`EnvironmentFile=${z.current}/.env`,...a?[`Environment=PORT=${a}`]:[],`Environment=STX_IMAGE_CACHE_DIR=${xo(z)}`,`Environment=STX_BUNDLE_CACHE_DIR=${Oi(z)}`,"","[Install]","WantedBy=multi-user.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",...tr(X,{auto:S!=="auto",high:S!=="auto",max:Boolean(x)}),...S==="auto"?ai(X):nn(),`systemctl enable ${X}`,`TS_CLOUD_PREV_CURRENT="$(readlink ${z.current} 2>/dev/null || true)"`,...hn(z),`systemctl restart ${X}`,`for TS_CLOUD_I in $(seq 1 ${Math.max(1,h)}); do sleep 1; systemctl is-active --quiet ${X} || { echo "[ts-cloud] release ${s} of ${X} did not stay up — restoring the previous release" >&2; journalctl -u ${X} -n 50 --no-pager >&2 || true; if [ -n "$TS_CLOUD_PREV_CURRENT" ] && [ -d "$TS_CLOUD_PREV_CURRENT" ] && [ "$(readlink -f "$TS_CLOUD_PREV_CURRENT")" != "$(readlink -f ${z.release})" ]; then ln -sfn "$TS_CLOUD_PREV_CURRENT" ${z.current}.tmp && mv -Tf ${z.current}.tmp ${z.current}; systemctl restart ${X} || true; fi; exit 1; }; done`,...Ws(z),...u,...yn(z,c),...f===!1||!a?[]:ci({...f,unitBase:q,unitPattern:X,port:a,path:f?.path??m})]}function an(e){let{siteName:t,artifactFetch:r,releaseId:n,keepReleases:s=_i,preStartCommands:i=[],postActivateCommands:o=[],beforeResetCommands:a=[]}=e,c=e.appDir??`/var/www/${t}`,l=Ht(c,n),u=It(e.slug,t,n),d=ui(l.release,i);return["set -euo pipefail",...zs(l),...r,...$i(l,[]),...pi(l,a),Gs(l),...Hs(l),`tar xzf ${u} -C "$TS_CLOUD_STAGED"`,...qs(l),`rm -f ${u}`,...d,...hn(l),...o,...yn(l,s)]}function It(e,t,r){return`/tmp/${[e,t,r].filter(Boolean).join("-")}-release.tar.gz`}function fn(e,t,r,n){return[`aws s3 cp "s3://${e}/${t}" ${n} --region ${r}`]}function gn(e,t){return[`mv "${e}" ${t}`]}function Ke(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function rr(e,t,r){return["set -euo pipefail",`chmod 600 ${Ke(e)}`,`mv -f -- ${Ke(e)} ${Ke(t)}`,`cp -- ${Ke(t)} ${Ke(r)}`].join(`
|
|
277
|
-
`)}function nr(e){return`rm -f -- ${
|
|
276
|
+
' "$TS_CLOUD_MEM_MB" "$TS_CLOUD_HIGH_MB" > /etc/systemd/system/${e}.d/50-ts-cloud-memory.conf`,"systemctl daemon-reload",'echo "[ts-cloud] memoryHigh=auto resolved to ${TS_CLOUD_HIGH_MB}M (one eighth of ${TS_CLOUD_MEM_MB}M)"',...nn()]}function tr(e,t={auto:!0}){let r=t.auto===!1?[]:[`/etc/systemd/system/${e}.d/50-ts-cloud-memory.conf`];for(let n of["/etc/systemd/system.control","/run/systemd/system.control"]){if(t.high)r.push(`${n}/${e}.d/50-MemoryHigh.conf`);if(t.max)r.push(`${n}/${e}.d/50-MemoryMax.conf`)}if(r.length===0)return[];return["TS_CLOUD_MEM_CLEARED=0",...r.map((n)=>`if [ -f ${n} ]; then rm -f ${n}; TS_CLOUD_MEM_CLEARED=1; echo "[ts-cloud] removed ${n}: memory limits now come from config"; fi`),'if [ "$TS_CLOUD_MEM_CLEARED" = 1 ]; then systemctl daemon-reload; fi']}function nn(){return[`TS_CLOUD_COMMIT=$(for f in /sys/fs/cgroup/system.slice/*.service/memory.high /sys/fs/cgroup/system.slice/*.slice/*.service/memory.high; do [ -f "$f" ] || continue; v=$(cat "$f" 2>/dev/null); [ "$v" = "max" ] && continue; echo "$v"; done | awk '{s+=$1} END {print int(s/1048576)}')`,"TS_CLOUD_RAM=$(awk '/^MemTotal:/{print int($2/1024)}' /proc/meminfo)",'if [ -n "$TS_CLOUD_COMMIT" ] && [ "$TS_CLOUD_COMMIT" -gt 0 ] && [ "$TS_CLOUD_RAM" -gt 0 ]; then TS_CLOUD_PCT=$((TS_CLOUD_COMMIT * 100 / TS_CLOUD_RAM)); if [ "$TS_CLOUD_PCT" -gt 300 ]; then echo "[ts-cloud] WARNING: services on this box declare ${TS_CLOUD_COMMIT}M of MemoryHigh against ${TS_CLOUD_RAM}M of RAM (${TS_CLOUD_PCT}% committed). Soft ceilings, so this is not fatal - but no ceiling can protect the box once enough of them are drawn on at once."; else echo "[ts-cloud] memory commitment: ${TS_CLOUD_COMMIT}M declared against ${TS_CLOUD_RAM}M RAM (${TS_CLOUD_PCT}%)"; fi; fi']}function Mc(e){return[`TS_CLOUD_SLICE=$(systemctl show ${e} -p Slice --value 2>/dev/null || true)`,'if [ -n "$TS_CLOUD_SLICE" ] && [ "$TS_CLOUD_SLICE" != "system.slice" ]; then systemctl set-property --runtime "$TS_CLOUD_SLICE" MemoryHigh=infinity MemoryMax=infinity 2>/dev/null || true; systemctl set-property "$TS_CLOUD_SLICE" MemoryHigh=infinity MemoryMax=infinity 2>/dev/null || true; fi']}var Ke={path:"/",intervalSeconds:60,failuresBeforeRestart:3,timeoutSeconds:10,startupGraceSeconds:600,maxBackoffSeconds:3600,backoffBaseSeconds:300};function Dt(e,t,r){return typeof e==="number"&&Number.isFinite(e)?Math.max(r,Math.floor(e)):t}function ci(e){let{unitBase:t,unitPattern:r,port:n}=e,s=e.path&&e.path.startsWith("/")?e.path:Ke.path,i=Dt(e.intervalSeconds,Ke.intervalSeconds,10),o=Dt(e.failuresBeforeRestart,Ke.failuresBeforeRestart,1),a=Dt(e.timeoutSeconds,Ke.timeoutSeconds,1),c=Dt(e.startupGraceSeconds,Ke.startupGraceSeconds,0),l=Dt(e.maxBackoffSeconds,Ke.maxBackoffSeconds,0),u=Math.min(Ke.backoffBaseSeconds,l),d=`/usr/local/sbin/${t}-liveness`,m=`/run/${t}-liveness`,h=`${t}-liveness`,f=`127.0.0.1:${n}${s}`,x=r.includes("@*")?`TS_CLOUD_UNIT=$(systemctl list-units --plain --no-legend --type=service '${r}' 2>/dev/null | awk '{print $1}' | head -1)`:`TS_CLOUD_UNIT=${r}`;return[`cat > ${d} <<'TS_CLOUD_LIVENESS_EOF'`,"#!/bin/sh",`# Liveness check for ${t}, written by ts-cloud on every deploy.`,`# Settings: interval=${i}s timeout=${a}s failures=${o} startup-grace=${c}s max-backoff=${l}s`,"set -eu",`TS_CLOUD_STATE=${m}`,'exec 9>"$TS_CLOUD_STATE.lock"',"flock -n 9 || exit 0",x,'[ -n "$TS_CLOUD_UNIT" ] || exit 0','systemctl is-active --quiet "$TS_CLOUD_UNIT" || exit 0',"TS_CLOUD_RC=0",`curl -s -o /dev/null --max-time ${a} "http://${f}" || TS_CLOUD_RC=$?`,'if [ "$TS_CLOUD_RC" -eq 0 ]; then',' rm -f "$TS_CLOUD_STATE.fail"',' if [ -f "$TS_CLOUD_STATE.restarts" ]; then',` TS_CLOUD_LAST=$(awk '{print $2}' "$TS_CLOUD_STATE.restarts" 2>/dev/null || true)`,` case "$TS_CLOUD_LAST" in ''|*[!0-9]*) TS_CLOUD_LAST=0 ;; esac`,` [ $(($(date +%s) - TS_CLOUD_LAST)) -lt ${l} ] || rm -f "$TS_CLOUD_STATE.restarts"`," fi"," exit 0","fi",'case "$TS_CLOUD_RC" in'," 7) TS_CLOUD_WHY='connection refused' ;;",` 28) TS_CLOUD_WHY='no response within ${a}s' ;;`,' *) TS_CLOUD_WHY="curl exit $TS_CLOUD_RC" ;;',"esac",'TS_CLOUD_SINCE=$(systemctl show "$TS_CLOUD_UNIT" -p ActiveEnterTimestampMonotonic --value 2>/dev/null || true)',"TS_CLOUD_UPTIME=$(cut -d' ' -f1 /proc/uptime 2>/dev/null || true)",`TS_CLOUD_AGE=$(awk -v up="$TS_CLOUD_UPTIME" -v since="$TS_CLOUD_SINCE" 'BEGIN { if (since + 0 > 0 && up + 0 > 0) print int(up - since / 1000000); else print -1 }')`,`case "$TS_CLOUD_AGE" in ''|*[!0-9-]*) TS_CLOUD_AGE=-1 ;; esac`,'if [ "$TS_CLOUD_AGE" -lt 0 ]; then',' TS_CLOUD_PID=$(systemctl show "$TS_CLOUD_UNIT" -p MainPID --value 2>/dev/null || true)',` case "$TS_CLOUD_PID" in ''|0|*[!0-9]*) ;; *) TS_CLOUD_AGE=$(ps -o etimes= -p "$TS_CLOUD_PID" 2>/dev/null | tr -d ' ' || true) ;; esac`,` case "$TS_CLOUD_AGE" in ''|*[!0-9]*) TS_CLOUD_AGE=-1 ;; esac`,"fi",`if [ "$TS_CLOUD_AGE" -ge 0 ] && [ "$TS_CLOUD_AGE" -lt ${c} ]; then`,' rm -f "$TS_CLOUD_STATE.fail"',` logger -t ${h} "$TS_CLOUD_UNIT is \${TS_CLOUD_AGE}s old, inside its ${c}s startup grace; not counting this failed check on ${f} ($TS_CLOUD_WHY)"`," exit 0","fi",'TS_CLOUD_FAILS=$(cat "$TS_CLOUD_STATE.fail" 2>/dev/null || echo 0)',`case "$TS_CLOUD_FAILS" in ''|*[!0-9]*) TS_CLOUD_FAILS=0 ;; esac`,"TS_CLOUD_FAILS=$((TS_CLOUD_FAILS + 1))",'echo "$TS_CLOUD_FAILS" > "$TS_CLOUD_STATE.fail"',`if [ "$TS_CLOUD_FAILS" -lt ${o} ]; then`,` logger -t ${h} "check $TS_CLOUD_FAILS of ${o} failed on ${f} ($TS_CLOUD_WHY)"`," exit 0","fi","TS_CLOUD_NOW=$(date +%s)","TS_CLOUD_STREAK=0","TS_CLOUD_LAST=0",'[ ! -f "$TS_CLOUD_STATE.restarts" ] || read -r TS_CLOUD_STREAK TS_CLOUD_LAST < "$TS_CLOUD_STATE.restarts" || true',`case "$TS_CLOUD_STREAK" in ''|*[!0-9]*) TS_CLOUD_STREAK=0 ;; esac`,`case "$TS_CLOUD_LAST" in ''|*[!0-9]*) TS_CLOUD_LAST=0 ;; esac`,'if [ "$TS_CLOUD_STREAK" -gt 0 ]; then',` TS_CLOUD_WAIT=${u}`," TS_CLOUD_I=1",` while [ "$TS_CLOUD_I" -lt "$TS_CLOUD_STREAK" ] && [ "$TS_CLOUD_WAIT" -lt ${l} ]; do TS_CLOUD_WAIT=$((TS_CLOUD_WAIT * 2)); TS_CLOUD_I=$((TS_CLOUD_I + 1)); done`,` [ "$TS_CLOUD_WAIT" -le ${l} ] || TS_CLOUD_WAIT=${l}`," TS_CLOUD_ELAPSED=$((TS_CLOUD_NOW - TS_CLOUD_LAST))",' if [ "$TS_CLOUD_ELAPSED" -lt "$TS_CLOUD_WAIT" ]; then',` logger -t ${h} "holding off: $TS_CLOUD_STREAK liveness restart(s) in a row have not brought $TS_CLOUD_UNIT back; next restart allowed in $((TS_CLOUD_WAIT - TS_CLOUD_ELAPSED))s ($TS_CLOUD_WHY on ${f})"`," exit 0"," fi","fi","TS_CLOUD_STREAK=$((TS_CLOUD_STREAK + 1))",`logger -t ${h} "no HTTP response on ${f} after $TS_CLOUD_FAILS checks ($TS_CLOUD_WHY); restarting $TS_CLOUD_UNIT (liveness restart $TS_CLOUD_STREAK in a row)"`,'echo "$TS_CLOUD_STREAK $TS_CLOUD_NOW" > "$TS_CLOUD_STATE.restarts"','rm -f "$TS_CLOUD_STATE.fail"','systemctl restart "$TS_CLOUD_UNIT"',"TS_CLOUD_LIVENESS_EOF",`chmod +x ${d}`,`rm -f ${m}.fail ${m}.restarts`,`cat > /etc/systemd/system/${t}-liveness.service <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=Liveness check for ${t} (managed by ts-cloud)`,"","[Service]","Type=oneshot",`ExecStart=${d}`,"TS_CLOUD_UNIT_EOF",`cat > /etc/systemd/system/${t}-liveness.timer <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=Run the ${t} liveness check every ${i}s`,"","[Timer]",`OnUnitActiveSec=${i}s`,`OnBootSec=${i}s`,"Persistent=true","","[Install]","WantedBy=timers.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",`systemctl enable --now ${t}-liveness.timer >/dev/null 2>&1 || true`]}var li=20,rn=3,Fc=5,Bc=60;function jc(e){let{paths:t,unitBase:r,releaseId:n,port:s}=e,i=`${r}@${n}.service`,o=`${r}.service`,a=Math.max(1,e.healthGateSeconds??5),c=e.healthCheckPath?e.healthCheckPath.startsWith("/")?e.healthCheckPath:`/${e.healthCheckPath}`:null,l=c?`curl -sf -o /dev/null --max-time ${Fc} "http://127.0.0.1:${s}${c}"`:null,u=(d)=>`{ echo "[ts-cloud] release ${n} ${d}" >&2; ts_cloud_restore_previous; exit 1; }`;return[`TS_CLOUD_OLD_UNITS=$(systemctl list-units --plain --no-legend --type=service "${r}@*.service" 2>/dev/null | awk '{print $1}' | grep -v "^${i}$" || true)`,`TS_CLOUD_PREV_CURRENT="$(readlink ${t.current} 2>/dev/null || true)"`,"TS_CLOUD_LEGACY_STOPPED=0","ts_cloud_restore_previous() {",` journalctl -u ${i} -n 50 --no-pager >&2 || true`,` systemctl stop ${i} 2>/dev/null || true`,` systemctl disable ${i} 2>/dev/null || true`,` if [ -n "$TS_CLOUD_PREV_CURRENT" ] && [ -d "$TS_CLOUD_PREV_CURRENT" ]; then ln -sfn "$TS_CLOUD_PREV_CURRENT" ${t.current}.tmp && mv -Tf ${t.current}.tmp ${t.current}; fi`,' for TS_CLOUD_RU in ${TS_CLOUD_OLD_UNITS}; do systemctl enable "$TS_CLOUD_RU" 2>/dev/null || true; systemctl start "$TS_CLOUD_RU" 2>/dev/null || true; done',` if [ "$TS_CLOUD_LEGACY_STOPPED" -eq 1 ]; then systemctl start ${o} 2>/dev/null || true; fi`,' if [ -n "$TS_CLOUD_OLD_UNITS" ] || [ "$TS_CLOUD_LEGACY_STOPPED" -eq 1 ]; then echo "[ts-cloud] previous release restored and serving" >&2; else echo "[ts-cloud] there was no previous release to restore" >&2; fi',"}",`if [ -f /etc/systemd/system/${o} ] && systemctl is-active --quiet ${o}; then echo "[ts-cloud] retiring pre-zero-downtime unit ${o} (one-time restart cutover)"; systemctl stop ${o}; TS_CLOUD_LEGACY_STOPPED=1; fi`,'TS_CLOUD_STARTED_AT="@$(date +%s)"',`systemctl restart ${i}`,`TS_CLOUD_GATE_OK=1; for TS_CLOUD_I in $(seq 1 ${a}); do sleep 1; systemctl is-active --quiet ${i} || { TS_CLOUD_GATE_OK=0; break; }; done`,'if [ "$TS_CLOUD_GATE_OK" -ne 1 ]; then',` if journalctl -u ${i} --since "$TS_CLOUD_STARTED_AT" -o cat --no-pager 2>/dev/null | grep -iE 'EADDRINUSE|address already in use|port [0-9]+ (is )?in use|Failed to start server' >/dev/null; then`,` echo "[ts-cloud] release ${n} could not share :${s} with the previous release (no SO_REUSEPORT?) — retiring it and retrying" >&2`,' for TS_CLOUD_RU in ${TS_CLOUD_OLD_UNITS}; do systemctl stop "$TS_CLOUD_RU" 2>/dev/null || true; done',` systemctl restart ${i}`,` for TS_CLOUD_I in $(seq 1 ${a}); do sleep 1; systemctl is-active --quiet ${i} || ${u("failed its health gate on the free port")}; done`," else",` ${u("crashed on start — the previous release was never stopped")}`," fi","fi",`if ss -ltnpH "sport = :${s}" >/dev/null 2>&1; then`," TS_CLOUD_BOUND=0",` for TS_CLOUD_I in $(seq 1 ${Bc}); do`,` TS_CLOUD_NEW_PID="$(systemctl show -p MainPID --value ${i} 2>/dev/null || true)"`,` if [ -n "$TS_CLOUD_NEW_PID" ] && [ "$TS_CLOUD_NEW_PID" != "0" ] && ss -ltnpH "sport = :${s}" 2>/dev/null | grep "pid=$TS_CLOUD_NEW_PID," >/dev/null; then TS_CLOUD_BOUND=1; break; fi`,` systemctl is-active --quiet ${i} || break`,` sleep ${rn}`," done",` [ "$TS_CLOUD_BOUND" -eq 1 ] || ${u(`never took :${s}`)}`,"else",' echo "[ts-cloud] cannot read listener PIDs on this host — the response probes run without the bind check" >&2',"fi",...l?[`TS_CLOUD_HEALTHY=0; for TS_CLOUD_I in $(seq 1 ${li}); do if ${l}; then TS_CLOUD_HEALTHY=1; break; fi; sleep ${rn}; done`,`[ "$TS_CLOUD_HEALTHY" -eq 1 ] || ${u(`never answered ${c}`)}`]:[],...hn(t),`systemctl enable ${i} 2>/dev/null || true`,'for TS_CLOUD_U in ${TS_CLOUD_OLD_UNITS}; do systemctl stop "$TS_CLOUD_U" 2>/dev/null || true; systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done',`TS_CLOUD_LISTENING=0; for TS_CLOUD_I in $(seq 1 ${a}); do if ss -ltnH "sport = :${s}" 2>/dev/null | grep . >/dev/null; then TS_CLOUD_LISTENING=1; break; fi; sleep 1; done`,`if [ "$TS_CLOUD_LISTENING" -ne 1 ]; then echo "[ts-cloud] nothing is listening on ${s} after retiring the previous release — restarting ${i} on the now-free port" >&2; systemctl restart ${i}; for TS_CLOUD_I in $(seq 1 ${a}); do if ss -ltnH "sport = :${s}" 2>/dev/null | grep . >/dev/null; then TS_CLOUD_LISTENING=1; break; fi; sleep 1; done; fi`,`[ "$TS_CLOUD_LISTENING" -eq 1 ] || ${u(`is active but never bound :${s}`)}`,...l?[`TS_CLOUD_HEALTHY=0; for TS_CLOUD_I in $(seq 1 ${li}); do if ${l}; then TS_CLOUD_HEALTHY=1; break; fi; sleep ${rn}; done`,`[ "$TS_CLOUD_HEALTHY" -eq 1 ] || ${u(`stopped answering ${c} once it served alone`)}`]:[],...Ws(t),`systemctl list-unit-files --plain --no-legend "${r}@*.service" 2>/dev/null | awk '{print $1}' | { grep -v -e "^${i}$" -e "^${r}@\\.service$" || true; } | while read -r TS_CLOUD_U; do systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,`if [ -f /etc/systemd/system/${o} ]; then systemctl disable ${o} 2>/dev/null || true; rm -f /etc/systemd/system/${o}; systemctl daemon-reload; fi`]}function pi(e,t){if(!t.length)return[];let r=Ae(e.release);return[`if [ -e ${r} ] || [ -L ${r} ]; then`,...t,"fi"]}function on(e){let{siteName:t,slug:r,artifactFetch:n,releaseId:s,execStart:i,envEntries:o,port:a,keepReleases:c=_i,preStartCommands:l=[],postActivateCommands:u=[],beforeResetCommands:d=[],healthCheckPath:m,healthGateSeconds:h=5,liveness:f,memoryHigh:S="auto",memoryMax:x,cpuWeight:B,ioWeight:L,tasksMax:N,stopTimeout:D}=e,E=[...B!=null?[`CPUWeight=${B}`]:[],...L!=null?[`IOWeight=${L}`]:[],...N!=null?[`TasksMax=${N}`]:[],...D?[`TimeoutStopSec=${D}`]:[]],M=e.zeroDowntime??a!=null,H=e.appDir??`/var/www/${t}`,z=Ht(H,s),q=`${r}-${t}`,X=`${q}.service`,J=It(r,t,s),te=e.sharedPaths??[],Z=Jc([".env",...te,...Zt(o,te)]),Y=Jt(o),ie=ui(z.release,l,o),ce=["set -euo pipefail",...zs(z),...n,...$i(z,Z),...pi(z,d),Gs(z),...Hs(z),`tar xzf ${J} -C "$TS_CLOUD_STAGED"`,`rm -f ${J}`,`cat > ${z.shared}/.env <<'TS_CLOUD_ENV_EOF'`,Y,"TS_CLOUD_ENV_EOF",`chmod 600 ${z.shared}/.env`,...qs(z),`find ${z.release} -maxdepth 1 \\( -type f -o -type l \\) -name '.env*' ! -name '.env.example' ! -name '*.example' -delete`,`sed -i -E '/^[[:space:]]*(PORT|PORT_BACKEND|PORT_ADMIN|PORT_FRONTEND)[[:space:]]*=/d' ${z.shared}/.env 2>/dev/null || true`,..._o(z,Z),...ie,...e.isolation?Xs(e.slug||t,z.base,Z.flatMap((y)=>typeof y==="string"||!y.target?[]:[y.target])):[]];if(M&&a!=null){let y=`${q}@${s}.service`,k=m?m.startsWith("/")?m:`/${m}`:null;return[...ce,`cat > /etc/systemd/system/${q}@.service <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=${t} release %i (managed by ts-cloud)`,"After=network.target","","[Service]","Type=simple",...e.isolation?xt(st(e.slug||t),z.base,e.slug||t):[],`WorkingDirectory=${z.releases}/%i`,`ExecStart=${i}`,"Restart=always","RestartSec=5","MemoryAccounting=true",...S&&S!=="auto"?[`MemoryHigh=${S}`]:[],...x?[`MemoryMax=${x}`]:[],...E,`EnvironmentFile=${z.releases}/%i/.env`,`Environment=PORT=${a}`,`Environment=STX_IMAGE_CACHE_DIR=${xo(z)}`,`Environment=STX_BUNDLE_CACHE_DIR=${Oi(z)}`,`Environment=STX_BUNDLE_CACHE_DIR=${Oi(z)}`,"","[Install]","WantedBy=multi-user.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",...S==="auto"?ai(`${q}@.service`):[...tr(`${q}@.service`),...nn()],...Mc(y),...jc({paths:z,unitBase:q,releaseId:s,port:a,healthCheckPath:k,healthGateSeconds:h}),...u,...yn(z,c),...f===!1?[]:ci({...f,unitBase:q,unitPattern:`${q}@*.service`,port:a,path:f?.path??m})]}return[...ce,`cat > /etc/systemd/system/${X} <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=${t} (managed by ts-cloud)`,"After=network.target","","[Service]","Type=simple",...e.isolation?xt(st(e.slug||t),z.base,e.slug||t):[],`WorkingDirectory=${z.current}`,`ExecStart=${i}`,"Restart=always","RestartSec=5","MemoryAccounting=true",...S&&S!=="auto"?[`MemoryHigh=${S}`]:[],...x?[`MemoryMax=${x}`]:[],...E,`EnvironmentFile=${z.current}/.env`,...a?[`Environment=PORT=${a}`]:[],`Environment=STX_IMAGE_CACHE_DIR=${xo(z)}`,`Environment=STX_BUNDLE_CACHE_DIR=${Oi(z)}`,"","[Install]","WantedBy=multi-user.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",...tr(X,{auto:S!=="auto",high:S!=="auto",max:Boolean(x)}),...S==="auto"?ai(X):nn(),`systemctl enable ${X}`,`TS_CLOUD_PREV_CURRENT="$(readlink ${z.current} 2>/dev/null || true)"`,...hn(z),`systemctl restart ${X}`,`for TS_CLOUD_I in $(seq 1 ${Math.max(1,h)}); do sleep 1; systemctl is-active --quiet ${X} || { echo "[ts-cloud] release ${s} of ${X} did not stay up — restoring the previous release" >&2; journalctl -u ${X} -n 50 --no-pager >&2 || true; if [ -n "$TS_CLOUD_PREV_CURRENT" ] && [ -d "$TS_CLOUD_PREV_CURRENT" ] && [ "$(readlink -f "$TS_CLOUD_PREV_CURRENT")" != "$(readlink -f ${z.release})" ]; then ln -sfn "$TS_CLOUD_PREV_CURRENT" ${z.current}.tmp && mv -Tf ${z.current}.tmp ${z.current}; systemctl restart ${X} || true; fi; exit 1; }; done`,...Ws(z),...u,...yn(z,c),...f===!1||!a?[]:ci({...f,unitBase:q,unitPattern:X,port:a,path:f?.path??m})]}function an(e){let{siteName:t,artifactFetch:r,releaseId:n,keepReleases:s=_i,preStartCommands:i=[],postActivateCommands:o=[],beforeResetCommands:a=[]}=e,c=e.appDir??`/var/www/${t}`,l=Ht(c,n),u=It(e.slug,t,n),d=ui(l.release,i);return["set -euo pipefail",...zs(l),...r,...$i(l,[]),...pi(l,a),Gs(l),...Hs(l),`tar xzf ${u} -C "$TS_CLOUD_STAGED"`,...qs(l),`rm -f ${u}`,...d,...hn(l),...o,...yn(l,s)]}function It(e,t,r){return`/tmp/${[e,t,r].filter(Boolean).join("-")}-release.tar.gz`}function fn(e,t,r,n){return[`aws s3 cp "s3://${e}/${t}" ${n} --region ${r}`]}function gn(e,t){return[`mv "${e}" ${t}`]}function Ae(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function rr(e,t,r){return["set -euo pipefail",`chmod 600 ${Ae(e)}`,`mv -f -- ${Ae(e)} ${Ae(t)}`,`cp -- ${Ae(t)} ${Ae(r)}`].join(`
|
|
277
|
+
`)}function nr(e){return`rm -f -- ${Ae(e)} 2>/dev/null || true`}function Sn(e={}){let t=Math.max(1,e.appServers??e.instances??1),r=!!e.servicesServer||t>1,n=t>1||!!e.server?.loadBalancer;return{appServers:t,loadBalancer:n,dedicatedServices:r,servicesOnApp:!r}}function di(e,t){let r={REDIS_HOST:e,MEILISEARCH_HOST:`http://${e}:7700`};if(t?.name){if(r.DB_CONNECTION=t.engine==="postgres"?"pgsql":"mysql",r.DB_HOST=e,r.DB_PORT=String(t.port??(t.engine==="postgres"?5432:3306)),r.DB_DATABASE=t.name,t.username)r.DB_USERNAME=t.username;if(t.password)r.DB_PASSWORD=t.password}return r}function vn(e){let t=e.infrastructure?.compute??{},r=A(e);return[...Do(t.managedServices??{mysql:!0,redis:!0},{bindPrivate:!0}),...wt(r,t.managedServices??{mysql:!0}),...Ee(!0),...Pe(!0),...Qe(t.sshKeys),...ze(e.notifications),...t.backups?.enabled?[...ko({database:r,backups:t.backups})]:[]]}var sr=["-S","ssh"];function mi(e={},t="ssh"){let r=[];if(e.identityFile)r.push("-i",e.identityFile);if(e.port)r.push(t==="scp"?"-P":"-p",String(e.port));return r.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR","-o",`ConnectTimeout=${e.connectTimeoutSec??10}`),r}async function wn(e,t,r={}){let n=Bun.spawn(["ssh",...mi(r),`${r.user??"root"}@${e}`,t],{stdout:"pipe",stderr:"pipe"}),[s,i]=await Promise.all([new Response(n.stdout).text(),new Response(n.stderr).text()]);return{code:await n.exited,stdout:s,stderr:i}}async function _e(e,t,r={}){let n=await wn(e,t,r);if(n.code!==0)throw Error(`ssh \`${t}\` on ${e} failed (${n.code}): ${n.stderr.trim()||n.stdout.trim()}`);return n.stdout}async function at(e,t={}){let r=t.timeoutMs??180000,n=t.pollIntervalMs??5000,s=Date.now();while(Date.now()-s<r){let i=await wn(e,"echo ready",t);if(i.code===0&&i.stdout.includes("ready"))return;await new Promise((o)=>setTimeout(o,n))}throw Error(`SSH not ready on ${e} after ${r}ms`)}function At(e={}){return["set -e","ts_cloud_script=$(mktemp /tmp/ts-cloud-deploy.XXXXXXXX)",`trap 'rm -f "$ts_cloud_script"' EXIT`,'cat > "$ts_cloud_script"',e.sudo?'sudo -n -H bash "$ts_cloud_script" < /dev/null':'bash "$ts_cloud_script" < /dev/null'].join(`
|
|
278
278
|
`)}var $n=At();import{homedir as Gc}from"node:os";import{join as Wc}from"node:path";var ir={location:"fsn1",image:"ubuntu-24.04",sshUser:"root",sshPrivateKeyPath:"~/.ssh/id_ed25519"};function ln(e){return e.startsWith("~/")?Wc(Gc(),e.slice(2)):e}function ct(...e){for(let t of e){let r=process.env[t]?.trim();if(r)return r}return}function lt(...e){for(let t of e){let r=t?.trim();if(r)return r}return}function Lt(e,t){return lt(e,t?.hetzner?.apiToken,ct("HCLOUD_TOKEN","HETZNER_API_TOKEN"))}function _n(e,t){return lt(t,e?.hetzner?.location,ct("HCLOUD_LOCATION","HETZNER_LOCATION"))??ir.location}function kt(e,t){let r=e?.infrastructure?.compute;return lt(t,r?.image,e?.hetzner?.image,ct("HCLOUD_IMAGE","HETZNER_IMAGE"))??ir.image}function qc(e,t){return lt(t,e?.hetzner?.sshUser,ct("HCLOUD_SSH_USER","HETZNER_SSH_USER"))??ir.sshUser}function fi(e,t){return ln(lt(t,e?.hetzner?.sshPrivateKeyPath,ct("HCLOUD_SSH_KEY","HETZNER_SSH_KEY"))??ir.sshPrivateKeyPath)}function Xc(e,t,r){let n=lt(t,e?.hetzner?.sshPublicKeyPath,ct("HCLOUD_SSH_PUBLIC_KEY","HETZNER_SSH_PUBLIC_KEY"));if(n)return ln(n);return`${r??fi(e)}.pub`}function St(e,t={}){let r=fi(e,t.sshPrivateKeyPath);return{apiToken:Lt(t.apiToken,e),location:_n(e,t.location),image:kt(e,t.image),sshUser:qc(e,t.sshUser),sshPrivateKeyPath:r,sshPublicKeyPath:Xc(e,t.sshPublicKeyPath,r)}}var Yc="https://api.hetzner.cloud/v1";class or extends Error{status;code;details;constructor(e,t,r,n){super(e);this.status=t;this.code=r;this.details=n;this.name="HetznerApiError"}}class ar extends Error{action;constructor(e,t){super(e);this.action=t;this.name="HetznerActionError"}}function gi(e){return e.map((t)=>({type:"server",server:{id:t.server}}))}class Oe{name="hetzner";apiToken;baseUrl;fetchImpl;constructor(e){this.apiToken=e.apiToken,this.baseUrl=e.baseUrl??Yc,this.fetchImpl=e.fetchImpl??fetch}async request(e,t,r){let n=await this.fetchImpl(`${this.baseUrl}${t}`,{method:e,headers:{Authorization:`Bearer ${this.apiToken}`,"Content-Type":"application/json"},body:r===void 0?void 0:JSON.stringify(r)}),s=await n.text(),i;try{i=s?JSON.parse(s):{}}catch{if(!n.ok){let o=s.trim().slice(0,200)||n.statusText||"Hetzner API error";throw Error(`Hetzner API ${e} ${t} (${n.status}): ${o}`)}throw Error(`Hetzner API ${e} ${t}: unexpected non-JSON response`)}if(!n.ok){let o=i.error?.message||n.statusText||"Hetzner API error",a=i.error?.code?` [${i.error.code}]`:"";throw new or(`Hetzner API ${e} ${t} (${n.status})${a}: ${o}`,n.status,i.error?.code,i.error?.details)}return i}async requestAll(e,t){let r=[],n=1;for(;;){let s=e.includes("?")?"&":"?",i=await this.request("GET",`${e}${s}per_page=50&page=${n}`);r.push(...i[t]??[]);let o=i.meta?.pagination?.next_page;if(!o)return r;n=o}}async listServers(){return this.requestAll("/servers","servers")}async getLocation(e){let r=(await this.request("GET",`/locations?name=${encodeURIComponent(e)}`)).locations.find((n)=>n.name===e);if(!r)throw Error(`Hetzner location not found: ${e}`);return r}async getServer(e){return(await this.request("GET",`/servers/${e}`)).server}async listVolumes(){return this.requestAll("/volumes","volumes")}async getVolume(e){return(await this.request("GET",`/volumes/${e}`)).volume}async createVolume(e){return this.request("POST","/volumes",{...e,automount:!1})}async attachVolume(e,t){return(await this.request("POST",`/volumes/${e}/actions/attach`,{server:t,automount:!1})).action}async resizeVolume(e,t){return(await this.request("POST",`/volumes/${e}/actions/resize`,{size:t})).action}async protectVolume(e,t){return(await this.request("POST",`/volumes/${e}/actions/change_protection`,{delete:t})).action}async getServerMetrics(e,t){let r=new URLSearchParams({type:t.types.join(","),start:t.start.toISOString(),end:t.end.toISOString(),step:String(Math.max(60,Math.floor(t.step??60)))});return(await this.request("GET",`/servers/${e}/metrics?${r}`)).metrics}async listServerTypes(e){let t=e?`?name=${encodeURIComponent(e)}`:"";return this.requestAll(`/server_types${t}`,"server_types")}async getServerType(e){return(await this.listServerTypes(e)).find((t)=>t.name===e)??null}async createServer(e){let t=await this.request("POST","/servers",{name:e.name,server_type:e.serverType,image:e.image,location:e.location,datacenter:e.datacenter,ssh_keys:e.sshKeys,user_data:e.userData,labels:e.labels,firewalls:e.firewalls,networks:e.networks,start_after_create:!0});return{server:t.server,action:t.action}}async listNetworks(){return this.requestAll("/networks","networks")}async createNetwork(e){let t=e.ipRange??"10.0.0.0/16";return(await this.request("POST","/networks",{name:e.name,ip_range:t,subnets:[{type:"cloud",ip_range:t,network_zone:"eu-central"}],labels:e.labels})).network}async deleteNetwork(e){await this.request("DELETE",`/networks/${e}`)}async listLoadBalancers(){return this.requestAll("/load_balancers","load_balancers")}async createLoadBalancer(e){return(await this.request("POST","/load_balancers",{name:e.name,load_balancer_type:e.type??"lb11",location:e.location,network_zone:e.network?void 0:e.networkZone??"eu-central",network:e.network,labels:e.labels,targets:[{type:"label_selector",label_selector:{selector:e.labelSelector},use_private_ip:!!e.network}],services:e.services.map((r)=>({protocol:r.protocol??"tcp",listen_port:r.listenPort,destination_port:r.destinationPort,health_check:{protocol:"http",port:80,interval:15,timeout:10,retries:3,http:{path:"/",status_codes:["2??","3??"]}}}))})).load_balancer}async deleteLoadBalancer(e){await this.request("DELETE",`/load_balancers/${e}`)}async deleteServer(e){return(await this.request("DELETE",`/servers/${e}`)).action}async listFirewalls(){return this.requestAll("/firewalls","firewalls")}async createFirewall(e){let t=await this.request("POST","/firewalls",{name:e.name,rules:e.rules,labels:e.labels,apply_to:e.applyTo?gi(e.applyTo):void 0});return{firewall:t.firewall,actions:t.actions}}async setFirewallRules(e,t){return(await this.request("POST",`/firewalls/${e}/actions/set_rules`,{rules:t})).actions??[]}async deleteFirewall(e){await this.request("DELETE",`/firewalls/${e}`)}async applyFirewallToResources(e,t){return(await this.request("POST",`/firewalls/${e}/actions/apply_to_resources`,{apply_to:gi(t)})).actions}async listSshKeys(){return this.requestAll("/ssh_keys","ssh_keys")}async createSshKey(e){return(await this.request("POST","/ssh_keys",{name:e.name,public_key:e.publicKey,labels:e.labels})).ssh_key}async deleteSshKey(e){await this.request("DELETE",`/ssh_keys/${e}`)}async getAction(e){return(await this.request("GET",`/actions/${e}`)).action}async shutdownServer(e){return(await this.request("POST",`/servers/${e}/actions/shutdown`,{})).action}async powerOnServer(e){return(await this.request("POST",`/servers/${e}/actions/poweron`,{})).action}async changeServerType(e,t,r){return(await this.request("POST",`/servers/${e}/actions/change_type`,{server_type:t,upgrade_disk:r})).action}async powerOffServer(e){return(await this.request("POST",`/servers/${e}/actions/poweroff`,{})).action}async stopServer(e,t){if((await this.getServer(e)).status==="off")return;await this.shutdownServer(e);let n=t?.gracefulWaitMs??120000,s=t?.pollIntervalMs??3000,i=Date.now();while(Date.now()-i<n){if((await this.getServer(e)).status==="off")return;await new Promise((a)=>setTimeout(a,s))}let o=await this.powerOffServer(e);await this.waitForAction(o.id),await this.waitForServerStatus(e,"off")}async renameServer(e,t){return(await this.request("PUT",`/servers/${e}`,{name:t})).server}async listImages(e){let t=e?.type?`?type=${encodeURIComponent(e.type)}`:"";return this.requestAll(`/images${t}`,"images")}async getImage(e){return(await this.request("GET",`/images/${e}`)).image}async createImage(e,t){return this.request("POST",`/servers/${e}/actions/create_image`,{description:t.description,type:t.type??"snapshot",labels:t.labels})}async deleteImage(e){await this.request("DELETE",`/images/${e}`)}async rebuildServer(e,t){return(await this.request("POST",`/servers/${e}/actions/rebuild`,{image:t})).action}async listPrimaryIps(){return this.requestAll("/primary_ips","primary_ips")}async unassignPrimaryIp(e){return(await this.request("POST",`/primary_ips/${e}/actions/unassign`,{})).action}async assignPrimaryIp(e,t){return(await this.request("POST",`/primary_ips/${e}/actions/assign`,{assignee_id:t,assignee_type:"server"})).action}async waitForAction(e,t){let r=t?.pollIntervalMs??2000,n=t?.maxWaitMs??300000,s=Date.now();while(Date.now()-s<n){let i=await this.getAction(e);if(i.status==="success")return i;if(i.status==="error")throw new ar(i.error?.message||"Hetzner action failed",i);await new Promise((o)=>setTimeout(o,r))}throw Error(`Timed out waiting for Hetzner action ${e}`)}async waitForServerRunning(e,t){let r=t?.pollIntervalMs??3000,n=t?.maxWaitMs??600000,s=Date.now();while(Date.now()-s<n){let i=await this.getServer(e);if(i.status==="running")return i;await new Promise((o)=>setTimeout(o,r))}throw Error(`Timed out waiting for server ${e} to reach running state`)}async waitForServerStatus(e,t,r){let n=r?.pollIntervalMs??3000,s=r?.maxWaitMs??600000,i=Date.now();while(Date.now()-i<s){let o=await this.getServer(e);if(o.status===t)return o;await new Promise((a)=>setTimeout(a,n))}throw Error(`Timed out waiting for server ${e} to reach ${t} state`)}}function Cr(e,t){let r=Lt(e,t);if(!r)throw Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");return r}function je(e){let[t,r]=e.trim().split(/\s+/);return r?`${t} ${r}`:t}function hi(e){if(!e)return"";return e.endsWith(`
|
|
279
279
|
`)?e:`${e}
|
|
280
280
|
`}function Ce(e,t={}){let r=t.scriptPath??"/var/lib/cloud/ts-cloud-bootstrap.sh",n=t.permissions??"0755",s=e.split(`
|
|
@@ -393,17 +393,17 @@ case "$root_fs" in
|
|
|
393
393
|
xfs) xfs_growfs / ;;
|
|
394
394
|
*) echo "Unsupported root filesystem: $root_fs" >&2; exit 1 ;;
|
|
395
395
|
esac`,e)}async function cl(e,t){let r=Buffer.from(t.join(`
|
|
396
|
-
`)).toString("base64");await _e(e.host,`printf %s ${r} | base64 -d | while read -r unit; do [ -z "$unit" ] || systemctl is-active --quiet "$unit" || systemctl start "$unit"; done`,e)}async function Ai(e,t,r,n={recovered:!1}){if(await at(e.host,{...e,timeoutMs:e.sshTimeoutMs??600000}),!n.recovered)await al(e);if(r)await cl(e,r.runningServices);let s=await un(e),i=[],o=r?pt(r.runningServices,s.runningServices):[],a=r?pt(r.releaseLinks,s.releaseLinks):[],c=r?pt(r.routeFragments,s.routeFragments):[],l=r?pt(r.routeIds,s.routeIds):[],u=r?.persistentData.map((D)=>D.split("=")[0])??[],d=s.persistentData.map((D)=>D.split("=")[0]),m=pt(u,d),h=r?pt(r.dataCatalog,s.dataCatalog):[],f=s.routeProbes.filter((D)=>!D.ok),S=n.recovered&&r?r.cpuCores:t.cores??0,x=n.recovered&&r?r.memoryBytes:ol(t),B=s.cpuCores>=S,L=s.memoryBytes>=x*0.9,N=n.recovered||s.rootDiskBytes>=(t.disk??0)*1e9*0.95;if(s.failedUnits.length>0)i.push(`Failed services: ${s.failedUnits.join(", ")}`);if(o.length>0)i.push(`Services did not return: ${o.join(", ")}`);if(a.length>0)i.push(`Release links changed: ${a.join(", ")}`);if(c.length>0)i.push(`Route manifests changed: ${c.join(", ")}`);if(l.length>0)i.push(`Routes disappeared: ${l.join(", ")}`);if(m.length>0)i.push(`Persistent data paths disappeared: ${m.join(", ")}`);if(h.length>0)i.push(`Databases or volumes disappeared: ${h.join(", ")}`);if(f.length>0)i.push(`Unhealthy routes: ${f.map((D)=>D.domain).join(", ")}`);if(!B)i.push(`Expected at least ${S} CPU cores, found ${s.cpuCores}`);if(!L)i.push(`Expected at least ${x} bytes of RAM`);if(!N)i.push(`Expected approximately ${t.disk??0} GB root disk`);return{ok:i.length===0,checks:{cpuCores:s.cpuCores,memoryBytes:s.memoryBytes,rootDiskBytes:s.rootDiskBytes,rootFilesystemBytes:s.rootFilesystemBytes,runningServices:s.runningServices.length,releaseLinks:s.releaseLinks.length,routeFragments:s.routeFragments.length,routes:s.routeIds.length,healthyRoutes:s.routeProbes.filter((D)=>D.ok).length,persistentDataPaths:s.persistentData.length,databasesAndVolumes:s.dataCatalog.length,failedUnits:s.failedUnits.length},failures:i}}import{mkdir as ll,readFile as ul,rename as pl,writeFile as dl}from"node:fs/promises";import{join as Si}from"node:path";var Rn="storage/cloud/state";function
|
|
397
|
-
`,"utf8"),await pl(s,n)}async function ge(e){return Ge(e)}async function vt(e,t){await mt(e,t)}async function En(e,t,r){let n=r.length?await e.listVolumes():[];return r.map((s)=>{let i=n.filter((l)=>l.name===s.name);if(i.length>1)throw Error(`Ambiguous volume name: ${s.name}`);let o=i[0];if(o&&o.server!==null&&o.server!==t.id)throw Error(`Volume ${o.name} belongs to another server; refusing to detach live storage`);let a=t.location?.name??t.datacenter?.location.name;if(o&&o.location.name!==a)throw Error(`Volume ${o.name} is in ${o.location.name}, server is in ${a}`);if(o&&o.size>s.size)throw Error(`Refusing to shrink volume ${o.name}: ${o.size} GB to ${s.size} GB`);let c=[];if(!o)c.push("create");else{if(o.server===null)c.push("attach");if(o.size<s.size)c.push("grow")}if(!o||o.protection.delete!==(s.deleteProtection!==!1))c.push("protect");return c.push("mount"),{declaration:s,existing:o,actions:c}})}async function xn(e,t,r){let n=[];for(let s of r){let i=s.existing,o=!i;if(!i){let a=await e.createVolume({name:s.declaration.name,size:s.declaration.size,server:t.id,labels:{...t.labels,"ts-cloud/volume":s.declaration.key}});await e.waitForAction(a.action.id);for(let c of a.next_actions??[])await e.waitForAction(c.id);i=await e.getVolume(a.volume.id)}if(i.server===null)await e.waitForAction((await e.attachVolume(i.id,t.id)).id),i=await e.getVolume(i.id);if(i.server!==t.id||i.location.name!==(t.location?.name??t.datacenter?.location.name))throw Error(`Volume attachment verification failed: ${i.name}`);if(i.linux_device!==`/dev/disk/by-id/scsi-0HC_Volume_${i.id}`)throw Error(`Unexpected Hetzner volume device: ${i.name}`);if(i.size<s.declaration.size)await e.waitForAction((await e.resizeVolume(i.id,s.declaration.size)).id);if(i.protection.delete!==(s.declaration.deleteProtection!==!1))await e.waitForAction((await e.protectVolume(i.id,s.declaration.deleteProtection!==!1)).id);n.push({...s.declaration,id:i.id,device:i.linux_device,allowFormat:o})}return n}var On=268435456;function Nt(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function Sl(e){let t=fl("sha256");for await(let r of gl(e))t.update(r);return t.digest("hex")}class
|
|
398
|
-
`))}if(l?.capacityAvailable&&!l.alreadyComplete){let f={host:m[0].publicIp,user:this.sshUser,identityFile:this.sshPrivateKeyPath},S=await Tr({client:this.client,serverId:a.id,targetType:l.targetType.name,upgradeDisk:l.upgradeDisk,hooks:{preflight:()=>wi(f),afterBoot:(x,B,L,N)=>Ai(f,B,L,N)}});if(S.status!=="completed"&&S.status!=="already-complete")throw Error(`Resize did not complete: ${S.status}`)}return u}constructor(e={}){this.residency=e.residency;let t=Lt(e.apiToken);if(this.stateOnly=!e.client&&!t&&e.allowStateOnly===!0,!e.client&&!t&&!this.stateOnly)throw Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");this.client=e.client??new Oe({apiToken:t??""});let r=St(void 0,{sshPrivateKeyPath:e.sshPrivateKeyPath,sshPublicKeyPath:e.sshPublicKeyPath,sshUser:e.sshUser,location:e.location});this.sshPrivateKeyPath=r.sshPrivateKeyPath,this.sshPublicKeyPath=r.sshPublicKeyPath,this.sshUser=r.sshUser,this.location=r.location,this.waitForBoot=e.waitForBoot??!0,this.bootWait={sshIntervalMs:e.bootWait?.sshIntervalMs??5000,sshTimeoutMs:e.bootWait?.sshTimeoutMs??300000,cloudInitIntervalMs:e.bootWait?.cloudInitIntervalMs??5000,cloudInitTimeoutMs:e.bootWait?.cloudInitTimeoutMs??600000}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=t.project.slug,s=t.infrastructure?.compute;if(!s)throw Error("infrastructure.compute is required to provision Hetzner compute");if(t.cloud?.attachTo)return this.attachToComputeInfrastructure(t.cloud.attachTo,t,r);let i=T(t,r),o=`${n}-${r}-app`,a=s.runtime==="php"||!!s.php,c=Sn(s),l=t.cloud?.residency||this.residency,{assertComputeResidency:u}=await import("./chunk-h28tfah9.js"),d=(y)=>{if(l)u(l,{country:y.location?.country||y.datacenter?.location?.country,location:y.location?.name||y.datacenter?.location?.name})};if(l){if(c.dedicatedServices||c.appServers>1)throw Error("Residency policy currently requires single-host compute");let y=await this.client.getLocation(t.hetzner?.location||this.location);u(l,{country:y.country,location:y.name})}if(c.dedicatedServices||c.appServers>1)return a?this.provisionFleet(e,c):this.provisionBunFleet(e,c);let m=ae(n,r,"app"),h=t.sites||{},f=ee(s)?[]:this.collectUpstreamPorts(h),S=`${n}-${r}-app-fw`,{firewall:x}=await this.ensureFirewall(S,m,dn({allowSsh:s.allowSsh!==!1,sitePorts:f,allowedPorts:[...s.firewall?.allowedPorts??[],...Rs(t.infrastructure?.sftp),...jr(ne(t,{environment:r}))]})),B=await this.ensureSshKey(n,r,m),L=await ge(i);if(L?.serverId){let y=await this.tryGetServer(L.serverId);if(y&&y.status!=="off"&&y.labels?.["ts-cloud/project"]===n&&y.labels?.["ts-cloud/environment"]===r){if(d(y),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(L,y)}}let N=await this.findExistingServer(n,r,o);if(N&&N.status!=="off"){d(N);let y={provider:"hetzner",stackName:i,serverId:N.id,serverName:N.name,publicIp:N.public_net.ipv4?.ip,publicIpv6:me(N.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};if(await vt(i,y),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(y,N)}let D=le(t,{environment:r}),E=D?.engine==="rpx"?$e({proxy:D,config:Te(h,{proxy:D,slug:t.project.slug,mail:ne(t,{environment:r})}),slug:t.project.slug,bunBin:s.runtime==="node"||s.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,M=ze(t,{environment:r}),H=s.bakedImage===!0,z=ye({runtime:M.runtime,runtimeVersion:M.runtimeVersion,systemPackages:s.systemPackages,database:t.infrastructure?.database,phpProvision:M.phpProvision,servicesProvision:M.servicesProvision,rpxProvision:E,baked:H,swapGb:s.swapGb}),q=Ce(z),X=tt(s.size),J=kt(t),{server:te,action:Z}=await this.client.createServer({name:o,serverType:X,image:J,location:t.hetzner?.location||this.location,userData:q,labels:m,sshKeys:B?[B]:void 0,firewalls:[{firewall:x.id}]});await this.client.waitForAction(Z.id);let Y=await this.client.waitForServerRunning(te.id);d(Y);let ie={provider:"hetzner",stackName:i,serverId:Y.id,serverName:Y.name,firewallId:x.id,publicIp:Y.public_net.ipv4?.ip,publicIpv6:me(Y.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};await vt(i,ie);let ce=Y.public_net.ipv4?.ip;if(ce&&this.waitForBoot)await this.waitForSshReady(ce),await this.waitForCloudInit(ce);if(s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(ie,Y)}async provisionFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=tt(i.size),c=kt(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=await ge(o);if(d?.loadBalancerId){let C=(await this.client.listLoadBalancers().catch(()=>[])).find((P)=>P.id===d.loadBalancerId);if(C)return{appPublicIp:C.public_net?.ipv4?.ip,loadBalancerIp:C.public_net?.ipv4?.ip,servicesPrivateIp:d.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}let m=await this.ensureSshKey(s,n,ae(s,n,"app")),h=`${s}-${n}-net`,S=(await this.client.listNetworks().catch(()=>[])).find((b)=>b.name===h)??await this.client.createNetwork({name:h,labels:ae(s,n,"app")}),{firewall:x}=await this.ensureFirewall(`${s}-${n}-app-fw`,ae(s,n,"app"),dn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),{firewall:B}=await this.ensureFirewall(`${s}-${n}-services-fw`,ae(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[S.ip_range]}]),L=vn(r),N=Ce(ye({runtime:"php",servicesProvision:L,baked:u,swapGb:i.swapGb})),D=await this.client.listServers().catch(()=>[]),E=[],M=D.find((b)=>Se(b.labels,s,n,"services"));if(!M){let{server:b,action:C}=await this.client.createServer({name:`${s}-${n}-services`,serverType:tt(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:N,labels:ae(s,n,"services"),sshKeys:m?[m]:void 0,firewalls:[{firewall:B.id}],networks:[S.id]});await this.client.waitForAction(C.id),M=await this.client.waitForServerRunning(b.id),E.push(b.id)}let H=M.id,z=M.private_net?.[0]?.ip??(await this.client.getServer(H)).private_net?.[0]?.ip,q=[...Ee(!0),...Pe(!0),...Qe(i.sshKeys),...Me(r.notifications)],X=Kt({versions:i.php?.versions,default:i.php?.default,extensions:i.php?.extensions,installNginx:!ee(i),optimizeForProduction:i.php?.optimizeForProduction,ini:i.php?.ini}),J=Ce(ye({runtime:"php",phpProvision:X,servicesProvision:q,baked:u,swapGb:i.swapGb})),te=D.filter((b)=>Se(b.labels,s,n,"app")),Z=te.map((b)=>b.id);if(te.length>t.appServers)for(let b of te.slice(t.appServers))await this.client.deleteServer(b.id).catch(()=>{}),Z.splice(Z.indexOf(b.id),1);for(let b=te.length;b<t.appServers;b++){let{server:C,action:P}=await this.client.createServer({name:`${s}-${n}-app-${b+1}`,serverType:a,image:c,location:l,userData:J,labels:ae(s,n,"app"),sshKeys:m?[m]:void 0,firewalls:[{firewall:x.id}],networks:[S.id]});await this.client.waitForAction(P.id),Z.push(C.id),E.push(C.id)}if(this.waitForBoot&&E.length>0)await Promise.all(E.map(async(b)=>{let P=(await this.client.waitForServerRunning(b)).public_net.ipv4?.ip;if(P)await this.waitForSshReady(P),await this.waitForCloudInit(P)}));let Y=`${s}-${n}-lb`,ie,ce;if(t.loadBalancer){let C=(await this.client.listLoadBalancers().catch(()=>[])).find((P)=>P.name===Y)??await this.client.createLoadBalancer({name:Y,location:l,network:S.id,labels:ae(s,n,"lb"),labelSelector:`ts-cloud/project=${s},ts-cloud/environment=${n},ts-cloud/role=app`,services:[{listenPort:80,destinationPort:80},{listenPort:443,destinationPort:443}]});ce=C.id,ie=C.public_net?.ipv4?.ip}let y=ie??(await this.client.getServer(Z[0]).catch(()=>{return}))?.public_net.ipv4?.ip,k={provider:"hetzner",stackName:o,networkId:S.id,loadBalancerId:ce,servicesServerId:H,servicesPrivateIp:z,publicIp:y,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await vt(o,k),{appPublicIp:y,loadBalancerIp:ie,servicesPrivateIp:z,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async provisionBunFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=tt(i.size),c=kt(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=r.sites||{},m=await ge(o);if(m?.lbServerId){let P=await this.tryGetServer(m.lbServerId);if(P&&P.status!=="off"){let G=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"},K=await this.findComputeTargets({slug:s,environment:n,role:"app",stackName:o});return this.refreshLbRoutes(P,d,K.map((j)=>({privateIp:j.privateIp,publicIp:j.publicIp})),G,s),{appPublicIp:P.public_net.ipv4?.ip??m.publicIp,loadBalancerIp:P.public_net.ipv4?.ip??m.publicIp,servicesPrivateIp:m.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}}let h=await this.ensureSshKey(s,n,ae(s,n,"app")),f=`${s}-${n}-net`,x=(await this.client.listNetworks().catch(()=>[])).find((P)=>P.name===f)??await this.client.createNetwork({name:f,labels:ae(s,n,"app")}),B=this.collectUpstreamPorts(d),{firewall:L}=await this.ensureFirewall(`${s}-${n}-app-fw`,ae(s,n,"app"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},...B.map((P)=>({direction:"in",protocol:"tcp",port:String(P),source_ips:[x.ip_range],description:`ts-cloud port ${P} (private, LB-only)`}))]),{firewall:N}=await this.ensureFirewall(`${s}-${n}-lb-fw`,ae(s,n,"lb"),dn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),D=await this.client.listServers().catch(()=>[]),E=[],M,H,z=!!i.servicesServer||!!i.managedServices,q=t.dedicatedServices&&z;if(q){let{firewall:P}=await this.ensureFirewall(`${s}-${n}-services-fw`,ae(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[x.ip_range]}]),G=vn(r),K=Ce(ye({runtime:"bun",servicesProvision:G,baked:u,swapGb:i.swapGb})),j=D.find((V)=>Se(V.labels,s,n,"services"));if(!j){let{server:V,action:se}=await this.client.createServer({name:`${s}-${n}-services`,serverType:tt(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:K,labels:ae(s,n,"services"),sshKeys:h?[h]:void 0,firewalls:[{firewall:P.id}],networks:[x.id]});await this.client.waitForAction(se.id),j=await this.client.waitForServerRunning(V.id),E.push(V.id)}M=j.id,H=j.private_net?.[0]?.ip??(await this.client.getServer(M)).private_net?.[0]?.ip}let X=q?{...i,managedServices:void 0,backups:void 0}:i,J=ze({...r,infrastructure:{...r.infrastructure,compute:X}},{environment:n}),te=Ce(ye({runtime:J.runtime,runtimeVersion:J.runtimeVersion,systemPackages:i.systemPackages,database:r.infrastructure?.database,servicesProvision:J.servicesProvision,baked:u,swapGb:i.swapGb})),Z=D.filter((P)=>Se(P.labels,s,n,"app")),Y=Z.map((P)=>P.id);if(Z.length>t.appServers)for(let P of Z.slice(t.appServers))await this.client.deleteServer(P.id).catch(()=>{}),Y.splice(Y.indexOf(P.id),1);for(let P=Z.length;P<t.appServers;P++){let{server:G,action:K}=await this.client.createServer({name:`${s}-${n}-app-${P+1}`,serverType:a,image:c,location:l,userData:te,labels:ae(s,n,"app"),sshKeys:h?[h]:void 0,firewalls:[{firewall:L.id}],networks:[x.id]});await this.client.waitForAction(K.id),Y.push(G.id),E.push(G.id)}if(this.waitForBoot&&E.length>0)await Promise.all(E.map(async(P)=>{let K=(await this.client.waitForServerRunning(P)).public_net.ipv4?.ip;if(K)await this.waitForSshReady(K),await this.waitForCloudInit(K)}));let ie=[];for(let P of Y){let G=D.find((K)=>K.id===P);if(!G||!G.private_net?.[0]?.ip)G=await this.client.getServer(P).catch(()=>G);ie.push({privateIp:G?.private_net?.[0]?.ip,publicIp:G?.public_net.ipv4?.ip})}let ce=`${s}-${n}-lb`,y,k;if(t.loadBalancer){let P=D.find((K)=>Se(K.labels,s,n,"lb")),G=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"};if(!P){let K=$e({proxy:G,config:Je(d,ie,{proxy:G,slug:s}),slug:s,bunBin:J.runtime==="node"||J.runtime==="deno"?void 0:"/usr/local/bin/bun"}),j=Ce(ye({runtime:"bun",rpxProvision:K,baked:!1})),V=typeof i.loadBalancer==="object"?i.loadBalancer.size:void 0,{server:se,action:_}=await this.client.createServer({name:ce,serverType:tt(V??"micro"),image:c,location:l,userData:j,labels:ae(s,n,"lb"),sshKeys:h?[h]:void 0,firewalls:[{firewall:N.id}],networks:[x.id]});if(await this.client.waitForAction(_.id),P=await this.client.waitForServerRunning(se.id),this.waitForBoot){let F=P.public_net.ipv4?.ip;if(F)await this.waitForSshReady(F),await this.waitForCloudInit(F)}}else this.refreshLbRoutes(P,d,ie,G,s);y=P.id,k=P.public_net.ipv4?.ip}let b=k??(await this.client.getServer(Y[0]).catch(()=>{return}))?.public_net.ipv4?.ip,C={provider:"hetzner",stackName:o,networkId:x.id,lbServerId:y,appServerIds:Y,servicesServerId:M,servicesPrivateIp:H,publicIp:b,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await vt(o,C),{appPublicIp:b,loadBalancerIp:k,servicesPrivateIp:H,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=T(t,r),i=await ge(s),o=[],a=`${n}-${r}-lb`,l=(await this.client.listLoadBalancers().catch(()=>[])).find((L)=>L.name===a);if(l)try{await this.client.deleteLoadBalancer(l.id),o.push(`load balancer ${a}`)}catch{}let u=await this.client.listServers().catch(()=>[]),d=new Set;for(let L of u)if(Se(L.labels,n,r,"app")||Se(L.labels,n,r,"services")||Se(L.labels,n,r,"lb"))d.add(L.id);if(i?.serverId)d.add(i.serverId);if(i?.servicesServerId)d.add(i.servicesServerId);if(i?.lbServerId)d.add(i.lbServerId);for(let L of i?.appServerIds??[])d.add(L);for(let L of d)try{await this.client.deleteServer(L),o.push(`server ${L}`)}catch{}let m=await this.client.listFirewalls().catch(()=>[]);for(let L of[`${n}-${r}-app-fw`,`${n}-${r}-services-fw`,`${n}-${r}-lb-fw`]){let N=m.find((D)=>D.name===L);if(!N)continue;for(let D=0;D<12;D++)try{await this.client.deleteFirewall(N.id),o.push(`firewall ${L}`);break}catch{await this.sleep(3000)}}let h=`${n}-${r}-net`,S=(await this.client.listNetworks().catch(()=>[])).find((L)=>L.name===h);if(S)for(let L=0;L<12;L++)try{await this.client.deleteNetwork(S.id),o.push(`network ${h}`);break}catch{await this.sleep(3000)}let B=(await this.client.listSshKeys().catch(()=>[])).filter((L)=>Cn(L.labels,n,r));for(let L of B)try{await this.client.deleteSshKey(L.id),o.push(`ssh key ${L.name}`)}catch{}return await vt(s,{provider:"hetzner",stackName:s}).catch(()=>{}),{destroyed:o}}async getComputeOutputs(e){let t=T(e.config,e.environment),r=await ge(t);if(r?.serverId){let i=await this.tryGetServer(r.serverId);if(i)return this.outputsFromState(r,i)}if(r?.lbServerId){let o=(await this.tryGetServer(r.lbServerId))?.public_net.ipv4?.ip??r.publicIp;return{...this.outputsFromState(r),appPublicIp:o,loadBalancerIp:o}}if(r?.loadBalancerId||r?.servicesPrivateIp)return this.outputsFromState(r);let s=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)}))[0];return{deployStoragePath:"/var/ts-cloud/staging",appInstanceId:s?.id,appPublicIp:s?.publicIp,sshUser:this.sshUser}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)});if(t.length===0)throw Error("No Hetzner compute targets found for release upload");let s=`/var/ts-cloud/staging/${`${e.remoteKey.replace(/^releases\//,"").replace(/\.tar\.gz$/,"").replace(/\//g,"-")}-${bi()}.tar.gz`}`,i=await Sl(e.localPath),o=ot,a=`${o}/${i}.tar.gz`;for(let c of t){if(!c.publicIp)throw Error(`Target ${c.id} has no public IP for SCP upload`);let l=["set -euo pipefail",`mkdir -p ${Nt(o)} /var/ts-cloud/staging`,`test -s ${Nt(a)}`,`cp -- ${Nt(a)} ${Nt(s)}`].join(`
|
|
396
|
+
`)).toString("base64");await _e(e.host,`printf %s ${r} | base64 -d | while read -r unit; do [ -z "$unit" ] || systemctl is-active --quiet "$unit" || systemctl start "$unit"; done`,e)}async function Ai(e,t,r,n={recovered:!1}){if(await at(e.host,{...e,timeoutMs:e.sshTimeoutMs??600000}),!n.recovered)await al(e);if(r)await cl(e,r.runningServices);let s=await un(e),i=[],o=r?pt(r.runningServices,s.runningServices):[],a=r?pt(r.releaseLinks,s.releaseLinks):[],c=r?pt(r.routeFragments,s.routeFragments):[],l=r?pt(r.routeIds,s.routeIds):[],u=r?.persistentData.map((D)=>D.split("=")[0])??[],d=s.persistentData.map((D)=>D.split("=")[0]),m=pt(u,d),h=r?pt(r.dataCatalog,s.dataCatalog):[],f=s.routeProbes.filter((D)=>!D.ok),S=n.recovered&&r?r.cpuCores:t.cores??0,x=n.recovered&&r?r.memoryBytes:ol(t),B=s.cpuCores>=S,L=s.memoryBytes>=x*0.9,N=n.recovered||s.rootDiskBytes>=(t.disk??0)*1e9*0.95;if(s.failedUnits.length>0)i.push(`Failed services: ${s.failedUnits.join(", ")}`);if(o.length>0)i.push(`Services did not return: ${o.join(", ")}`);if(a.length>0)i.push(`Release links changed: ${a.join(", ")}`);if(c.length>0)i.push(`Route manifests changed: ${c.join(", ")}`);if(l.length>0)i.push(`Routes disappeared: ${l.join(", ")}`);if(m.length>0)i.push(`Persistent data paths disappeared: ${m.join(", ")}`);if(h.length>0)i.push(`Databases or volumes disappeared: ${h.join(", ")}`);if(f.length>0)i.push(`Unhealthy routes: ${f.map((D)=>D.domain).join(", ")}`);if(!B)i.push(`Expected at least ${S} CPU cores, found ${s.cpuCores}`);if(!L)i.push(`Expected at least ${x} bytes of RAM`);if(!N)i.push(`Expected approximately ${t.disk??0} GB root disk`);return{ok:i.length===0,checks:{cpuCores:s.cpuCores,memoryBytes:s.memoryBytes,rootDiskBytes:s.rootDiskBytes,rootFilesystemBytes:s.rootFilesystemBytes,runningServices:s.runningServices.length,releaseLinks:s.releaseLinks.length,routeFragments:s.routeFragments.length,routes:s.routeIds.length,healthyRoutes:s.routeProbes.filter((D)=>D.ok).length,persistentDataPaths:s.persistentData.length,databasesAndVolumes:s.dataCatalog.length,failedUnits:s.failedUnits.length},failures:i}}import{mkdir as ll,readFile as ul,rename as pl,writeFile as dl}from"node:fs/promises";import{join as Si}from"node:path";var Rn="storage/cloud/state";function Le(e=process.cwd()){return pa()?I(e,"state"):Si(e,Rn)}function dt(e,t=process.cwd()){return Si(Le(t),`${e}.json`)}async function Ge(e,t=process.cwd()){try{let r=await ul(dt(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function mt(e,t,r=process.cwd()){let n=dt(e,r);await ll(Le(r),{recursive:!0});let s=`${n}.${process.pid}.tmp`;await dl(s,`${JSON.stringify(t,null,2)}
|
|
397
|
+
`,"utf8"),await pl(s,n)}async function ge(e){return Ge(e)}async function vt(e,t){await mt(e,t)}async function En(e,t,r){let n=r.length?await e.listVolumes():[];return r.map((s)=>{let i=n.filter((l)=>l.name===s.name);if(i.length>1)throw Error(`Ambiguous volume name: ${s.name}`);let o=i[0];if(o&&o.server!==null&&o.server!==t.id)throw Error(`Volume ${o.name} belongs to another server; refusing to detach live storage`);let a=t.location?.name??t.datacenter?.location.name;if(o&&o.location.name!==a)throw Error(`Volume ${o.name} is in ${o.location.name}, server is in ${a}`);if(o&&o.size>s.size)throw Error(`Refusing to shrink volume ${o.name}: ${o.size} GB to ${s.size} GB`);let c=[];if(!o)c.push("create");else{if(o.server===null)c.push("attach");if(o.size<s.size)c.push("grow")}if(!o||o.protection.delete!==(s.deleteProtection!==!1))c.push("protect");return c.push("mount"),{declaration:s,existing:o,actions:c}})}async function xn(e,t,r){let n=[];for(let s of r){let i=s.existing,o=!i;if(!i){let a=await e.createVolume({name:s.declaration.name,size:s.declaration.size,server:t.id,labels:{...t.labels,"ts-cloud/volume":s.declaration.key}});await e.waitForAction(a.action.id);for(let c of a.next_actions??[])await e.waitForAction(c.id);i=await e.getVolume(a.volume.id)}if(i.server===null)await e.waitForAction((await e.attachVolume(i.id,t.id)).id),i=await e.getVolume(i.id);if(i.server!==t.id||i.location.name!==(t.location?.name??t.datacenter?.location.name))throw Error(`Volume attachment verification failed: ${i.name}`);if(i.linux_device!==`/dev/disk/by-id/scsi-0HC_Volume_${i.id}`)throw Error(`Unexpected Hetzner volume device: ${i.name}`);if(i.size<s.declaration.size)await e.waitForAction((await e.resizeVolume(i.id,s.declaration.size)).id);if(i.protection.delete!==(s.declaration.deleteProtection!==!1))await e.waitForAction((await e.protectVolume(i.id,s.declaration.deleteProtection!==!1)).id);n.push({...s.declaration,id:i.id,device:i.linux_device,allowFormat:o})}return n}var On=268435456;function Nt(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function Sl(e){let t=fl("sha256");for await(let r of gl(e))t.update(r);return t.digest("hex")}class ke{name="hetzner";usesCloudFormation=!1;client;sshPrivateKeyPath;sshPublicKeyPath;sshUser;location;stateOnly;residency;waitForBoot;bootWait;async applyComputeChanges(e){let{config:t,environment:r}=e;if(t.cloud?.attachTo)throw Error("Only the compute owner may reconcile server size and volumes");let n=t.infrastructure?.compute;if(n?.resize&&!n.size)throw Error("compute.resize requires an explicit compute.size");let s=Xr(t,r),i=Sn(n??{});if(i.appServers>1||i.dedicatedServices)throw Error("Managed compute volumes currently require a single app server");let o=await this.getComputeOutputs(e);if(!o.appInstanceId)throw Error("No existing compute server found; provision the server first");let a=await this.client.getServer(Number(o.appInstanceId)),c=await En(this.client,a,s),l=n?.resize&&n.size?await rt(this.client,a.id,tt(n.size),n.resize.upgradeDisk!==!1):void 0,u={serverId:String(a.id),changes:c.flatMap((f)=>f.actions.map((S)=>({resource:f.declaration.name,action:S,detail:`${f.declaration.size} GB at ${f.declaration.mountPath}`})))};if(n?.deploymentStorage)u.changes.push({resource:n.deploymentStorage.volume,action:"place-deployments",detail:`Reconcile release directories${n.deploymentStorage.includeBackups!==!1?" and deployment backups":""} on the volume; ${n.deploymentStorage.migrateExisting?"verify and migrate existing files":"refuse nonempty directories"}`});if(l&&!l.alreadyComplete)if(!l.capacityAvailable){if(n?.resize?.whenUnavailable!=="keep-current")throw Error(`${l.targetType.name} is unavailable in ${l.location}`);u.waitingForCapacity=`${l.targetType.name} unavailable in ${l.location}; keeping ${a.server_type.name} running`}else u.changes.push({resource:a.name,action:"resize",detail:`${a.server_type.name} to ${l.targetType.name}`});if(e.dryRun)return u;if(u.waitingForCapacity)console.warn(`[ts-cloud] ${u.waitingForCapacity}`);let d=await xn(this.client,a,c),m=[{id:String(a.id),name:a.name,publicIp:a.public_net.ipv4?.ip}],h=[...ze(t.notifications),...n?.volumes||n?.deploymentStorage?Yr({volumes:d,deploymentStorage:n?.deploymentStorage,webRoot:"/var/www"}):[],...n?.monitoring!==!1?Pe(n?.monitoring):[]];if(h.length){let f=await this.runRemoteDeploy({targets:m,commands:h,timeoutSeconds:7200,comment:"ts-cloud apply configured compute storage"});if(!f.success)throw Error(f.error||f.perInstance.map((S)=>S.error||S.output).join(`
|
|
398
|
+
`))}if(l?.capacityAvailable&&!l.alreadyComplete){let f={host:m[0].publicIp,user:this.sshUser,identityFile:this.sshPrivateKeyPath},S=await Tr({client:this.client,serverId:a.id,targetType:l.targetType.name,upgradeDisk:l.upgradeDisk,hooks:{preflight:()=>wi(f),afterBoot:(x,B,L,N)=>Ai(f,B,L,N)}});if(S.status!=="completed"&&S.status!=="already-complete")throw Error(`Resize did not complete: ${S.status}`)}return u}constructor(e={}){this.residency=e.residency;let t=Lt(e.apiToken);if(this.stateOnly=!e.client&&!t&&e.allowStateOnly===!0,!e.client&&!t&&!this.stateOnly)throw Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");this.client=e.client??new Oe({apiToken:t??""});let r=St(void 0,{sshPrivateKeyPath:e.sshPrivateKeyPath,sshPublicKeyPath:e.sshPublicKeyPath,sshUser:e.sshUser,location:e.location});this.sshPrivateKeyPath=r.sshPrivateKeyPath,this.sshPublicKeyPath=r.sshPublicKeyPath,this.sshUser=r.sshUser,this.location=r.location,this.waitForBoot=e.waitForBoot??!0,this.bootWait={sshIntervalMs:e.bootWait?.sshIntervalMs??5000,sshTimeoutMs:e.bootWait?.sshTimeoutMs??300000,cloudInitIntervalMs:e.bootWait?.cloudInitIntervalMs??5000,cloudInitTimeoutMs:e.bootWait?.cloudInitTimeoutMs??600000}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=t.project.slug,s=t.infrastructure?.compute;if(!s)throw Error("infrastructure.compute is required to provision Hetzner compute");if(t.cloud?.attachTo)return this.attachToComputeInfrastructure(t.cloud.attachTo,t,r);let i=T(t,r),o=`${n}-${r}-app`,a=s.runtime==="php"||!!s.php,c=Sn(s),l=t.cloud?.residency||this.residency,{assertComputeResidency:u}=await import("./chunk-h28tfah9.js"),d=(y)=>{if(l)u(l,{country:y.location?.country||y.datacenter?.location?.country,location:y.location?.name||y.datacenter?.location?.name})};if(l){if(c.dedicatedServices||c.appServers>1)throw Error("Residency policy currently requires single-host compute");let y=await this.client.getLocation(t.hetzner?.location||this.location);u(l,{country:y.country,location:y.name})}if(c.dedicatedServices||c.appServers>1)return a?this.provisionFleet(e,c):this.provisionBunFleet(e,c);let m=ae(n,r,"app"),h=t.sites||{},f=ee(s)?[]:this.collectUpstreamPorts(h),S=`${n}-${r}-app-fw`,{firewall:x}=await this.ensureFirewall(S,m,dn({allowSsh:s.allowSsh!==!1,sitePorts:f,allowedPorts:[...s.firewall?.allowedPorts??[],...Rs(t.infrastructure?.sftp),...jr(ne(t,{environment:r}))]})),B=await this.ensureSshKey(n,r,m),L=await ge(i);if(L?.serverId){let y=await this.tryGetServer(L.serverId);if(y&&y.status!=="off"&&y.labels?.["ts-cloud/project"]===n&&y.labels?.["ts-cloud/environment"]===r){if(d(y),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(L,y)}}let N=await this.findExistingServer(n,r,o);if(N&&N.status!=="off"){d(N);let y={provider:"hetzner",stackName:i,serverId:N.id,serverName:N.name,publicIp:N.public_net.ipv4?.ip,publicIpv6:me(N.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};if(await vt(i,y),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(y,N)}let D=le(t,{environment:r}),E=D?.engine==="rpx"?$e({proxy:D,config:Te(h,{proxy:D,slug:t.project.slug,mail:ne(t,{environment:r})}),slug:t.project.slug,bunBin:s.runtime==="node"||s.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,M=Fe(t,{environment:r}),H=s.bakedImage===!0,z=ye({runtime:M.runtime,runtimeVersion:M.runtimeVersion,systemPackages:s.systemPackages,database:t.infrastructure?.database,phpProvision:M.phpProvision,servicesProvision:M.servicesProvision,rpxProvision:E,baked:H,swapGb:s.swapGb}),q=Ce(z),X=tt(s.size),J=kt(t),{server:te,action:Z}=await this.client.createServer({name:o,serverType:X,image:J,location:t.hetzner?.location||this.location,userData:q,labels:m,sshKeys:B?[B]:void 0,firewalls:[{firewall:x.id}]});await this.client.waitForAction(Z.id);let Y=await this.client.waitForServerRunning(te.id);d(Y);let ie={provider:"hetzner",stackName:i,serverId:Y.id,serverName:Y.name,firewallId:x.id,publicIp:Y.public_net.ipv4?.ip,publicIpv6:me(Y.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};await vt(i,ie);let ce=Y.public_net.ipv4?.ip;if(ce&&this.waitForBoot)await this.waitForSshReady(ce),await this.waitForCloudInit(ce);if(s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(ie,Y)}async provisionFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=tt(i.size),c=kt(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=await ge(o);if(d?.loadBalancerId){let C=(await this.client.listLoadBalancers().catch(()=>[])).find((P)=>P.id===d.loadBalancerId);if(C)return{appPublicIp:C.public_net?.ipv4?.ip,loadBalancerIp:C.public_net?.ipv4?.ip,servicesPrivateIp:d.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}let m=await this.ensureSshKey(s,n,ae(s,n,"app")),h=`${s}-${n}-net`,S=(await this.client.listNetworks().catch(()=>[])).find((b)=>b.name===h)??await this.client.createNetwork({name:h,labels:ae(s,n,"app")}),{firewall:x}=await this.ensureFirewall(`${s}-${n}-app-fw`,ae(s,n,"app"),dn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),{firewall:B}=await this.ensureFirewall(`${s}-${n}-services-fw`,ae(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[S.ip_range]}]),L=vn(r),N=Ce(ye({runtime:"php",servicesProvision:L,baked:u,swapGb:i.swapGb})),D=await this.client.listServers().catch(()=>[]),E=[],M=D.find((b)=>Se(b.labels,s,n,"services"));if(!M){let{server:b,action:C}=await this.client.createServer({name:`${s}-${n}-services`,serverType:tt(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:N,labels:ae(s,n,"services"),sshKeys:m?[m]:void 0,firewalls:[{firewall:B.id}],networks:[S.id]});await this.client.waitForAction(C.id),M=await this.client.waitForServerRunning(b.id),E.push(b.id)}let H=M.id,z=M.private_net?.[0]?.ip??(await this.client.getServer(H)).private_net?.[0]?.ip,q=[...Ee(!0),...Pe(!0),...Qe(i.sshKeys),...ze(r.notifications)],X=Kt({versions:i.php?.versions,default:i.php?.default,extensions:i.php?.extensions,installNginx:!ee(i),optimizeForProduction:i.php?.optimizeForProduction,ini:i.php?.ini}),J=Ce(ye({runtime:"php",phpProvision:X,servicesProvision:q,baked:u,swapGb:i.swapGb})),te=D.filter((b)=>Se(b.labels,s,n,"app")),Z=te.map((b)=>b.id);if(te.length>t.appServers)for(let b of te.slice(t.appServers))await this.client.deleteServer(b.id).catch(()=>{}),Z.splice(Z.indexOf(b.id),1);for(let b=te.length;b<t.appServers;b++){let{server:C,action:P}=await this.client.createServer({name:`${s}-${n}-app-${b+1}`,serverType:a,image:c,location:l,userData:J,labels:ae(s,n,"app"),sshKeys:m?[m]:void 0,firewalls:[{firewall:x.id}],networks:[S.id]});await this.client.waitForAction(P.id),Z.push(C.id),E.push(C.id)}if(this.waitForBoot&&E.length>0)await Promise.all(E.map(async(b)=>{let P=(await this.client.waitForServerRunning(b)).public_net.ipv4?.ip;if(P)await this.waitForSshReady(P),await this.waitForCloudInit(P)}));let Y=`${s}-${n}-lb`,ie,ce;if(t.loadBalancer){let C=(await this.client.listLoadBalancers().catch(()=>[])).find((P)=>P.name===Y)??await this.client.createLoadBalancer({name:Y,location:l,network:S.id,labels:ae(s,n,"lb"),labelSelector:`ts-cloud/project=${s},ts-cloud/environment=${n},ts-cloud/role=app`,services:[{listenPort:80,destinationPort:80},{listenPort:443,destinationPort:443}]});ce=C.id,ie=C.public_net?.ipv4?.ip}let y=ie??(await this.client.getServer(Z[0]).catch(()=>{return}))?.public_net.ipv4?.ip,k={provider:"hetzner",stackName:o,networkId:S.id,loadBalancerId:ce,servicesServerId:H,servicesPrivateIp:z,publicIp:y,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await vt(o,k),{appPublicIp:y,loadBalancerIp:ie,servicesPrivateIp:z,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async provisionBunFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=tt(i.size),c=kt(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=r.sites||{},m=await ge(o);if(m?.lbServerId){let P=await this.tryGetServer(m.lbServerId);if(P&&P.status!=="off"){let G=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"},K=await this.findComputeTargets({slug:s,environment:n,role:"app",stackName:o});return this.refreshLbRoutes(P,d,K.map((j)=>({privateIp:j.privateIp,publicIp:j.publicIp})),G,s),{appPublicIp:P.public_net.ipv4?.ip??m.publicIp,loadBalancerIp:P.public_net.ipv4?.ip??m.publicIp,servicesPrivateIp:m.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}}let h=await this.ensureSshKey(s,n,ae(s,n,"app")),f=`${s}-${n}-net`,x=(await this.client.listNetworks().catch(()=>[])).find((P)=>P.name===f)??await this.client.createNetwork({name:f,labels:ae(s,n,"app")}),B=this.collectUpstreamPorts(d),{firewall:L}=await this.ensureFirewall(`${s}-${n}-app-fw`,ae(s,n,"app"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},...B.map((P)=>({direction:"in",protocol:"tcp",port:String(P),source_ips:[x.ip_range],description:`ts-cloud port ${P} (private, LB-only)`}))]),{firewall:N}=await this.ensureFirewall(`${s}-${n}-lb-fw`,ae(s,n,"lb"),dn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),D=await this.client.listServers().catch(()=>[]),E=[],M,H,z=!!i.servicesServer||!!i.managedServices,q=t.dedicatedServices&&z;if(q){let{firewall:P}=await this.ensureFirewall(`${s}-${n}-services-fw`,ae(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[x.ip_range]}]),G=vn(r),K=Ce(ye({runtime:"bun",servicesProvision:G,baked:u,swapGb:i.swapGb})),j=D.find((V)=>Se(V.labels,s,n,"services"));if(!j){let{server:V,action:se}=await this.client.createServer({name:`${s}-${n}-services`,serverType:tt(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:K,labels:ae(s,n,"services"),sshKeys:h?[h]:void 0,firewalls:[{firewall:P.id}],networks:[x.id]});await this.client.waitForAction(se.id),j=await this.client.waitForServerRunning(V.id),E.push(V.id)}M=j.id,H=j.private_net?.[0]?.ip??(await this.client.getServer(M)).private_net?.[0]?.ip}let X=q?{...i,managedServices:void 0,backups:void 0}:i,J=Fe({...r,infrastructure:{...r.infrastructure,compute:X}},{environment:n}),te=Ce(ye({runtime:J.runtime,runtimeVersion:J.runtimeVersion,systemPackages:i.systemPackages,database:r.infrastructure?.database,servicesProvision:J.servicesProvision,baked:u,swapGb:i.swapGb})),Z=D.filter((P)=>Se(P.labels,s,n,"app")),Y=Z.map((P)=>P.id);if(Z.length>t.appServers)for(let P of Z.slice(t.appServers))await this.client.deleteServer(P.id).catch(()=>{}),Y.splice(Y.indexOf(P.id),1);for(let P=Z.length;P<t.appServers;P++){let{server:G,action:K}=await this.client.createServer({name:`${s}-${n}-app-${P+1}`,serverType:a,image:c,location:l,userData:te,labels:ae(s,n,"app"),sshKeys:h?[h]:void 0,firewalls:[{firewall:L.id}],networks:[x.id]});await this.client.waitForAction(K.id),Y.push(G.id),E.push(G.id)}if(this.waitForBoot&&E.length>0)await Promise.all(E.map(async(P)=>{let K=(await this.client.waitForServerRunning(P)).public_net.ipv4?.ip;if(K)await this.waitForSshReady(K),await this.waitForCloudInit(K)}));let ie=[];for(let P of Y){let G=D.find((K)=>K.id===P);if(!G||!G.private_net?.[0]?.ip)G=await this.client.getServer(P).catch(()=>G);ie.push({privateIp:G?.private_net?.[0]?.ip,publicIp:G?.public_net.ipv4?.ip})}let ce=`${s}-${n}-lb`,y,k;if(t.loadBalancer){let P=D.find((K)=>Se(K.labels,s,n,"lb")),G=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"};if(!P){let K=$e({proxy:G,config:Je(d,ie,{proxy:G,slug:s}),slug:s,bunBin:J.runtime==="node"||J.runtime==="deno"?void 0:"/usr/local/bin/bun"}),j=Ce(ye({runtime:"bun",rpxProvision:K,baked:!1})),V=typeof i.loadBalancer==="object"?i.loadBalancer.size:void 0,{server:se,action:_}=await this.client.createServer({name:ce,serverType:tt(V??"micro"),image:c,location:l,userData:j,labels:ae(s,n,"lb"),sshKeys:h?[h]:void 0,firewalls:[{firewall:N.id}],networks:[x.id]});if(await this.client.waitForAction(_.id),P=await this.client.waitForServerRunning(se.id),this.waitForBoot){let F=P.public_net.ipv4?.ip;if(F)await this.waitForSshReady(F),await this.waitForCloudInit(F)}}else this.refreshLbRoutes(P,d,ie,G,s);y=P.id,k=P.public_net.ipv4?.ip}let b=k??(await this.client.getServer(Y[0]).catch(()=>{return}))?.public_net.ipv4?.ip,C={provider:"hetzner",stackName:o,networkId:x.id,lbServerId:y,appServerIds:Y,servicesServerId:M,servicesPrivateIp:H,publicIp:b,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await vt(o,C),{appPublicIp:b,loadBalancerIp:k,servicesPrivateIp:H,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=T(t,r),i=await ge(s),o=[],a=`${n}-${r}-lb`,l=(await this.client.listLoadBalancers().catch(()=>[])).find((L)=>L.name===a);if(l)try{await this.client.deleteLoadBalancer(l.id),o.push(`load balancer ${a}`)}catch{}let u=await this.client.listServers().catch(()=>[]),d=new Set;for(let L of u)if(Se(L.labels,n,r,"app")||Se(L.labels,n,r,"services")||Se(L.labels,n,r,"lb"))d.add(L.id);if(i?.serverId)d.add(i.serverId);if(i?.servicesServerId)d.add(i.servicesServerId);if(i?.lbServerId)d.add(i.lbServerId);for(let L of i?.appServerIds??[])d.add(L);for(let L of d)try{await this.client.deleteServer(L),o.push(`server ${L}`)}catch{}let m=await this.client.listFirewalls().catch(()=>[]);for(let L of[`${n}-${r}-app-fw`,`${n}-${r}-services-fw`,`${n}-${r}-lb-fw`]){let N=m.find((D)=>D.name===L);if(!N)continue;for(let D=0;D<12;D++)try{await this.client.deleteFirewall(N.id),o.push(`firewall ${L}`);break}catch{await this.sleep(3000)}}let h=`${n}-${r}-net`,S=(await this.client.listNetworks().catch(()=>[])).find((L)=>L.name===h);if(S)for(let L=0;L<12;L++)try{await this.client.deleteNetwork(S.id),o.push(`network ${h}`);break}catch{await this.sleep(3000)}let B=(await this.client.listSshKeys().catch(()=>[])).filter((L)=>Cn(L.labels,n,r));for(let L of B)try{await this.client.deleteSshKey(L.id),o.push(`ssh key ${L.name}`)}catch{}return await vt(s,{provider:"hetzner",stackName:s}).catch(()=>{}),{destroyed:o}}async getComputeOutputs(e){let t=T(e.config,e.environment),r=await ge(t);if(r?.serverId){let i=await this.tryGetServer(r.serverId);if(i)return this.outputsFromState(r,i)}if(r?.lbServerId){let o=(await this.tryGetServer(r.lbServerId))?.public_net.ipv4?.ip??r.publicIp;return{...this.outputsFromState(r),appPublicIp:o,loadBalancerIp:o}}if(r?.loadBalancerId||r?.servicesPrivateIp)return this.outputsFromState(r);let s=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)}))[0];return{deployStoragePath:"/var/ts-cloud/staging",appInstanceId:s?.id,appPublicIp:s?.publicIp,sshUser:this.sshUser}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)});if(t.length===0)throw Error("No Hetzner compute targets found for release upload");let s=`/var/ts-cloud/staging/${`${e.remoteKey.replace(/^releases\//,"").replace(/\.tar\.gz$/,"").replace(/\//g,"-")}-${bi()}.tar.gz`}`,i=await Sl(e.localPath),o=ot,a=`${o}/${i}.tar.gz`;for(let c of t){if(!c.publicIp)throw Error(`Target ${c.id} has no public IP for SCP upload`);let l=["set -euo pipefail",`mkdir -p ${Nt(o)} /var/ts-cloud/staging`,`test -s ${Nt(a)}`,`cp -- ${Nt(a)} ${Nt(s)}`].join(`
|
|
399
399
|
`);try{this.sshExec(c.publicIp,l);continue}catch{}let u=`${o}/.${i}-${bi()}.tmp`;try{this.scpToHost(c.publicIp,e.localPath,u),this.sshExec(c.publicIp,rr(u,a,s))}catch(d){try{this.sshExec(c.publicIp,nr(u))}catch{}throw d}}return{artifactRef:s}}async findComputeTargets(e){let{assertComputeResidency:t}=await import("./chunk-h28tfah9.js"),r=e.role||"app",n=(o)=>{if(this.residency)t(this.residency,{country:o.location?.country||o.datacenter?.location?.country,location:o.location?.name||o.datacenter?.location?.name});return{id:String(o.id),name:o.name,publicIp:o.public_net.ipv4?.ip,publicIpv6:me(o.public_net.ipv6?.ip),privateIp:o.private_net?.[0]?.ip,status:o.status}};if(r==="app"||r==="lb"||r==="services"){let o=await ge(e.stackName??`${e.slug}-${e.environment}`),a=[...new Set(r==="app"?[o?.serverId,...o?.appServerIds??[]]:r==="lb"?[o?.lbServerId]:[o?.servicesServerId])].filter((u)=>typeof u==="number");if(this.stateOnly){if(this.residency)throw Error("Residency requires fresh provider evidence; a local state pin alone is insufficient");if(a.length>0&&o?.publicIp)return a.map((u)=>({id:String(u),name:u===o.serverId?o.serverName:void 0,publicIp:o.publicIp,publicIpv6:o.publicIpv6,status:"running"}));if(r!=="app")return[];throw Error(`Hetzner API token required because '${e.stackName??`${e.slug}-${e.environment}`}' has no complete local compute state pin.`)}let c=await this.client.listServers();if(a.length>0){let u=[];for(let d of a){let m=c.find((h)=>h.id===d)??await this.tryGetServer(d);if(d===o?.serverId&&o.serverName&&m?.name!==o.serverName)continue;if(m&&m.status!=="off")u.push(m)}if(u.length>0)return u.map(n)}let l=c.filter((u)=>Se(u.labels,e.slug,e.environment,r));if(l.length>0)return l.map(n);if(r==="app"){let u=c.filter((d)=>d.status!=="off"&&d.labels?.[`${oe}/managed-by`]==="ts-cloud"&&d.labels?.[`${oe}/environment`]===e.environment&&d.labels?.[`${oe}/role`]==="app");if(u.length===1)return u.map(n)}return[]}if(this.stateOnly)throw Error(`Hetzner API token required to discover '${r}' compute targets.`);let i=(await this.client.listServers()).filter((o)=>Se(o.labels,e.slug,e.environment,r));if(i.length>0)return i.map(n);return[]}async attachToComputeInfrastructure(e,t,r){let n=await this.client.listServers(),s=(m)=>n.filter((h)=>h.status!=="off"&&Se(h.labels,e,r,m)),i=s("app");if(i.length===0)throw Error(`Cannot attach '${t.project.slug}' to '${e}': no running Hetzner app server is labeled for ${e}/${r}. Deploy the owner project first.`);let o=s("lb")[0],a=s("services")[0],c=i[0],l=o??c,u=T(t,r),d={provider:"hetzner",stackName:u,serverId:c.id,serverName:c.name,appServerIds:i.map((m)=>m.id),lbServerId:o?.id,servicesServerId:a?.id,servicesPrivateIp:a?.private_net?.[0]?.ip,publicIp:l.public_net.ipv4?.ip,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await vt(u,d),this.outputsFromState(d)}async listReachableResources(){return(await this.client.listServers()).map((t)=>({name:t.name,labels:t.labels}))}async runRemoteDeploy(e){if(e.targets.length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets provided"};let t=e.commands.join(`
|
|
400
|
-
`),r=[];for(let s of e.targets){if(!s.publicIp){r.push({instanceId:s.id,status:"Failed",error:"Missing public IP"});continue}try{let i=this.sshExec(s.publicIp,t);qe(i),r.push({instanceId:s.id,status:"Success",output:i})}catch(i){r.push({instanceId:s.id,status:"Failed",error:i.message})}}let n=r.every((s)=>s.status==="Success");return{success:n,instanceCount:e.targets.length,perInstance:r,error:n?void 0:
|
|
400
|
+
`),r=[];for(let s of e.targets){if(!s.publicIp){r.push({instanceId:s.id,status:"Failed",error:"Missing public IP"});continue}try{let i=this.sshExec(s.publicIp,t);qe(i),r.push({instanceId:s.id,status:"Success",output:i})}catch(i){r.push({instanceId:s.id,status:"Failed",error:i.message})}}let n=r.every((s)=>s.status==="Success");return{success:n,instanceCount:e.targets.length,perInstance:r,error:n?void 0:Me(r,"One or more SSH deploy commands failed")}}async ensureSshKey(e,t,r){if(!hl(this.sshPublicKeyPath))throw Error(`SSH public key not found at ${this.sshPublicKeyPath}. ts-cloud deploys to Hetzner over SSH and needs a public key to authorize on the server. Generate one (\`ssh-keygen -t ed25519\`) or set hetzner.sshPrivateKeyPath / HCLOUD_SSH_PUBLIC_KEY.`);let n=yl(this.sshPublicKeyPath,"utf8").trim(),s=je(n),o=(await this.client.listSshKeys()).find((c)=>je(c.public_key)===s);if(o)return o.id;return(await this.client.createSshKey({name:`${e}-${t}-deploy`,publicKey:n,labels:r})).id}async tryGetServer(e){try{return await this.client.getServer(e)}catch{return null}}refreshLbRoutes(e,t,r,n,s){let i=e.public_net.ipv4?.ip;if(!i){console.warn(`[ts-cloud] LB server '${e.name}' has no public IP — skipping the rpx route refresh; its routes may be stale.`);return}let o=r.filter((c)=>c.privateIp||c.publicIp);if(o.length===0){console.warn(`[ts-cloud] No routable app boxes found for LB '${e.name}' — leaving its current rpx routes untouched.`);return}let a=Je(t,o,{proxy:n,slug:s});this.sshExec(i,et({config:a,slug:s}).join(`
|
|
401
401
|
`))}async findExistingServer(e,t,r){let n=await this.client.listServers(),s=n.find((o)=>Se(o.labels,e,t,"app")||o.name===r);if(s)return s;let i=n.filter((o)=>o.status!=="off"&&o.labels?.[`${oe}/managed-by`]==="ts-cloud"&&o.labels?.[`${oe}/environment`]===t&&o.labels?.[`${oe}/role`]==="app");if(i.length===1){let o=i[0],a=o.labels?.[`${oe}/project`]??"unknown";return console.warn(`[ts-cloud] No server named '${r}' found; adopting existing ts-cloud app server '${o.name}' (project label '${a}') for project '${e}' — `+"updating it in place instead of provisioning a new server."),o}return}async ensureFirewall(e,t,r){let s=(await this.client.listFirewalls()).find((o)=>o.name===e);if(s)return await this.client.setFirewallRules(s.id,r),{firewall:s};let{firewall:i}=await this.client.createFirewall({name:e,labels:t,rules:r});return{firewall:i}}collectUpstreamPorts(e){let t=new Set;for(let r of Object.values(e))if(typeof r.port==="number")t.add(r.port);return[...t].filter((r)=>![80,443].includes(r))}async sleep(e){await new Promise((t)=>setTimeout(t,e))}async waitForSshReady(e){let{sshIntervalMs:t,sshTimeoutMs:r}=this.bootWait,n=Date.now(),s;while(Date.now()-n<r)try{vi(`ssh ${this.sshBaseArgs(e,["-o","ConnectTimeout=5"]).map(Nt).join(" ")} true`,{stdio:"pipe",maxBuffer:On});return}catch(i){s=i,await this.sleep(t)}throw Error(`Timed out waiting for SSH on ${e} after ${r}ms: ${s?.message??"unknown error"}`)}async waitForCloudInit(e){let{cloudInitIntervalMs:t,cloudInitTimeoutMs:r}=this.bootWait,n=Date.now();while(Date.now()-n<r){try{let s=this.sshExec(e,"if command -v cloud-init >/dev/null 2>&1; then cloud-init status --long 2>/dev/null || true; else echo 'status: done'; fi");if(/status:\s*error/.test(s))throw Error(`cloud-init reported an error on ${e}:
|
|
402
|
-
${s}`);if(/status:\s*(?:done|degraded)/.test(s))return}catch(s){if(s instanceof Error&&/cloud-init reported an error/.test(s.message))throw s}await this.sleep(t)}throw Error(`Timed out waiting for cloud-init to finish on ${e} after ${r}ms`)}outputsFromState(e,t){return{deployStoragePath:e.deployStoragePath||"/var/ts-cloud/staging",appInstanceId:e.serverId?String(e.serverId):void 0,appPublicIp:t?.public_net.ipv4?.ip||e.publicIp,appPublicIpv6:me(t?.public_net.ipv6?.ip)||e.publicIpv6,sshUser:e.sshUser||this.sshUser,servicesPrivateIp:e.servicesPrivateIp}}static SSH_HOST_KEY_OPTS=["-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"];static SSH_KEEPALIVE_OPTS=["-o","ConnectTimeout=30","-o","ServerAliveInterval=15","-o","ServerAliveCountMax=4"];sshBaseArgs(e,t=[]){return["-i",this.sshPrivateKeyPath,...
|
|
402
|
+
${s}`);if(/status:\s*(?:done|degraded)/.test(s))return}catch(s){if(s instanceof Error&&/cloud-init reported an error/.test(s.message))throw s}await this.sleep(t)}throw Error(`Timed out waiting for cloud-init to finish on ${e} after ${r}ms`)}outputsFromState(e,t){return{deployStoragePath:e.deployStoragePath||"/var/ts-cloud/staging",appInstanceId:e.serverId?String(e.serverId):void 0,appPublicIp:t?.public_net.ipv4?.ip||e.publicIp,appPublicIpv6:me(t?.public_net.ipv6?.ip)||e.publicIpv6,sshUser:e.sshUser||this.sshUser,servicesPrivateIp:e.servicesPrivateIp}}static SSH_HOST_KEY_OPTS=["-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"];static SSH_KEEPALIVE_OPTS=["-o","ConnectTimeout=30","-o","ServerAliveInterval=15","-o","ServerAliveCountMax=4"];sshBaseArgs(e,t=[]){return["-i",this.sshPrivateKeyPath,...ke.SSH_HOST_KEY_OPTS,...ke.SSH_KEEPALIVE_OPTS,"-o","BatchMode=yes",...t,`${this.sshUser}@${e}`]}scpToHost(e,t,r){let n=["scp",...sr,"-i",this.sshPrivateKeyPath,...ke.SSH_HOST_KEY_OPTS,...ke.SSH_KEEPALIVE_OPTS,"-o","BatchMode=yes",t,`${this.sshUser}@${e}:${r}`].map((i)=>`"${i.replace(/"/g,"\\\"")}"`).join(" "),s=3;for(let i=1;i<=3;i++)try{vi(n,{stdio:"pipe",maxBuffer:On});return}catch(o){if(i===3)throw Error(Ue(o))}}sshExec(e,t){try{return ml("ssh",[...this.sshBaseArgs(e),$n],{encoding:"utf8",input:t,stdio:["pipe","pipe","pipe"],maxBuffer:On})}catch(r){throw Error(`${Ue(r)}${Pt(r,t)}`)}}}class lr{name="ssh";usesCloudFormation=!1;async getComputeOutputs(){return{appPublicIp:"127.0.0.1",sshUser:"root"}}async uploadRelease(e){return{artifactRef:e.localPath}}async findComputeTargets(e){if((e.role||"app")!=="app")return[];return[{id:"localhost",name:"localhost",publicIp:"127.0.0.1",status:"running"}]}async runRemoteDeploy(e){let t=e.commands.join(`
|
|
403
403
|
`);try{let r=Bun.spawn(["bash","-c",t],{stdout:"pipe",stderr:"pipe",env:process.env}),[n,s,i]=await Promise.all([new Response(r.stdout).text(),new Response(r.stderr).text(),r.exited]);return{success:i===0,instanceCount:1,perInstance:[{instanceId:"localhost",status:i===0?"Success":"Failed",output:n,error:s}]}}catch(r){return{success:!1,instanceCount:1,perInstance:[{instanceId:"localhost",status:"Failed",error:r?.message??String(r)}],error:r?.message??String(r)}}}}function ur(){let e=process.env.TS_CLOUD_DASHBOARD_BOX;return e!=null&&e!==""&&e!=="0"&&e.toLowerCase()!=="false"}import{createHash as Al,randomUUID as to}from"node:crypto";import{createReadStream as Ll}from"node:fs";import{isIP as kl}from"node:net";import{createHash as vl}from"node:crypto";import{appendFile as bl,mkdir as wl,readFile as $l}from"node:fs/promises";import{dirname as _l}from"node:path";var Ti=268435456,Ci=async(e,t)=>{let r=Bun.spawn(e,{stdin:t===void 0?"ignore":Buffer.from(t),stdout:"pipe",stderr:"pipe"}),[n,s,i]=await Promise.all([r.exited,new Response(r.stdout).text(),new Response(r.stderr).text()]);return{code:n,signal:r.signalCode,stdout:s.slice(-Ti),stderr:i.slice(-Ti)}},xi=["-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"],Di=["-o","ConnectTimeout=30","-o","ServerAliveInterval=15","-o","ServerAliveCountMax=4"];function Li(e){if(e.hostKey==="insecure")return[...xi];return["-o",`StrictHostKeyChecking=${e.hostKey.policy==="pin"?"yes":"accept-new"}`,"-o",`UserKnownHostsFile=${e.hostKey.knownHostsFile}`,"-o","LogLevel=ERROR"]}function Hi(e){let t=[...Di];if(e.connectTimeoutSec!==void 0)t[1]=`ConnectTimeout=${e.connectTimeoutSec}`;return t}function Dn(e,t,r=[]){return["-i",e.identityFile,"-p",String(e.port??22),...Li(e),...Hi(e),"-o","BatchMode=yes",...r,`${e.user}@${t}`]}function Ui(e,t,r,n){return["-i",e.identityFile,"-P",String(e.port??22),...Li(e),...Hi(e),"-o","BatchMode=yes",r,`${e.user}@${t}:${n}`]}function Er(e){return`SHA256:${vl("sha256").update(Buffer.from(e,"base64")).digest("base64").replace(/=+$/,"")}`}async function yo(e,t=22,r=Ci){let n=await r(["ssh-keyscan","-T","10","-p",String(t),"-t","ed25519",e]),s=n.stdout.split(`
|
|
404
404
|
`).find((i)=>i&&!i.startsWith("#"))?.trim().split(/\s+/);if(n.code!==0||!s?.[1]||!s[2])throw Error(`SSH host-key scan failed: ${n.stderr.trim()||"no key returned"}`);return{algorithm:s[1],key:s[2]}}function In(e,t=22){return t===22?e:`[${e}]:${t}`}function Mi(e,t,r){return`${In(e,t)} ${r.algorithm} ${r.key}`}function zi(e,t,r=22){let n=In(t,r);for(let s of e.split(`
|
|
405
405
|
`)){let i=s.trim();if(!i||i.startsWith("#")||i.startsWith("@"))continue;let[o,a,c]=i.split(/\s+/);if(!o||!a||!c)continue;if(o.split(",").includes(n))return{algorithm:a,key:c}}return null}function Ri(e=process.cwd()){return I(e,"ssh","known_hosts")}async function Tl(e){try{return await $l(e,"utf8")}catch{return""}}async function Cl(e,t){await wl(_l(e),{recursive:!0}),await bl(e,`${t}
|
|
406
|
-
`,{mode:384})}class ft{options;run;scan;readKnownHosts;appendKnownHosts;constructor(e,t={}){this.options=e;this.run=t.run??Ci,this.scan=t.scan??yo,this.readKnownHosts=t.readKnownHosts??Tl,this.appendKnownHosts=t.appendKnownHosts??Cl}runnerFor(e={}){if(e.sudo!==void 0)return At({sudo:e.sudo});return this.options.runner??At()}async exec(e,t,r={}){let n=await this.run(["ssh",...Dn(this.options,e),this.runnerFor(r)],t);if(n.code!==0){let s={status:n.code,signal:n.signal,stdout:n.stdout,stderr:n.stderr};throw Error(`${
|
|
406
|
+
`,{mode:384})}class ft{options;run;scan;readKnownHosts;appendKnownHosts;constructor(e,t={}){this.options=e;this.run=t.run??Ci,this.scan=t.scan??yo,this.readKnownHosts=t.readKnownHosts??Tl,this.appendKnownHosts=t.appendKnownHosts??Cl}runnerFor(e={}){if(e.sudo!==void 0)return At({sudo:e.sudo});return this.options.runner??At()}async exec(e,t,r={}){let n=await this.run(["ssh",...Dn(this.options,e),this.runnerFor(r)],t);if(n.code!==0){let s={status:n.code,signal:n.signal,stdout:n.stdout,stderr:n.stderr};throw Error(`${Ue(s)}${Pt(s,t)}`)}return n.stdout}async scp(e,t,r){let n=["scp",...sr,...Ui(this.options,e,t,r)],s=3;for(let i=1;i<=3;i++){let o=await this.run(n);if(o.code===0)return;if(i===3)throw Error(Ue({status:o.code,signal:o.signal,stdout:o.stdout,stderr:o.stderr}))}}async probe(e){return(await this.run(["ssh",...Dn(this.options,e,["-o","ConnectTimeout=5"]),"true"])).code===0}async ensureHostKey(e){let t=this.options.port??22;if(this.options.hostKey==="insecure")try{return{fingerprint:Er((await this.scan(e,t,this.run)).key),pinnedNow:!1}}catch{return{fingerprint:"",pinnedNow:!1}}let{policy:r,knownHostsFile:n}=this.options.hostKey,s=zi(await this.readKnownHosts(n),e,t);if(s)return{fingerprint:Er(s.key),pinnedNow:!1};let i=await this.scan(e,t,this.run);if(r==="pin")return await this.appendKnownHosts(n,Mi(e,t,i)),{fingerprint:Er(i.key),pinnedNow:!0};return{fingerprint:Er(i.key),pinnedNow:!1}}}import{isIP as Pl}from"node:net";var Ne=1,An="/var/lib/ts-cloud";function Bi(e=Ne){return`${An}/bootstrap.v${e}`}var Ki=1;function Rl(e){return e==="aarch64"||e==="arm64"}function Fi(e){let t=e.lanIp?.trim()||e.facts?.lanIp?.trim();return t&&Pl(t)!==0?t:void 0}function pr(e,t,r){if(!Ni(e.infrastructure?.compute?.managedServices?.vitess))return;if(t==="raspberry-pi"||Rl(r))throw Error(`managedServices.vitess is enabled, but Vitess publishes only x86_64 release tarballs and this host is ${r??"a Raspberry Pi (arm64)"}. Drop managedServices.vitess or deploy it to an x86_64 host.`)}function We(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function El(){return`
|
|
407
407
|
# A Pi 5 has no battery-backed RTC and boots in 1970 until NTP answers. apt
|
|
408
408
|
# rejects Release files "from the future" and ACME rejects a skewed clock, so
|
|
409
409
|
# wait (bounded) for systemd-timesyncd before anything touches the network
|
|
@@ -434,16 +434,16 @@ systemctl restart systemd-journald
|
|
|
434
434
|
# The deploy user uploads releases as itself (scp), while the scripts run as
|
|
435
435
|
# root through sudo. setgid group-writable directories let both write.
|
|
436
436
|
install -d -m 2775 -g "$(id -gn ${We(e)})" /var/ts-cloud/artifacts /var/ts-cloud/staging
|
|
437
|
-
`}function Dl(e){let t=We(e),r=`/home/${e}`;return[`chown -R ${t}:"$(id -gn ${t})" ${We(`${r}/.ssh`)}`,`chmod 700 ${We(`${r}/.ssh`)}`]}function wr(e){let{config:t,environment:r,profile:n,sudoUser:s}=e;pr(t,n,e.facts?.arch);let i={...t.infrastructure?.compute,proxy:le(t,{environment:e.environment,environmentType:e.environment})},o=t.sites??{},a=t.project.slug,c=e.lan?{...e.lan.hostname?{hostname:e.lan.hostname}:{},tls:e.lan.tls??"local-ca",...Fi(e)?{ip:Fi(e)}:{}}:void 0,l=i.proxy?.engine==="rpx"?Te(o,{proxy:i.proxy,slug:a,lan:c,mail:ne(t,{environment:e.environment,environmentType:e.environment})}):void 0,u=i.proxy?.engine==="rpx"&&l?$e({proxy:i.proxy,config:l,slug:a,profile:n,bunBin:i.runtime==="node"||i.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,d=i.proxy?.engine==="rpx"&&l?et({config:l,slug:a}):void 0,m=s?`/home/${s}/.ssh/authorized_keys`:void 0,h=
|
|
437
|
+
`}function Dl(e){let t=We(e),r=`/home/${e}`;return[`chown -R ${t}:"$(id -gn ${t})" ${We(`${r}/.ssh`)}`,`chmod 700 ${We(`${r}/.ssh`)}`]}function wr(e){let{config:t,environment:r,profile:n,sudoUser:s}=e;pr(t,n,e.facts?.arch);let i={...t.infrastructure?.compute,proxy:le(t,{environment:e.environment,environmentType:e.environment})},o=t.sites??{},a=t.project.slug,c=e.lan?{...e.lan.hostname?{hostname:e.lan.hostname}:{},tls:e.lan.tls??"local-ca",...Fi(e)?{ip:Fi(e)}:{}}:void 0,l=i.proxy?.engine==="rpx"?Te(o,{proxy:i.proxy,slug:a,lan:c,mail:ne(t,{environment:e.environment,environmentType:e.environment})}):void 0,u=i.proxy?.engine==="rpx"&&l?$e({proxy:i.proxy,config:l,slug:a,profile:n,bunBin:i.runtime==="node"||i.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,d=i.proxy?.engine==="rpx"&&l?et({config:l,slug:a}):void 0,m=s?`/home/${s}/.ssh/authorized_keys`:void 0,h=Fe(t,{environment:r,authorizedKeysPath:m}),f=s?[...h.servicesProvision??[],...Dl(s)]:h.servicesProvision,S=[...new Set([...i.systemPackages??[],"psmisc","ca-certificates"])],x=i.swapGb??(n==="raspberry-pi"?Ki:void 0),B=ye({runtime:h.runtime,runtimeVersion:h.runtimeVersion,systemPackages:S,database:t.infrastructure?.database,phpProvision:h.phpProvision,servicesProvision:f,rpxProvision:u,swapGb:x}).replace(/^#!\/bin\/bash\nset -euo pipefail\n/,""),L=Bi(),N=l?.localCa?` lan=${l.localCa.hosts.join(",")} tls=local-ca`:c?` lan=${c.hostname??`${a}.local`} tls=${c.tls??"local-ca"}`:"",D=`#!/bin/bash
|
|
438
438
|
set -euo pipefail
|
|
439
|
-
# ts-cloud ssh bootstrap v${
|
|
439
|
+
# ts-cloud ssh bootstrap v${Ne} profile=${n}${N}
|
|
440
440
|
|
|
441
441
|
# Idempotence guard. This script may run again (a cloud-init rerun, a repeated
|
|
442
442
|
# adopt, a deploy): once this version has been applied, only the gateway's
|
|
443
443
|
# route fragment is refreshed, so a rerun cannot re-download the runtime or
|
|
444
444
|
# re-create swap on every boot.
|
|
445
445
|
if [ -e ${We(L)} ]; then
|
|
446
|
-
echo "[ts-cloud] bootstrap v${
|
|
446
|
+
echo "[ts-cloud] bootstrap v${Ne} already applied; refreshing the gateway fragment only"
|
|
447
447
|
`;if(d){let E=d[0]==="set -euo pipefail"?d.slice(1):d;D+=`${E.join(`
|
|
448
448
|
`)}
|
|
449
449
|
`}if(D+=` exit 0
|
|
@@ -452,9 +452,9 @@ fi
|
|
|
452
452
|
${B}`,D+=`
|
|
453
453
|
install -d -m 0755 ${We(An)}
|
|
454
454
|
date -u +%Y-%m-%dT%H:%M:%SZ > ${We(L)}
|
|
455
|
-
echo "[ts-cloud] bootstrap v${
|
|
455
|
+
echo "[ts-cloud] bootstrap v${Ne} applied (${n}); gateway fragment at ${he}/${a}.json"
|
|
456
456
|
`,l?.localCa)D+=`echo "[ts-cloud] LAN certificate authority for ${l.localCa.hosts.join(", ")} at ${Gt(l.localCa.dir)} (copy it to a client to trust this box)"
|
|
457
|
-
`;return D}var
|
|
457
|
+
`;return D}var He={user:"root",port:22,privateKeyPath:"~/.ssh/id_ed25519",hostKey:"pin",profile:"generic",publicIp:"auto"},ji=["pin","accept-new","insecure"],Gi=["raspberry-pi","generic"];function gt(...e){for(let t of e){let r=process.env[t]?.trim();if(r)return r}return}function Ut(...e){for(let t of e){let r=t?.trim();if(r)return r}return}function Wi(e,t){if(t?.length)return t;if(e?.ssh?.hosts?.length)return e.ssh.hosts;let r=gt("TS_CLOUD_SSH_HOST");if(r)return[{host:r}];throw Error("No ssh host configured. Set ssh.hosts in cloud.config.ts or TS_CLOUD_SSH_HOST.")}function Vi(e,t){return Ut(t,e?.user,gt("TS_CLOUD_SSH_USER"))??He.user}function qi(e,t){let r=[{source:"the explicit port",value:t},{source:`ssh.hosts[].port for ${e?.host??"the host"}`,value:e?.port},{source:"TS_CLOUD_SSH_PORT",value:gt("TS_CLOUD_SSH_PORT")}];for(let{source:n,value:s}of r){if(s===void 0||s==="")continue;let i=typeof s==="number"?s:Number(s);if(!Number.isInteger(i)||i<1||i>65535)throw Error(`Invalid SSH port from ${n}: ${String(s)}`);return i}return He.port}function Xi(e,t){return ln(Ut(t,e?.privateKeyPath,gt("TS_CLOUD_SSH_KEY"))??He.privateKeyPath)}function Yi(e,t){let r=Ut(t,e?.ssh?.hostKey,gt("TS_CLOUD_SSH_HOST_KEY"))??He.hostKey;if(!ji.includes(r))throw Error(`Invalid ssh.hostKey '${r}'. Use one of: ${ji.join(", ")}.`);return r}function Ji(e,t=He.user,r){return r??e?.ssh?.sudo??t!=="root"}function Zi(e,t){let r=Ut(t,e?.ssh?.profile,gt("TS_CLOUD_SSH_PROFILE"))??He.profile;if(!Gi.includes(r))throw Error(`Invalid ssh.profile '${r}'. Use one of: ${Gi.join(", ")}.`);return r}function Qi(e,t){return Ut(t,e?.ssh?.publicIp)??He.publicIp}function eo(e,t){return t??e?.ssh?.lan}function Ar(e,t={}){let r=Wi(e,t.hosts).map((n)=>({host:n.host,user:Vi(n,t.user),port:qi(n,t.port),privateKeyPath:Xi(n,t.privateKeyPath),role:n.role??"app"}));return{hosts:r,hostKey:Yi(e,t.hostKey),sudo:Ji(e,r[0]?.user,t.sudo),profile:Zi(e,t.profile),publicIp:Qi(e,t.publicIp),lan:eo(e,t.lan)}}var Ln=`set -u
|
|
458
458
|
. /etc/os-release 2>/dev/null || true
|
|
459
459
|
# Strings are stripped of quotes, backslashes and newlines rather than escaped:
|
|
460
460
|
# nothing reported here legitimately contains them, and a probe that cannot
|
|
@@ -492,9 +492,9 @@ ${e.trim().slice(-2000)}`);try{return JSON.parse(t)}catch(r){throw Error(`Prefli
|
|
|
492
492
|
${" ".repeat(t+10)}${r.remediation}`:n}).join(`
|
|
493
493
|
`)}var Ft="/var/ts-cloud/staging",Hn=ot,ro="TS_CLOUD_SSH_SKIP_BOOTSTRAP",Nl=["size","instances","appServers","servicesServer","loadBalancer","fleet","image","bakedImage","allowSsh"];function Mt(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function Hl(e){let t=Al("sha256");for await(let r of Ll(e))t.update(r);return t.digest("hex")}function no(e,t){if(e.cloud?.attachTo)throw Error("cloud.attachTo is not supported by the ssh provider: it deploys to the hosts in ssh.hosts and nothing else.");if(t.hosts.length>1)throw Error(`ssh.hosts lists ${t.hosts.length} hosts; multi-host ssh fleets are not supported yet.`);if(Ni(e.infrastructure?.compute?.managedServices?.vitess))pr(e,t.profile);let r=e.infrastructure?.compute??{};return Nl.filter((n)=>r[n]!==void 0).map((n)=>`infrastructure.compute.${n} has no effect with the ssh provider; the host is what it is.`)}class Ir{name="ssh";usesCloudFormation=!1;settings;cwd;bootWait;transportInstance;constructor(e={}){this.settings=Ar(void 0,{hosts:e.hosts,user:e.user,port:e.port,privateKeyPath:e.privateKeyPath,hostKey:e.hostKey,sudo:e.sudo,profile:e.profile,publicIp:e.publicIp,lan:e.lan}),this.cwd=e.cwd??process.cwd(),this.transportInstance=e.transport,this.bootWait={sshIntervalMs:e.bootWait?.sshIntervalMs??5000,sshTimeoutMs:e.bootWait?.sshTimeoutMs??300000,cloudInitIntervalMs:e.bootWait?.cloudInitIntervalMs??5000,cloudInitTimeoutMs:e.bootWait?.cloudInitTimeoutMs??600000}}get host(){if(this.settings.hosts.length>1)throw Error(`ssh.hosts lists ${this.settings.hosts.length} hosts; multi-host ssh fleets are not supported yet.`);return this.settings.hosts[0]}get transport(){if(!this.transportInstance){let e=this.host;this.transportInstance=new ft({user:e.user,port:e.port,identityFile:e.privateKeyPath,hostKey:this.settings.hostKey==="insecure"?"insecure":{policy:this.settings.hostKey,knownHostsFile:Ri(this.cwd)}})}return this.transportInstance}exec(e,t){return this.transport.exec(e,t,{sudo:this.settings.sudo})}async findComputeTargets(e){if((e.role||"app")!=="app")return[];let{host:t}=this.host;return[{id:t,name:t,publicIp:t,status:"running"}]}async getComputeOutputs(){let{host:e,user:t}=this.host;return{appInstanceId:e,appPublicIp:e,sshUser:t,deployStoragePath:Ft}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)});if(t.length===0)throw Error("No ssh compute targets found for release upload");let n=`${e.remoteKey.replace(/^releases\//,"").replace(/\.tar\.gz$/,"").replace(/\//g,"-")}-${to()}.tar.gz`,s=`${Ft}/${n}`,i=await Hl(e.localPath),o=`${Hn}/${i}.tar.gz`;for(let a of t){if(!a.publicIp)throw Error(`Target ${a.id} has no address for SCP upload`);let c=["set -euo pipefail",`mkdir -p ${Mt(Hn)} ${Mt(Ft)}`,`test -s ${Mt(o)}`,`cp -- ${Mt(o)} ${Mt(s)}`].join(`
|
|
494
494
|
`);try{await this.exec(a.publicIp,c);continue}catch{}let l=`${Hn}/.${i}-${to()}.tmp`;try{await this.transport.scp(a.publicIp,e.localPath,l),await this.exec(a.publicIp,rr(l,o,s))}catch(u){throw await this.exec(a.publicIp,nr(l)).catch(()=>{}),u}}return{artifactRef:s}}async runRemoteDeploy(e){if(e.targets.length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets provided"};let t=e.commands.join(`
|
|
495
|
-
`),r=[];for(let s of e.targets){if(!s.publicIp){r.push({instanceId:s.id,status:"Failed",error:"Missing address"});continue}try{let i=await this.exec(s.publicIp,t);qe(i),r.push({instanceId:s.id,status:"Success",output:i})}catch(i){r.push({instanceId:s.id,status:"Failed",error:i.message})}}let n=r.every((s)=>s.status==="Success");return{success:n,instanceCount:e.targets.length,perInstance:r,error:n?void 0:
|
|
496
|
-
${pn(l)}`);for(let f of l)console.warn(`[ts-cloud] ${f.severity}: ${f.message}`);if(c.cloudInitPresent&&/running/i.test(c.cloudInitStatus??""))await this.waitForCloudInit(n.host);let u=c.lanIp?.trim()||(kl(n.host)!==0?n.host:void 0),d=i?.provider==="ssh"?i.bootstrappedAt:void 0;if(!o){let f=wr({config:t,environment:r,profile:this.settings.profile,facts:c,sudoUser:n.user!=="root"?n.user:void 0,lan:this.settings.lan,lanIp:u});qe(await this.exec(n.host,f)),d=new Date().toISOString()}let m={provider:"ssh",stackName:s,host:n.host,sshUser:n.user,sshPort:n.port,hostKeyFingerprint:a.fingerprint||void 0,publicIp:this.settings.publicIp==="auto"?void 0:this.settings.publicIp,lanIp:c.lanIp||void 0,deployStoragePath:Ft,profile:this.settings.profile,bootstrapVersion:
|
|
497
|
-
${s}`);if(/status:\s*(?:done|degraded)/.test(s))return}catch(s){if(s instanceof Error&&/cloud-init reported an error/.test(s.message))throw s}await this.sleep(t)}throw Error(`Timed out waiting for cloud-init to finish on ${e} after ${r}ms`)}}function O(e){let t=e.provider??U(e.config);if(e.config.cloud?.residency&&(ur()||t!=="hetzner"))throw Error("Residency policy requires a Hetzner host with live provider evidence");if(ur())return new lr;switch(e.provider??U(e.config)){case"aws":return new qt({region:e.config.project.region});case"hetzner":return new
|
|
495
|
+
`),r=[];for(let s of e.targets){if(!s.publicIp){r.push({instanceId:s.id,status:"Failed",error:"Missing address"});continue}try{let i=await this.exec(s.publicIp,t);qe(i),r.push({instanceId:s.id,status:"Success",output:i})}catch(i){r.push({instanceId:s.id,status:"Failed",error:i.message})}}let n=r.every((s)=>s.status==="Success");return{success:n,instanceCount:e.targets.length,perInstance:r,error:n?void 0:Me(r,"One or more SSH deploy commands failed")}}targetHost(){return this.host.host}ensureHostKey(e=this.host.host){return this.transport.ensureHostKey(e)}async preflight(e=this.host.host){let t=await this.transport.exec(e,Ln,{sudo:!1}),r=kn(t);return{facts:r,findings:Nn(r,{user:this.host.user,profile:this.settings.profile})}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e;for(let f of no(t,this.settings))console.warn(`[ts-cloud] ${f}`);let n=this.host,s=T(t,r),i=await Ge(s,this.cwd),o=process.env[ro]==="1"&&i?.provider==="ssh"&&i.host===n.host&&i.bootstrapVersion===Ne,a=await this.ensureHostKey(n.host);if(a.pinnedNow)console.warn(`[ts-cloud] pinned ${n.host} host key ${a.fingerprint}`);await this.waitForSshReady(n.host);let{facts:c,findings:l}=await this.preflight(n.host);if(Rr(l))throw Error(`Preflight failed for ${n.host}; refusing to bootstrap it.
|
|
496
|
+
${pn(l)}`);for(let f of l)console.warn(`[ts-cloud] ${f.severity}: ${f.message}`);if(c.cloudInitPresent&&/running/i.test(c.cloudInitStatus??""))await this.waitForCloudInit(n.host);let u=c.lanIp?.trim()||(kl(n.host)!==0?n.host:void 0),d=i?.provider==="ssh"?i.bootstrappedAt:void 0;if(!o){let f=wr({config:t,environment:r,profile:this.settings.profile,facts:c,sudoUser:n.user!=="root"?n.user:void 0,lan:this.settings.lan,lanIp:u});qe(await this.exec(n.host,f)),d=new Date().toISOString()}let m={provider:"ssh",stackName:s,host:n.host,sshUser:n.user,sshPort:n.port,hostKeyFingerprint:a.fingerprint||void 0,publicIp:this.settings.publicIp==="auto"?void 0:this.settings.publicIp,lanIp:c.lanIp||void 0,deployStoragePath:Ft,profile:this.settings.profile,bootstrapVersion:Ne,bootstrappedAt:d};await mt(s,m,this.cwd);let h=await this.getComputeOutputs();if(ee(t.infrastructure?.compute)&&Br(this.settings.lan)==="local-ca")h.lanCaCertPath=Gt(),console.warn(`[ts-cloud] LAN certificate authority on ${n.host} at ${h.lanCaCertPath}. Fetch it with: scp ${n.user}@${n.host}:${h.lanCaCertPath} ./rpx-root-ca.crt`);return h}async destroyCompute(e){let t=T(e.config,e.environment),r=this.host;return console.warn(`[ts-cloud] forgot ${r.host} for ${t}; nothing was removed on the host (stop the systemd units and remove /var/ts-cloud and /etc/rpx by hand if you want it clean).`),await mt(t,{provider:"ssh",stackName:t,host:r.host,sshUser:r.user,sshPort:r.port},this.cwd).catch(()=>{}),{destroyed:[`local state for ${t}`]}}async sleep(e){await new Promise((t)=>setTimeout(t,e))}async waitForSshReady(e){let{sshIntervalMs:t,sshTimeoutMs:r}=this.bootWait,n=Date.now();do{if(await this.transport.probe(e))return;await this.sleep(t)}while(Date.now()-n<r);throw Error(`Timed out waiting for SSH on ${e} after ${r}ms`)}async waitForCloudInit(e){let{cloudInitIntervalMs:t,cloudInitTimeoutMs:r}=this.bootWait,n=Date.now();while(Date.now()-n<r){try{let s=await this.transport.exec(e,"if command -v cloud-init >/dev/null 2>&1; then cloud-init status --long 2>/dev/null || true; else echo 'status: done'; fi",{sudo:!1});if(/status:\s*error/.test(s))throw Error(`cloud-init reported an error on ${e}:
|
|
497
|
+
${s}`);if(/status:\s*(?:done|degraded)/.test(s))return}catch(s){if(s instanceof Error&&/cloud-init reported an error/.test(s.message))throw s}await this.sleep(t)}throw Error(`Timed out waiting for cloud-init to finish on ${e} after ${r}ms`)}}function O(e){let t=e.provider??U(e.config);if(e.config.cloud?.residency&&(ur()||t!=="hetzner"))throw Error("Residency policy requires a Hetzner host with live provider evidence");if(ur())return new lr;switch(e.provider??U(e.config)){case"aws":return new qt({region:e.config.project.region});case"hetzner":return new ke({apiToken:e.config.hetzner?.apiToken,allowStateOnly:Boolean(e.config.cloud?.attachTo),sshPrivateKeyPath:e.config.hetzner?.sshPrivateKeyPath,sshPublicKeyPath:e.config.hetzner?.sshPublicKeyPath,sshUser:e.config.hetzner?.sshUser,location:e.config.hetzner?.location,residency:e.config.cloud?.residency});case"ssh":return new Ir({hosts:e.config.ssh?.hosts,hostKey:e.config.ssh?.hostKey,sudo:e.config.ssh?.sudo,profile:e.config.ssh?.profile,publicIp:e.config.ssh?.publicIp,lan:e.config.ssh?.lan});case"fly":throw Error("Fly.io has no box to provision or reach over SSH: deploy it with deployToFly() from @stacksjs/ts-cloud.");default:throw Error(`Unknown cloud provider: ${e.provider??U(e.config)}`)}}class so{drivers=new Map;getDriver(e,t){let r=t??U(e),n=`${r}:${e.project.slug}:${e.project.region||"default"}:${JSON.stringify(e.cloud?.residency||null)}`,s=this.drivers.get(n);if(s)return s;let i=O({config:e,provider:r});return this.drivers.set(n,i),i}}var xf=new so;var io="/var/lib/ts-cloud/bootstrap.sh",bo={"raspberry-pi-os":"bootfs",ubuntu:"system-boot"},Ul=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/,Ml=/^[a-z_][a-z0-9_-]{0,31}$/,zl=/^[A-Z]{2}$/;function ve(e){return JSON.stringify(e)}function Fl(e){if(!Ul.test(e.hostname))throw Error(`Invalid hostname '${e.hostname}': use lowercase letters, digits and hyphens.`);if(!Ml.test(e.user))throw Error(`Invalid user '${e.user}': use a lowercase Unix user name.`);let t=e.publicKey.trim();if(!t||t.includes(`
|
|
498
498
|
`)||!/^(?:ssh|ecdsa|sk)-/.test(t))throw Error("publicKey must be one public key line, e.g. the contents of ~/.ssh/id_ed25519.pub.");if(e.wifi){if(!e.wifi.ssid)throw Error("wifi.ssid is required.");if(!e.wifi.passphrase)throw Error("wifi.passphrase is required.");if(!zl.test(e.wifi.country))throw Error(`wifi.country must be a two-letter regulatory domain such as DE or US, not '${e.wifi.country}'.`)}}function Bl(e,t){let r=[`hostname: ${ve(e.hostname)}`,"manage_etc_hosts: true","users:",` - name: ${ve(e.user)}`," ssh_authorized_keys:",` - ${ve(e.publicKey.trim())}`,` sudo: ${ve("ALL=(ALL) NOPASSWD:ALL")}`," shell: /bin/bash"," lock_passwd: true","ssh_pwauth: false"];if(e.timezone)r.push(`timezone: ${ve(e.timezone)}`);if(e.locale)r.push(`locale: ${ve(e.locale)}`);if(r.push("package_update: true"),t==="raspberry-pi-os")r.push("rpi:"," enable_ssh: true"," interfaces:"," spi: false"," i2c: false"," onewire: false"," remote_gpio: false"," serial:"," console: false"," hardware: false");return`${r.join(`
|
|
499
499
|
`)}
|
|
500
500
|
`}function Kl(e){let t=["version: 2","ethernets:"," eth0:"," match:",` name: ${ve("e*")}`," dhcp4: true"," dhcp6: true"," optional: true"];if(e.wifi)t.push("wifis:"," wlan0:"," dhcp4: true"," dhcp6: true"," optional: true",` regulatory-domain: ${ve(e.wifi.country)}`," access-points:",` ${ve(e.wifi.ssid)}:`,` password: ${ve(e.wifi.passphrase)}`);return`${t.join(`
|
|
@@ -534,8 +534,8 @@ console.log(JSON.stringify({
|
|
|
534
534
|
publicTcpPorts: [...new Set(publicTcpPorts)].sort((a, b) => a - b),
|
|
535
535
|
}))
|
|
536
536
|
`;function Xl(e){return`printf %s ${Buffer.from(e).toString("base64")} | base64 -d | bun -`}async function Ns(e,t){let r=co(e).join(`
|
|
537
|
-
`),n=`printf %s ${Buffer.from(r).toString("base64")} | base64 -d | bash`;await _e(t.host,n,t)}async function xs(e){let t=await _e(e.host,Xl(ql),e);return JSON.parse(t)}function _s(e,t){let r=[],n=new Set(t.firewallPorts),s=e.firewallPorts.filter((o)=>!n.has(o)),i=t.firewallPorts.filter((o)=>!e.firewallPorts.includes(o));if(!t.firewallActive)r.push("UFW is not active");if(s.length>0)r.push(`UFW is missing ports: ${s.join(", ")}`);if(i.length>0)r.push(`UFW has unexpected ports: ${i.join(", ")}`);if(t.passwordAuthentication)r.push("SSH password authentication remains enabled");if(t.rootPasswordLogin)r.push("SSH root password login remains enabled");if(!t.fail2banActive)r.push("fail2ban is not active");if(e.autoUpdates&&!t.unattendedUpgradesActive)r.push("unattended upgrades are not active");if(e.monitoring&&!t.metricsTimerActive)r.push("metrics timer is not active");if(e.monitoring&&!t.metricsSnapshotFresh)r.push("metrics snapshot is stale");if(e.swapGb>0&&t.swapBytes<e.swapGb*1073741824*0.9)r.push("configured swap is missing");if(t.failedUnits.length>0)r.push(`failed services: ${t.failedUnits.join(", ")}`);return r}import{mkdir as mr,readFile as Yl,rename as lo,rmdir as Jl,stat as Zl,unlink as Ql,writeFile as uo}from"node:fs/promises";import{join as fr}from"node:path";function Un(e){return fr(
|
|
538
|
-
`,"utf8"),await lo(r,t)}async function $s(e,t=1800000){let r=wo(e);await mr(
|
|
537
|
+
`),n=`printf %s ${Buffer.from(r).toString("base64")} | base64 -d | bash`;await _e(t.host,n,t)}async function xs(e){let t=await _e(e.host,Xl(ql),e);return JSON.parse(t)}function _s(e,t){let r=[],n=new Set(t.firewallPorts),s=e.firewallPorts.filter((o)=>!n.has(o)),i=t.firewallPorts.filter((o)=>!e.firewallPorts.includes(o));if(!t.firewallActive)r.push("UFW is not active");if(s.length>0)r.push(`UFW is missing ports: ${s.join(", ")}`);if(i.length>0)r.push(`UFW has unexpected ports: ${i.join(", ")}`);if(t.passwordAuthentication)r.push("SSH password authentication remains enabled");if(t.rootPasswordLogin)r.push("SSH root password login remains enabled");if(!t.fail2banActive)r.push("fail2ban is not active");if(e.autoUpdates&&!t.unattendedUpgradesActive)r.push("unattended upgrades are not active");if(e.monitoring&&!t.metricsTimerActive)r.push("metrics timer is not active");if(e.monitoring&&!t.metricsSnapshotFresh)r.push("metrics snapshot is stale");if(e.swapGb>0&&t.swapBytes<e.swapGb*1073741824*0.9)r.push("configured swap is missing");if(t.failedUnits.length>0)r.push(`failed services: ${t.failedUnits.join(", ")}`);return r}import{mkdir as mr,readFile as Yl,rename as lo,rmdir as Jl,stat as Zl,unlink as Ql,writeFile as uo}from"node:fs/promises";import{join as fr}from"node:path";function Un(e){return fr(Le(),`${e}-resize.json`)}function wo(e){return fr(Le(),`${e}-resize.lock`)}async function Os(e){try{return JSON.parse(await Yl(Un(e),"utf8"))}catch{return null}}async function Ct(e){let t=Un(e.stackName);await mr(Le(),{recursive:!0});let r=`${t}.${process.pid}.tmp`;await uo(r,`${JSON.stringify(e,null,2)}
|
|
538
|
+
`,"utf8"),await lo(r,t)}async function $s(e,t=1800000){let r=wo(e);await mr(Le(),{recursive:!0});try{await mr(r)}catch(n){if(Date.now()-(await Zl(r)).mtimeMs<=t)throw Error(`Another resize process holds ${r}.`);await lo(r,`${r}.stale-${Date.now()}`),await mr(r)}return await uo(fr(r,"owner.json"),`${JSON.stringify({pid:process.pid,acquiredAt:new Date().toISOString()},null,2)}
|
|
539
539
|
`,"utf8"),async()=>{await Ql(fr(r,"owner.json")).catch(()=>{}),await Jl(r).catch(()=>{})}}async function Mn(e,t){let r=je(t.publicKey),n=(await e.listSshKeys()).find((i)=>je(i.public_key)===r);if(n)return{id:n.id,name:n.name,created:!1};let s=await e.createSshKey({name:t.name,publicKey:t.publicKey.trim(),labels:t.labels});return{id:s.id,name:s.name,created:!0}}async function zn(e,t){let r=(await e.listFirewalls()).find((s)=>s.name===t.name);if(r)return await e.setFirewallRules(r.id,t.rules),{id:r.id,name:r.name,created:!1};let{firewall:n}=await e.createFirewall({name:t.name,rules:t.rules,labels:t.labels});return{id:n.id,name:n.name,created:!0}}async function Fn(e,t){let{waitForRunning:r=!0,...n}=t,s=(await e.listServers()).find((o)=>o.name===t.name),i=!s;if(!s)s=(await e.createServer(n)).server;if(r)s=await e.waitForServerRunning(s.id);return{server:s,created:i}}function Bn(e){let t=e.public_net.ipv4?.ip;if(!t)throw Error(`Hetzner server "${e.name}" (#${e.id}) has no public IPv4`);return t}function $o(e){let t=e.sshPublicKey.trim(),r=["#cloud-config","disable_root: false","ssh_authorized_keys:",` - ${t}`];if(e.bootstrapScript){let s=e.bootstrapScript.split(`
|
|
540
540
|
`).map((i)=>` ${i}`).join(`
|
|
541
541
|
`);r.push("write_files:"," - path: /var/lib/cloud/box-bootstrap.sh"," permissions: '0755'"," owner: root:root"," content: |",s)}if(r.push("runcmd:",` - mkdir -p /root/.ssh && grep -qF "${t}" /root/.ssh/authorized_keys 2>/dev/null || echo "${t}" >> /root/.ssh/authorized_keys`),e.bootstrapScript)r.push(" - [ bash, /var/lib/cloud/box-bootstrap.sh ]");return`${r.join(`
|