@stacksjs/ts-cloud 0.16.94 → 0.16.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/bin/{chunk-d892pf66.js → chunk-18c0bsep.js} +131 -120
  2. package/dist/bin/{chunk-x9pqayv4.js → chunk-s2hqcbeq.js} +1 -1
  3. package/dist/bin/{chunk-1g2bnvz2.js → chunk-zbxfpe5p.js} +1 -1
  4. package/dist/bin/cli.js +5 -5
  5. package/dist/bin/dashboard-server.js +1 -1
  6. package/dist/{chunk-bxn4n4qa.js → chunk-jcxm737x.js} +1 -1
  7. package/dist/{chunk-71p77ycz.js → chunk-ybhjh309.js} +119 -108
  8. package/dist/deploy/index.js +1 -1
  9. package/dist/drivers/index.js +1 -1
  10. package/dist/drivers/shared/compute-storage.d.ts +1 -0
  11. package/dist/drivers/shared/persistent-storage-agent.d.ts +3 -0
  12. package/dist/index.js +1 -1
  13. package/dist/ui/404.html +1 -1
  14. package/dist/ui/accept-invitation.html +1 -1
  15. package/dist/ui/access-denied.html +1 -1
  16. package/dist/ui/account/automation.html +5 -5
  17. package/dist/ui/account/security.html +2 -2
  18. package/dist/ui/applications/compose.html +6 -6
  19. package/dist/ui/applications/new.html +2 -2
  20. package/dist/ui/data/backups.html +6 -6
  21. package/dist/ui/data/services.html +6 -6
  22. package/dist/ui/data/volumes.html +6 -6
  23. package/dist/ui/index.html +6 -6
  24. package/dist/ui/infrastructure/topology.html +5 -5
  25. package/dist/ui/integrations.html +2 -2
  26. package/dist/ui/operations/alerts.html +6 -6
  27. package/dist/ui/operations/configuration.html +6 -6
  28. package/dist/ui/operations/jobs.html +5 -5
  29. package/dist/ui/operations/maintenance.html +6 -6
  30. package/dist/ui/operations/observability.html +6 -6
  31. package/dist/ui/operations/previews.html +5 -5
  32. package/dist/ui/operations/queue.html +6 -6
  33. package/dist/ui/operations/regions.html +6 -6
  34. package/dist/ui/operations/releases.html +5 -5
  35. package/dist/ui/operations/spend.html +5 -5
  36. package/dist/ui/operations/workloads.html +5 -5
  37. package/dist/ui/security.html +2 -2
  38. package/dist/ui/server/actions.html +6 -6
  39. package/dist/ui/server/activity.html +1 -1
  40. package/dist/ui/server/capacity.html +6 -6
  41. package/dist/ui/server/database.html +5 -5
  42. package/dist/ui/server/deployments.html +5 -5
  43. package/dist/ui/server/diagnostics.html +2 -2
  44. package/dist/ui/server/firewall.html +6 -6
  45. package/dist/ui/server/fleet.html +6 -6
  46. package/dist/ui/server/logs.html +6 -6
  47. package/dist/ui/server/metrics.html +5 -5
  48. package/dist/ui/server/security.html +2 -2
  49. package/dist/ui/server/services.html +1 -1
  50. package/dist/ui/server/sites.html +6 -6
  51. package/dist/ui/server/ssh-keys.html +6 -6
  52. package/dist/ui/server/team.html +6 -6
  53. package/dist/ui/server/terminal.html +2 -2
  54. package/dist/ui/serverless/alarms.html +6 -6
  55. package/dist/ui/serverless/assets.html +2 -2
  56. package/dist/ui/serverless/cost.html +2 -2
  57. package/dist/ui/serverless/data.html +6 -6
  58. package/dist/ui/serverless/deployments.html +1 -1
  59. package/dist/ui/serverless/firewall.html +1 -1
  60. package/dist/ui/serverless/functions.html +6 -6
  61. package/dist/ui/serverless/logs.html +6 -6
  62. package/dist/ui/serverless/metrics.html +2 -2
  63. package/dist/ui/serverless/queues.html +6 -6
  64. package/dist/ui/serverless/secrets.html +6 -6
  65. package/dist/ui/serverless/traces.html +6 -6
  66. package/dist/ui/serverless.html +6 -6
  67. package/package.json +3 -3
@@ -1,40 +1,40 @@
1
- import{ml,H,l,p,C,rt,Zi,R,ge,m,pe,gn,Nt,Bt,mr,fr,rs}from"./chunk-316cbffs.js";import{y}from"./chunk-cjqw4ns4.js";import{jr,Le}from"./chunk-93xknw91.js";import{o}from"./chunk-byt4re1c.js";import{Q,Kr,d}from"./chunk-zpkf2jt2.js";import{Io,Sd,Bp,xp,x,nr,z,W,bd,Cd,V,_e}from"./chunk-bzdfx427.js";import{Bi}from"./chunk-wggeew90.js";import{Yi}from"./chunk-x1k1sbgf.js";import{readFileSync as Is}from"node:fs";import{join as Qa}from"node:path";function Qg(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 en(e){return e.replace(/\.$/,"").toLowerCase()}function ts(e,t,r){let n=en(e.name),s=en(t),i=en(r),a=i===s?"@":i.endsWith(`.${s}`)?i.slice(0,-(s.length+1)):i;return n===i||n===a||a==="@"&&(n===""||n===s)}async function Vc(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 a=i.records.filter((g)=>g.type===s&&ts(g,t,r)),c=a.findIndex((g)=>g.content===n);if(a.length<=1||c===-1)return[];let f=[];for(let[g,h]of a.entries()){if(g===c)continue;let S=await e.deleteRecord(t,h);if(!S.success)f.push(`could not remove stale ${h.name} ${s} ${h.content}: ${S.message||"unknown provider error"}`)}return f}async function Hg(e,t,r){let n=await e.listRecords(t);if(!n.success)return{removed:[],warnings:[`could not list records for ${t}: ${n.message||"unknown provider error"}`]};let s=[],i=[];for(let a of n.records){if(a.type!=="A"&&a.type!=="AAAA"||!ts(a,t,r))continue;let c=await e.deleteRecord(t,a);if(c.success)s.push(a);else i.push(`could not remove ${a.type} ${r} ${a.content}: ${c.message||"unknown provider error"}`)}return{removed:s,warnings:i}}function ae(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 qc=new Set(["mail","smtp","imap","mx"]);function ei(e){return!qc.has(e.split(".")[0]??"")}async function Gg(e){let{provider:t,zone:r,fqdn:n,ipv4:s,ipv6:i,ttl:a=600}=e,c={published:[],warnings:[]},f=[{type:"A",content:s}];if(i&&ei(n))f.push({type:"AAAA",content:i});for(let{type:g,content:h}of f){let S=await t.upsertRecord(r,{name:n,type:g,content:h,ttl:a});if(S?.success===!1){c.warnings.push(`${n} → ${h} failed: ${S.error||S.message||"unknown error"}`);continue}if(c.warnings.push(...(await Vc(t,r,n,h,g)).map((_)=>`${n} cleanup: ${_}`)),await jc(t,r,n,h,g))c.published.push({fqdn:n,type:g,content:h});else c.warnings.push(`${n} → ${h} reported success at ${t.name} but no matching ${g} record exists — create it manually: ${g} ${n} → ${h}`)}return c}async function jc(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((a)=>{let c=typeof a?.name==="string"?a.name.replace(/\.$/,""):"";return a?.type===s&&c===r&&a?.content===n})}catch{return!0}}function tn(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 ti(e,t){let r=tn(e?.stderr),n=/line (\d+): syntax error/.exec(r);if(!n)return"";let s=Number(n[1]),i=t.split(`
2
- `),a=Math.max(1,s-12),c=Math.min(i.length,s+2),f=i.slice(a-1,c).map((g,h)=>{let S=a+h,_=/^[A-Z_][A-Z0-9_]*=/.test(g)?g.replace(/=.*/,"=<redacted>"):g;return` ${String(S).padStart(4)}${S===s?" >":" "} ${_}`});return["",`The deploy script bash rejected is ${i.length} lines; it stopped at ${s}.`,...f].join(`
1
+ import{ml,H,l,p,C,rt,Zi,R,ge,m,pe,gn,Nt,Bt,mr,fr,rs}from"./chunk-316cbffs.js";import{y}from"./chunk-cjqw4ns4.js";import{jr,Le}from"./chunk-93xknw91.js";import{o}from"./chunk-byt4re1c.js";import{Q,Kr,d}from"./chunk-zpkf2jt2.js";import{Io,Sd,Bp,xp,x,nr,z,W,bd,Cd,V,_e}from"./chunk-bzdfx427.js";import{Bi}from"./chunk-wggeew90.js";import{Yi}from"./chunk-x1k1sbgf.js";import{readFileSync as Is}from"node:fs";import{join as ec}from"node:path";function Qg(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 en(e){return e.replace(/\.$/,"").toLowerCase()}function ts(e,t,r){let n=en(e.name),s=en(t),i=en(r),a=i===s?"@":i.endsWith(`.${s}`)?i.slice(0,-(s.length+1)):i;return n===i||n===a||a==="@"&&(n===""||n===s)}async function Vc(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 a=i.records.filter((h)=>h.type===s&&ts(h,t,r)),c=a.findIndex((h)=>h.content===n);if(a.length<=1||c===-1)return[];let f=[];for(let[h,g]of a.entries()){if(h===c)continue;let v=await e.deleteRecord(t,g);if(!v.success)f.push(`could not remove stale ${g.name} ${s} ${g.content}: ${v.message||"unknown provider error"}`)}return f}async function Hg(e,t,r){let n=await e.listRecords(t);if(!n.success)return{removed:[],warnings:[`could not list records for ${t}: ${n.message||"unknown provider error"}`]};let s=[],i=[];for(let a of n.records){if(a.type!=="A"&&a.type!=="AAAA"||!ts(a,t,r))continue;let c=await e.deleteRecord(t,a);if(c.success)s.push(a);else i.push(`could not remove ${a.type} ${r} ${a.content}: ${c.message||"unknown provider error"}`)}return{removed:s,warnings:i}}function ae(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 qc=new Set(["mail","smtp","imap","mx"]);function ei(e){return!qc.has(e.split(".")[0]??"")}async function Gg(e){let{provider:t,zone:r,fqdn:n,ipv4:s,ipv6:i,ttl:a=600}=e,c={published:[],warnings:[]},f=[{type:"A",content:s}];if(i&&ei(n))f.push({type:"AAAA",content:i});for(let{type:h,content:g}of f){let v=await t.upsertRecord(r,{name:n,type:h,content:g,ttl:a});if(v?.success===!1){c.warnings.push(`${n} → ${g} failed: ${v.error||v.message||"unknown error"}`);continue}if(c.warnings.push(...(await Vc(t,r,n,g,h)).map((_)=>`${n} cleanup: ${_}`)),await jc(t,r,n,g,h))c.published.push({fqdn:n,type:h,content:g});else c.warnings.push(`${n} → ${g} reported success at ${t.name} but no matching ${h} record exists — create it manually: ${h} ${n} → ${g}`)}return c}async function jc(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((a)=>{let c=typeof a?.name==="string"?a.name.replace(/\.$/,""):"";return a?.type===s&&c===r&&a?.content===n})}catch{return!0}}function tn(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 ti(e,t){let r=tn(e?.stderr),n=/line (\d+): syntax error/.exec(r);if(!n)return"";let s=Number(n[1]),i=t.split(`
2
+ `),a=Math.max(1,s-12),c=Math.min(i.length,s+2),f=i.slice(a-1,c).map((h,g)=>{let v=a+g,_=/^[A-Z_][A-Z0-9_]*=/.test(h)?h.replace(/=.*/,"=<redacted>"):h;return` ${String(v).padStart(4)}${v===s?" >":" "} ${_}`});return["",`The deploy script bash rejected is ${i.length} lines; it stopped at ${s}.`,...f].join(`
3
3
  `)}function Yt(e){let t=e,r=typeof t?.status==="number"?` (exit ${t.status})`:"",n=t?.signal?` (signal ${t.signal})`:"",s=[tn(t?.stderr),tn(t?.stdout)].filter(Boolean).join(`
4
4
  `);return`Remote SSH command failed${r}${n}${s?`
5
5
  ${s}`:""}`}function _r(e){for(let t of e.split(`
6
- `)){let r=t.trim();if(!r.startsWith("[ts-cloud]"))continue;console.warn(r)}}var ns=4000,ea=3;function ta(e){return e.length>ns?`…
7
- ${e.slice(-ns)}`:e}function Zt(e,t){let r=e.filter((a)=>a.status!=="Success");if(r.length===0)return t;let n=r.slice(0,ea).map((a)=>{let c=(a.error||a.output||"").trim(),f=`${a.instanceId}: ${a.status}`;return c?`${f}
8
- ${ta(c)}`:f}),s=r.length-n.length,i=s>0?[`(+${s} more instance${s===1?"":"s"} failed)`]:[];return[t,...n,...i].join(`
9
- `)}function ra(e){let t=2166136261,r=16777619;for(let n=0;n<e.length;n++){let s=e.charCodeAt(n);t=Math.imul(t^s,16777619)>>>0,r=Math.imul(r+s,2246822507)>>>0}return`${t.toString(36)}${r.toString(36)}`.padEnd(12,"0").slice(0,12)}function Vg(e,t,r={}){let n=r.mode??"testing",s=r.maxAge??604800,i=r.mx?.length?r.mx:[e.hostname],a=`${["version: STSv1",`mode: ${n}`,...i.map((f)=>`mx: ${f}`),`max_age: ${s}`].join(`\r
6
+ `)){let r=t.trim();if(!r.startsWith("[ts-cloud]"))continue;console.warn(r)}}var ns=4000,ta=3;function ra(e){return e.length>ns?`…
7
+ ${e.slice(-ns)}`:e}function Zt(e,t){let r=e.filter((a)=>a.status!=="Success");if(r.length===0)return t;let n=r.slice(0,ta).map((a)=>{let c=(a.error||a.output||"").trim(),f=`${a.instanceId}: ${a.status}`;return c?`${f}
8
+ ${ra(c)}`:f}),s=r.length-n.length,i=s>0?[`(+${s} more instance${s===1?"":"s"} failed)`]:[];return[t,...n,...i].join(`
9
+ `)}function na(e){let t=2166136261,r=16777619;for(let n=0;n<e.length;n++){let s=e.charCodeAt(n);t=Math.imul(t^s,16777619)>>>0,r=Math.imul(r+s,2246822507)>>>0}return`${t.toString(36)}${r.toString(36)}`.padEnd(12,"0").slice(0,12)}function Vg(e,t,r={}){let n=r.mode??"testing",s=r.maxAge??604800,i=r.mx?.length?r.mx:[e.hostname],a=`${["version: STSv1",`mode: ${n}`,...i.map((f)=>`mx: ${f}`),`max_age: ${s}`].join(`\r
10
10
  `)}\r
11
- `,c=ra(a);return{host:`mta-sts.${t}`,body:a,id:c,txt:`v=STSv1; id=${c}`}}var qg=8461;function Jh(e,t=qg){return[`const policy = ${JSON.stringify(e.body)}`,"Bun.serve({",` port: ${t},`," hostname: '127.0.0.1',"," fetch(req) {"," const { pathname } = new URL(req.url)"," if (pathname !== '/.well-known/mta-sts.txt') return new Response('Not Found', { status: 404 })"," return new Response(policy, { headers: { 'content-type': 'text/plain; charset=utf-8' } })"," },","})"].join(`
12
- `)}var xt={php:"php.net",nginx:"nginx.org",composer:"getcomposer.org",mysql:"mysql.com",mariadb:"mariadb.com/server",postgres:"postgresql.org",vitess:"vitess.io",etcd:"etcd.io",redis:"redis.io",memcached:"memcached.org",meilisearch:"meilisearch.com",mail:"github.com/mail-os/mail",git:"git-scm.com",certbot:"certbot.eff.org",bun:"bun.sh",node:"nodejs.org",deno:"deno.land"};var u="/opt/pantry";function Dt(e){return`'${e.split("'").join("'\\''")}'`}function yt(e={}){let t=e.installDir??"/usr/local/bin",r=e.version?`export PANTRY_VERSION=${Dt(e.version)}`:'export PANTRY_VERSION="${PANTRY_VERSION:-latest}"';return["export DEBIAN_FRONTEND=noninteractive","export PANTRY_SERVICE_SCOPE=system",`export PANTRY_INSTALL_DIR=${t}`,r,"command -v curl >/dev/null 2>&1 || (apt-get update -y && apt-get install -y curl ca-certificates)","command -v unzip >/dev/null 2>&1 || (apt-get update -y && apt-get install -y unzip)",`OS=$(uname -s | tr '[:upper:]' '[:lower:]'); ARCH=$(uname -m); case "$ARCH" in x86_64|amd64) ARCH=x64 ;; arm64|aarch64) ARCH=arm64 ;; esac; ZIP="pantry-\${OS}-\${ARCH}.zip"; if [ "\${PANTRY_VERSION:-latest}" = latest ]; then REL='latest/download'; else REL="download/v\${PANTRY_VERSION}"; fi; TMP=$(mktemp -d); curl -fsSL -o "\${TMP}/\${ZIP}" "https://github.com/pantry-pm/pantry/releases/\${REL}/\${ZIP}"; mkdir -p ${t}; unzip -o "\${TMP}/\${ZIP}" -d ${t}; chmod +x ${t}/pantry; rm -rf "\${TMP}"`,`export PATH="${t}:$PATH"`,"mkdir -p /opt/pantry"]}function Re(){return'eval "$(cd /opt/pantry && pantry env 2>/dev/null)" || true'}function Ae(e){if(e.length===0)return[];let t=[...new Set(e)],r=new Set(["/opt/pantry","/opt/pantry/pantry"]);for(let s of t){let i="/opt/pantry/pantry";for(let a of s.split("@")[0].split("/")){if(!/^[a-zA-Z0-9._-]+$/.test(a))throw Error("Invalid pantry package domain");i+=`/${a}`,r.add(i)}}let n=[...r].map(Dt).join(" ");return[`(umask 022; mkdir -p ${n} && chmod 0755 ${n} && cd /opt/pantry && pantry install ${t.map(Dt).join(" ")})`]}function sr(e){return[...new Set(e)].flatMap((t)=>[`(cd /opt/pantry && pantry start ${Dt(t)})`,`(cd /opt/pantry && pantry enable ${Dt(t)})`])}var he="/etc/mail",De="mail-server",rn="mail",nn=`${u}/pantry/.bin`;function Ie(e){return`'${String(e).split("'").join("'\\''")}'`}function Ve(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`}function na(e){return[e.version?`${xt.mail}@${e.version}`:xt.mail]}function sn(e,t){return`${he}/dkim/${t}.${e.dkim.selector}.pem`}function ss(e){return`${e.storagePath}/smtp.db`}function sa(e){let t=["# Generated by ts-cloud. Edits are overwritten on the next deploy —","# change `services.mail` in the cloud config instead.","","[server]",`host = ${Ve(e.bindAddress)}`,`port = ${e.ports.smtp}`,`hostname = ${Ve(e.hostname)}`];if(e.domains.length>0)t.push(`local_domains = ${Ve(e.domains.join(", "))}`);if(e.mode==="catcher")t.push("catch_all = true");if(t.push(`max_message_size = ${e.maxMessageSize}`,"","[tls]",`enabled = ${e.tls.enabled}`),e.tls.certPath)t.push(`cert_path = ${Ve(e.tls.certPath)}`);if(e.tls.keyPath)t.push(`key_path = ${Ve(e.tls.keyPath)}`);if(t.push("","[auth]",`enabled = ${e.mode==="server"}`,"","[antispam]",`dnsbl = ${e.spam.dnsbl}`,`dnsbl_reject = ${e.spam.dnsblReject}`,`greylist = ${e.spam.greylist}`,`spam_filter = ${e.spam.enabled}`,`junk_score = ${e.spam.junkScore}`,`reject_score = ${e.spam.rejectScore}`,"","[dkim]",`rotation_enabled = ${e.dkim.enabled&&e.dkim.rotate}`,`rotation_interval_days = ${e.dkim.rotateIntervalDays}`,"","[acme]",`enabled = ${e.tls.acme}`),e.tls.acmeEmail)t.push(`email = ${Ve(e.tls.acmeEmail)}`);if(t.push("","[webhook]",`enabled = ${!!e.webhookUrl}`),e.webhookUrl)t.push(`url = ${Ve(e.webhookUrl)}`);return t.push(""),t.join(`
13
- `)}function oa(e){let t=["# Generated by ts-cloud. See /etc/mail/config.toml for the rest.",`SMTP_HOSTNAME=${e.hostname}`,`SMTP_DB_PATH=${ss(e)}`,`SMTP_DNSBL_REJECT=${e.spam.dnsblReject}`,`SMTP_DELIVERY_METHOD=${e.delivery==="ses"?"ses":"direct"}`];if(e.delivery==="ses")t.push(`SMTP_SES_REGION=${e.sesRegion}`);if(e.ports.submission)t.push(`SUBMISSION_PORT=${e.ports.submission}`);if(e.ports.submissions)t.push(`SMTPS_PORT=${e.ports.submissions}`);if(e.ports.imap)t.push("IMAP_ENABLED=1",`IMAP_PORT=${e.ports.imap}`);if(e.webmail.enabled)t.push("SMTP_ENABLE_WEBMAIL=true",`SMTP_WEBMAIL_PORT=${e.webmail.port}`,`SMTP_WEBMAIL_TLS=${e.webmail.tls}`,`SMTP_WEBMAIL_SECURE_COOKIES=${e.webmail.secureCookies}`,`SMTP_WEBMAIL_ATTACHMENT_MAX_FILE=${e.webmail.attachmentMaxFileBytes}`,`SMTP_WEBMAIL_ATTACHMENT_MAX_TOTAL=${e.webmail.attachmentMaxTotalBytes}`,`SMTP_WEBMAIL_ATTACHMENT_MAX_COUNT=${e.webmail.attachmentMaxCount}`,`SMTP_WEBMAIL_UNDO_SEND_SECONDS=${e.webmail.undoSendSeconds}`);if(e.tls.requireForAuth)t.push("SMTP_REQUIRE_TLS_FOR_AUTH=true");if(e.dkim.enabled&&e.domains.length>0){let r=e.domains.find((s)=>s!==e.hostname)||e.hostname,n=e.domains.filter((s)=>s!==r);if(t.push(`DKIM_DOMAIN=${r}`,`DKIM_SELECTOR=${e.dkim.selector}`,`DKIM_PRIVATE_KEY_PATH=${sn(e,r)}`),n.length>0){let s=n.map((i)=>`${i}:${e.dkim.selector}:${sn(e,i)}`);t.push(`DKIM_EXTRA_KEYS=${s.join(",")}`)}}return t.push(""),t.join(`
14
- `)}function ia(e){let t=[e.ports.smtp,e.ports.submission,e.ports.submissions,e.ports.imap,e.ports.imaps].some((r)=>typeof r==="number"&&r<1024);return["[Unit]","Description=Mail server (SMTP/IMAP + webmail)","After=network-online.target","Wants=network-online.target","","[Service]","Type=simple",`User=${De}`,`Group=${De}`,`WorkingDirectory=${e.storagePath}`,`EnvironmentFile=${he}/mail.env`,...e.tls.systemCredentials?[`LoadCredential=server.crt:${e.tls.systemCredentials.certPath}`,`LoadCredential=server.key:${e.tls.systemCredentials.keyPath}`]:[],`Environment="PATH=${nn}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"`,`ExecStart=${nn}/mail serve --config ${he}/config.toml`,"Restart=always","RestartSec=5","LimitNOFILE=65536",...t?["AmbientCapabilities=CAP_NET_BIND_SERVICE","CapabilityBoundingSet=CAP_NET_BIND_SERVICE"]:[],"NoNewPrivileges=true","PrivateTmp=true","ProtectSystem=strict","ProtectHome=true","ProtectKernelTunables=true","ProtectKernelModules=true","ProtectControlGroups=true",`ReadWritePaths=${e.storagePath} ${he}`,"","[Install]","WantedBy=multi-user.target",""].join(`
15
- `)}function aa(e){if(!e.dkim.enabled||e.domains.length===0)return[];let t=[`mkdir -p ${he}/dkim`];for(let r of e.domains){let n=sn(e,r);t.push(`if [ ! -f ${Ie(n)} ]; then openssl genrsa -out ${Ie(n)} 2048; fi`,`chmod 0600 ${Ie(n)}`,`echo "DKIM ${e.dkim.selector}._domainkey.${r} IN TXT v=DKIM1; k=rsa; p=$(openssl rsa -in ${Ie(n)} -pubout -outform PEM 2>/dev/null | grep -v '^-----' | tr -d '\\n')"`)}return t.push(`chown -R ${De}:${De} ${he}/dkim`),t}function ca(e){if(e.accounts.length===0)return[];let t=(r)=>`runuser -u ${De} -- env SMTP_DB_PATH="$TS_CLOUD_MAIL_DB" ${nn}/mail user:local ${r}`;return e.accounts.flatMap((r)=>[t(`create ${Ie(r.address)} ${Ie(r.password)} ${Ie(r.address)}`),...r.passwordPolicy==="managed"?[t(`change-password ${Ie(r.address)} ${Ie(r.password)}`)]:[]])}function or(e){if(!e.enabled)return[];return[...Ae(na(e)),`id -u ${De} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin ${De}`,`mkdir -p ${e.storagePath} ${he}`,`chown root:${De} ${he}`,`chmod 0750 ${he}`,`chown -R ${De}:${De} ${e.storagePath}`,`TS_CLOUD_MAIL_DB="$(grep -sE '^SMTP_DB_PATH=' ${he}/mail.env | head -1 | cut -d= -f2- || true)"`,`[ -n "$TS_CLOUD_MAIL_DB" ] || TS_CLOUD_MAIL_DB=${Ie(ss(e))}`,`cat > ${he}/config.toml <<'TS_CLOUD_MAIL_CONF_EOF'`,sa(e),"TS_CLOUD_MAIL_CONF_EOF",`cat > ${he}/mail.env <<'TS_CLOUD_MAIL_ENV_EOF'`,oa(e),"TS_CLOUD_MAIL_ENV_EOF",`sed -i "s|^SMTP_DB_PATH=.*|SMTP_DB_PATH=$TS_CLOUD_MAIL_DB|" ${he}/mail.env`,`chmod 0640 ${he}/mail.env ${he}/config.toml`,`chown root:${De} ${he}/mail.env ${he}/config.toml`,...aa(e),`cat > /etc/systemd/system/${rn}.service <<'TS_CLOUD_MAIL_UNIT_EOF'`,ia(e),"TS_CLOUD_MAIL_UNIT_EOF",...e.tls.systemCredentials?["cat > /etc/systemd/system/ts-cloud-mail-tls.path <<'TS_CLOUD_MAIL_TLS_PATH'","[Unit]","Description=Refresh mail after gateway certificate renewal","[Path]",`PathChanged=${e.tls.systemCredentials.certPath}`,`PathChanged=${e.tls.systemCredentials.keyPath}`,"Unit=ts-cloud-mail-tls.service","[Install]","WantedBy=multi-user.target","TS_CLOUD_MAIL_TLS_PATH","cat > /etc/systemd/system/ts-cloud-mail-tls.service <<'TS_CLOUD_MAIL_TLS_UNIT'","[Unit]","Description=Load renewed mail TLS credentials","[Service]","Type=oneshot","ExecStart=/usr/bin/systemctl restart mail.service","TS_CLOUD_MAIL_TLS_UNIT"]:["systemctl disable --now ts-cloud-mail-tls.path 2>/dev/null || true"],"systemctl daemon-reload",...e.tls.systemCredentials?["systemctl enable --now ts-cloud-mail-tls.path"]:[],`systemctl enable ${rn}`,`systemctl restart ${rn}`,...ca(e)]}var Ce=`${u}/pantry/.bin`,cn=15306,on=15991,ir=15999,os=16101,ar=2379,Ue="/var/lib/vitess",ln=100;function Lt(e){return e===!0||typeof e==="object"&&e!==null}function la(e){return typeof e==="object"&&e!==null?e:{}}function ue(e){return`'${String(e).split("'").join("'\\''")}'`}function ua(e){let r=[e.version?`vitess.io@${e.version}`:"vitess.io"];if(e.mode!=="combo"&&!e.etcdEndpoint)r.push("etcd.io");return r.push("mysql.com@8.0.43"),r}var Te="vitess",pa=`${Ce}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,ls="/usr/local/lib/vitess",un=new Map;function an(e){return`${ls}/${e}.sh`}function it(e,t){return un.set(e,da(t)),`/bin/sh ${an(e)}`}function is(){let e=[`mkdir -p ${ls}`];for(let[t,r]of un)e.push(`cat > ${an(t)} <<'TS_CLOUD_VITESS_LAUNCHER_EOF'`,r,"TS_CLOUD_VITESS_LAUNCHER_EOF",`chmod 0755 ${an(t)}`);return e}function da(e){return["#!/bin/sh","set -e",`cd ${u}`,`PANTRY_ENV="$(pantry env 2>/dev/null | grep -E '^(export [A-Za-z_]|[A-Za-z_][A-Za-z0-9_]*=)' || true)"`,'if [ -n "$PANTRY_ENV" ]; then eval "$PANTRY_ENV"; fi',`VT_MYSQL_ROOT="$(dirname "$(dirname "$(readlink -f ${Ce}/mysqld 2>/dev/null)")")"`,`VTROOT="$(dirname "$(dirname "$(readlink -f ${Ce}/mysqlctld 2>/dev/null)")")"`,"export VT_MYSQL_ROOT VTROOT",'LD_LIBRARY_PATH="$VT_MYSQL_ROOT/lib:$VTROOT/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"',"export LD_LIBRARY_PATH",`exec ${e.join(" ")}`].join(`
16
- `)}function at(e){let t=["[Unit]",`Description=${e.description}`,`After=network.target${e.after?.length?` ${e.after.join(" ")}`:""}`];if(e.requires?.length)t.push(`Requires=${e.requires.join(" ")}`);return t.push("","[Service]","Type=simple",`User=${Te}`,`Group=${Te}`,`Environment="PATH=${pa}"`,`Environment="VTDATAROOT=${Ue}"`,...Object.entries(e.environment??{}).map(([r,n])=>`Environment="${r}=${n}"`),...(e.execStartPre??[]).map((r)=>`ExecStartPre=${r}`),`ExecStart=${e.execStart}`,"Restart=always","RestartSec=5","LimitNOFILE=65535","","[Install]","WantedBy=multi-user.target"),t.join(`
17
- `)}function as(e,t){return[`cat > /etc/systemd/system/${e}.service <<'TS_CLOUD_VITESS_UNIT_EOF'`,t,"TS_CLOUD_VITESS_UNIT_EOF"]}function ma(e){let t=e.cell??"zone1",r=e.vtgatePort??cn,s=`${((e.keyspaces?.length)?e.keyspaces:[{name:"app"}]).map((i)=>{let a=(i.sharded?["-80","80-"]:["0"]).map((c)=>`shards:{name:"${c}"}`).join(" ");return`keyspaces:{name:"${i.name}" ${a}}`}).join(" ")} cells:"${t}"`;return at({description:"Vitess (vtcombo, single-process development stack)",execStart:it("vtcombo",[`${Ce}/vtcombo`,`--cell ${t}`,`--proto-topo ${ue(s)}`,`--mysql-server-port ${r}`,"--mysql-server-bind-address 127.0.0.1","--mysql-auth-server-impl none","--start-mysql",`--port ${on}`,`--grpc-port ${ir}`])})}function fa(){return at({description:"etcd (Vitess topology store)",execStart:it("etcd",[`${Ce}/etcd`,`--data-dir ${Ue}/etcd`,`--listen-client-urls http://127.0.0.1:${ar}`,`--advertise-client-urls http://127.0.0.1:${ar}`]),execStartPre:[`+/bin/mkdir -p ${Ue}/etcd`,`+/bin/chown ${Te}:${Te} ${Ue}/etcd`]})}function pn(e){return["--topo-implementation etcd2",`--topo-global-server-address ${e.etcdEndpoint??`http://127.0.0.1:${ar}`}`,"--topo-global-root /vitess/global"]}function ga(e){let t=!e.etcdEndpoint;return at({description:"Vitess vtctld (control plane)",after:t?["vitess-etcd.service"]:[],requires:t?["vitess-etcd.service"]:[],execStart:it("vtctld",[`${Ce}/vtctld`,...pn(e),`--cell ${e.cell??"zone1"}`,"--service-map grpc-vtctl,grpc-vtctld",`--grpc-port ${ir}`,`--port ${ir+1}`])})}function ha(e,t,r){let n=e.cell??"zone1";return at({description:`Vitess vttablet (${t}/${r})`,after:["vitess-vtctld.service","vitess-mysqlctld.service"],requires:["vitess-mysqlctld.service"],execStart:it("vttablet",[`${Ce}/vttablet`,...pn(e),`--tablet-path ${n}-${String(ln).padStart(10,"0")}`,`--init-keyspace ${t}`,`--init-shard ${r}`,"--init-tablet-type replica",`--service-map ${ue("grpc-queryservice,grpc-tabletmanager,grpc-updatestream")}`,`--port ${os}`,`--grpc-port ${os+1}`,`--db-port ${e.mysqlPort??3306}`,`--mycnf-mysql-port ${e.mysqlPort??3306}`])})}function Sa(e){return at({description:"Vitess mysqlctld (managed mysqld)",execStart:it("mysqlctld",[`${Ce}/mysqlctld`,`--tablet-uid ${ln}`,`--mysql-port ${e.mysqlPort??3306}`,'--init-db-sql-file "$VTROOT/config/init_db.sql"',"--wait-time 2m"]),execStartPre:[`+/bin/mkdir -p ${Ue}`,`+/bin/chown ${Te}:${Te} ${Ue}`]})}var us="/etc/vitess/auth.json",At="/var/lib/vitess/vtgate-auth.sha256";function _a(e){let t=e.username??"vitess",r=e.password??"";return["mkdir -p /etc/vitess",'VTESS_AUTH_TMP="$(mktemp)"',`cat > "$VTESS_AUTH_TMP" <<'TS_CLOUD_VITESS_AUTH_EOF'`,JSON.stringify({[t]:[{Password:r,UserData:t}]}),"TS_CLOUD_VITESS_AUTH_EOF",`VTESS_VTGATE_AUTH_SHA="$(sha256sum "$VTESS_AUTH_TMP" | awk '{print $1}')"`,"VTESS_VTGATE_AUTH_CHANGED=1",`if [ -f ${At} ] && [ "$(cat ${At})" = "$VTESS_VTGATE_AUTH_SHA" ]; then VTESS_VTGATE_AUTH_CHANGED=0; fi`,`install -o ${Te} -g ${Te} -m 0600 "$VTESS_AUTH_TMP" ${us}`,'rm -f "$VTESS_AUTH_TMP"']}function va(e){let t=e.cell??"zone1";return at({description:"Vitess vtgate (query router)",after:["vitess-vtctld.service"],execStart:it("vtgate",[`${Ce}/vtgate`,...pn(e),`--cell ${t}`,`--cells-to-watch ${t}`,"--tablet-types-to-wait PRIMARY,REPLICA",`--service-map ${ue("grpc-vtgateservice")}`,`--mysql-server-port ${e.vtgatePort??cn}`,`--mysql-server-bind-address ${e.bindAddress??"127.0.0.1"}`,"--mysql-auth-server-impl static",`--mysql-auth-server-static-file ${us}`,`--port ${on}`,`--grpc-port ${on+1}`])})}function $a(e){let t=e.cell??"zone1",r=e.etcdEndpoint??`http://127.0.0.1:${ar}`,n=`${Ce}/vtctldclient --server 127.0.0.1:${ir}`,s=[`for i in $(seq 1 30); do ${n} GetCellInfoNames >/dev/null 2>&1 && break; sleep 2; done`,`if ! ${n} GetCellInfoNames 2>/dev/null | grep -qx ${ue(t)}; then`,` ${n} AddCellInfo --root ${ue(`/vitess/${t}`)} --server-address ${ue(r)} ${ue(t)}`,"fi"];for(let i of e.keyspaces??[]){let a=i.sharded?" --sharded":"";s.push(`if ! ${n} GetKeyspace ${ue(i.name)} >/dev/null 2>&1; then`,` ${n} CreateKeyspace${a} ${ue(i.name)}`,"fi");let c=`${t}-${String(ln).padStart(10,"0")}`;for(let f of i.sharded?["-80","80-"]:["0"]){let g=`${i.name}/${f}`;s.push("for i in $(seq 1 90); do",` if ${n} GetShard ${ue(g)} 2>/dev/null | grep -q '"is_primary_serving": *true'; then break; fi`,` ${n} PlannedReparentShard ${ue(g)} --new-primary ${ue(c)} >/dev/null 2>&1 && break`,` ${n} InitShardPrimary --force ${ue(g)} ${ue(c)} >/dev/null 2>&1 && break`," sleep 2","done",`if ! ${n} GetShard ${ue(g)} 2>/dev/null | grep -q '"is_primary_serving": *true'; then`,` echo "no primary could be elected for ${g}" >&2; exit 1`,"fi")}s.push(`${n} RebuildKeyspaceGraph ${ue(i.name)} >/dev/null`)}return s}function cs(e){let t=e.vtgatePort??cn;return["for i in $(seq 1 60); do",` if (cd ${u} && eval "$(pantry env 2>/dev/null)"; MYSQL_PWD=${ue(e.password??"")} ${Ce}/mysql -h 127.0.0.1 -P ${t} -u ${ue(e.username??"vitess")} --connect-timeout=2 -e 'SHOW KEYSPACES') >/dev/null 2>&1; then`,` echo "vtgate is serving on ${t}"; exit 0`," fi"," sleep 2","done",`echo "vtgate did not become ready on port ${t}" >&2`,"exit 1"]}function oo(e){if(!Lt(e))return[];let t=la(e),r=t.mode==="combo";un.clear();let n=['if [ "$(uname -m)" != "x86_64" ]; then',' echo "vitess: Vitess publishes only x86_64 release tarballs; this box is $(uname -m). Drop managedServices.vitess or use an x86_64 host." >&2'," exit 1","fi",...Ae(ua(t)),`VT_MYSQL_LIB="$(dirname "$(dirname "$(readlink -f ${Ce}/mysqld)")")/lib"`,'if [ -d "$VT_MYSQL_LIB" ]; then',` printf '%s\\n' "$VT_MYSQL_LIB" > /etc/ld.so.conf.d/vitess-mysql.conf`," ldconfig","else",' echo "mysqld library directory not found under $VT_MYSQL_LIB" >&2; exit 1',"fi",`id -u ${Te} >/dev/null 2>&1 || useradd --system --home-dir ${Ue} --shell /usr/sbin/nologin ${Te}`,`mkdir -p ${Ue}`,`chown -R ${Te}:${Te} ${Ue}`];if(r)return n.push(...as("vitess-vtcombo",ma(t)),...is(),"systemctl daemon-reload","systemctl enable --now vitess-vtcombo.service",...cs(t)),n;n.push(..._a(t));let s=[];if(!t.etcdEndpoint)s.push(["vitess-etcd",fa()]);s.push(["vitess-vtctld",ga(t)],["vitess-mysqlctld",Sa(t)],["vitess-vtgate",va(t)]);for(let i of t.keyspaces??[])for(let a of i.sharded?["-80","80-"]:["0"])s.push([`vitess-vttablet-${i.name}-${a.replace(/-/g,"x")}`,ha(t,i.name,a)]);for(let[i,a]of s)n.push(...as(i,a));n.push(...is()),n.push("systemctl daemon-reload");for(let[i]of s)n.push(`systemctl enable --now ${i}.service`);return n.push(`if [ "$VTESS_VTGATE_AUTH_CHANGED" = 1 ]; then systemctl restart vitess-vtgate.service; printf '%s\\n' "$VTESS_VTGATE_AUTH_SHA" > ${At}; chown ${Te}:${Te} ${At}; chmod 0600 ${At}; fi`,...$a(t),...cs(t)),n}var ya=new Set(["singlestore","vitess"]);function Lr(e){if(e?.engine&&ya.has(e.engine))return!1;return!e?.host||e.host==="127.0.0.1"||e.host==="localhost"}function Ke(e,t="psql"){let r=e?.port??5432;if(Lr(e))return`${t} -p ${r} -U postgres`;let n=e?.username||"postgres";return`${e?.password?`PGPASSWORD='${e.password.replace(/'/g,"'\\''")}' `:""}${t} -h ${e.host} -p ${r} -U ${n} -w`}function ke(e){return e===!0||typeof e==="object"&&e!=null}function ba(e){let t=[],r=[];if(ke(e.mysql))t.push(typeof e.mysql==="object"&&e.mysql.version?`mysql.com@${e.mysql.version}`:"mysql.com@8.0.43"),r.push("mysql");else if(ke(e.mariadb))t.push(xt.mariadb),r.push("mariadb");if(ke(e.postgres))t.push("postgresql.org"),r.push("postgres");if(ke(e.redis))t.push("redis.io"),r.push("redis");if(ke(e.memcached))t.push("memcached.org"),r.push("memcached");if(ke(e.meilisearch))t.push("meilisearch.com"),r.push("meilisearch");return{packages:t,services:r}}function cr(e={},t={}){let r=ba(e),n=oo(e.vitess),s=t.mail?or(t.mail):[];if(r.packages.length===0)return[...n,...s];return[...Ae(r.packages),...sr(r.services),...n,...s]}function ct(e,t={}){if(e?.requireLocalPasswordAuth&&(e.engine!=="postgres"||!Lr(e)||!e.name||!e.username||!e.password||e.users?.some((v)=>!v.password)))throw Error("Password authentication requires a named local PostgreSQL database and nonempty login passwords");if(!e?.name)return[];if(!Lr(e))return[];let r=e.name,n=e.username||r,s=e.password||"",i=ke(t.postgres)||e.engine==="postgres",a=ke(t.mariadb)||e.engine==="mariadb",c=ke(t.mysql)||e.engine==="mysql";if(i&&!c&&!a){let v=(D)=>`"${D.replace(/"/g,'""')}"`,b=(D)=>`'${D.replace(/'/g,"''")}'`,T=(D,N)=>[...e.requireLocalPasswordAuth?["SET password_encryption = 'scram-sha-256';"]:[],"DO $$ BEGIN",` IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${b(D)}) THEN`,` CREATE ROLE ${v(D)} LOGIN PASSWORD ${b(N)};`," ELSE",` ALTER ROLE ${v(D)} LOGIN PASSWORD ${b(N)};`," END IF;","END $$;"],L=(D)=>{let N=D.databases&&D.databases.length>0?D.databases:[r],k=[];for(let j of N)if(D.access==="readonly")k.push(`GRANT CONNECT ON DATABASE ${v(j)} TO ${v(D.username)};`,`\\connect ${v(j)}`,`GRANT USAGE ON SCHEMA public TO ${v(D.username)};`,`GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${v(D.username)};`,`ALTER DEFAULT PRIVILEGES FOR ROLE ${v(n)} IN SCHEMA public GRANT SELECT ON TABLES TO ${v(D.username)};`,"\\connect postgres");else k.push(`GRANT ALL PRIVILEGES ON DATABASE ${v(j)} TO ${v(D.username)};`);return k},K=e.users||[],M=e.port??5432;return[Re(),`for i in $(seq 1 30); do pg_isready -p ${M} -q && break; sleep 2; done`,`${Ke(e)}${e.requireLocalPasswordAuth?" -v ON_ERROR_STOP=1":""} <<'TS_CLOUD_PG_EOF'`,...T(n,s),`SELECT 'CREATE DATABASE ${v(r)} OWNER ${v(n)}' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = ${b(r)})\\gexec`,...K.flatMap((D)=>[...T(D.username,D.password),...L(D)]),"TS_CLOUD_PG_EOF",...e.requireLocalPasswordAuth?[`TS_CLOUD_PG_HBA=$(${Ke(e)} -Atc 'SHOW hba_file')`,'case "$TS_CLOUD_PG_HBA" in /*) ;; *) echo "Invalid PostgreSQL authentication path" >&2; exit 1 ;; esac','[ -f "$TS_CLOUD_PG_HBA.before-ts-cloud-password-auth" ] || cp -p "$TS_CLOUD_PG_HBA" "$TS_CLOUD_PG_HBA.before-ts-cloud-password-auth"','TS_CLOUD_PG_AUTH=$(mktemp "$TS_CLOUD_PG_HBA.ts-cloud.XXXXXXXX")',"{"," printf '%s\\n' '# BEGIN ts-cloud password authentication' 'host all all 127.0.0.1/32 scram-sha-256' 'host all all ::1/128 scram-sha-256' 'host replication all 127.0.0.1/32 scram-sha-256' 'host replication all ::1/128 scram-sha-256' '# END ts-cloud password authentication'",` sed '/^# BEGIN ts-cloud password authentication$/,/^# END ts-cloud password authentication$/d' "$TS_CLOUD_PG_HBA"`,'} > "$TS_CLOUD_PG_AUTH"','chown --reference="$TS_CLOUD_PG_HBA" "$TS_CLOUD_PG_AUTH"','chmod --reference="$TS_CLOUD_PG_HBA" "$TS_CLOUD_PG_AUTH"','mv -f "$TS_CLOUD_PG_AUTH" "$TS_CLOUD_PG_HBA"',`${Ke(e)} -v ON_ERROR_STOP=1 -c 'SELECT pg_reload_conf()'`]:[],...Ta(e)]}let f=a?"/var/lib/pantry/mariadb/mariadbd.sock":"/var/lib/pantry/mysql/mysqld.sock",g=(v)=>v.replace(/`/g,"``"),h=(v)=>v.replace(/\\/g,"\\\\").replace(/'/g,"\\'"),S=(v)=>{let b=v.databases&&v.databases.length>0?v.databases:[r],T=v.access==="readonly"?"SELECT":"ALL PRIVILEGES",L=[`CREATE USER IF NOT EXISTS '${h(v.username)}'@'%' IDENTIFIED BY '${h(v.password)}';`,`CREATE USER IF NOT EXISTS '${h(v.username)}'@'localhost' IDENTIFIED BY '${h(v.password)}';`,`ALTER USER '${h(v.username)}'@'%' IDENTIFIED BY '${h(v.password)}';`,`ALTER USER '${h(v.username)}'@'localhost' IDENTIFIED BY '${h(v.password)}';`];for(let K of b)L.push(`GRANT ${T} ON \`${g(K)}\`.* TO '${h(v.username)}'@'%';`,`GRANT ${T} ON \`${g(K)}\`.* TO '${h(v.username)}'@'localhost';`);return L},_=e.users||[];return[Re(),`for i in $(seq 1 30); do mysqladmin --socket=${f} -u root ping 2>/dev/null | grep -q alive && break; sleep 2; done`,`mysql --socket=${f} -u root <<'TS_CLOUD_SQL_EOF'`,`CREATE DATABASE IF NOT EXISTS \`${g(r)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`,...S({username:n,password:s}),..._.flatMap(S),"FLUSH PRIVILEGES;","TS_CLOUD_SQL_EOF"]}function Ta(e){return[`${Ke(e)} -tA <<'TS_CLOUD_PG_CAP_EOF' || true`,"\\set QUIET on","SELECT CASE WHEN used::float / NULLIF(cap, 0) >= 0.8"," THEN '[ts-cloud] postgres is at ' || used || '/' || cap || ' connections on this server.'"," || ' Raise max_connections (ALTER SYSTEM SET max_connections = ...; then restart) or'"," || ' set idle_session_timeout so abandoned pools are reclaimed.'"," ELSE NULL END","FROM (SELECT (SELECT count(*) FROM pg_stat_activity) AS used,"," (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS cap) c","WHERE CASE WHEN used::float / NULLIF(cap, 0) >= 0.8 THEN true ELSE false END;","SELECT '[ts-cloud] ' || coalesce(datname, '(background)') || ': ' || count(*) || ' connection(s), oldest idle ' ||"," coalesce(round(extract(epoch from max(now() - state_change)) / 60)::text || 'm', 'n/a')","FROM pg_stat_activity","WHERE (SELECT count(*)::float / NULLIF((SELECT setting::int FROM pg_settings WHERE name = 'max_connections'), 0) FROM pg_stat_activity) >= 0.8","GROUP BY datname ORDER BY count(*) DESC;","TS_CLOUD_PG_CAP_EOF"]}function ps(e){if(!e?.name)return{};let t=e.engine==="singlestore",r=e.engine==="vitess",n=e.engine==="postgres"?"pgsql":t?"singlestore":r?"vitess":"mysql",s=e.port??(e.engine==="postgres"?5432:r?15306:3306),i={DB_CONNECTION:n,DB_HOST:e.host||"127.0.0.1",DB_PORT:String(s),DB_DATABASE:e.name};if(e.username)i.DB_USERNAME=e.username;if(e.password)i.DB_PASSWORD=e.password;if((t||r)&&e.ssl!==!1)i.DB_SSL="true";return i}var lr="/var/backups/ts-cloud",dn="/etc/ts-cloud/backups.config.ts",ds="/etc/cron.d/ts-cloud-backups",$r="/usr/local/bin/ts-cloud-backup.sh";function wa(e){if(!e.name)return null;if(!["postgres","mysql","mariadb"].includes(e.engine||""))throw Error("Unsupported scheduled SQL backup engine");return JSON.stringify({type:e.engine==="postgres"?"postgresql":"mysql",name:e.name,connection:{hostname:e.host||"127.0.0.1",port:e.port??(e.engine==="postgres"?5432:3306),database:e.name,username:e.username||e.name,password:e.password||"",ssl:!1},includeSchema:!0,includeData:!0})}function Ca(e,t){let r=e?wa(e):null,n=t.bucket?{type:"s3",bucket:t.bucket,prefix:t.prefix||"db-backups",...t.endpoint?{endpoint:t.endpoint}:{},...t.region?{region:t.region}:{},keepDays:t.retentionDays??30,optional:!1,...t.encryptionKey?{encryption:{keyEnv:"TS_CLOUD_BACKUP_ENCRYPTION_KEY"}}:{}}:void 0,s={verbose:!0,outputPath:lr,retention:{count:t.retentionCount??5,maxAge:t.retentionDays??30},databases:[],files:t.files||[],destinations:n?[n]:[]};return["import type { BackupConfig } from 'ts-backups'",`const config: BackupConfig = ${JSON.stringify(s,null,2)}`,`config.databases = [
11
+ `,c=na(a);return{host:`mta-sts.${t}`,body:a,id:c,txt:`v=STSv1; id=${c}`}}var qg=8461;function Jh(e,t=qg){return[`const policy = ${JSON.stringify(e.body)}`,"Bun.serve({",` port: ${t},`," hostname: '127.0.0.1',"," fetch(req) {"," const { pathname } = new URL(req.url)"," if (pathname !== '/.well-known/mta-sts.txt') return new Response('Not Found', { status: 404 })"," return new Response(policy, { headers: { 'content-type': 'text/plain; charset=utf-8' } })"," },","})"].join(`
12
+ `)}var xt={php:"php.net",nginx:"nginx.org",composer:"getcomposer.org",mysql:"mysql.com",mariadb:"mariadb.com/server",postgres:"postgresql.org",vitess:"vitess.io",etcd:"etcd.io",redis:"redis.io",memcached:"memcached.org",meilisearch:"meilisearch.com",mail:"github.com/mail-os/mail",git:"git-scm.com",certbot:"certbot.eff.org",bun:"bun.sh",node:"nodejs.org",deno:"deno.land"};var u="/opt/pantry";function Dt(e){return`'${e.split("'").join("'\\''")}'`}function yt(e={}){let t=e.installDir??"/usr/local/bin",r=e.version?`export PANTRY_VERSION=${Dt(e.version)}`:'export PANTRY_VERSION="${PANTRY_VERSION:-latest}"';return["export DEBIAN_FRONTEND=noninteractive","export PANTRY_SERVICE_SCOPE=system",`export PANTRY_INSTALL_DIR=${t}`,r,"command -v curl >/dev/null 2>&1 || (apt-get update -y && apt-get install -y curl ca-certificates)","command -v unzip >/dev/null 2>&1 || (apt-get update -y && apt-get install -y unzip)",`OS=$(uname -s | tr '[:upper:]' '[:lower:]'); ARCH=$(uname -m); case "$ARCH" in x86_64|amd64) ARCH=x64 ;; arm64|aarch64) ARCH=arm64 ;; esac; ZIP="pantry-\${OS}-\${ARCH}.zip"; if [ "\${PANTRY_VERSION:-latest}" = latest ]; then REL='latest/download'; else REL="download/v\${PANTRY_VERSION}"; fi; TMP=$(mktemp -d); curl -fsSL -o "\${TMP}/\${ZIP}" "https://github.com/pantry-pm/pantry/releases/\${REL}/\${ZIP}"; mkdir -p ${t}; unzip -o "\${TMP}/\${ZIP}" -d ${t}; chmod +x ${t}/pantry; rm -rf "\${TMP}"`,`export PATH="${t}:$PATH"`,"mkdir -p /opt/pantry"]}function Re(){return'eval "$(cd /opt/pantry && pantry env 2>/dev/null)" || true'}function Ae(e){if(e.length===0)return[];let t=[...new Set(e)],r=new Set(["/opt/pantry","/opt/pantry/pantry"]);for(let s of t){let i="/opt/pantry/pantry";for(let a of s.split("@")[0].split("/")){if(!/^[a-zA-Z0-9._-]+$/.test(a))throw Error("Invalid pantry package domain");i+=`/${a}`,r.add(i)}}let n=[...r].map(Dt).join(" ");return[`(umask 022; mkdir -p ${n} && chmod 0755 ${n} && cd /opt/pantry && pantry install ${t.map(Dt).join(" ")})`]}function sr(e){return[...new Set(e)].flatMap((t)=>[`(cd /opt/pantry && pantry start ${Dt(t)})`,`(cd /opt/pantry && pantry enable ${Dt(t)})`])}var he="/etc/mail",De="mail-server",rn="mail",nn=`${u}/pantry/.bin`;function Ie(e){return`'${String(e).split("'").join("'\\''")}'`}function Ve(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`}function sa(e){return[e.version?`${xt.mail}@${e.version}`:xt.mail]}function sn(e,t){return`${he}/dkim/${t}.${e.dkim.selector}.pem`}function ss(e){return`${e.storagePath}/smtp.db`}function oa(e){let t=["# Generated by ts-cloud. Edits are overwritten on the next deploy —","# change `services.mail` in the cloud config instead.","","[server]",`host = ${Ve(e.bindAddress)}`,`port = ${e.ports.smtp}`,`hostname = ${Ve(e.hostname)}`];if(e.domains.length>0)t.push(`local_domains = ${Ve(e.domains.join(", "))}`);if(e.mode==="catcher")t.push("catch_all = true");if(t.push(`max_message_size = ${e.maxMessageSize}`,"","[tls]",`enabled = ${e.tls.enabled}`),e.tls.certPath)t.push(`cert_path = ${Ve(e.tls.certPath)}`);if(e.tls.keyPath)t.push(`key_path = ${Ve(e.tls.keyPath)}`);if(t.push("","[auth]",`enabled = ${e.mode==="server"}`,"","[antispam]",`dnsbl = ${e.spam.dnsbl}`,`dnsbl_reject = ${e.spam.dnsblReject}`,`greylist = ${e.spam.greylist}`,`spam_filter = ${e.spam.enabled}`,`junk_score = ${e.spam.junkScore}`,`reject_score = ${e.spam.rejectScore}`,"","[dkim]",`rotation_enabled = ${e.dkim.enabled&&e.dkim.rotate}`,`rotation_interval_days = ${e.dkim.rotateIntervalDays}`,"","[acme]",`enabled = ${e.tls.acme}`),e.tls.acmeEmail)t.push(`email = ${Ve(e.tls.acmeEmail)}`);if(t.push("","[webhook]",`enabled = ${!!e.webhookUrl}`),e.webhookUrl)t.push(`url = ${Ve(e.webhookUrl)}`);return t.push(""),t.join(`
13
+ `)}function ia(e){let t=["# Generated by ts-cloud. See /etc/mail/config.toml for the rest.",`SMTP_HOSTNAME=${e.hostname}`,`SMTP_DB_PATH=${ss(e)}`,`SMTP_DNSBL_REJECT=${e.spam.dnsblReject}`,`SMTP_DELIVERY_METHOD=${e.delivery==="ses"?"ses":"direct"}`];if(e.delivery==="ses")t.push(`SMTP_SES_REGION=${e.sesRegion}`);if(e.ports.submission)t.push(`SUBMISSION_PORT=${e.ports.submission}`);if(e.ports.submissions)t.push(`SMTPS_PORT=${e.ports.submissions}`);if(e.ports.imap)t.push("IMAP_ENABLED=1",`IMAP_PORT=${e.ports.imap}`);if(e.webmail.enabled)t.push("SMTP_ENABLE_WEBMAIL=true",`SMTP_WEBMAIL_PORT=${e.webmail.port}`,`SMTP_WEBMAIL_TLS=${e.webmail.tls}`,`SMTP_WEBMAIL_SECURE_COOKIES=${e.webmail.secureCookies}`,`SMTP_WEBMAIL_ATTACHMENT_MAX_FILE=${e.webmail.attachmentMaxFileBytes}`,`SMTP_WEBMAIL_ATTACHMENT_MAX_TOTAL=${e.webmail.attachmentMaxTotalBytes}`,`SMTP_WEBMAIL_ATTACHMENT_MAX_COUNT=${e.webmail.attachmentMaxCount}`,`SMTP_WEBMAIL_UNDO_SEND_SECONDS=${e.webmail.undoSendSeconds}`);if(e.tls.requireForAuth)t.push("SMTP_REQUIRE_TLS_FOR_AUTH=true");if(e.dkim.enabled&&e.domains.length>0){let r=e.domains.find((s)=>s!==e.hostname)||e.hostname,n=e.domains.filter((s)=>s!==r);if(t.push(`DKIM_DOMAIN=${r}`,`DKIM_SELECTOR=${e.dkim.selector}`,`DKIM_PRIVATE_KEY_PATH=${sn(e,r)}`),n.length>0){let s=n.map((i)=>`${i}:${e.dkim.selector}:${sn(e,i)}`);t.push(`DKIM_EXTRA_KEYS=${s.join(",")}`)}}return t.push(""),t.join(`
14
+ `)}function aa(e){let t=[e.ports.smtp,e.ports.submission,e.ports.submissions,e.ports.imap,e.ports.imaps].some((r)=>typeof r==="number"&&r<1024);return["[Unit]","Description=Mail server (SMTP/IMAP + webmail)","After=network-online.target","Wants=network-online.target","","[Service]","Type=simple",`User=${De}`,`Group=${De}`,`WorkingDirectory=${e.storagePath}`,`EnvironmentFile=${he}/mail.env`,...e.tls.systemCredentials?[`LoadCredential=server.crt:${e.tls.systemCredentials.certPath}`,`LoadCredential=server.key:${e.tls.systemCredentials.keyPath}`]:[],`Environment="PATH=${nn}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"`,`ExecStart=${nn}/mail serve --config ${he}/config.toml`,"Restart=always","RestartSec=5","LimitNOFILE=65536",...t?["AmbientCapabilities=CAP_NET_BIND_SERVICE","CapabilityBoundingSet=CAP_NET_BIND_SERVICE"]:[],"NoNewPrivileges=true","PrivateTmp=true","ProtectSystem=strict","ProtectHome=true","ProtectKernelTunables=true","ProtectKernelModules=true","ProtectControlGroups=true",`ReadWritePaths=${e.storagePath} ${he}`,"","[Install]","WantedBy=multi-user.target",""].join(`
15
+ `)}function ca(e){if(!e.dkim.enabled||e.domains.length===0)return[];let t=[`mkdir -p ${he}/dkim`];for(let r of e.domains){let n=sn(e,r);t.push(`if [ ! -f ${Ie(n)} ]; then openssl genrsa -out ${Ie(n)} 2048; fi`,`chmod 0600 ${Ie(n)}`,`echo "DKIM ${e.dkim.selector}._domainkey.${r} IN TXT v=DKIM1; k=rsa; p=$(openssl rsa -in ${Ie(n)} -pubout -outform PEM 2>/dev/null | grep -v '^-----' | tr -d '\\n')"`)}return t.push(`chown -R ${De}:${De} ${he}/dkim`),t}function la(e){if(e.accounts.length===0)return[];let t=(r)=>`runuser -u ${De} -- env SMTP_DB_PATH="$TS_CLOUD_MAIL_DB" ${nn}/mail user:local ${r}`;return e.accounts.flatMap((r)=>[t(`create ${Ie(r.address)} ${Ie(r.password)} ${Ie(r.address)}`),...r.passwordPolicy==="managed"?[t(`change-password ${Ie(r.address)} ${Ie(r.password)}`)]:[]])}function or(e){if(!e.enabled)return[];return[...Ae(sa(e)),`id -u ${De} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin ${De}`,`mkdir -p ${e.storagePath} ${he}`,`chown root:${De} ${he}`,`chmod 0750 ${he}`,`chown -R ${De}:${De} ${e.storagePath}`,`TS_CLOUD_MAIL_DB="$(grep -sE '^SMTP_DB_PATH=' ${he}/mail.env | head -1 | cut -d= -f2- || true)"`,`[ -n "$TS_CLOUD_MAIL_DB" ] || TS_CLOUD_MAIL_DB=${Ie(ss(e))}`,`cat > ${he}/config.toml <<'TS_CLOUD_MAIL_CONF_EOF'`,oa(e),"TS_CLOUD_MAIL_CONF_EOF",`cat > ${he}/mail.env <<'TS_CLOUD_MAIL_ENV_EOF'`,ia(e),"TS_CLOUD_MAIL_ENV_EOF",`sed -i "s|^SMTP_DB_PATH=.*|SMTP_DB_PATH=$TS_CLOUD_MAIL_DB|" ${he}/mail.env`,`chmod 0640 ${he}/mail.env ${he}/config.toml`,`chown root:${De} ${he}/mail.env ${he}/config.toml`,...ca(e),`cat > /etc/systemd/system/${rn}.service <<'TS_CLOUD_MAIL_UNIT_EOF'`,aa(e),"TS_CLOUD_MAIL_UNIT_EOF",...e.tls.systemCredentials?["cat > /etc/systemd/system/ts-cloud-mail-tls.path <<'TS_CLOUD_MAIL_TLS_PATH'","[Unit]","Description=Refresh mail after gateway certificate renewal","[Path]",`PathChanged=${e.tls.systemCredentials.certPath}`,`PathChanged=${e.tls.systemCredentials.keyPath}`,"Unit=ts-cloud-mail-tls.service","[Install]","WantedBy=multi-user.target","TS_CLOUD_MAIL_TLS_PATH","cat > /etc/systemd/system/ts-cloud-mail-tls.service <<'TS_CLOUD_MAIL_TLS_UNIT'","[Unit]","Description=Load renewed mail TLS credentials","[Service]","Type=oneshot","ExecStart=/usr/bin/systemctl restart mail.service","TS_CLOUD_MAIL_TLS_UNIT"]:["systemctl disable --now ts-cloud-mail-tls.path 2>/dev/null || true"],"systemctl daemon-reload",...e.tls.systemCredentials?["systemctl enable --now ts-cloud-mail-tls.path"]:[],`systemctl enable ${rn}`,`systemctl restart ${rn}`,...la(e)]}var Ce=`${u}/pantry/.bin`,cn=15306,on=15991,ir=15999,os=16101,ar=2379,Ue="/var/lib/vitess",ln=100;function Lt(e){return e===!0||typeof e==="object"&&e!==null}function ua(e){return typeof e==="object"&&e!==null?e:{}}function fe(e){return`'${String(e).split("'").join("'\\''")}'`}function pa(e){let r=[e.version?`vitess.io@${e.version}`:"vitess.io"];if(e.mode!=="combo"&&!e.etcdEndpoint)r.push("etcd.io");return r.push("mysql.com@8.0.43"),r}var Te="vitess",da=`${Ce}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,ls="/usr/local/lib/vitess",un=new Map;function an(e){return`${ls}/${e}.sh`}function it(e,t){return un.set(e,ma(t)),`/bin/sh ${an(e)}`}function is(){let e=[`mkdir -p ${ls}`];for(let[t,r]of un)e.push(`cat > ${an(t)} <<'TS_CLOUD_VITESS_LAUNCHER_EOF'`,r,"TS_CLOUD_VITESS_LAUNCHER_EOF",`chmod 0755 ${an(t)}`);return e}function ma(e){return["#!/bin/sh","set -e",`cd ${u}`,`PANTRY_ENV="$(pantry env 2>/dev/null | grep -E '^(export [A-Za-z_]|[A-Za-z_][A-Za-z0-9_]*=)' || true)"`,'if [ -n "$PANTRY_ENV" ]; then eval "$PANTRY_ENV"; fi',`VT_MYSQL_ROOT="$(dirname "$(dirname "$(readlink -f ${Ce}/mysqld 2>/dev/null)")")"`,`VTROOT="$(dirname "$(dirname "$(readlink -f ${Ce}/mysqlctld 2>/dev/null)")")"`,"export VT_MYSQL_ROOT VTROOT",'LD_LIBRARY_PATH="$VT_MYSQL_ROOT/lib:$VTROOT/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"',"export LD_LIBRARY_PATH",`exec ${e.join(" ")}`].join(`
16
+ `)}function at(e){let t=["[Unit]",`Description=${e.description}`,`After=network.target${e.after?.length?` ${e.after.join(" ")}`:""}`];if(e.requires?.length)t.push(`Requires=${e.requires.join(" ")}`);return t.push("","[Service]","Type=simple",`User=${Te}`,`Group=${Te}`,`Environment="PATH=${da}"`,`Environment="VTDATAROOT=${Ue}"`,...Object.entries(e.environment??{}).map(([r,n])=>`Environment="${r}=${n}"`),...(e.execStartPre??[]).map((r)=>`ExecStartPre=${r}`),`ExecStart=${e.execStart}`,"Restart=always","RestartSec=5","LimitNOFILE=65535","","[Install]","WantedBy=multi-user.target"),t.join(`
17
+ `)}function as(e,t){return[`cat > /etc/systemd/system/${e}.service <<'TS_CLOUD_VITESS_UNIT_EOF'`,t,"TS_CLOUD_VITESS_UNIT_EOF"]}function fa(e){let t=e.cell??"zone1",r=e.vtgatePort??cn,s=`${((e.keyspaces?.length)?e.keyspaces:[{name:"app"}]).map((i)=>{let a=(i.sharded?["-80","80-"]:["0"]).map((c)=>`shards:{name:"${c}"}`).join(" ");return`keyspaces:{name:"${i.name}" ${a}}`}).join(" ")} cells:"${t}"`;return at({description:"Vitess (vtcombo, single-process development stack)",execStart:it("vtcombo",[`${Ce}/vtcombo`,`--cell ${t}`,`--proto-topo ${fe(s)}`,`--mysql-server-port ${r}`,"--mysql-server-bind-address 127.0.0.1","--mysql-auth-server-impl none","--start-mysql",`--port ${on}`,`--grpc-port ${ir}`])})}function ha(){return at({description:"etcd (Vitess topology store)",execStart:it("etcd",[`${Ce}/etcd`,`--data-dir ${Ue}/etcd`,`--listen-client-urls http://127.0.0.1:${ar}`,`--advertise-client-urls http://127.0.0.1:${ar}`]),execStartPre:[`+/bin/mkdir -p ${Ue}/etcd`,`+/bin/chown ${Te}:${Te} ${Ue}/etcd`]})}function pn(e){return["--topo-implementation etcd2",`--topo-global-server-address ${e.etcdEndpoint??`http://127.0.0.1:${ar}`}`,"--topo-global-root /vitess/global"]}function ga(e){let t=!e.etcdEndpoint;return at({description:"Vitess vtctld (control plane)",after:t?["vitess-etcd.service"]:[],requires:t?["vitess-etcd.service"]:[],execStart:it("vtctld",[`${Ce}/vtctld`,...pn(e),`--cell ${e.cell??"zone1"}`,"--service-map grpc-vtctl,grpc-vtctld",`--grpc-port ${ir}`,`--port ${ir+1}`])})}function Sa(e,t,r){let n=e.cell??"zone1";return at({description:`Vitess vttablet (${t}/${r})`,after:["vitess-vtctld.service","vitess-mysqlctld.service"],requires:["vitess-mysqlctld.service"],execStart:it("vttablet",[`${Ce}/vttablet`,...pn(e),`--tablet-path ${n}-${String(ln).padStart(10,"0")}`,`--init-keyspace ${t}`,`--init-shard ${r}`,"--init-tablet-type replica",`--service-map ${fe("grpc-queryservice,grpc-tabletmanager,grpc-updatestream")}`,`--port ${os}`,`--grpc-port ${os+1}`,`--db-port ${e.mysqlPort??3306}`,`--mycnf-mysql-port ${e.mysqlPort??3306}`])})}function va(e){return at({description:"Vitess mysqlctld (managed mysqld)",execStart:it("mysqlctld",[`${Ce}/mysqlctld`,`--tablet-uid ${ln}`,`--mysql-port ${e.mysqlPort??3306}`,'--init-db-sql-file "$VTROOT/config/init_db.sql"',"--wait-time 2m"]),execStartPre:[`+/bin/mkdir -p ${Ue}`,`+/bin/chown ${Te}:${Te} ${Ue}`]})}var us="/etc/vitess/auth.json",At="/var/lib/vitess/vtgate-auth.sha256";function ya(e){let t=e.username??"vitess",r=e.password??"";return["mkdir -p /etc/vitess",'VTESS_AUTH_TMP="$(mktemp)"',`cat > "$VTESS_AUTH_TMP" <<'TS_CLOUD_VITESS_AUTH_EOF'`,JSON.stringify({[t]:[{Password:r,UserData:t}]}),"TS_CLOUD_VITESS_AUTH_EOF",`VTESS_VTGATE_AUTH_SHA="$(sha256sum "$VTESS_AUTH_TMP" | awk '{print $1}')"`,"VTESS_VTGATE_AUTH_CHANGED=1",`if [ -f ${At} ] && [ "$(cat ${At})" = "$VTESS_VTGATE_AUTH_SHA" ]; then VTESS_VTGATE_AUTH_CHANGED=0; fi`,`install -o ${Te} -g ${Te} -m 0600 "$VTESS_AUTH_TMP" ${us}`,'rm -f "$VTESS_AUTH_TMP"']}function _a(e){let t=e.cell??"zone1";return at({description:"Vitess vtgate (query router)",after:["vitess-vtctld.service"],execStart:it("vtgate",[`${Ce}/vtgate`,...pn(e),`--cell ${t}`,`--cells-to-watch ${t}`,"--tablet-types-to-wait PRIMARY,REPLICA",`--service-map ${fe("grpc-vtgateservice")}`,`--mysql-server-port ${e.vtgatePort??cn}`,`--mysql-server-bind-address ${e.bindAddress??"127.0.0.1"}`,"--mysql-auth-server-impl static",`--mysql-auth-server-static-file ${us}`,`--port ${on}`,`--grpc-port ${on+1}`])})}function $a(e){let t=e.cell??"zone1",r=e.etcdEndpoint??`http://127.0.0.1:${ar}`,n=`${Ce}/vtctldclient --server 127.0.0.1:${ir}`,s=[`for i in $(seq 1 30); do ${n} GetCellInfoNames >/dev/null 2>&1 && break; sleep 2; done`,`if ! ${n} GetCellInfoNames 2>/dev/null | grep -qx ${fe(t)}; then`,` ${n} AddCellInfo --root ${fe(`/vitess/${t}`)} --server-address ${fe(r)} ${fe(t)}`,"fi"];for(let i of e.keyspaces??[]){let a=i.sharded?" --sharded":"";s.push(`if ! ${n} GetKeyspace ${fe(i.name)} >/dev/null 2>&1; then`,` ${n} CreateKeyspace${a} ${fe(i.name)}`,"fi");let c=`${t}-${String(ln).padStart(10,"0")}`;for(let f of i.sharded?["-80","80-"]:["0"]){let h=`${i.name}/${f}`;s.push("for i in $(seq 1 90); do",` if ${n} GetShard ${fe(h)} 2>/dev/null | grep -q '"is_primary_serving": *true'; then break; fi`,` ${n} PlannedReparentShard ${fe(h)} --new-primary ${fe(c)} >/dev/null 2>&1 && break`,` ${n} InitShardPrimary --force ${fe(h)} ${fe(c)} >/dev/null 2>&1 && break`," sleep 2","done",`if ! ${n} GetShard ${fe(h)} 2>/dev/null | grep -q '"is_primary_serving": *true'; then`,` echo "no primary could be elected for ${h}" >&2; exit 1`,"fi")}s.push(`${n} RebuildKeyspaceGraph ${fe(i.name)} >/dev/null`)}return s}function cs(e){let t=e.vtgatePort??cn;return["for i in $(seq 1 60); do",` if (cd ${u} && eval "$(pantry env 2>/dev/null)"; MYSQL_PWD=${fe(e.password??"")} ${Ce}/mysql -h 127.0.0.1 -P ${t} -u ${fe(e.username??"vitess")} --connect-timeout=2 -e 'SHOW KEYSPACES') >/dev/null 2>&1; then`,` echo "vtgate is serving on ${t}"; exit 0`," fi"," sleep 2","done",`echo "vtgate did not become ready on port ${t}" >&2`,"exit 1"]}function oo(e){if(!Lt(e))return[];let t=ua(e),r=t.mode==="combo";un.clear();let n=['if [ "$(uname -m)" != "x86_64" ]; then',' echo "vitess: Vitess publishes only x86_64 release tarballs; this box is $(uname -m). Drop managedServices.vitess or use an x86_64 host." >&2'," exit 1","fi",...Ae(pa(t)),`VT_MYSQL_LIB="$(dirname "$(dirname "$(readlink -f ${Ce}/mysqld)")")/lib"`,'if [ -d "$VT_MYSQL_LIB" ]; then',` printf '%s\\n' "$VT_MYSQL_LIB" > /etc/ld.so.conf.d/vitess-mysql.conf`," ldconfig","else",' echo "mysqld library directory not found under $VT_MYSQL_LIB" >&2; exit 1',"fi",`id -u ${Te} >/dev/null 2>&1 || useradd --system --home-dir ${Ue} --shell /usr/sbin/nologin ${Te}`,`mkdir -p ${Ue}`,`chown -R ${Te}:${Te} ${Ue}`];if(r)return n.push(...as("vitess-vtcombo",fa(t)),...is(),"systemctl daemon-reload","systemctl enable --now vitess-vtcombo.service",...cs(t)),n;n.push(...ya(t));let s=[];if(!t.etcdEndpoint)s.push(["vitess-etcd",ha()]);s.push(["vitess-vtctld",ga(t)],["vitess-mysqlctld",va(t)],["vitess-vtgate",_a(t)]);for(let i of t.keyspaces??[])for(let a of i.sharded?["-80","80-"]:["0"])s.push([`vitess-vttablet-${i.name}-${a.replace(/-/g,"x")}`,Sa(t,i.name,a)]);for(let[i,a]of s)n.push(...as(i,a));n.push(...is()),n.push("systemctl daemon-reload");for(let[i]of s)n.push(`systemctl enable --now ${i}.service`);return n.push(`if [ "$VTESS_VTGATE_AUTH_CHANGED" = 1 ]; then systemctl restart vitess-vtgate.service; printf '%s\\n' "$VTESS_VTGATE_AUTH_SHA" > ${At}; chown ${Te}:${Te} ${At}; chmod 0600 ${At}; fi`,...$a(t),...cs(t)),n}var ba=new Set(["singlestore","vitess"]);function Lr(e){if(e?.engine&&ba.has(e.engine))return!1;return!e?.host||e.host==="127.0.0.1"||e.host==="localhost"}function Ke(e,t="psql"){let r=e?.port??5432;if(Lr(e))return`${t} -p ${r} -U postgres`;let n=e?.username||"postgres";return`${e?.password?`PGPASSWORD='${e.password.replace(/'/g,"'\\''")}' `:""}${t} -h ${e.host} -p ${r} -U ${n} -w`}function ke(e){return e===!0||typeof e==="object"&&e!=null}function Ta(e){let t=[],r=[];if(ke(e.mysql))t.push(typeof e.mysql==="object"&&e.mysql.version?`mysql.com@${e.mysql.version}`:"mysql.com@8.0.43"),r.push("mysql");else if(ke(e.mariadb))t.push(xt.mariadb),r.push("mariadb");if(ke(e.postgres))t.push("postgresql.org"),r.push("postgres");if(ke(e.redis))t.push("redis.io"),r.push("redis");if(ke(e.memcached))t.push("memcached.org"),r.push("memcached");if(ke(e.meilisearch))t.push("meilisearch.com"),r.push("meilisearch");return{packages:t,services:r}}function cr(e={},t={}){let r=Ta(e),n=oo(e.vitess),s=t.mail?or(t.mail):[];if(r.packages.length===0)return[...n,...s];return[...Ae(r.packages),...sr(r.services),...n,...s]}function ct(e,t={}){if(e?.requireLocalPasswordAuth&&(e.engine!=="postgres"||!Lr(e)||!e.name||!e.username||!e.password||e.users?.some((b)=>!b.password)))throw Error("Password authentication requires a named local PostgreSQL database and nonempty login passwords");if(!e?.name)return[];if(!Lr(e))return[];let r=e.name,n=e.username||r,s=e.password||"",i=ke(t.postgres)||e.engine==="postgres",a=ke(t.mariadb)||e.engine==="mariadb",c=ke(t.mysql)||e.engine==="mysql";if(i&&!c&&!a){let b=(L)=>`"${L.replace(/"/g,'""')}"`,S=(L)=>`'${L.replace(/'/g,"''")}'`,P=(L,j)=>[...e.requireLocalPasswordAuth?["SET password_encryption = 'scram-sha-256';"]:[],"DO $$ BEGIN",` IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${S(L)}) THEN`,` CREATE ROLE ${b(L)} LOGIN PASSWORD ${S(j)};`," ELSE",` ALTER ROLE ${b(L)} LOGIN PASSWORD ${S(j)};`," END IF;","END $$;"],M=(L)=>{let j=L.databases&&L.databases.length>0?L.databases:[r],B=[];for(let Z of j)if(L.access==="readonly")B.push(`GRANT CONNECT ON DATABASE ${b(Z)} TO ${b(L.username)};`,`\\connect ${b(Z)}`,`GRANT USAGE ON SCHEMA public TO ${b(L.username)};`,`GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${b(L.username)};`,`ALTER DEFAULT PRIVILEGES FOR ROLE ${b(n)} IN SCHEMA public GRANT SELECT ON TABLES TO ${b(L.username)};`,"\\connect postgres");else B.push(`GRANT ALL PRIVILEGES ON DATABASE ${b(Z)} TO ${b(L.username)};`);return B},q=e.users||[],G=e.port??5432;return[Re(),`for i in $(seq 1 30); do pg_isready -p ${G} -q && break; sleep 2; done`,`${Ke(e)}${e.requireLocalPasswordAuth?" -v ON_ERROR_STOP=1":""} <<'TS_CLOUD_PG_EOF'`,...P(n,s),`SELECT 'CREATE DATABASE ${b(r)} OWNER ${b(n)}' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = ${S(r)})\\gexec`,...q.flatMap((L)=>[...P(L.username,L.password),...M(L)]),"TS_CLOUD_PG_EOF",...e.requireLocalPasswordAuth?[`TS_CLOUD_PG_HBA=$(${Ke(e)} -Atc 'SHOW hba_file')`,'case "$TS_CLOUD_PG_HBA" in /*) ;; *) echo "Invalid PostgreSQL authentication path" >&2; exit 1 ;; esac','[ -f "$TS_CLOUD_PG_HBA.before-ts-cloud-password-auth" ] || cp -p "$TS_CLOUD_PG_HBA" "$TS_CLOUD_PG_HBA.before-ts-cloud-password-auth"','TS_CLOUD_PG_AUTH=$(mktemp "$TS_CLOUD_PG_HBA.ts-cloud.XXXXXXXX")',"{"," printf '%s\\n' '# BEGIN ts-cloud password authentication' 'host all all 127.0.0.1/32 scram-sha-256' 'host all all ::1/128 scram-sha-256' 'host replication all 127.0.0.1/32 scram-sha-256' 'host replication all ::1/128 scram-sha-256' '# END ts-cloud password authentication'",` sed '/^# BEGIN ts-cloud password authentication$/,/^# END ts-cloud password authentication$/d' "$TS_CLOUD_PG_HBA"`,'} > "$TS_CLOUD_PG_AUTH"','chown --reference="$TS_CLOUD_PG_HBA" "$TS_CLOUD_PG_AUTH"','chmod --reference="$TS_CLOUD_PG_HBA" "$TS_CLOUD_PG_AUTH"','mv -f "$TS_CLOUD_PG_AUTH" "$TS_CLOUD_PG_HBA"',`${Ke(e)} -v ON_ERROR_STOP=1 -c 'SELECT pg_reload_conf()'`]:[],...wa(e)]}let f=a?"/var/lib/pantry/mariadb/mariadbd.sock":"/var/lib/pantry/mysql/mysqld.sock",h=(b)=>b.replace(/`/g,"``"),g=(b)=>b.replace(/\\/g,"\\\\").replace(/'/g,"\\'"),v=(b)=>{let S=b.databases&&b.databases.length>0?b.databases:[r],P=b.access==="readonly"?"SELECT":"ALL PRIVILEGES",M=[`CREATE USER IF NOT EXISTS '${g(b.username)}'@'%' IDENTIFIED BY '${g(b.password)}';`,`CREATE USER IF NOT EXISTS '${g(b.username)}'@'localhost' IDENTIFIED BY '${g(b.password)}';`,`ALTER USER '${g(b.username)}'@'%' IDENTIFIED BY '${g(b.password)}';`,`ALTER USER '${g(b.username)}'@'localhost' IDENTIFIED BY '${g(b.password)}';`];for(let q of S)M.push(`GRANT ${P} ON \`${h(q)}\`.* TO '${g(b.username)}'@'%';`,`GRANT ${P} ON \`${h(q)}\`.* TO '${g(b.username)}'@'localhost';`);return M},_=e.users||[];return[Re(),`for i in $(seq 1 30); do mysqladmin --socket=${f} -u root ping 2>/dev/null | grep -q alive && break; sleep 2; done`,`mysql --socket=${f} -u root <<'TS_CLOUD_SQL_EOF'`,`CREATE DATABASE IF NOT EXISTS \`${h(r)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`,...v({username:n,password:s}),..._.flatMap(v),"FLUSH PRIVILEGES;","TS_CLOUD_SQL_EOF"]}function wa(e){return[`${Ke(e)} -tA <<'TS_CLOUD_PG_CAP_EOF' || true`,"\\set QUIET on","SELECT CASE WHEN used::float / NULLIF(cap, 0) >= 0.8"," THEN '[ts-cloud] postgres is at ' || used || '/' || cap || ' connections on this server.'"," || ' Raise max_connections (ALTER SYSTEM SET max_connections = ...; then restart) or'"," || ' set idle_session_timeout so abandoned pools are reclaimed.'"," ELSE NULL END","FROM (SELECT (SELECT count(*) FROM pg_stat_activity) AS used,"," (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS cap) c","WHERE CASE WHEN used::float / NULLIF(cap, 0) >= 0.8 THEN true ELSE false END;","SELECT '[ts-cloud] ' || coalesce(datname, '(background)') || ': ' || count(*) || ' connection(s), oldest idle ' ||"," coalesce(round(extract(epoch from max(now() - state_change)) / 60)::text || 'm', 'n/a')","FROM pg_stat_activity","WHERE (SELECT count(*)::float / NULLIF((SELECT setting::int FROM pg_settings WHERE name = 'max_connections'), 0) FROM pg_stat_activity) >= 0.8","GROUP BY datname ORDER BY count(*) DESC;","TS_CLOUD_PG_CAP_EOF"]}function ps(e){if(!e?.name)return{};let t=e.engine==="singlestore",r=e.engine==="vitess",n=e.engine==="postgres"?"pgsql":t?"singlestore":r?"vitess":"mysql",s=e.port??(e.engine==="postgres"?5432:r?15306:3306),i={DB_CONNECTION:n,DB_HOST:e.host||"127.0.0.1",DB_PORT:String(s),DB_DATABASE:e.name};if(e.username)i.DB_USERNAME=e.username;if(e.password)i.DB_PASSWORD=e.password;if((t||r)&&e.ssl!==!1)i.DB_SSL="true";return i}var lr="/var/backups/ts-cloud",dn="/etc/ts-cloud/backups.config.ts",ds="/etc/cron.d/ts-cloud-backups",$r="/usr/local/bin/ts-cloud-backup.sh";function Ca(e){if(!e.name)return null;if(!["postgres","mysql","mariadb"].includes(e.engine||""))throw Error("Unsupported scheduled SQL backup engine");return JSON.stringify({type:e.engine==="postgres"?"postgresql":"mysql",name:e.name,connection:{hostname:e.host||"127.0.0.1",port:e.port??(e.engine==="postgres"?5432:3306),database:e.name,username:e.username||e.name,password:e.password||"",ssl:!1},includeSchema:!0,includeData:!0})}function Pa(e,t){let r=e?Ca(e):null,n=t.bucket?{type:"s3",bucket:t.bucket,prefix:t.prefix||"db-backups",...t.endpoint?{endpoint:t.endpoint}:{},...t.region?{region:t.region}:{},keepDays:t.retentionDays??30,optional:!1,...t.encryptionKey?{encryption:{keyEnv:"TS_CLOUD_BACKUP_ENCRYPTION_KEY"}}:{}}:void 0,s={verbose:!0,outputPath:lr,retention:{count:t.retentionCount??5,maxAge:t.retentionDays??30},databases:[],files:t.files||[],destinations:n?[n]:[]};return["import type { BackupConfig } from 'ts-backups'",`const config: BackupConfig = ${JSON.stringify(s,null,2)}`,`config.databases = [
18
18
  ${r||""}
19
19
  ]`,...t.credentials?["config.destinations![0]!.credentials = { accessKeyId: process.env.TS_CLOUD_BACKUP_ACCESS_KEY!, secretAccessKey: process.env.TS_CLOUD_BACKUP_SECRET_KEY! }"]:[],"export default config",""].join(`
20
- `)}function Ut(e){let{database:t,backups:r}=e;if(!r.enabled)return[];if(r.credentials&&(!r.bucket||!r.credentials.accessKeyId||!r.credentials.secretAccessKey))throw Error("Scheduled backups require complete destination credentials and a bucket");if(r.encryptionKey&&(r.encryptionKey.length<32||!r.bucket))throw Error("Encrypted offsite backups require a bucket and a separate recovery key of at least 32 characters");let n=r.version||"0.1.6",s=r.archive||(!r.version?{url:"https://github.com/stacksjs/ts-backups/releases/download/v0.1.6/ts-backups-0.1.6.tgz",sha256:"20b8295d8c092f9e0ed17239b0233e781d71466077b555f163d67dfcfa2a9744"}:void 0);if(!/^\d+\.\d+\.\d+$/.test(n))throw Error("Pin a released ts-backups version");let i=r.schedule||"0 2 * * *";if(!/^[0-9*,/ -]+$/.test(i)||i.trim().split(/\s+/).length!==5)throw Error("Invalid backup schedule");let a=(g)=>`'${g.replace(/'/g,"'\\''")}'`;if(s){let g=new URL(s.url);if(g.protocol!=="https:"||g.username||g.password||!/^[a-f0-9]{64}$/i.test(s.sha256))throw Error("Backup archives require an HTTPS URL and SHA-256 checksum")}let c=Object.entries({...r.credentials?{TS_CLOUD_BACKUP_ACCESS_KEY:r.credentials.accessKeyId,TS_CLOUD_BACKUP_SECRET_KEY:r.credentials.secretAccessKey}:{},...r.encryptionKey?{TS_CLOUD_BACKUP_ENCRYPTION_KEY:r.encryptionKey}:{}}).map(([g,h])=>`${g}=${a(h)}`).join(`
21
- `),f="/opt/ts-cloud/backups";return["(","umask 077",`mkdir -p /etc/ts-cloud ${lr} ${f}`,`chmod 700 ${lr} ${f}`,'export PATH="/opt/ts-cloud/bun/bin:/opt/pantry/pantry/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"',`test -f ${f}/package.json || printf '%s' '{"private":true}' > ${f}/package.json`,...s?[`curl --fail --silent --show-error --location ${a(s.url)} --output ${f}/release.tgz`,`printf '%s\\n' ${a(`${s.sha256.toLowerCase()} ${f}/release.tgz`)} | sha256sum --check --status`,`bun add --cwd ${f} --no-save ${f}/release.tgz`,`test "$(cd ${f} && bun -e 'console.log((await Bun.file("node_modules/ts-backups/package.json").json()).version)')" = ${a(n)}`]:[`bun add --cwd ${f} --no-save ts-backups@${n}`],`printf '%s' ${a(Ca(t,r))} > ${dn}`,`printf '%s\\n' ${a(c)} > /etc/ts-cloud/backups.env`,`chmod 600 ${dn} /etc/ts-cloud/backups.env`,`cat > ${$r} <<'TS_CLOUD_BACKUP_RUN_EOF'`,"#!/bin/bash","set -euo pipefail","umask 077","exec 9>/run/lock/ts-cloud-backups.lock","flock -n 9 || exit 0",'export PATH="/opt/ts-cloud/bun/bin:/opt/pantry/pantry/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"','eval "$(cd /opt/pantry && pantry env)"',"set -a",". /etc/ts-cloud/backups.env","set +a",'notify() { [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "$1" || true; }',"cd /etc/ts-cloud",`if ! ${f}/node_modules/.bin/ts-backups start --config ${dn}; then notify "ts-cloud backup failed"; exit 1; fi`,"date -u +%FT%TZ > /var/backups/ts-cloud/last-success","TS_CLOUD_BACKUP_RUN_EOF",`chmod 700 ${$r}`,`printf '%s\\n' ${a(`${i} root ${$r} >> /var/log/ts-cloud-backup.log 2>&1`)} > ${ds}`,`chmod 644 ${ds}`,")"]}function Pa(e){return e==="mariadb"?"/var/lib/pantry/mariadb/mariadbd.sock":"/var/lib/pantry/mysql/mysqld.sock"}function jg(e,t={}){if(!e?.name)return[];let r=e.name,n=e.engine==="postgres",s=t.from?`TS_CLOUD_DUMP="${t.from}"`:`TS_CLOUD_DUMP="$(find ${lr} -type f -name '*${r}*.sql' -o -type f -name '*${r}*.sql.gz' 2>/dev/null | xargs -r ls -1t 2>/dev/null | head -1)"`,i=n?`${Ke(e)} -d "${r}"`:`mysql --socket=${Pa(e.engine)} -u root "${r}"`;return["set -uo pipefail",'eval "$(cd /opt/pantry && pantry env 2>/dev/null)" || true',s,'[ -n "$TS_CLOUD_DUMP" ] && [ -f "$TS_CLOUD_DUMP" ] || { echo "no backup dump found to restore" >&2; exit 1; }','echo "[ts-cloud] restoring '+r+' from $TS_CLOUD_DUMP"',`case "$TS_CLOUD_DUMP" in *.gz) gunzip -c "$TS_CLOUD_DUMP" ;; *) cat "$TS_CLOUD_DUMP" ;; esac | ${i}`,'echo "restore complete"']}function Ea(e){return`/etc/ts-cloud/${e}-upgrade.env`}function Ra(e){return`${e}-upgrade`}function ao(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:a="upgrade",channel:c="stable",schedule:f="daily",randomizedDelay:g="4h",args:h=[]}=n,S=Ra(s),_=Ea(s),v=[`--path ${i}`,`--service ${s}`,...c==="canary"?["--canary"]:[],...h].join(" ");t.push(`if [ ! -f ${_} ]; then`,` cat > ${_} <<'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 ${_}`,`cat > /etc/systemd/system/${S}.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=-${_}`,`ExecStart=/bin/sh -c 'if [ "\${ENABLED:-true}" != "true" ]; then echo "auto-update disabled via ${_}"; exit 0; fi; exec ${i} ${a} ${v} $ARGS'`,"TS_CLOUD_APPUPD_SVC_EOF",`cat > /etc/systemd/system/${S}.timer <<'TS_CLOUD_APPUPD_TMR_EOF'`,"[Unit]",`Description=Scheduled ${s} update check`,"","[Timer]",`OnCalendar=${f}`,`RandomizedDelaySec=${g}`,"Persistent=true","","[Install]","WantedBy=timers.target","TS_CLOUD_APPUPD_TMR_EOF"),r.push(`systemctl enable --now ${S}.timer`)}if(r.length===0)return[];return[...t,"systemctl daemon-reload",...r]}function ze(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 ms=[["nginx",80],["https",443],["phpFpm",9074],["mysql",3306],["postgres",5432],["redis",6379],["meilisearch",7700],["typesense",8108],["smtp",25]];function Oa(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 He(e=!0){let{enabled:t,cpuLoadPerCore:r,memPercent:n,diskPercent:s,bandwidthTb:i,bandwidthPercent:a}=Oa(e);if(!t)return[];let c=Math.round(i*1000000000000),f=ms.map(([b,T])=>`SVC_${b.toUpperCase()}=$(probe ${T})`),g=ms.map(([b])=>`"${b}":"$SVC_${b.toUpperCase()}"`).join(","),h=(typeof e==="object"?e.filesystems:void 0)??["/"];if(!h.length)throw Error("monitoring.filesystems must include at least one path");let S=h.map((b)=>typeof b==="string"?{path:b,requiredMount:!1}:b);for(let b of S)if(b.path!=="/"&&(!b.path.startsWith("/")||!b.path.slice(1).split("/").every((T)=>/^[\w-]+$/.test(T))))throw Error(`Invalid monitoring filesystem path: ${b.path}`);let _=S.flatMap((b,T)=>[`DISK_${T}_MOUNTED=true; DISK_${T}_PCT=null`,...b.requiredMount?[`if ! mountpoint -q '${b.path}'; then DISK_${T}_MOUNTED=false; DISK_ALERTS="$DISK_ALERTS missing-mount=${b.path}"; ALERT_KEY="$ALERT_KEY mount:${b.path}"; fi`]:[],`if [ "$DISK_${T}_MOUNTED" = true ]; then`,` DISK_${T}_PCT=$(df -Pk '${b.path}' 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5}')`,` if [[ "$DISK_${T}_PCT" =~ ^[0-9]+$ ]]; then`,` [ "$DISK_${T}_PCT" -le "$DISK_PCT" ] || DISK_PCT=$DISK_${T}_PCT`,` if [ "$DISK_${T}_PCT" -ge ${s} ]; then DISK_ALERTS="$DISK_ALERTS disk=${b.path}:$DISK_${T}_PCT%"; ALERT_KEY="$ALERT_KEY disk:${b.path}"; fi`,` else DISK_${T}_PCT=null; DISK_ALERTS="$DISK_ALERTS unreadable-disk=${b.path}"; ALERT_KEY="$ALERT_KEY unreadable:${b.path}"; fi`,"fi"]),v=S.map((b,T)=>`{"path":"${b.path}","usedPercent":$DISK_${T}_PCT,"mounted":$DISK_${T}_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
20
+ `)}function Ut(e){let{database:t,backups:r}=e;if(!r.enabled)return[];if(r.credentials&&(!r.bucket||!r.credentials.accessKeyId||!r.credentials.secretAccessKey))throw Error("Scheduled backups require complete destination credentials and a bucket");if(r.encryptionKey&&(r.encryptionKey.length<32||!r.bucket))throw Error("Encrypted offsite backups require a bucket and a separate recovery key of at least 32 characters");let n=r.version||"0.1.6",s=r.archive||(!r.version?{url:"https://github.com/stacksjs/ts-backups/releases/download/v0.1.6/ts-backups-0.1.6.tgz",sha256:"20b8295d8c092f9e0ed17239b0233e781d71466077b555f163d67dfcfa2a9744"}:void 0);if(!/^\d+\.\d+\.\d+$/.test(n))throw Error("Pin a released ts-backups version");let i=r.schedule||"0 2 * * *";if(!/^[0-9*,/ -]+$/.test(i)||i.trim().split(/\s+/).length!==5)throw Error("Invalid backup schedule");let a=(h)=>`'${h.replace(/'/g,"'\\''")}'`;if(s){let h=new URL(s.url);if(h.protocol!=="https:"||h.username||h.password||!/^[a-f0-9]{64}$/i.test(s.sha256))throw Error("Backup archives require an HTTPS URL and SHA-256 checksum")}let c=Object.entries({...r.credentials?{TS_CLOUD_BACKUP_ACCESS_KEY:r.credentials.accessKeyId,TS_CLOUD_BACKUP_SECRET_KEY:r.credentials.secretAccessKey}:{},...r.encryptionKey?{TS_CLOUD_BACKUP_ENCRYPTION_KEY:r.encryptionKey}:{}}).map(([h,g])=>`${h}=${a(g)}`).join(`
21
+ `),f="/opt/ts-cloud/backups";return["(","umask 077",`mkdir -p /etc/ts-cloud ${lr} ${f}`,`chmod 700 ${lr} ${f}`,'export PATH="/opt/ts-cloud/bun/bin:/opt/pantry/pantry/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"',`test -f ${f}/package.json || printf '%s' '{"private":true}' > ${f}/package.json`,...s?[`curl --fail --silent --show-error --location ${a(s.url)} --output ${f}/release.tgz`,`printf '%s\\n' ${a(`${s.sha256.toLowerCase()} ${f}/release.tgz`)} | sha256sum --check --status`,`bun add --cwd ${f} --no-save ${f}/release.tgz`,`test "$(cd ${f} && bun -e 'console.log((await Bun.file("node_modules/ts-backups/package.json").json()).version)')" = ${a(n)}`]:[`bun add --cwd ${f} --no-save ts-backups@${n}`],`printf '%s' ${a(Pa(t,r))} > ${dn}`,`printf '%s\\n' ${a(c)} > /etc/ts-cloud/backups.env`,`chmod 600 ${dn} /etc/ts-cloud/backups.env`,`cat > ${$r} <<'TS_CLOUD_BACKUP_RUN_EOF'`,"#!/bin/bash","set -euo pipefail","umask 077","exec 9>/run/lock/ts-cloud-backups.lock","flock -n 9 || exit 0",'export PATH="/opt/ts-cloud/bun/bin:/opt/pantry/pantry/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"','eval "$(cd /opt/pantry && pantry env)"',"set -a",". /etc/ts-cloud/backups.env","set +a",'notify() { [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "$1" || true; }',"cd /etc/ts-cloud",`if ! ${f}/node_modules/.bin/ts-backups start --config ${dn}; then notify "ts-cloud backup failed"; exit 1; fi`,"date -u +%FT%TZ > /var/backups/ts-cloud/last-success","TS_CLOUD_BACKUP_RUN_EOF",`chmod 700 ${$r}`,`printf '%s\\n' ${a(`${i} root ${$r} >> /var/log/ts-cloud-backup.log 2>&1`)} > ${ds}`,`chmod 644 ${ds}`,")"]}function Ea(e){return e==="mariadb"?"/var/lib/pantry/mariadb/mariadbd.sock":"/var/lib/pantry/mysql/mysqld.sock"}function jg(e,t={}){if(!e?.name)return[];let r=e.name,n=e.engine==="postgres",s=t.from?`TS_CLOUD_DUMP="${t.from}"`:`TS_CLOUD_DUMP="$(find ${lr} -type f -name '*${r}*.sql' -o -type f -name '*${r}*.sql.gz' 2>/dev/null | xargs -r ls -1t 2>/dev/null | head -1)"`,i=n?`${Ke(e)} -d "${r}"`:`mysql --socket=${Ea(e.engine)} -u root "${r}"`;return["set -uo pipefail",'eval "$(cd /opt/pantry && pantry env 2>/dev/null)" || true',s,'[ -n "$TS_CLOUD_DUMP" ] && [ -f "$TS_CLOUD_DUMP" ] || { echo "no backup dump found to restore" >&2; exit 1; }','echo "[ts-cloud] restoring '+r+' from $TS_CLOUD_DUMP"',`case "$TS_CLOUD_DUMP" in *.gz) gunzip -c "$TS_CLOUD_DUMP" ;; *) cat "$TS_CLOUD_DUMP" ;; esac | ${i}`,'echo "restore complete"']}function Ra(e){return`/etc/ts-cloud/${e}-upgrade.env`}function Oa(e){return`${e}-upgrade`}function ao(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:a="upgrade",channel:c="stable",schedule:f="daily",randomizedDelay:h="4h",args:g=[]}=n,v=Oa(s),_=Ra(s),b=[`--path ${i}`,`--service ${s}`,...c==="canary"?["--canary"]:[],...g].join(" ");t.push(`if [ ! -f ${_} ]; then`,` cat > ${_} <<'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 ${_}`,`cat > /etc/systemd/system/${v}.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=-${_}`,`ExecStart=/bin/sh -c 'if [ "\${ENABLED:-true}" != "true" ]; then echo "auto-update disabled via ${_}"; exit 0; fi; exec ${i} ${a} ${b} $ARGS'`,"TS_CLOUD_APPUPD_SVC_EOF",`cat > /etc/systemd/system/${v}.timer <<'TS_CLOUD_APPUPD_TMR_EOF'`,"[Unit]",`Description=Scheduled ${s} update check`,"","[Timer]",`OnCalendar=${f}`,`RandomizedDelaySec=${h}`,"Persistent=true","","[Install]","WantedBy=timers.target","TS_CLOUD_APPUPD_TMR_EOF"),r.push(`systemctl enable --now ${v}.timer`)}if(r.length===0)return[];return[...t,"systemctl daemon-reload",...r]}function Fe(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 ms=[["nginx",80],["https",443],["phpFpm",9074],["mysql",3306],["postgres",5432],["redis",6379],["meilisearch",7700],["typesense",8108],["smtp",25]];function Da(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 He(e=!0){let{enabled:t,cpuLoadPerCore:r,memPercent:n,diskPercent:s,bandwidthTb:i,bandwidthPercent:a}=Da(e);if(!t)return[];let c=Math.round(i*1000000000000),f=ms.map(([S,P])=>`SVC_${S.toUpperCase()}=$(probe ${P})`),h=ms.map(([S])=>`"${S}":"$SVC_${S.toUpperCase()}"`).join(","),g=(typeof e==="object"?e.filesystems:void 0)??["/"];if(!g.length)throw Error("monitoring.filesystems must include at least one path");let v=g.map((S)=>typeof S==="string"?{path:S,requiredMount:!1}:S);for(let S of v)if(S.path!=="/"&&(!S.path.startsWith("/")||!S.path.slice(1).split("/").every((P)=>/^[\w-]+$/.test(P))))throw Error(`Invalid monitoring filesystem path: ${S.path}`);let _=v.flatMap((S,P)=>[`DISK_${P}_MOUNTED=true; DISK_${P}_PCT=null`,...S.requiredMount?[`if ! mountpoint -q '${S.path}'; then DISK_${P}_MOUNTED=false; DISK_ALERTS="$DISK_ALERTS missing-mount=${S.path}"; ALERT_KEY="$ALERT_KEY mount:${S.path}"; fi`]:[],`if [ "$DISK_${P}_MOUNTED" = true ]; then`,` DISK_${P}_PCT=$(df -Pk '${S.path}' 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5}')`,` if [[ "$DISK_${P}_PCT" =~ ^[0-9]+$ ]]; then`,` [ "$DISK_${P}_PCT" -le "$DISK_PCT" ] || DISK_PCT=$DISK_${P}_PCT`,` if [ "$DISK_${P}_PCT" -ge ${s} ]; then DISK_ALERTS="$DISK_ALERTS disk=${S.path}:$DISK_${P}_PCT%"; ALERT_KEY="$ALERT_KEY disk:${S.path}"; fi`,` else DISK_${P}_PCT=null; DISK_ALERTS="$DISK_ALERTS unreadable-disk=${S.path}"; ALERT_KEY="$ALERT_KEY unreadable:${S.path}"; fi`,"fi"]),b=v.map((S,P)=>`{"path":"${S.path}","usedPercent":$DISK_${P}_PCT,"mounted":$DISK_${P}_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
22
22
  $(free -m | awk '/^Mem:/{total=$2; used=$3} /^Swap:/{print total, used, $2, $3}')
23
- EOF`,'DISK_PCT=0; DISK_ALERTS=""; ALERT_KEY=""',..._,"UPTIME_SEC=$(cut -d' ' -f1 /proc/uptime | cut -d. -f1)",`RX_BYTES=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{rx+=$3} END{print rx+0}' /proc/net/dev)`,`TX_BYTES=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{tx+=$11} END{print tx+0}' /proc/net/dev)`,"NOW_EPOCH=$(date -u +%s); TODAY=$(date -u +%Y-%m-%d); MONTH=$(date -u +%Y-%m)","if [ -r /var/lib/ts-cloud/bandwidth-state ]; then . /var/lib/ts-cloud/bandwidth-state; fi","PREV_EPOCH=${PREV_EPOCH:-0}; PREV_RX=${PREV_RX:-0}; PREV_TX=${PREV_TX:-0}","DAY_KEY=${DAY_KEY:-$TODAY}; MONTH_KEY=${MONTH_KEY:-$MONTH}","DAY_RX=${DAY_RX:-0}; DAY_TX=${DAY_TX:-0}; MONTH_RX=${MONTH_RX:-0}; MONTH_TX=${MONTH_TX:-0}","ELAPSED=$(( NOW_EPOCH - PREV_EPOCH ))",'if [ "$PREV_EPOCH" -le 0 ] || [ "$ELAPSED" -le 0 ] || [ "$RX_BYTES" -lt "$PREV_RX" ] || [ "$TX_BYTES" -lt "$PREV_TX" ]; then'," DELTA_RX=0; DELTA_TX=0; ELAPSED=0","else"," DELTA_RX=$(( RX_BYTES - PREV_RX )); DELTA_TX=$(( TX_BYTES - PREV_TX ))","fi",'if [ "$DAY_KEY" != "$TODAY" ]; then DAY_KEY=$TODAY; DAY_RX=0; DAY_TX=0; fi','if [ "$MONTH_KEY" != "$MONTH" ]; then MONTH_KEY=$MONTH; MONTH_RX=0; MONTH_TX=0; fi',"DAY_RX=$(( DAY_RX + DELTA_RX )); DAY_TX=$(( DAY_TX + DELTA_TX ))","MONTH_RX=$(( MONTH_RX + DELTA_RX )); MONTH_TX=$(( MONTH_TX + DELTA_TX ))",'if [ "$ELAPSED" -gt 0 ]; then RX_RATE=$(( DELTA_RX / ELAPSED )); TX_RATE=$(( DELTA_TX / ELAPSED )); else RX_RATE=0; TX_RATE=0; fi',"cat > /var/lib/ts-cloud/bandwidth-state.tmp <<BWSTATE","PREV_EPOCH=$NOW_EPOCH","PREV_RX=$RX_BYTES","PREV_TX=$TX_BYTES","RX_RATE=$RX_RATE","TX_RATE=$TX_RATE","DAY_KEY=$DAY_KEY","DAY_RX=$DAY_RX","DAY_TX=$DAY_TX","MONTH_KEY=$MONTH_KEY","MONTH_RX=$MONTH_RX","MONTH_TX=$MONTH_TX","BWSTATE","mv -f /var/lib/ts-cloud/bandwidth-state.tmp /var/lib/ts-cloud/bandwidth-state","probe(){ (exec 3<>/dev/tcp/127.0.0.1/$1) 2>/dev/null && echo up || echo down; }",...f,"LOAD=${LOAD:-0}; CPUS=${CPUS:-1}; MEM_TOTAL=${MEM_TOTAL:-0}; MEM_USED=${MEM_USED:-0}","SWAP_TOTAL=${SWAP_TOTAL:-0}; SWAP_USED=${SWAP_USED:-0}; DISK_PCT=${DISK_PCT:-0}","UPTIME_SEC=${UPTIME_SEC:-0}; RX_BYTES=${RX_BYTES:-0}; TX_BYTES=${TX_BYTES:-0}","MEM_PCT=$(( MEM_TOTAL > 0 ? MEM_USED * 100 / MEM_TOTAL : 0 ))","cat > /var/lib/ts-cloud/metrics.json.tmp <<JSON",`{"load":$LOAD,"cpus":$CPUS,"memTotalMb":$MEM_TOTAL,"memUsedMb":$MEM_USED,"memUsedPct":$MEM_PCT,"swapTotalMb":$SWAP_TOTAL,"swapUsedMb":$SWAP_USED,"diskUsedPct":$DISK_PCT,"uptimeSec":$UPTIME_SEC,"disks":[${v}],"network":{"rxBytes":$RX_BYTES,"txBytes":$TX_BYTES,"rxBytesPerSec":$RX_RATE,"txBytesPerSec":$TX_RATE,"dayKey":"$DAY_KEY","rxBytesToday":$DAY_RX,"txBytesToday":$DAY_TX,"monthKey":"$MONTH_KEY","rxBytesMonth":$MONTH_RX,"txBytesMonth":$MONTH_TX,"budgetBytes":${c}},"services":{`+g+"}}","JSON","mv -f /var/lib/ts-cloud/metrics.json.tmp /var/lib/ts-cloud/metrics.json",'ALERTS="$DISK_ALERTS"',`if awk -v l="$LOAD" -v c="$CPUS" -v t=${r} 'BEGIN{exit !(c>0 && l/c > t)}'; then ALERTS="$ALERTS load=$LOAD/${r}xCPU"; ALERT_KEY="$ALERT_KEY cpu"; fi`,`if [ "\${MEM_PCT:-0}" -ge ${n} ]; then ALERTS="$ALERTS mem=\${MEM_PCT}%"; ALERT_KEY="$ALERT_KEY memory"; fi`,"PREV=$(cat /var/lib/ts-cloud/alert-state 2>/dev/null || echo ok)",'if [ -n "$ALERTS" ]; then',` if [ "$PREV" != "$ALERT_KEY" ] && [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): resource alert:$ALERTS"; then printf '%s\\n' "$ALERT_KEY" > ${"/var/lib/ts-cloud/alert-state"}; fi`,"else",` if [ "$PREV" != ok ] && [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "✅ $(hostname): resource usage back to normal"; then echo ok > ${"/var/lib/ts-cloud/alert-state"}; fi`,"fi",...c>0?["BW_TOTAL=$(( MONTH_RX + MONTH_TX ))",`BW_PCT=$(( BW_TOTAL * 100 / ${c} ))`,"BW_PREV=$(cat /var/lib/ts-cloud/bandwidth-alert-state 2>/dev/null || echo ok)",`if [ "$BW_PCT" -ge ${a} ]; then`,` if [ "$BW_PREV" != alert ] && [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): bandwidth at \${BW_PCT}% of the ${i} TB monthly allowance ($MONTH_KEY)"; then echo alert > ${"/var/lib/ts-cloud/bandwidth-alert-state"}; fi`,"else",` if [ "$BW_PREV" = alert ] && [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "✅ $(hostname): bandwidth back under the monthly allowance (\${BW_PCT}%)"; then echo ok > ${"/var/lib/ts-cloud/bandwidth-alert-state"}; fi`,"fi"]:[],"TS_CLOUD_METRICS_EOF","chmod +x /usr/local/bin/ts-cloud-metrics.sh","cat > /etc/systemd/system/ts-cloud-metrics.service <<'TS_CLOUD_METRICS_SVC_EOF'","[Unit]","Description=ts-cloud metrics collector","","[Service]","Type=oneshot","ExecStart=/usr/local/bin/ts-cloud-metrics.sh","TS_CLOUD_METRICS_SVC_EOF","cat > /etc/systemd/system/ts-cloud-metrics.timer <<'TS_CLOUD_METRICS_TMR_EOF'","[Unit]","Description=Run ts-cloud metrics collector every minute","","[Timer]","OnCalendar=*-*-* *:*:00","AccuracySec=1s","RandomizedDelaySec=5s","Persistent=true","","[Install]","WantedBy=timers.target","TS_CLOUD_METRICS_TMR_EOF","systemctl daemon-reload","systemctl enable --now ts-cloud-metrics.timer","systemctl start ts-cloud-metrics.service"]}var Da={"opcache.enable":"1","opcache.enable_cli":"1","opcache.memory_consumption":"256","opcache.interned_strings_buffer":"16","opcache.max_accelerated_files":"20000","opcache.validate_timestamps":"0","opcache.revalidate_freq":"0","opcache.save_comments":"1","opcache.fast_shutdown":"1",realpath_cache_size:"4096K",realpath_cache_ttl:"600"},xa="127.0.0.1:9074";function fs(e){return xa}function Aa(e={}){let t=e.versions?.length?e.versions:["8.3"];return e.default&&t.includes(e.default)?e.default:t[0]}function Ia(e={}){return{...e.optimizeForProduction===!1?{}:{...Da},...e.ini||{}}}function La(e={}){let t=Ia(e),r=Object.keys(t);if(r.length===0)return[];let n=r.map((i)=>`${i}=${t[i]}`),s="ts-cloud-managed";return[Re(),"TS_CLOUD_SCAN_DIR=$(php -i 2>/dev/null | awk -F' => ' '/^Scan this dir for additional .ini files/{print $2}' | head -1)",`TS_CLOUD_LOADED_INI=$(php -r 'echo php_ini_loaded_file() ?: "";' 2>/dev/null)`,'if [ -n "$TS_CLOUD_SCAN_DIR" ] && [ "$TS_CLOUD_SCAN_DIR" != "(none)" ]; then',' mkdir -p "$TS_CLOUD_SCAN_DIR"',' TS_CLOUD_PHP_INI="$TS_CLOUD_SCAN_DIR/zz-ts-cloud.ini"',` cat > "$TS_CLOUD_PHP_INI" <<'TS_CLOUD_PHPINI_EOF'`,`; ${s} — production PHP tuning`,...n,"TS_CLOUD_PHPINI_EOF","else",' if [ -z "$TS_CLOUD_LOADED_INI" ]; then'," TS_CLOUD_INI_DIR=$(php -i 2>/dev/null | awk -F' => ' '/^Configuration File \\(php.ini\\) Path/{print $2}' | head -1)",' TS_CLOUD_LOADED_INI="$TS_CLOUD_INI_DIR/php.ini"',' mkdir -p "$TS_CLOUD_INI_DIR"',' touch "$TS_CLOUD_LOADED_INI"'," fi",` sed -i '/; ${s} BEGIN/,/; ${s} END/d' "$TS_CLOUD_LOADED_INI"`,` cat >> "$TS_CLOUD_LOADED_INI" <<'TS_CLOUD_PHPINI_EOF'`,`; ${s} BEGIN`,...n,`; ${s} END`,"TS_CLOUD_PHPINI_EOF","fi",`(cd ${u} && pantry restart php-fpm) 2>/dev/null || true`]}function ur(e={}){let t=Aa(e),r=e.installNginx!==!1,n=e.installComposer!==!1,s=[`php.net@${t}`];if(n)s.push("getcomposer.org");if(r)s.push("nginx.org");return[...Ae(s),...sr(["php-fpm"]),...La(e)]}function mn(e,t){if(!e)return[];let r=[];if(e.template&&t?.[e.template])r.push(...t[e.template]);if(e.serverSnippet)r.push(...e.serverSnippet);return r}function Ua(e){if(!e)return"";let t=typeof e==="object"?e:{},n=[`max-age=${t.maxAge??31536000}`];if(t.includeSubDomains??!0)n.push("includeSubDomains");if(t.preload)n.push("preload");return` add_header Strict-Transport-Security "${n.join("; ")}" always;`}function ka(e){if(!e||!e.allow?.length&&!e.deny?.length)return[];let t=[];for(let r of e.deny||[])t.push(` deny ${r};`);if(e.allow?.length){for(let r of e.allow)t.push(` allow ${r};`);t.push(" deny all;")}return t}function gs(e){return`/etc/nginx/.htpasswd-${e}`}var Ha=new Set(["laravel","php","statamic","wordpress"]);function Na(e){return Ha.has(e)}function Ma(e){return e==="laravel"||e==="statamic"?"public":""}function za(e,t){let r=e.replace(/\/+$/,""),n=t.replace(/^\/+|\/+$/g,"");return n?`${r}/${n}`:r}function Fa(e){let t=e.type??"laravel",r=e.webDirectory??Ma(t),n=za(e.appDir,r),s=Na(t),i=e.phpVersion??"8.3",a=[` root ${n};`,"",' add_header X-Frame-Options "SAMEORIGIN";',' add_header X-Content-Type-Options "nosniff";'],c=Ua(e.hsts);if(c)a.push(c);for(let f of ka(e.security))a.push(f);if(a.push("",` index ${s?"index.php index.html":"index.html index.htm"};`,""," charset utf-8;",""),e.clientMaxBodySize)a.push(` client_max_body_size ${e.clientMaxBodySize};`,"");if(e.auth)a.push(` auth_basic "${e.auth.realm||"Restricted"}";`,` auth_basic_user_file ${gs(e.siteName)};`,"");for(let[f,g]of Object.entries(e.redirects||{}))a.push(` location = ${f} { return 301 ${g}; }`);if(Object.keys(e.redirects||{}).length>0)a.push("");if(s){if(a.push(" location / {"," try_files $uri $uri/ /index.php?$query_string;"," }",""," location = /favicon.ico { access_log off; log_not_found off; }"," location = /robots.txt { access_log off; log_not_found off; }",""," error_page 404 /index.php;",""," location ~ \\.php$ {",` fastcgi_pass ${e.fastcgiPass??fs(i)};`," fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;"," include fastcgi_params;"," }",""," location ~ /\\.(?!well-known).* {"," deny all;"," }"),t==="wordpress")a.push(""," location = /xmlrpc.php { deny all; }"," location ~* /(?:wp-config\\.php|readme\\.html|license\\.txt)$ { deny all; }"," location ~* /wp-content/uploads/.*\\.php$ { deny all; }")}else if(t==="spa")a.push(" location / {"," try_files $uri $uri/ /index.html;"," }");else a.push(" location / {"," try_files $uri $uri/ =404;"," }");if(e.serverSnippet&&e.serverSnippet.length>0){a.push("");for(let f of e.serverSnippet)a.push(f?` ${f}`:"")}return a}function Ba(e){let t=[e.domain,...e.aliases||[]].filter(Boolean);for(let i of t)if(!/^(?=.{1,253}$)(?:\*\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(i.trim()))throw Error(`Refusing to build a vhost: '${i}' is not a valid hostname.`);if(!t.length)throw Error("Refusing to build a vhost: no server_name (domain) was given.");let r=t.join(" "),n=Fa(e);if(e.ssl){let i=["server {"," listen 80;"," listen [::]:80;",` server_name ${r};`," return 301 https://$host$request_uri;","}"],a=["server {"," listen 443 ssl;"," listen [::]:443 ssl;",` server_name ${r};`,` ssl_certificate ${e.ssl.certPath};`,` ssl_certificate_key ${e.ssl.keyPath};`,...e.tlsProtocols?.length?[` ssl_protocols ${e.tlsProtocols.join(" ")};`]:[],"",...n,"}"];return`${[...i,"",...a].join(`
23
+ EOF`,'DISK_PCT=0; DISK_ALERTS=""; ALERT_KEY=""',..._,"UPTIME_SEC=$(cut -d' ' -f1 /proc/uptime | cut -d. -f1)",`RX_BYTES=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{rx+=$3} END{print rx+0}' /proc/net/dev)`,`TX_BYTES=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{tx+=$11} END{print tx+0}' /proc/net/dev)`,"NOW_EPOCH=$(date -u +%s); TODAY=$(date -u +%Y-%m-%d); MONTH=$(date -u +%Y-%m)","if [ -r /var/lib/ts-cloud/bandwidth-state ]; then . /var/lib/ts-cloud/bandwidth-state; fi","PREV_EPOCH=${PREV_EPOCH:-0}; PREV_RX=${PREV_RX:-0}; PREV_TX=${PREV_TX:-0}","DAY_KEY=${DAY_KEY:-$TODAY}; MONTH_KEY=${MONTH_KEY:-$MONTH}","DAY_RX=${DAY_RX:-0}; DAY_TX=${DAY_TX:-0}; MONTH_RX=${MONTH_RX:-0}; MONTH_TX=${MONTH_TX:-0}","ELAPSED=$(( NOW_EPOCH - PREV_EPOCH ))",'if [ "$PREV_EPOCH" -le 0 ] || [ "$ELAPSED" -le 0 ] || [ "$RX_BYTES" -lt "$PREV_RX" ] || [ "$TX_BYTES" -lt "$PREV_TX" ]; then'," DELTA_RX=0; DELTA_TX=0; ELAPSED=0","else"," DELTA_RX=$(( RX_BYTES - PREV_RX )); DELTA_TX=$(( TX_BYTES - PREV_TX ))","fi",'if [ "$DAY_KEY" != "$TODAY" ]; then DAY_KEY=$TODAY; DAY_RX=0; DAY_TX=0; fi','if [ "$MONTH_KEY" != "$MONTH" ]; then MONTH_KEY=$MONTH; MONTH_RX=0; MONTH_TX=0; fi',"DAY_RX=$(( DAY_RX + DELTA_RX )); DAY_TX=$(( DAY_TX + DELTA_TX ))","MONTH_RX=$(( MONTH_RX + DELTA_RX )); MONTH_TX=$(( MONTH_TX + DELTA_TX ))",'if [ "$ELAPSED" -gt 0 ]; then RX_RATE=$(( DELTA_RX / ELAPSED )); TX_RATE=$(( DELTA_TX / ELAPSED )); else RX_RATE=0; TX_RATE=0; fi',"cat > /var/lib/ts-cloud/bandwidth-state.tmp <<BWSTATE","PREV_EPOCH=$NOW_EPOCH","PREV_RX=$RX_BYTES","PREV_TX=$TX_BYTES","RX_RATE=$RX_RATE","TX_RATE=$TX_RATE","DAY_KEY=$DAY_KEY","DAY_RX=$DAY_RX","DAY_TX=$DAY_TX","MONTH_KEY=$MONTH_KEY","MONTH_RX=$MONTH_RX","MONTH_TX=$MONTH_TX","BWSTATE","mv -f /var/lib/ts-cloud/bandwidth-state.tmp /var/lib/ts-cloud/bandwidth-state","probe(){ (exec 3<>/dev/tcp/127.0.0.1/$1) 2>/dev/null && echo up || echo down; }",...f,"LOAD=${LOAD:-0}; CPUS=${CPUS:-1}; MEM_TOTAL=${MEM_TOTAL:-0}; MEM_USED=${MEM_USED:-0}","SWAP_TOTAL=${SWAP_TOTAL:-0}; SWAP_USED=${SWAP_USED:-0}; DISK_PCT=${DISK_PCT:-0}","UPTIME_SEC=${UPTIME_SEC:-0}; RX_BYTES=${RX_BYTES:-0}; TX_BYTES=${TX_BYTES:-0}","MEM_PCT=$(( MEM_TOTAL > 0 ? MEM_USED * 100 / MEM_TOTAL : 0 ))","cat > /var/lib/ts-cloud/metrics.json.tmp <<JSON",`{"load":$LOAD,"cpus":$CPUS,"memTotalMb":$MEM_TOTAL,"memUsedMb":$MEM_USED,"memUsedPct":$MEM_PCT,"swapTotalMb":$SWAP_TOTAL,"swapUsedMb":$SWAP_USED,"diskUsedPct":$DISK_PCT,"uptimeSec":$UPTIME_SEC,"disks":[${b}],"network":{"rxBytes":$RX_BYTES,"txBytes":$TX_BYTES,"rxBytesPerSec":$RX_RATE,"txBytesPerSec":$TX_RATE,"dayKey":"$DAY_KEY","rxBytesToday":$DAY_RX,"txBytesToday":$DAY_TX,"monthKey":"$MONTH_KEY","rxBytesMonth":$MONTH_RX,"txBytesMonth":$MONTH_TX,"budgetBytes":${c}},"services":{`+h+"}}","JSON","mv -f /var/lib/ts-cloud/metrics.json.tmp /var/lib/ts-cloud/metrics.json",'ALERTS="$DISK_ALERTS"',`if awk -v l="$LOAD" -v c="$CPUS" -v t=${r} 'BEGIN{exit !(c>0 && l/c > t)}'; then ALERTS="$ALERTS load=$LOAD/${r}xCPU"; ALERT_KEY="$ALERT_KEY cpu"; fi`,`if [ "\${MEM_PCT:-0}" -ge ${n} ]; then ALERTS="$ALERTS mem=\${MEM_PCT}%"; ALERT_KEY="$ALERT_KEY memory"; fi`,"PREV=$(cat /var/lib/ts-cloud/alert-state 2>/dev/null || echo ok)",'if [ -n "$ALERTS" ]; then',` if [ "$PREV" != "$ALERT_KEY" ] && [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): resource alert:$ALERTS"; then printf '%s\\n' "$ALERT_KEY" > ${"/var/lib/ts-cloud/alert-state"}; fi`,"else",` if [ "$PREV" != ok ] && [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "✅ $(hostname): resource usage back to normal"; then echo ok > ${"/var/lib/ts-cloud/alert-state"}; fi`,"fi",...c>0?["BW_TOTAL=$(( MONTH_RX + MONTH_TX ))",`BW_PCT=$(( BW_TOTAL * 100 / ${c} ))`,"BW_PREV=$(cat /var/lib/ts-cloud/bandwidth-alert-state 2>/dev/null || echo ok)",`if [ "$BW_PCT" -ge ${a} ]; then`,` if [ "$BW_PREV" != alert ] && [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): bandwidth at \${BW_PCT}% of the ${i} TB monthly allowance ($MONTH_KEY)"; then echo alert > ${"/var/lib/ts-cloud/bandwidth-alert-state"}; fi`,"else",` if [ "$BW_PREV" = alert ] && [ -x /usr/local/bin/ts-cloud-notify ] && /usr/local/bin/ts-cloud-notify "✅ $(hostname): bandwidth back under the monthly allowance (\${BW_PCT}%)"; then echo ok > ${"/var/lib/ts-cloud/bandwidth-alert-state"}; fi`,"fi"]:[],"TS_CLOUD_METRICS_EOF","chmod +x /usr/local/bin/ts-cloud-metrics.sh","cat > /etc/systemd/system/ts-cloud-metrics.service <<'TS_CLOUD_METRICS_SVC_EOF'","[Unit]","Description=ts-cloud metrics collector","","[Service]","Type=oneshot","ExecStart=/usr/local/bin/ts-cloud-metrics.sh","TS_CLOUD_METRICS_SVC_EOF","cat > /etc/systemd/system/ts-cloud-metrics.timer <<'TS_CLOUD_METRICS_TMR_EOF'","[Unit]","Description=Run ts-cloud metrics collector every minute","","[Timer]","OnCalendar=*-*-* *:*:00","AccuracySec=1s","RandomizedDelaySec=5s","Persistent=true","","[Install]","WantedBy=timers.target","TS_CLOUD_METRICS_TMR_EOF","systemctl daemon-reload","systemctl enable --now ts-cloud-metrics.timer","systemctl start ts-cloud-metrics.service"]}var xa={"opcache.enable":"1","opcache.enable_cli":"1","opcache.memory_consumption":"256","opcache.interned_strings_buffer":"16","opcache.max_accelerated_files":"20000","opcache.validate_timestamps":"0","opcache.revalidate_freq":"0","opcache.save_comments":"1","opcache.fast_shutdown":"1",realpath_cache_size:"4096K",realpath_cache_ttl:"600"},Aa="127.0.0.1:9074";function fs(e){return Aa}function Ia(e={}){let t=e.versions?.length?e.versions:["8.3"];return e.default&&t.includes(e.default)?e.default:t[0]}function La(e={}){return{...e.optimizeForProduction===!1?{}:{...xa},...e.ini||{}}}function Ua(e={}){let t=La(e),r=Object.keys(t);if(r.length===0)return[];let n=r.map((i)=>`${i}=${t[i]}`),s="ts-cloud-managed";return[Re(),"TS_CLOUD_SCAN_DIR=$(php -i 2>/dev/null | awk -F' => ' '/^Scan this dir for additional .ini files/{print $2}' | head -1)",`TS_CLOUD_LOADED_INI=$(php -r 'echo php_ini_loaded_file() ?: "";' 2>/dev/null)`,'if [ -n "$TS_CLOUD_SCAN_DIR" ] && [ "$TS_CLOUD_SCAN_DIR" != "(none)" ]; then',' mkdir -p "$TS_CLOUD_SCAN_DIR"',' TS_CLOUD_PHP_INI="$TS_CLOUD_SCAN_DIR/zz-ts-cloud.ini"',` cat > "$TS_CLOUD_PHP_INI" <<'TS_CLOUD_PHPINI_EOF'`,`; ${s} — production PHP tuning`,...n,"TS_CLOUD_PHPINI_EOF","else",' if [ -z "$TS_CLOUD_LOADED_INI" ]; then'," TS_CLOUD_INI_DIR=$(php -i 2>/dev/null | awk -F' => ' '/^Configuration File \\(php.ini\\) Path/{print $2}' | head -1)",' TS_CLOUD_LOADED_INI="$TS_CLOUD_INI_DIR/php.ini"',' mkdir -p "$TS_CLOUD_INI_DIR"',' touch "$TS_CLOUD_LOADED_INI"'," fi",` sed -i '/; ${s} BEGIN/,/; ${s} END/d' "$TS_CLOUD_LOADED_INI"`,` cat >> "$TS_CLOUD_LOADED_INI" <<'TS_CLOUD_PHPINI_EOF'`,`; ${s} BEGIN`,...n,`; ${s} END`,"TS_CLOUD_PHPINI_EOF","fi",`(cd ${u} && pantry restart php-fpm) 2>/dev/null || true`]}function ur(e={}){let t=Ia(e),r=e.installNginx!==!1,n=e.installComposer!==!1,s=[`php.net@${t}`];if(n)s.push("getcomposer.org");if(r)s.push("nginx.org");return[...Ae(s),...sr(["php-fpm"]),...Ua(e)]}function mn(e,t){if(!e)return[];let r=[];if(e.template&&t?.[e.template])r.push(...t[e.template]);if(e.serverSnippet)r.push(...e.serverSnippet);return r}function ka(e){if(!e)return"";let t=typeof e==="object"?e:{},n=[`max-age=${t.maxAge??31536000}`];if(t.includeSubDomains??!0)n.push("includeSubDomains");if(t.preload)n.push("preload");return` add_header Strict-Transport-Security "${n.join("; ")}" always;`}function Ha(e){if(!e||!e.allow?.length&&!e.deny?.length)return[];let t=[];for(let r of e.deny||[])t.push(` deny ${r};`);if(e.allow?.length){for(let r of e.allow)t.push(` allow ${r};`);t.push(" deny all;")}return t}function hs(e){return`/etc/nginx/.htpasswd-${e}`}var Na=new Set(["laravel","php","statamic","wordpress"]);function Ma(e){return Na.has(e)}function Fa(e){return e==="laravel"||e==="statamic"?"public":""}function za(e,t){let r=e.replace(/\/+$/,""),n=t.replace(/^\/+|\/+$/g,"");return n?`${r}/${n}`:r}function Ba(e){let t=e.type??"laravel",r=e.webDirectory??Fa(t),n=za(e.appDir,r),s=Ma(t),i=e.phpVersion??"8.3",a=[` root ${n};`,"",' add_header X-Frame-Options "SAMEORIGIN";',' add_header X-Content-Type-Options "nosniff";'],c=ka(e.hsts);if(c)a.push(c);for(let f of Ha(e.security))a.push(f);if(a.push("",` index ${s?"index.php index.html":"index.html index.htm"};`,""," charset utf-8;",""),e.clientMaxBodySize)a.push(` client_max_body_size ${e.clientMaxBodySize};`,"");if(e.auth)a.push(` auth_basic "${e.auth.realm||"Restricted"}";`,` auth_basic_user_file ${hs(e.siteName)};`,"");for(let[f,h]of Object.entries(e.redirects||{}))a.push(` location = ${f} { return 301 ${h}; }`);if(Object.keys(e.redirects||{}).length>0)a.push("");if(s){if(a.push(" location / {"," try_files $uri $uri/ /index.php?$query_string;"," }",""," location = /favicon.ico { access_log off; log_not_found off; }"," location = /robots.txt { access_log off; log_not_found off; }",""," error_page 404 /index.php;",""," location ~ \\.php$ {",` fastcgi_pass ${e.fastcgiPass??fs(i)};`," fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;"," include fastcgi_params;"," }",""," location ~ /\\.(?!well-known).* {"," deny all;"," }"),t==="wordpress")a.push(""," location = /xmlrpc.php { deny all; }"," location ~* /(?:wp-config\\.php|readme\\.html|license\\.txt)$ { deny all; }"," location ~* /wp-content/uploads/.*\\.php$ { deny all; }")}else if(t==="spa")a.push(" location / {"," try_files $uri $uri/ /index.html;"," }");else a.push(" location / {"," try_files $uri $uri/ =404;"," }");if(e.serverSnippet&&e.serverSnippet.length>0){a.push("");for(let f of e.serverSnippet)a.push(f?` ${f}`:"")}return a}function Ka(e){let t=[e.domain,...e.aliases||[]].filter(Boolean);for(let i of t)if(!/^(?=.{1,253}$)(?:\*\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(i.trim()))throw Error(`Refusing to build a vhost: '${i}' is not a valid hostname.`);if(!t.length)throw Error("Refusing to build a vhost: no server_name (domain) was given.");let r=t.join(" "),n=Ba(e);if(e.ssl){let i=["server {"," listen 80;"," listen [::]:80;",` server_name ${r};`," return 301 https://$host$request_uri;","}"],a=["server {"," listen 443 ssl;"," listen [::]:443 ssl;",` server_name ${r};`,` ssl_certificate ${e.ssl.certPath};`,` ssl_certificate_key ${e.ssl.keyPath};`,...e.tlsProtocols?.length?[` ssl_protocols ${e.tlsProtocols.join(" ")};`]:[],"",...n,"}"];return`${[...i,"",...a].join(`
24
24
  `)}
25
25
  `}return`${["server {"," listen 80;"," listen [::]:80;",` server_name ${r};`,...n,"}"].join(`
26
26
  `)}
27
- `}function fn(e){let t=`/etc/nginx/sites-available/${e.siteName}`,r=`/etc/nginx/sites-enabled/${e.siteName}`,n=Ba(e),s=[...hn()];if(e.auth){let i=gs(e.siteName),a=(g)=>g.split("'").join("'\\''"),c=a(e.auth.password),f=a(e.auth.username);s.push(`TS_CLOUD_HTPASS=$(openssl passwd -apr1 '${c}')`,`printf '%s:%s\\n' '${f}' "$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",`${Fe} -t`,"systemctl reload ts-cloud-nginx 2>/dev/null || systemctl restart ts-cloud-nginx 2>/dev/null || true"),s}var Fe="/usr/local/bin/ts-cloud-nginx";function hn(e="/opt/pantry"){return[...yt(),...Ae(["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 > ${Fe} <<'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 ${Fe}`,"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=${Fe} -t`,`ExecStart=${Fe} -g 'daemon off;'`,`ExecReload=${Fe} -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 Ka}from"node:fs";function ja(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=Ka(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 kt(e,t){return!e.events||e.events.includes(t)}async function Sn(e,t,r,n={}){if(!e)return[];let s=n.fetchImpl??globalThis.fetch,i=[],a=[],c=async(f,g)=>{try{return await s(ja(f),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(g)})}catch{return}};if(e.slack?.webhookUrl&&kt(e,t))i.push("slack"),a.push(c(e.slack.webhookUrl,{text:r}));if(e.discord?.webhookUrl&&kt(e,t))i.push("discord"),a.push(c(e.discord.webhookUrl,{content:r}));if(e.telegram?.botToken&&e.telegram.chatId&&kt(e,t)){i.push("telegram");let f=`https://api.telegram.org/bot${e.telegram.botToken}/sendMessage`;a.push(c(f,{chat_id:e.telegram.chatId,text:r}))}if(e.webhook?.url&&kt(e,t))if(i.push("webhook"),(e.webhook.method||"POST")==="GET"){let f=e.webhook.url.includes("?")?"&":"?",g=`${e.webhook.url}${f}event=${encodeURIComponent(t)}&message=${encodeURIComponent(r)}`;a.push(s(g,{method:"GET"}).catch(()=>{return}))}else a.push(c(e.webhook.url,{event:t,message:r}));if(e.email?.to&&kt(e,t))i.push("email"),a.push(Ga(e.email,t,r));return await Promise.all(a),i}async function Ga(e,t,r){try{let n=await import("./chunk-w2gsveze.js");await n.email.send({to:e.to,from:e.from,subject:`[ts-cloud] ${t}`,text:r})}catch{}}function hs(e,t){return t??e}function qe(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 a=e[s]?.webhookUrl;if(a)n.push("URL="+r(a),'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"]}function lt(e=[],t={}){let r=t.path??"/root/.ssh/authorized_keys",s=[`mkdir -p ${r.replace(/\/[^/]*$/,"")}`,`touch ${r}`,`sed -i '/^${Ss("# >>> ts-cloud managed keys >>>")}$/,/^${Ss("# <<< ts-cloud managed keys <<<")}$/d' ${r}`];if(e.length>0){let i=["# >>> ts-cloud managed keys >>>",...e.map((a)=>`${a.publicKey.trim()} ${a.name}`),"# <<< ts-cloud managed keys <<<"].join(`
28
- `);s.push(`cat >> ${r} <<'TS_CLOUD_KEYS_EOF'`,i,"TS_CLOUD_KEYS_EOF",`chmod 600 ${r}`)}return s}function Ss(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 $s(e,t){if(e===!1)return;if(e===!0||e===void 0)return t;return e}var _s={enabled:!0,ports:[80,443]},Va={mode:"detection",paranoiaLevel:1};function qa(e=!0,t=[]){let r=$s(e,_s);if(!r||r.enabled===!1)return[];let n=[...new Set([...r.ports??_s.ports??[],...t])].sort((s,i)=>s-i);return vs("ddos",Yi({...r,ports:n}))}function Wa(e=!0){let t=$s(e,Va);if(!t||t.mode==="off")return[];return vs("waf",Bi(t))}function ys(e={},t=[]){return[...qa(e.ddos,t),...Wa(e.waf)]}function Ya(e){return`${e}-sftp`}function bs(e){return e.port??2222}function Xa(e){let t=e.sftp.storage;if(t?.type==="efs"&&t.path)return t.path;return`/var/sftp/${e.slug}`}function ut(e){return`'${e.split("'").join("'\\''")}'`}function Ts(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 ws(e){let{slug:t,sftp:r}=e,n=Ya(t),s=Xa(e),i=bs(r),a="/etc/ts-sftp/host_key",c="/etc/ts-sftp/users",f=r.serviceUser??"ts-sftp",g=[`# --- ts-sftp (${n}) ---`,`id -u ${f} >/dev/null 2>&1 || useradd --system --home-dir ${ut(s)} --shell /usr/sbin/nologin ${f}`,'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 ${ut(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 ${ut(`${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'],h=[];for(let[S,_]of Object.entries(r.users)){let v=(_.homeDirectory??S).replace(/^\/+|\/+$/g,"");if(!v||v.split("/").includes(".."))throw Error(`sftp: invalid homeDirectory for user ${S}`);let b=`/etc/ts-sftp/users/${S}.pub`;g.push(`mkdir -p ${ut(`${s}/${v}`)}`,`printf '%s\\n' ${ut(_.sshPublicKeys.join(`
29
- `))} > ${b}`,`chmod 644 ${b}`),h.push(`--user ${S}:${b}`)}return g.push(`chown -R ${f}:${f} ${ut(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=${f}`,`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":""} ${h.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`),g}function Cs(e){return e?[bs(e)]:[]}var Ja=[80,443];function pr(e={}){if(e.enabled===!1)return[];let t=[...new Set([...Ja,...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 Ye(e,t){let r=e.infrastructure?.compute??{},n=r.runtime==="php"||!!r.php,s=C(e,{environment:t?.environment}),i=n||!!r.managedServices,a=i?yt():[],c=!z({...r,proxy:R(e,{environment:t?.environment})}),f=n?[...a,...ur({versions:r.php?.versions,default:r.php?.default,extensions:r.php?.extensions,installNginx:c,optimizeForProduction:r.php?.optimizeForProduction,ini:r.php?.ini}),...c?hn():[]]:void 0,g=[],h=p(e);if(!n&&i)g.push(...a);if(g.push(...qe(e.notifications)),r.managedServices)g.push(...cr(r.managedServices,{mail:s.enabled?s:void 0}),...ct(h,r.managedServices));let S=e.infrastructure?.sftp;if(S)Ts(S,ge(e)),g.push(...ws({slug:e.project.slug,sftp:S}));let _=r.firewall??(n?{enabled:!0}:{enabled:!1}),v=rt(s);if(g.push(...pr(v.length>0?{..._,allowedPorts:[..._.allowedPorts??[],...v]}:_)),g.push(...ys({ddos:r.ddos,waf:r.waf},[...r.firewall?.allowedPorts??[],...v])),g.push(...ze(r.autoUpdates??n)),g.push(...ao(r.appUpdates)),g.push(...He(r.monitoring??n)),g.push(...lt(r.sshKeys,{path:t?.authorizedKeysPath})),r.backups?.enabled)g.push(...Ut({database:h,backups:r.backups}));return{runtime:r.runtime||"bun",runtimeVersion:r.runtimeVersion||"latest",phpBox:n,phpProvision:f,servicesProvision:g.length>0?g:void 0}}function Ps(e,t){if(e.cloud?.attachTo)return[];let r=e.infrastructure?.compute,n=C(e,{environment:t});return[...r?.backups?.enabled?Ut({database:p(e),backups:r.backups}):[],...n.enabled?or(n):[]]}function F(e={}){let{runtime:t="bun",runtimeVersion:r="latest",systemPackages:n=[],database:s,phpProvision:i,servicesProvision:a,caddyfile:c,rpxProvision:f,baked:g=!1,swapGb:h=2}=e,S=new Set(n);if(s==="sqlite")S.add("sqlite3");else if(s==="mysql")S.add("mysql-client");else if(s==="postgres")S.add("postgresql-client");let _=`#!/bin/bash
27
+ `}function fn(e){let t=`/etc/nginx/sites-available/${e.siteName}`,r=`/etc/nginx/sites-enabled/${e.siteName}`,n=Ka(e),s=[...hn()];if(e.auth){let i=hs(e.siteName),a=(h)=>h.split("'").join("'\\''"),c=a(e.auth.password),f=a(e.auth.username);s.push(`TS_CLOUD_HTPASS=$(openssl passwd -apr1 '${c}')`,`printf '%s:%s\\n' '${f}' "$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",`${ze} -t`,"systemctl reload ts-cloud-nginx 2>/dev/null || systemctl restart ts-cloud-nginx 2>/dev/null || true"),s}var ze="/usr/local/bin/ts-cloud-nginx";function hn(e="/opt/pantry"){return[...yt(),...Ae(["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 > ${ze} <<'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 ${ze}`,"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=${ze} -t`,`ExecStart=${ze} -g 'daemon off;'`,`ExecReload=${ze} -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 ja}from"node:fs";function Ga(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=ja(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 kt(e,t){return!e.events||e.events.includes(t)}async function Sn(e,t,r,n={}){if(!e)return[];let s=n.fetchImpl??globalThis.fetch,i=[],a=[],c=async(f,h)=>{try{return await s(Ga(f),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(h)})}catch{return}};if(e.slack?.webhookUrl&&kt(e,t))i.push("slack"),a.push(c(e.slack.webhookUrl,{text:r}));if(e.discord?.webhookUrl&&kt(e,t))i.push("discord"),a.push(c(e.discord.webhookUrl,{content:r}));if(e.telegram?.botToken&&e.telegram.chatId&&kt(e,t)){i.push("telegram");let f=`https://api.telegram.org/bot${e.telegram.botToken}/sendMessage`;a.push(c(f,{chat_id:e.telegram.chatId,text:r}))}if(e.webhook?.url&&kt(e,t))if(i.push("webhook"),(e.webhook.method||"POST")==="GET"){let f=e.webhook.url.includes("?")?"&":"?",h=`${e.webhook.url}${f}event=${encodeURIComponent(t)}&message=${encodeURIComponent(r)}`;a.push(s(h,{method:"GET"}).catch(()=>{return}))}else a.push(c(e.webhook.url,{event:t,message:r}));if(e.email?.to&&kt(e,t))i.push("email"),a.push(Va(e.email,t,r));return await Promise.all(a),i}async function Va(e,t,r){try{let n=await import("./chunk-w2gsveze.js");await n.email.send({to:e.to,from:e.from,subject:`[ts-cloud] ${t}`,text:r})}catch{}}function gs(e,t){return t??e}function qe(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 a=e[s]?.webhookUrl;if(a)n.push("URL="+r(a),'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"]}function lt(e=[],t={}){let r=t.path??"/root/.ssh/authorized_keys",s=[`mkdir -p ${r.replace(/\/[^/]*$/,"")}`,`touch ${r}`,`sed -i '/^${Ss("# >>> ts-cloud managed keys >>>")}$/,/^${Ss("# <<< ts-cloud managed keys <<<")}$/d' ${r}`];if(e.length>0){let i=["# >>> ts-cloud managed keys >>>",...e.map((a)=>`${a.publicKey.trim()} ${a.name}`),"# <<< ts-cloud managed keys <<<"].join(`
28
+ `);s.push(`cat >> ${r} <<'TS_CLOUD_KEYS_EOF'`,i,"TS_CLOUD_KEYS_EOF",`chmod 600 ${r}`)}return s}function Ss(e){return e.replace(/[.*[\]\\/^$]/g,"\\$&")}function ys(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 _s(e,t){if(e===!1)return;if(e===!0||e===void 0)return t;return e}var vs={enabled:!0,ports:[80,443]},qa={mode:"detection",paranoiaLevel:1};function Wa(e=!0,t=[]){let r=_s(e,vs);if(!r||r.enabled===!1)return[];let n=[...new Set([...r.ports??vs.ports??[],...t])].sort((s,i)=>s-i);return ys("ddos",Yi({...r,ports:n}))}function Ya(e=!0){let t=_s(e,qa);if(!t||t.mode==="off")return[];return ys("waf",Bi(t))}function $s(e={},t=[]){return[...Wa(e.ddos,t),...Ya(e.waf)]}function Xa(e){return`${e}-sftp`}function bs(e){return e.port??2222}function Ja(e){let t=e.sftp.storage;if(t?.type==="efs"&&t.path)return t.path;return`/var/sftp/${e.slug}`}function ut(e){return`'${e.split("'").join("'\\''")}'`}function Ts(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 ws(e){let{slug:t,sftp:r}=e,n=Xa(t),s=Ja(e),i=bs(r),a="/etc/ts-sftp/host_key",c="/etc/ts-sftp/users",f=r.serviceUser??"ts-sftp",h=[`# --- ts-sftp (${n}) ---`,`id -u ${f} >/dev/null 2>&1 || useradd --system --home-dir ${ut(s)} --shell /usr/sbin/nologin ${f}`,'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 ${ut(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 ${ut(`${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'],g=[];for(let[v,_]of Object.entries(r.users)){let b=(_.homeDirectory??v).replace(/^\/+|\/+$/g,"");if(!b||b.split("/").includes(".."))throw Error(`sftp: invalid homeDirectory for user ${v}`);let S=`/etc/ts-sftp/users/${v}.pub`;h.push(`mkdir -p ${ut(`${s}/${b}`)}`,`printf '%s\\n' ${ut(_.sshPublicKeys.join(`
29
+ `))} > ${S}`,`chmod 644 ${S}`),g.push(`--user ${v}:${S}`)}return h.push(`chown -R ${f}:${f} ${ut(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=${f}`,`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":""} ${g.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`),h}function Cs(e){return e?[bs(e)]:[]}var Za=[80,443];function pr(e={}){if(e.enabled===!1)return[];let t=[...new Set([...Za,...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 Ye(e,t){let r=e.infrastructure?.compute??{},n=r.runtime==="php"||!!r.php,s=C(e,{environment:t?.environment}),i=n||!!r.managedServices,a=i?yt():[],c=!z({...r,proxy:R(e,{environment:t?.environment})}),f=n?[...a,...ur({versions:r.php?.versions,default:r.php?.default,extensions:r.php?.extensions,installNginx:c,optimizeForProduction:r.php?.optimizeForProduction,ini:r.php?.ini}),...c?hn():[]]:void 0,h=[],g=p(e);if(!n&&i)h.push(...a);if(h.push(...qe(e.notifications)),r.managedServices)h.push(...cr(r.managedServices,{mail:s.enabled?s:void 0}),...ct(g,r.managedServices));let v=e.infrastructure?.sftp;if(v)Ts(v,ge(e)),h.push(...ws({slug:e.project.slug,sftp:v}));let _=r.firewall??(n?{enabled:!0}:{enabled:!1}),b=rt(s);if(h.push(...pr(b.length>0?{..._,allowedPorts:[..._.allowedPorts??[],...b]}:_)),h.push(...$s({ddos:r.ddos,waf:r.waf},[...r.firewall?.allowedPorts??[],...b])),h.push(...Fe(r.autoUpdates??n)),h.push(...ao(r.appUpdates)),h.push(...He(r.monitoring??n)),h.push(...lt(r.sshKeys,{path:t?.authorizedKeysPath})),r.backups?.enabled)h.push(...Ut({database:g,backups:r.backups}));return{runtime:r.runtime||"bun",runtimeVersion:r.runtimeVersion||"latest",phpBox:n,phpProvision:f,servicesProvision:h.length>0?h:void 0}}function Ps(e,t){if(e.cloud?.attachTo)return[];let r=e.infrastructure?.compute,n=C(e,{environment:t});return[...r?.backups?.enabled?Ut({database:p(e),backups:r.backups}):[],...n.enabled?or(n):[]]}function F(e={}){let{runtime:t="bun",runtimeVersion:r="latest",systemPackages:n=[],database:s,phpProvision:i,servicesProvision:a,caddyfile:c,rpxProvision:f,baked:h=!1,swapGb:g=2}=e,v=new Set(n);if(s==="sqlite")v.add("sqlite3");else if(s==="mysql")v.add("mysql-client");else if(s==="postgres")v.add("postgresql-client");let _=`#!/bin/bash
30
30
  set -euo pipefail
31
- `,v=Math.floor(h);if(v>0)_+=`
31
+ `,b=Math.floor(g);if(b>0)_+=`
32
32
  # Swap: a swapless box under memory pressure lets the kernel OOM killer choose
33
33
  # victims box-wide (the gateway, a tenant app, postgres). A modest swapfile
34
34
  # with low swappiness gives the kernel headroom without hurting responsiveness.
35
35
  if ! swapon --show=NAME --noheadings 2>/dev/null | grep -qx '/swapfile'; then
36
36
  if [ ! -f /swapfile ]; then
37
- fallocate -l ${v}G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=${v*1024} status=none
37
+ fallocate -l ${b}G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=${b*1024} status=none
38
38
  chmod 600 /swapfile
39
39
  mkswap /swapfile >/dev/null
40
40
  fi
@@ -43,13 +43,13 @@ fi
43
43
  grep -qE '^/swapfile\\s' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
44
44
  echo 'vm.swappiness=10' > /etc/sysctl.d/99-ts-cloud-swap.conf
45
45
  sysctl -w vm.swappiness=10 >/dev/null
46
- `;if(!g){if(_+=`
46
+ `;if(!h){if(_+=`
47
47
  export DEBIAN_FRONTEND=noninteractive
48
48
  apt-get update -y
49
49
  apt-get upgrade -y
50
50
  apt-get install -y curl tar gzip unzip git ca-certificates
51
- `,S.size>0)_+=`
52
- apt-get install -y ${[...S].join(" ")}
51
+ `,v.size>0)_+=`
52
+ apt-get install -y ${[...v].join(" ")}
53
53
  `;if(i&&i.length>0)_+=`
54
54
  ${i.join(`
55
55
  `)}
@@ -80,8 +80,8 @@ chmod 755 /opt/ts-cloud/bun/bin/bun
80
80
  ln -sf /opt/ts-cloud/bun/bin/bun /usr/local/bin/bun
81
81
  echo 'export BUN_INSTALL="/opt/ts-cloud/bun"' > /etc/profile.d/bun.sh
82
82
  echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> /etc/profile.d/bun.sh
83
- `;else if(t==="node"){let b=r==="latest"||!r?"20":r.split(".")[0];_+=`
84
- curl -fsSL https://deb.nodesource.com/setup_${b}.x | bash -
83
+ `;else if(t==="node"){let S=r==="latest"||!r?"20":r.split(".")[0];_+=`
84
+ curl -fsSL https://deb.nodesource.com/setup_${S}.x | bash -
85
85
  apt-get install -y nodejs
86
86
  ln -sf /usr/bin/node /usr/local/bin/node
87
87
  ln -sf /usr/bin/npm /usr/local/bin/npm
@@ -133,41 +133,52 @@ CADDY_CONFIG_EOF
133
133
  systemctl daemon-reload
134
134
  systemctl enable caddy
135
135
  systemctl start caddy
136
- `;if(f&&f.length>0){let b=f[0]==="set -euo pipefail"?f.slice(1):f;_+=`
137
- ${b.join(`
136
+ `;if(f&&f.length>0){let S=f[0]==="set -euo pipefail"?f.slice(1):f;_+=`
137
+ ${S.join(`
138
138
  `)}
139
139
  `}return _+=`
140
140
  echo "ts-cloud bootstrap complete — instance is ready for site deploys"
141
- `,_}var Es="/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id";function Rs(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 rt(C(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 Os(e,t){let r={...e.infrastructure?.compute,proxy:R(e,{environment:t})},n=Ye(e,{environment:t}),s=r.proxy?.engine==="rpx"?V({proxy:r.proxy,config:x(e.sites??{},{proxy:r.proxy,slug:e.project.slug,mail:C(e,{environment:t})}),slug:e.project.slug,bunBin:r.runtime==="node"||r.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0;return F({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 Ds(e){return Buffer.from(e,"utf8").toString("base64")}function xs(e){return e.infrastructure?.compute?.image??null}function dr(e,t){return`${e}-${t}-app-sg`}function _n(e,t,r="app"){return[{Key:"Project",Value:e},{Key:"Environment",Value:t},{Key:"Role",Value:r},{Key:"ManagedBy",Value:"ts-cloud"}]}function Za(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 As(e,t){let r=dr(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 Za(n.Tags,t.slug,t.environment);return!!t.vpcId&&n.VpcId===t.vpcId})}function ec(e){try{let t=Is(Qa(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 Mr{name="aws";usesCloudFormation=!0;region;ec2Client;cfnClient;constructor(e={}){this.region=e.region||"us-east-1",this.ec2Client=e.ec2Client??((t)=>new jr(t)),this.cfnClient=e.cfnClient??((t)=>new y(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 a=this.ec2Client(n),c=await this.findComputeTargets({slug:s,environment:r,role:"app",region:n});if(c.length>0){let q=c[0];return{appInstanceId:q.id,appPublicIp:q.publicIp,appPublicIpv6:q.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}let f=xs(t);if(!f){if(f=(await new Le(n).getParameter({Name:Es})).Parameter?.Value??null,!f)throw Error("Could not resolve the Ubuntu 24.04 AMI from SSM")}let g=await a.describeVpcs(),h=(g.Vpcs||[]).find((q)=>q.IsDefault)||(g.Vpcs||[])[0];if(!h?.VpcId)throw Error("No VPC found to launch the instance into");let S=(await a.describeSubnets({Filters:[{Name:"vpc-id",Values:[h.VpcId]}]})).Subnets||[],_=S.find((q)=>q.MapPublicIpOnLaunch)||S[0],v=_?.SubnetId;if(!_?.MapPublicIpOnLaunch)console.warn("ts-cloud: no public subnet found; the instance may not get a public IP (deploys/SSL need one).");let b=dr(s,r),L=(await a.describeSecurityGroups({Filters:[{Name:"group-name",Values:[b]},{Name:"vpc-id",Values:[h.VpcId]}]})).SecurityGroups?.[0]?.GroupId;if(!L)L=(await a.createSecurityGroup({GroupName:b,Description:`ts-cloud ${s}/${r} app`,VpcId:h.VpcId,TagSpecifications:[{ResourceType:"security-group",Tags:_n(s,r)}]})).GroupId;let K=Rs(t,r);await a.authorizeSecurityGroupIngress({GroupId:L,IpPermissions:K.map((q)=>({IpProtocol:q.protocol,FromPort:q.port,ToPort:q.port,IpRanges:[{CidrIp:q.cidr}]}))}).catch((q)=>{if(!/InvalidPermission\.Duplicate/.test(q instanceof Error?q.message:""))throw q});let M=Ds(Os(t,r)),D=i.server?.instanceType||"t3.small",k=(await a.runInstances({ImageId:f,InstanceType:D,MinCount:1,MaxCount:1,SecurityGroupIds:L?[L]:void 0,SubnetId:v,UserData:M,IamInstanceProfile:i.server?.iamInstanceProfile?{Name:i.server.iamInstanceProfile}:void 0,TagSpecifications:[{ResourceType:"instance",Tags:[{Key:"Name",Value:`${s}-${r}-app`},..._n(s,r)]}]})).Instances?.[0]?.InstanceId;if(!k)throw Error("RunInstances did not return an instance id");let j=await a.waitForInstanceState(k,"running");if(!j)throw Error(`Instance ${k} did not reach 'running' before timeout`);return{appInstanceId:k,appPublicIp:j.PublicIpAddress,appPublicIpv6:ae(j.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),a=[],f=(await this.findComputeTargets({slug:n,environment:r,role:"app",region:s})).map((b)=>b.id),g;if(f.length>0)g=(await i.describeInstances({InstanceIds:f}).catch(()=>({Reservations:[]}))).Reservations?.flatMap((T)=>T.Instances??[]).find((T)=>T.VpcId)?.VpcId;if(f.length>0)await i.terminateInstances(f),a.push(...f.map((b)=>`instance ${b}`)),await Promise.all(f.map((b)=>i.waitForInstanceState(b,"terminated").catch(()=>{return})));let h=dr(n,r),S=[{Name:"group-name",Values:[h]}];if(g)S.push({Name:"vpc-id",Values:[g]});let _=await i.describeSecurityGroups({Filters:S}).catch(()=>({SecurityGroups:[]})),v=As(_.SecurityGroups??[],{slug:n,environment:r,vpcId:g});if(v?.GroupId)for(let b=0;b<10;b++)try{await i.deleteSecurityGroup(v.GroupId),a.push(`security group ${h}`);break}catch{await new Promise((T)=>setTimeout(T,3000))}return{destroyed:a}}async getComputeOutputs(e){let t=this.resolveRegion(e.config),r=m(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 a=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",region:t}))[0];return{appInstanceId:a?.id,appPublicIp:a?.publicIp,appPublicIpv6:a?.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 o(t).putObject({bucket:n,key:e.remoteKey,body:Is(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 a=e.stackName??`${e.slug}-${e.environment}`,c=ec(a);if(!c)try{c=(await this.cfnClient(t).getStackOutputs(a)).appInstanceId??null}catch{c=null}if(!c)return[];try{let f=await r.describeInstances({InstanceIds:[c],Filters:[{Name:"instance-state-name",Values:["running","pending"]}]});return this.reservationsToTargets(f.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:ae(n.Ipv6Address),privateIp:n.PrivateIpAddress,status:n.State?.Name})}return t}bashWrap(e){return["bash <<'TS_CLOUD_BASH_EOF'",e.join(`
142
- `),"TS_CLOUD_BASH_EOF"]}async runRemoteDeploy(e){let t=this.region,r=new Le(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(),a=new Set(["Success","Failed","Cancelled","TimedOut"]),c=[];while(Date.now()-i<n){await new Promise((h)=>setTimeout(h,3000));try{if(c=await e.listCommandInvocations({CommandId:t,Details:!0}),c.length>=r&&c.every((S)=>a.has(S.Status||"")))break}catch{}}let f=c.map((h)=>({instanceId:h.InstanceId,status:h.Status||"Unknown",output:h.StandardOutputContent,error:h.StandardErrorContent})),g=f.length>0&&f.every((h)=>h.status==="Success");return{success:g,instanceCount:f.length,perInstance:f,error:g?void 0:Zt(f,"One or more SSM command invocations failed")}}}import{execFileSync as Bc,execSync as io}from"node:child_process";import{createHash as Gc,randomUUID as So}from"node:crypto";import{createReadStream as Qc,existsSync as el,readFileSync as tl}from"node:fs";async function Ls(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",a="/var/lib/ts-cloud/compute-storage-state.json",c="/run/ts-cloud-root-web",f=t[0]??"status";if(!["plan","status","check","apply","ensure-site","reclaim"].includes(f))throw Error(`Unknown compute storage command: ${f}`);let g=(P,E)=>{try{return JSON.parse(r.readFileSync(P,"utf8"))}catch(I){if(I.code==="ENOENT")return E;throw I}},h=(P,E)=>{r.mkdirSync(n.dirname(P),{recursive:!0});let I=`${P}.${process.pid}.tmp`;r.writeFileSync(I,`${JSON.stringify(E,null,2)}
143
- `,{mode:420}),r.renameSync(I,P)},S=(P,E)=>s.execFileSync(P,E,{encoding:"utf8",maxBuffer:16777216}),_=(P,E)=>{let I=s.spawnSync(P,E,{encoding:"utf8"});return I.status===0?I.stdout.trim():""},v=(P)=>!!_("findmnt",["-n","--mountpoint",P]),b=(P,E)=>{try{let I=r.statSync(P),w=r.statSync(E);return I.dev===w.dev&&I.ino===w.ino}catch{return!1}},T=g("/var/lib/ts-cloud/compute-storage-state.json",{entries:[]}),L=e.deploymentStorage,K=e.volumes.find((P)=>P.key===L?.volume),M=(P,E)=>{if(!K)return[];let I=[];for(let w of r.readdirSync(e.webRoot,{withFileTypes:!0})){if(!w.isDirectory()||w.isSymbolicLink())continue;let G=n.join(e.webRoot,w.name);if(P&&G!==P&&G!==n.join(e.webRoot,`${E}-shared`))continue;for(let Z of["releases",...L?.includeBackups===!1?[]:["backups"]]){let Y=n.join(G,Z);if(!r.existsSync(Y)||!r.lstatSync(Y).isDirectory())continue;I.push({source:Y,destination:n.join(K.mountPath,Z,w.name)})}}return I},D=(P)=>{try{return v(P.mountPath)&&r.statSync(P.mountPath).dev===r.statSync(P.device).rdev}catch{return!1}},N=(P)=>{let E=r.statfsSync(P),I=(E.blocks-E.bfree)*E.bsize,w=E.bavail*E.bsize;return{path:P,sizeBytes:E.blocks*E.bsize,usedBytes:I,availableBytes:w,usedPercent:Math.ceil(100*I/(I+w))}},k=()=>{let P=[],E=[N("/")];for(let w of e.volumes)if(!D(w))P.push(`missing or incorrect volume mount: ${w.mountPath}`);else E.push(N(w.mountPath));for(let w of T.entries)if(!v(w.source)||!b(w.source,w.destination))P.push(`missing deployment mount: ${w.source}`);let I=new Set(T.entries.map((w)=>w.source));for(let w of M())if(!I.has(w.source))P.push(`deployment needs volume placement: ${w.source}`);return{timestamp:new Date().toISOString(),volumes:e.volumes.map((w)=>({key:w.key,name:w.name,id:w.id,size:w.size,mountPath:w.mountPath})),disks:E,mounts:T.entries.length,problems:P}};if(f==="plan")return console.log(JSON.stringify({volumes:e.volumes,entries:M(),state:T},null,2));if(f==="status"||f==="check"){let P=k();if(f==="check"){h("/var/lib/ts-cloud/compute-storage-status.json",P);let E="/var/lib/ts-cloud/compute-storage-alert.json",I=g(E,{problems:[]});if(JSON.stringify(I.problems)!==JSON.stringify(P.problems)){let w=P.problems.join("; ")||"all configured mounts are healthy",G=`${P.problems.length?"Storage alert":"Storage recovered"} on ${_("hostname",[])}: ${w}`;S("/usr/local/bin/ts-cloud-notify",[G.length>1800?`${G.slice(0,1600)}... Full report: /var/lib/ts-cloud/compute-storage-status.json`:G])}h(E,{problems:P.problems,timestamp:P.timestamp})}if(console.log(JSON.stringify(P)),P.problems.length)process.exitCode=1;return P}if(process.getuid?.()!==0)throw Error("Compute storage changes require root");let j=()=>{let w=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 G=[];for(let J of e.volumes){let te=S("blkid",["-s","UUID","-o","value",J.device]).trim();if(!te)throw Error(`Volume ${J.name} has no filesystem UUID`);G.push(`UUID=${te} ${J.mountPath} ext4 defaults,noatime,nodev,nosuid,nofail,x-systemd.device-timeout=30s 0 2`)}for(let J of T.entries)G.push(`${J.destination} ${J.source} none bind,nofail,x-systemd.requires-mounts-for=${K.mountPath} 0 0`);let Z=`# BEGIN ts-cloud compute storage
144
- ${G.join(`
141
+ `,_}var Es="/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id";function Rs(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 rt(C(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 Os(e,t){let r={...e.infrastructure?.compute,proxy:R(e,{environment:t})},n=Ye(e,{environment:t}),s=r.proxy?.engine==="rpx"?V({proxy:r.proxy,config:x(e.sites??{},{proxy:r.proxy,slug:e.project.slug,mail:C(e,{environment:t})}),slug:e.project.slug,bunBin:r.runtime==="node"||r.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0;return F({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 Ds(e){return Buffer.from(e,"utf8").toString("base64")}function xs(e){return e.infrastructure?.compute?.image??null}function dr(e,t){return`${e}-${t}-app-sg`}function vn(e,t,r="app"){return[{Key:"Project",Value:e},{Key:"Environment",Value:t},{Key:"Role",Value:r},{Key:"ManagedBy",Value:"ts-cloud"}]}function Qa(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 As(e,t){let r=dr(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 Qa(n.Tags,t.slug,t.environment);return!!t.vpcId&&n.VpcId===t.vpcId})}function tc(e){try{let t=Is(ec(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 Mr{name="aws";usesCloudFormation=!0;region;ec2Client;cfnClient;constructor(e={}){this.region=e.region||"us-east-1",this.ec2Client=e.ec2Client??((t)=>new jr(t)),this.cfnClient=e.cfnClient??((t)=>new y(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 a=this.ec2Client(n),c=await this.findComputeTargets({slug:s,environment:r,role:"app",region:n});if(c.length>0){let ee=c[0];return{appInstanceId:ee.id,appPublicIp:ee.publicIp,appPublicIpv6:ee.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}let f=xs(t);if(!f){if(f=(await new Le(n).getParameter({Name:Es})).Parameter?.Value??null,!f)throw Error("Could not resolve the Ubuntu 24.04 AMI from SSM")}let h=await a.describeVpcs(),g=(h.Vpcs||[]).find((ee)=>ee.IsDefault)||(h.Vpcs||[])[0];if(!g?.VpcId)throw Error("No VPC found to launch the instance into");let v=(await a.describeSubnets({Filters:[{Name:"vpc-id",Values:[g.VpcId]}]})).Subnets||[],_=v.find((ee)=>ee.MapPublicIpOnLaunch)||v[0],b=_?.SubnetId;if(!_?.MapPublicIpOnLaunch)console.warn("ts-cloud: no public subnet found; the instance may not get a public IP (deploys/SSL need one).");let S=dr(s,r),M=(await a.describeSecurityGroups({Filters:[{Name:"group-name",Values:[S]},{Name:"vpc-id",Values:[g.VpcId]}]})).SecurityGroups?.[0]?.GroupId;if(!M)M=(await a.createSecurityGroup({GroupName:S,Description:`ts-cloud ${s}/${r} app`,VpcId:g.VpcId,TagSpecifications:[{ResourceType:"security-group",Tags:vn(s,r)}]})).GroupId;let q=Rs(t,r);await a.authorizeSecurityGroupIngress({GroupId:M,IpPermissions:q.map((ee)=>({IpProtocol:ee.protocol,FromPort:ee.port,ToPort:ee.port,IpRanges:[{CidrIp:ee.cidr}]}))}).catch((ee)=>{if(!/InvalidPermission\.Duplicate/.test(ee instanceof Error?ee.message:""))throw ee});let G=Ds(Os(t,r)),L=i.server?.instanceType||"t3.small",B=(await a.runInstances({ImageId:f,InstanceType:L,MinCount:1,MaxCount:1,SecurityGroupIds:M?[M]:void 0,SubnetId:b,UserData:G,IamInstanceProfile:i.server?.iamInstanceProfile?{Name:i.server.iamInstanceProfile}:void 0,TagSpecifications:[{ResourceType:"instance",Tags:[{Key:"Name",Value:`${s}-${r}-app`},...vn(s,r)]}]})).Instances?.[0]?.InstanceId;if(!B)throw Error("RunInstances did not return an instance id");let Z=await a.waitForInstanceState(B,"running");if(!Z)throw Error(`Instance ${B} did not reach 'running' before timeout`);return{appInstanceId:B,appPublicIp:Z.PublicIpAddress,appPublicIpv6:ae(Z.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),a=[],f=(await this.findComputeTargets({slug:n,environment:r,role:"app",region:s})).map((S)=>S.id),h;if(f.length>0)h=(await i.describeInstances({InstanceIds:f}).catch(()=>({Reservations:[]}))).Reservations?.flatMap((P)=>P.Instances??[]).find((P)=>P.VpcId)?.VpcId;if(f.length>0)await i.terminateInstances(f),a.push(...f.map((S)=>`instance ${S}`)),await Promise.all(f.map((S)=>i.waitForInstanceState(S,"terminated").catch(()=>{return})));let g=dr(n,r),v=[{Name:"group-name",Values:[g]}];if(h)v.push({Name:"vpc-id",Values:[h]});let _=await i.describeSecurityGroups({Filters:v}).catch(()=>({SecurityGroups:[]})),b=As(_.SecurityGroups??[],{slug:n,environment:r,vpcId:h});if(b?.GroupId)for(let S=0;S<10;S++)try{await i.deleteSecurityGroup(b.GroupId),a.push(`security group ${g}`);break}catch{await new Promise((P)=>setTimeout(P,3000))}return{destroyed:a}}async getComputeOutputs(e){let t=this.resolveRegion(e.config),r=m(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 a=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",region:t}))[0];return{appInstanceId:a?.id,appPublicIp:a?.publicIp,appPublicIpv6:a?.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 o(t).putObject({bucket:n,key:e.remoteKey,body:Is(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 a=e.stackName??`${e.slug}-${e.environment}`,c=tc(a);if(!c)try{c=(await this.cfnClient(t).getStackOutputs(a)).appInstanceId??null}catch{c=null}if(!c)return[];try{let f=await r.describeInstances({InstanceIds:[c],Filters:[{Name:"instance-state-name",Values:["running","pending"]}]});return this.reservationsToTargets(f.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:ae(n.Ipv6Address),privateIp:n.PrivateIpAddress,status:n.State?.Name})}return t}bashWrap(e){return["bash <<'TS_CLOUD_BASH_EOF'",e.join(`
142
+ `),"TS_CLOUD_BASH_EOF"]}async runRemoteDeploy(e){let t=this.region,r=new Le(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(),a=new Set(["Success","Failed","Cancelled","TimedOut"]),c=[];while(Date.now()-i<n){await new Promise((g)=>setTimeout(g,3000));try{if(c=await e.listCommandInvocations({CommandId:t,Details:!0}),c.length>=r&&c.every((v)=>a.has(v.Status||"")))break}catch{}}let f=c.map((g)=>({instanceId:g.InstanceId,status:g.Status||"Unknown",output:g.StandardOutputContent,error:g.StandardErrorContent})),h=f.length>0&&f.every((g)=>g.status==="Success");return{success:h,instanceCount:f.length,perInstance:f,error:h?void 0:Zt(f,"One or more SSM command invocations failed")}}}import{execFileSync as Gc,execSync as So}from"node:child_process";import{createHash as Qc,randomUUID as vo}from"node:crypto";import{createReadStream as el,existsSync as tl,readFileSync as rl}from"node:fs";async function Ls(e,t="status"){let r=await import("node:fs"),n=await import("node:path"),s=await import("node:child_process"),i=e.persistentStorage;if(!i)return;let a=e.volumes.find((E)=>E.key===i.volume);if(!a)throw Error("Persistent storage volume is not declared");let c="/var/lib/ts-cloud/persistent-storage-state.json",f="/run/ts-cloud-root-data",h=(E,X)=>s.execFileSync(E,X,{encoding:"utf8",maxBuffer:16777216}),g=(E,X)=>{try{return JSON.parse(r.readFileSync(E,"utf8"))}catch(A){if(A.code==="ENOENT")return X;throw A}},v=(E,X)=>{r.mkdirSync(n.dirname(E),{recursive:!0}),r.writeFileSync(`${E}.new`,JSON.stringify(X,null,2)+`
143
+ `,{mode:384});let A=r.openSync(`${E}.new`,"r");try{r.fsyncSync(A)}finally{r.closeSync(A)}r.renameSync(`${E}.new`,E);let T=r.openSync(n.dirname(E),"r");try{r.fsyncSync(T)}finally{r.closeSync(T)}},_=(E)=>s.spawnSync("mountpoint",["-q",E]).status===0,b=(E,X)=>{try{let A=r.statSync(E),T=r.statSync(X);return A.dev===T.dev&&A.ino===T.ino}catch{return!1}},S=g(c,{entries:[],stopped:[]}),P=i.paths.map((E)=>({source:E.path,destination:n.join(a.mountPath,"persistent",E.path.slice(1))}));for(let E of S.entries)if(!P.some((X)=>X.source===E.source&&X.destination===E.destination))throw Error(`Refusing to remove active persistent placement: ${E.source}`);if(!_(a.mountPath)||r.statSync(a.mountPath).dev!==r.statSync(a.device).rdev)throw Error("Required persistent volume is missing or incorrect");let M=()=>P.filter((E)=>!_(E.source)||!b(E.source,E.destination)).map((E)=>`Persistent mount missing: ${E.source}`);if(t==="check"){if(M().length)throw Error(M().join("; "));return{healthy:!0}}if(t==="status"){let E={entries:P,phase:S.phase,problems:M()};return console.log(JSON.stringify(E)),E}if(t!=="apply"||process.getuid?.()!==0)throw Error("Persistent storage apply requires root");let q=P.filter((E)=>!S.entries.some((X)=>X.source===E.source&&X.verified));if(!i.migrateExisting)throw Error("Persistent data migration requires migrateExisting: true");let G=()=>{for(let E of q){if(_(E.source)&&b(E.source,E.destination)){if(!S.entries.some((T)=>T.source===E.source&&T.checksummedAt))throw Error("Mounted persistent source has no verified cutover journal");continue}if(r.realpathSync(E.source)!==E.source||r.lstatSync(E.source).isSymbolicLink()||!r.statSync(E.source).isDirectory()||_(E.source))throw Error(`Unsafe persistent source: ${E.source}`);if(h("findmnt",["-rn","-o","TARGET"]).split(`
144
+ `).filter((T)=>T.startsWith(E.source+"/")).length)throw Error(`Persistent source contains another mount: ${E.source}`);let A=E.destination;while(!r.existsSync(A))A=n.dirname(A);if(r.realpathSync(A)!==A||!A.startsWith(a.mountPath))throw Error("Unsafe persistent destination ancestry");if(r.existsSync(E.destination)&&r.readdirSync(E.destination).length&&!S.entries.some((T)=>T.source===E.source))throw Error(`Refusing to overwrite unowned destination: ${E.destination}`)}},{dlopen:L,read:j}=await import("bun:ffi"),B=L("libc.so.6",{flock:{args:["i32","i32"],returns:"i32"},__errno_location:{args:[],returns:"ptr"}}),Z=[],ee=(E)=>{r.mkdirSync(n.dirname(E),{recursive:!0});let X=r.openSync(E,r.constants.O_CREAT|r.constants.O_RDWR|r.constants.O_NOFOLLOW,384);if(B.symbols.flock(X,2)!==0)throw r.closeSync(X),Error(`Storage lock failed: ${j.i32(B.symbols.__errno_location())}`);Z.push(X)},Y=async(E,X=!1,A=!1)=>{r.mkdirSync(E.destination,{recursive:!0});let T=["-aHAXx","--numeric-ids","--delete",...X?["--checksum"]:[],...A?["--dry-run","--itemize-changes"]:[],E.source+"/",E.destination+"/"],w=Bun.spawn(["rsync",...T],{stdout:"pipe",stderr:"pipe"}),[I,O,N]=await Promise.all([new Response(w.stdout).text(),new Response(w.stderr).text(),w.exited]);if(N!==0||A&&I.trim())throw Error(`Persistent copy verification failed (${N}): ${E.source}${O?": "+O.slice(-500):""}`)},se=()=>{let E=[];for(let X of r.readdirSync("/proc").filter((A)=>/^\d+$/.test(A))){let A=[];try{A=r.readdirSync(`/proc/${X}/fd`)}catch{continue}for(let T of A)try{let w=r.readlinkSync(`/proc/${X}/fd/${T}`);if(q.some((I)=>w===I.source||w.startsWith(I.source+"/")))E.push(w)}catch{}}return E},re=(E,X)=>new Bun.Glob(X).match(E),oe=[...new Set(i.paths.flatMap((E)=>E.units))],me=async()=>{for(let E of[...new Set(i.paths.flatMap((X)=>X.healthChecks??[]))]){let X=!1;for(let A=0;A<30;A++){try{let T=await fetch(E,{signal:AbortSignal.timeout(5000),redirect:"follow"});if(await T.body?.cancel(),T.status>=200&&T.status<400){X=!0;break}}catch{}await Bun.sleep(1000)}if(!X)throw Error(`Persistent storage health check failed: ${E}`)}},le=!1,ie=()=>{if(S.fstab!==void 0)r.writeFileSync("/etc/fstab",S.fstab);for(let[E,X]of Object.entries(S.dropins??{}))if(X===null)r.rmSync(E,{force:!0});else r.writeFileSync(E,X);try{h("systemctl",["daemon-reload"])}catch{console.error("Rollback restored configuration; systemd reload needs retry")}};try{ee("/run/lock/ts-cloud-persistent-storage.lock");for(let A of[...new Set(i.locks??[])].sort())ee(A);S=g(c,{entries:[],stopped:[]});for(let A of S.entries)if(!P.some((T)=>T.source===A.source&&T.destination===A.destination))throw Error(`Refusing to remove active persistent placement: ${A.source}`);if(q=P.filter((A)=>!S.entries.some((T)=>T.source===A.source&&T.verified)),S.phase==="complete"&&!q.length){if(M().length)throw Error(M().join("; "));return{healthy:!0}}le=S.phase==="active",G();let E=Number(h("du",["-scx","--block-size=1",...q.map((A)=>A.source)]).trim().split(`
145
+ `).at(-1)?.split(/\s+/)[0]),X=r.statfsSync(a.mountPath);if(!Number.isFinite(E)||E+1073741824>X.bavail*X.bsize)throw Error("Insufficient volume capacity for persistent data");if(!le){S.fstab??=r.readFileSync("/etc/fstab","utf8"),S.dropins??={},S.entries=P.map((K)=>S.entries.find((J)=>J.source===K.source)??K),S.phase="copying",v(c,S);for(let K of q.filter((J)=>!_(J.source)))console.log(`Online persistent copy: ${K.source}`),await Y(K);let A=h("systemctl",["list-units","--all","--plain","--no-legend","--no-pager"]).split(`
146
+ `).map((K)=>K.trim().split(/\s+/)).filter((K)=>oe.some((J)=>re(K[0],J)));if(S.stopped=[...new Set([...S.stopped,...A.filter((K)=>K[2]==="active"||K[2]==="activating").map((K)=>K[0])])],S.phase="quiescing",v(c,S),S.stopped.length)h("systemctl",["stop",...S.stopped]);if(se().length)throw Error("Persistent files remain open after stopping declared writers");for(let K of q.filter((J)=>!_(J.source))){console.log(`Final checksum verification: ${K.source}`),await Y(K,!0),await Y(K,!0,!0);let J=S.entries.find((ne)=>ne.source===K.source);J.checksummedAt=new Date().toISOString(),J.bytes=Number(h("du",["-sx","--block-size=1",K.source]).split(/\s+/)[0]),v(c,S)}if(h("sync",[]),r.mkdirSync(f,{recursive:!0}),!_(f))h("mount",["--bind","/",f]);h("mount",["--make-private",f]);for(let K of q){let J=n.join(f,K.source.slice(1));if(b(J,K.destination)||r.statSync(J).dev!==r.statSync("/").dev)throw Error("Unsafe persistent recovery view");if(!_(K.source))h("mount",["--bind",K.destination,K.source]);if(!b(K.source,K.destination))throw Error("Persistent bind identity mismatch");S.phase="switching",v(c,S)}let T="# BEGIN ts-cloud persistent storage",w="# END ts-cloud persistent storage",I=r.readFileSync("/etc/fstab","utf8"),O=[T,...P.map((K)=>`${K.destination} ${K.source} none bind,nofail,x-systemd.requires-mounts-for=${a.mountPath} 0 0`),w].join(`
147
+ `),N=I.indexOf(T),te=I.indexOf(w,N);if(N>=0&&te<0)throw Error("Incomplete persistent fstab block");r.writeFileSync("/etc/fstab.persistent-new",N<0?I.trimEnd()+`
148
+ `+O+`
149
+ `:I.slice(0,N)+O+I.slice(te+w.length)),r.renameSync("/etc/fstab.persistent-new","/etc/fstab");for(let K of i.paths)for(let J of K.units){let D=`/etc/systemd/system/${J.replace("@*.","@.").replace("-*.","-.")}.d`;r.mkdirSync(D,{recursive:!0});let k=i.paths.filter((de)=>de.units.includes(J)).map((de)=>de.path),ue=n.join(D,"ts-cloud-persistent-storage.conf");if(!(ue in S.dropins))S.dropins[ue]=r.existsSync(ue)?r.readFileSync(ue,"utf8"):null,v(c,S);r.writeFileSync(ue,`[Unit]
150
+ RequiresMountsFor=${a.mountPath} ${k.join(" ")}
151
+ `)}h("findmnt",["--verify"]),h("systemctl",["daemon-reload"]),S.phase="active",v(c,S),le=!0}if(M().length)throw Error(M().join("; "));if(S.stopped.length)h("systemctl",["start",...S.stopped]);await me();for(let A of S.stopped)if(s.spawnSync("systemctl",["is-active","--quiet",A]).status!==0)throw Error(`Persistent writer did not recover: ${A}`);if(r.mkdirSync(f,{recursive:!0}),!_(f))h("mount",["--bind","/",f]);h("mount",["--make-private",f]);for(let A of q){if(!S.entries.some((w)=>w.source===A.source&&w.checksummedAt))throw Error("Cannot reclaim an unverified root copy");if(!b(A.source,A.destination))throw Error("Persistent mount changed before reclamation");let T=n.join(f,A.source.slice(1));if(b(T,A.destination)||r.statSync(T).dev!==r.statSync("/").dev)throw Error("Unsafe persistent root reclamation");for(let w of r.readdirSync(T))r.rmSync(n.join(T,w),{recursive:!0,force:!1})}for(let A of S.entries)A.verified=!0;if(S.phase="complete",S.stopped=[],delete S.fstab,delete S.dropins,v(c,S),h("sync",[]),console.log(JSON.stringify({healthy:!0,entries:P,rootAvailableBytes:r.statfsSync("/").bavail*r.statfsSync("/").bsize})),_(f))h("umount",[f]);return{healthy:!0}}catch(E){if(!le){for(let X of[...q].reverse())if(_(X.source)&&b(X.source,X.destination))h("umount",[X.source]);if(ie(),S.phase="copying",v(c,S),S.stopped.length)h("systemctl",["start",...S.stopped])}throw E}finally{for(let E of Z.reverse())B.symbols.flock(E,8),r.closeSync(E);B.close()}}async function Us(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",a="/var/lib/ts-cloud/compute-storage-state.json",c="/run/ts-cloud-root-web",f=t[0]??"status";if(!["plan","status","check","apply","ensure-site","reclaim"].includes(f))throw Error(`Unknown compute storage command: ${f}`);let h=(T,w)=>{try{return JSON.parse(r.readFileSync(T,"utf8"))}catch(I){if(I.code==="ENOENT")return w;throw I}},g=(T,w)=>{r.mkdirSync(n.dirname(T),{recursive:!0});let I=`${T}.${process.pid}.tmp`;r.writeFileSync(I,`${JSON.stringify(w,null,2)}
152
+ `,{mode:420}),r.renameSync(I,T)},v=(T,w)=>s.execFileSync(T,w,{encoding:"utf8",maxBuffer:16777216}),_=(T,w)=>{let I=s.spawnSync(T,w,{encoding:"utf8"});return I.status===0?I.stdout.trim():""},b=(T)=>!!_("findmnt",["-n","--mountpoint",T]),S=(T,w)=>{try{let I=r.statSync(T),O=r.statSync(w);return I.dev===O.dev&&I.ino===O.ino}catch{return!1}},P=h("/var/lib/ts-cloud/compute-storage-state.json",{entries:[]}),M=e.deploymentStorage,q=e.volumes.find((T)=>T.key===M?.volume),G=(T,w)=>{if(!q)return[];let I=[];for(let O of r.readdirSync(e.webRoot,{withFileTypes:!0})){if(!O.isDirectory()||O.isSymbolicLink())continue;let N=n.join(e.webRoot,O.name);if(T&&N!==T&&N!==n.join(e.webRoot,`${w}-shared`))continue;for(let te of["releases",...M?.includeBackups===!1?[]:["backups"]]){let K=n.join(N,te);if(!r.existsSync(K)||!r.lstatSync(K).isDirectory())continue;I.push({source:K,destination:n.join(q.mountPath,te,O.name)})}}return I},L=(T)=>{try{return b(T.mountPath)&&r.statSync(T.mountPath).dev===r.statSync(T.device).rdev}catch{return!1}},j=(T)=>{let w=r.statfsSync(T),I=(w.blocks-w.bfree)*w.bsize,O=w.bavail*w.bsize;return{path:T,sizeBytes:w.blocks*w.bsize,usedBytes:I,availableBytes:O,usedPercent:Math.ceil(100*I/(I+O))}},B=()=>{let T=[],w=[j("/")];for(let N of e.volumes)if(!L(N))T.push(`missing or incorrect volume mount: ${N.mountPath}`);else w.push(j(N.mountPath));for(let N of P.entries)if(!b(N.source)||!S(N.source,N.destination))T.push(`missing deployment mount: ${N.source}`);let I=e.volumes.find((N)=>N.key===e.persistentStorage?.volume);for(let N of e.persistentStorage?.paths??[]){let te=n.join(I.mountPath,"persistent",N.path.slice(1));if(!b(N.path)||!S(N.path,te))T.push(`missing persistent mount: ${N.path}`)}let O=new Set(P.entries.map((N)=>N.source));for(let N of G())if(!O.has(N.source))T.push(`deployment needs volume placement: ${N.source}`);return{timestamp:new Date().toISOString(),volumes:e.volumes.map((N)=>({key:N.key,name:N.name,id:N.id,size:N.size,mountPath:N.mountPath})),disks:w,mounts:P.entries.length,problems:T}};if(f==="plan")return console.log(JSON.stringify({volumes:e.volumes,entries:G(),state:P},null,2));if(f==="status"||f==="check"){let T=B();if(f==="check"){g("/var/lib/ts-cloud/compute-storage-status.json",T);let w="/var/lib/ts-cloud/compute-storage-alert.json",I=h(w,{problems:[]});if(JSON.stringify(I.problems)!==JSON.stringify(T.problems)){let O=T.problems.join("; ")||"all configured mounts are healthy",N=`${T.problems.length?"Storage alert":"Storage recovered"} on ${_("hostname",[])}: ${O}`;v("/usr/local/bin/ts-cloud-notify",[N.length>1800?`${N.slice(0,1600)}... Full report: /var/lib/ts-cloud/compute-storage-status.json`:N])}g(w,{problems:T.problems,timestamp:T.timestamp})}if(console.log(JSON.stringify(T)),T.problems.length)process.exitCode=1;return T}if(process.getuid?.()!==0)throw Error("Compute storage changes require root");let Z=()=>{let O=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 N=[];for(let J of e.volumes){let ne=v("blkid",["-s","UUID","-o","value",J.device]).trim();if(!ne)throw Error(`Volume ${J.name} has no filesystem UUID`);N.push(`UUID=${ne} ${J.mountPath} ext4 defaults,noatime,nodev,nosuid,nofail,x-systemd.device-timeout=30s 0 2`)}for(let J of P.entries)N.push(`${J.destination} ${J.source} none bind,nofail,x-systemd.requires-mounts-for=${q.mountPath} 0 0`);let te=`# BEGIN ts-cloud compute storage
153
+ ${N.join(`
145
154
  `)}
146
- # END ts-cloud compute storage`,Y;if(w.includes("# BEGIN ts-cloud compute storage")){let J=w.indexOf("# BEGIN ts-cloud compute storage"),te=w.indexOf("# END ts-cloud compute storage",J);if(te<0)throw Error("Incomplete managed storage block in fstab");Y=w.slice(0,J)+Z+w.slice(te+30)}else Y=`${w.trimEnd()}
147
- ${Z}
148
- `;r.writeFileSync("/etc/fstab.ts-cloud-new",Y),r.renameSync("/etc/fstab.ts-cloud-new","/etc/fstab")},q=()=>{for(let P of e.volumes){if(!r.existsSync(P.device)||!r.statSync(P.device).isBlockDevice())throw Error(`Volume device missing: ${P.device}`);let E=_("blkid",["-s","TYPE","-o","value",P.device]);if(!E){if(!P.allowFormat)throw Error(`Refusing to format adopted volume ${P.name}; no recognized filesystem`);S("mkfs.ext4",["-m","1",P.device]),E="ext4"}if(E!=="ext4")throw Error(`Refusing to replace ${E} filesystem on ${P.name}`);if(r.mkdirSync(P.mountPath,{recursive:!0}),v(P.mountPath)&&!D(P))throw Error(`Another filesystem occupies ${P.mountPath}`);if(!v(P.mountPath)&&r.readdirSync(P.mountPath).length)throw Error(`Refusing to hide existing files beneath ${P.mountPath}`);if(!v(P.mountPath))S("mount",["-o","noatime,nodev,nosuid",P.device,P.mountPath]);if(!D(P))throw Error(`Volume identity verification failed: ${P.name}`);S("resize2fs",[P.device]),P.allowFormat=!1}h("/etc/ts-cloud/compute-storage.json",e),j(),S("systemctl",["daemon-reload"])},{dlopen:B,read:re}=await import("bun:ffi"),ee=B("libc.so.6",{flock:{args:["i32","i32"],returns:"i32"},__errno_location:{args:[],returns:"ptr"}}),ne=[],oe=(P,E=!0)=>{r.mkdirSync(n.dirname(P),{recursive:!0});let I=r.openSync(P,r.constants.O_CREAT|r.constants.O_RDWR|r.constants.O_NOFOLLOW,384);if(ee.symbols.flock(I,E?2:6)!==0){let w=re.i32(ee.symbols.__errno_location());if(r.closeSync(I),!E&&w===11)return!1;throw Error(`Could not acquire lock: ${P} (errno ${w})`)}return ne.push(I),!0},ie=()=>{let P=[];for(let E of r.readdirSync("/proc").filter((I)=>/^\d+$/.test(I))){let I=[];try{I=r.readdirSync(`/proc/${E}/fd`)}catch{continue}for(let w of["cwd","exe",...I.map((G)=>`fd/${G}`)]){let G=`/proc/${E}/${w}`;try{let Z=r.readlinkSync(G);if(!Z.startsWith(`${e.webRoot}/`)||!Z.includes("/releases/"))continue;let Y=!1;if(w.startsWith("fd/")){let J=r.readFileSync(`/proc/${E}/fdinfo/${w.slice(3)}`,"utf8").match(/^flags:\s+(\d+)/m);Y=!!J&&(Number.parseInt(J[1],8)&3)!==0&&r.statSync(G).isFile()}P.push({file:Z,write:Y,device:r.statSync(G).dev})}catch{}}}return P},se=async(P,E)=>{r.mkdirSync(P.destination,{recursive:!0});let I=["rsync","-aHAXx","--numeric-ids","--delete",...E?["--checksum"]:[],`${P.source}/`,`${P.destination}/`],G=await Bun.spawn(I,{stdout:"inherit",stderr:"inherit"}).exited;if(G!==0&&!(G===24&&!E))throw Error(`rsync failed (${G}): ${P.source}`)},de=async()=>{let P=S("systemctl",["list-units","--type=service","--state=running","--no-legend","--plain"]).split(`
149
- `).map((Y)=>Y.trim().split(/\s+/)[0]).filter(Boolean),E={};if(P.length){let Y=S("systemctl",["show",...P,"-p","Id","-p","MainPID","-p","WorkingDirectory"]);for(let J of Y.split(`
155
+ # END ts-cloud compute storage`,K;if(O.includes("# BEGIN ts-cloud compute storage")){let J=O.indexOf("# BEGIN ts-cloud compute storage"),ne=O.indexOf("# END ts-cloud compute storage",J);if(ne<0)throw Error("Incomplete managed storage block in fstab");K=O.slice(0,J)+te+O.slice(ne+30)}else K=`${O.trimEnd()}
156
+ ${te}
157
+ `;r.writeFileSync("/etc/fstab.ts-cloud-new",K),r.renameSync("/etc/fstab.ts-cloud-new","/etc/fstab")},ee=()=>{for(let T of e.volumes){if(!r.existsSync(T.device)||!r.statSync(T.device).isBlockDevice())throw Error(`Volume device missing: ${T.device}`);let w=_("blkid",["-s","TYPE","-o","value",T.device]);if(!w){if(!T.allowFormat)throw Error(`Refusing to format adopted volume ${T.name}; no recognized filesystem`);v("mkfs.ext4",["-m","1",T.device]),w="ext4"}if(w!=="ext4")throw Error(`Refusing to replace ${w} filesystem on ${T.name}`);if(r.mkdirSync(T.mountPath,{recursive:!0}),b(T.mountPath)&&!L(T))throw Error(`Another filesystem occupies ${T.mountPath}`);if(!b(T.mountPath)&&r.readdirSync(T.mountPath).length)throw Error(`Refusing to hide existing files beneath ${T.mountPath}`);if(!b(T.mountPath))v("mount",["-o","noatime,nodev,nosuid",T.device,T.mountPath]);if(!L(T))throw Error(`Volume identity verification failed: ${T.name}`);v("resize2fs",[T.device]),T.allowFormat=!1}g("/etc/ts-cloud/compute-storage.json",e),Z(),v("systemctl",["daemon-reload"])},{dlopen:Y,read:se}=await import("bun:ffi"),re=Y("libc.so.6",{flock:{args:["i32","i32"],returns:"i32"},__errno_location:{args:[],returns:"ptr"}}),oe=[],me=(T,w=!0)=>{r.mkdirSync(n.dirname(T),{recursive:!0});let I=r.openSync(T,r.constants.O_CREAT|r.constants.O_RDWR|r.constants.O_NOFOLLOW,384);if(re.symbols.flock(I,w?2:6)!==0){let O=se.i32(re.symbols.__errno_location());if(r.closeSync(I),!w&&O===11)return!1;throw Error(`Could not acquire lock: ${T} (errno ${O})`)}return oe.push(I),!0},le=()=>{let T=[];for(let w of r.readdirSync("/proc").filter((I)=>/^\d+$/.test(I))){let I=[];try{I=r.readdirSync(`/proc/${w}/fd`)}catch{continue}for(let O of["cwd","exe",...I.map((N)=>`fd/${N}`)]){let N=`/proc/${w}/${O}`;try{let te=r.readlinkSync(N);if(!te.startsWith(`${e.webRoot}/`)||!te.includes("/releases/"))continue;let K=!1;if(O.startsWith("fd/")){let J=r.readFileSync(`/proc/${w}/fdinfo/${O.slice(3)}`,"utf8").match(/^flags:\s+(\d+)/m);K=!!J&&(Number.parseInt(J[1],8)&3)!==0&&r.statSync(N).isFile()}T.push({file:te,write:K,device:r.statSync(N).dev})}catch{}}}return T},ie=async(T,w)=>{r.mkdirSync(T.destination,{recursive:!0});let I=["rsync","-aHAXx","--numeric-ids","--delete",...w?["--checksum"]:[],`${T.source}/`,`${T.destination}/`],N=await Bun.spawn(I,{stdout:"inherit",stderr:"inherit"}).exited;if(N!==0&&!(N===24&&!w))throw Error(`rsync failed (${N}): ${T.source}`)},E=async()=>{let T=v("systemctl",["list-units","--type=service","--state=running","--no-legend","--plain"]).split(`
158
+ `).map((K)=>K.trim().split(/\s+/)[0]).filter(Boolean),w={};if(T.length){let K=v("systemctl",["show",...T,"-p","Id","-p","MainPID","-p","WorkingDirectory"]);for(let J of K.split(`
150
159
 
151
- `)){let te=Object.fromEntries(J.split(`
152
- `).filter(Boolean).map((O)=>{let A=O.indexOf("=");return[O.slice(0,A),O.slice(A+1)]}));if(te.WorkingDirectory?.startsWith(`${e.webRoot}/`)||/^(?:mail|rpx-gateway|postgresql|mysql|redis)[@.]/.test(te.Id??""))E[te.Id]=Number(te.MainPID)}}let I=new Map;if(r.existsSync("/etc/rpx/sites.d"))for(let Y of r.readdirSync("/etc/rpx/sites.d").filter((J)=>J.endsWith(".json"))){let J=JSON.parse(r.readFileSync(n.join("/etc/rpx/sites.d",Y),"utf8"));for(let te of J.proxies??[]){if(typeof te.to!=="string"||!/^[a-zA-Z0-9.-]+$/.test(te.to))continue;let O=typeof te.path==="string"&&te.path.startsWith("/")?te.path:"/";I.set(`${te.to}${O}`,{domain:te.to,path:O})}}let w=[...I.values()],G=[],Z=0;return await Promise.all(Array.from({length:Math.min(8,w.length)},async()=>{while(Z<w.length){let Y=w[Z++],J=Bun.spawn(["curl","-sS","--max-time","12","--output","/dev/null","--write-out","%{http_code}","--resolve",`${Y.domain}:443:127.0.0.1`,`https://${Y.domain}${Y.path}`],{stdout:"pipe",stderr:"pipe"}),[te,,O]=await Promise.all([new Response(J.stdout).text(),new Response(J.stderr).text(),J.exited]),A=Number(te);G.push({...Y,status:A,ok:O===0&&A>=200&&A<500})}})),{routes:G.sort((Y,J)=>`${Y.domain}${Y.path}`.localeCompare(`${J.domain}${J.path}`)),services:E}},me=()=>{let P=[K.mountPath,...T.entries.map((I)=>I.source)],E=new Set(["rpx-gateway.service",...L?.dependentUnits??[]]);for(let I of r.readdirSync("/etc/systemd/system")){if(!I.endsWith(".service"))continue;try{if(r.readFileSync(`/etc/systemd/system/${I}`,"utf8").includes(e.webRoot))E.add(I)}catch{}}for(let I of E){let w=`/etc/systemd/system/${I}.d`;r.mkdirSync(w,{recursive:!0}),r.writeFileSync(`${w}/ts-cloud-storage.conf`,`[Unit]
153
- RequiresMountsFor=${P.join(" ")}
154
- `)}S("systemctl",["daemon-reload"])},X=()=>{T=g("/var/lib/ts-cloud/compute-storage-state.json",{entries:[]});let P=g("/etc/ts-cloud/compute-storage.json",null);if(P){for(let E of P.volumes)if(!e.volumes.some((I)=>I.id===E.id&&I.key===E.key&&I.mountPath===E.mountPath))throw Error(`Refusing to remove or relocate managed volume ${E.name}; migrate its data explicitly first`);if(T.entries.length&&(P.deploymentStorage?.volume!==L?.volume||P.deploymentStorage?.includeBackups!==!1!==(L?.includeBackups!==!1)))throw Error("Refusing to change an active deployment-storage policy; migrate its data explicitly first")}};try{let P=[...new Set([...L?.locks??[],"/run/lock/ts-cloud-host-cleanup.lock"])].sort();if(f==="reclaim"){for(let O of["/run/lock/ts-cloud-compute-storage.lock",...P])if(!oe(O,!1))return console.log(`Deferring deployment storage reclamation; lock busy: ${O}`),{deferred:!0,lock:O}}else oe("/run/lock/ts-cloud-compute-storage.lock");if(X(),f==="apply")q();for(let O of e.volumes)if(!D(O))throw Error(`Required volume is not mounted: ${O.name}`);if(!L||!K)return{volumes:e.volumes};if(f==="ensure-site"){let[O,A]=t.slice(1);if(!O?.startsWith(`${e.webRoot}/`)||!/^[\w-]+$/.test(n.basename(O))||O.includes("..")||!/^[\w-]+$/.test(A??""))throw Error("Invalid deployment site path or project slug");if(r.mkdirSync(n.join(O,"releases"),{recursive:!0}),L.includeBackups!==!1)r.mkdirSync(n.join(e.webRoot,`${A}-shared`,"backups"),{recursive:!0})}let E=(f==="reclaim"?[]:M(f==="ensure-site"?t[1]:void 0,t[2])).filter((O)=>!T.entries.some((A)=>A.source===O.source));for(let O of T.entries)if(!v(O.source)||!b(O.source,O.destination))throw Error(`Recorded deployment mount is missing: ${O.source}`);if(!E.length&&f==="ensure-site")return{inherited:!0,volume:K.name};if(!E.length&&f==="apply"){me();let O=k();return h("/var/lib/ts-cloud/compute-storage-status.json",O),console.log(JSON.stringify(O)),O}if(f!=="reclaim"){let O=ne.pop();ee.symbols.flock(O,8),r.closeSync(O),console.log("Waiting for deployment storage maintenance exclusion");for(let A of P)oe(A);oe("/run/lock/ts-cloud-compute-storage.lock"),X();for(let A of e.volumes)if(!D(A))throw Error(`Required volume is not mounted: ${A.name}`);for(let A of T.entries)if(!v(A.source)||!b(A.source,A.destination))throw Error(`Recorded deployment mount is missing: ${A.source}`);if(E=M(f==="ensure-site"?t[1]:void 0,t[2]).filter((A)=>!T.entries.some((fe)=>fe.source===A.source)),!E.length&&f==="ensure-site")return{inherited:!0,volume:K.name};if(!E.length){me();let A=k();return h("/var/lib/ts-cloud/compute-storage-status.json",A),console.log(JSON.stringify(A)),A}}if(!L.migrateExisting&&E.some((O)=>r.readdirSync(O.source).length>0))throw Error("Existing deployments require deploymentStorage.migrateExisting: true");if(E.length){let O=S("du",["-scx","--block-size=1",...E.map((fe)=>fe.source)]).trim().split(`
155
- `).at(-1)?.split(/\s+/)[0],A=r.statfsSync(K.mountPath);if(Number(O)>A.bavail*A.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(!v("/run/ts-cloud-root-web"))S("mount",["--bind",e.webRoot,"/run/ts-cloud-root-web"]);S("mount",["--make-private","/run/ts-cloud-root-web"]);let I=r.statSync(e.webRoot).dev;for(let O of T.entries){let A=n.join("/run/ts-cloud-root-web",n.relative(e.webRoot,O.source));if(r.existsSync(A)&&(r.statSync(A).dev!==I||b(A,O.destination)))throw Error(`Root recovery view contains a deployment mount: ${A}`)}for(let O=0;O<E.length;O++){if(v(E[O].source)&&b(E[O].source,E[O].destination))continue;console.log(`copy ${O+1}/${E.length}: ${E[O].source}`),await se(E[O],!1)}for(let O of r.readdirSync(e.webRoot,{withFileTypes:!0}).filter((A)=>A.isDirectory()).sort((A,fe)=>A.name.localeCompare(fe.name))){let A=n.join(e.webRoot,O.name,".ts-cloud/deploy.lock");if(!oe(A,f!=="reclaim"))return console.log(`Deferring deployment storage reclamation; lock busy: ${A}`),{deferred:!0,lock:A}}for(let O of f==="reclaim"?[]:M(f==="ensure-site"?t[1]:void 0,t[2]))if(!T.entries.some((A)=>A.source===O.source)&&!E.some((A)=>A.source===O.source))E.push(O);if(E.length)console.log("Checking application services and HTTPS routes before activation");let w=E.length?await de():void 0;if(w)h("/var/lib/ts-cloud/compute-storage-health-before.json",w);let G=ie().filter((O)=>O.write);if(G.length)throw Error(`Live writable files must be shared before migration: ${G.map((O)=>O.file).join(", ")}`);let Z={entries:[...T.entries],fstab:r.readFileSync("/etc/fstab","utf8")};if(E.length)S("sync",[]);let Y=[];try{for(let O=0;O<E.length;O++){let A=E[O];if(v(A.source)){if(!b(A.source,A.destination))throw Error(`Unrecorded mount occupies ${A.source}`);T.entries.push(A),h("/var/lib/ts-cloud/compute-storage-state.json",T);continue}if(console.log(`verify and mount ${O+1}/${E.length}: ${A.source}`),await se(A,!0),S("mount",["--bind",A.destination,A.source]),!b(A.source,A.destination))throw Error(`Bind verification failed: ${A.source}`);Y.push(A),T.entries.push(A),h("/var/lib/ts-cloud/compute-storage-state.json",T),j()}if(j(),S("findmnt",["--verify"]),T.activatedAt=new Date().toISOString(),h("/var/lib/ts-cloud/compute-storage-state.json",T),me(),w){console.log("Checking application services and HTTPS routes after activation");let O=await de();h("/var/lib/ts-cloud/compute-storage-health-after.json",O);let A=w.routes.filter((le)=>le.ok&&!O.routes.some((be)=>be.domain===le.domain&&be.path===le.path&&be.ok)),fe=Object.keys(w.services).filter((le)=>!O.services[le]);if(A.length||fe.length)throw Error(`Application verification failed: ${A.map((le)=>le.domain+le.path).concat(fe).join(", ")}`);console.log(`Verified ${O.routes.length} routes and ${Object.keys(O.services).length} services`)}}catch(O){let A=[];for(let fe of Y.reverse())try{S("umount",[fe.source])}catch{A.push(fe),console.error(`Rollback mount busy, original data retained: ${fe.source}`)}if(T.entries=[...Z.entries,...A],r.writeFileSync("/etc/fstab",Z.fstab),A.length)j();throw h("/var/lib/ts-cloud/compute-storage-state.json",T),S("systemctl",["daemon-reload"]),O}let J=ie().filter((O)=>O.device===I).map((O)=>O.file);for(let O of T.entries){if(!v(O.source)||!b(O.source,O.destination))throw Error(`Lost deployment mount: ${O.source}`);let A=n.join("/run/ts-cloud-root-web",n.relative(e.webRoot,O.source));if(!r.existsSync(A))continue;if(r.statSync(A).dev!==I||b(A,O.destination))throw Error(`Refusing to clean a deployment volume through the root recovery view: ${A}`);let fe="";try{fe=n.basename(r.realpathSync(n.join(n.dirname(O.source),"current")))}catch{}for(let le of r.readdirSync(A,{withFileTypes:!0})){let be=n.join(A,le.name);if(n.basename(O.source)==="backups"){if(le.isFile()&&r.existsSync(n.join(O.destination,le.name)))r.unlinkSync(be)}else if(le.isDirectory()&&!le.isSymbolicLink()&&le.name!==fe&&!J.some((Ot)=>Ot===n.join(O.source,le.name)||Ot.startsWith(`${n.join(O.source,le.name)}/`)))r.rmSync(be,{recursive:!0})}}S("sync",[]);let te=k();return h("/var/lib/ts-cloud/compute-storage-status.json",te),console.log(JSON.stringify(te)),te}finally{for(let P of ne.reverse())ee.symbols.flock(P,8),r.closeSync(P);ee.close()}}var vn="/etc/ts-cloud/compute-storage.json",Be="/usr/local/lib/ts-cloud/compute-storage.mjs";function ri(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((c)=>/^[\w-]+$/.test(c))||i.mountPath==="/var"||i.mountPath==="/var/lib"||["/etc","/usr","/var/www","/root","/boot","/dev","/proc","/sys","/run","/bin","/sbin","/lib"].some((c)=>i.mountPath===c||i.mountPath.startsWith(`${c}/`)))throw Error(`Unsafe compute volume mount path: ${i.mountPath}`);let a=i.name??`${e.project.slug}-${t}-${s}`;if(!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(a))throw Error(`Invalid compute volume name: ${a}`);return{...i,key:s,name:a,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 gr(e){return`'${e.replaceAll("'",`'"'"'`)}'`}function ni(e){let t=`${vn}.candidate`,r=`#!/usr/bin/env bun
156
- await (${Ls.toString()})(JSON.parse(await Bun.file(process.env.TS_CLOUD_STORAGE_POLICY || ${JSON.stringify(vn)}).text()), process.argv.slice(2));
157
- `;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 ${gr(Buffer.from(r).toString("base64"))} | base64 -d > ${Be}.new`,`chmod 0755 ${Be}.new && mv ${Be}.new ${Be}`,`printf %s ${gr(JSON.stringify(e))} > ${t}`,`chmod 0644 ${t}`,`TS_CLOUD_STORAGE_POLICY=${t} bun ${Be} 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 ${Be} 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 ${Be} 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 Fr(e,t){return[`if [ -f ${vn} ]; then bun ${Be} ensure-site ${gr(e)} ${gr(t)}; fi`]}import{createHash as tc}from"node:crypto";function rc(){return["if [ -x /usr/local/bin/bun ]; then",' TS_CLOUD_HOST_BUN="$(readlink -f /usr/local/bin/bun)"',' case "$TS_CLOUD_HOST_BUN" in /root/*|/home/*)'," install -d -m 755 /opt/ts-cloud /opt/ts-cloud/bun /opt/ts-cloud/bun/bin",' TS_CLOUD_BUN_STAGE="$(mktemp /opt/ts-cloud/bun/bin/.bun.XXXXXX)"',' install -m 755 "$TS_CLOUD_HOST_BUN" "$TS_CLOUD_BUN_STAGE"',' mv -f "$TS_CLOUD_BUN_STAGE" /opt/ts-cloud/bun/bin/bun'," ln -sfn /opt/ts-cloud/bun/bin/bun /usr/local/bin/bun"," ;; esac","fi"]}function pt(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)}-${tc("sha256").update(e).digest("hex").slice(0,8)}`}`}function Ht(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 Us(e,t,r){let n=pt(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`,a=[...new Set([t,...r])];for(let c of a)if(!/^\/[a-zA-Z0-9_./-]+$/.test(c)||c.split("/").includes("..")||!(c===t||c.startsWith(`${t}/`)||c===i||c.startsWith(`${i}/`)))throw Error(`Runtime isolation cannot take ownership outside project ${e}`);return[...rc(),`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}`,...a.map((c)=>`chown -hR ${n}:${n} ${c}`),`chmod 750 ${t}`]}function hr(e){return Object.entries(e).map(([t,r])=>`${t}=${nc(String(r))}`).join(`
158
- `)}function nc(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t")}"`}var Sr=["storage",".env"],Mt=4;function Ne(e){return`${e.replace(/\/+$/,"")}/.ts-cloud`}function sc(e){return`${Ne(e)}/deploy-history.log`}function oc(e,t){return`${Ne(e)}/deploys/${t}.log`}function ks(e){return`${Ne(e)}/shared-paths`}function vr(e){return`${e.shared}/stx-images`}function zt(e){return`${e.shared}/stx-bundles`}function er(e,t){let r=e.replace(/\/+$/,"");return{base:r,releases:`${r}/releases`,shared:`${r}/shared`,current:`${r}/current`,release:`${r}/releases/${t}`}}function Hs(e){let t=e.split("/").pop()||e;if(t===".env"||t.startsWith(".env."))return!0;return/\.[a-z0-9]+$/i.test(t)}function $n(e){return typeof e==="string"?e:e.path}function Ns(e){let t=new Map;for(let r of e)t.set($n(r),r);return[...t.values()]}function Ms(e,t){if(typeof t==="string")return{path:t,target:`${e.shared}/${t}`,seed:!0};return{path:t.path,target:t.target??`${e.shared}/${t.path}`,seed:t.seed!==!1}}function ic(e){return["ts_cloud_adopt_shared() {",' TS_CLOUD_SP="$1"',' TS_CLOUD_DST="$2"',` TS_CLOUD_SRC="${e.current}/$TS_CLOUD_SP"`,' if [ -e "$TS_CLOUD_DST" ]; then return 0; fi',' if [ -L "$TS_CLOUD_SRC" ] || [ ! -e "$TS_CLOUD_SRC" ]; then return 0; fi',' mkdir -p "$(dirname "$TS_CLOUD_DST")"',' cp -a "$TS_CLOUD_SRC" "$TS_CLOUD_DST"'," for TS_CLOUD_SIDECAR in -wal -shm; do",' if [ -f "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" ]; then cp -a "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" "$TS_CLOUD_DST$TS_CLOUD_SIDECAR"; fi'," done",' echo "[ts-cloud] adopted $TS_CLOUD_SP from the live release into shared/ — it now survives deploys"'," return 0","}"]}function Ft(e,t=Sr){let r=[`mkdir -p ${e.releases} ${e.shared} ${vr(e)} ${zt(e)}`,...ic(e)],n=t.map((i)=>Ms(e,i));for(let{path:i,target:a,seed:c}of n){if(!c)continue;if(r.push(`ts_cloud_adopt_shared '${i}' '${a}'`),Hs(i))r.push(`mkdir -p "$(dirname ${a})"`,`touch ${a}`);else if(i==="storage")r.push(`mkdir -p ${a}/app/public`,`mkdir -p ${a}/framework/cache/data`,`mkdir -p ${a}/framework/sessions`,`mkdir -p ${a}/framework/testing`,`mkdir -p ${a}/framework/views`,`mkdir -p ${a}/logs`);else r.push(`mkdir -p ${a}`)}let s=ks(e.base);return r.push(`mkdir -p ${Ne(e.base)}`,`cat > ${s} <<'TS_CLOUD_SHARED_PATHS_EOF'`,...n.map((i)=>`${i.path} ${i.target}`),"TS_CLOUD_SHARED_PATHS_EOF"),r}function yn(e,t=900){let r=`${Ne(e.base)}/deploy.lock`;return[`mkdir -p "$(dirname ${r})"`,`exec 9>${r}`,`flock -w ${t} 9 || { echo "[ts-cloud] another deploy of this site has held the lock for ${t}s — refusing to race it" >&2; exit 1; }`]}function bn(e){let t=`${e.release}.incoming`;return[`rm -rf ${t}`,`mkdir -p ${t}`,...ac(e),'if [ "$TS_CLOUD_IS_LIVE" = "yes" ]; then',` TS_CLOUD_STAGED=${t}`,"else",` rm -rf ${e.release}`,` mkdir -p ${e.release}`,` rmdir ${t}`,` TS_CLOUD_STAGED=${e.release}`,"fi"]}function Tn(e){let t=`TS_CLOUD_TRAP_LIVE="$(readlink -f ${e.current} 2>/dev/null || true)"`,r=`TS_CLOUD_TRAP_TARGET="$(readlink -f ${e.release} 2>/dev/null || echo ${e.release})"`;return`trap '${`if [ $? -ne 0 ]; then ${t}; ${r}; [ "$TS_CLOUD_TRAP_LIVE" = "$TS_CLOUD_TRAP_TARGET" ] || rm -rf ${e.release}; fi`}' EXIT`}function ac(e){return[`TS_CLOUD_LIVE_PATH="$(readlink -f ${e.current} 2>/dev/null || true)"`,`TS_CLOUD_RELEASE_PATH="$(readlink -f ${e.release} 2>/dev/null || echo ${e.release})"`,'if [ -n "$TS_CLOUD_LIVE_PATH" ] && [ "$TS_CLOUD_LIVE_PATH" = "$TS_CLOUD_RELEASE_PATH" ]; then TS_CLOUD_IS_LIVE=yes; else TS_CLOUD_IS_LIVE=no; fi']}function wn(e){let t=`${e.release}.incoming`;return[`if [ "$TS_CLOUD_STAGED" = "${t}" ]; then`,` rm -rf ${e.release}.previous`,` mv ${e.release} ${e.release}.previous`,` mv ${t} ${e.release}`,` rm -rf ${e.release}.previous`,"fi"]}function cc(e,t){return[`if [ -f ${e} ] && [ ! -L ${e} ] && [ -s ${e} ] && [ ! -s ${t} ]; then`,` cp -a ${e} ${t}`,` echo "[ts-cloud] seeded shared/ from the release's own copy of the file"`,"fi"]}function yr(e,t=Sr){let r=[];for(let n of t){let{path:s,target:i,seed:a}=Ms(e,n),c=`${e.release}/${s}`,f=[`rm -rf ${c}`,`mkdir -p "$(dirname ${c})"`,`ln -sfn ${i} ${c}`];if(a){if(s!==".env"&&Hs(s))r.push(...cc(c,i));r.push(...f);continue}r.push(`if [ -e ${i} ]; then`,...f.map((g)=>` ${g}`),"fi")}return r}function lc(e,t){let r=ks(e.base);return[`if [ -f ${r} ]; then`,' while IFS="\t" read -r TS_CLOUD_SP TS_CLOUD_TGT; do',' [ -n "$TS_CLOUD_SP" ] || continue',` [ -n "$TS_CLOUD_TGT" ] || TS_CLOUD_TGT="${e.shared}/$TS_CLOUD_SP"`,' [ -e "$TS_CLOUD_TGT" ] || continue',` rm -rf ${t}/"$TS_CLOUD_SP"`,` mkdir -p "$(dirname ${t}/"$TS_CLOUD_SP")"`,` ln -sfn "$TS_CLOUD_TGT" ${t}/"$TS_CLOUD_SP"`,` done < ${r}`,"fi"]}function dt(e){return[`ln -sfn ${e.release} ${e.current}.tmp`,`mv -Tf ${e.current}.tmp ${e.current}`]}function Cn(e){return`${Ne(e)}/previous-release`}function Pn(e,t="TS_CLOUD_PREV_CURRENT"){let r=Cn(e.base);return[`if [ -n "\${${t}:-}" ] && [ -d "$${t}" ] && [ "$(readlink -f "$${t}")" != "$(readlink -f ${e.release})" ]; then mkdir -p ${Ne(e.base)}; basename "$${t}" > ${r}; fi`]}function zs(e,t){let r=Cn(e.base);return[`TS_CLOUD_CURRENT=$(readlink -f ${e.current} 2>/dev/null || true)`,...t?[`TS_CLOUD_RB_TARGET=${e.releases}/${t}`]:["TS_CLOUD_RB_TARGET=",`if [ -f ${r} ]; then TS_CLOUD_REC="${e.releases}/$(cat ${r})"; if [ -d "$TS_CLOUD_REC" ] && [ "$(readlink -f "$TS_CLOUD_REC")" != "$TS_CLOUD_CURRENT" ]; then TS_CLOUD_RB_TARGET="$TS_CLOUD_REC"; fi; fi`,`[ -n "$TS_CLOUD_RB_TARGET" ] || TS_CLOUD_RB_TARGET=$(ls -1dt ${e.releases}/*/ 2>/dev/null | sed 's#/$##' | while read -r r; do [ "$(readlink -f "$r")" != "$TS_CLOUD_CURRENT" ] && { echo "$r"; break; }; done)`],'[ -n "$TS_CLOUD_RB_TARGET" ] && [ -d "$TS_CLOUD_RB_TARGET" ] || { echo "no release to roll back to (looked for ${TS_CLOUD_RB_TARGET:-a previous release})" >&2; exit 1; }','[ "$(readlink -f "$TS_CLOUD_RB_TARGET")" != "$TS_CLOUD_CURRENT" ] || { echo "$(basename "$TS_CLOUD_RB_TARGET") is already the live release" >&2; exit 1; }','TS_CLOUD_RB_ID=$(basename "$TS_CLOUD_RB_TARGET")']}function zg(e,t={}){return[...zs(e,t.to),'echo "would roll back from $(basename "${TS_CLOUD_CURRENT:-none}") to $TS_CLOUD_RB_ID (dry run: nothing changed)"']}function Kg(e,t={}){let r=Math.max(1,t.gateSeconds??5),n=t.systemdDir??"/etc/systemd/system",s=[`ln -sfn "$TS_CLOUD_RB_TARGET" ${e.current}.tmp`,`mv -Tf ${e.current}.tmp ${e.current}`,`rm -f ${Cn(e.base)}`,'echo "rolled back to $TS_CLOUD_RB_ID"'],i=[...zs(e,t.to),...lc(e,'"$TS_CLOUD_RB_TARGET"')];if(!t.unitBase)return[...i,...s];let a=t.unitBase,c=`${a}@\${TS_CLOUD_RB_ID}.service`;return[...i,`if [ -f ${n}/${a}@.service ]; then`,` systemctl start "${c}"`,` for TS_CLOUD_I in $(seq 1 ${r}); do sleep 1; systemctl is-active --quiet "${c}" || { echo "the release rolled back to, $TS_CLOUD_RB_ID, did not stay up; the current release keeps serving" >&2; journalctl -u "${c}" -n 30 --no-pager >&2 || true; systemctl stop "${c}" 2>/dev/null || true; exit 1; }; done`,...s.map((f)=>` ${f}`),` systemctl enable "${c}" 2>/dev/null || true`,` systemctl list-units --plain --no-legend --type=service "${a}@*.service" 2>/dev/null | awk '{print $1}' | { grep -v "^${a}@\${TS_CLOUD_RB_ID}.service$" || true; } | while read -r TS_CLOUD_U; do systemctl stop "$TS_CLOUD_U" 2>/dev/null || true; systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,`elif [ -f ${n}/${a}.service ]; then`,...s.map((f)=>` ${f}`),` systemctl restart ${a}.service`,"else",...s.map((f)=>` ${f}`),"fi"]}function mt(e,t=4){let r=Math.max(1,t);return[`TS_CLOUD_CURRENT=$(readlink -f ${e.current} 2>/dev/null || true)`,`ls -1dt ${e.releases}/*/ 2>/dev/null | sed 's#/$##' | tail -n +${r+1} | while read -r TS_CLOUD_OLD; do`,' [ "$(readlink -f "$TS_CLOUD_OLD")" = "$TS_CLOUD_CURRENT" ] || rm -rf "$TS_CLOUD_OLD"',"done"]}function uc(e){return`${Ne(e)}/owner`}function En(e,t){let r=uc(e);return[`if [ -f "${r}" ]; then`,` TS_CLOUD_OWNER=$(cat "${r}")`,` if [ "$TS_CLOUD_OWNER" != "${t}" ]; then`,` echo "[ts-cloud] REFUSING deploy: ${e} belongs to project '$TS_CLOUD_OWNER', not '${t}'. Another project on this box derives the same site key — give one of them a distinct site name/domain, or remove ${r} to transfer ownership." >&2`," exit 1"," fi","else",` mkdir -p "$(dirname "${r}")"`,` printf '%s\\n' "${t}" > "${r}"`,"fi"]}function br(e,t){let r=Ne(e),n=oc(e,t.releaseId),s=sc(e),i=Math.max(1,t.keepLogs??20),a=t.commit||"",c=t.branch||"";return[`mkdir -p ${r}/deploys`,`exec > >(tee -a ${n}) 2>&1`,`echo "[ts-cloud] deploy ${t.releaseId} commit=${a} branch=${c} starting $(date -u +%Y-%m-%dT%H:%M:%SZ)"`,"ts_cloud_record_deploy() {"," TS_CLOUD_RC=$?",' if [ "$TS_CLOUD_RC" -eq 0 ]; then TS_CLOUD_ST=success; else TS_CLOUD_ST=failed; fi',` printf '%s\\t%s\\t%s\\t%s\\trc=%s\\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${t.releaseId}" "${a}" "$TS_CLOUD_ST" "$TS_CLOUD_RC" >> ${s}`,"}","trap ts_cloud_record_deploy EXIT",`ls -1t ${r}/deploys/*.log 2>/dev/null | tail -n +${i+1} | while read -r TS_CLOUD_OLDLOG; do rm -f "$TS_CLOUD_OLDLOG"; done`]}var pc=new Set(["sqlite","sqlite3"]),dc=new Set([":memory:","memory"]);function mc(e){let t=e?.DB_CONNECTION?.trim().toLowerCase();return t!=null&&pc.has(t)}function fc(e){let t=e.trim().replace(/^\.\//,"");if(t===""||t.startsWith("/")||t.startsWith("~"))return;if(t.split("/").includes(".."))return;return t.replace(/\/+$/,"")}function Rn(e,t=[]){if(!mc(e))return{};let r=e?.DB_DATABASE_PATH?.trim()||e?.DB_DATABASE?.trim();if(r&&dc.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=fc(r);if(!n)return{};if(t.some((s)=>$n(s)===n))return{};return{path:n}}function Tr(e,t=[]){let{path:r}=Rn(e,t);return r?[r]:[]}var Ur="/var/ts-cloud/artifacts",Xh="/usr/local/bin/ts-cloud-host-cleanup.sh",Yh="ts-cloud-host-cleanup";var Wg=Object.freeze({stagingMaxAgeMinutes:60,artifactMaxAgeDays:2,artifactUploadMaxAgeMinutes:60,bunCacheMaxAgeDays:7,journalMaxAgeDays:14,journalMaxSizeMb:512,containerImageMaxAgeHours:168}),Jg=Object.freeze({artifactMaxAgeDays:0,bunCacheMaxAgeDays:1,journalMaxAgeDays:3,journalMaxSizeMb:256,containerImageMaxAgeHours:24}),Zh=50,eA=85,tA="daily";function Fs(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Qe(e,t,r=0){if(!Number.isInteger(t)||t<r)throw Error(`hostCleanup: ${e} must be a whole number >= ${r} (got ${t})`)}function Bs(e,t){for(let[r,n]of Object.entries(t))Qe(`${e}.${r}`,n);Qe(`${e}.stagingMaxAgeMinutes`,t.stagingMaxAgeMinutes,15),Qe(`${e}.artifactUploadMaxAgeMinutes`,t.artifactUploadMaxAgeMinutes,15),Qe(`${e}.journalMaxSizeMb`,t.journalMaxSizeMb,1)}function gc(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(Qe(`${r}.maxAgeDays`,e.maxAgeDays),e.type!==void 0&&e.type!=="file"&&e.type!=="directory")throw Error(`${r}.type must be 'file' or 'directory'`)}function Qr(e){let t=typeof e==="object"&&e!==null?e:{},r=e!==!1,n={...Wg,...Ks(t.retention)};Bs("hostCleanup.retention",n);let s=null;if(t.pressure!==!1){let c=typeof t.pressure==="object"?t.pressure:{},f=c.lowWaterPercent??50,g=c.highWaterPercent??85;if(Qe("hostCleanup.pressure.lowWaterPercent",f),Qe("hostCleanup.pressure.highWaterPercent",g,1),g>100||f>=g)throw Error(`hostCleanup.pressure: need 0 <= lowWaterPercent < highWaterPercent <= 100 (got ${f} and ${g})`);let h={...Jg,...Ks(c.escalated)},S=Object.fromEntries(Object.keys(n).map((_)=>[_,Math.min(n[_],h[_]??n[_])]));Bs("hostCleanup.pressure.escalated",S),s={lowWaterPercent:f,highWaterPercent:g,escalated:S}}let i=t.schedule??"daily";if(!/^[\w*:,./~ -]+$/.test(i))throw Error(`hostCleanup.schedule must be a systemd OnCalendar expression (got ${JSON.stringify(i)})`);let a=t.paths??[];return a.forEach(gc),{enabled:r,timer:r&&t.timer!==!1,schedule:i,retention:n,pressure:s,paths:a}}function Ks(e){return Object.fromEntries(Object.entries(e??{}).filter(([,t])=>t!==void 0))}var Kt={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 On(e){return Object.keys(Kt).map((t)=>`${Kt[t]}=${e[t]}`).join(" ")}var Ze=(e)=>`"$${Kt[e]}"`;function js(e){return[`find /var/ts-cloud/staging -xdev -maxdepth 1 -type f -mmin +${Ze("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /tmp -xdev -maxdepth 1 -type f -name "*-release.tar.gz" -mmin +${Ze("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,"TS_CLOUD_HC_ARTIFACT_MIN=$((TS_CLOUD_HC_ARTIFACT_DAYS * 1440))",'[ "$TS_CLOUD_HC_ARTIFACT_MIN" -gt 0 ] || TS_CLOUD_HC_ARTIFACT_MIN=1','find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name "*.tar.gz" -mmin +"$TS_CLOUD_HC_ARTIFACT_MIN" -delete 2>/dev/null || true',`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name ".*.tmp" -mmin +${Ze("artifactUploadMaxAgeMinutes")} -delete 2>/dev/null || true`,...e.map(hc),`journalctl --vacuum-time=${Ze("journalMaxAgeDays")}d --vacuum-size=${Ze("journalMaxSizeMb")}M >/dev/null 2>&1 || true`]}function hc(e){let t=Fs(e.path.replace(/\/+$/,"")),r=Fs(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 Gs(){return[`find /root/.bun/install/cache -xdev -type f -mtime +${Ze("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`find /root/.bun/install/cache -xdev -depth -type d -empty -mtime +${Ze("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`if command -v docker >/dev/null 2>&1; then docker image prune --all --force --filter "until=\${${Kt.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,`if command -v podman >/dev/null 2>&1; then podman image prune --all --force --filter "until=\${${Kt.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 Vs(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 ii(e){let{retention:t,pressure:r,paths:n}=Qr(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)"',On(t),...js(n),...Gs(),'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"'];let{lowWaterPercent:a,highWaterPercent:c,escalated:f}=r;return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',...Vs("TS_CLOUD_HC_BEFORE"),"TS_CLOUD_HC_TIER=normal",On(t),`if [ "$TS_CLOUD_HC_BEFORE" -ge 0 ] && [ "$TS_CLOUD_HC_BEFORE" -lt ${a} ]; then`," TS_CLOUD_HC_TIER=light",` echo "[ts-cloud] host cleanup: / at \${TS_CLOUD_HC_BEFORE}% (under ${a}%), skipping cache and image pruning"`,`elif [ "$TS_CLOUD_HC_BEFORE" -ge ${c} ]; then`," TS_CLOUD_HC_TIER=pressure",` ${On(f)}`,` echo "[ts-cloud] warning: / at \${TS_CLOUD_HC_BEFORE}% (at or above ${c}%), host cleanup is using shortened retention windows" >&2`,"fi",...js(n),'if [ "$TS_CLOUD_HC_TIER" != light ]; then',...Gs().map((g)=>` ${g}`),"fi",'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"',...Vs("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\\":${a},\\"highWaterPercent\\":${c}}"`,"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 ${c} ]; then`,` echo "[ts-cloud] warning: / still at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${c}%); 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 ${c}%)" >/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 ${c}% 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 Xg(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",...ii(e),"exit 0"]}function Dn(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 co(e){let t=Qr(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=Dn("/usr/local/bin/ts-cloud-host-cleanup.sh",Xg(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",...Dn("/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"),...Dn("/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 si(e){let t=co(e);if(!Qr(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 Xs(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}=${je(String(i))}`]});return[`cd ${e}`,`if [ -d ${u} ] && command -v pantry >/dev/null 2>&1; then`,` ${Re()}`,"fi",...n,...t]}var Sc=/\.(?:[cm]?[jt]sx?)$/;function oi(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="",...a]=s.split(/\s+/);if(i&&!Sc.test(i)){let c=i.startsWith("/")?i:r&&i.startsWith("./")?`${r.replace(/\/+$/,"")}/${i.replace(/^\.\//,"")}`:void 0;if(c)return[c,...a].join(" ")}return`${n} ${s}`}function qs(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.
160
+ `)){let ne=Object.fromEntries(J.split(`
161
+ `).filter(Boolean).map((D)=>{let k=D.indexOf("=");return[D.slice(0,k),D.slice(k+1)]}));if(ne.WorkingDirectory?.startsWith(`${e.webRoot}/`)||/^(?:mail|rpx-gateway|postgresql|mysql|redis)[@.]/.test(ne.Id??""))w[ne.Id]=Number(ne.MainPID)}}let I=new Map;if(r.existsSync("/etc/rpx/sites.d"))for(let K of r.readdirSync("/etc/rpx/sites.d").filter((J)=>J.endsWith(".json"))){let J=JSON.parse(r.readFileSync(n.join("/etc/rpx/sites.d",K),"utf8"));for(let ne of J.proxies??[]){if(typeof ne.to!=="string"||!/^[a-zA-Z0-9.-]+$/.test(ne.to))continue;let D=typeof ne.path==="string"&&ne.path.startsWith("/")?ne.path:"/";I.set(`${ne.to}${D}`,{domain:ne.to,path:D})}}let O=[...I.values()],N=[],te=0;return await Promise.all(Array.from({length:Math.min(8,O.length)},async()=>{while(te<O.length){let K=O[te++],J=Bun.spawn(["curl","-sS","--max-time","12","--output","/dev/null","--write-out","%{http_code}","--resolve",`${K.domain}:443:127.0.0.1`,`https://${K.domain}${K.path}`],{stdout:"pipe",stderr:"pipe"}),[ne,,D]=await Promise.all([new Response(J.stdout).text(),new Response(J.stderr).text(),J.exited]),k=Number(ne);N.push({...K,status:k,ok:D===0&&k>=200&&k<500})}})),{routes:N.sort((K,J)=>`${K.domain}${K.path}`.localeCompare(`${J.domain}${J.path}`)),services:w}},X=()=>{let T=[q.mountPath,...P.entries.map((I)=>I.source)],w=new Set(["rpx-gateway.service",...M?.dependentUnits??[]]);for(let I of r.readdirSync("/etc/systemd/system")){if(!I.endsWith(".service"))continue;try{if(r.readFileSync(`/etc/systemd/system/${I}`,"utf8").includes(e.webRoot))w.add(I)}catch{}}for(let I of w){let O=`/etc/systemd/system/${I}.d`;r.mkdirSync(O,{recursive:!0}),r.writeFileSync(`${O}/ts-cloud-storage.conf`,`[Unit]
162
+ RequiresMountsFor=${T.join(" ")}
163
+ `)}v("systemctl",["daemon-reload"])},A=()=>{P=h("/var/lib/ts-cloud/compute-storage-state.json",{entries:[]});let T=h("/etc/ts-cloud/compute-storage.json",null);if(T){for(let w of T.volumes)if(!e.volumes.some((I)=>I.id===w.id&&I.key===w.key&&I.mountPath===w.mountPath))throw Error(`Refusing to remove or relocate managed volume ${w.name}; migrate its data explicitly first`);if(P.entries.length&&(T.deploymentStorage?.volume!==M?.volume||T.deploymentStorage?.includeBackups!==!1!==(M?.includeBackups!==!1)))throw Error("Refusing to change an active deployment-storage policy; migrate its data explicitly first")}};try{let T=[...new Set([...M?.locks??[],"/run/lock/ts-cloud-host-cleanup.lock"])].sort();if(f==="reclaim"){for(let D of["/run/lock/ts-cloud-compute-storage.lock",...T])if(!me(D,!1))return console.log(`Deferring deployment storage reclamation; lock busy: ${D}`),{deferred:!0,lock:D}}else me("/run/lock/ts-cloud-compute-storage.lock");if(A(),f==="apply")ee();for(let D of e.volumes)if(!L(D))throw Error(`Required volume is not mounted: ${D.name}`);if(!M||!q)return{volumes:e.volumes};if(f==="ensure-site"){let[D,k]=t.slice(1);if(!D?.startsWith(`${e.webRoot}/`)||!/^[\w-]+$/.test(n.basename(D))||D.includes("..")||!/^[\w-]+$/.test(k??""))throw Error("Invalid deployment site path or project slug");if(r.mkdirSync(n.join(D,"releases"),{recursive:!0}),M.includeBackups!==!1)r.mkdirSync(n.join(e.webRoot,`${k}-shared`,"backups"),{recursive:!0})}let w=(f==="reclaim"?[]:G(f==="ensure-site"?t[1]:void 0,t[2])).filter((D)=>!P.entries.some((k)=>k.source===D.source));for(let D of P.entries)if(!b(D.source)||!S(D.source,D.destination))throw Error(`Recorded deployment mount is missing: ${D.source}`);if(!w.length&&f==="ensure-site")return{inherited:!0,volume:q.name};if(!w.length&&f==="apply"){X();let D=B();return g("/var/lib/ts-cloud/compute-storage-status.json",D),console.log(JSON.stringify(D)),D}if(f!=="reclaim"){let D=oe.pop();re.symbols.flock(D,8),r.closeSync(D),console.log("Waiting for deployment storage maintenance exclusion");for(let k of T)me(k);me("/run/lock/ts-cloud-compute-storage.lock"),A();for(let k of e.volumes)if(!L(k))throw Error(`Required volume is not mounted: ${k.name}`);for(let k of P.entries)if(!b(k.source)||!S(k.source,k.destination))throw Error(`Recorded deployment mount is missing: ${k.source}`);if(w=G(f==="ensure-site"?t[1]:void 0,t[2]).filter((k)=>!P.entries.some((ue)=>ue.source===k.source)),!w.length&&f==="ensure-site")return{inherited:!0,volume:q.name};if(!w.length){X();let k=B();return g("/var/lib/ts-cloud/compute-storage-status.json",k),console.log(JSON.stringify(k)),k}}if(!M.migrateExisting&&w.some((D)=>r.readdirSync(D.source).length>0))throw Error("Existing deployments require deploymentStorage.migrateExisting: true");if(w.length){let D=v("du",["-scx","--block-size=1",...w.map((ue)=>ue.source)]).trim().split(`
164
+ `).at(-1)?.split(/\s+/)[0],k=r.statfsSync(q.mountPath);if(Number(D)>k.bavail*k.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(!b("/run/ts-cloud-root-web"))v("mount",["--bind",e.webRoot,"/run/ts-cloud-root-web"]);v("mount",["--make-private","/run/ts-cloud-root-web"]);let I=r.statSync(e.webRoot).dev;for(let D of P.entries){let k=n.join("/run/ts-cloud-root-web",n.relative(e.webRoot,D.source));if(r.existsSync(k)&&(r.statSync(k).dev!==I||S(k,D.destination)))throw Error(`Root recovery view contains a deployment mount: ${k}`)}for(let D=0;D<w.length;D++){if(b(w[D].source)&&S(w[D].source,w[D].destination))continue;console.log(`copy ${D+1}/${w.length}: ${w[D].source}`),await ie(w[D],!1)}for(let D of r.readdirSync(e.webRoot,{withFileTypes:!0}).filter((k)=>k.isDirectory()).sort((k,ue)=>k.name.localeCompare(ue.name))){let k=n.join(e.webRoot,D.name,".ts-cloud/deploy.lock");if(!me(k,f!=="reclaim"))return console.log(`Deferring deployment storage reclamation; lock busy: ${k}`),{deferred:!0,lock:k}}for(let D of f==="reclaim"?[]:G(f==="ensure-site"?t[1]:void 0,t[2]))if(!P.entries.some((k)=>k.source===D.source)&&!w.some((k)=>k.source===D.source))w.push(D);if(w.length)console.log("Checking application services and HTTPS routes before activation");let O=w.length?await E():void 0;if(O)g("/var/lib/ts-cloud/compute-storage-health-before.json",O);let N=le().filter((D)=>D.write);if(N.length)throw Error(`Live writable files must be shared before migration: ${N.map((D)=>D.file).join(", ")}`);let te={entries:[...P.entries],fstab:r.readFileSync("/etc/fstab","utf8")};if(w.length)v("sync",[]);let K=[];try{for(let D=0;D<w.length;D++){let k=w[D];if(b(k.source)){if(!S(k.source,k.destination))throw Error(`Unrecorded mount occupies ${k.source}`);P.entries.push(k),g("/var/lib/ts-cloud/compute-storage-state.json",P);continue}if(console.log(`verify and mount ${D+1}/${w.length}: ${k.source}`),await ie(k,!0),v("mount",["--bind",k.destination,k.source]),!S(k.source,k.destination))throw Error(`Bind verification failed: ${k.source}`);K.push(k),P.entries.push(k),g("/var/lib/ts-cloud/compute-storage-state.json",P),Z()}if(Z(),v("findmnt",["--verify"]),P.activatedAt=new Date().toISOString(),g("/var/lib/ts-cloud/compute-storage-state.json",P),X(),O){console.log("Checking application services and HTTPS routes after activation");let D=await E();g("/var/lib/ts-cloud/compute-storage-health-after.json",D);let k=O.routes.filter((de)=>de.ok&&!D.routes.some((be)=>be.domain===de.domain&&be.path===de.path&&be.ok)),ue=Object.keys(O.services).filter((de)=>!D.services[de]);if(k.length||ue.length)throw Error(`Application verification failed: ${k.map((de)=>de.domain+de.path).concat(ue).join(", ")}`);console.log(`Verified ${D.routes.length} routes and ${Object.keys(D.services).length} services`)}}catch(D){let k=[];for(let ue of K.reverse())try{v("umount",[ue.source])}catch{k.push(ue),console.error(`Rollback mount busy, original data retained: ${ue.source}`)}if(P.entries=[...te.entries,...k],r.writeFileSync("/etc/fstab",te.fstab),k.length)Z();throw g("/var/lib/ts-cloud/compute-storage-state.json",P),v("systemctl",["daemon-reload"]),D}let J=le().filter((D)=>D.device===I).map((D)=>D.file);for(let D of P.entries){if(!b(D.source)||!S(D.source,D.destination))throw Error(`Lost deployment mount: ${D.source}`);let k=n.join("/run/ts-cloud-root-web",n.relative(e.webRoot,D.source));if(!r.existsSync(k))continue;if(r.statSync(k).dev!==I||S(k,D.destination))throw Error(`Refusing to clean a deployment volume through the root recovery view: ${k}`);let ue="";try{ue=n.basename(r.realpathSync(n.join(n.dirname(D.source),"current")))}catch{}for(let de of r.readdirSync(k,{withFileTypes:!0})){let be=n.join(k,de.name);if(n.basename(D.source)==="backups"){if(de.isFile()&&r.existsSync(n.join(D.destination,de.name)))r.unlinkSync(be)}else if(de.isDirectory()&&!de.isSymbolicLink()&&de.name!==ue&&!J.some((Ot)=>Ot===n.join(D.source,de.name)||Ot.startsWith(`${n.join(D.source,de.name)}/`)))r.rmSync(be,{recursive:!0})}}v("sync",[]);let ne=B();return g("/var/lib/ts-cloud/compute-storage-status.json",ne),console.log(JSON.stringify(ne)),ne}finally{for(let T of oe.reverse())re.symbols.flock(T,8),r.closeSync(T);re.close()}}var yn="/etc/ts-cloud/compute-storage.json",Be="/usr/local/lib/ts-cloud/compute-storage.mjs";function ri(e,t){let r=Object.entries(e.infrastructure?.compute?.volumes??{}).map(([i,a])=>{if(!/^[a-z][a-z0-9-]*$/.test(i))throw Error(`Invalid compute volume key: ${i}`);if(!Number.isInteger(a.size)||a.size<10||a.size>10240)throw Error(`compute.volumes.${i}.size must be a whole number from 10 to 10240 GB`);if(!a.mountPath.startsWith("/")||!a.mountPath.slice(1).split("/").every((f)=>/^[\w-]+$/.test(f))||a.mountPath==="/var"||a.mountPath==="/var/lib"||["/etc","/usr","/var/www","/root","/boot","/dev","/proc","/sys","/run","/bin","/sbin","/lib"].some((f)=>a.mountPath===f||a.mountPath.startsWith(`${f}/`)))throw Error(`Unsafe compute volume mount path: ${a.mountPath}`);let c=a.name??`${e.project.slug}-${t}-${i}`;if(!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(c))throw Error(`Invalid compute volume name: ${c}`);return{...a,key:i,name:c,mountPath:a.mountPath.replace(/\/+$/,"")}});for(let i of r)if(r.some((a)=>a!==i&&(a.name===i.name||a.mountPath===i.mountPath||a.mountPath.startsWith(`${i.mountPath}/`))))throw Error("Compute volumes must have distinct names and non-overlapping mount paths");let n=e.infrastructure?.compute?.deploymentStorage;for(let i of n?.locks??[])if(!i.startsWith("/run/lock/")||!i.slice(10).split("/").every((a)=>/^[\w.-]+$/.test(a)&&a!=="."&&a!=="..")||i==="/run/lock/ts-cloud-compute-storage.lock")throw Error(`Invalid deployment storage lock: ${i}`);for(let i of n?.dependentUnits??[])if(!/^[\w@.-]+\.service$/.test(i))throw Error(`Invalid deployment storage dependent unit: ${i}`);if(n&&!r.some((i)=>i.key===n.volume))throw Error(`deploymentStorage.volume references an undeclared compute volume: ${n.volume}`);let s=e.infrastructure?.compute?.persistentStorage;if(s){if(!r.some((i)=>i.key===s.volume))throw Error("persistentStorage references an undeclared volume");for(let i of s.locks??[])if(!/^\/run\/lock\/[\w.-]+$/.test(i)||i.includes("..")||i.includes("ts-cloud-"))throw Error("Invalid persistent storage lock");if(!s.paths.length)throw Error("Persistent storage paths must not be empty");for(let i of s.paths){if(!i.path.startsWith("/")||!["var","srv","home","root"].includes(i.path.split("/")[1])||i.path.split("/").length<3||!i.path.slice(1).split("/").every((a)=>/^[\w.-]+$/.test(a))||i.path.split("/").some((a)=>a==="."||a==="..")||["/var/lib","/var/www","/var/cache","/var/log"].includes(i.path)||r.some((a)=>i.path===a.mountPath||i.path.startsWith(a.mountPath+"/")))throw Error(`Unsafe persistent storage path: ${i.path}`);if(s.paths.some((a)=>a!==i&&(a.path===i.path||a.path.startsWith(i.path+"/"))))throw Error("Persistent storage paths overlap");for(let a of i.units)if(!/^[\w@.-]+(?:\*)?\.(?:service|timer)$/.test(a)||a.includes("*")&&!/[-@]\*\.(?:service|timer)$/.test(a))throw Error("Invalid persistent storage unit");for(let a of i.healthChecks??[]){let c=new URL(a);if(!["http:","https:"].includes(c.protocol)||c.username||c.password)throw Error("Invalid persistent storage health URL")}}}return r}function hr(e){return`'${e.replaceAll("'",`'"'"'`)}'`}function ni(e){let t=`${yn}.candidate`,r=`#!/usr/bin/env bun
165
+ const policy = JSON.parse(await Bun.file(process.env.TS_CLOUD_STORAGE_POLICY || ${JSON.stringify(yn)}).text());
166
+ await (${Us.toString()})(policy, process.argv.slice(2));
167
+ if (['apply', 'check', 'status'].includes(process.argv[2])) await (${Ls.toString()})(policy, process.argv[2] === 'apply' ? 'apply' : 'check');
168
+ `;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 ${hr(Buffer.from(r).toString("base64"))} | base64 -d > ${Be}.new`,`chmod 0755 ${Be}.new && mv ${Be}.new ${Be}`,`printf %s ${hr(JSON.stringify(e))} > ${t}`,`chmod 0644 ${t}`,`TS_CLOUD_STORAGE_POLICY=${t} bun ${Be} 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 ${Be} 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 ${Be} 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 Fr(e,t){return[`if [ -f ${yn} ]; then bun ${Be} ensure-site ${hr(e)} ${hr(t)}; fi`]}import{createHash as rc}from"node:crypto";function nc(){return["if [ -x /usr/local/bin/bun ]; then",' TS_CLOUD_HOST_BUN="$(readlink -f /usr/local/bin/bun)"',' case "$TS_CLOUD_HOST_BUN" in /root/*|/home/*)'," install -d -m 755 /opt/ts-cloud /opt/ts-cloud/bun /opt/ts-cloud/bun/bin",' TS_CLOUD_BUN_STAGE="$(mktemp /opt/ts-cloud/bun/bin/.bun.XXXXXX)"',' install -m 755 "$TS_CLOUD_HOST_BUN" "$TS_CLOUD_BUN_STAGE"',' mv -f "$TS_CLOUD_BUN_STAGE" /opt/ts-cloud/bun/bin/bun'," ln -sfn /opt/ts-cloud/bun/bin/bun /usr/local/bin/bun"," ;; esac","fi"]}function pt(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)}-${rc("sha256").update(e).digest("hex").slice(0,8)}`}`}function Ht(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 ks(e,t,r){let n=pt(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`,a=[...new Set([t,...r])];for(let c of a)if(!/^\/[a-zA-Z0-9_./-]+$/.test(c)||c.split("/").includes("..")||!(c===t||c.startsWith(`${t}/`)||c===i||c.startsWith(`${i}/`)))throw Error(`Runtime isolation cannot take ownership outside project ${e}`);return[...nc(),`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}`,...a.map((c)=>`chown -hR ${n}:${n} ${c}`),`chmod 750 ${t}`]}function gr(e){return Object.entries(e).map(([t,r])=>`${t}=${sc(String(r))}`).join(`
169
+ `)}function sc(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t")}"`}var Sr=["storage",".env"],Mt=4;function Ne(e){return`${e.replace(/\/+$/,"")}/.ts-cloud`}function oc(e){return`${Ne(e)}/deploy-history.log`}function ic(e,t){return`${Ne(e)}/deploys/${t}.log`}function Hs(e){return`${Ne(e)}/shared-paths`}function vr(e){return`${e.shared}/stx-images`}function Ft(e){return`${e.shared}/stx-bundles`}function er(e,t){let r=e.replace(/\/+$/,"");return{base:r,releases:`${r}/releases`,shared:`${r}/shared`,current:`${r}/current`,release:`${r}/releases/${t}`}}function Ns(e){let t=e.split("/").pop()||e;if(t===".env"||t.startsWith(".env."))return!0;return/\.[a-z0-9]+$/i.test(t)}function _n(e){return typeof e==="string"?e:e.path}function Ms(e){let t=new Map;for(let r of e)t.set(_n(r),r);return[...t.values()]}function Fs(e,t){if(typeof t==="string")return{path:t,target:`${e.shared}/${t}`,seed:!0};return{path:t.path,target:t.target??`${e.shared}/${t.path}`,seed:t.seed!==!1}}function ac(e){return["ts_cloud_adopt_shared() {",' TS_CLOUD_SP="$1"',' TS_CLOUD_DST="$2"',` TS_CLOUD_SRC="${e.current}/$TS_CLOUD_SP"`,' if [ -e "$TS_CLOUD_DST" ]; then return 0; fi',' if [ -L "$TS_CLOUD_SRC" ] || [ ! -e "$TS_CLOUD_SRC" ]; then return 0; fi',' mkdir -p "$(dirname "$TS_CLOUD_DST")"',' cp -a "$TS_CLOUD_SRC" "$TS_CLOUD_DST"'," for TS_CLOUD_SIDECAR in -wal -shm; do",' if [ -f "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" ]; then cp -a "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" "$TS_CLOUD_DST$TS_CLOUD_SIDECAR"; fi'," done",' echo "[ts-cloud] adopted $TS_CLOUD_SP from the live release into shared/ — it now survives deploys"'," return 0","}"]}function zt(e,t=Sr){let r=[`mkdir -p ${e.releases} ${e.shared} ${vr(e)} ${Ft(e)}`,...ac(e)],n=t.map((i)=>Fs(e,i));for(let{path:i,target:a,seed:c}of n){if(!c)continue;if(r.push(`ts_cloud_adopt_shared '${i}' '${a}'`),Ns(i))r.push(`mkdir -p "$(dirname ${a})"`,`touch ${a}`);else if(i==="storage")r.push(`mkdir -p ${a}/app/public`,`mkdir -p ${a}/framework/cache/data`,`mkdir -p ${a}/framework/sessions`,`mkdir -p ${a}/framework/testing`,`mkdir -p ${a}/framework/views`,`mkdir -p ${a}/logs`);else r.push(`mkdir -p ${a}`)}let s=Hs(e.base);return r.push(`mkdir -p ${Ne(e.base)}`,`cat > ${s} <<'TS_CLOUD_SHARED_PATHS_EOF'`,...n.map((i)=>`${i.path} ${i.target}`),"TS_CLOUD_SHARED_PATHS_EOF"),r}function $n(e,t=900){let r=`${Ne(e.base)}/deploy.lock`;return[`mkdir -p "$(dirname ${r})"`,`exec 9>${r}`,`flock -w ${t} 9 || { echo "[ts-cloud] another deploy of this site has held the lock for ${t}s — refusing to race it" >&2; exit 1; }`]}function bn(e){let t=`${e.release}.incoming`;return[`rm -rf ${t}`,`mkdir -p ${t}`,...cc(e),'if [ "$TS_CLOUD_IS_LIVE" = "yes" ]; then',` TS_CLOUD_STAGED=${t}`,"else",` rm -rf ${e.release}`,` mkdir -p ${e.release}`,` rmdir ${t}`,` TS_CLOUD_STAGED=${e.release}`,"fi"]}function Tn(e){let t=`TS_CLOUD_TRAP_LIVE="$(readlink -f ${e.current} 2>/dev/null || true)"`,r=`TS_CLOUD_TRAP_TARGET="$(readlink -f ${e.release} 2>/dev/null || echo ${e.release})"`;return`trap '${`if [ $? -ne 0 ]; then ${t}; ${r}; [ "$TS_CLOUD_TRAP_LIVE" = "$TS_CLOUD_TRAP_TARGET" ] || rm -rf ${e.release}; fi`}' EXIT`}function cc(e){return[`TS_CLOUD_LIVE_PATH="$(readlink -f ${e.current} 2>/dev/null || true)"`,`TS_CLOUD_RELEASE_PATH="$(readlink -f ${e.release} 2>/dev/null || echo ${e.release})"`,'if [ -n "$TS_CLOUD_LIVE_PATH" ] && [ "$TS_CLOUD_LIVE_PATH" = "$TS_CLOUD_RELEASE_PATH" ]; then TS_CLOUD_IS_LIVE=yes; else TS_CLOUD_IS_LIVE=no; fi']}function wn(e){let t=`${e.release}.incoming`;return[`if [ "$TS_CLOUD_STAGED" = "${t}" ]; then`,` rm -rf ${e.release}.previous`,` mv ${e.release} ${e.release}.previous`,` mv ${t} ${e.release}`,` rm -rf ${e.release}.previous`,"fi"]}function lc(e,t){return[`if [ -f ${e} ] && [ ! -L ${e} ] && [ -s ${e} ] && [ ! -s ${t} ]; then`,` cp -a ${e} ${t}`,` echo "[ts-cloud] seeded shared/ from the release's own copy of the file"`,"fi"]}function yr(e,t=Sr){let r=[];for(let n of t){let{path:s,target:i,seed:a}=Fs(e,n),c=`${e.release}/${s}`,f=[`rm -rf ${c}`,`mkdir -p "$(dirname ${c})"`,`ln -sfn ${i} ${c}`];if(a){if(s!==".env"&&Ns(s))r.push(...lc(c,i));r.push(...f);continue}r.push(`if [ -e ${i} ]; then`,...f.map((h)=>` ${h}`),"fi")}return r}function uc(e,t){let r=Hs(e.base);return[`if [ -f ${r} ]; then`,' while IFS="\t" read -r TS_CLOUD_SP TS_CLOUD_TGT; do',' [ -n "$TS_CLOUD_SP" ] || continue',` [ -n "$TS_CLOUD_TGT" ] || TS_CLOUD_TGT="${e.shared}/$TS_CLOUD_SP"`,' [ -e "$TS_CLOUD_TGT" ] || continue',` rm -rf ${t}/"$TS_CLOUD_SP"`,` mkdir -p "$(dirname ${t}/"$TS_CLOUD_SP")"`,` ln -sfn "$TS_CLOUD_TGT" ${t}/"$TS_CLOUD_SP"`,` done < ${r}`,"fi"]}function dt(e){return[`ln -sfn ${e.release} ${e.current}.tmp`,`mv -Tf ${e.current}.tmp ${e.current}`]}function Cn(e){return`${Ne(e)}/previous-release`}function Pn(e,t="TS_CLOUD_PREV_CURRENT"){let r=Cn(e.base);return[`if [ -n "\${${t}:-}" ] && [ -d "$${t}" ] && [ "$(readlink -f "$${t}")" != "$(readlink -f ${e.release})" ]; then mkdir -p ${Ne(e.base)}; basename "$${t}" > ${r}; fi`]}function zs(e,t){let r=Cn(e.base);return[`TS_CLOUD_CURRENT=$(readlink -f ${e.current} 2>/dev/null || true)`,...t?[`TS_CLOUD_RB_TARGET=${e.releases}/${t}`]:["TS_CLOUD_RB_TARGET=",`if [ -f ${r} ]; then TS_CLOUD_REC="${e.releases}/$(cat ${r})"; if [ -d "$TS_CLOUD_REC" ] && [ "$(readlink -f "$TS_CLOUD_REC")" != "$TS_CLOUD_CURRENT" ]; then TS_CLOUD_RB_TARGET="$TS_CLOUD_REC"; fi; fi`,`[ -n "$TS_CLOUD_RB_TARGET" ] || TS_CLOUD_RB_TARGET=$(ls -1dt ${e.releases}/*/ 2>/dev/null | sed 's#/$##' | while read -r r; do [ "$(readlink -f "$r")" != "$TS_CLOUD_CURRENT" ] && { echo "$r"; break; }; done)`],'[ -n "$TS_CLOUD_RB_TARGET" ] && [ -d "$TS_CLOUD_RB_TARGET" ] || { echo "no release to roll back to (looked for ${TS_CLOUD_RB_TARGET:-a previous release})" >&2; exit 1; }','[ "$(readlink -f "$TS_CLOUD_RB_TARGET")" != "$TS_CLOUD_CURRENT" ] || { echo "$(basename "$TS_CLOUD_RB_TARGET") is already the live release" >&2; exit 1; }','TS_CLOUD_RB_ID=$(basename "$TS_CLOUD_RB_TARGET")']}function zg(e,t={}){return[...zs(e,t.to),'echo "would roll back from $(basename "${TS_CLOUD_CURRENT:-none}") to $TS_CLOUD_RB_ID (dry run: nothing changed)"']}function Kg(e,t={}){let r=Math.max(1,t.gateSeconds??5),n=t.systemdDir??"/etc/systemd/system",s=[`ln -sfn "$TS_CLOUD_RB_TARGET" ${e.current}.tmp`,`mv -Tf ${e.current}.tmp ${e.current}`,`rm -f ${Cn(e.base)}`,'echo "rolled back to $TS_CLOUD_RB_ID"'],i=[...zs(e,t.to),...uc(e,'"$TS_CLOUD_RB_TARGET"')];if(!t.unitBase)return[...i,...s];let a=t.unitBase,c=`${a}@\${TS_CLOUD_RB_ID}.service`;return[...i,`if [ -f ${n}/${a}@.service ]; then`,` systemctl start "${c}"`,` for TS_CLOUD_I in $(seq 1 ${r}); do sleep 1; systemctl is-active --quiet "${c}" || { echo "the release rolled back to, $TS_CLOUD_RB_ID, did not stay up; the current release keeps serving" >&2; journalctl -u "${c}" -n 30 --no-pager >&2 || true; systemctl stop "${c}" 2>/dev/null || true; exit 1; }; done`,...s.map((f)=>` ${f}`),` systemctl enable "${c}" 2>/dev/null || true`,` systemctl list-units --plain --no-legend --type=service "${a}@*.service" 2>/dev/null | awk '{print $1}' | { grep -v "^${a}@\${TS_CLOUD_RB_ID}.service$" || true; } | while read -r TS_CLOUD_U; do systemctl stop "$TS_CLOUD_U" 2>/dev/null || true; systemctl disable "$TS_CLOUD_U" 2>/dev/null || true; done`,`elif [ -f ${n}/${a}.service ]; then`,...s.map((f)=>` ${f}`),` systemctl restart ${a}.service`,"else",...s.map((f)=>` ${f}`),"fi"]}function mt(e,t=4){let r=Math.max(1,t);return[`TS_CLOUD_CURRENT=$(readlink -f ${e.current} 2>/dev/null || true)`,`ls -1dt ${e.releases}/*/ 2>/dev/null | sed 's#/$##' | tail -n +${r+1} | while read -r TS_CLOUD_OLD; do`,' [ "$(readlink -f "$TS_CLOUD_OLD")" = "$TS_CLOUD_CURRENT" ] || rm -rf "$TS_CLOUD_OLD"',"done"]}function pc(e){return`${Ne(e)}/owner`}function En(e,t){let r=pc(e);return[`if [ -f "${r}" ]; then`,` TS_CLOUD_OWNER=$(cat "${r}")`,` if [ "$TS_CLOUD_OWNER" != "${t}" ]; then`,` echo "[ts-cloud] REFUSING deploy: ${e} belongs to project '$TS_CLOUD_OWNER', not '${t}'. Another project on this box derives the same site key — give one of them a distinct site name/domain, or remove ${r} to transfer ownership." >&2`," exit 1"," fi","else",` mkdir -p "$(dirname "${r}")"`,` printf '%s\\n' "${t}" > "${r}"`,"fi"]}function br(e,t){let r=Ne(e),n=ic(e,t.releaseId),s=oc(e),i=Math.max(1,t.keepLogs??20),a=t.commit||"",c=t.branch||"";return[`mkdir -p ${r}/deploys`,`exec > >(tee -a ${n}) 2>&1`,`echo "[ts-cloud] deploy ${t.releaseId} commit=${a} branch=${c} starting $(date -u +%Y-%m-%dT%H:%M:%SZ)"`,"ts_cloud_record_deploy() {"," TS_CLOUD_RC=$?",' if [ "$TS_CLOUD_RC" -eq 0 ]; then TS_CLOUD_ST=success; else TS_CLOUD_ST=failed; fi',` printf '%s\\t%s\\t%s\\t%s\\trc=%s\\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${t.releaseId}" "${a}" "$TS_CLOUD_ST" "$TS_CLOUD_RC" >> ${s}`,"}","trap ts_cloud_record_deploy EXIT",`ls -1t ${r}/deploys/*.log 2>/dev/null | tail -n +${i+1} | while read -r TS_CLOUD_OLDLOG; do rm -f "$TS_CLOUD_OLDLOG"; done`]}var dc=new Set(["sqlite","sqlite3"]),mc=new Set([":memory:","memory"]);function fc(e){let t=e?.DB_CONNECTION?.trim().toLowerCase();return t!=null&&dc.has(t)}function hc(e){let t=e.trim().replace(/^\.\//,"");if(t===""||t.startsWith("/")||t.startsWith("~"))return;if(t.split("/").includes(".."))return;return t.replace(/\/+$/,"")}function Rn(e,t=[]){if(!fc(e))return{};let r=e?.DB_DATABASE_PATH?.trim()||e?.DB_DATABASE?.trim();if(r&&mc.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=hc(r);if(!n)return{};if(t.some((s)=>_n(s)===n))return{};return{path:n}}function Tr(e,t=[]){let{path:r}=Rn(e,t);return r?[r]:[]}var Ur="/var/ts-cloud/artifacts",Xh="/usr/local/bin/ts-cloud-host-cleanup.sh",Yh="ts-cloud-host-cleanup";var Wg=Object.freeze({stagingMaxAgeMinutes:60,artifactMaxAgeDays:2,artifactUploadMaxAgeMinutes:60,bunCacheMaxAgeDays:7,journalMaxAgeDays:14,journalMaxSizeMb:512,containerImageMaxAgeHours:168}),Jg=Object.freeze({artifactMaxAgeDays:0,bunCacheMaxAgeDays:1,journalMaxAgeDays:3,journalMaxSizeMb:256,containerImageMaxAgeHours:24}),Zh=50,eA=85,tA="daily";function Bs(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Qe(e,t,r=0){if(!Number.isInteger(t)||t<r)throw Error(`hostCleanup: ${e} must be a whole number >= ${r} (got ${t})`)}function Ks(e,t){for(let[r,n]of Object.entries(t))Qe(`${e}.${r}`,n);Qe(`${e}.stagingMaxAgeMinutes`,t.stagingMaxAgeMinutes,15),Qe(`${e}.artifactUploadMaxAgeMinutes`,t.artifactUploadMaxAgeMinutes,15),Qe(`${e}.journalMaxSizeMb`,t.journalMaxSizeMb,1)}function gc(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(Qe(`${r}.maxAgeDays`,e.maxAgeDays),e.type!==void 0&&e.type!=="file"&&e.type!=="directory")throw Error(`${r}.type must be 'file' or 'directory'`)}function Qr(e){let t=typeof e==="object"&&e!==null?e:{},r=e!==!1,n={...Wg,...js(t.retention)};Ks("hostCleanup.retention",n);let s=null;if(t.pressure!==!1){let c=typeof t.pressure==="object"?t.pressure:{},f=c.lowWaterPercent??50,h=c.highWaterPercent??85;if(Qe("hostCleanup.pressure.lowWaterPercent",f),Qe("hostCleanup.pressure.highWaterPercent",h,1),h>100||f>=h)throw Error(`hostCleanup.pressure: need 0 <= lowWaterPercent < highWaterPercent <= 100 (got ${f} and ${h})`);let g={...Jg,...js(c.escalated)},v=Object.fromEntries(Object.keys(n).map((_)=>[_,Math.min(n[_],g[_]??n[_])]));Ks("hostCleanup.pressure.escalated",v),s={lowWaterPercent:f,highWaterPercent:h,escalated:v}}let i=t.schedule??"daily";if(!/^[\w*:,./~ -]+$/.test(i))throw Error(`hostCleanup.schedule must be a systemd OnCalendar expression (got ${JSON.stringify(i)})`);let a=t.paths??[];return a.forEach(gc),{enabled:r,timer:r&&t.timer!==!1,schedule:i,retention:n,pressure:s,paths:a}}function js(e){return Object.fromEntries(Object.entries(e??{}).filter(([,t])=>t!==void 0))}var Kt={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 On(e){return Object.keys(Kt).map((t)=>`${Kt[t]}=${e[t]}`).join(" ")}var Ze=(e)=>`"$${Kt[e]}"`;function Gs(e){return[`find /var/ts-cloud/staging -xdev -maxdepth 1 -type f -mmin +${Ze("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /tmp -xdev -maxdepth 1 -type f -name "*-release.tar.gz" -mmin +${Ze("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,"TS_CLOUD_HC_ARTIFACT_MIN=$((TS_CLOUD_HC_ARTIFACT_DAYS * 1440))",'[ "$TS_CLOUD_HC_ARTIFACT_MIN" -gt 0 ] || TS_CLOUD_HC_ARTIFACT_MIN=1','find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name "*.tar.gz" -mmin +"$TS_CLOUD_HC_ARTIFACT_MIN" -delete 2>/dev/null || true',`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name ".*.tmp" -mmin +${Ze("artifactUploadMaxAgeMinutes")} -delete 2>/dev/null || true`,...e.map(Sc),`journalctl --vacuum-time=${Ze("journalMaxAgeDays")}d --vacuum-size=${Ze("journalMaxSizeMb")}M >/dev/null 2>&1 || true`]}function Sc(e){let t=Bs(e.path.replace(/\/+$/,"")),r=Bs(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 Vs(){return[`find /root/.bun/install/cache -xdev -type f -mtime +${Ze("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`find /root/.bun/install/cache -xdev -depth -type d -empty -mtime +${Ze("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`if command -v docker >/dev/null 2>&1; then docker image prune --all --force --filter "until=\${${Kt.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,`if command -v podman >/dev/null 2>&1; then podman image prune --all --force --filter "until=\${${Kt.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 qs(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 ii(e){let{retention:t,pressure:r,paths:n}=Qr(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)"',On(t),...Gs(n),...Vs(),'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"'];let{lowWaterPercent:a,highWaterPercent:c,escalated:f}=r;return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',...qs("TS_CLOUD_HC_BEFORE"),"TS_CLOUD_HC_TIER=normal",On(t),`if [ "$TS_CLOUD_HC_BEFORE" -ge 0 ] && [ "$TS_CLOUD_HC_BEFORE" -lt ${a} ]; then`," TS_CLOUD_HC_TIER=light",` echo "[ts-cloud] host cleanup: / at \${TS_CLOUD_HC_BEFORE}% (under ${a}%), skipping cache and image pruning"`,`elif [ "$TS_CLOUD_HC_BEFORE" -ge ${c} ]; then`," TS_CLOUD_HC_TIER=pressure",` ${On(f)}`,` echo "[ts-cloud] warning: / at \${TS_CLOUD_HC_BEFORE}% (at or above ${c}%), host cleanup is using shortened retention windows" >&2`,"fi",...Gs(n),'if [ "$TS_CLOUD_HC_TIER" != light ]; then',...Vs().map((h)=>` ${h}`),"fi",'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"',...qs("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\\":${a},\\"highWaterPercent\\":${c}}"`,"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 ${c} ]; then`,` echo "[ts-cloud] warning: / still at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${c}%); 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 ${c}%)" >/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 ${c}% 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 Xg(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",...ii(e),"exit 0"]}function Dn(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 co(e){let t=Qr(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=Dn("/usr/local/bin/ts-cloud-host-cleanup.sh",Xg(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",...Dn("/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"),...Dn("/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 si(e){let t=co(e);if(!Qr(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 Js(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}=${je(String(i))}`]});return[`cd ${e}`,`if [ -d ${u} ] && command -v pantry >/dev/null 2>&1; then`,` ${Re()}`,"fi",...n,...t]}var vc=/\.(?:[cm]?[jt]sx?)$/;function oi(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="",...a]=s.split(/\s+/);if(i&&!vc.test(i)){let c=i.startsWith("/")?i:r&&i.startsWith("./")?`${r.replace(/\/+$/,"")}/${i.replace(/^\.\//,"")}`:void 0;if(c)return[c,...a].join(" ")}return`${n} ${s}`}function Ws(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.
159
170
  # An eighth of %sMB RAM. Set memoryHigh in config to override.
160
171
  [Service]
161
172
  MemoryAccounting=true
162
173
  MemoryHigh=%sM
163
- ' "$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)"',...An()]}function wr(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 An(){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 _c(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 et={path:"/",intervalSeconds:60,failuresBeforeRestart:3,timeoutSeconds:10,startupGraceSeconds:600,maxBackoffSeconds:3600,backoffBaseSeconds:300};function jt(e,t,r){return typeof e==="number"&&Number.isFinite(e)?Math.max(r,Math.floor(e)):t}function Ws(e){let{unitBase:t,unitPattern:r,port:n}=e,s=e.path&&e.path.startsWith("/")?e.path:et.path,i=jt(e.intervalSeconds,et.intervalSeconds,10),a=jt(e.failuresBeforeRestart,et.failuresBeforeRestart,1),c=jt(e.timeoutSeconds,et.timeoutSeconds,1),f=jt(e.startupGraceSeconds,et.startupGraceSeconds,0),g=jt(e.maxBackoffSeconds,et.maxBackoffSeconds,0),h=Math.min(et.backoffBaseSeconds,g),S=`/usr/local/sbin/${t}-liveness`,_=`/run/${t}-liveness`,v=`${t}-liveness`,b=`127.0.0.1:${n}${s}`,L=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 > ${S} <<'TS_CLOUD_LIVENESS_EOF'`,"#!/bin/sh",`# Liveness check for ${t}, written by ts-cloud on every deploy.`,`# Settings: interval=${i}s timeout=${c}s failures=${a} startup-grace=${f}s max-backoff=${g}s`,"set -eu",`TS_CLOUD_STATE=${_}`,'exec 9>"$TS_CLOUD_STATE.lock"',"flock -n 9 || exit 0",L,'[ -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 ${c} "http://${b}" || 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 ${g} ] || 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 ${c}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 ${f} ]; then`,' rm -f "$TS_CLOUD_STATE.fail"',` logger -t ${v} "$TS_CLOUD_UNIT is \${TS_CLOUD_AGE}s old, inside its ${f}s startup grace; not counting this failed check on ${b} ($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 ${a} ]; then`,` logger -t ${v} "check $TS_CLOUD_FAILS of ${a} failed on ${b} ($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=${h}`," TS_CLOUD_I=1",` while [ "$TS_CLOUD_I" -lt "$TS_CLOUD_STREAK" ] && [ "$TS_CLOUD_WAIT" -lt ${g} ]; do TS_CLOUD_WAIT=$((TS_CLOUD_WAIT * 2)); TS_CLOUD_I=$((TS_CLOUD_I + 1)); done`,` [ "$TS_CLOUD_WAIT" -le ${g} ] || TS_CLOUD_WAIT=${g}`," TS_CLOUD_ELAPSED=$((TS_CLOUD_NOW - TS_CLOUD_LAST))",' if [ "$TS_CLOUD_ELAPSED" -lt "$TS_CLOUD_WAIT" ]; then',` logger -t ${v} "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 ${b})"`," exit 0"," fi","fi","TS_CLOUD_STREAK=$((TS_CLOUD_STREAK + 1))",`logger -t ${v} "no HTTP response on ${b} 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 ${S}`,`rm -f ${_}.fail ${_}.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=${S}`,"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 Ys=20,xn=3,vc=5,$c=60;function yc(e){let{paths:t,unitBase:r,releaseId:n,port:s}=e,i=`${r}@${n}.service`,a=`${r}.service`,c=Math.max(1,e.healthGateSeconds??5),f=e.healthCheckPath?e.healthCheckPath.startsWith("/")?e.healthCheckPath:`/${e.healthCheckPath}`:null,g=f?`curl -sf -o /dev/null --max-time ${vc} "http://127.0.0.1:${s}${f}"`:null,h=(S)=>`{ echo "[ts-cloud] release ${n} ${S}" >&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 ${a} 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/${a} ] && systemctl is-active --quiet ${a}; then echo "[ts-cloud] retiring pre-zero-downtime unit ${a} (one-time restart cutover)"; systemctl stop ${a}; 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 ${c}); 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 ${c}); do sleep 1; systemctl is-active --quiet ${i} || ${h("failed its health gate on the free port")}; done`," else",` ${h("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 ${$c}); 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 ${xn}`," done",` [ "$TS_CLOUD_BOUND" -eq 1 ] || ${h(`never took :${s}`)}`,"else",' echo "[ts-cloud] cannot read listener PIDs on this host — the response probes run without the bind check" >&2',"fi",...g?[`TS_CLOUD_HEALTHY=0; for TS_CLOUD_I in $(seq 1 ${Ys}); do if ${g}; then TS_CLOUD_HEALTHY=1; break; fi; sleep ${xn}; done`,`[ "$TS_CLOUD_HEALTHY" -eq 1 ] || ${h(`never answered ${f}`)}`]:[],...dt(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 ${c}); 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 ${c}); 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 ] || ${h(`is active but never bound :${s}`)}`,...g?[`TS_CLOUD_HEALTHY=0; for TS_CLOUD_I in $(seq 1 ${Ys}); do if ${g}; then TS_CLOUD_HEALTHY=1; break; fi; sleep ${xn}; done`,`[ "$TS_CLOUD_HEALTHY" -eq 1 ] || ${h(`stopped answering ${f} once it served alone`)}`]:[],...Pn(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/${a} ]; then systemctl disable ${a} 2>/dev/null || true; rm -f /etc/systemd/system/${a}; systemctl daemon-reload; fi`]}function Js(e,t){if(!t.length)return[];let r=je(e.release);return[`if [ -e ${r} ] || [ -L ${r} ]; then`,...t,"fi"]}function ai(e){let{siteName:t,slug:r,artifactFetch:n,releaseId:s,execStart:i,envEntries:a,port:c,keepReleases:f=Mt,preStartCommands:g=[],postActivateCommands:h=[],beforeResetCommands:S=[],healthCheckPath:_,healthGateSeconds:v=5,liveness:b,memoryHigh:T="auto",memoryMax:L,cpuWeight:K,ioWeight:M,tasksMax:D,stopTimeout:N}=e,k=[...K!=null?[`CPUWeight=${K}`]:[],...M!=null?[`IOWeight=${M}`]:[],...D!=null?[`TasksMax=${D}`]:[],...N?[`TimeoutStopSec=${N}`]:[]],j=e.zeroDowntime??c!=null,q=e.appDir??`/var/www/${t}`,B=er(q,s),re=`${r}-${t}`,ee=`${re}.service`,ne=di(r,t,s),oe=e.sharedPaths??[],ie=Ns([".env",...oe,...Tr(a,oe)]),se=hr(a),de=Xs(B.release,g,a),me=["set -euo pipefail",...yn(B),...n,...Ft(B,ie),...Js(B,S),Tn(B),...bn(B),`tar xzf ${ne} --no-same-owner -C "$TS_CLOUD_STAGED"`,`rm -f ${ne}`,`cat > ${B.shared}/.env <<'TS_CLOUD_ENV_EOF'`,se,"TS_CLOUD_ENV_EOF",`chmod 600 ${B.shared}/.env`,...wn(B),`find ${B.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' ${B.shared}/.env 2>/dev/null || true`,...yr(B,ie),...de,...e.isolation?Us(e.slug||t,B.base,ie.flatMap((X)=>typeof X==="string"||!X.target?[]:[X.target])):[]];if(j&&c!=null){let X=`${re}@${s}.service`,P=_?_.startsWith("/")?_:`/${_}`:null;return[...me,`cat > /etc/systemd/system/${re}@.service <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=${t} release %i (managed by ts-cloud)`,"After=network.target","","[Service]","Type=simple",...e.isolation?Ht(pt(e.slug||t),B.base,e.slug||t):[],`WorkingDirectory=${B.releases}/%i`,`ExecStart=${i}`,"Restart=always","RestartSec=5","MemoryAccounting=true",...T&&T!=="auto"?[`MemoryHigh=${T}`]:[],...L?[`MemoryMax=${L}`]:[],...k,`EnvironmentFile=${B.releases}/%i/.env`,`Environment=PORT=${c}`,`Environment=STX_IMAGE_CACHE_DIR=${vr(B)}`,`Environment=STX_BUNDLE_CACHE_DIR=${zt(B)}`,`Environment=STX_BUNDLE_CACHE_DIR=${zt(B)}`,"","[Install]","WantedBy=multi-user.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",...T==="auto"?qs(`${re}@.service`):[...wr(`${re}@.service`),...An()],..._c(X),...yc({paths:B,unitBase:re,releaseId:s,port:c,healthCheckPath:P,healthGateSeconds:v}),...h,...mt(B,f),...b===!1?[]:Ws({...b,unitBase:re,unitPattern:`${re}@*.service`,port:c,path:b?.path??_})]}return[...me,`cat > /etc/systemd/system/${ee} <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=${t} (managed by ts-cloud)`,"After=network.target","","[Service]","Type=simple",...e.isolation?Ht(pt(e.slug||t),B.base,e.slug||t):[],`WorkingDirectory=${B.current}`,`ExecStart=${i}`,"Restart=always","RestartSec=5","MemoryAccounting=true",...T&&T!=="auto"?[`MemoryHigh=${T}`]:[],...L?[`MemoryMax=${L}`]:[],...k,`EnvironmentFile=${B.current}/.env`,...c?[`Environment=PORT=${c}`]:[],`Environment=STX_IMAGE_CACHE_DIR=${vr(B)}`,`Environment=STX_BUNDLE_CACHE_DIR=${zt(B)}`,"","[Install]","WantedBy=multi-user.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",...wr(ee,{auto:T!=="auto",high:T!=="auto",max:Boolean(L)}),...T==="auto"?qs(ee):An(),`systemctl enable ${ee}`,`TS_CLOUD_PREV_CURRENT="$(readlink ${B.current} 2>/dev/null || true)"`,...dt(B),`systemctl restart ${ee}`,`for TS_CLOUD_I in $(seq 1 ${Math.max(1,v)}); do sleep 1; systemctl is-active --quiet ${ee} || { echo "[ts-cloud] release ${s} of ${ee} did not stay up — restoring the previous release" >&2; journalctl -u ${ee} -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 ${B.release})" ]; then ln -sfn "$TS_CLOUD_PREV_CURRENT" ${B.current}.tmp && mv -Tf ${B.current}.tmp ${B.current}; systemctl restart ${ee} || true; fi; exit 1; }; done`,...Pn(B),...h,...mt(B,f),...b===!1||!c?[]:Ws({...b,unitBase:re,unitPattern:ee,port:c,path:b?.path??_})]}function ci(e){let{siteName:t,artifactFetch:r,releaseId:n,keepReleases:s=Mt,preStartCommands:i=[],postActivateCommands:a=[],beforeResetCommands:c=[]}=e,f=e.appDir??`/var/www/${t}`,g=er(f,n),h=di(e.slug,t,n),S=Xs(g.release,i);return["set -euo pipefail",...yn(g),...r,...Ft(g,[]),...Js(g,c),Tn(g),...bn(g),`tar xzf ${h} --no-same-owner -C "$TS_CLOUD_STAGED"`,...wn(g),`rm -f ${h}`,...S,...dt(g),...a,...mt(g,s)]}function di(e,t,r){return`/tmp/${[e,t,r].filter(Boolean).join("-")}-release.tar.gz`}function zc(e,t,r,n){return[`aws s3 cp "s3://${e}/${t}" ${n} --region ${r}`]}function Kc(e,t){return[`mv "${e}" ${t}`]}function je(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Cr(e,t,r){return["set -euo pipefail",`chmod 600 ${je(e)}`,`mv -f -- ${je(e)} ${je(t)}`,`cp -- ${je(t)} ${je(r)}`].join(`
164
- `)}function Pr(e){return`rm -f -- ${je(e)} 2>/dev/null || true`}function In(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 Zs(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 Ln(e){let t=e.infrastructure?.compute??{},r=p(e);return[...cr(t.managedServices??{mysql:!0,redis:!0},{bindPrivate:!0}),...ct(r,t.managedServices??{mysql:!0}),...ze(!0),...He(!0),...lt(t.sshKeys),...qe(e.notifications),...t.backups?.enabled?[...Ut({database:r,backups:t.backups})]:[]]}var Gt=["-S","ssh"];function li(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 Hr(e,t,r={}){let n=Bun.spawn(["ssh",...li(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 ve(e,t,r={}){let n=await Hr(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 Wc(e,t,r,n={}){let s=Bun.spawn(["scp",...Gt,...li(n,"scp"),...t,`${n.user??"root"}@${e}:${r}`],{stdout:"pipe",stderr:"pipe"}),i=await s.exited;if(i!==0){let a=await new Response(s.stderr).text();throw Error(`scp to ${e}:${r} failed (${i}): ${a.trim()}`)}}async function St(e,t={}){let r=t.timeoutMs??180000,n=t.pollIntervalMs??5000,s=Date.now();while(Date.now()-s<r){let i=await Hr(e,"echo ready",t);if(i.code===0&&i.stdout.includes("ready"))return;await new Promise((a)=>setTimeout(a,n))}throw Error(`SSH not ready on ${e} after ${r}ms`)}async function Jc(e,t={}){let r=t.timeoutMs??300000,n=t.pollIntervalMs??5000,s=Date.now();while(Date.now()-s<r){let i=await Hr(e,"cloud-init status 2>/dev/null || echo unknown",t);if(i.stdout.includes("status: done"))return;if(i.stdout.includes("status: error"))throw Error(`cloud-init reported an error on ${e}; check /var/log/cloud-init-output.log`);await new Promise((a)=>setTimeout(a,n))}throw Error(`cloud-init did not finish on ${e} after ${r}ms`)}function ui(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(`
165
- `)}var Xc=ui();import{homedir as bc}from"node:os";import{join as Tc}from"node:path";var Er={location:"fsn1",image:"ubuntu-24.04",sshUser:"root",sshPrivateKeyPath:"~/.ssh/id_ed25519"};function Rr(e){return e.startsWith("~/")?Tc(bc(),e.slice(2)):e}function ft(...e){for(let t of e){let r=process.env[t]?.trim();if(r)return r}return}function gt(...e){for(let t of e){let r=t?.trim();if(r)return r}return}function Vt(e,t){return gt(e,t?.hetzner?.apiToken,ft("HCLOUD_TOKEN","HETZNER_API_TOKEN"))}function Yc(e,t){return gt(t,e?.hetzner?.location,ft("HCLOUD_LOCATION","HETZNER_LOCATION"))??Er.location}function qt(e,t){let r=e?.infrastructure?.compute;return gt(t,r?.image,e?.hetzner?.image,ft("HCLOUD_IMAGE","HETZNER_IMAGE"))??Er.image}function wc(e,t){return gt(t,e?.hetzner?.sshUser,ft("HCLOUD_SSH_USER","HETZNER_SSH_USER"))??Er.sshUser}function Qs(e,t){return Rr(gt(t,e?.hetzner?.sshPrivateKeyPath,ft("HCLOUD_SSH_KEY","HETZNER_SSH_KEY"))??Er.sshPrivateKeyPath)}function Cc(e,t,r){let n=gt(t,e?.hetzner?.sshPublicKeyPath,ft("HCLOUD_SSH_PUBLIC_KEY","HETZNER_SSH_PUBLIC_KEY"));if(n)return Rr(n);return`${r??Qs(e)}.pub`}function eo(e,t={}){let r=Qs(e,t.sshPrivateKeyPath);return{apiToken:Vt(t.apiToken,e),location:Yc(e,t.location),image:qt(e,t.image),sshUser:wc(e,t.sshUser),sshPrivateKeyPath:r,sshPublicKeyPath:Cc(e,t.sshPublicKeyPath,r)}}var Pc="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 Dr extends Error{action;constructor(e,t){super(e);this.action=t;this.name="HetznerActionError"}}function to(e){return e.map((t)=>({type:"server",server:{id:t.server}}))}class Gr{name="hetzner";apiToken;baseUrl;fetchImpl;constructor(e){this.apiToken=e.apiToken,this.baseUrl=e.baseUrl??Pc,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 a=s.trim().slice(0,200)||n.statusText||"Hetzner API error";throw Error(`Hetzner API ${e} ${t} (${n.status}): ${a}`)}throw Error(`Hetzner API ${e} ${t}: unexpected non-JSON response`)}if(!n.ok){let a=i.error?.message||n.statusText||"Hetzner API error",c=i.error?.code?` [${i.error.code}]`:"";throw new Or(`Hetzner API ${e} ${t} (${n.status})${c}: ${a}`,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 a=i.meta?.pagination?.next_page;if(!a)return r;n=a}}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 changeServerReverseDns(e,t,r){return(await this.request("POST",`/servers/${e}/actions/change_dns_ptr`,{ip:t,dns_ptr:r})).action}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?to(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:to(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((c)=>setTimeout(c,s))}let a=await this.powerOffServer(e);await this.waitForAction(a.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 Dr(i.error?.message||"Hetzner action failed",i);await new Promise((a)=>setTimeout(a,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((a)=>setTimeout(a,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 a=await this.getServer(e);if(a.status===t)return a;await new Promise((c)=>setTimeout(c,n))}throw Error(`Timed out waiting for server ${e} to reach ${t} state`)}}function Zc(e,t){let r=Vt(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 We(e){let[t,r]=e.trim().split(/\s+/);return r?`${t} ${r}`:t}function ro(e,t){if(!t)return[];if(t.length>253||!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]*$/i.test(t))throw Error("Reverse DNS requires a valid fully qualified hostname");let r=[],{ipv4:n,ipv6:s}=e.public_net;if(n?.ip&&n.dns_ptr!==t)r.push({ip:n.ip,hostname:t});let i=ae(s?.ip);if(i&&!s?.dns_ptr?.some((a)=>a.ip===i&&a.dns_ptr===t))r.push({ip:i,hostname:t});return r}function no(e){if(!e)return"";return e.endsWith(`
174
+ ' "$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)"',...An()]}function wr(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 An(){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 yc(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 et={path:"/",intervalSeconds:60,failuresBeforeRestart:3,timeoutSeconds:10,startupGraceSeconds:600,maxBackoffSeconds:3600,backoffBaseSeconds:300};function jt(e,t,r){return typeof e==="number"&&Number.isFinite(e)?Math.max(r,Math.floor(e)):t}function Ys(e){let{unitBase:t,unitPattern:r,port:n}=e,s=e.path&&e.path.startsWith("/")?e.path:et.path,i=jt(e.intervalSeconds,et.intervalSeconds,10),a=jt(e.failuresBeforeRestart,et.failuresBeforeRestart,1),c=jt(e.timeoutSeconds,et.timeoutSeconds,1),f=jt(e.startupGraceSeconds,et.startupGraceSeconds,0),h=jt(e.maxBackoffSeconds,et.maxBackoffSeconds,0),g=Math.min(et.backoffBaseSeconds,h),v=`/usr/local/sbin/${t}-liveness`,_=`/run/${t}-liveness`,b=`${t}-liveness`,S=`127.0.0.1:${n}${s}`,M=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 > ${v} <<'TS_CLOUD_LIVENESS_EOF'`,"#!/bin/sh",`# Liveness check for ${t}, written by ts-cloud on every deploy.`,`# Settings: interval=${i}s timeout=${c}s failures=${a} startup-grace=${f}s max-backoff=${h}s`,"set -eu",`TS_CLOUD_STATE=${_}`,'exec 9>"$TS_CLOUD_STATE.lock"',"flock -n 9 || exit 0",M,'[ -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 ${c} "http://${S}" || 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 ${h} ] || 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 ${c}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 ${f} ]; then`,' rm -f "$TS_CLOUD_STATE.fail"',` logger -t ${b} "$TS_CLOUD_UNIT is \${TS_CLOUD_AGE}s old, inside its ${f}s startup grace; not counting this failed check on ${S} ($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 ${a} ]; then`,` logger -t ${b} "check $TS_CLOUD_FAILS of ${a} failed on ${S} ($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=${g}`," TS_CLOUD_I=1",` while [ "$TS_CLOUD_I" -lt "$TS_CLOUD_STREAK" ] && [ "$TS_CLOUD_WAIT" -lt ${h} ]; do TS_CLOUD_WAIT=$((TS_CLOUD_WAIT * 2)); TS_CLOUD_I=$((TS_CLOUD_I + 1)); done`,` [ "$TS_CLOUD_WAIT" -le ${h} ] || TS_CLOUD_WAIT=${h}`," TS_CLOUD_ELAPSED=$((TS_CLOUD_NOW - TS_CLOUD_LAST))",' if [ "$TS_CLOUD_ELAPSED" -lt "$TS_CLOUD_WAIT" ]; then',` logger -t ${b} "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 ${S})"`," exit 0"," fi","fi","TS_CLOUD_STREAK=$((TS_CLOUD_STREAK + 1))",`logger -t ${b} "no HTTP response on ${S} 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 ${v}`,`rm -f ${_}.fail ${_}.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=${v}`,"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 Xs=20,xn=3,_c=5,$c=60;function bc(e){let{paths:t,unitBase:r,releaseId:n,port:s}=e,i=`${r}@${n}.service`,a=`${r}.service`,c=Math.max(1,e.healthGateSeconds??5),f=e.healthCheckPath?e.healthCheckPath.startsWith("/")?e.healthCheckPath:`/${e.healthCheckPath}`:null,h=f?`curl -sf -o /dev/null --max-time ${_c} "http://127.0.0.1:${s}${f}"`:null,g=(v)=>`{ echo "[ts-cloud] release ${n} ${v}" >&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 ${a} 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/${a} ] && systemctl is-active --quiet ${a}; then echo "[ts-cloud] retiring pre-zero-downtime unit ${a} (one-time restart cutover)"; systemctl stop ${a}; 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 ${c}); 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 ${c}); do sleep 1; systemctl is-active --quiet ${i} || ${g("failed its health gate on the free port")}; done`," else",` ${g("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 ${$c}); 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 ${xn}`," done",` [ "$TS_CLOUD_BOUND" -eq 1 ] || ${g(`never took :${s}`)}`,"else",' echo "[ts-cloud] cannot read listener PIDs on this host — the response probes run without the bind check" >&2',"fi",...h?[`TS_CLOUD_HEALTHY=0; for TS_CLOUD_I in $(seq 1 ${Xs}); do if ${h}; then TS_CLOUD_HEALTHY=1; break; fi; sleep ${xn}; done`,`[ "$TS_CLOUD_HEALTHY" -eq 1 ] || ${g(`never answered ${f}`)}`]:[],...dt(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 ${c}); 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 ${c}); 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 ] || ${g(`is active but never bound :${s}`)}`,...h?[`TS_CLOUD_HEALTHY=0; for TS_CLOUD_I in $(seq 1 ${Xs}); do if ${h}; then TS_CLOUD_HEALTHY=1; break; fi; sleep ${xn}; done`,`[ "$TS_CLOUD_HEALTHY" -eq 1 ] || ${g(`stopped answering ${f} once it served alone`)}`]:[],...Pn(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/${a} ]; then systemctl disable ${a} 2>/dev/null || true; rm -f /etc/systemd/system/${a}; systemctl daemon-reload; fi`]}function Zs(e,t){if(!t.length)return[];let r=je(e.release);return[`if [ -e ${r} ] || [ -L ${r} ]; then`,...t,"fi"]}function ai(e){let{siteName:t,slug:r,artifactFetch:n,releaseId:s,execStart:i,envEntries:a,port:c,keepReleases:f=Mt,preStartCommands:h=[],postActivateCommands:g=[],beforeResetCommands:v=[],healthCheckPath:_,healthGateSeconds:b=5,liveness:S,memoryHigh:P="auto",memoryMax:M,cpuWeight:q,ioWeight:G,tasksMax:L,stopTimeout:j}=e,B=[...q!=null?[`CPUWeight=${q}`]:[],...G!=null?[`IOWeight=${G}`]:[],...L!=null?[`TasksMax=${L}`]:[],...j?[`TimeoutStopSec=${j}`]:[]],Z=e.zeroDowntime??c!=null,ee=e.appDir??`/var/www/${t}`,Y=er(ee,s),se=`${r}-${t}`,re=`${se}.service`,oe=di(r,t,s),me=e.sharedPaths??[],le=Ms([".env",...me,...Tr(a,me)]),ie=gr(a),E=Js(Y.release,h,a),X=["set -euo pipefail",...$n(Y),...n,...zt(Y,le),...Zs(Y,v),Tn(Y),...bn(Y),`tar xzf ${oe} --no-same-owner -C "$TS_CLOUD_STAGED"`,`rm -f ${oe}`,`cat > ${Y.shared}/.env <<'TS_CLOUD_ENV_EOF'`,ie,"TS_CLOUD_ENV_EOF",`chmod 600 ${Y.shared}/.env`,...wn(Y),`find ${Y.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' ${Y.shared}/.env 2>/dev/null || true`,...yr(Y,le),...E,...e.isolation?ks(e.slug||t,Y.base,le.flatMap((A)=>typeof A==="string"||!A.target?[]:[A.target])):[]];if(Z&&c!=null){let A=`${se}@${s}.service`,T=_?_.startsWith("/")?_:`/${_}`:null;return[...X,`cat > /etc/systemd/system/${se}@.service <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=${t} release %i (managed by ts-cloud)`,"After=network.target","","[Service]","Type=simple",...e.isolation?Ht(pt(e.slug||t),Y.base,e.slug||t):[],`WorkingDirectory=${Y.releases}/%i`,`ExecStart=${i}`,"Restart=always","RestartSec=5","MemoryAccounting=true",...P&&P!=="auto"?[`MemoryHigh=${P}`]:[],...M?[`MemoryMax=${M}`]:[],...B,`EnvironmentFile=${Y.releases}/%i/.env`,`Environment=PORT=${c}`,`Environment=STX_IMAGE_CACHE_DIR=${vr(Y)}`,`Environment=STX_BUNDLE_CACHE_DIR=${Ft(Y)}`,`Environment=STX_BUNDLE_CACHE_DIR=${Ft(Y)}`,"","[Install]","WantedBy=multi-user.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",...P==="auto"?Ws(`${se}@.service`):[...wr(`${se}@.service`),...An()],...yc(A),...bc({paths:Y,unitBase:se,releaseId:s,port:c,healthCheckPath:T,healthGateSeconds:b}),...g,...mt(Y,f),...S===!1?[]:Ys({...S,unitBase:se,unitPattern:`${se}@*.service`,port:c,path:S?.path??_})]}return[...X,`cat > /etc/systemd/system/${re} <<'TS_CLOUD_UNIT_EOF'`,"[Unit]",`Description=${t} (managed by ts-cloud)`,"After=network.target","","[Service]","Type=simple",...e.isolation?Ht(pt(e.slug||t),Y.base,e.slug||t):[],`WorkingDirectory=${Y.current}`,`ExecStart=${i}`,"Restart=always","RestartSec=5","MemoryAccounting=true",...P&&P!=="auto"?[`MemoryHigh=${P}`]:[],...M?[`MemoryMax=${M}`]:[],...B,`EnvironmentFile=${Y.current}/.env`,...c?[`Environment=PORT=${c}`]:[],`Environment=STX_IMAGE_CACHE_DIR=${vr(Y)}`,`Environment=STX_BUNDLE_CACHE_DIR=${Ft(Y)}`,"","[Install]","WantedBy=multi-user.target","TS_CLOUD_UNIT_EOF","systemctl daemon-reload",...wr(re,{auto:P!=="auto",high:P!=="auto",max:Boolean(M)}),...P==="auto"?Ws(re):An(),`systemctl enable ${re}`,`TS_CLOUD_PREV_CURRENT="$(readlink ${Y.current} 2>/dev/null || true)"`,...dt(Y),`systemctl restart ${re}`,`for TS_CLOUD_I in $(seq 1 ${Math.max(1,b)}); do sleep 1; systemctl is-active --quiet ${re} || { echo "[ts-cloud] release ${s} of ${re} did not stay up — restoring the previous release" >&2; journalctl -u ${re} -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 ${Y.release})" ]; then ln -sfn "$TS_CLOUD_PREV_CURRENT" ${Y.current}.tmp && mv -Tf ${Y.current}.tmp ${Y.current}; systemctl restart ${re} || true; fi; exit 1; }; done`,...Pn(Y),...g,...mt(Y,f),...S===!1||!c?[]:Ys({...S,unitBase:se,unitPattern:re,port:c,path:S?.path??_})]}function ci(e){let{siteName:t,artifactFetch:r,releaseId:n,keepReleases:s=Mt,preStartCommands:i=[],postActivateCommands:a=[],beforeResetCommands:c=[]}=e,f=e.appDir??`/var/www/${t}`,h=er(f,n),g=di(e.slug,t,n),v=Js(h.release,i);return["set -euo pipefail",...$n(h),...r,...zt(h,[]),...Zs(h,c),Tn(h),...bn(h),`tar xzf ${g} --no-same-owner -C "$TS_CLOUD_STAGED"`,...wn(h),`rm -f ${g}`,...v,...dt(h),...a,...mt(h,s)]}function di(e,t,r){return`/tmp/${[e,t,r].filter(Boolean).join("-")}-release.tar.gz`}function zc(e,t,r,n){return[`aws s3 cp "s3://${e}/${t}" ${n} --region ${r}`]}function Kc(e,t){return[`mv "${e}" ${t}`]}function je(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Cr(e,t,r){return["set -euo pipefail",`chmod 600 ${je(e)}`,`mv -f -- ${je(e)} ${je(t)}`,`cp -- ${je(t)} ${je(r)}`].join(`
175
+ `)}function Pr(e){return`rm -f -- ${je(e)} 2>/dev/null || true`}function In(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 Qs(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 Ln(e){let t=e.infrastructure?.compute??{},r=p(e);return[...cr(t.managedServices??{mysql:!0,redis:!0},{bindPrivate:!0}),...ct(r,t.managedServices??{mysql:!0}),...Fe(!0),...He(!0),...lt(t.sshKeys),...qe(e.notifications),...t.backups?.enabled?[...Ut({database:r,backups:t.backups})]:[]]}var Gt=["-S","ssh"];function li(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 Hr(e,t,r={}){let n=Bun.spawn(["ssh",...li(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 ve(e,t,r={}){let n=await Hr(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 Wc(e,t,r,n={}){let s=Bun.spawn(["scp",...Gt,...li(n,"scp"),...t,`${n.user??"root"}@${e}:${r}`],{stdout:"pipe",stderr:"pipe"}),i=await s.exited;if(i!==0){let a=await new Response(s.stderr).text();throw Error(`scp to ${e}:${r} failed (${i}): ${a.trim()}`)}}async function St(e,t={}){let r=t.timeoutMs??180000,n=t.pollIntervalMs??5000,s=Date.now();while(Date.now()-s<r){let i=await Hr(e,"echo ready",t);if(i.code===0&&i.stdout.includes("ready"))return;await new Promise((a)=>setTimeout(a,n))}throw Error(`SSH not ready on ${e} after ${r}ms`)}async function Jc(e,t={}){let r=t.timeoutMs??300000,n=t.pollIntervalMs??5000,s=Date.now();while(Date.now()-s<r){let i=await Hr(e,"cloud-init status 2>/dev/null || echo unknown",t);if(i.stdout.includes("status: done"))return;if(i.stdout.includes("status: error"))throw Error(`cloud-init reported an error on ${e}; check /var/log/cloud-init-output.log`);await new Promise((a)=>setTimeout(a,n))}throw Error(`cloud-init did not finish on ${e} after ${r}ms`)}function ui(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(`
176
+ `)}var Xc=ui();import{homedir as Tc}from"node:os";import{join as wc}from"node:path";var Er={location:"fsn1",image:"ubuntu-24.04",sshUser:"root",sshPrivateKeyPath:"~/.ssh/id_ed25519"};function Rr(e){return e.startsWith("~/")?wc(Tc(),e.slice(2)):e}function ft(...e){for(let t of e){let r=process.env[t]?.trim();if(r)return r}return}function ht(...e){for(let t of e){let r=t?.trim();if(r)return r}return}function Vt(e,t){return ht(e,t?.hetzner?.apiToken,ft("HCLOUD_TOKEN","HETZNER_API_TOKEN"))}function Yc(e,t){return ht(t,e?.hetzner?.location,ft("HCLOUD_LOCATION","HETZNER_LOCATION"))??Er.location}function qt(e,t){let r=e?.infrastructure?.compute;return ht(t,r?.image,e?.hetzner?.image,ft("HCLOUD_IMAGE","HETZNER_IMAGE"))??Er.image}function Cc(e,t){return ht(t,e?.hetzner?.sshUser,ft("HCLOUD_SSH_USER","HETZNER_SSH_USER"))??Er.sshUser}function eo(e,t){return Rr(ht(t,e?.hetzner?.sshPrivateKeyPath,ft("HCLOUD_SSH_KEY","HETZNER_SSH_KEY"))??Er.sshPrivateKeyPath)}function Pc(e,t,r){let n=ht(t,e?.hetzner?.sshPublicKeyPath,ft("HCLOUD_SSH_PUBLIC_KEY","HETZNER_SSH_PUBLIC_KEY"));if(n)return Rr(n);return`${r??eo(e)}.pub`}function to(e,t={}){let r=eo(e,t.sshPrivateKeyPath);return{apiToken:Vt(t.apiToken,e),location:Yc(e,t.location),image:qt(e,t.image),sshUser:Cc(e,t.sshUser),sshPrivateKeyPath:r,sshPublicKeyPath:Pc(e,t.sshPublicKeyPath,r)}}var Ec="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 Dr extends Error{action;constructor(e,t){super(e);this.action=t;this.name="HetznerActionError"}}function ro(e){return e.map((t)=>({type:"server",server:{id:t.server}}))}class Gr{name="hetzner";apiToken;baseUrl;fetchImpl;constructor(e){this.apiToken=e.apiToken,this.baseUrl=e.baseUrl??Ec,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 a=s.trim().slice(0,200)||n.statusText||"Hetzner API error";throw Error(`Hetzner API ${e} ${t} (${n.status}): ${a}`)}throw Error(`Hetzner API ${e} ${t}: unexpected non-JSON response`)}if(!n.ok){let a=i.error?.message||n.statusText||"Hetzner API error",c=i.error?.code?` [${i.error.code}]`:"";throw new Or(`Hetzner API ${e} ${t} (${n.status})${c}: ${a}`,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 a=i.meta?.pagination?.next_page;if(!a)return r;n=a}}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 changeServerReverseDns(e,t,r){return(await this.request("POST",`/servers/${e}/actions/change_dns_ptr`,{ip:t,dns_ptr:r})).action}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?ro(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:ro(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((c)=>setTimeout(c,s))}let a=await this.powerOffServer(e);await this.waitForAction(a.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 Dr(i.error?.message||"Hetzner action failed",i);await new Promise((a)=>setTimeout(a,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((a)=>setTimeout(a,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 a=await this.getServer(e);if(a.status===t)return a;await new Promise((c)=>setTimeout(c,n))}throw Error(`Timed out waiting for server ${e} to reach ${t} state`)}}function Zc(e,t){let r=Vt(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 We(e){let[t,r]=e.trim().split(/\s+/);return r?`${t} ${r}`:t}function no(e,t){if(!t)return[];if(t.length>253||!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]*$/i.test(t))throw Error("Reverse DNS requires a valid fully qualified hostname");let r=[],{ipv4:n,ipv6:s}=e.public_net;if(n?.ip&&n.dns_ptr!==t)r.push({ip:n.ip,hostname:t});let i=ae(s?.ip);if(i&&!s?.dns_ptr?.some((a)=>a.ip===i&&a.dns_ptr===t))r.push({ip:i,hostname:t});return r}function so(e){if(!e)return"";return e.endsWith(`
166
177
  `)?e:`${e}
167
178
  `}function ce(e,t={}){let r=t.scriptPath??"/var/lib/cloud/ts-cloud-bootstrap.sh",n=t.permissions??"0755",s=e.split(`
168
179
  `).map((i)=>` ${i}`).join(`
169
180
  `);return`#cloud-config
170
- ${no(t.before)}write_files:
181
+ ${so(t.before)}write_files:
171
182
  - path: ${r}
172
183
  permissions: '${n}'
173
184
  owner: root:root
@@ -175,7 +186,7 @@ ${no(t.before)}write_files:
175
186
  ${s}
176
187
  runcmd:
177
188
  - [ bash, ${r} ]
178
- ${no(t.after)}`}function xr(e){let t=new Set([80,443,...e.sitePorts,...e.allowedPorts??[]]);if(e.allowSsh)t.add(22);return[...t].map((r)=>({direction:"in",protocol:"tcp",port:String(r),source_ips:["0.0.0.0/0","::/0"],description:`ts-cloud port ${r}`}))}var Un={micro:"cpx11",small:"cx23",medium:"cx33",large:"cx43",xlarge:"cx53","2xlarge":"ccx33"};function Ge(e){if(!e)return Un.micro;if(e in Un)return Un[e];return e}var Se="ts-cloud";function $e(e,t,r="app"){return{[`${Se}/project`]:e,[`${Se}/environment`]:t,[`${Se}/role`]:r,[`${Se}/managed-by`]:"ts-cloud"}}function we(e,t,r,n="app"){return kn(e,t,r)&&e[`${Se}/role`]===n}function kn(e,t,r){if(!e)return!1;return e[`${Se}/project`]===t&&e[`${Se}/environment`]===r}function Ec(e){return e.location?.name??e.datacenter?.location.name??"unknown"}function ht(e){return typeof e==="number"&&Number.isFinite(e)?e:0}function Rc(e,t){if(e.architecture&&t.architecture&&e.architecture!==t.architecture)throw Error(`Cannot resize ${e.name} (${e.architecture}) to ${t.name} (${t.architecture}) in place.`);let r=[];if(ht(t.cores)<ht(e.cores))r.push("CPU");if(ht(t.memory)<ht(e.memory))r.push("memory");if(ht(t.disk)<ht(e.disk))r.push("disk");if(r.length>0)throw Error(`Refusing an in-place downgrade from ${e.name} to ${t.name}: ${r.join(", ")} would shrink.`)}async function tr(e,t,r,n=!0){let[s,i]=await Promise.all([e.getServer(t),e.getServerType(r)]);if(!i)throw Error(`Hetzner server type ${r} was not found.`);let a=s.server_type,c=a.name===i.name;if(!c)Rc(a,i);let f=Ec(s),g=i.locations?.find((h)=>h.name===f);return{serverId:t,serverName:s.name,location:f,currentType:a,targetType:i,capacityAvailable:g?.available!==!1,alreadyComplete:c,upgradeDisk:n}}function Yg(e){if(e instanceof Or&&e.code==="resource_unavailable")return!0;if(e instanceof Dr){if(e.action.error?.code==="resource_unavailable")return!0;if(/no fitting vhost|placement|free resources/i.test(e.action.error?.message??""))return!0}return/resource_unavailable|no fitting vhost|error during placement|free resources/i.test(e instanceof Error?e.message:String(e))}async function Me(e,t,r){await e?.onPhase?.(t,r)}async function Hn(e,t,r){await e.waitForAction(t.id,{pollIntervalMs:r.actionPollIntervalMs,maxWaitMs:r.actionTimeoutMs})}var Ar=async({client:e,hooks:t,actionPollIntervalMs:r,actionTimeoutMs:n,serverPollIntervalMs:s,serverTimeoutMs:i},a,c,f)=>{let g=await e.getServer(a.serverId);if(g.status!=="running"){await Me(t,f?"recovering":"powering-on",{serverId:a.serverId});let S=await e.powerOnServer(a.serverId);await Hn(e,S,{actionPollIntervalMs:r,actionTimeoutMs:n}),g=await e.waitForServerRunning(a.serverId,{pollIntervalMs:s,maxWaitMs:i})}await Me(t,"verifying",{serverId:a.serverId,recovered:f});let h=await t?.afterBoot?.(g,a.targetType,c,{recovered:f});return{server:g,verification:h}};async function gi(e){let{client:t,hooks:r}=e,n=e.upgradeDisk??!0;await Me(r,"planning",{serverId:e.serverId,targetType:e.targetType});let s=await tr(t,e.serverId,e.targetType,n),i=await t.getServer(s.serverId);if(s.alreadyComplete){let f=await Ar(e,s,e.manifest,!1);return await Me(r,"complete",{alreadyComplete:!0}),{status:"already-complete",plan:s,verification:f.verification}}if(!s.capacityAvailable&&!e.attemptUnavailable)return await Me(r,"waiting-capacity",{location:s.location,targetType:s.targetType.name}),{status:"waiting-capacity",plan:s};let a=e.manifest;if(i.status==="off"&&!e.resumeFromOff){let f=await Ar(e,s,void 0,!0);return{status:"recovered",plan:s,server:f.server,verification:f.verification,retryable:!0,error:"Server was already off without a resumable resize checkpoint; powered it back on."}}if(i.status==="running"){await Me(r,"preflight",{serverId:s.serverId}),a=await r?.preflight?.(i,s.targetType),await Me(r,"shutting-down",{serverId:s.serverId});let f=await t.shutdownServer(s.serverId);await Hn(t,f,e),i=await t.waitForServerStatus(s.serverId,"off",{pollIntervalMs:e.serverPollIntervalMs,maxWaitMs:e.serverTimeoutMs})}try{await Me(r,"resizing",{serverId:s.serverId,from:s.currentType.name,to:s.targetType.name,upgradeDisk:n});let f=await t.changeServerType(s.serverId,s.targetType.name,n);await Hn(t,f,e)}catch(f){let g=await Ar(e,s,a,!0);return{status:"recovered",plan:s,server:g.server,manifest:a,verification:g.verification,retryable:Yg(f),error:f instanceof Error?f.message:String(f)}}let c=await Ar(e,s,a,!1);if(c.verification&&!c.verification.ok)throw Error(`Resize completed but verification failed: ${(c.verification.failures??[]).join("; ")}`);return await Me(r,"complete",{serverId:s.serverId,targetType:s.targetType.name}),{status:"completed",plan:s,server:c.server,manifest:a,verification:c.verification}}var Oc=String.raw`
189
+ ${so(t.after)}`}function xr(e){let t=new Set([80,443,...e.sitePorts,...e.allowedPorts??[]]);if(e.allowSsh)t.add(22);return[...t].map((r)=>({direction:"in",protocol:"tcp",port:String(r),source_ips:["0.0.0.0/0","::/0"],description:`ts-cloud port ${r}`}))}var Un={micro:"cpx11",small:"cx23",medium:"cx33",large:"cx43",xlarge:"cx53","2xlarge":"ccx33"};function Ge(e){if(!e)return Un.micro;if(e in Un)return Un[e];return e}var Se="ts-cloud";function ye(e,t,r="app"){return{[`${Se}/project`]:e,[`${Se}/environment`]:t,[`${Se}/role`]:r,[`${Se}/managed-by`]:"ts-cloud"}}function we(e,t,r,n="app"){return kn(e,t,r)&&e[`${Se}/role`]===n}function kn(e,t,r){if(!e)return!1;return e[`${Se}/project`]===t&&e[`${Se}/environment`]===r}function Rc(e){return e.location?.name??e.datacenter?.location.name??"unknown"}function gt(e){return typeof e==="number"&&Number.isFinite(e)?e:0}function Oc(e,t){if(e.architecture&&t.architecture&&e.architecture!==t.architecture)throw Error(`Cannot resize ${e.name} (${e.architecture}) to ${t.name} (${t.architecture}) in place.`);let r=[];if(gt(t.cores)<gt(e.cores))r.push("CPU");if(gt(t.memory)<gt(e.memory))r.push("memory");if(gt(t.disk)<gt(e.disk))r.push("disk");if(r.length>0)throw Error(`Refusing an in-place downgrade from ${e.name} to ${t.name}: ${r.join(", ")} would shrink.`)}async function tr(e,t,r,n=!0){let[s,i]=await Promise.all([e.getServer(t),e.getServerType(r)]);if(!i)throw Error(`Hetzner server type ${r} was not found.`);let a=s.server_type,c=a.name===i.name;if(!c)Oc(a,i);let f=Rc(s),h=i.locations?.find((g)=>g.name===f);return{serverId:t,serverName:s.name,location:f,currentType:a,targetType:i,capacityAvailable:h?.available!==!1,alreadyComplete:c,upgradeDisk:n}}function Yg(e){if(e instanceof Or&&e.code==="resource_unavailable")return!0;if(e instanceof Dr){if(e.action.error?.code==="resource_unavailable")return!0;if(/no fitting vhost|placement|free resources/i.test(e.action.error?.message??""))return!0}return/resource_unavailable|no fitting vhost|error during placement|free resources/i.test(e instanceof Error?e.message:String(e))}async function Me(e,t,r){await e?.onPhase?.(t,r)}async function Hn(e,t,r){await e.waitForAction(t.id,{pollIntervalMs:r.actionPollIntervalMs,maxWaitMs:r.actionTimeoutMs})}var Ar=async({client:e,hooks:t,actionPollIntervalMs:r,actionTimeoutMs:n,serverPollIntervalMs:s,serverTimeoutMs:i},a,c,f)=>{let h=await e.getServer(a.serverId);if(h.status!=="running"){await Me(t,f?"recovering":"powering-on",{serverId:a.serverId});let v=await e.powerOnServer(a.serverId);await Hn(e,v,{actionPollIntervalMs:r,actionTimeoutMs:n}),h=await e.waitForServerRunning(a.serverId,{pollIntervalMs:s,maxWaitMs:i})}await Me(t,"verifying",{serverId:a.serverId,recovered:f});let g=await t?.afterBoot?.(h,a.targetType,c,{recovered:f});return{server:h,verification:g}};async function gi(e){let{client:t,hooks:r}=e,n=e.upgradeDisk??!0;await Me(r,"planning",{serverId:e.serverId,targetType:e.targetType});let s=await tr(t,e.serverId,e.targetType,n),i=await t.getServer(s.serverId);if(s.alreadyComplete){let f=await Ar(e,s,e.manifest,!1);return await Me(r,"complete",{alreadyComplete:!0}),{status:"already-complete",plan:s,verification:f.verification}}if(!s.capacityAvailable&&!e.attemptUnavailable)return await Me(r,"waiting-capacity",{location:s.location,targetType:s.targetType.name}),{status:"waiting-capacity",plan:s};let a=e.manifest;if(i.status==="off"&&!e.resumeFromOff){let f=await Ar(e,s,void 0,!0);return{status:"recovered",plan:s,server:f.server,verification:f.verification,retryable:!0,error:"Server was already off without a resumable resize checkpoint; powered it back on."}}if(i.status==="running"){await Me(r,"preflight",{serverId:s.serverId}),a=await r?.preflight?.(i,s.targetType),await Me(r,"shutting-down",{serverId:s.serverId});let f=await t.shutdownServer(s.serverId);await Hn(t,f,e),i=await t.waitForServerStatus(s.serverId,"off",{pollIntervalMs:e.serverPollIntervalMs,maxWaitMs:e.serverTimeoutMs})}try{await Me(r,"resizing",{serverId:s.serverId,from:s.currentType.name,to:s.targetType.name,upgradeDisk:n});let f=await t.changeServerType(s.serverId,s.targetType.name,n);await Hn(t,f,e)}catch(f){let h=await Ar(e,s,a,!0);return{status:"recovered",plan:s,server:h.server,manifest:a,verification:h.verification,retryable:Yg(f),error:f instanceof Error?f.message:String(f)}}let c=await Ar(e,s,a,!1);if(c.verification&&!c.verification.ok)throw Error(`Resize completed but verification failed: ${(c.verification.failures??[]).join("; ")}`);return await Me(r,"complete",{serverId:s.serverId,targetType:s.targetType.name}),{status:"completed",plan:s,server:c.server,manifest:a,verification:c.verification}}var Dc=String.raw`
179
190
  const { readdir, readFile, readlink, stat } = await import('node:fs/promises')
180
191
 
181
192
  const text = (command) => {
@@ -267,7 +278,7 @@ console.log(JSON.stringify({
267
278
  persistentData: persistentData.sort(),
268
279
  dataCatalog: dataCatalog.sort(),
269
280
  }))
270
- `;function Dc(e){return`printf %s ${Buffer.from(e).toString("base64")} | base64 -d | bun -`}async function xc(e){let t=await ve(e.host,Dc(Oc),e);return JSON.parse(t)}async function Ac(e,t){let r="";for(let n=1;n<=3;n++){let s=Bun.spawn(["curl","--location","--silent","--show-error","--output","/dev/null","--max-time",String(Math.max(1,Math.ceil(t/1000))),"--write-out","%{http_code}",`https://${e}/`],{stdout:"pipe",stderr:"pipe"}),[i,a,c]=await Promise.all([new Response(s.stdout).text(),new Response(s.stderr).text(),s.exited]),f=Number.parseInt(i.trim(),10);if(c===0&&f>0)return{domain:e,ok:f<500,status:f};if(r=a.trim()||`curl exited ${c}`,n<3)await Bun.sleep(500*n)}return{domain:e,ok:!1,error:r}}async function Ic(e,t){let r=[],n=0,s=Array.from({length:Math.min(4,e.length)},async()=>{for(;;){let i=n++,a=e[i];if(!a)return;r[i]=await Ac(a,t)}});return await Promise.all(s),r}async function ed(e){let t=await xc(e),r=await Ic(t.routeDomains,e.routeTimeoutMs??15000);return{...t,routeProbes:r}}async function td(e){let t=await ed(e),r=t.routeProbes.filter((n)=>!n.ok);if(t.failedUnits.length>0)throw Error(`Preflight found failed services: ${t.failedUnits.join(", ")}`);if(r.length>0)throw Error(`Preflight found unhealthy routes: ${r.map((n)=>n.domain).join(", ")}`);return await ve(e.host,String.raw`systemctl list-units --type=service --state=running --no-legend --plain | awk '{print $1}' | while read -r unit; do if systemctl cat "$unit" 2>/dev/null | grep -Eq '/var/www|/usr/local/bin/rpx|/opt/ts-cloud'; then systemctl enable "$unit" >/dev/null 2>&1 || true; fi; done; systemctl daemon-reload; sync`,e),t}function _t(e,t){let r=new Set(t);return e.filter((n)=>!r.has(n))}function Lc(e){return Math.floor((e.memory??0)*1024*1024*1024)}async function Uc(e){await ve(e.host,String.raw`set -e
281
+ `;function xc(e){return`printf %s ${Buffer.from(e).toString("base64")} | base64 -d | bun -`}async function Ac(e){let t=await ve(e.host,xc(Dc),e);return JSON.parse(t)}async function Ic(e,t){let r="";for(let n=1;n<=3;n++){let s=Bun.spawn(["curl","--location","--silent","--show-error","--output","/dev/null","--max-time",String(Math.max(1,Math.ceil(t/1000))),"--write-out","%{http_code}",`https://${e}/`],{stdout:"pipe",stderr:"pipe"}),[i,a,c]=await Promise.all([new Response(s.stdout).text(),new Response(s.stderr).text(),s.exited]),f=Number.parseInt(i.trim(),10);if(c===0&&f>0)return{domain:e,ok:f<500,status:f};if(r=a.trim()||`curl exited ${c}`,n<3)await Bun.sleep(500*n)}return{domain:e,ok:!1,error:r}}async function Lc(e,t){let r=[],n=0,s=Array.from({length:Math.min(4,e.length)},async()=>{for(;;){let i=n++,a=e[i];if(!a)return;r[i]=await Ic(a,t)}});return await Promise.all(s),r}async function ed(e){let t=await Ac(e),r=await Lc(t.routeDomains,e.routeTimeoutMs??15000);return{...t,routeProbes:r}}async function td(e){let t=await ed(e),r=t.routeProbes.filter((n)=>!n.ok);if(t.failedUnits.length>0)throw Error(`Preflight found failed services: ${t.failedUnits.join(", ")}`);if(r.length>0)throw Error(`Preflight found unhealthy routes: ${r.map((n)=>n.domain).join(", ")}`);return await ve(e.host,String.raw`systemctl list-units --type=service --state=running --no-legend --plain | awk '{print $1}' | while read -r unit; do if systemctl cat "$unit" 2>/dev/null | grep -Eq '/var/www|/usr/local/bin/rpx|/opt/ts-cloud'; then systemctl enable "$unit" >/dev/null 2>&1 || true; fi; done; systemctl daemon-reload; sync`,e),t}function _t(e,t){let r=new Set(t);return e.filter((n)=>!r.has(n))}function Uc(e){return Math.floor((e.memory??0)*1024*1024*1024)}async function kc(e){await ve(e.host,String.raw`set -e
271
282
  root_source="$(findmnt -n -o SOURCE /)"
272
283
  root_fs="$(findmnt -n -o FSTYPE /)"
273
284
  parent="$(lsblk -n -o PKNAME "$root_source" | head -1)"
@@ -279,19 +290,19 @@ case "$root_fs" in
279
290
  ext2|ext3|ext4) resize2fs "$root_source" ;;
280
291
  xfs) xfs_growfs / ;;
281
292
  *) echo "Unsupported root filesystem: $root_fs" >&2; exit 1 ;;
282
- esac`,e)}async function kc(e,t){let r=Buffer.from(t.join(`
283
- `)).toString("base64");await ve(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 rd(e,t,r,n={recovered:!1}){if(await St(e.host,{...e,timeoutMs:e.sshTimeoutMs??600000}),!n.recovered)await Uc(e);if(r)await kc(e,r.runningServices);let s=await ed(e),i=[],a=r?_t(r.runningServices,s.runningServices):[],c=r?_t(r.releaseLinks,s.releaseLinks):[],f=r?_t(r.routeFragments,s.routeFragments):[],g=r?_t(r.routeIds,s.routeIds):[],h=r?.persistentData.map((N)=>N.split("=")[0])??[],S=s.persistentData.map((N)=>N.split("=")[0]),_=_t(h,S),v=r?_t(r.dataCatalog,s.dataCatalog):[],b=s.routeProbes.filter((N)=>!N.ok),T=n.recovered&&r?r.cpuCores:t.cores??0,L=n.recovered&&r?r.memoryBytes:Lc(t),K=s.cpuCores>=T,M=s.memoryBytes>=L*0.9,D=n.recovered||s.rootDiskBytes>=(t.disk??0)*1e9*0.95;if(s.failedUnits.length>0)i.push(`Failed services: ${s.failedUnits.join(", ")}`);if(a.length>0)i.push(`Services did not return: ${a.join(", ")}`);if(c.length>0)i.push(`Release links changed: ${c.join(", ")}`);if(f.length>0)i.push(`Route manifests changed: ${f.join(", ")}`);if(g.length>0)i.push(`Routes disappeared: ${g.join(", ")}`);if(_.length>0)i.push(`Persistent data paths disappeared: ${_.join(", ")}`);if(v.length>0)i.push(`Databases or volumes disappeared: ${v.join(", ")}`);if(b.length>0)i.push(`Unhealthy routes: ${b.map((N)=>N.domain).join(", ")}`);if(!K)i.push(`Expected at least ${T} CPU cores, found ${s.cpuCores}`);if(!M)i.push(`Expected at least ${L} bytes of RAM`);if(!D)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((N)=>N.ok).length,persistentDataPaths:s.persistentData.length,databasesAndVolumes:s.dataCatalog.length,failedUnits:s.failedUnits.length},failures:i}}import{mkdir as Hc,readFile as Nc,rename as Mc,writeFile as Fc}from"node:fs/promises";import{join as so}from"node:path";var nd="storage/cloud/state";function It(e=process.cwd()){return ml()?l(e,"state"):so(e,nd)}function Je(e,t=process.cwd()){return so(It(t),`${e}.json`)}async function Xe(e,t=process.cwd()){try{let r=await Nc(Je(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function bt(e,t,r=process.cwd()){let n=Je(e,r);await Hc(It(r),{recursive:!0});let s=`${n}.${process.pid}.tmp`;await Fc(s,`${JSON.stringify(t,null,2)}
284
- `,"utf8"),await Mc(s,n)}async function tt(e){return Xe(e)}async function nt(e,t){await bt(e,t)}async function pi(e,t,r){let n=r.length?await e.listVolumes():[];return r.map((s)=>{let i=n.filter((g)=>g.name===s.name);if(i.length>1)throw Error(`Ambiguous volume name: ${s.name}`);let a=i[0];if(a&&a.server!==null&&a.server!==t.id)throw Error(`Volume ${a.name} belongs to another server; refusing to detach live storage`);let c=t.location?.name??t.datacenter?.location.name;if(a&&a.location.name!==c)throw Error(`Volume ${a.name} is in ${a.location.name}, server is in ${c}`);if(a&&a.size>s.size)throw Error(`Refusing to shrink volume ${a.name}: ${a.size} GB to ${s.size} GB`);let f=[];if(!a)f.push("create");else{if(a.server===null)f.push("attach");if(a.size<s.size)f.push("grow")}if(!a||a.protection.delete!==(s.deleteProtection!==!1))f.push("protect");return f.push("mount"),{declaration:s,existing:a,actions:f}})}async function mi(e,t,r){let n=[];for(let s of r){let i=s.existing,a=!i;if(!i){let c=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(c.action.id);for(let f of c.next_actions??[])await e.waitForAction(f.id);i=await e.getVolume(c.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:a})}return n}var Nn=268435456;function Wt(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function rl(e){let t=Gc("sha256");for await(let r of Qc(e))t.update(r);return t.digest("hex")}class xe{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=ri(t,r),i=In(n??{});if(i.appServers>1||i.dedicatedServices)throw Error("Managed compute volumes currently require a single app server");let a=await this.getComputeOutputs(e);if(!a.appInstanceId)throw Error("No existing compute server found; provision the server first");let c=await this.client.getServer(Number(a.appInstanceId)),f=await pi(this.client,c,s),g=n?.resize&&n.size?await tr(this.client,c.id,Ge(n.size),n.resize.upgradeDisk!==!1):void 0,h={serverId:String(c.id),changes:f.flatMap((b)=>b.actions.map((T)=>({resource:b.declaration.name,action:T,detail:`${b.declaration.size} GB at ${b.declaration.mountPath}`})))};if(n?.deploymentStorage)h.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(g&&!g.alreadyComplete)if(!g.capacityAvailable){if(n?.resize?.whenUnavailable!=="keep-current")throw Error(`${g.targetType.name} is unavailable in ${g.location}`);h.waitingForCapacity=`${g.targetType.name} unavailable in ${g.location}; keeping ${c.server_type.name} running`}else h.changes.push({resource:c.name,action:"resize",detail:`${c.server_type.name} to ${g.targetType.name}`});if(e.dryRun)return h;if(h.waitingForCapacity)console.warn(`[ts-cloud] ${h.waitingForCapacity}`);let S=await mi(this.client,c,f),_=[{id:String(c.id),name:c.name,publicIp:c.public_net.ipv4?.ip}],v=[...qe(t.notifications),...n?.volumes||n?.deploymentStorage?ni({volumes:S,deploymentStorage:n?.deploymentStorage,webRoot:"/var/www"}):[],...n?.monitoring!==!1?He(n?.monitoring):[]];if(v.length){let b=await this.runRemoteDeploy({targets:_,commands:v,timeoutSeconds:7200,comment:"ts-cloud apply configured compute storage"});if(!b.success)throw Error(b.error||b.perInstance.map((T)=>T.error||T.output).join(`
285
- `))}if(g?.capacityAvailable&&!g.alreadyComplete){let b={host:_[0].publicIp,user:this.sshUser,identityFile:this.sshPrivateKeyPath},T=await gi({client:this.client,serverId:c.id,targetType:g.targetType.name,upgradeDisk:g.upgradeDisk,hooks:{preflight:()=>td(b),afterBoot:(L,K,M,D)=>rd(b,K,M,D)}});if(T.status!=="completed"&&T.status!=="already-complete")throw Error(`Resize did not complete: ${T.status}`)}return h}constructor(e={}){this.residency=e.residency;let t=Vt(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 Gr({apiToken:t??""});let r=eo(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=m(t,r),a=`${n}-${r}-app`,c=s.runtime==="php"||!!s.php,f=In(s);if(s.reverseDns&&(f.dedicatedServices||f.appServers>1))throw Error("Reverse DNS currently requires a single owned app server");let g=t.cloud?.residency||this.residency,{assertComputeResidency:h}=await import("./chunk-cq34675f.js"),S=(X)=>{if(g)h(g,{country:X.location?.country||X.datacenter?.location?.country,location:X.location?.name||X.datacenter?.location?.name})};if(g){if(f.dedicatedServices||f.appServers>1)throw Error("Residency policy currently requires single-host compute");let X=await this.client.getLocation(t.hetzner?.location||this.location);h(g,{country:X.country,location:X.name})}if(f.dedicatedServices||f.appServers>1)return c?this.provisionFleet(e,f):this.provisionBunFleet(e,f);let _=$e(n,r,"app"),v=t.sites||{},b=z(s)?[]:this.collectUpstreamPorts(v),T=`${n}-${r}-app-fw`,{firewall:L}=await this.ensureFirewall(T,_,xr({allowSsh:s.allowSsh!==!1,sitePorts:b,allowedPorts:[...s.firewall?.allowedPorts??[],...Cs(t.infrastructure?.sftp),...rt(C(t,{environment:r}))]})),K=await this.ensureSshKey(n,r,_),M=await tt(i);if(M?.serverId){let X=await this.tryGetServer(M.serverId);if(X&&X.status!=="off"&&X.labels?.["ts-cloud/project"]===n&&X.labels?.["ts-cloud/environment"]===r){if(S(X),await this.reconcileReverseDns(e,X),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.reconcileOwnedServices(e,X),this.outputsFromState(M,X)}}let D=await this.findExistingServer(n,r,a);if(D&&D.status!=="off"){S(D);let X={provider:"hetzner",stackName:i,serverId:D.id,serverName:D.name,publicIp:D.public_net.ipv4?.ip,publicIpv6:ae(D.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};if(await nt(i,X),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.reconcileOwnedServices(e,D),await this.reconcileReverseDns(e,D),this.outputsFromState(X,D)}let N=R(t,{environment:r}),k=N?.engine==="rpx"?V({proxy:N,config:x(v,{proxy:N,slug:t.project.slug,mail:C(t,{environment:r})}),slug:t.project.slug,bunBin:s.runtime==="node"||s.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,j=Ye(t,{environment:r}),q=s.bakedImage===!0,B=F({runtime:j.runtime,runtimeVersion:j.runtimeVersion,systemPackages:s.systemPackages,database:t.infrastructure?.database,phpProvision:j.phpProvision,servicesProvision:j.servicesProvision,rpxProvision:k,baked:q,swapGb:s.swapGb}),re=ce(B),ee=Ge(s.size),ne=qt(t),{server:oe,action:ie}=await this.client.createServer({name:a,serverType:ee,image:ne,location:t.hetzner?.location||this.location,userData:re,labels:_,sshKeys:K?[K]:void 0,firewalls:[{firewall:L.id}]});await this.client.waitForAction(ie.id);let se=await this.client.waitForServerRunning(oe.id);S(se),await this.reconcileReverseDns(e,se);let de={provider:"hetzner",stackName:i,serverId:se.id,serverName:se.name,firewallId:L.id,publicIp:se.public_net.ipv4?.ip,publicIpv6:ae(se.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};await nt(i,de);let me=se.public_net.ipv4?.ip;if(me&&this.waitForBoot)await this.waitForSshReady(me),await this.waitForCloudInit(me);if(s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(de,se)}async provisionFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,a=m(r,n),c=Ge(i.size),f=qt(r),g=r.hetzner?.location||this.location,h=i.bakedImage===!0,S=await tt(a);if(S?.loadBalancerId){let I=(await this.client.listLoadBalancers().catch(()=>[])).find((w)=>w.id===S.loadBalancerId);if(I)return{appPublicIp:I.public_net?.ipv4?.ip,loadBalancerIp:I.public_net?.ipv4?.ip,servicesPrivateIp:S.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}let _=await this.ensureSshKey(s,n,$e(s,n,"app")),v=`${s}-${n}-net`,T=(await this.client.listNetworks().catch(()=>[])).find((E)=>E.name===v)??await this.client.createNetwork({name:v,labels:$e(s,n,"app")}),{firewall:L}=await this.ensureFirewall(`${s}-${n}-app-fw`,$e(s,n,"app"),xr({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),{firewall:K}=await this.ensureFirewall(`${s}-${n}-services-fw`,$e(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:[T.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[T.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[T.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[T.ip_range]}]),M=Ln(r),D=ce(F({runtime:"php",servicesProvision:M,baked:h,swapGb:i.swapGb})),N=await this.client.listServers().catch(()=>[]),k=[],j=N.find((E)=>we(E.labels,s,n,"services"));if(!j){let{server:E,action:I}=await this.client.createServer({name:`${s}-${n}-services`,serverType:Ge(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:f,location:g,userData:D,labels:$e(s,n,"services"),sshKeys:_?[_]:void 0,firewalls:[{firewall:K.id}],networks:[T.id]});await this.client.waitForAction(I.id),j=await this.client.waitForServerRunning(E.id),k.push(E.id)}let q=j.id,B=j.private_net?.[0]?.ip??(await this.client.getServer(q)).private_net?.[0]?.ip,re=[...ze(!0),...He(!0),...lt(i.sshKeys),...qe(r.notifications)],ee=ur({versions:i.php?.versions,default:i.php?.default,extensions:i.php?.extensions,installNginx:!z(i),optimizeForProduction:i.php?.optimizeForProduction,ini:i.php?.ini}),ne=ce(F({runtime:"php",phpProvision:ee,servicesProvision:re,baked:h,swapGb:i.swapGb})),oe=N.filter((E)=>we(E.labels,s,n,"app")),ie=oe.map((E)=>E.id);if(oe.length>t.appServers)for(let E of oe.slice(t.appServers))await this.client.deleteServer(E.id).catch(()=>{}),ie.splice(ie.indexOf(E.id),1);for(let E=oe.length;E<t.appServers;E++){let{server:I,action:w}=await this.client.createServer({name:`${s}-${n}-app-${E+1}`,serverType:c,image:f,location:g,userData:ne,labels:$e(s,n,"app"),sshKeys:_?[_]:void 0,firewalls:[{firewall:L.id}],networks:[T.id]});await this.client.waitForAction(w.id),ie.push(I.id),k.push(I.id)}if(this.waitForBoot&&k.length>0)await Promise.all(k.map(async(E)=>{let w=(await this.client.waitForServerRunning(E)).public_net.ipv4?.ip;if(w)await this.waitForSshReady(w),await this.waitForCloudInit(w)}));let se=`${s}-${n}-lb`,de,me;if(t.loadBalancer){let I=(await this.client.listLoadBalancers().catch(()=>[])).find((w)=>w.name===se)??await this.client.createLoadBalancer({name:se,location:g,network:T.id,labels:$e(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}]});me=I.id,de=I.public_net?.ipv4?.ip}let X=de??(await this.client.getServer(ie[0]).catch(()=>{return}))?.public_net.ipv4?.ip,P={provider:"hetzner",stackName:a,networkId:T.id,loadBalancerId:me,servicesServerId:q,servicesPrivateIp:B,publicIp:X,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await nt(a,P),{appPublicIp:X,loadBalancerIp:de,servicesPrivateIp:B,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,a=m(r,n),c=Ge(i.size),f=qt(r),g=r.hetzner?.location||this.location,h=i.bakedImage===!0,S=r.sites||{},_=await tt(a);if(_?.lbServerId){let w=await this.tryGetServer(_.lbServerId);if(w&&w.status!=="off"){let G=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"},Z=await this.findComputeTargets({slug:s,environment:n,role:"app",stackName:a});return this.refreshLbRoutes(w,S,Z.map((Y)=>({privateIp:Y.privateIp,publicIp:Y.publicIp})),G,s),{appPublicIp:w.public_net.ipv4?.ip??_.publicIp,loadBalancerIp:w.public_net.ipv4?.ip??_.publicIp,servicesPrivateIp:_.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}}let v=await this.ensureSshKey(s,n,$e(s,n,"app")),b=`${s}-${n}-net`,L=(await this.client.listNetworks().catch(()=>[])).find((w)=>w.name===b)??await this.client.createNetwork({name:b,labels:$e(s,n,"app")}),K=this.collectUpstreamPorts(S),{firewall:M}=await this.ensureFirewall(`${s}-${n}-app-fw`,$e(s,n,"app"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},...K.map((w)=>({direction:"in",protocol:"tcp",port:String(w),source_ips:[L.ip_range],description:`ts-cloud port ${w} (private, LB-only)`}))]),{firewall:D}=await this.ensureFirewall(`${s}-${n}-lb-fw`,$e(s,n,"lb"),xr({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),N=await this.client.listServers().catch(()=>[]),k=[],j,q,B=!!i.servicesServer||!!i.managedServices,re=t.dedicatedServices&&B;if(re){let{firewall:w}=await this.ensureFirewall(`${s}-${n}-services-fw`,$e(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:[L.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[L.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[L.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[L.ip_range]}]),G=Ln(r),Z=ce(F({runtime:"bun",servicesProvision:G,baked:h,swapGb:i.swapGb})),Y=N.find((J)=>we(J.labels,s,n,"services"));if(!Y){let{server:J,action:te}=await this.client.createServer({name:`${s}-${n}-services`,serverType:Ge(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:f,location:g,userData:Z,labels:$e(s,n,"services"),sshKeys:v?[v]:void 0,firewalls:[{firewall:w.id}],networks:[L.id]});await this.client.waitForAction(te.id),Y=await this.client.waitForServerRunning(J.id),k.push(J.id)}j=Y.id,q=Y.private_net?.[0]?.ip??(await this.client.getServer(j)).private_net?.[0]?.ip}let ee=re?{...i,managedServices:void 0,backups:void 0}:i,ne=Ye({...r,infrastructure:{...r.infrastructure,compute:ee}},{environment:n}),oe=ce(F({runtime:ne.runtime,runtimeVersion:ne.runtimeVersion,systemPackages:i.systemPackages,database:r.infrastructure?.database,servicesProvision:ne.servicesProvision,baked:h,swapGb:i.swapGb})),ie=N.filter((w)=>we(w.labels,s,n,"app")),se=ie.map((w)=>w.id);if(ie.length>t.appServers)for(let w of ie.slice(t.appServers))await this.client.deleteServer(w.id).catch(()=>{}),se.splice(se.indexOf(w.id),1);for(let w=ie.length;w<t.appServers;w++){let{server:G,action:Z}=await this.client.createServer({name:`${s}-${n}-app-${w+1}`,serverType:c,image:f,location:g,userData:oe,labels:$e(s,n,"app"),sshKeys:v?[v]:void 0,firewalls:[{firewall:M.id}],networks:[L.id]});await this.client.waitForAction(Z.id),se.push(G.id),k.push(G.id)}if(this.waitForBoot&&k.length>0)await Promise.all(k.map(async(w)=>{let Z=(await this.client.waitForServerRunning(w)).public_net.ipv4?.ip;if(Z)await this.waitForSshReady(Z),await this.waitForCloudInit(Z)}));let de=[];for(let w of se){let G=N.find((Z)=>Z.id===w);if(!G||!G.private_net?.[0]?.ip)G=await this.client.getServer(w).catch(()=>G);de.push({privateIp:G?.private_net?.[0]?.ip,publicIp:G?.public_net.ipv4?.ip})}let me=`${s}-${n}-lb`,X,P;if(t.loadBalancer){let w=N.find((Z)=>we(Z.labels,s,n,"lb")),G=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"};if(!w){let Z=V({proxy:G,config:nr(S,de,{proxy:G,slug:s}),slug:s,bunBin:ne.runtime==="node"||ne.runtime==="deno"?void 0:"/usr/local/bin/bun"}),Y=ce(F({runtime:"bun",rpxProvision:Z,baked:!1})),J=typeof i.loadBalancer==="object"?i.loadBalancer.size:void 0,{server:te,action:O}=await this.client.createServer({name:me,serverType:Ge(J??"micro"),image:f,location:g,userData:Y,labels:$e(s,n,"lb"),sshKeys:v?[v]:void 0,firewalls:[{firewall:D.id}],networks:[L.id]});if(await this.client.waitForAction(O.id),w=await this.client.waitForServerRunning(te.id),this.waitForBoot){let A=w.public_net.ipv4?.ip;if(A)await this.waitForSshReady(A),await this.waitForCloudInit(A)}}else this.refreshLbRoutes(w,S,de,G,s);X=w.id,P=w.public_net.ipv4?.ip}let E=P??(await this.client.getServer(se[0]).catch(()=>{return}))?.public_net.ipv4?.ip,I={provider:"hetzner",stackName:a,networkId:L.id,lbServerId:X,appServerIds:se,servicesServerId:j,servicesPrivateIp:q,publicIp:E,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await nt(a,I),{appPublicIp:E,loadBalancerIp:P,servicesPrivateIp:q,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=m(t,r),i=await tt(s),a=[],c=`${n}-${r}-lb`,g=(await this.client.listLoadBalancers().catch(()=>[])).find((M)=>M.name===c);if(g)try{await this.client.deleteLoadBalancer(g.id),a.push(`load balancer ${c}`)}catch{}let h=await this.client.listServers().catch(()=>[]),S=new Set;for(let M of h)if(we(M.labels,n,r,"app")||we(M.labels,n,r,"services")||we(M.labels,n,r,"lb"))S.add(M.id);if(i?.serverId)S.add(i.serverId);if(i?.servicesServerId)S.add(i.servicesServerId);if(i?.lbServerId)S.add(i.lbServerId);for(let M of i?.appServerIds??[])S.add(M);for(let M of S)try{await this.client.deleteServer(M),a.push(`server ${M}`)}catch{}let _=await this.client.listFirewalls().catch(()=>[]);for(let M of[`${n}-${r}-app-fw`,`${n}-${r}-services-fw`,`${n}-${r}-lb-fw`]){let D=_.find((N)=>N.name===M);if(!D)continue;for(let N=0;N<12;N++)try{await this.client.deleteFirewall(D.id),a.push(`firewall ${M}`);break}catch{await this.sleep(3000)}}let v=`${n}-${r}-net`,T=(await this.client.listNetworks().catch(()=>[])).find((M)=>M.name===v);if(T)for(let M=0;M<12;M++)try{await this.client.deleteNetwork(T.id),a.push(`network ${v}`);break}catch{await this.sleep(3000)}let K=(await this.client.listSshKeys().catch(()=>[])).filter((M)=>kn(M.labels,n,r));for(let M of K)try{await this.client.deleteSshKey(M.id),a.push(`ssh key ${M.name}`)}catch{}return await nt(s,{provider:"hetzner",stackName:s}).catch(()=>{}),{destroyed:a}}async getComputeOutputs(e){let t=m(e.config,e.environment),r=await tt(t);if(r?.serverId){let i=await this.tryGetServer(r.serverId);if(i)return this.outputsFromState(r,i)}if(r?.lbServerId){let a=(await this.tryGetServer(r.lbServerId))?.public_net.ipv4?.ip??r.publicIp;return{...this.outputsFromState(r),appPublicIp:a,loadBalancerIp:a}}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:m(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:m(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,"-")}-${So()}.tar.gz`}`,i=await rl(e.localPath),a=Ur,c=`${a}/${i}.tar.gz`;for(let f of t){if(!f.publicIp)throw Error(`Target ${f.id} has no public IP for SCP upload`);let g=["set -euo pipefail",`mkdir -p ${Wt(a)} /var/ts-cloud/staging`,`test -s ${Wt(c)}`,`cp -- ${Wt(c)} ${Wt(s)}`].join(`
286
- `);try{this.sshExec(f.publicIp,g);continue}catch{}let h=`${a}/.${i}-${So()}.tmp`;try{this.scpToHost(f.publicIp,e.localPath,h),this.sshExec(f.publicIp,Cr(h,c,s))}catch(S){try{this.sshExec(f.publicIp,Pr(h))}catch{}throw S}}return{artifactRef:s}}async findComputeTargets(e){let{assertComputeResidency:t}=await import("./chunk-cq34675f.js"),r=e.role||"app",n=(a)=>{if(this.residency)t(this.residency,{country:a.location?.country||a.datacenter?.location?.country,location:a.location?.name||a.datacenter?.location?.name});return{id:String(a.id),name:a.name,publicIp:a.public_net.ipv4?.ip,publicIpv6:ae(a.public_net.ipv6?.ip),privateIp:a.private_net?.[0]?.ip,status:a.status}};if(r==="app"||r==="lb"||r==="services"){let a=await tt(e.stackName??`${e.slug}-${e.environment}`),c=[...new Set(r==="app"?[a?.serverId,...a?.appServerIds??[]]:r==="lb"?[a?.lbServerId]:[a?.servicesServerId])].filter((h)=>typeof h==="number");if(this.stateOnly){if(this.residency)throw Error("Residency requires fresh provider evidence; a local state pin alone is insufficient");if(c.length>0&&a?.publicIp)return c.map((h)=>({id:String(h),name:h===a.serverId?a.serverName:void 0,publicIp:a.publicIp,publicIpv6:a.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 f=await this.client.listServers();if(c.length>0){let h=[];for(let S of c){let _=f.find((v)=>v.id===S)??await this.tryGetServer(S);if(S===a?.serverId&&a.serverName&&_?.name!==a.serverName)continue;if(_&&_.status!=="off")h.push(_)}if(h.length>0)return h.map(n)}let g=f.filter((h)=>we(h.labels,e.slug,e.environment,r));if(g.length>0)return g.map(n);if(r==="app"){let h=f.filter((S)=>S.status!=="off"&&S.labels?.[`${Se}/managed-by`]==="ts-cloud"&&S.labels?.[`${Se}/environment`]===e.environment&&S.labels?.[`${Se}/role`]==="app");if(h.length===1)return h.map(n)}return[]}if(this.stateOnly)throw Error(`Hetzner API token required to discover '${r}' compute targets.`);let i=(await this.client.listServers()).filter((a)=>we(a.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=(_)=>n.filter((v)=>v.status!=="off"&&we(v.labels,e,r,_)),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 a=s("lb")[0],c=s("services")[0],f=i[0],g=a??f,h=m(t,r),S={provider:"hetzner",stackName:h,serverId:f.id,serverName:f.name,appServerIds:i.map((_)=>_.id),lbServerId:a?.id,servicesServerId:c?.id,servicesPrivateIp:c?.private_net?.[0]?.ip,publicIp:g.public_net.ipv4?.ip,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await nt(h,S),this.outputsFromState(S)}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(`
287
- `),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);_r(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:Zt(r,"One or more SSH deploy commands failed")}}async ensureSshKey(e,t,r){if(!el(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=tl(this.sshPublicKeyPath,"utf8").trim(),s=We(n),a=(await this.client.listSshKeys()).find((f)=>We(f.public_key)===s);if(a)return a.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 a=r.filter((f)=>f.privateIp||f.publicIp);if(a.length===0){console.warn(`[ts-cloud] No routable app boxes found for LB '${e.name}' — leaving its current rpx routes untouched.`);return}let c=nr(t,a,{proxy:n,slug:s});this.sshExec(i,_e({config:c,slug:s}).join(`
288
- `))}async findExistingServer(e,t,r){let n=await this.client.listServers(),s=n.find((a)=>we(a.labels,e,t,"app")||a.name===r);if(s)return s;let i=n.filter((a)=>a.status!=="off"&&a.labels?.[`${Se}/managed-by`]==="ts-cloud"&&a.labels?.[`${Se}/environment`]===t&&a.labels?.[`${Se}/role`]==="app");if(i.length===1){let a=i[0],c=a.labels?.[`${Se}/project`]??"unknown";return console.warn(`[ts-cloud] No server named '${r}' found; adopting existing ts-cloud app server '${a.name}' (project label '${c}') for project '${e}' — `+"updating it in place instead of provisioning a new server."),a}return}async ensureFirewall(e,t,r){let s=(await this.client.listFirewalls()).find((a)=>a.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{io(`ssh ${this.sshBaseArgs(e,["-o","ConnectTimeout=5"]).map(Wt).join(" ")} true`,{stdio:"pipe",maxBuffer:Nn});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}:
289
- ${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:ae(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,...xe.SSH_HOST_KEY_OPTS,...xe.SSH_KEEPALIVE_OPTS,"-o","BatchMode=yes",...t,`${this.sshUser}@${e}`]}scpToHost(e,t,r){let n=["scp",...Gt,"-i",this.sshPrivateKeyPath,...xe.SSH_HOST_KEY_OPTS,...xe.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{io(n,{stdio:"pipe",maxBuffer:Nn});return}catch(a){if(i===3)throw Error(Yt(a))}}reconcileOwnedServices(e,t){let r=Ps(e.config,e.environment);if(!r.length)return;let n=t.public_net.ipv4?.ip;if(!n)throw Error("The owned server needs a reachable address to reconcile operational services");this.sshExec(n,["set -e",...r].join(`
290
- `))}async reconcileReverseDns(e,t){if(e.config.cloud?.attachTo)return;for(let r of ro(t,e.config.infrastructure?.compute?.reverseDns)){let n=await this.client.changeServerReverseDns(t.id,r.ip,r.hostname);await this.client.waitForAction(n.id)}}sshExec(e,t){try{return Bc("ssh",[...this.sshBaseArgs(e),Xc],{encoding:"utf8",input:t,stdio:["pipe","pipe","pipe"],maxBuffer:Nn})}catch(r){throw Error(`${Yt(r)}${ti(r,t)}`)}}}class lo{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(`
291
- `);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 uo(){let e=process.env.TS_CLOUD_DASHBOARD_BOX;return e!=null&&e!==""&&e!=="0"&&e.toLowerCase()!=="false"}import{createHash as _l,randomUUID as Co}from"node:crypto";import{createReadStream as vl}from"node:fs";import{isIP as $l}from"node:net";import{createHash as nl}from"node:crypto";import{appendFile as sl,mkdir as ol,readFile as il}from"node:fs/promises";import{dirname as al}from"node:path";var _o=268435456,vo=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(-_o),stderr:i.slice(-_o)}},Zg=["-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"],ep=["-o","ConnectTimeout=30","-o","ServerAliveInterval=15","-o","ServerAliveCountMax=4"];function $o(e){if(e.hostKey==="insecure")return[...Zg];return["-o",`StrictHostKeyChecking=${e.hostKey.policy==="pin"?"yes":"accept-new"}`,"-o",`UserKnownHostsFile=${e.hostKey.knownHostsFile}`,"-o","LogLevel=ERROR"]}function yo(e){let t=[...ep];if(e.connectTimeoutSec!==void 0)t[1]=`ConnectTimeout=${e.connectTimeoutSec}`;return t}function id(e,t,r=[]){return["-i",e.identityFile,"-p",String(e.port??22),...$o(e),...yo(e),"-o","BatchMode=yes",...r,`${e.user}@${t}`]}function tp(e,t,r,n){return["-i",e.identityFile,"-P",String(e.port??22),...$o(e),...yo(e),"-o","BatchMode=yes",r,`${e.user}@${t}:${n}`]}function Ct(e){return`SHA256:${nl("sha256").update(Buffer.from(e,"base64")).digest("base64").replace(/=+$/,"")}`}async function fi(e,t=22,r=vo){let n=await r(["ssh-keyscan","-T","10","-p",String(t),"-t","ed25519",e]),s=n.stdout.split(`
293
+ esac`,e)}async function Hc(e,t){let r=Buffer.from(t.join(`
294
+ `)).toString("base64");await ve(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 rd(e,t,r,n={recovered:!1}){if(await St(e.host,{...e,timeoutMs:e.sshTimeoutMs??600000}),!n.recovered)await kc(e);if(r)await Hc(e,r.runningServices);let s=await ed(e),i=[],a=r?_t(r.runningServices,s.runningServices):[],c=r?_t(r.releaseLinks,s.releaseLinks):[],f=r?_t(r.routeFragments,s.routeFragments):[],h=r?_t(r.routeIds,s.routeIds):[],g=r?.persistentData.map((j)=>j.split("=")[0])??[],v=s.persistentData.map((j)=>j.split("=")[0]),_=_t(g,v),b=r?_t(r.dataCatalog,s.dataCatalog):[],S=s.routeProbes.filter((j)=>!j.ok),P=n.recovered&&r?r.cpuCores:t.cores??0,M=n.recovered&&r?r.memoryBytes:Uc(t),q=s.cpuCores>=P,G=s.memoryBytes>=M*0.9,L=n.recovered||s.rootDiskBytes>=(t.disk??0)*1e9*0.95;if(s.failedUnits.length>0)i.push(`Failed services: ${s.failedUnits.join(", ")}`);if(a.length>0)i.push(`Services did not return: ${a.join(", ")}`);if(c.length>0)i.push(`Release links changed: ${c.join(", ")}`);if(f.length>0)i.push(`Route manifests changed: ${f.join(", ")}`);if(h.length>0)i.push(`Routes disappeared: ${h.join(", ")}`);if(_.length>0)i.push(`Persistent data paths disappeared: ${_.join(", ")}`);if(b.length>0)i.push(`Databases or volumes disappeared: ${b.join(", ")}`);if(S.length>0)i.push(`Unhealthy routes: ${S.map((j)=>j.domain).join(", ")}`);if(!q)i.push(`Expected at least ${P} CPU cores, found ${s.cpuCores}`);if(!G)i.push(`Expected at least ${M} bytes of RAM`);if(!L)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((j)=>j.ok).length,persistentDataPaths:s.persistentData.length,databasesAndVolumes:s.dataCatalog.length,failedUnits:s.failedUnits.length},failures:i}}import{mkdir as Nc,readFile as Mc,rename as Fc,writeFile as Bc}from"node:fs/promises";import{join as io}from"node:path";var nd="storage/cloud/state";function It(e=process.cwd()){return ml()?l(e,"state"):io(e,nd)}function Je(e,t=process.cwd()){return io(It(t),`${e}.json`)}async function Xe(e,t=process.cwd()){try{let r=await Mc(Je(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function bt(e,t,r=process.cwd()){let n=Je(e,r);await Nc(It(r),{recursive:!0});let s=`${n}.${process.pid}.tmp`;await Bc(s,`${JSON.stringify(t,null,2)}
295
+ `,"utf8"),await Fc(s,n)}async function tt(e){return Xe(e)}async function nt(e,t){await bt(e,t)}async function pi(e,t,r){let n=r.length?await e.listVolumes():[];return r.map((s)=>{let i=n.filter((h)=>h.name===s.name);if(i.length>1)throw Error(`Ambiguous volume name: ${s.name}`);let a=i[0];if(a&&a.server!==null&&a.server!==t.id)throw Error(`Volume ${a.name} belongs to another server; refusing to detach live storage`);let c=t.location?.name??t.datacenter?.location.name;if(a&&a.location.name!==c)throw Error(`Volume ${a.name} is in ${a.location.name}, server is in ${c}`);if(a&&a.size>s.size)throw Error(`Refusing to shrink volume ${a.name}: ${a.size} GB to ${s.size} GB`);let f=[];if(!a)f.push("create");else{if(a.server===null)f.push("attach");if(a.size<s.size)f.push("grow")}if(!a||a.protection.delete!==(s.deleteProtection!==!1))f.push("protect");return f.push("mount"),{declaration:s,existing:a,actions:f}})}async function mi(e,t,r){let n=[];for(let s of r){let i=s.existing,a=!i;if(!i){let c=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(c.action.id);for(let f of c.next_actions??[])await e.waitForAction(f.id);i=await e.getVolume(c.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:a})}return n}var Nn=268435456;function Wt(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function nl(e){let t=Qc("sha256");for await(let r of el(e))t.update(r);return t.digest("hex")}class xe{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=ri(t,r),i=In(n??{});if(i.appServers>1||i.dedicatedServices)throw Error("Managed compute volumes currently require a single app server");let a=await this.getComputeOutputs(e);if(!a.appInstanceId)throw Error("No existing compute server found; provision the server first");let c=await this.client.getServer(Number(a.appInstanceId)),f=await pi(this.client,c,s),h=!e.storageOnly&&n?.resize&&n.size?await tr(this.client,c.id,Ge(n.size),n.resize.upgradeDisk!==!1):void 0,g={serverId:String(c.id),changes:f.flatMap((S)=>S.actions.map((P)=>({resource:S.declaration.name,action:P,detail:`${S.declaration.size} GB at ${S.declaration.mountPath}`})))};if(n?.deploymentStorage)g.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(n?.persistentStorage)g.changes.push({resource:n.persistentStorage.volume,action:"place-persistent-data",detail:`Verify and migrate ${n.persistentStorage.paths.length} persistent directories with declared writers stopped during cutover`});if(h&&!h.alreadyComplete)if(!h.capacityAvailable){if(n?.resize?.whenUnavailable!=="keep-current")throw Error(`${h.targetType.name} is unavailable in ${h.location}`);g.waitingForCapacity=`${h.targetType.name} unavailable in ${h.location}; keeping ${c.server_type.name} running`}else g.changes.push({resource:c.name,action:"resize",detail:`${c.server_type.name} to ${h.targetType.name}`});if(e.dryRun)return g;if(g.waitingForCapacity)console.warn(`[ts-cloud] ${g.waitingForCapacity}`);let v=await mi(this.client,c,f),_=[{id:String(c.id),name:c.name,publicIp:c.public_net.ipv4?.ip}],b=[...qe(t.notifications),...n?.volumes||n?.deploymentStorage?ni({volumes:v,deploymentStorage:n?.deploymentStorage,persistentStorage:n?.persistentStorage,webRoot:"/var/www"}):[],...n?.monitoring!==!1?He(n?.monitoring):[]];if(b.length){let S=await this.runRemoteDeploy({targets:_,commands:b,timeoutSeconds:7200,comment:"ts-cloud apply configured compute storage"});if(!S.success)throw Error(S.error||S.perInstance.map((P)=>P.error||P.output).join(`
296
+ `))}if(h?.capacityAvailable&&!h.alreadyComplete){let S={host:_[0].publicIp,user:this.sshUser,identityFile:this.sshPrivateKeyPath},P=await gi({client:this.client,serverId:c.id,targetType:h.targetType.name,upgradeDisk:h.upgradeDisk,hooks:{preflight:()=>td(S),afterBoot:(M,q,G,L)=>rd(S,q,G,L)}});if(P.status!=="completed"&&P.status!=="already-complete")throw Error(`Resize did not complete: ${P.status}`)}return g}constructor(e={}){this.residency=e.residency;let t=Vt(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 Gr({apiToken:t??""});let r=to(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=m(t,r),a=`${n}-${r}-app`,c=s.runtime==="php"||!!s.php,f=In(s);if(s.reverseDns&&(f.dedicatedServices||f.appServers>1))throw Error("Reverse DNS currently requires a single owned app server");let h=t.cloud?.residency||this.residency,{assertComputeResidency:g}=await import("./chunk-cq34675f.js"),v=(A)=>{if(h)g(h,{country:A.location?.country||A.datacenter?.location?.country,location:A.location?.name||A.datacenter?.location?.name})};if(h){if(f.dedicatedServices||f.appServers>1)throw Error("Residency policy currently requires single-host compute");let A=await this.client.getLocation(t.hetzner?.location||this.location);g(h,{country:A.country,location:A.name})}if(f.dedicatedServices||f.appServers>1)return c?this.provisionFleet(e,f):this.provisionBunFleet(e,f);let _=ye(n,r,"app"),b=t.sites||{},S=z(s)?[]:this.collectUpstreamPorts(b),P=`${n}-${r}-app-fw`,{firewall:M}=await this.ensureFirewall(P,_,xr({allowSsh:s.allowSsh!==!1,sitePorts:S,allowedPorts:[...s.firewall?.allowedPorts??[],...Cs(t.infrastructure?.sftp),...rt(C(t,{environment:r}))]})),q=await this.ensureSshKey(n,r,_),G=await tt(i);if(G?.serverId){let A=await this.tryGetServer(G.serverId);if(A&&A.status!=="off"&&A.labels?.["ts-cloud/project"]===n&&A.labels?.["ts-cloud/environment"]===r){if(v(A),await this.reconcileReverseDns(e,A),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.reconcileOwnedServices(e,A),this.outputsFromState(G,A)}}let L=await this.findExistingServer(n,r,a);if(L&&L.status!=="off"){v(L);let A={provider:"hetzner",stackName:i,serverId:L.id,serverName:L.name,publicIp:L.public_net.ipv4?.ip,publicIpv6:ae(L.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};if(await nt(i,A),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.reconcileOwnedServices(e,L),await this.reconcileReverseDns(e,L),this.outputsFromState(A,L)}let j=R(t,{environment:r}),B=j?.engine==="rpx"?V({proxy:j,config:x(b,{proxy:j,slug:t.project.slug,mail:C(t,{environment:r})}),slug:t.project.slug,bunBin:s.runtime==="node"||s.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,Z=Ye(t,{environment:r}),ee=s.bakedImage===!0,Y=F({runtime:Z.runtime,runtimeVersion:Z.runtimeVersion,systemPackages:s.systemPackages,database:t.infrastructure?.database,phpProvision:Z.phpProvision,servicesProvision:Z.servicesProvision,rpxProvision:B,baked:ee,swapGb:s.swapGb}),se=ce(Y),re=Ge(s.size),oe=qt(t),{server:me,action:le}=await this.client.createServer({name:a,serverType:re,image:oe,location:t.hetzner?.location||this.location,userData:se,labels:_,sshKeys:q?[q]:void 0,firewalls:[{firewall:M.id}]});await this.client.waitForAction(le.id);let ie=await this.client.waitForServerRunning(me.id);v(ie),await this.reconcileReverseDns(e,ie);let E={provider:"hetzner",stackName:i,serverId:ie.id,serverName:ie.name,firewallId:M.id,publicIp:ie.public_net.ipv4?.ip,publicIpv6:ae(ie.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};await nt(i,E);let X=ie.public_net.ipv4?.ip;if(X&&this.waitForBoot)await this.waitForSshReady(X),await this.waitForCloudInit(X);if(s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(E,ie)}async provisionFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,a=m(r,n),c=Ge(i.size),f=qt(r),h=r.hetzner?.location||this.location,g=i.bakedImage===!0,v=await tt(a);if(v?.loadBalancerId){let I=(await this.client.listLoadBalancers().catch(()=>[])).find((O)=>O.id===v.loadBalancerId);if(I)return{appPublicIp:I.public_net?.ipv4?.ip,loadBalancerIp:I.public_net?.ipv4?.ip,servicesPrivateIp:v.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}let _=await this.ensureSshKey(s,n,ye(s,n,"app")),b=`${s}-${n}-net`,P=(await this.client.listNetworks().catch(()=>[])).find((w)=>w.name===b)??await this.client.createNetwork({name:b,labels:ye(s,n,"app")}),{firewall:M}=await this.ensureFirewall(`${s}-${n}-app-fw`,ye(s,n,"app"),xr({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),{firewall:q}=await this.ensureFirewall(`${s}-${n}-services-fw`,ye(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:[P.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[P.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[P.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[P.ip_range]}]),G=Ln(r),L=ce(F({runtime:"php",servicesProvision:G,baked:g,swapGb:i.swapGb})),j=await this.client.listServers().catch(()=>[]),B=[],Z=j.find((w)=>we(w.labels,s,n,"services"));if(!Z){let{server:w,action:I}=await this.client.createServer({name:`${s}-${n}-services`,serverType:Ge(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:f,location:h,userData:L,labels:ye(s,n,"services"),sshKeys:_?[_]:void 0,firewalls:[{firewall:q.id}],networks:[P.id]});await this.client.waitForAction(I.id),Z=await this.client.waitForServerRunning(w.id),B.push(w.id)}let ee=Z.id,Y=Z.private_net?.[0]?.ip??(await this.client.getServer(ee)).private_net?.[0]?.ip,se=[...Fe(!0),...He(!0),...lt(i.sshKeys),...qe(r.notifications)],re=ur({versions:i.php?.versions,default:i.php?.default,extensions:i.php?.extensions,installNginx:!z(i),optimizeForProduction:i.php?.optimizeForProduction,ini:i.php?.ini}),oe=ce(F({runtime:"php",phpProvision:re,servicesProvision:se,baked:g,swapGb:i.swapGb})),me=j.filter((w)=>we(w.labels,s,n,"app")),le=me.map((w)=>w.id);if(me.length>t.appServers)for(let w of me.slice(t.appServers))await this.client.deleteServer(w.id).catch(()=>{}),le.splice(le.indexOf(w.id),1);for(let w=me.length;w<t.appServers;w++){let{server:I,action:O}=await this.client.createServer({name:`${s}-${n}-app-${w+1}`,serverType:c,image:f,location:h,userData:oe,labels:ye(s,n,"app"),sshKeys:_?[_]:void 0,firewalls:[{firewall:M.id}],networks:[P.id]});await this.client.waitForAction(O.id),le.push(I.id),B.push(I.id)}if(this.waitForBoot&&B.length>0)await Promise.all(B.map(async(w)=>{let O=(await this.client.waitForServerRunning(w)).public_net.ipv4?.ip;if(O)await this.waitForSshReady(O),await this.waitForCloudInit(O)}));let ie=`${s}-${n}-lb`,E,X;if(t.loadBalancer){let I=(await this.client.listLoadBalancers().catch(()=>[])).find((O)=>O.name===ie)??await this.client.createLoadBalancer({name:ie,location:h,network:P.id,labels:ye(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}]});X=I.id,E=I.public_net?.ipv4?.ip}let A=E??(await this.client.getServer(le[0]).catch(()=>{return}))?.public_net.ipv4?.ip,T={provider:"hetzner",stackName:a,networkId:P.id,loadBalancerId:X,servicesServerId:ee,servicesPrivateIp:Y,publicIp:A,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await nt(a,T),{appPublicIp:A,loadBalancerIp:E,servicesPrivateIp:Y,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,a=m(r,n),c=Ge(i.size),f=qt(r),h=r.hetzner?.location||this.location,g=i.bakedImage===!0,v=r.sites||{},_=await tt(a);if(_?.lbServerId){let O=await this.tryGetServer(_.lbServerId);if(O&&O.status!=="off"){let N=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"},te=await this.findComputeTargets({slug:s,environment:n,role:"app",stackName:a});return this.refreshLbRoutes(O,v,te.map((K)=>({privateIp:K.privateIp,publicIp:K.publicIp})),N,s),{appPublicIp:O.public_net.ipv4?.ip??_.publicIp,loadBalancerIp:O.public_net.ipv4?.ip??_.publicIp,servicesPrivateIp:_.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}}let b=await this.ensureSshKey(s,n,ye(s,n,"app")),S=`${s}-${n}-net`,M=(await this.client.listNetworks().catch(()=>[])).find((O)=>O.name===S)??await this.client.createNetwork({name:S,labels:ye(s,n,"app")}),q=this.collectUpstreamPorts(v),{firewall:G}=await this.ensureFirewall(`${s}-${n}-app-fw`,ye(s,n,"app"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},...q.map((O)=>({direction:"in",protocol:"tcp",port:String(O),source_ips:[M.ip_range],description:`ts-cloud port ${O} (private, LB-only)`}))]),{firewall:L}=await this.ensureFirewall(`${s}-${n}-lb-fw`,ye(s,n,"lb"),xr({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),j=await this.client.listServers().catch(()=>[]),B=[],Z,ee,Y=!!i.servicesServer||!!i.managedServices,se=t.dedicatedServices&&Y;if(se){let{firewall:O}=await this.ensureFirewall(`${s}-${n}-services-fw`,ye(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:[M.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[M.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[M.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[M.ip_range]}]),N=Ln(r),te=ce(F({runtime:"bun",servicesProvision:N,baked:g,swapGb:i.swapGb})),K=j.find((J)=>we(J.labels,s,n,"services"));if(!K){let{server:J,action:ne}=await this.client.createServer({name:`${s}-${n}-services`,serverType:Ge(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:f,location:h,userData:te,labels:ye(s,n,"services"),sshKeys:b?[b]:void 0,firewalls:[{firewall:O.id}],networks:[M.id]});await this.client.waitForAction(ne.id),K=await this.client.waitForServerRunning(J.id),B.push(J.id)}Z=K.id,ee=K.private_net?.[0]?.ip??(await this.client.getServer(Z)).private_net?.[0]?.ip}let re=se?{...i,managedServices:void 0,backups:void 0}:i,oe=Ye({...r,infrastructure:{...r.infrastructure,compute:re}},{environment:n}),me=ce(F({runtime:oe.runtime,runtimeVersion:oe.runtimeVersion,systemPackages:i.systemPackages,database:r.infrastructure?.database,servicesProvision:oe.servicesProvision,baked:g,swapGb:i.swapGb})),le=j.filter((O)=>we(O.labels,s,n,"app")),ie=le.map((O)=>O.id);if(le.length>t.appServers)for(let O of le.slice(t.appServers))await this.client.deleteServer(O.id).catch(()=>{}),ie.splice(ie.indexOf(O.id),1);for(let O=le.length;O<t.appServers;O++){let{server:N,action:te}=await this.client.createServer({name:`${s}-${n}-app-${O+1}`,serverType:c,image:f,location:h,userData:me,labels:ye(s,n,"app"),sshKeys:b?[b]:void 0,firewalls:[{firewall:G.id}],networks:[M.id]});await this.client.waitForAction(te.id),ie.push(N.id),B.push(N.id)}if(this.waitForBoot&&B.length>0)await Promise.all(B.map(async(O)=>{let te=(await this.client.waitForServerRunning(O)).public_net.ipv4?.ip;if(te)await this.waitForSshReady(te),await this.waitForCloudInit(te)}));let E=[];for(let O of ie){let N=j.find((te)=>te.id===O);if(!N||!N.private_net?.[0]?.ip)N=await this.client.getServer(O).catch(()=>N);E.push({privateIp:N?.private_net?.[0]?.ip,publicIp:N?.public_net.ipv4?.ip})}let X=`${s}-${n}-lb`,A,T;if(t.loadBalancer){let O=j.find((te)=>we(te.labels,s,n,"lb")),N=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"};if(!O){let te=V({proxy:N,config:nr(v,E,{proxy:N,slug:s}),slug:s,bunBin:oe.runtime==="node"||oe.runtime==="deno"?void 0:"/usr/local/bin/bun"}),K=ce(F({runtime:"bun",rpxProvision:te,baked:!1})),J=typeof i.loadBalancer==="object"?i.loadBalancer.size:void 0,{server:ne,action:D}=await this.client.createServer({name:X,serverType:Ge(J??"micro"),image:f,location:h,userData:K,labels:ye(s,n,"lb"),sshKeys:b?[b]:void 0,firewalls:[{firewall:L.id}],networks:[M.id]});if(await this.client.waitForAction(D.id),O=await this.client.waitForServerRunning(ne.id),this.waitForBoot){let k=O.public_net.ipv4?.ip;if(k)await this.waitForSshReady(k),await this.waitForCloudInit(k)}}else this.refreshLbRoutes(O,v,E,N,s);A=O.id,T=O.public_net.ipv4?.ip}let w=T??(await this.client.getServer(ie[0]).catch(()=>{return}))?.public_net.ipv4?.ip,I={provider:"hetzner",stackName:a,networkId:M.id,lbServerId:A,appServerIds:ie,servicesServerId:Z,servicesPrivateIp:ee,publicIp:w,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await nt(a,I),{appPublicIp:w,loadBalancerIp:T,servicesPrivateIp:ee,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=m(t,r),i=await tt(s),a=[],c=`${n}-${r}-lb`,h=(await this.client.listLoadBalancers().catch(()=>[])).find((G)=>G.name===c);if(h)try{await this.client.deleteLoadBalancer(h.id),a.push(`load balancer ${c}`)}catch{}let g=await this.client.listServers().catch(()=>[]),v=new Set;for(let G of g)if(we(G.labels,n,r,"app")||we(G.labels,n,r,"services")||we(G.labels,n,r,"lb"))v.add(G.id);if(i?.serverId)v.add(i.serverId);if(i?.servicesServerId)v.add(i.servicesServerId);if(i?.lbServerId)v.add(i.lbServerId);for(let G of i?.appServerIds??[])v.add(G);for(let G of v)try{await this.client.deleteServer(G),a.push(`server ${G}`)}catch{}let _=await this.client.listFirewalls().catch(()=>[]);for(let G of[`${n}-${r}-app-fw`,`${n}-${r}-services-fw`,`${n}-${r}-lb-fw`]){let L=_.find((j)=>j.name===G);if(!L)continue;for(let j=0;j<12;j++)try{await this.client.deleteFirewall(L.id),a.push(`firewall ${G}`);break}catch{await this.sleep(3000)}}let b=`${n}-${r}-net`,P=(await this.client.listNetworks().catch(()=>[])).find((G)=>G.name===b);if(P)for(let G=0;G<12;G++)try{await this.client.deleteNetwork(P.id),a.push(`network ${b}`);break}catch{await this.sleep(3000)}let q=(await this.client.listSshKeys().catch(()=>[])).filter((G)=>kn(G.labels,n,r));for(let G of q)try{await this.client.deleteSshKey(G.id),a.push(`ssh key ${G.name}`)}catch{}return await nt(s,{provider:"hetzner",stackName:s}).catch(()=>{}),{destroyed:a}}async getComputeOutputs(e){let t=m(e.config,e.environment),r=await tt(t);if(r?.serverId){let i=await this.tryGetServer(r.serverId);if(i)return this.outputsFromState(r,i)}if(r?.lbServerId){let a=(await this.tryGetServer(r.lbServerId))?.public_net.ipv4?.ip??r.publicIp;return{...this.outputsFromState(r),appPublicIp:a,loadBalancerIp:a}}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:m(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:m(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,"-")}-${vo()}.tar.gz`}`,i=await nl(e.localPath),a=Ur,c=`${a}/${i}.tar.gz`;for(let f of t){if(!f.publicIp)throw Error(`Target ${f.id} has no public IP for SCP upload`);let h=["set -euo pipefail",`mkdir -p ${Wt(a)} /var/ts-cloud/staging`,`test -s ${Wt(c)}`,`cp -- ${Wt(c)} ${Wt(s)}`].join(`
297
+ `);try{this.sshExec(f.publicIp,h);continue}catch{}let g=`${a}/.${i}-${vo()}.tmp`;try{this.scpToHost(f.publicIp,e.localPath,g),this.sshExec(f.publicIp,Cr(g,c,s))}catch(v){try{this.sshExec(f.publicIp,Pr(g))}catch{}throw v}}return{artifactRef:s}}async findComputeTargets(e){let{assertComputeResidency:t}=await import("./chunk-cq34675f.js"),r=e.role||"app",n=(a)=>{if(this.residency)t(this.residency,{country:a.location?.country||a.datacenter?.location?.country,location:a.location?.name||a.datacenter?.location?.name});return{id:String(a.id),name:a.name,publicIp:a.public_net.ipv4?.ip,publicIpv6:ae(a.public_net.ipv6?.ip),privateIp:a.private_net?.[0]?.ip,status:a.status}};if(r==="app"||r==="lb"||r==="services"){let a=await tt(e.stackName??`${e.slug}-${e.environment}`),c=[...new Set(r==="app"?[a?.serverId,...a?.appServerIds??[]]:r==="lb"?[a?.lbServerId]:[a?.servicesServerId])].filter((g)=>typeof g==="number");if(this.stateOnly){if(this.residency)throw Error("Residency requires fresh provider evidence; a local state pin alone is insufficient");if(c.length>0&&a?.publicIp)return c.map((g)=>({id:String(g),name:g===a.serverId?a.serverName:void 0,publicIp:a.publicIp,publicIpv6:a.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 f=await this.client.listServers();if(c.length>0){let g=[];for(let v of c){let _=f.find((b)=>b.id===v)??await this.tryGetServer(v);if(v===a?.serverId&&a.serverName&&_?.name!==a.serverName)continue;if(_&&_.status!=="off")g.push(_)}if(g.length>0)return g.map(n)}let h=f.filter((g)=>we(g.labels,e.slug,e.environment,r));if(h.length>0)return h.map(n);if(r==="app"){let g=f.filter((v)=>v.status!=="off"&&v.labels?.[`${Se}/managed-by`]==="ts-cloud"&&v.labels?.[`${Se}/environment`]===e.environment&&v.labels?.[`${Se}/role`]==="app");if(g.length===1)return g.map(n)}return[]}if(this.stateOnly)throw Error(`Hetzner API token required to discover '${r}' compute targets.`);let i=(await this.client.listServers()).filter((a)=>we(a.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=(_)=>n.filter((b)=>b.status!=="off"&&we(b.labels,e,r,_)),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 a=s("lb")[0],c=s("services")[0],f=i[0],h=a??f,g=m(t,r),v={provider:"hetzner",stackName:g,serverId:f.id,serverName:f.name,appServerIds:i.map((_)=>_.id),lbServerId:a?.id,servicesServerId:c?.id,servicesPrivateIp:c?.private_net?.[0]?.ip,publicIp:h.public_net.ipv4?.ip,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await nt(g,v),this.outputsFromState(v)}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(`
298
+ `),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);_r(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:Zt(r,"One or more SSH deploy commands failed")}}async ensureSshKey(e,t,r){if(!tl(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=rl(this.sshPublicKeyPath,"utf8").trim(),s=We(n),a=(await this.client.listSshKeys()).find((f)=>We(f.public_key)===s);if(a)return a.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 a=r.filter((f)=>f.privateIp||f.publicIp);if(a.length===0){console.warn(`[ts-cloud] No routable app boxes found for LB '${e.name}' — leaving its current rpx routes untouched.`);return}let c=nr(t,a,{proxy:n,slug:s});this.sshExec(i,_e({config:c,slug:s}).join(`
299
+ `))}async findExistingServer(e,t,r){let n=await this.client.listServers(),s=n.find((a)=>we(a.labels,e,t,"app")||a.name===r);if(s)return s;let i=n.filter((a)=>a.status!=="off"&&a.labels?.[`${Se}/managed-by`]==="ts-cloud"&&a.labels?.[`${Se}/environment`]===t&&a.labels?.[`${Se}/role`]==="app");if(i.length===1){let a=i[0],c=a.labels?.[`${Se}/project`]??"unknown";return console.warn(`[ts-cloud] No server named '${r}' found; adopting existing ts-cloud app server '${a.name}' (project label '${c}') for project '${e}' — `+"updating it in place instead of provisioning a new server."),a}return}async ensureFirewall(e,t,r){let s=(await this.client.listFirewalls()).find((a)=>a.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{So(`ssh ${this.sshBaseArgs(e,["-o","ConnectTimeout=5"]).map(Wt).join(" ")} true`,{stdio:"pipe",maxBuffer:Nn});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}:
300
+ ${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:ae(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,...xe.SSH_HOST_KEY_OPTS,...xe.SSH_KEEPALIVE_OPTS,"-o","BatchMode=yes",...t,`${this.sshUser}@${e}`]}scpToHost(e,t,r){let n=["scp",...Gt,"-i",this.sshPrivateKeyPath,...xe.SSH_HOST_KEY_OPTS,...xe.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{So(n,{stdio:"pipe",maxBuffer:Nn});return}catch(a){if(i===3)throw Error(Yt(a))}}reconcileOwnedServices(e,t){let r=Ps(e.config,e.environment);if(!r.length)return;let n=t.public_net.ipv4?.ip;if(!n)throw Error("The owned server needs a reachable address to reconcile operational services");this.sshExec(n,["set -e",...r].join(`
301
+ `))}async reconcileReverseDns(e,t){if(e.config.cloud?.attachTo)return;for(let r of no(t,e.config.infrastructure?.compute?.reverseDns)){let n=await this.client.changeServerReverseDns(t.id,r.ip,r.hostname);await this.client.waitForAction(n.id)}}sshExec(e,t){try{return Gc("ssh",[...this.sshBaseArgs(e),Xc],{encoding:"utf8",input:t,stdio:["pipe","pipe","pipe"],maxBuffer:Nn})}catch(r){throw Error(`${Yt(r)}${ti(r,t)}`)}}}class lo{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(`
302
+ `);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 uo(){let e=process.env.TS_CLOUD_DASHBOARD_BOX;return e!=null&&e!==""&&e!=="0"&&e.toLowerCase()!=="false"}import{createHash as yl,randomUUID as Po}from"node:crypto";import{createReadStream as _l}from"node:fs";import{isIP as $l}from"node:net";import{createHash as sl}from"node:crypto";import{appendFile as ol,mkdir as il,readFile as al}from"node:fs/promises";import{dirname as cl}from"node:path";var yo=268435456,_o=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(-yo),stderr:i.slice(-yo)}},Zg=["-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"],ep=["-o","ConnectTimeout=30","-o","ServerAliveInterval=15","-o","ServerAliveCountMax=4"];function $o(e){if(e.hostKey==="insecure")return[...Zg];return["-o",`StrictHostKeyChecking=${e.hostKey.policy==="pin"?"yes":"accept-new"}`,"-o",`UserKnownHostsFile=${e.hostKey.knownHostsFile}`,"-o","LogLevel=ERROR"]}function bo(e){let t=[...ep];if(e.connectTimeoutSec!==void 0)t[1]=`ConnectTimeout=${e.connectTimeoutSec}`;return t}function id(e,t,r=[]){return["-i",e.identityFile,"-p",String(e.port??22),...$o(e),...bo(e),"-o","BatchMode=yes",...r,`${e.user}@${t}`]}function tp(e,t,r,n){return["-i",e.identityFile,"-P",String(e.port??22),...$o(e),...bo(e),"-o","BatchMode=yes",r,`${e.user}@${t}:${n}`]}function Ct(e){return`SHA256:${sl("sha256").update(Buffer.from(e,"base64")).digest("base64").replace(/=+$/,"")}`}async function fi(e,t=22,r=_o){let n=await r(["ssh-keyscan","-T","10","-p",String(t),"-t","ed25519",e]),s=n.stdout.split(`
292
303
  `).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 sd(e,t=22){return t===22?e:`[${e}]:${t}`}function rp(e,t,r){return`${sd(e,t)} ${r.algorithm} ${r.key}`}function np(e,t,r=22){let n=sd(t,r);for(let s of e.split(`
293
- `)){let i=s.trim();if(!i||i.startsWith("#")||i.startsWith("@"))continue;let[a,c,f]=i.split(/\s+/);if(!a||!c||!f)continue;if(a.split(",").includes(n))return{algorithm:c,key:f}}return null}function od(e=process.cwd()){return l(e,"ssh","known_hosts")}async function cl(e){try{return await il(e,"utf8")}catch{return""}}async function ll(e,t){await ol(al(e),{recursive:!0}),await sl(e,`${t}
294
- `,{mode:384})}class Et{options;run;scan;readKnownHosts;appendKnownHosts;constructor(e,t={}){this.options=e;this.run=t.run??vo,this.scan=t.scan??fi,this.readKnownHosts=t.readKnownHosts??cl,this.appendKnownHosts=t.appendKnownHosts??ll}runnerFor(e={}){if(e.sudo!==void 0)return ui({sudo:e.sudo});return this.options.runner??ui()}async exec(e,t,r={}){let n=await this.run(["ssh",...id(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(`${Yt(s)}${ti(s,t)}`)}return n.stdout}async scp(e,t,r){let n=["scp",...Gt,...tp(this.options,e,t,r)],s=3;for(let i=1;i<=3;i++){let a=await this.run(n);if(a.code===0)return;if(i===3)throw Error(Yt({status:a.code,signal:a.signal,stdout:a.stdout,stderr:a.stderr}))}}async probe(e){return(await this.run(["ssh",...id(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:Ct((await this.scan(e,t,this.run)).key),pinnedNow:!1}}catch{return{fingerprint:"",pinnedNow:!1}}let{policy:r,knownHostsFile:n}=this.options.hostKey,s=np(await this.readKnownHosts(n),e,t);if(s)return{fingerprint:Ct(s.key),pinnedNow:!1};let i=await this.scan(e,t,this.run);if(r==="pin")return await this.appendKnownHosts(n,rp(e,t,i)),{fingerprint:Ct(i.key),pinnedNow:!0};return{fingerprint:Ct(i.key),pinnedNow:!1}}}import{isIP as ul}from"node:net";var Oe=1,ad="/var/lib/ts-cloud";function ip(e=Oe){return`${ad}/bootstrap.v${e}`}var sp=1;function pl(e){return e==="aarch64"||e==="arm64"}function bo(e){let t=e.lanIp?.trim()||e.facts?.lanIp?.trim();return t&&ul(t)!==0?t:void 0}function go(e,t,r){if(!Lt(e.infrastructure?.compute?.managedServices?.vitess))return;if(t==="raspberry-pi"||pl(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 st(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function dl(){return`
304
+ `)){let i=s.trim();if(!i||i.startsWith("#")||i.startsWith("@"))continue;let[a,c,f]=i.split(/\s+/);if(!a||!c||!f)continue;if(a.split(",").includes(n))return{algorithm:c,key:f}}return null}function od(e=process.cwd()){return l(e,"ssh","known_hosts")}async function ll(e){try{return await al(e,"utf8")}catch{return""}}async function ul(e,t){await il(cl(e),{recursive:!0}),await ol(e,`${t}
305
+ `,{mode:384})}class Et{options;run;scan;readKnownHosts;appendKnownHosts;constructor(e,t={}){this.options=e;this.run=t.run??_o,this.scan=t.scan??fi,this.readKnownHosts=t.readKnownHosts??ll,this.appendKnownHosts=t.appendKnownHosts??ul}runnerFor(e={}){if(e.sudo!==void 0)return ui({sudo:e.sudo});return this.options.runner??ui()}async exec(e,t,r={}){let n=await this.run(["ssh",...id(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(`${Yt(s)}${ti(s,t)}`)}return n.stdout}async scp(e,t,r){let n=["scp",...Gt,...tp(this.options,e,t,r)],s=3;for(let i=1;i<=3;i++){let a=await this.run(n);if(a.code===0)return;if(i===3)throw Error(Yt({status:a.code,signal:a.signal,stdout:a.stdout,stderr:a.stderr}))}}async probe(e){return(await this.run(["ssh",...id(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:Ct((await this.scan(e,t,this.run)).key),pinnedNow:!1}}catch{return{fingerprint:"",pinnedNow:!1}}let{policy:r,knownHostsFile:n}=this.options.hostKey,s=np(await this.readKnownHosts(n),e,t);if(s)return{fingerprint:Ct(s.key),pinnedNow:!1};let i=await this.scan(e,t,this.run);if(r==="pin")return await this.appendKnownHosts(n,rp(e,t,i)),{fingerprint:Ct(i.key),pinnedNow:!0};return{fingerprint:Ct(i.key),pinnedNow:!1}}}import{isIP as pl}from"node:net";var Oe=1,ad="/var/lib/ts-cloud";function ip(e=Oe){return`${ad}/bootstrap.v${e}`}var sp=1;function dl(e){return e==="aarch64"||e==="arm64"}function To(e){let t=e.lanIp?.trim()||e.facts?.lanIp?.trim();return t&&pl(t)!==0?t:void 0}function go(e,t,r){if(!Lt(e.infrastructure?.compute?.managedServices?.vitess))return;if(t==="raspberry-pi"||dl(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 st(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function fl(){return`
295
306
  # A Pi 5 has no battery-backed RTC and boots in 1970 until NTP answers. apt
296
307
  # rejects Release files "from the future" and ACME rejects a skewed clock, so
297
308
  # wait (bounded) for systemd-timesyncd before anything touches the network
@@ -307,7 +318,7 @@ if command -v timedatectl >/dev/null 2>&1; then
307
318
  systemctl start systemd-time-wait-sync >/dev/null 2>&1 || true
308
319
  fi
309
320
  fi
310
- `}function fl(){return`
321
+ `}function hl(){return`
311
322
  # SD cards wear; a journal allowed to grow to the default 10% of the disk is
312
323
  # the single biggest writer on an idle Pi. Bound it.
313
324
  install -d -m 0755 /etc/systemd/journald.conf.d
@@ -322,27 +333,27 @@ systemctl restart systemd-journald
322
333
  # The deploy user uploads releases as itself (scp), while the scripts run as
323
334
  # root through sudo. setgid group-writable directories let both write.
324
335
  install -d -m 2775 -g "$(id -gn ${st(e)})" /var/ts-cloud/artifacts /var/ts-cloud/staging
325
- `}function hl(e){let t=st(e),r=`/home/${e}`;return[`chown -R ${t}:"$(id -gn ${t})" ${st(`${r}/.ssh`)}`,`chmod 700 ${st(`${r}/.ssh`)}`]}function hi(e){let{config:t,environment:r,profile:n,sudoUser:s}=e;go(t,n,e.facts?.arch);let i={...t.infrastructure?.compute,proxy:R(t,{environment:e.environment,environmentType:e.environment})},a=t.sites??{},c=t.project.slug,f=e.lan?{...e.lan.hostname?{hostname:e.lan.hostname}:{},tls:e.lan.tls??"local-ca",...bo(e)?{ip:bo(e)}:{}}:void 0,g=i.proxy?.engine==="rpx"?x(a,{proxy:i.proxy,slug:c,lan:f,mail:C(t,{environment:e.environment,environmentType:e.environment})}):void 0,h=i.proxy?.engine==="rpx"&&g?V({proxy:i.proxy,config:g,slug:c,profile:n,bunBin:i.runtime==="node"||i.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,S=i.proxy?.engine==="rpx"&&g?_e({config:g,slug:c}):void 0,_=s?`/home/${s}/.ssh/authorized_keys`:void 0,v=Ye(t,{environment:r,authorizedKeysPath:_}),b=s?[...v.servicesProvision??[],...hl(s)]:v.servicesProvision,T=[...new Set([...i.systemPackages??[],"psmisc","ca-certificates"])],L=i.swapGb??(n==="raspberry-pi"?sp:void 0),K=F({runtime:v.runtime,runtimeVersion:v.runtimeVersion,systemPackages:T,database:t.infrastructure?.database,phpProvision:v.phpProvision,servicesProvision:b,rpxProvision:h,swapGb:L}).replace(/^#!\/bin\/bash\nset -euo pipefail\n/,""),M=ip(),D=g?.localCa?` lan=${g.localCa.hosts.join(",")} tls=local-ca`:f?` lan=${f.hostname??`${c}.local`} tls=${f.tls??"local-ca"}`:"",N=`#!/bin/bash
336
+ `}function Sl(e){let t=st(e),r=`/home/${e}`;return[`chown -R ${t}:"$(id -gn ${t})" ${st(`${r}/.ssh`)}`,`chmod 700 ${st(`${r}/.ssh`)}`]}function hi(e){let{config:t,environment:r,profile:n,sudoUser:s}=e;go(t,n,e.facts?.arch);let i={...t.infrastructure?.compute,proxy:R(t,{environment:e.environment,environmentType:e.environment})},a=t.sites??{},c=t.project.slug,f=e.lan?{...e.lan.hostname?{hostname:e.lan.hostname}:{},tls:e.lan.tls??"local-ca",...To(e)?{ip:To(e)}:{}}:void 0,h=i.proxy?.engine==="rpx"?x(a,{proxy:i.proxy,slug:c,lan:f,mail:C(t,{environment:e.environment,environmentType:e.environment})}):void 0,g=i.proxy?.engine==="rpx"&&h?V({proxy:i.proxy,config:h,slug:c,profile:n,bunBin:i.runtime==="node"||i.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,v=i.proxy?.engine==="rpx"&&h?_e({config:h,slug:c}):void 0,_=s?`/home/${s}/.ssh/authorized_keys`:void 0,b=Ye(t,{environment:r,authorizedKeysPath:_}),S=s?[...b.servicesProvision??[],...Sl(s)]:b.servicesProvision,P=[...new Set([...i.systemPackages??[],"psmisc","ca-certificates"])],M=i.swapGb??(n==="raspberry-pi"?sp:void 0),q=F({runtime:b.runtime,runtimeVersion:b.runtimeVersion,systemPackages:P,database:t.infrastructure?.database,phpProvision:b.phpProvision,servicesProvision:S,rpxProvision:g,swapGb:M}).replace(/^#!\/bin\/bash\nset -euo pipefail\n/,""),G=ip(),L=h?.localCa?` lan=${h.localCa.hosts.join(",")} tls=local-ca`:f?` lan=${f.hostname??`${c}.local`} tls=${f.tls??"local-ca"}`:"",j=`#!/bin/bash
326
337
  set -euo pipefail
327
- # ts-cloud ssh bootstrap v${Oe} profile=${n}${D}
338
+ # ts-cloud ssh bootstrap v${Oe} profile=${n}${L}
328
339
 
329
340
  # Idempotence guard. This script may run again (a cloud-init rerun, a repeated
330
341
  # adopt, a deploy): once this version has been applied, only the gateway's
331
342
  # route fragment is refreshed, so a rerun cannot re-download the runtime or
332
343
  # re-create swap on every boot.
333
- if [ -e ${st(M)} ]; then
344
+ if [ -e ${st(G)} ]; then
334
345
  echo "[ts-cloud] bootstrap v${Oe} already applied; refreshing the gateway fragment only"
335
- `;if(S){let k=S[0]==="set -euo pipefail"?S.slice(1):S;N+=`${k.join(`
346
+ `;if(v){let B=v[0]==="set -euo pipefail"?v.slice(1):v;j+=`${B.join(`
336
347
  `)}
337
- `}if(N+=` exit 0
348
+ `}if(j+=` exit 0
338
349
  fi
339
- `,N+=dl(),n==="raspberry-pi")N+=fl();if(s)N+=gl(s);if(N+=`
340
- ${K}`,N+=`
350
+ `,j+=fl(),n==="raspberry-pi")j+=hl();if(s)j+=gl(s);if(j+=`
351
+ ${q}`,j+=`
341
352
  install -d -m 0755 ${st(ad)}
342
- date -u +%Y-%m-%dT%H:%M:%SZ > ${st(M)}
353
+ date -u +%Y-%m-%dT%H:%M:%SZ > ${st(G)}
343
354
  echo "[ts-cloud] bootstrap v${Oe} applied (${n}); gateway fragment at ${W}/${c}.json"
344
- `,g?.localCa)N+=`echo "[ts-cloud] LAN certificate authority for ${g.localCa.hosts.join(", ")} at ${Io(g.localCa.dir)} (copy it to a client to trust this box)"
345
- `;return N}var vt={user:"root",port:22,privateKeyPath:"~/.ssh/id_ed25519",hostKey:"pin",profile:"generic",publicIp:"auto"},To=["pin","accept-new","insecure"],wo=["raspberry-pi","generic"];function $t(...e){for(let t of e){let r=process.env[t]?.trim();if(r)return r}return}function Xt(...e){for(let t of e){let r=t?.trim();if(r)return r}return}function op(e,t){if(t?.length)return t;if(e?.ssh?.hosts?.length)return e.ssh.hosts;let r=$t("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 ap(e,t){return Xt(t,e?.user,$t("TS_CLOUD_SSH_USER"))??vt.user}function cp(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:$t("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 vt.port}function dp(e,t){return Rr(Xt(t,e?.privateKeyPath,$t("TS_CLOUD_SSH_KEY"))??vt.privateKeyPath)}function lp(e,t){let r=Xt(t,e?.ssh?.hostKey,$t("TS_CLOUD_SSH_HOST_KEY"))??vt.hostKey;if(!To.includes(r))throw Error(`Invalid ssh.hostKey '${r}'. Use one of: ${To.join(", ")}.`);return r}function up(e,t=vt.user,r){return r??e?.ssh?.sudo??t!=="root"}function gp(e,t){let r=Xt(t,e?.ssh?.profile,$t("TS_CLOUD_SSH_PROFILE"))??vt.profile;if(!wo.includes(r))throw Error(`Invalid ssh.profile '${r}'. Use one of: ${wo.join(", ")}.`);return r}function pp(e,t){return Xt(t,e?.ssh?.publicIp)??vt.publicIp}function mp(e,t){return t??e?.ssh?.lan}function Ai(e,t={}){let r=op(e,t.hosts).map((n)=>({host:n.host,user:ap(n,t.user),port:cp(n,t.port),privateKeyPath:dp(n,t.privateKeyPath),role:n.role??"app"}));return{hosts:r,hostKey:lp(e,t.hostKey),sudo:up(e,r[0]?.user,t.sudo),profile:gp(e,t.profile),publicIp:pp(e,t.publicIp),lan:mp(e,t.lan)}}var yi=`set -u
355
+ `,h?.localCa)j+=`echo "[ts-cloud] LAN certificate authority for ${h.localCa.hosts.join(", ")} at ${Io(h.localCa.dir)} (copy it to a client to trust this box)"
356
+ `;return j}var vt={user:"root",port:22,privateKeyPath:"~/.ssh/id_ed25519",hostKey:"pin",profile:"generic",publicIp:"auto"},wo=["pin","accept-new","insecure"],Co=["raspberry-pi","generic"];function $t(...e){for(let t of e){let r=process.env[t]?.trim();if(r)return r}return}function Xt(...e){for(let t of e){let r=t?.trim();if(r)return r}return}function op(e,t){if(t?.length)return t;if(e?.ssh?.hosts?.length)return e.ssh.hosts;let r=$t("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 ap(e,t){return Xt(t,e?.user,$t("TS_CLOUD_SSH_USER"))??vt.user}function cp(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:$t("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 vt.port}function dp(e,t){return Rr(Xt(t,e?.privateKeyPath,$t("TS_CLOUD_SSH_KEY"))??vt.privateKeyPath)}function lp(e,t){let r=Xt(t,e?.ssh?.hostKey,$t("TS_CLOUD_SSH_HOST_KEY"))??vt.hostKey;if(!wo.includes(r))throw Error(`Invalid ssh.hostKey '${r}'. Use one of: ${wo.join(", ")}.`);return r}function up(e,t=vt.user,r){return r??e?.ssh?.sudo??t!=="root"}function gp(e,t){let r=Xt(t,e?.ssh?.profile,$t("TS_CLOUD_SSH_PROFILE"))??vt.profile;if(!Co.includes(r))throw Error(`Invalid ssh.profile '${r}'. Use one of: ${Co.join(", ")}.`);return r}function pp(e,t){return Xt(t,e?.ssh?.publicIp)??vt.publicIp}function mp(e,t){return t??e?.ssh?.lan}function Ai(e,t={}){let r=op(e,t.hosts).map((n)=>({host:n.host,user:ap(n,t.user),port:cp(n,t.port),privateKeyPath:dp(n,t.privateKeyPath),role:n.role??"app"}));return{hosts:r,hostKey:lp(e,t.hostKey),sudo:up(e,r[0]?.user,t.sudo),profile:gp(e,t.profile),publicIp:pp(e,t.publicIp),lan:mp(e,t.lan)}}var yi=`set -u
346
357
  . /etc/os-release 2>/dev/null || true
347
358
  # Strings are stripped of quotes, backslashes and newlines rather than escaped:
348
359
  # nothing reported here legitimately contains them, and a probe that cannot
@@ -376,21 +387,21 @@ printf '"privilege":"%s"' "$(sudo -n true >/dev/null 2>&1 && printf sudo || prin
376
387
  printf '}\\n'
377
388
  `;function Si(e){let t=e.split(`
378
389
  `).map((r)=>r.trim()).filter(Boolean).at(-1);if(!t||!t.startsWith("{"))throw Error(`Preflight returned no facts:
379
- ${e.trim().slice(-2000)}`);try{return JSON.parse(t)}catch(r){throw Error(`Preflight returned malformed facts: ${r.message}`)}}var Tt=1073741824;function Sl(e){return e==="aarch64"||e==="arm64"}function Ii(e,t){let r=[],n=e.arch??"";if(!Sl(n)&&!(n==="x86_64"&&t.profile==="generic"))r.push({code:"arch.not-aarch64",severity:"error",message:t.profile==="raspberry-pi"?`The raspberry-pi profile expects a 64-bit ARM OS; this host reports ${n||"an unknown architecture"}.`:`Unsupported architecture ${n||"(unknown)"}.`,remediation:t.profile==="raspberry-pi"?"Flash the 64-bit Raspberry Pi OS or Ubuntu Server arm64 image.":"Use an x86_64 or arm64 Linux host."});let s=`${e.osId??""} ${e.osIdLike??""}`.toLowerCase();if(!/\b(?:debian|ubuntu|raspbian)\b/.test(s))r.push({code:"os.unsupported",severity:"error",message:`The bootstrap targets Debian and Ubuntu (apt); this host reports ${e.os||"an unknown OS"}.`,remediation:"Install Raspberry Pi OS, Debian or Ubuntu Server."});let i=e.memoryBytes??0;if(i<Tt)r.push({code:"memory.low",severity:"error",message:`${(i/Tt).toFixed(2)} GiB of memory; the runtime plus a gateway needs at least 1 GiB.`,remediation:"Use a host with 2 GiB or more."});else if(i<2*Tt)r.push({code:"memory.low",severity:"warning",message:`${(i/Tt).toFixed(2)} GiB of memory; on-box builds and managed services will be tight.`,remediation:"Build releases in CI rather than on the host, and keep managedServices small."});if((e.diskFreeBytes??0)<4*Tt)r.push({code:"disk.low",severity:"error",message:`${((e.diskFreeBytes??0)/Tt).toFixed(1)} GiB free on /; releases, the artifact cache and swap need at least 4 GiB.`,remediation:"Free space or use a larger card or disk."});if(t.user!=="root"&&!e.sudoOk)r.push({code:"sudo.missing",severity:"error",message:`${t.user} cannot run sudo without a password, and the bootstrap needs root.`,remediation:`Add '${t.user} ALL=(ALL) NOPASSWD:ALL' to /etc/sudoers.d/ts-cloud, or deploy as root.`});if(!e.timeSynced)r.push({code:"time.unsynced",severity:"warning",message:"The clock is not NTP-synchronised yet; the bootstrap waits for it before running apt.",remediation:"Nothing to do unless it never syncs: then check the network and systemd-timesyncd."});if(!e.httpsOk)r.push({code:"https.blocked",severity:"error",message:"The host cannot reach https://github.com, so it cannot download the runtime or packages.",remediation:"Check DNS, the default route and any egress firewall from the host."});if(e.cloudInitPresent&&/running/i.test(e.cloudInitStatus??""))r.push({code:"cloud-init.running",severity:"info",message:"cloud-init is still running its first boot; the adopt waits for it to finish."});if(e.piModel)r.push({code:"pi.model",severity:"info",message:e.piModel});if(e.kernelBits===32)r.push({code:"bits.32",severity:"error",message:"This is a 32-bit userland; bun ships no 32-bit build.",remediation:"Flash a 64-bit OS image."});return r}function bi(e){return e.some((t)=>t.severity==="error")}function Ci(e){if(e.length===0)return"No findings.";let t=Math.max(...e.map((r)=>r.code.length));return e.map((r)=>{let n=`${r.severity.padEnd(7)} ${r.code.padEnd(t)} ${r.message}`;return r.remediation?`${n}
390
+ ${e.trim().slice(-2000)}`);try{return JSON.parse(t)}catch(r){throw Error(`Preflight returned malformed facts: ${r.message}`)}}var Tt=1073741824;function vl(e){return e==="aarch64"||e==="arm64"}function Ii(e,t){let r=[],n=e.arch??"";if(!vl(n)&&!(n==="x86_64"&&t.profile==="generic"))r.push({code:"arch.not-aarch64",severity:"error",message:t.profile==="raspberry-pi"?`The raspberry-pi profile expects a 64-bit ARM OS; this host reports ${n||"an unknown architecture"}.`:`Unsupported architecture ${n||"(unknown)"}.`,remediation:t.profile==="raspberry-pi"?"Flash the 64-bit Raspberry Pi OS or Ubuntu Server arm64 image.":"Use an x86_64 or arm64 Linux host."});let s=`${e.osId??""} ${e.osIdLike??""}`.toLowerCase();if(!/\b(?:debian|ubuntu|raspbian)\b/.test(s))r.push({code:"os.unsupported",severity:"error",message:`The bootstrap targets Debian and Ubuntu (apt); this host reports ${e.os||"an unknown OS"}.`,remediation:"Install Raspberry Pi OS, Debian or Ubuntu Server."});let i=e.memoryBytes??0;if(i<Tt)r.push({code:"memory.low",severity:"error",message:`${(i/Tt).toFixed(2)} GiB of memory; the runtime plus a gateway needs at least 1 GiB.`,remediation:"Use a host with 2 GiB or more."});else if(i<2*Tt)r.push({code:"memory.low",severity:"warning",message:`${(i/Tt).toFixed(2)} GiB of memory; on-box builds and managed services will be tight.`,remediation:"Build releases in CI rather than on the host, and keep managedServices small."});if((e.diskFreeBytes??0)<4*Tt)r.push({code:"disk.low",severity:"error",message:`${((e.diskFreeBytes??0)/Tt).toFixed(1)} GiB free on /; releases, the artifact cache and swap need at least 4 GiB.`,remediation:"Free space or use a larger card or disk."});if(t.user!=="root"&&!e.sudoOk)r.push({code:"sudo.missing",severity:"error",message:`${t.user} cannot run sudo without a password, and the bootstrap needs root.`,remediation:`Add '${t.user} ALL=(ALL) NOPASSWD:ALL' to /etc/sudoers.d/ts-cloud, or deploy as root.`});if(!e.timeSynced)r.push({code:"time.unsynced",severity:"warning",message:"The clock is not NTP-synchronised yet; the bootstrap waits for it before running apt.",remediation:"Nothing to do unless it never syncs: then check the network and systemd-timesyncd."});if(!e.httpsOk)r.push({code:"https.blocked",severity:"error",message:"The host cannot reach https://github.com, so it cannot download the runtime or packages.",remediation:"Check DNS, the default route and any egress firewall from the host."});if(e.cloudInitPresent&&/running/i.test(e.cloudInitStatus??""))r.push({code:"cloud-init.running",severity:"info",message:"cloud-init is still running its first boot; the adopt waits for it to finish."});if(e.piModel)r.push({code:"pi.model",severity:"info",message:e.piModel});if(e.kernelBits===32)r.push({code:"bits.32",severity:"error",message:"This is a 32-bit userland; bun ships no 32-bit build.",remediation:"Flash a 64-bit OS image."});return r}function bi(e){return e.some((t)=>t.severity==="error")}function Ci(e){if(e.length===0)return"No findings.";let t=Math.max(...e.map((r)=>r.code.length));return e.map((r)=>{let n=`${r.severity.padEnd(7)} ${r.code.padEnd(t)} ${r.message}`;return r.remediation?`${n}
380
391
  ${" ".repeat(t+10)}${r.remediation}`:n}).join(`
381
- `)}var Ei="/var/ts-cloud/staging",Mn=Ur,fp="TS_CLOUD_SSH_SKIP_BOOTSTRAP",yl=["size","instances","appServers","servicesServer","loadBalancer","fleet","image","bakedImage","allowSsh"];function Jt(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function bl(e){let t=_l("sha256");for await(let r of vl(e))t.update(r);return t.digest("hex")}function hp(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(Lt(e.infrastructure?.compute?.managedServices?.vitess))go(e,t.profile);let r=e.infrastructure?.compute??{};return yl.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 Vr{name="ssh";usesCloudFormation=!1;settings;cwd;bootWait;transportInstance;constructor(e={}){this.settings=Ai(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 Et({user:e.user,port:e.port,identityFile:e.privateKeyPath,hostKey:this.settings.hostKey==="insecure"?"insecure":{policy:this.settings.hostKey,knownHostsFile:od(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:Ei}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:m(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,"-")}-${Co()}.tar.gz`,s=`${Ei}/${n}`,i=await bl(e.localPath),a=`${Mn}/${i}.tar.gz`;for(let c of t){if(!c.publicIp)throw Error(`Target ${c.id} has no address for SCP upload`);let f=["set -euo pipefail",`mkdir -p ${Jt(Mn)} ${Jt(Ei)}`,`test -s ${Jt(a)}`,`cp -- ${Jt(a)} ${Jt(s)}`].join(`
382
- `);try{await this.exec(c.publicIp,f);continue}catch{}let g=`${Mn}/.${i}-${Co()}.tmp`;try{await this.transport.scp(c.publicIp,e.localPath,g),await this.exec(c.publicIp,Cr(g,a,s))}catch(h){throw await this.exec(c.publicIp,Pr(g)).catch(()=>{}),h}}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(`
383
- `),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);_r(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:Zt(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,yi,{sudo:!1}),r=Si(t);return{facts:r,findings:Ii(r,{user:this.host.user,profile:this.settings.profile})}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e;for(let b of hp(t,this.settings))console.warn(`[ts-cloud] ${b}`);let n=this.host,s=m(t,r),i=await Xe(s,this.cwd),a=process.env[fp]==="1"&&i?.provider==="ssh"&&i.host===n.host&&i.bootstrapVersion===Oe,c=await this.ensureHostKey(n.host);if(c.pinnedNow)console.warn(`[ts-cloud] pinned ${n.host} host key ${c.fingerprint}`);await this.waitForSshReady(n.host);let{facts:f,findings:g}=await this.preflight(n.host);if(bi(g))throw Error(`Preflight failed for ${n.host}; refusing to bootstrap it.
384
- ${Ci(g)}`);for(let b of g)console.warn(`[ts-cloud] ${b.severity}: ${b.message}`);if(f.cloudInitPresent&&/running/i.test(f.cloudInitStatus??""))await this.waitForCloudInit(n.host);let h=f.lanIp?.trim()||($l(n.host)!==0?n.host:void 0),S=i?.provider==="ssh"?i.bootstrappedAt:void 0;if(!a){let b=hi({config:t,environment:r,profile:this.settings.profile,facts:f,sudoUser:n.user!=="root"?n.user:void 0,lan:this.settings.lan,lanIp:h});_r(await this.exec(n.host,b)),S=new Date().toISOString()}let _={provider:"ssh",stackName:s,host:n.host,sshUser:n.user,sshPort:n.port,hostKeyFingerprint:c.fingerprint||void 0,publicIp:this.settings.publicIp==="auto"?void 0:this.settings.publicIp,lanIp:f.lanIp||void 0,deployStoragePath:Ei,profile:this.settings.profile,bootstrapVersion:Oe,bootstrappedAt:S};await bt(s,_,this.cwd);let v=await this.getComputeOutputs();if(z(t.infrastructure?.compute)&&Sd(this.settings.lan)==="local-ca")v.lanCaCertPath=Io(),console.warn(`[ts-cloud] LAN certificate authority on ${n.host} at ${v.lanCaCertPath}. Fetch it with: scp ${n.user}@${n.host}:${v.lanCaCertPath} ./rpx-root-ca.crt`);return v}async destroyCompute(e){let t=m(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 bt(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}:
385
- ${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 U(e){let t=e.provider??ge(e.config);if(e.config.cloud?.residency&&(uo()||t!=="hetzner"))throw Error("Residency policy requires a Hetzner host with live provider evidence");if(uo())return new lo;switch(e.provider??ge(e.config)){case"aws":return new Mr({region:e.config.project.region});case"hetzner":return new xe({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 Vr({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??ge(e.config)}`)}}class po{drivers=new Map;getDriver(e,t){let r=t??ge(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=U({config:e,provider:r});return this.drivers.set(n,i),i}}var Ap=new po;var yp="/var/lib/ts-cloud/bootstrap.sh",Sp={"raspberry-pi-os":"bootfs",ubuntu:"system-boot"},Tl=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/,wl=/^[a-z_][a-z0-9_-]{0,31}$/,Cl=/^[A-Z]{2}$/;function Pe(e){return JSON.stringify(e)}function Pl(e){if(!Tl.test(e.hostname))throw Error(`Invalid hostname '${e.hostname}': use lowercase letters, digits and hyphens.`);if(!wl.test(e.user))throw Error(`Invalid user '${e.user}': use a lowercase Unix user name.`);let t=e.publicKey.trim();if(!t||t.includes(`
386
- `)||!/^(?: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(!Cl.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 El(e,t){let r=[`hostname: ${Pe(e.hostname)}`,"manage_etc_hosts: true","users:",` - name: ${Pe(e.user)}`," ssh_authorized_keys:",` - ${Pe(e.publicKey.trim())}`,` sudo: ${Pe("ALL=(ALL) NOPASSWD:ALL")}`," shell: /bin/bash"," lock_passwd: true","ssh_pwauth: false"];if(e.timezone)r.push(`timezone: ${Pe(e.timezone)}`);if(e.locale)r.push(`locale: ${Pe(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(`
392
+ `)}var Ei="/var/ts-cloud/staging",Mn=Ur,fp="TS_CLOUD_SSH_SKIP_BOOTSTRAP",bl=["size","instances","appServers","servicesServer","loadBalancer","fleet","image","bakedImage","allowSsh"];function Jt(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function Tl(e){let t=yl("sha256");for await(let r of _l(e))t.update(r);return t.digest("hex")}function hp(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(Lt(e.infrastructure?.compute?.managedServices?.vitess))go(e,t.profile);let r=e.infrastructure?.compute??{};return bl.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 Vr{name="ssh";usesCloudFormation=!1;settings;cwd;bootWait;transportInstance;constructor(e={}){this.settings=Ai(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 Et({user:e.user,port:e.port,identityFile:e.privateKeyPath,hostKey:this.settings.hostKey==="insecure"?"insecure":{policy:this.settings.hostKey,knownHostsFile:od(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:Ei}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:m(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,"-")}-${Po()}.tar.gz`,s=`${Ei}/${n}`,i=await Tl(e.localPath),a=`${Mn}/${i}.tar.gz`;for(let c of t){if(!c.publicIp)throw Error(`Target ${c.id} has no address for SCP upload`);let f=["set -euo pipefail",`mkdir -p ${Jt(Mn)} ${Jt(Ei)}`,`test -s ${Jt(a)}`,`cp -- ${Jt(a)} ${Jt(s)}`].join(`
393
+ `);try{await this.exec(c.publicIp,f);continue}catch{}let h=`${Mn}/.${i}-${Po()}.tmp`;try{await this.transport.scp(c.publicIp,e.localPath,h),await this.exec(c.publicIp,Cr(h,a,s))}catch(g){throw await this.exec(c.publicIp,Pr(h)).catch(()=>{}),g}}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(`
394
+ `),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);_r(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:Zt(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,yi,{sudo:!1}),r=Si(t);return{facts:r,findings:Ii(r,{user:this.host.user,profile:this.settings.profile})}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e;for(let S of hp(t,this.settings))console.warn(`[ts-cloud] ${S}`);let n=this.host,s=m(t,r),i=await Xe(s,this.cwd),a=process.env[fp]==="1"&&i?.provider==="ssh"&&i.host===n.host&&i.bootstrapVersion===Oe,c=await this.ensureHostKey(n.host);if(c.pinnedNow)console.warn(`[ts-cloud] pinned ${n.host} host key ${c.fingerprint}`);await this.waitForSshReady(n.host);let{facts:f,findings:h}=await this.preflight(n.host);if(bi(h))throw Error(`Preflight failed for ${n.host}; refusing to bootstrap it.
395
+ ${Ci(h)}`);for(let S of h)console.warn(`[ts-cloud] ${S.severity}: ${S.message}`);if(f.cloudInitPresent&&/running/i.test(f.cloudInitStatus??""))await this.waitForCloudInit(n.host);let g=f.lanIp?.trim()||($l(n.host)!==0?n.host:void 0),v=i?.provider==="ssh"?i.bootstrappedAt:void 0;if(!a){let S=hi({config:t,environment:r,profile:this.settings.profile,facts:f,sudoUser:n.user!=="root"?n.user:void 0,lan:this.settings.lan,lanIp:g});_r(await this.exec(n.host,S)),v=new Date().toISOString()}let _={provider:"ssh",stackName:s,host:n.host,sshUser:n.user,sshPort:n.port,hostKeyFingerprint:c.fingerprint||void 0,publicIp:this.settings.publicIp==="auto"?void 0:this.settings.publicIp,lanIp:f.lanIp||void 0,deployStoragePath:Ei,profile:this.settings.profile,bootstrapVersion:Oe,bootstrappedAt:v};await bt(s,_,this.cwd);let b=await this.getComputeOutputs();if(z(t.infrastructure?.compute)&&Sd(this.settings.lan)==="local-ca")b.lanCaCertPath=Io(),console.warn(`[ts-cloud] LAN certificate authority on ${n.host} at ${b.lanCaCertPath}. Fetch it with: scp ${n.user}@${n.host}:${b.lanCaCertPath} ./rpx-root-ca.crt`);return b}async destroyCompute(e){let t=m(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 bt(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}:
396
+ ${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 U(e){let t=e.provider??ge(e.config);if(e.config.cloud?.residency&&(uo()||t!=="hetzner"))throw Error("Residency policy requires a Hetzner host with live provider evidence");if(uo())return new lo;switch(e.provider??ge(e.config)){case"aws":return new Mr({region:e.config.project.region});case"hetzner":return new xe({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 Vr({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??ge(e.config)}`)}}class po{drivers=new Map;getDriver(e,t){let r=t??ge(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=U({config:e,provider:r});return this.drivers.set(n,i),i}}var Ap=new po;var yp="/var/lib/ts-cloud/bootstrap.sh",Sp={"raspberry-pi-os":"bootfs",ubuntu:"system-boot"},wl=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/,Cl=/^[a-z_][a-z0-9_-]{0,31}$/,Pl=/^[A-Z]{2}$/;function Pe(e){return JSON.stringify(e)}function El(e){if(!wl.test(e.hostname))throw Error(`Invalid hostname '${e.hostname}': use lowercase letters, digits and hyphens.`);if(!Cl.test(e.user))throw Error(`Invalid user '${e.user}': use a lowercase Unix user name.`);let t=e.publicKey.trim();if(!t||t.includes(`
397
+ `)||!/^(?: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(!Pl.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 Rl(e,t){let r=[`hostname: ${Pe(e.hostname)}`,"manage_etc_hosts: true","users:",` - name: ${Pe(e.user)}`," ssh_authorized_keys:",` - ${Pe(e.publicKey.trim())}`,` sudo: ${Pe("ALL=(ALL) NOPASSWD:ALL")}`," shell: /bin/bash"," lock_passwd: true","ssh_pwauth: false"];if(e.timezone)r.push(`timezone: ${Pe(e.timezone)}`);if(e.locale)r.push(`locale: ${Pe(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(`
387
398
  `)}
388
- `}function Rl(e){let t=["version: 2","ethernets:"," eth0:"," match:",` name: ${Pe("e*")}`," dhcp4: true"," dhcp6: true"," optional: true"];if(e.wifi)t.push("wifis:"," wlan0:"," dhcp4: true"," dhcp6: true"," optional: true",` regulatory-domain: ${Pe(e.wifi.country)}`," access-points:",` ${Pe(e.wifi.ssid)}:`,` password: ${Pe(e.wifi.passphrase)}`);return`${t.join(`
399
+ `}function Ol(e){let t=["version: 2","ethernets:"," eth0:"," match:",` name: ${Pe("e*")}`," dhcp4: true"," dhcp6: true"," optional: true"];if(e.wifi)t.push("wifis:"," wlan0:"," dhcp4: true"," dhcp6: true"," optional: true",` regulatory-domain: ${Pe(e.wifi.country)}`," access-points:",` ${Pe(e.wifi.ssid)}:`,` password: ${Pe(e.wifi.passphrase)}`);return`${t.join(`
389
400
  `)}
390
- `}function Ol(e){return[`instance-id: ${Pe(`ts-cloud-${e.hostname}`)}`,`local-hostname: ${Pe(e.hostname)}`].join(`
401
+ `}function Dl(e){return[`instance-id: ${Pe(`ts-cloud-${e.hostname}`)}`,`local-hostname: ${Pe(e.hostname)}`].join(`
391
402
  `)+`
392
- `}function Dl(e,t){return[`Copy user-data, network-config and meta-data onto the boot partition of the flashed card (the volume named '${Sp[t]}'), replacing the files already there.`,"Eject the card, put it in the board and power it on.","The first boot takes a few minutes: cloud-init creates the user, waits for the clock to sync, then runs the ts-cloud bootstrap (packages, runtime, gateway).",`Then, from this machine: cloud ssh:preflight ${e.hostname}.local --user ${e.user}`,"When the preflight is clean, cloud deploy adopts the host and ships the sites."].join(`
393
- `)}function cd(e,t,r){Pl(e);let n={scriptPath:yp,permissions:"0700",before:El(e,r.os)};return{files:{"user-data":ce(t,n),"network-config":Rl(e),"meta-data":Ol(e)},instructions:Dl(e,r.os)}}function Po(e){return e.infrastructure?.compute??{}}function Ip(e){let t=Po(e),r=t.monitoring;return{firewallPorts:[...new Set([22,80,443,...t.firewall?.allowedPorts??[]])].filter((s)=>Number.isInteger(s)&&s>=1&&s<=65535).sort((s,i)=>s-i),monitoring:r!==!1&&(typeof r!=="object"||r.enabled!==!1),autoUpdates:t.autoUpdates!==!1,swapGb:Math.max(0,Math.floor(t.swapGb??2)),sshPasswordAuthentication:!1,sshMaxStartups:"100:30:200",journalMaxUse:"256M",journalRetention:"14day"}}function xl(e){return["install -d -m 0755 /etc/ssh/sshd_config.d /etc/systemd/journald.conf.d /etc/sysctl.d /etc/fail2ban/jail.d","cat > /etc/ssh/sshd_config.d/99-ts-cloud-hardening.conf <<'TS_CLOUD_SSH_EOF'","# Managed by ts-cloud host optimization.","PasswordAuthentication no","KbdInteractiveAuthentication no","PermitRootLogin prohibit-password","MaxAuthTries 4","LoginGraceTime 30",`MaxStartups ${e.sshMaxStartups}`,"TS_CLOUD_SSH_EOF","sshd -t","systemctl reload ssh","cat > /etc/systemd/journald.conf.d/99-ts-cloud-retention.conf <<'TS_CLOUD_JOURNAL_EOF'","[Journal]",`SystemMaxUse=${e.journalMaxUse}`,`MaxRetentionSec=${e.journalRetention}`,"Compress=yes","TS_CLOUD_JOURNAL_EOF","systemctl restart systemd-journald","journalctl --vacuum-size=256M --vacuum-time=14d >/dev/null","cat > /etc/sysctl.d/99-ts-cloud-production.conf <<'TS_CLOUD_SYSCTL_EOF'","# Managed by ts-cloud host optimization.","vm.swappiness=10","vm.dirty_background_ratio=5","vm.dirty_ratio=15","net.core.somaxconn=4096","net.ipv4.tcp_max_syn_backlog=4096","net.ipv4.tcp_fin_timeout=30","net.ipv4.tcp_keepalive_time=300","net.ipv4.tcp_keepalive_intvl=30","net.ipv4.tcp_keepalive_probes=5","fs.inotify.max_user_watches=524288","TS_CLOUD_SYSCTL_EOF","sysctl --system >/dev/null","export DEBIAN_FRONTEND=noninteractive","command -v fail2ban-client >/dev/null 2>&1 || apt-get install -y fail2ban","cat > /etc/fail2ban/jail.d/99-ts-cloud-sshd.conf <<'TS_CLOUD_FAIL2BAN_EOF'","[sshd]","enabled = true","backend = systemd","bantime = 1h","findtime = 10m","maxretry = 5","TS_CLOUD_FAIL2BAN_EOF","systemctl enable --now fail2ban","systemctl restart fail2ban"]}function Al(e){if(e===0)return[];return["if ! swapon --show=NAME --noheadings 2>/dev/null | grep -qx '/swapfile'; then",` if [ ! -f /swapfile ]; then fallocate -l ${e}G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=${e*1024} status=none; fi`," chmod 600 /swapfile"," mkswap /swapfile >/dev/null"," swapon /swapfile","fi","grep -qE '^/swapfile\\s' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab"]}function bp(e){let t=Po(e),r=Ip(e),n=t.monitoring??!0,s=t.firewall??{enabled:!0};return["set -euo pipefail",...z(t)?["systemctl disable --now nginx.service bun-gateway.service ts-cloud-nginx.service 2>/dev/null || true","systemctl reset-failed nginx.service bun-gateway.service ts-cloud-nginx.service 2>/dev/null || true"]:[],...Al(r.swapGb),...ze(r.autoUpdates),...He(n),...xl(r),...pr(s),"systemctl daemon-reload","systemctl reset-failed"]}var Il=String.raw`
403
+ `}function xl(e,t){return[`Copy user-data, network-config and meta-data onto the boot partition of the flashed card (the volume named '${Sp[t]}'), replacing the files already there.`,"Eject the card, put it in the board and power it on.","The first boot takes a few minutes: cloud-init creates the user, waits for the clock to sync, then runs the ts-cloud bootstrap (packages, runtime, gateway).",`Then, from this machine: cloud ssh:preflight ${e.hostname}.local --user ${e.user}`,"When the preflight is clean, cloud deploy adopts the host and ships the sites."].join(`
404
+ `)}function cd(e,t,r){El(e);let n={scriptPath:yp,permissions:"0700",before:Rl(e,r.os)};return{files:{"user-data":ce(t,n),"network-config":Ol(e),"meta-data":Dl(e)},instructions:xl(e,r.os)}}function Eo(e){return e.infrastructure?.compute??{}}function Ip(e){let t=Eo(e),r=t.monitoring;return{firewallPorts:[...new Set([22,80,443,...t.firewall?.allowedPorts??[]])].filter((s)=>Number.isInteger(s)&&s>=1&&s<=65535).sort((s,i)=>s-i),monitoring:r!==!1&&(typeof r!=="object"||r.enabled!==!1),autoUpdates:t.autoUpdates!==!1,swapGb:Math.max(0,Math.floor(t.swapGb??2)),sshPasswordAuthentication:!1,sshMaxStartups:"100:30:200",journalMaxUse:"256M",journalRetention:"14day"}}function Al(e){return["install -d -m 0755 /etc/ssh/sshd_config.d /etc/systemd/journald.conf.d /etc/sysctl.d /etc/fail2ban/jail.d","cat > /etc/ssh/sshd_config.d/99-ts-cloud-hardening.conf <<'TS_CLOUD_SSH_EOF'","# Managed by ts-cloud host optimization.","PasswordAuthentication no","KbdInteractiveAuthentication no","PermitRootLogin prohibit-password","MaxAuthTries 4","LoginGraceTime 30",`MaxStartups ${e.sshMaxStartups}`,"TS_CLOUD_SSH_EOF","sshd -t","systemctl reload ssh","cat > /etc/systemd/journald.conf.d/99-ts-cloud-retention.conf <<'TS_CLOUD_JOURNAL_EOF'","[Journal]",`SystemMaxUse=${e.journalMaxUse}`,`MaxRetentionSec=${e.journalRetention}`,"Compress=yes","TS_CLOUD_JOURNAL_EOF","systemctl restart systemd-journald","journalctl --vacuum-size=256M --vacuum-time=14d >/dev/null","cat > /etc/sysctl.d/99-ts-cloud-production.conf <<'TS_CLOUD_SYSCTL_EOF'","# Managed by ts-cloud host optimization.","vm.swappiness=10","vm.dirty_background_ratio=5","vm.dirty_ratio=15","net.core.somaxconn=4096","net.ipv4.tcp_max_syn_backlog=4096","net.ipv4.tcp_fin_timeout=30","net.ipv4.tcp_keepalive_time=300","net.ipv4.tcp_keepalive_intvl=30","net.ipv4.tcp_keepalive_probes=5","fs.inotify.max_user_watches=524288","TS_CLOUD_SYSCTL_EOF","sysctl --system >/dev/null","export DEBIAN_FRONTEND=noninteractive","command -v fail2ban-client >/dev/null 2>&1 || apt-get install -y fail2ban","cat > /etc/fail2ban/jail.d/99-ts-cloud-sshd.conf <<'TS_CLOUD_FAIL2BAN_EOF'","[sshd]","enabled = true","backend = systemd","bantime = 1h","findtime = 10m","maxretry = 5","TS_CLOUD_FAIL2BAN_EOF","systemctl enable --now fail2ban","systemctl restart fail2ban"]}function Il(e){if(e===0)return[];return["if ! swapon --show=NAME --noheadings 2>/dev/null | grep -qx '/swapfile'; then",` if [ ! -f /swapfile ]; then fallocate -l ${e}G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=${e*1024} status=none; fi`," chmod 600 /swapfile"," mkswap /swapfile >/dev/null"," swapon /swapfile","fi","grep -qE '^/swapfile\\s' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab"]}function bp(e){let t=Eo(e),r=Ip(e),n=t.monitoring??!0,s=t.firewall??{enabled:!0};return["set -euo pipefail",...z(t)?["systemctl disable --now nginx.service bun-gateway.service ts-cloud-nginx.service 2>/dev/null || true","systemctl reset-failed nginx.service bun-gateway.service ts-cloud-nginx.service 2>/dev/null || true"]:[],...Il(r.swapGb),...Fe(r.autoUpdates),...He(n),...Al(r),...pr(s),"systemctl daemon-reload","systemctl reset-failed"]}var Ll=String.raw`
394
405
  const text = (command) => {
395
406
  const result = Bun.spawnSync(['bash', '-lc', command], { stdout: 'pipe', stderr: 'pipe' })
396
407
  return result.stdout.toString().trim()
@@ -421,39 +432,39 @@ console.log(JSON.stringify({
421
432
  failedUnits: lines("systemctl --failed --type=service --no-legend --plain | awk '{print $1}'"),
422
433
  publicTcpPorts: [...new Set(publicTcpPorts)].sort((a, b) => a - b),
423
434
  }))
424
- `;function Ll(e){return`printf %s ${Buffer.from(e).toString("base64")} | base64 -d | bun -`}async function rA(e,t){let r=bp(e).join(`
425
- `),n=`printf %s ${Buffer.from(r).toString("base64")} | base64 -d | bash`;await ve(t.host,n,t)}async function nA(e){let t=await ve(e.host,Ll(Il),e);return JSON.parse(t)}function iA(e,t){let r=[],n=new Set(t.firewallPorts),s=e.firewallPorts.filter((a)=>!n.has(a)),i=t.firewallPorts.filter((a)=>!e.firewallPorts.includes(a));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 Ir,readFile as Ul,rename as Eo,rmdir as kl,stat as Hl,unlink as Nl,writeFile as Ro}from"node:fs/promises";import{join as kr}from"node:path";function dd(e){return kr(It(),`${e}-resize.json`)}function Cp(e){return kr(It(),`${e}-resize.lock`)}async function sA(e){try{return JSON.parse(await Ul(dd(e),"utf8"))}catch{return null}}async function oA(e){let t=dd(e.stackName);await Ir(It(),{recursive:!0});let r=`${t}.${process.pid}.tmp`;await Ro(r,`${JSON.stringify(e,null,2)}
426
- `,"utf8"),await Eo(r,t)}async function aA(e,t=1800000){let r=Cp(e);await Ir(It(),{recursive:!0});try{await Ir(r)}catch(n){if(Date.now()-(await Hl(r)).mtimeMs<=t)throw Error(`Another resize process holds ${r}.`);await Eo(r,`${r}.stale-${Date.now()}`),await Ir(r)}return await Ro(kr(r,"owner.json"),`${JSON.stringify({pid:process.pid,acquiredAt:new Date().toISOString()},null,2)}
427
- `,"utf8"),async()=>{await Nl(kr(r,"owner.json")).catch(()=>{}),await kl(r).catch(()=>{})}}async function vi(e,t){let r=We(t.publicKey),n=(await e.listSshKeys()).find((i)=>We(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 Ti(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 wi(e,t){let{waitForRunning:r=!0,...n}=t,s=(await e.listServers()).find((a)=>a.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 Ri(e){let t=e.public_net.ipv4?.ip;if(!t)throw Error(`Hetzner server "${e.name}" (#${e.id}) has no public IPv4`);return t}var ld="/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id";function ud(e){let t=e.sshPublicKey.trim(),r=["#cloud-config","disable_root: false","ssh_authorized_keys:",` - ${t}`];if(e.bootstrapScript){let s=e.bootstrapScript.split(`
435
+ `;function Ul(e){return`printf %s ${Buffer.from(e).toString("base64")} | base64 -d | bun -`}async function rA(e,t){let r=bp(e).join(`
436
+ `),n=`printf %s ${Buffer.from(r).toString("base64")} | base64 -d | bash`;await ve(t.host,n,t)}async function nA(e){let t=await ve(e.host,Ul(Ll),e);return JSON.parse(t)}function iA(e,t){let r=[],n=new Set(t.firewallPorts),s=e.firewallPorts.filter((a)=>!n.has(a)),i=t.firewallPorts.filter((a)=>!e.firewallPorts.includes(a));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 Ir,readFile as kl,rename as Ro,rmdir as Hl,stat as Nl,unlink as Ml,writeFile as Oo}from"node:fs/promises";import{join as kr}from"node:path";function dd(e){return kr(It(),`${e}-resize.json`)}function Cp(e){return kr(It(),`${e}-resize.lock`)}async function sA(e){try{return JSON.parse(await kl(dd(e),"utf8"))}catch{return null}}async function oA(e){let t=dd(e.stackName);await Ir(It(),{recursive:!0});let r=`${t}.${process.pid}.tmp`;await Oo(r,`${JSON.stringify(e,null,2)}
437
+ `,"utf8"),await Ro(r,t)}async function aA(e,t=1800000){let r=Cp(e);await Ir(It(),{recursive:!0});try{await Ir(r)}catch(n){if(Date.now()-(await Nl(r)).mtimeMs<=t)throw Error(`Another resize process holds ${r}.`);await Ro(r,`${r}.stale-${Date.now()}`),await Ir(r)}return await Oo(kr(r,"owner.json"),`${JSON.stringify({pid:process.pid,acquiredAt:new Date().toISOString()},null,2)}
438
+ `,"utf8"),async()=>{await Ml(kr(r,"owner.json")).catch(()=>{}),await Hl(r).catch(()=>{})}}async function vi(e,t){let r=We(t.publicKey),n=(await e.listSshKeys()).find((i)=>We(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 Ti(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 wi(e,t){let{waitForRunning:r=!0,...n}=t,s=(await e.listServers()).find((a)=>a.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 Ri(e){let t=e.public_net.ipv4?.ip;if(!t)throw Error(`Hetzner server "${e.name}" (#${e.id}) has no public IPv4`);return t}var ld="/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id";function ud(e){let t=e.sshPublicKey.trim(),r=["#cloud-config","disable_root: false","ssh_authorized_keys:",` - ${t}`];if(e.bootstrapScript){let s=e.bootstrapScript.split(`
428
439
  `).map((i)=>` ${i}`).join(`
429
440
  `);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(`
430
441
  `)}
431
- `}function Ml(e=[]){let t=[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"],description:"SSH"}];for(let r of e)t.push({direction:"in",protocol:r.protocol,...r.protocol==="icmp"?{}:{port:String(r.port)},source_ips:["0.0.0.0/0","::/0"]});return t}class Di{client;provider="hetzner";constructor(e){this.client=e}async ensureBox(e){let t=await vi(this.client,{name:`${e.name}-key`,publicKey:e.sshPublicKey}),r=await Ti(this.client,{name:`${e.name}-fw`,rules:Ml(e.ports)}),{server:n,created:s}=await wi(this.client,{name:e.name,serverType:e.size,image:e.image??"ubuntu-24.04",location:e.location,sshKeys:[t.id],firewalls:[{firewall:r.id}],userData:ud(e),labels:e.labels});if(!s){if(!((await this.client.listFirewalls()).find((c)=>c.id===r.id)?.applied_to??[]).some((c)=>c.type==="server"&&c.server?.id===n.id)){let c=await this.client.applyFirewallToResources(r.id,[{type:"server",server:n.id}]);for(let f of c)await this.client.waitForAction(f.id).catch(()=>{})}}return{provider:"hetzner",id:String(n.id),name:n.name,publicIp:Ri(n),created:s}}async destroyBox(e){let t=[],r=(await this.client.listServers()).find((s)=>s.name===e);if(r){let s=await this.client.deleteServer(r.id);await this.client.waitForAction(s.id).catch(()=>{}),t.push(`server ${e} (#${r.id})`)}let n=(await this.client.listFirewalls()).find((s)=>s.name===`${e}-fw`);if(n)for(let s=0;s<10;s++)try{await this.client.deleteFirewall(n.id),t.push(`firewall ${e}-fw (#${n.id})`);break}catch{if(s===9)break;await new Promise((i)=>setTimeout(i,3000))}return{destroyed:t}}}function zl(e=[]){let t=[{IpProtocol:"tcp",FromPort:22,ToPort:22,IpRanges:[{CidrIp:"0.0.0.0/0",Description:"SSH"}]}];for(let r of e)t.push(r.protocol==="icmp"?{IpProtocol:"icmp",FromPort:-1,ToPort:-1,IpRanges:[{CidrIp:"0.0.0.0/0"}]}:{IpProtocol:r.protocol,FromPort:r.port,ToPort:r.port,IpRanges:[{CidrIp:"0.0.0.0/0"}]});return t}class gd{provider="aws";ec2;ssm;constructor(e){this.ec2=e.ec2,this.ssm=e.ssm}async findInstance(e,t){return(await this.ec2.describeInstances({Filters:[{Name:"tag:Name",Values:[e]},{Name:"instance-state-name",Values:t}]})).Reservations?.flatMap((n)=>n.Instances??[])[0]}async waitForPublicIp(e,t=300000){let r=Date.now();while(Date.now()-r<t){let n=await this.ec2.getInstance(e);if(n?.State?.Name==="running"&&n.PublicIpAddress)return n;await new Promise((s)=>setTimeout(s,5000))}throw Error(`EC2 instance ${e} did not reach running with a public IP within ${t}ms`)}async ensureSecurityGroup(e){let t=`${e.name}-sg`,n=(await this.ec2.describeSecurityGroups({Filters:[{Name:"group-name",Values:[t]}]})).SecurityGroups?.[0]?.GroupId;if(n)return n;let s=await this.ec2.createSecurityGroup({GroupName:t,Description:`box ${e.name} (managed by ts-cloud box-provision)`});if(!s.GroupId)throw Error(`could not create security group ${t}`);return await this.ec2.authorizeSecurityGroupIngress({GroupId:s.GroupId,IpPermissions:zl(e.ports)}),s.GroupId}async resolveImage(e){if(e.image?.startsWith("ami-"))return e.image;let r=(await this.ssm.getParameter({Name:ld})).Parameter?.Value;if(!r)throw Error(`could not resolve the Ubuntu 24.04 AMI via SSM (${ld})`);return r}async ensureBox(e){let t=await this.findInstance(e.name,["pending","running"]);if(t?.InstanceId){let f=await this.waitForPublicIp(t.InstanceId);return{provider:"aws",id:f.InstanceId,name:e.name,publicIp:f.PublicIpAddress,created:!1}}let[r,n]=await Promise.all([this.ensureSecurityGroup(e),this.resolveImage(e)]),s=[{Key:"Name",Value:e.name},...Object.entries(e.labels??{}).map(([f,g])=>({Key:f,Value:g}))],a=(await this.ec2.runInstances({ImageId:n,InstanceType:e.size,MinCount:1,MaxCount:1,SecurityGroupIds:[r],UserData:Buffer.from(ud(e),"utf8").toString("base64"),TagSpecifications:[{ResourceType:"instance",Tags:s}]})).Instances?.[0]?.InstanceId;if(!a)throw Error(`RunInstances returned no instance for box ${e.name}`);let c=await this.waitForPublicIp(a);return{provider:"aws",id:a,name:e.name,publicIp:c.PublicIpAddress,created:!0}}async destroyBox(e){let t=[],r=await this.findInstance(e,["pending","running","stopping","stopped"]);if(r?.InstanceId){await this.ec2.terminateInstances([r.InstanceId]),t.push(`instance ${e} (${r.InstanceId})`);let i=Date.now();while(Date.now()-i<300000){let a=await this.ec2.getInstance(r.InstanceId);if(!a||a.State?.Name==="terminated")break;await new Promise((c)=>setTimeout(c,5000))}}let s=(await this.ec2.describeSecurityGroups({Filters:[{Name:"group-name",Values:[`${e}-sg`]}]})).SecurityGroups?.[0]?.GroupId;if(s)for(let i=0;i<10;i++)try{await this.ec2.deleteSecurityGroup(s),t.push(`security group ${e}-sg (${s})`);break}catch{if(i===9)break;await new Promise((a)=>setTimeout(a,3000))}return{destroyed:t}}}function cA(e){if(e.provider==="hetzner"){if(!e.hetzner)throw Error('createBoxProvisioner: options.hetzner (a HetznerClient) is required for provider "hetzner"');return new Di(e.hetzner)}if(!e.aws)throw Error('createBoxProvisioner: options.aws ({ ec2, ssm }) is required for provider "aws"');return new gd(e.aws)}var mo="/opt/pantry/cli",Ep=`${u}/pantry/.bin`,Fl={"google.com/chrome":{profile:"ts-cloud-pantry-chrome",binary:`${u}/pantry/google.com/chrome/*/lib/chrome-for-testing/chrome`}};function Oo(e){if(!e.startsWith("/")||/[^A-Za-z0-9_./*-]/.test(e)||e.includes("**")||e.split("/").slice(1).some((t)=>!t||t==="."||t===".."))throw Error("Sandbox paths must be absolute without traversal or AppArmor control syntax")}function Do(e){let t=[...new Set(e.sandboxedPackageProjects??[])].sort();if(t.length>16)throw Error("At most 16 sandboxed package projects may be declared");for(let r of t){Oo(r);let n=r.indexOf("*"),s=r.slice(r.lastIndexOf("/")+1);if(r.length>240||n!==-1&&(n!==r.length-1||!/^[A-Za-z0-9_][A-Za-z0-9_.-]+-\*$/.test(s)))throw Error("Sandbox project wildcards must follow a named final directory prefix")}if(t.length&&!(e.packages??[]).some((r)=>r==="google.com/chrome"||r.startsWith("google.com/chrome@")))throw Error("Sandboxed projects require the Chrome package to be explicitly declared");return t}function Bl(e,t,r=[]){if(Pt("sandbox profile",e),/[^A-Za-z0-9_.-]/.test(e))throw Error("Invalid sandbox profile name");let n=[...new Set([t,...r])];for(let i of n)Oo(i);let s=n.length===1?n[0]:`/{${n.map((i)=>i.slice(1)).join(",")}}`;return["# Written by ts-cloud: lets this pantry package create user namespaces for its","# sandbox, like Ubuntu's own profile for /opt/google/chrome/chrome.","abi <abi/4.0>,","include <tunables/global>","",`profile ${e} ${s} flags=(unconfined) {`," userns,","",` include if exists <local/${e}>`,"}",""].join(`
432
- `)}function xo(e){let t=Do(e),r=e.packages??[];return Object.entries(Fl).filter(([n])=>r.some((s)=>s===n||s.startsWith(`${n}@`))).map(([,{profile:n,binary:s}])=>({name:n,content:Bl(n,s,t.map((i)=>`${i}/pantry/google.com/chrome/*/lib/chrome-for-testing/chrome`))}))}function vp(e){let t=xo(e);if(!t.length)return`set -euo pipefail
442
+ `}function Fl(e=[]){let t=[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"],description:"SSH"}];for(let r of e)t.push({direction:"in",protocol:r.protocol,...r.protocol==="icmp"?{}:{port:String(r.port)},source_ips:["0.0.0.0/0","::/0"]});return t}class Di{client;provider="hetzner";constructor(e){this.client=e}async ensureBox(e){let t=await vi(this.client,{name:`${e.name}-key`,publicKey:e.sshPublicKey}),r=await Ti(this.client,{name:`${e.name}-fw`,rules:Fl(e.ports)}),{server:n,created:s}=await wi(this.client,{name:e.name,serverType:e.size,image:e.image??"ubuntu-24.04",location:e.location,sshKeys:[t.id],firewalls:[{firewall:r.id}],userData:ud(e),labels:e.labels});if(!s){if(!((await this.client.listFirewalls()).find((c)=>c.id===r.id)?.applied_to??[]).some((c)=>c.type==="server"&&c.server?.id===n.id)){let c=await this.client.applyFirewallToResources(r.id,[{type:"server",server:n.id}]);for(let f of c)await this.client.waitForAction(f.id).catch(()=>{})}}return{provider:"hetzner",id:String(n.id),name:n.name,publicIp:Ri(n),created:s}}async destroyBox(e){let t=[],r=(await this.client.listServers()).find((s)=>s.name===e);if(r){let s=await this.client.deleteServer(r.id);await this.client.waitForAction(s.id).catch(()=>{}),t.push(`server ${e} (#${r.id})`)}let n=(await this.client.listFirewalls()).find((s)=>s.name===`${e}-fw`);if(n)for(let s=0;s<10;s++)try{await this.client.deleteFirewall(n.id),t.push(`firewall ${e}-fw (#${n.id})`);break}catch{if(s===9)break;await new Promise((i)=>setTimeout(i,3000))}return{destroyed:t}}}function zl(e=[]){let t=[{IpProtocol:"tcp",FromPort:22,ToPort:22,IpRanges:[{CidrIp:"0.0.0.0/0",Description:"SSH"}]}];for(let r of e)t.push(r.protocol==="icmp"?{IpProtocol:"icmp",FromPort:-1,ToPort:-1,IpRanges:[{CidrIp:"0.0.0.0/0"}]}:{IpProtocol:r.protocol,FromPort:r.port,ToPort:r.port,IpRanges:[{CidrIp:"0.0.0.0/0"}]});return t}class gd{provider="aws";ec2;ssm;constructor(e){this.ec2=e.ec2,this.ssm=e.ssm}async findInstance(e,t){return(await this.ec2.describeInstances({Filters:[{Name:"tag:Name",Values:[e]},{Name:"instance-state-name",Values:t}]})).Reservations?.flatMap((n)=>n.Instances??[])[0]}async waitForPublicIp(e,t=300000){let r=Date.now();while(Date.now()-r<t){let n=await this.ec2.getInstance(e);if(n?.State?.Name==="running"&&n.PublicIpAddress)return n;await new Promise((s)=>setTimeout(s,5000))}throw Error(`EC2 instance ${e} did not reach running with a public IP within ${t}ms`)}async ensureSecurityGroup(e){let t=`${e.name}-sg`,n=(await this.ec2.describeSecurityGroups({Filters:[{Name:"group-name",Values:[t]}]})).SecurityGroups?.[0]?.GroupId;if(n)return n;let s=await this.ec2.createSecurityGroup({GroupName:t,Description:`box ${e.name} (managed by ts-cloud box-provision)`});if(!s.GroupId)throw Error(`could not create security group ${t}`);return await this.ec2.authorizeSecurityGroupIngress({GroupId:s.GroupId,IpPermissions:zl(e.ports)}),s.GroupId}async resolveImage(e){if(e.image?.startsWith("ami-"))return e.image;let r=(await this.ssm.getParameter({Name:ld})).Parameter?.Value;if(!r)throw Error(`could not resolve the Ubuntu 24.04 AMI via SSM (${ld})`);return r}async ensureBox(e){let t=await this.findInstance(e.name,["pending","running"]);if(t?.InstanceId){let f=await this.waitForPublicIp(t.InstanceId);return{provider:"aws",id:f.InstanceId,name:e.name,publicIp:f.PublicIpAddress,created:!1}}let[r,n]=await Promise.all([this.ensureSecurityGroup(e),this.resolveImage(e)]),s=[{Key:"Name",Value:e.name},...Object.entries(e.labels??{}).map(([f,h])=>({Key:f,Value:h}))],a=(await this.ec2.runInstances({ImageId:n,InstanceType:e.size,MinCount:1,MaxCount:1,SecurityGroupIds:[r],UserData:Buffer.from(ud(e),"utf8").toString("base64"),TagSpecifications:[{ResourceType:"instance",Tags:s}]})).Instances?.[0]?.InstanceId;if(!a)throw Error(`RunInstances returned no instance for box ${e.name}`);let c=await this.waitForPublicIp(a);return{provider:"aws",id:a,name:e.name,publicIp:c.PublicIpAddress,created:!0}}async destroyBox(e){let t=[],r=await this.findInstance(e,["pending","running","stopping","stopped"]);if(r?.InstanceId){await this.ec2.terminateInstances([r.InstanceId]),t.push(`instance ${e} (${r.InstanceId})`);let i=Date.now();while(Date.now()-i<300000){let a=await this.ec2.getInstance(r.InstanceId);if(!a||a.State?.Name==="terminated")break;await new Promise((c)=>setTimeout(c,5000))}}let s=(await this.ec2.describeSecurityGroups({Filters:[{Name:"group-name",Values:[`${e}-sg`]}]})).SecurityGroups?.[0]?.GroupId;if(s)for(let i=0;i<10;i++)try{await this.ec2.deleteSecurityGroup(s),t.push(`security group ${e}-sg (${s})`);break}catch{if(i===9)break;await new Promise((a)=>setTimeout(a,3000))}return{destroyed:t}}}function cA(e){if(e.provider==="hetzner"){if(!e.hetzner)throw Error('createBoxProvisioner: options.hetzner (a HetznerClient) is required for provider "hetzner"');return new Di(e.hetzner)}if(!e.aws)throw Error('createBoxProvisioner: options.aws ({ ec2, ssm }) is required for provider "aws"');return new gd(e.aws)}var mo="/opt/pantry/cli",Ep=`${u}/pantry/.bin`,Bl={"google.com/chrome":{profile:"ts-cloud-pantry-chrome",binary:`${u}/pantry/google.com/chrome/*/lib/chrome-for-testing/chrome`}};function Do(e){if(!e.startsWith("/")||/[^A-Za-z0-9_./*-]/.test(e)||e.includes("**")||e.split("/").slice(1).some((t)=>!t||t==="."||t===".."))throw Error("Sandbox paths must be absolute without traversal or AppArmor control syntax")}function xo(e){let t=[...new Set(e.sandboxedPackageProjects??[])].sort();if(t.length>16)throw Error("At most 16 sandboxed package projects may be declared");for(let r of t){Do(r);let n=r.indexOf("*"),s=r.slice(r.lastIndexOf("/")+1);if(r.length>240||n!==-1&&(n!==r.length-1||!/^[A-Za-z0-9_][A-Za-z0-9_.-]+-\*$/.test(s)))throw Error("Sandbox project wildcards must follow a named final directory prefix")}if(t.length&&!(e.packages??[]).some((r)=>r==="google.com/chrome"||r.startsWith("google.com/chrome@")))throw Error("Sandboxed projects require the Chrome package to be explicitly declared");return t}function Kl(e,t,r=[]){if(Pt("sandbox profile",e),/[^A-Za-z0-9_.-]/.test(e))throw Error("Invalid sandbox profile name");let n=[...new Set([t,...r])];for(let i of n)Do(i);let s=n.length===1?n[0]:`/{${n.map((i)=>i.slice(1)).join(",")}}`;return["# Written by ts-cloud: lets this pantry package create user namespaces for its","# sandbox, like Ubuntu's own profile for /opt/google/chrome/chrome.","abi <abi/4.0>,","include <tunables/global>","",`profile ${e} ${s} flags=(unconfined) {`," userns,","",` include if exists <local/${e}>`,"}",""].join(`
443
+ `)}function Lo(e){let t=xo(e),r=e.packages??[];return Object.entries(Bl).filter(([n])=>r.some((s)=>s===n||s.startsWith(`${n}@`))).map(([,{profile:n,binary:s}])=>({name:n,content:Kl(n,s,t.map((i)=>`${i}/pantry/google.com/chrome/*/lib/chrome-for-testing/chrome`))}))}function vp(e){let t=Lo(e);if(!t.length)return`set -euo pipefail
433
444
  # No declared sandboxed package profiles.
434
445
  `;return["set -euo pipefail","PROFILE_DIR=/etc/apparmor.d","PROFILE_STATE=/sys/kernel/security/apparmor/profiles",'[ "$(id -u)" = 0 ] || { echo "Sandbox policy convergence requires root" >&2; exit 1; }','command -v apparmor_parser >/dev/null || { echo "AppArmor parser is required" >&2; exit 1; }','[ -d "$PROFILE_DIR" ] && [ ! -L "$PROFILE_DIR" ] || { echo "AppArmor policy directory must be a real directory" >&2; exit 1; }','[ "$(stat -c %u "$PROFILE_DIR")" = 0 ] || { echo "AppArmor policy directory must be root-owned" >&2; exit 1; }','MODE=$(stat -c %a "$PROFILE_DIR")','(( (8#$MODE & 022) == 0 )) || { echo "AppArmor policy directory must not be group/other writable" >&2; exit 1; }','WORK=$(mktemp -d "$PROFILE_DIR/.ts-cloud-sandbox.XXXXXX")',"KEEP_BACKUP=0",`trap 'if [ "$KEEP_BACKUP" = 0 ]; then rm -rf -- "$WORK"; fi' EXIT`,...t.flatMap(({name:r,content:n})=>[`TARGET="$PROFILE_DIR/${r}"`,'if [ -L "$TARGET" ] || { [ -e "$TARGET" ] && [ ! -f "$TARGET" ]; }; then echo "Refusing non-regular sandbox policy" >&2; exit 1; fi','if [ -f "$TARGET" ] && [ "$(stat -c %u:%g "$TARGET")" != 0:0 ]; then echo "Sandbox policy must be root-owned" >&2; exit 1; fi','if [ -f "$TARGET" ]; then MODE=$(stat -c %a "$TARGET"); (( (8#$MODE & 022) == 0 )) || { echo "Sandbox policy must not be group/other writable" >&2; exit 1; }; fi','if [ ! -f "$TARGET" ]; then',' [ -r "$PROFILE_STATE" ] || { echo "Cannot inspect existing sandbox policy state" >&2; exit 1; }',' cat -- "$PROFILE_STATE" > "$WORK/profile-state"',' while IFS= read -r LOADED || [ -n "$LOADED" ]; do',' case "$LOADED" in',` "${r} ("*) echo "Refusing to replace an orphaned loaded policy without a recovery definition" >&2; exit 1 ;;`," esac",' done < "$WORK/profile-state"',"fi",`printf %s ${wt(Nr(n))} | base64 -d > "$WORK/candidate"`,'apparmor_parser -Q -K "$WORK/candidate"','if [ -f "$TARGET" ] && cmp -s "$WORK/candidate" "$TARGET"; then',' apparmor_parser -r -K "$TARGET"',` echo "${Rt} sandbox profile ${r} reconciled"`,"else"," HAD_PREVIOUS=0"," PREVIOUS_MODE=644",' if [ -f "$TARGET" ]; then cp -p -- "$TARGET" "$WORK/previous"; PREVIOUS_MODE=$(stat -c %a "$TARGET"); HAD_PREVIOUS=1; fi'," KEEP_BACKUP=1",' if install -m 644 -o root -g root "$WORK/candidate" "$WORK/ready" && mv -f -- "$WORK/ready" "$TARGET" && apparmor_parser -r -K "$TARGET"; then'," KEEP_BACKUP=0",` echo "${Rt} sandbox profile ${r} converged"`," else"," KEEP_BACKUP=1",' if [ "$HAD_PREVIOUS" = 1 ]; then',' if install -m "$PREVIOUS_MODE" -o root -g root "$WORK/previous" "$WORK/restore" && mv -f -- "$WORK/restore" "$TARGET" && apparmor_parser -r -K "$TARGET"; then KEEP_BACKUP=0; fi'," else",' if [ ! -e "$TARGET" ] || apparmor_parser -R -K "$TARGET"; then rm -f -- "$TARGET"; KEEP_BACKUP=0; fi'," fi",' echo "Sandbox reload failed; retained recovery files if rollback failed: $WORK" >&2'," exit 1"," fi","fi"]),""].join(`
435
- `)}var Rt="ts-cloud:",Kl=["/usr/local/bin","/usr/bin","/bin"];function wt(e){return`'${e.split("'").join("'\\''")}'`}function Nr(e){return Buffer.from(e,"utf8").toString("base64")}function Pt(e,t){if(!/^[a-z_][a-z0-9_.-]*$/i.test(t))throw Error(`Box ${e} "${t}" may only contain letters, digits, '.', '_' and '-'`)}function Tp(e){return Object.entries(e).map(([t,r])=>{if(!/^[A-Z_][A-Z0-9_]*$/i.test(t))throw Error(`Environment variable name "${t}" is not valid`);let n=String(r).replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n");return`${t}="${n}"`}).join(`
446
+ `)}var Rt="ts-cloud:",jl=["/usr/local/bin","/usr/bin","/bin"];function wt(e){return`'${e.split("'").join("'\\''")}'`}function Nr(e){return Buffer.from(e,"utf8").toString("base64")}function Pt(e,t){if(!/^[a-z_][a-z0-9_.-]*$/i.test(t))throw Error(`Box ${e} "${t}" may only contain letters, digits, '.', '_' and '-'`)}function Tp(e){return Object.entries(e).map(([t,r])=>{if(!/^[A-Z_][A-Z0-9_]*$/i.test(t))throw Error(`Environment variable name "${t}" is not valid`);let n=String(r).replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n");return`${t}="${n}"`}).join(`
436
447
  `).concat(`
437
- `)}function wp(e,t,r){let n=r.userUnit?"%h":"/root",s=["# Written by ts-cloud from the box declaration; edits here are overwritten.","[Unit]",`Description=${t.description??e}`,"After=network-online.target","","[Service]","Type=simple",`WorkingDirectory=${t.workingDirectory??n}`];if(t.environmentFile)s.push(`EnvironmentFile=${t.environmentFile}`);if(t.environment&&Object.keys(t.environment).length>0)s.push(`EnvironmentFile=${n}/.config/ts-cloud/${e}.env`);return s.push(`Environment=PATH=${[...t.path??[],...r.pantryOnPath===!1?[]:[Ep],...Kl].join(":")}`),s.push(`ExecStart=${t.exec}`),s.push(`Restart=${t.restart??"always"}`),s.push(`RestartSec=${t.restartSec??10}`),s.push("","[Install]",`WantedBy=${r.userUnit?"default.target":"multi-user.target"}`,""),s.join(`
438
- `)}function Rp(e){Do(e);let t=e.user?.name;if(t)Pt("user",t);for(let f of e.user?.groups??[])Pt("group",f);let r=Object.entries(e.services??{});for(let[f]of r)Pt("service",f);let n=e.pantryVersion,s=["set -euo pipefail","export DEBIAN_FRONTEND=noninteractive",`say() { echo "${Rt} $*"; }`,"write_if_changed() {",' local path="$1" mode="$2" owner="$3" tmp'," tmp=$(mktemp)",' printf %s "$4" | base64 -d > "$tmp"',' if [ -f "$path" ] && cmp -s "$tmp" "$path"; then rm -f "$tmp"; return 1; fi',' install -D -m "$mode" -o "${owner%%:*}" -g "${owner##*:}" "$tmp" "$path"',' rm -f "$tmp"',"}",`PANTRY="${mo}/pantry"`,n?`if [ ! -x "$PANTRY" ] || ! "$PANTRY" --version 2>/dev/null | grep -q "pantry ${n.replace(/[^0-9A-Za-z.+-]/g,"")}\\b"; then`:'if [ ! -x "$PANTRY" ]; then'," (",...yt({version:n,installDir:mo}).map((f)=>` ${f}`)," )",' say "pantry $("$PANTRY" --version 2>/dev/null | head -1)"',"fi",`mkdir -p ${u}`],i=[...new Set(e.packages??[])];if(i.length>0){let f=Math.round((e.minFreeDiskGb??10)*1024*1024),g=`${u}/pantry/.ts-cloud-packages`,h=`${[...i].sort().join(`
448
+ `)}function wp(e,t,r){let n=r.userUnit?"%h":"/root",s=["# Written by ts-cloud from the box declaration; edits here are overwritten.","[Unit]",`Description=${t.description??e}`,"After=network-online.target","","[Service]","Type=simple",`WorkingDirectory=${t.workingDirectory??n}`];if(t.environmentFile)s.push(`EnvironmentFile=${t.environmentFile}`);if(t.environment&&Object.keys(t.environment).length>0)s.push(`EnvironmentFile=${n}/.config/ts-cloud/${e}.env`);return s.push(`Environment=PATH=${[...t.path??[],...r.pantryOnPath===!1?[]:[Ep],...jl].join(":")}`),s.push(`ExecStart=${t.exec}`),s.push(`Restart=${t.restart??"always"}`),s.push(`RestartSec=${t.restartSec??10}`),s.push("","[Install]",`WantedBy=${r.userUnit?"default.target":"multi-user.target"}`,""),s.join(`
449
+ `)}function Rp(e){xo(e);let t=e.user?.name;if(t)Pt("user",t);for(let f of e.user?.groups??[])Pt("group",f);let r=Object.entries(e.services??{});for(let[f]of r)Pt("service",f);let n=e.pantryVersion,s=["set -euo pipefail","export DEBIAN_FRONTEND=noninteractive",`say() { echo "${Rt} $*"; }`,"write_if_changed() {",' local path="$1" mode="$2" owner="$3" tmp'," tmp=$(mktemp)",' printf %s "$4" | base64 -d > "$tmp"',' if [ -f "$path" ] && cmp -s "$tmp" "$path"; then rm -f "$tmp"; return 1; fi',' install -D -m "$mode" -o "${owner%%:*}" -g "${owner##*:}" "$tmp" "$path"',' rm -f "$tmp"',"}",`PANTRY="${mo}/pantry"`,n?`if [ ! -x "$PANTRY" ] || ! "$PANTRY" --version 2>/dev/null | grep -q "pantry ${n.replace(/[^0-9A-Za-z.+-]/g,"")}\\b"; then`:'if [ ! -x "$PANTRY" ]; then'," (",...yt({version:n,installDir:mo}).map((f)=>` ${f}`)," )",' say "pantry $("$PANTRY" --version 2>/dev/null | head -1)"',"fi",`mkdir -p ${u}`],i=[...new Set(e.packages??[])];if(i.length>0){let f=Math.round((e.minFreeDiskGb??10)*1024*1024),h=`${u}/pantry/.ts-cloud-packages`,g=`${[...i].sort().join(`
439
450
  `)}
440
- `;if(s.push(`WANTED_PACKAGES=${wt(Nr(h))}`,`if [ -f ${g} ] && [ "$(base64 -w0 < ${g})" = "$WANTED_PACKAGES" ]; then MISSING=0; else MISSING=1; fi`),s.push('if [ "$MISSING" = 1 ]; then'," FREE_KB=$(df -Pk / | awk 'NR==2 {print $4}')",` if [ "$FREE_KB" -lt ${f} ]; then`,` say "not installing packages: $((FREE_KB / 1048576)) GB free, the box declares at least ${e.minFreeDiskGb??10} GB"`," exit 3"," fi",` PANTRY_SERVICE_SCOPE=system PATH="${mo}:$PATH" sh -c ${wt(`cd ${u} && pantry install ${i.map(wt).join(" ")}`)}`,` chmod -R a+rX ${u}/pantry 2>/dev/null || true`,` printf %s "$WANTED_PACKAGES" | base64 -d > ${g}`,` say "packages ${i.join(" ")}"`,"fi"),xo(e).length>0)s.push("if command -v apparmor_parser >/dev/null 2>&1 && [ -d /etc/apparmor.d ]; then"," ("),s.push(...vp(e).trimEnd().split(`
441
- `).map((S)=>` ${S}`)),s.push(" )","fi")}if(t)s.push(`if ! id -u ${t} >/dev/null 2>&1; then useradd --create-home --shell /bin/bash ${t}; say "created user ${t}"; fi`,...(e.user?.groups??[]).map((f)=>`if getent group ${f} >/dev/null && ! id -nG ${t} | tr ' ' '\\n' | grep -qx ${f}; then usermod -aG ${f} ${t}; say "added ${t} to ${f}"; fi`),`if [ "$(loginctl show-user ${t} -p Linger --value 2>/dev/null)" != yes ]; then loginctl enable-linger ${t}; say "enabled linger for ${t}"; fi`);s.push(...Uo(e));let a=t?`${t}:${t}`:"root:root",c=t?"$SVC_HOME/.config/systemd/user":"/etc/systemd/system";if(r.length>0)s.push("RELOAD=0");for(let[f,g]of r){let h=wp(f,g,{userUnit:Boolean(t),pantryOnPath:i.length>0});s.push(`CHANGED_${f.replace(/[^A-Za-z0-9_]/g,"_")}=0`);let S=`CHANGED_${f.replace(/[^A-Za-z0-9_]/g,"_")}`;if(g.environment&&Object.keys(g.environment).length>0)s.push(`if write_if_changed "$SVC_HOME/.config/ts-cloud/${f}.env" 600 ${a} ${wt(Nr(Tp(g.environment)))}; then ${S}=1; fi`);s.push(`if write_if_changed "${c}/${f}.service" 644 ${a} ${wt(Nr(h))}; then ${S}=1; RELOAD=1; fi`)}if(r.length>0)s.push('if [ "$RELOAD" = 1 ]; then svc daemon-reload; fi');for(let[f,g]of r){let h=`CHANGED_${f.replace(/[^A-Za-z0-9_]/g,"_")}`;s.push(`svc enable ${f}.service >/dev/null 2>&1 || svc enable ${f}.service`,`if ! svc is-active --quiet ${f}.service; then svc start ${f}.service; say "started ${f}"`,g.restartOnChange?`elif [ "$${h}" = 1 ]; then svc restart ${f}.service; say "restarted ${f}"`:jl(g)?`elif [ "$${h}" = 1 ]; then svc kill --kill-who=main -s ${Lo(f,g)} ${f}.service; say "changed ${f}; it restarts on the new unit once its current work is done"`:`elif [ "$${h}" = 1 ]; then say "changed ${f}; restart it to apply"`,"fi")}return s.push('say "converged"'),`${s.join(`
451
+ `;if(s.push(`WANTED_PACKAGES=${wt(Nr(g))}`,`if [ -f ${h} ] && [ "$(base64 -w0 < ${h})" = "$WANTED_PACKAGES" ]; then MISSING=0; else MISSING=1; fi`),s.push('if [ "$MISSING" = 1 ]; then'," FREE_KB=$(df -Pk / | awk 'NR==2 {print $4}')",` if [ "$FREE_KB" -lt ${f} ]; then`,` say "not installing packages: $((FREE_KB / 1048576)) GB free, the box declares at least ${e.minFreeDiskGb??10} GB"`," exit 3"," fi",` PANTRY_SERVICE_SCOPE=system PATH="${mo}:$PATH" sh -c ${wt(`cd ${u} && pantry install ${i.map(wt).join(" ")}`)}`,` chmod -R a+rX ${u}/pantry 2>/dev/null || true`,` printf %s "$WANTED_PACKAGES" | base64 -d > ${h}`,` say "packages ${i.join(" ")}"`,"fi"),Lo(e).length>0)s.push("if command -v apparmor_parser >/dev/null 2>&1 && [ -d /etc/apparmor.d ]; then"," ("),s.push(...vp(e).trimEnd().split(`
452
+ `).map((v)=>` ${v}`)),s.push(" )","fi")}if(t)s.push(`if ! id -u ${t} >/dev/null 2>&1; then useradd --create-home --shell /bin/bash ${t}; say "created user ${t}"; fi`,...(e.user?.groups??[]).map((f)=>`if getent group ${f} >/dev/null && ! id -nG ${t} | tr ' ' '\\n' | grep -qx ${f}; then usermod -aG ${f} ${t}; say "added ${t} to ${f}"; fi`),`if [ "$(loginctl show-user ${t} -p Linger --value 2>/dev/null)" != yes ]; then loginctl enable-linger ${t}; say "enabled linger for ${t}"; fi`);s.push(...ko(e));let a=t?`${t}:${t}`:"root:root",c=t?"$SVC_HOME/.config/systemd/user":"/etc/systemd/system";if(r.length>0)s.push("RELOAD=0");for(let[f,h]of r){let g=wp(f,h,{userUnit:Boolean(t),pantryOnPath:i.length>0});s.push(`CHANGED_${f.replace(/[^A-Za-z0-9_]/g,"_")}=0`);let v=`CHANGED_${f.replace(/[^A-Za-z0-9_]/g,"_")}`;if(h.environment&&Object.keys(h.environment).length>0)s.push(`if write_if_changed "$SVC_HOME/.config/ts-cloud/${f}.env" 600 ${a} ${wt(Nr(Tp(h.environment)))}; then ${v}=1; fi`);s.push(`if write_if_changed "${c}/${f}.service" 644 ${a} ${wt(Nr(g))}; then ${v}=1; RELOAD=1; fi`)}if(r.length>0)s.push('if [ "$RELOAD" = 1 ]; then svc daemon-reload; fi');for(let[f,h]of r){let g=`CHANGED_${f.replace(/[^A-Za-z0-9_]/g,"_")}`;s.push(`svc enable ${f}.service >/dev/null 2>&1 || svc enable ${f}.service`,`if ! svc is-active --quiet ${f}.service; then svc start ${f}.service; say "started ${f}"`,h.restartOnChange?`elif [ "$${g}" = 1 ]; then svc restart ${f}.service; say "restarted ${f}"`:Gl(h)?`elif [ "$${g}" = 1 ]; then svc kill --kill-who=main -s ${Uo(f,h)} ${f}.service; say "changed ${f}; it restarts on the new unit once its current work is done"`:`elif [ "$${g}" = 1 ]; then say "changed ${f}; restart it to apply"`,"fi")}return s.push('say "converged"'),`${s.join(`
442
453
  `)}
443
- `}function Lo(e,t){let r=String(t.drainSignal).replace(/^SIG/i,"").toUpperCase();if(!/^[A-Z0-9]+$/.test(r))throw Error(`Box service "${e}" has an invalid drainSignal`);return`SIG${r}`}function jl(e){return Boolean(e.drainSignal)&&(e.restart??"always")==="always"}function Uo(e){let t=e.user?.name;if(!t)return["SVC_HOME=/root",'svc() { systemctl "$@"; }'];return[`SVC_UID=$(id -u ${t})`,`SVC_HOME=$(getent passwd ${t} | cut -d: -f6)`,`svc() { sudo -u ${t} XDG_RUNTIME_DIR="/run/user/$SVC_UID" systemctl --user "$@"; }`,'for _ in $(seq 1 30); do [ -S "/run/user/$SVC_UID/bus" ] && break; sleep 1; done']}function Gl(e){if(e.user?.name)Pt("user",e.user.name);let t=Object.entries(e.services??{}).filter(([,n])=>n.drainSignal),r=["set -euo pipefail",`say() { echo "${Rt} $*"; }`,...Uo(e)];for(let[n,s]of t){Pt("service",n);let i=Lo(n,s).slice(3),a=s.drainTimeoutSec??3600;r.push(`if svc is-active --quiet ${n}.service; then`,` PID=$(svc show -p MainPID --value ${n}.service)`,` svc kill --kill-who=main -s SIG${i} ${n}.service`,` say "draining ${n} (SIG${i})"`," WAITED=0",' while [ "$PID" != 0 ] && kill -0 "$PID" 2>/dev/null; do',` if [ "$WAITED" -ge ${a} ]; then say "${n} still busy after ${a}s; not powering off"; exit 4; fi`," sleep 5; WAITED=$((WAITED + 5))"," done",` svc stop ${n}.service`,` say "drained ${n} after \${WAITED}s"`,"fi")}return r.push('say "drained"'),`${r.join(`
454
+ `}function Uo(e,t){let r=String(t.drainSignal).replace(/^SIG/i,"").toUpperCase();if(!/^[A-Z0-9]+$/.test(r))throw Error(`Box service "${e}" has an invalid drainSignal`);return`SIG${r}`}function Gl(e){return Boolean(e.drainSignal)&&(e.restart??"always")==="always"}function ko(e){let t=e.user?.name;if(!t)return["SVC_HOME=/root",'svc() { systemctl "$@"; }'];return[`SVC_UID=$(id -u ${t})`,`SVC_HOME=$(getent passwd ${t} | cut -d: -f6)`,`svc() { sudo -u ${t} XDG_RUNTIME_DIR="/run/user/$SVC_UID" systemctl --user "$@"; }`,'for _ in $(seq 1 30); do [ -S "/run/user/$SVC_UID/bus" ] && break; sleep 1; done']}function Vl(e){if(e.user?.name)Pt("user",e.user.name);let t=Object.entries(e.services??{}).filter(([,n])=>n.drainSignal),r=["set -euo pipefail",`say() { echo "${Rt} $*"; }`,...ko(e)];for(let[n,s]of t){Pt("service",n);let i=Uo(n,s).slice(3),a=s.drainTimeoutSec??3600;r.push(`if svc is-active --quiet ${n}.service; then`,` PID=$(svc show -p MainPID --value ${n}.service)`,` svc kill --kill-who=main -s SIG${i} ${n}.service`,` say "draining ${n} (SIG${i})"`," WAITED=0",' while [ "$PID" != 0 ] && kill -0 "$PID" 2>/dev/null; do',` if [ "$WAITED" -ge ${a} ]; then say "${n} still busy after ${a}s; not powering off"; exit 4; fi`," sleep 5; WAITED=$((WAITED + 5))"," done",` svc stop ${n}.service`,` say "drained ${n} after \${WAITED}s"`,"fi")}return r.push('say "drained"'),`${r.join(`
444
455
  `)}
445
- `}function ko(e,t){if(!e.resize||t.alreadyComplete)return"none";if(t.capacityAvailable||e.resize.attemptUnavailable)return"resize";if(e.resize.whenUnavailable==="keep-current")return"unavailable";throw Error(`${t.targetType.name} is out of stock in ${t.location}; set resize.attemptUnavailable to ask anyway, or whenUnavailable: 'keep-current' to wait`)}function pd(e){return e.split(`
446
- `).filter((t)=>t.startsWith(Rt)).map((t)=>t.slice(Rt.length).trim())}async function dA(e){let{box:t}=e;if(e.dryRun)return Vl(e);let r=await new Di(e.client).ensureBox({name:t.name,size:t.size,image:t.image,location:t.location,ports:t.ports,sshPublicKey:e.sshPublicKey,labels:{"ts-cloud/role":"box","ts-cloud/managed-by":"ts-cloud",...t.labels}});await St(r.publicIp,{identityFile:e.identityFile});let n=new Et({user:"root",identityFile:e.identityFile,hostKey:{policy:"pin",knownHostsFile:e.knownHostsFile}}),s=[],i=await n.ensureHostKey(r.publicIp);if(i.pinnedNow)s.push(`pinned host key ${i.fingerprint}`);if(t.resize&&!r.created){let c=await tr(e.client,Number(r.id),t.size,t.resize.upgradeDisk!==!1),f=ko(t,c);if(f==="unavailable")s.push(`${c.targetType.name} is out of stock in ${c.location}; keeping ${c.currentType.name}`);if(f==="resize"){let g=await gi({client:e.client,serverId:Number(r.id),targetType:c.targetType.name,upgradeDisk:c.upgradeDisk,attemptUnavailable:t.resize.attemptUnavailable,hooks:{preflight:async()=>{s.push(...pd(await n.exec(r.publicIp,Gl(t))))},onPhase:(h)=>e.onPhase?.(h)}});if(g.status==="completed")s.push(`resized ${c.currentType.name} to ${c.targetType.name}`);else if(g.status==="recovered")s.push(`could not resize to ${c.targetType.name} (${g.error}); back on ${c.currentType.name}`);else if(g.status==="waiting-capacity")s.push(`${c.targetType.name} is out of stock in ${c.location}; keeping ${c.currentType.name}`);await St(r.publicIp,{identityFile:e.identityFile})}}let a=await n.exec(r.publicIp,Rp(t));return{box:r,report:[...s,...pd(a)]}}async function Vl(e){let{box:t}=e,r=(await e.client.listServers()).find((i)=>i.name===t.name);if(!r)return{box:{provider:"hetzner",id:"",name:t.name,publicIp:"",created:!1},report:[`would create ${t.name} (${t.size})`,"would converge"]};let n={provider:"hetzner",id:String(r.id),name:r.name,publicIp:r.public_net?.ipv4?.ip??"",created:!1},s=[];if(t.resize){let i=await tr(e.client,r.id,t.size,t.resize.upgradeDisk!==!1),a=ko(t,i);if(a==="resize")s.push(`would resize ${i.currentType.name} to ${i.targetType.name}${i.capacityAvailable?"":" (out of stock; asking placement anyway)"}, draining ${Object.entries(t.services??{}).filter(([,c])=>c.drainSignal).map(([c])=>c).join(", ")||"nothing"} first`);else if(a==="unavailable")s.push(`${i.targetType.name} is out of stock in ${i.location}; would keep ${i.currentType.name}`)}else if(r.server_type.name!==t.size)s.push(`runs ${r.server_type.name}, declared ${t.size}; set resize to change it`);return s.push("would converge"),{box:n,report:s}}import{copyFileSync as xu}from"node:fs";import{tmpdir as Au}from"node:os";import{join as Iu}from"node:path";var ql=`${Se}/project`;function Ho(e,t){let r=t.ownerSlug.trim(),n=t.selfSlug?.trim(),s={total:e.length,owner:[],self:[],others:new Map,unmanaged:[]};for(let i of e){let a=i.labels?.[ql]?.trim();if(!a){s.unmanaged.push(i.name);continue}if(a===r){s.owner.push(i.name);continue}if(n&&a===n){s.self.push(i.name);continue}let c=s.others.get(a);if(c)c.push(i.name);else s.others.set(a,[i.name])}return s}function zn(e){let t=e.unmanaged.length;for(let r of e.others.values())t+=r.length;return t}function No(e,t){let r=zn(e),n=[];if(n.push(`Attaching to '${t.ownerSlug}' shares one provider project, so this deploy's credential can write to all ${e.total} server(s) it can see.`),r===0)return n.push("Nothing outside the two projects being joined is reachable with it."),n;n.push(`${r} of them belong to neither project:`);for(let s of[...e.others.keys()].sort())n.push(` ${s}: ${[...e.others.get(s)].sort().join(", ")}`);if(e.unmanaged.length>0)n.push(` not managed by ts-cloud: ${[...e.unmanaged].sort().join(", ")}`);return n.push("A compromised CI run or a mistargeted teardown in this project now reaches those. Keep the app in its own provider project instead if that is not acceptable, which rules out attaching."),n}import{execSync as zo}from"node:child_process";import{randomBytes as Yl}from"node:crypto";import{chmodSync as Xl,copyFileSync as Jl,existsSync as ot,mkdirSync as Fo,readFileSync as Bo,statSync as Zl,writeFileSync as Bn}from"node:fs";import{tmpdir as Ql}from"node:os";import{basename as eu,dirname as jn,isAbsolute as Ko,join as ye,resolve as tu}from"node:path";import{fileURLToPath as jo}from"node:url";function Fn(e){if(!e||typeof e!=="object")return e;return Object.fromEntries(Object.keys(e).map((t)=>[t,""]))}function Wl(e){let t=JSON.parse(JSON.stringify(e??{}));if(t.hetzner)delete t.hetzner.apiToken;delete t.aws,delete t.stateDir;for(let r of Object.values(t.sites??{})){if(r?.env)r.env=Fn(r.env);if(r?.environment)r.environment=Fn(r.environment)}for(let r of Object.values(t.environments??{}))if(r?.variables)r.variables=Fn(r.variables);for(let r of Object.values(t.infrastructure?.databases??{}))if(r?.password)r.password="";if(t.infrastructure?.appDatabase?.password)t.infrastructure.appDatabase.password="";if(t.infrastructure?.compute?.database?.password)t.infrastructure.compute.database.password="";if(t.infrastructure?.compute?.managedServices?.vitess?.password)t.infrastructure.compute.managedServices.vitess.password="";return t}function Mo(e){let t=Wl(e);return["// Generated by ts-cloud for the management dashboard — do not edit.","//","// This is the project's resolved cloud config, inlined so it needs no","// imports on the box. Credentials are deliberately absent.",`export default ${JSON.stringify(t,null,2)}`,""].join(`
447
- `)}var md="dashboard";function fo(){return H("dashboard-credentials.json")}function ru(){return Yl(24).toString("base64url")}function fd(e,t,r){let n=process.env.TS_CLOUD_UI_PASSWORD?.trim();if(n)return{password:n,source:"env"};if(Kn(process.env.TS_CLOUD_UI_PUBLIC))return{password:void 0,source:"public"};let s=fo(),i=l(e,"dashboard-credentials.json");try{if(ot(i)){let c=JSON.parse(Bo(i,"utf8"));if(c?.password)return{password:c.password,source:"generated"}}}catch{}let a=ru();try{Fo(jn(i),{recursive:!0}),Bn(i,`${JSON.stringify({username:t,password:a,generatedAt:new Date().toISOString()},null,2)}
448
- `),Xl(i,384),r.info(`Management dashboard: generated a password for '${t}' and saved it to ${s} (read it there — it is not printed). Set TS_CLOUD_UI_PASSWORD to pin your own, or TS_CLOUD_UI_PUBLIC=1 to serve without auth.`)}catch(c){r.warn(`Management dashboard: could not persist the generated password (${c?.message??c}). Using it for this deploy only — pass: ${a}
449
- This password is now in your deploy log. Set TS_CLOUD_UI_PASSWORD to a value of your own and redeploy once ${s} is writable.`)}return{password:a,source:"generated"}}var Go={info:()=>{},warn:()=>{}};function Kn(e){return e!=null&&e!==""&&e!=="0"&&e.toLowerCase()!=="false"}function nu(){return H("dashboard-release")}function Vo(){let e=jn(jo(import.meta.url));for(let t of[ye(e,"..","..","package.json"),ye(e,"..","package.json")])try{let r=JSON.parse(Bo(t,"utf8"));if(r.name==="@stacksjs/ts-cloud"&&r.version)return`^${r.version}`}catch{}return"latest"}function su(){return process.env.TS_CLOUD_UI_VERSION?.trim()||Vo()}function ou(e,t,r){if(!e?.project?.slug)return r.warn("Management dashboard: the cloud config has no project slug — skipping the live dashboard."),null;let n=l(t,"dashboard-release");try{Fo(n,{recursive:!0}),Bn(ye(n,"cloud.config.ts"),Mo(e));let s=su();if(s.startsWith("file:")){let a=s.slice(5),c=Ko(a)?a:tu(t,a);if(!ot(c)||!Zl(c).isFile())s=Vo(),r.warn(`Management dashboard: TS_CLOUD_UI_VERSION points to a missing package file (${c}); using the deploying ts-cloud version ${s} instead.`);else{let f=eu(c),g=ye(n,f);if(c!==g)Jl(c,g);s=`file:./${f}`}}let i={name:"ts-cloud-dashboard",private:!0,type:"module",dependencies:{"@stacksjs/ts-cloud":s}};return Bn(ye(n,"package.json"),`${JSON.stringify(i,null,2)}
450
- `),nu()}catch(s){return r.warn(`Management dashboard: could not stage the live release (${s?.message??s}) — skipping.`),null}}function rr(e){if(ot(ye(e,"packages","ui","pages"))||ot(ye(e,"packages","ui","package.json")))return{uiRoot:"packages/ui/dist",build:"cd packages/ui && bun install && bun run build"};let t=jn(jo(import.meta.url)),r=[ye(t,"ui"),ye(t,"..","ui"),ye(t,"..","..","ui"),ye(t,"..","dist","ui")];for(let n of r)if(ot(ye(n,"index.html"))||ot(ye(n,"serverless.html")))return{uiRoot:n,build:!1};return null}function Pi(e,t={}){let r=t.logger??Go,n=t.cwd??process.cwd();if(Kn(process.env.TS_CLOUD_UI_DISABLE))return r.info("Management dashboard: skipped (TS_CLOUD_UI_DISABLE set)."),e;if(e.cloud?.attachTo)return r.info(`Management dashboard: ${e.project.slug} is attached to ${e.cloud.attachTo}; the server owner's dashboard monitors every attached project.`),e;if(mr(e))return e;let s=e.environments&&Object.keys(e.environments)[0],i=process.env.TS_CLOUD_UI_DOMAIN?.trim()||void 0,a=Number(process.env.TS_CLOUD_UI_PORT)||void 0,c={...e.sites??{}};if(!Kn(process.env.TS_CLOUD_UI_STATIC)){let v=ou(e,n,r);if(!v)return e;let b=rs(e,s??"production",{uiRoot:v,build:!1,domain:i,port:a,live:!0});if(b.length===0)return r.info("Management dashboard: no domain resolved (set TS_CLOUD_UI_DOMAIN or configure a site domain) — skipping."),e;for(let{name:T,site:L}of b)c[T]=L,r.info(`Management dashboard → https://${L.domain} (sign in with your ts-cloud account; the first deploy prints an admin password once)`);return e.sites=c,e}let f=rr(n);if(!f)return r.info("Management dashboard: UI not found (no local ui/ or packaged dist/ui) — skipping auto-deploy."),e;let g=process.env.TS_CLOUD_UI_USERNAME?.trim()||"admin",h=fd(n,g,r),S=rs(e,s??"production",{uiRoot:f.uiRoot,build:f.build,domain:i,username:g,password:h.password,realm:process.env.TS_CLOUD_UI_REALM?.trim()||void 0,live:!1,port:a});if(S.length===0)return r.info("Management dashboard: no domain resolved (set TS_CLOUD_UI_DOMAIN or configure a site domain) — skipping."),e;let _=h.source==="public"?"NO AUTH — TS_CLOUD_UI_PUBLIC is set (dashboard is publicly reachable)":h.source==="env"?"htpasswd-protected (TS_CLOUD_UI_PASSWORD)":`htpasswd-protected (auto-generated — see ${fo()})`;r.warn("Management dashboard: static mode — one shared password, and no per-site collaborators. Unset TS_CLOUD_UI_STATIC for the live dashboard.");for(let{name:v,site:b}of S)c[v]=b,r.info(`Management dashboard → https://${b.domain} (${_})`);return e.sites=c,e}function Gn(e){return Object.keys(e.sites??{}).filter(fr)}function ho(e,t){if(!e?.root)return null;let r=t.cwd??process.cwd(),n=t.logger??Go;try{if(typeof e.build==="string"&&e.build.trim())n.info(`Management dashboard: building UI (${e.build})`),zo(e.build,{cwd:r,stdio:"inherit"});let s=Ko(e.root)?e.root:ye(r,e.root);if(!ot(s))return n.warn(`Management dashboard: build output not found at ${s} — skipping dashboard artifact.`),null;let i=ye(Ql(),`${t.slug}-${t.siteName??md}-${t.sha}.tar.gz`);return zo(`tar czf "${i}" -C "${s}" .`,{stdio:"inherit"}),i}catch(s){return n.warn(`Management dashboard: failed to build artifact — ${s?.message??s}. Skipping dashboard deploy.`),null}}function iu(e){let t=e.split("/");if(!/^[a-zA-Z0-9_./-]+$/.test(e)||e.startsWith("/")||t.some((r)=>!r||r==="."||r==="..")||t.at(-1)!=="bun")throw Error("Stacks backgroundBun must select a safe release-relative Bun executable")}function qo(e,t){if(iu(t),!/^\/[a-zA-Z0-9_./-]+$/.test(e)||e.split("/").includes(".."))throw Error("Stacks backgroundBun requires a safe absolute release directory");return`${e}/${t}`}var au=new Set(["laravel","php","statamic","wordpress"]);function qr(e){if(e.framework)return e.framework;if(e.type&&au.has(e.type))return"laravel";return"stacks"}var Wo=`eval "$(cd ${u} && pantry env 2>/dev/null)"`,cu="/usr/local/bin/bun",zr={PATH:"/usr/local/bin:/usr/bin:/bin",BUN_INSTALL:"/opt/ts-cloud/bun",APP_ENV:"production",NODE_ENV:"production"};function Vn(e){if(e.backgroundBun===void 0)return cu;return qo(e.current,e.backgroundBun)}function lu(e){let t=e.queue??"default",r=[e.sleep,e.tries,e.timeout,e.processes,e.stopWaitSecs];if(typeof t!=="string"||!/^[a-zA-Z0-9_.:,{}/@+-]+$/.test(t||"default")||r.some((n)=>n!==void 0&&(typeof n!=="number"||!Number.isFinite(n))))throw Error("Stacks background queue options require safe queue names and finite numeric values")}var uu="node_modules/@stacksjs/ts-cloud/dist/bin/stacks-scheduler.js",pu="node_modules/@stacksjs/ts-cloud/dist/bin/stacks-queue-worker.js",Yo={id:"stacks",wrapExec:(e)=>e,execEnv:zr,executionEnvironment:(e)=>{let t=Vn(e);return e.backgroundBun===void 0?zr:{...zr,PATH:`${t.slice(0,t.lastIndexOf("/"))}:${zr.PATH}`,TS_CLOUD_BACKGROUND_BUN:e.backgroundBun}},schedulerMode:"daemon",schedulerCommand:(e)=>`${Vn(e)} ${uu}`,queueWorkerCommand:(e,t)=>{if(t.backgroundBun!==void 0)lu(e);let{current:r}=t,n=[`--queue=${e.queue||"default"}`,`--sleep=${e.sleep??3}`,`--tries=${e.tries??3}`,`--timeout=${e.timeout??60}`];return`${Vn(t)} ${r}/${pu} ${n.join(" ")}`}},du={id:"laravel",wrapExec:(e)=>`/bin/sh -lc '${Wo}; exec ${e}'`,schedulerMode:"cron",schedulerCommand:({current:e})=>`cd ${e} && ${Wo} && php artisan schedule:run`,queueWorkerCommand:(e,{current:t})=>{let r=`${t}/artisan`;if(e.horizon)return`php ${r} horizon`;let n=[e.connection||"default",`--queue=${e.queue||"default"}`,`--sleep=${e.sleep??3}`,`--tries=${e.tries??3}`,`--timeout=${e.timeout??60}`,`--memory=${e.memory??128}`];if(e.maxJobs)n.push(`--max-jobs=${e.maxJobs}`);if(e.maxTime)n.push(`--max-time=${e.maxTime}`);return`php ${r} queue:work ${n.join(" ")}`}},mu={stacks:Yo,laravel:du};function Xo(e){return mu[e]??Yo}function Jo(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function fu(e){return`'${e.split("'").join("'\\''")}'`}function Zo(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unnamed"}function Qo(e,t){return`for unit in $(ls /etc/systemd/system/ 2>/dev/null | grep -E '^${Jo(e)}-${Jo(t)}-((queue|daemon)-.*|scheduler)\\.service$' || true); do`}function Yn(e){return[Qo(e.slug,e.siteName),' systemctl stop "$unit"',"done"]}function gu(e,t,r){return`${e}-${t}-queue-${r}`}function hu(e,t,r,n){return`${e}-${t}-daemon-${r.name?Zo(r.name):Zo(r.command).slice(0,32)||String(n)}`}function qn(e){let t=["[Unit]",`Description=${e.description}`,"After=network.target","","[Service]","Type=simple",`WorkingDirectory=${e.workingDir}`,...Object.entries(e.environment??{}).map(([r,n])=>`Environment="${r}=${n}"`),...e.environmentFile?[`EnvironmentFile=-${e.environmentFile}`]:[],`ExecStart=${e.execStart}`,`Restart=${e.restart||"always"}`,"RestartSec=5","MemoryAccounting=true",`MemoryHigh=${e.memoryHigh??"2G"}`,...e.memoryMax?[`MemoryMax=${e.memoryMax}`]:[]];if(e.isolatedUser){if(!e.isolationRoot||!e.isolationSlug)throw Error("Runtime isolation requires its project directory");t.push(...Ht(e.isolatedUser,e.isolationRoot,e.isolationSlug))}else if(e.user)t.push(`User=${e.user}`);if(typeof e.stopWaitSecs==="number")t.push(`TimeoutStopSec=${e.stopWaitSecs}`);return t.push("","[Install]","WantedBy=multi-user.target"),`${t.join(`
456
+ `}function Ho(e,t){if(!e.resize||t.alreadyComplete)return"none";if(t.capacityAvailable||e.resize.attemptUnavailable)return"resize";if(e.resize.whenUnavailable==="keep-current")return"unavailable";throw Error(`${t.targetType.name} is out of stock in ${t.location}; set resize.attemptUnavailable to ask anyway, or whenUnavailable: 'keep-current' to wait`)}function pd(e){return e.split(`
457
+ `).filter((t)=>t.startsWith(Rt)).map((t)=>t.slice(Rt.length).trim())}async function dA(e){let{box:t}=e;if(e.dryRun)return ql(e);let r=await new Di(e.client).ensureBox({name:t.name,size:t.size,image:t.image,location:t.location,ports:t.ports,sshPublicKey:e.sshPublicKey,labels:{"ts-cloud/role":"box","ts-cloud/managed-by":"ts-cloud",...t.labels}});await St(r.publicIp,{identityFile:e.identityFile});let n=new Et({user:"root",identityFile:e.identityFile,hostKey:{policy:"pin",knownHostsFile:e.knownHostsFile}}),s=[],i=await n.ensureHostKey(r.publicIp);if(i.pinnedNow)s.push(`pinned host key ${i.fingerprint}`);if(t.resize&&!r.created){let c=await tr(e.client,Number(r.id),t.size,t.resize.upgradeDisk!==!1),f=Ho(t,c);if(f==="unavailable")s.push(`${c.targetType.name} is out of stock in ${c.location}; keeping ${c.currentType.name}`);if(f==="resize"){let h=await gi({client:e.client,serverId:Number(r.id),targetType:c.targetType.name,upgradeDisk:c.upgradeDisk,attemptUnavailable:t.resize.attemptUnavailable,hooks:{preflight:async()=>{s.push(...pd(await n.exec(r.publicIp,Vl(t))))},onPhase:(g)=>e.onPhase?.(g)}});if(h.status==="completed")s.push(`resized ${c.currentType.name} to ${c.targetType.name}`);else if(h.status==="recovered")s.push(`could not resize to ${c.targetType.name} (${h.error}); back on ${c.currentType.name}`);else if(h.status==="waiting-capacity")s.push(`${c.targetType.name} is out of stock in ${c.location}; keeping ${c.currentType.name}`);await St(r.publicIp,{identityFile:e.identityFile})}}let a=await n.exec(r.publicIp,Rp(t));return{box:r,report:[...s,...pd(a)]}}async function ql(e){let{box:t}=e,r=(await e.client.listServers()).find((i)=>i.name===t.name);if(!r)return{box:{provider:"hetzner",id:"",name:t.name,publicIp:"",created:!1},report:[`would create ${t.name} (${t.size})`,"would converge"]};let n={provider:"hetzner",id:String(r.id),name:r.name,publicIp:r.public_net?.ipv4?.ip??"",created:!1},s=[];if(t.resize){let i=await tr(e.client,r.id,t.size,t.resize.upgradeDisk!==!1),a=Ho(t,i);if(a==="resize")s.push(`would resize ${i.currentType.name} to ${i.targetType.name}${i.capacityAvailable?"":" (out of stock; asking placement anyway)"}, draining ${Object.entries(t.services??{}).filter(([,c])=>c.drainSignal).map(([c])=>c).join(", ")||"nothing"} first`);else if(a==="unavailable")s.push(`${i.targetType.name} is out of stock in ${i.location}; would keep ${i.currentType.name}`)}else if(r.server_type.name!==t.size)s.push(`runs ${r.server_type.name}, declared ${t.size}; set resize to change it`);return s.push("would converge"),{box:n,report:s}}import{copyFileSync as Au}from"node:fs";import{tmpdir as Iu}from"node:os";import{join as Lu}from"node:path";var Wl=`${Se}/project`;function No(e,t){let r=t.ownerSlug.trim(),n=t.selfSlug?.trim(),s={total:e.length,owner:[],self:[],others:new Map,unmanaged:[]};for(let i of e){let a=i.labels?.[Wl]?.trim();if(!a){s.unmanaged.push(i.name);continue}if(a===r){s.owner.push(i.name);continue}if(n&&a===n){s.self.push(i.name);continue}let c=s.others.get(a);if(c)c.push(i.name);else s.others.set(a,[i.name])}return s}function Fn(e){let t=e.unmanaged.length;for(let r of e.others.values())t+=r.length;return t}function Mo(e,t){let r=Fn(e),n=[];if(n.push(`Attaching to '${t.ownerSlug}' shares one provider project, so this deploy's credential can write to all ${e.total} server(s) it can see.`),r===0)return n.push("Nothing outside the two projects being joined is reachable with it."),n;n.push(`${r} of them belong to neither project:`);for(let s of[...e.others.keys()].sort())n.push(` ${s}: ${[...e.others.get(s)].sort().join(", ")}`);if(e.unmanaged.length>0)n.push(` not managed by ts-cloud: ${[...e.unmanaged].sort().join(", ")}`);return n.push("A compromised CI run or a mistargeted teardown in this project now reaches those. Keep the app in its own provider project instead if that is not acceptable, which rules out attaching."),n}import{execSync as zo}from"node:child_process";import{randomBytes as Xl}from"node:crypto";import{chmodSync as Jl,copyFileSync as Zl,existsSync as ot,mkdirSync as Bo,readFileSync as Ko,statSync as Ql,writeFileSync as Bn}from"node:fs";import{tmpdir as eu}from"node:os";import{basename as tu,dirname as jn,isAbsolute as jo,join as $e,resolve as ru}from"node:path";import{fileURLToPath as Go}from"node:url";function zn(e){if(!e||typeof e!=="object")return e;return Object.fromEntries(Object.keys(e).map((t)=>[t,""]))}function Yl(e){let t=JSON.parse(JSON.stringify(e??{}));if(t.hetzner)delete t.hetzner.apiToken;delete t.aws,delete t.stateDir;for(let r of Object.values(t.sites??{})){if(r?.env)r.env=zn(r.env);if(r?.environment)r.environment=zn(r.environment)}for(let r of Object.values(t.environments??{}))if(r?.variables)r.variables=zn(r.variables);for(let r of Object.values(t.infrastructure?.databases??{}))if(r?.password)r.password="";if(t.infrastructure?.appDatabase?.password)t.infrastructure.appDatabase.password="";if(t.infrastructure?.compute?.database?.password)t.infrastructure.compute.database.password="";if(t.infrastructure?.compute?.managedServices?.vitess?.password)t.infrastructure.compute.managedServices.vitess.password="";return t}function Fo(e){let t=Yl(e);return["// Generated by ts-cloud for the management dashboard — do not edit.","//","// This is the project's resolved cloud config, inlined so it needs no","// imports on the box. Credentials are deliberately absent.",`export default ${JSON.stringify(t,null,2)}`,""].join(`
458
+ `)}var md="dashboard";function fo(){return H("dashboard-credentials.json")}function nu(){return Xl(24).toString("base64url")}function fd(e,t,r){let n=process.env.TS_CLOUD_UI_PASSWORD?.trim();if(n)return{password:n,source:"env"};if(Kn(process.env.TS_CLOUD_UI_PUBLIC))return{password:void 0,source:"public"};let s=fo(),i=l(e,"dashboard-credentials.json");try{if(ot(i)){let c=JSON.parse(Ko(i,"utf8"));if(c?.password)return{password:c.password,source:"generated"}}}catch{}let a=nu();try{Bo(jn(i),{recursive:!0}),Bn(i,`${JSON.stringify({username:t,password:a,generatedAt:new Date().toISOString()},null,2)}
459
+ `),Jl(i,384),r.info(`Management dashboard: generated a password for '${t}' and saved it to ${s} (read it there — it is not printed). Set TS_CLOUD_UI_PASSWORD to pin your own, or TS_CLOUD_UI_PUBLIC=1 to serve without auth.`)}catch(c){r.warn(`Management dashboard: could not persist the generated password (${c?.message??c}). Using it for this deploy only — pass: ${a}
460
+ This password is now in your deploy log. Set TS_CLOUD_UI_PASSWORD to a value of your own and redeploy once ${s} is writable.`)}return{password:a,source:"generated"}}var Vo={info:()=>{},warn:()=>{}};function Kn(e){return e!=null&&e!==""&&e!=="0"&&e.toLowerCase()!=="false"}function su(){return H("dashboard-release")}function qo(){let e=jn(Go(import.meta.url));for(let t of[$e(e,"..","..","package.json"),$e(e,"..","package.json")])try{let r=JSON.parse(Ko(t,"utf8"));if(r.name==="@stacksjs/ts-cloud"&&r.version)return`^${r.version}`}catch{}return"latest"}function ou(){return process.env.TS_CLOUD_UI_VERSION?.trim()||qo()}function iu(e,t,r){if(!e?.project?.slug)return r.warn("Management dashboard: the cloud config has no project slug — skipping the live dashboard."),null;let n=l(t,"dashboard-release");try{Bo(n,{recursive:!0}),Bn($e(n,"cloud.config.ts"),Fo(e));let s=ou();if(s.startsWith("file:")){let a=s.slice(5),c=jo(a)?a:ru(t,a);if(!ot(c)||!Ql(c).isFile())s=qo(),r.warn(`Management dashboard: TS_CLOUD_UI_VERSION points to a missing package file (${c}); using the deploying ts-cloud version ${s} instead.`);else{let f=tu(c),h=$e(n,f);if(c!==h)Zl(c,h);s=`file:./${f}`}}let i={name:"ts-cloud-dashboard",private:!0,type:"module",dependencies:{"@stacksjs/ts-cloud":s}};return Bn($e(n,"package.json"),`${JSON.stringify(i,null,2)}
461
+ `),su()}catch(s){return r.warn(`Management dashboard: could not stage the live release (${s?.message??s}) — skipping.`),null}}function rr(e){if(ot($e(e,"packages","ui","pages"))||ot($e(e,"packages","ui","package.json")))return{uiRoot:"packages/ui/dist",build:"cd packages/ui && bun install && bun run build"};let t=jn(Go(import.meta.url)),r=[$e(t,"ui"),$e(t,"..","ui"),$e(t,"..","..","ui"),$e(t,"..","dist","ui")];for(let n of r)if(ot($e(n,"index.html"))||ot($e(n,"serverless.html")))return{uiRoot:n,build:!1};return null}function Pi(e,t={}){let r=t.logger??Vo,n=t.cwd??process.cwd();if(Kn(process.env.TS_CLOUD_UI_DISABLE))return r.info("Management dashboard: skipped (TS_CLOUD_UI_DISABLE set)."),e;if(e.cloud?.attachTo)return r.info(`Management dashboard: ${e.project.slug} is attached to ${e.cloud.attachTo}; the server owner's dashboard monitors every attached project.`),e;if(mr(e))return e;let s=e.environments&&Object.keys(e.environments)[0],i=process.env.TS_CLOUD_UI_DOMAIN?.trim()||void 0,a=Number(process.env.TS_CLOUD_UI_PORT)||void 0,c={...e.sites??{}};if(!Kn(process.env.TS_CLOUD_UI_STATIC)){let b=iu(e,n,r);if(!b)return e;let S=rs(e,s??"production",{uiRoot:b,build:!1,domain:i,port:a,live:!0});if(S.length===0)return r.info("Management dashboard: no domain resolved (set TS_CLOUD_UI_DOMAIN or configure a site domain) — skipping."),e;for(let{name:P,site:M}of S)c[P]=M,r.info(`Management dashboard → https://${M.domain} (sign in with your ts-cloud account; the first deploy prints an admin password once)`);return e.sites=c,e}let f=rr(n);if(!f)return r.info("Management dashboard: UI not found (no local ui/ or packaged dist/ui) — skipping auto-deploy."),e;let h=process.env.TS_CLOUD_UI_USERNAME?.trim()||"admin",g=fd(n,h,r),v=rs(e,s??"production",{uiRoot:f.uiRoot,build:f.build,domain:i,username:h,password:g.password,realm:process.env.TS_CLOUD_UI_REALM?.trim()||void 0,live:!1,port:a});if(v.length===0)return r.info("Management dashboard: no domain resolved (set TS_CLOUD_UI_DOMAIN or configure a site domain) — skipping."),e;let _=g.source==="public"?"NO AUTH — TS_CLOUD_UI_PUBLIC is set (dashboard is publicly reachable)":g.source==="env"?"htpasswd-protected (TS_CLOUD_UI_PASSWORD)":`htpasswd-protected (auto-generated — see ${fo()})`;r.warn("Management dashboard: static mode — one shared password, and no per-site collaborators. Unset TS_CLOUD_UI_STATIC for the live dashboard.");for(let{name:b,site:S}of v)c[b]=S,r.info(`Management dashboard → https://${S.domain} (${_})`);return e.sites=c,e}function Gn(e){return Object.keys(e.sites??{}).filter(fr)}function ho(e,t){if(!e?.root)return null;let r=t.cwd??process.cwd(),n=t.logger??Vo;try{if(typeof e.build==="string"&&e.build.trim())n.info(`Management dashboard: building UI (${e.build})`),zo(e.build,{cwd:r,stdio:"inherit"});let s=jo(e.root)?e.root:$e(r,e.root);if(!ot(s))return n.warn(`Management dashboard: build output not found at ${s} — skipping dashboard artifact.`),null;let i=$e(eu(),`${t.slug}-${t.siteName??md}-${t.sha}.tar.gz`);return zo(`tar czf "${i}" -C "${s}" .`,{stdio:"inherit"}),i}catch(s){return n.warn(`Management dashboard: failed to build artifact — ${s?.message??s}. Skipping dashboard deploy.`),null}}function au(e){let t=e.split("/");if(!/^[a-zA-Z0-9_./-]+$/.test(e)||e.startsWith("/")||t.some((r)=>!r||r==="."||r==="..")||t.at(-1)!=="bun")throw Error("Stacks backgroundBun must select a safe release-relative Bun executable")}function Wo(e,t){if(au(t),!/^\/[a-zA-Z0-9_./-]+$/.test(e)||e.split("/").includes(".."))throw Error("Stacks backgroundBun requires a safe absolute release directory");return`${e}/${t}`}var cu=new Set(["laravel","php","statamic","wordpress"]);function qr(e){if(e.framework)return e.framework;if(e.type&&cu.has(e.type))return"laravel";return"stacks"}var Yo=`eval "$(cd ${u} && pantry env 2>/dev/null)"`,lu="/usr/local/bin/bun",zr={PATH:"/usr/local/bin:/usr/bin:/bin",BUN_INSTALL:"/opt/ts-cloud/bun",APP_ENV:"production",NODE_ENV:"production"};function Vn(e){if(e.backgroundBun===void 0)return lu;return Wo(e.current,e.backgroundBun)}function uu(e){let t=e.queue??"default",r=[e.sleep,e.tries,e.timeout,e.processes,e.stopWaitSecs];if(typeof t!=="string"||!/^[a-zA-Z0-9_.:,{}/@+-]+$/.test(t||"default")||r.some((n)=>n!==void 0&&(typeof n!=="number"||!Number.isFinite(n))))throw Error("Stacks background queue options require safe queue names and finite numeric values")}var pu="node_modules/@stacksjs/ts-cloud/dist/bin/stacks-scheduler.js",du="node_modules/@stacksjs/ts-cloud/dist/bin/stacks-queue-worker.js",Xo={id:"stacks",wrapExec:(e)=>e,execEnv:zr,executionEnvironment:(e)=>{let t=Vn(e);return e.backgroundBun===void 0?zr:{...zr,PATH:`${t.slice(0,t.lastIndexOf("/"))}:${zr.PATH}`,TS_CLOUD_BACKGROUND_BUN:e.backgroundBun}},schedulerMode:"daemon",schedulerCommand:(e)=>`${Vn(e)} ${pu}`,queueWorkerCommand:(e,t)=>{if(t.backgroundBun!==void 0)uu(e);let{current:r}=t,n=[`--queue=${e.queue||"default"}`,`--sleep=${e.sleep??3}`,`--tries=${e.tries??3}`,`--timeout=${e.timeout??60}`];return`${Vn(t)} ${r}/${du} ${n.join(" ")}`}},mu={id:"laravel",wrapExec:(e)=>`/bin/sh -lc '${Yo}; exec ${e}'`,schedulerMode:"cron",schedulerCommand:({current:e})=>`cd ${e} && ${Yo} && php artisan schedule:run`,queueWorkerCommand:(e,{current:t})=>{let r=`${t}/artisan`;if(e.horizon)return`php ${r} horizon`;let n=[e.connection||"default",`--queue=${e.queue||"default"}`,`--sleep=${e.sleep??3}`,`--tries=${e.tries??3}`,`--timeout=${e.timeout??60}`,`--memory=${e.memory??128}`];if(e.maxJobs)n.push(`--max-jobs=${e.maxJobs}`);if(e.maxTime)n.push(`--max-time=${e.maxTime}`);return`php ${r} queue:work ${n.join(" ")}`}},fu={stacks:Xo,laravel:mu};function Jo(e){return fu[e]??Xo}function Zo(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function hu(e){return`'${e.split("'").join("'\\''")}'`}function Qo(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unnamed"}function _i(e,t){return`for unit in $(ls /etc/systemd/system/ 2>/dev/null | grep -E '^${Zo(e)}-${Zo(t)}-((queue|daemon)-.*|scheduler)\\.service$' || true); do`}function Yn(e){return[_i(e.slug,e.siteName),' systemctl stop "$unit"',"done"]}function gu(e,t,r){return`${e}-${t}-queue-${r}`}function Su(e,t,r,n){return`${e}-${t}-daemon-${r.name?Qo(r.name):Qo(r.command).slice(0,32)||String(n)}`}function qn(e){let t=["[Unit]",`Description=${e.description}`,"After=network.target","","[Service]","Type=simple",`WorkingDirectory=${e.workingDir}`,...Object.entries(e.environment??{}).map(([r,n])=>`Environment="${r}=${n}"`),...e.environmentFile?[`EnvironmentFile=-${e.environmentFile}`]:[],`ExecStart=${e.execStart}`,`Restart=${e.restart||"always"}`,"RestartSec=5","MemoryAccounting=true",`MemoryHigh=${e.memoryHigh??"2G"}`,...e.memoryMax?[`MemoryMax=${e.memoryMax}`]:[]];if(e.isolatedUser){if(!e.isolationRoot||!e.isolationSlug)throw Error("Runtime isolation requires its project directory");t.push(...Ht(e.isolatedUser,e.isolationRoot,e.isolationSlug))}else if(e.user)t.push(`User=${e.user}`);if(typeof e.stopWaitSecs==="number")t.push(`TimeoutStopSec=${e.stopWaitSecs}`);return t.push("","[Install]","WantedBy=multi-user.target"),`${t.join(`
451
462
  `)}
452
- `}function Wn(e,t){return[`cat > /etc/systemd/system/${e}.service <<'TS_CLOUD_UNIT_EOF'`,t.replace(/\n$/,""),"TS_CLOUD_UNIT_EOF"]}function Su(e,t){return`/etc/cron.d/${e}-${t}-scheduler`}function Xn(e){let t=e.site.isolation&&!["laravel","php","statamic","wordpress"].includes(e.site.type||"")?pt(e.slug):void 0,{slug:r,siteName:n,site:s}=e,i=e.appBase??`/var/www/${n}`,a=`${i}/current`,c=Xo(qr(s)),f=c.id==="stacks"&&s.backgroundBun!==void 0;if(f&&(!e.releaseId||!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(e.releaseId)))throw Error("Stacks backgroundBun requires a safe physical releaseId");let g=f?`${i}/releases/${e.releaseId}`:a,h={current:g,backgroundBun:s.backgroundBun},S=c.executionEnvironment?.(h)??c.execEnv,_=[],v=[];(s.queues||[]).forEach((k,j)=>{let q=c.wrapExec(c.queueWorkerCommand(k,h)),B=Math.max(1,k.processes??1);for(let re=0;re<B;re++){let ee=gu(r,n,v.length);v.push(ee),_.push(...Wn(ee,qn({description:`${n} queue worker ${j}.${re} (managed by ts-cloud)`,workingDir:g,execStart:q,environment:S,isolatedUser:t,isolationRoot:i,isolationSlug:r,stopWaitSecs:k.stopWaitSecs??90})))}}),(s.daemons||[]).forEach((k,j)=>{if(t&&k.user&&k.user!==t)throw Error("An isolated runtime daemon cannot change its project identity");let q=Math.max(1,k.processes??1),B=k.directory||a,re=k.command.trim().replace(/^\.\//,()=>`${B.replace(/\/+$/,"")}/`);for(let ee=0;ee<q;ee++){let ne=`${hu(r,n,k,j)}-${ee}`;v.push(ne),_.push(...Wn(ne,qn({description:`${n} daemon ${k.name||k.command} (managed by ts-cloud)`,workingDir:B,execStart:c.wrapExec(re),environment:c.execEnv,environmentFile:`${a}/.env`,restart:k.restart,user:k.user,isolatedUser:t,isolationRoot:i,isolationSlug:r})))}});let L=s.scheduler,K=L===!0||typeof L==="object"&&L!==null,M=`${r}-${n}-scheduler`;if(K&&c.schedulerMode==="daemon"){v.push(M);let k=typeof L==="object"&&L!==null?L:{};_.push(...Wn(M,qn({description:`${n} scheduler (managed by ts-cloud)`,workingDir:g,execStart:c.wrapExec(c.schedulerCommand(h)),environment:S,isolatedUser:t,isolationRoot:i,isolationSlug:r,memoryHigh:k.memoryHigh,memoryMax:k.memoryMax})),...wr(`${M}.service`,{auto:!1,high:Boolean(k.memoryHigh),max:Boolean(k.memoryMax)}))}let D=v.map((k)=>`${k}.service`).join(" ");_.push("systemctl daemon-reload",`TS_CLOUD_DESIRED="${D}"`,Qo(r,n),' case " $TS_CLOUD_DESIRED " in',' *" $unit "*) ;;',' *) systemctl stop "$unit"; systemctl disable "$unit"; rm -f "/etc/systemd/system/$unit" ;;'," esac","done","systemctl daemon-reload");for(let k of v)_.push(`systemctl enable ${k}.service`,`systemctl restart ${k}.service`);let N=Su(r,n);if(K&&c.schedulerMode==="cron"){let k=typeof L==="object"&&L!==null?L:void 0,j=`${c.schedulerCommand(h)} >> /dev/null 2>&1`;if(k?.heartbeatUrl){let B=k.heartbeatMethod||"GET",re=B==="GET"?"":`-X ${B} `;j+=` && curl -fsS -m 10 ${re}${fu(k.heartbeatUrl)} >/dev/null 2>&1`}let q=`* * * * * root ${j}
453
- `.replace(/%/g,"\\%");_.push(`cat > ${N} <<'TS_CLOUD_CRON_EOF'`,q.replace(/\n$/,""),"TS_CLOUD_CRON_EOF",`chmod 644 ${N}`)}else _.push(`rm -f ${N}`);return _}function _i(e){return!!(e.queues?.length||e.daemons?.length||e.scheduler)}function Jn(e){if(e.ssl?.provider)return e.ssl.provider;return e.domain?"letsencrypt":"none"}var $i={cloudflare:{pkg:"python3-certbot-dns-cloudflare",plugin:"dns-cloudflare"},route53:{pkg:"python3-certbot-dns-route53",plugin:"dns-route53"},digitalocean:{pkg:"python3-certbot-dns-digitalocean",plugin:"dns-digitalocean"},google:{pkg:"python3-certbot-dns-google",plugin:"dns-google"}};function Oi(e){return`/etc/letsencrypt/ts-cloud-${e}.ini`}function _u(e){let t=e?$i[e.provider]:void 0,r=["certbot","python3-certbot-nginx",...t?[t.pkg]:[]].join(" ");return["export DEBIAN_FRONTEND=noninteractive",t?`apt-get update -y && apt-get install -y ${r}`:`command -v certbot >/dev/null 2>&1 || { apt-get update -y && apt-get install -y ${r}; }`,"mkdir -p /etc/letsencrypt/renewal-hooks/deploy","cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'TS_CLOUD_HOOK_EOF'","#!/bin/sh","systemctl reload ts-cloud-nginx 2>/dev/null || systemctl reload nginx 2>/dev/null || true","TS_CLOUD_HOOK_EOF","chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh","systemctl enable certbot.timer 2>/dev/null || true","systemctl start certbot.timer 2>/dev/null || true"]}function vu(e){if(!e.credentials||Object.keys(e.credentials).length===0)return[];let t=Oi(e.provider),r=Object.entries(e.credentials).map(([n,s])=>`${n} = ${s}`).join(`
454
- `);return[`cat > ${t} <<'TS_CLOUD_DNSCREDS_EOF'`,r,"TS_CLOUD_DNSCREDS_EOF",`chmod 600 ${t}`]}function $u(e){let t=e.dns,r=e.wildcard?[`*.${e.domain}`,e.domain]:[e.domain,...e.aliases||[]].filter(Boolean),n=["certbot","--non-interactive","--agree-tos","--keep-until-expiring"];if(t){let s=$i[t.provider].plugin;if(n.push(`--${s}`),t.provider!=="route53"&&t.credentials)n.push(`--${s}-credentials ${Oi(t.provider)}`);if(typeof t.propagationSeconds==="number"&&t.provider!=="route53")n.push(`--${s}-propagation-seconds ${t.propagationSeconds}`);n.push("certonly")}else n.push("--nginx",`--nginx-ctl ${Fe}`,e.redirect===!1?"--no-redirect":"--redirect");if(e.email)n.push(`-m ${e.email}`);else n.push("--register-unsafely-without-email");for(let s of r)n.push(`-d ${s}`);return[n.join(" ")]}function Zn(e){if(Jn(e)!=="letsencrypt"||!e.domain)return[];let t=e.ssl,r=t?.dns,n=t?.wildcard===!0;if(n&&!r)return[];return[..._u(r),...r?vu(r):[],...$u({domain:e.domain,aliases:e.aliases,email:t?.email,wildcard:n,dns:r})]}function xi(e){let{repository:t,releaseDir:r,commit:n}=e,s=t.branch||"main",i=t.url,a=[`rm -rf ${r}`,`mkdir -p ${r}`];if(t.strategy==="tag")if(t.tag)a.push(`git clone -q --depth 1 --branch ${Ee(t.tag)} ${Ee(i)} ${r}`),a.push(`printf '%s' ${Ee(t.tag)} > ${r}/.ts-cloud-tag`);else{let c=t.tagPattern||"v*";a.push(`TS_CLOUD_TAG="$(git ls-remote --tags --refs --sort=-v:refname ${Ee(i)} ${Ee(`refs/tags/${c}`)} | head -n1 | sed 's#.*refs/tags/##')"`,`test -n "$TS_CLOUD_TAG" || { echo "no tags matching" ${Ee(c)} "found in" ${Ee(i)} >&2; exit 1; }`,`git clone -q --depth 1 --branch "$TS_CLOUD_TAG" ${Ee(i)} ${r}`,`printf '%s' "$TS_CLOUD_TAG" > ${r}/.ts-cloud-tag`)}else if(n)a.push(`git -C ${r} init -q`,`git -C ${r} remote add origin ${Ee(i)}`,`git -C ${r} fetch -q --depth 1 origin ${Ee(n)}`,`git -C ${r} checkout -q FETCH_HEAD`);else a.push(`git clone -q --depth 1 --branch ${Ee(s)} ${Ee(i)} ${r}`);return a.push(`git -C ${r} rev-parse HEAD > ${r}/.ts-cloud-sha`),a}function Ee(e){return`'${e.split("'").join("'\\''")}'`}var Br="$CREATE_RELEASE",Wr="$ACTIVATE_RELEASE",Hi="$RESTART_QUEUES",Li="composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev",yu=["php artisan migrate --force","php artisan config:cache","php artisan route:cache","php artisan view:cache","php artisan event:cache","php artisan storage:link"];function bu(e){switch(e){case"laravel":case"statamic":return[Br,Li,...yu,Wr,Hi];case"php":case"wordpress":return[Br,`${Li} || true`,Wr];case"static":case"spa":return[Br,Wr]}}function Tu(e,t){return e.replace(/^php\s+/,`${t} `).replace(/(\s)php\s+artisan\s+/g,`$1${t} artisan `)}function wu(e,t){let r=hr(t);return[`cat > ${e} <<'TS_CLOUD_ENV_EOF'`,r,"TS_CLOUD_ENV_EOF",`chmod 600 ${e}`]}function Ui(e,t,r){return[`cat > ${e} <<'${r}'`,t.replace(/\n$/,""),r,`chmod 600 ${e}`]}function Cu(e,t){if(!t)return[];let r=[];if(t.composerAuth){let n=typeof t.composerAuth==="string"?t.composerAuth:JSON.stringify(t.composerAuth,null,2);r.push(...Ui(`${e}/auth.json`,n,"TS_CLOUD_AUTHJSON_EOF"))}if(t.npmrc)r.push(...Ui(`${e}/.npmrc`,t.npmrc,"TS_CLOUD_NPMRC_EOF"));return r}function Ni(e){let t=e.healthCheck?.path;if(!t||!e.domain)return[];let n=`http://127.0.0.1${t.startsWith("/")?t:`/${t}`}`;return[`echo "[ts-cloud] health check ${n} (Host: ${e.domain})"`,"TS_CLOUD_HC_OK=0",`for i in $(seq 1 10); do TS_CLOUD_HC=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: ${e.domain}" -m 10 "${n}" 2>/dev/null || echo 000); case "$TS_CLOUD_HC" in 2*|3*) TS_CLOUD_HC_OK=1; break ;; esac; sleep 3; done`,'[ "$TS_CLOUD_HC_OK" = 1 ] && echo "health check passed ($TS_CLOUD_HC)" || { echo "health check FAILED (last=$TS_CLOUD_HC)" >&2; exit 1; }']}function Mi(e){let{siteName:t,site:r,releaseId:n,commit:s}=e;if(!r.repository?.url)throw Error(`Site '${t}' is a PHP/git site but has no repository.url to clone`);let i=e.appBase??`/var/www/${t}`,a="php",c=er(i,n),f=r.sharedPaths??Sr,g=[...f,...Tr(r.env,f)],h=r.keepReleases??Mt,S=r.deployScript?.length?r.deployScript:bu(r.type??"laravel"),_=["set -euo pipefail",'export HOME="${HOME:-/root}"','export COMPOSER_HOME="${COMPOSER_HOME:-/root/.composer}"',"export COMPOSER_ALLOW_SUPERUSER=1",Re()];if(_.push(...br(i,{releaseId:n,commit:s,branch:r.repository.branch,keepLogs:h})),_.push(...Ft(c,g)),r.env&&Object.keys(r.env).length>0)_.push(...wu(`${c.shared}/.env`,r.env));for(let v of S){let b=v.trim();if(b===Br)_.push(...xi({repository:r.repository,releaseDir:c.release,commit:s})),_.push(...yr(c,g)),_.push(`cd ${c.release}`),_.push(...Cu(c.release,r.credentials)),_.push(`chown -R www-data:www-data ${c.shared}/storage 2>/dev/null || true`,`[ -d ${c.release}/bootstrap/cache ] && chown -R www-data:www-data ${c.release}/bootstrap/cache 2>/dev/null || true`,`chmod -R ug+rwX ${c.shared}/storage 2>/dev/null || true`);else if(b===Wr)_.push(...dt(c)),_.push(...mt(c,h)),_.push(`(cd ${u} && pantry restart php-fpm) 2>/dev/null || true`);else if(b===Hi)_.push(`${a} artisan queue:restart || true`);else _.push(Tu(v,a))}return _}var Yr={mysql:{port:3306,binaries:["mysqld","mysql"]},mariadb:{port:3306,binaries:["mariadbd","mariadb","mysqld"]},postgres:{port:5432,binaries:["postgres","psql"]},redis:{port:6379,binaries:["redis-server","redis-cli"]},memcached:{port:11211,binaries:["memcached"]},meilisearch:{port:7700,binaries:["meilisearch"]},vitess:{port:15306,binaries:["vtgate"]}},Qt="ts-cloud-service:";function Pu(e){return e===!0||typeof e==="object"&&e!=null}function zi(e){if(!e)return[];return Object.keys(Yr).filter((t)=>Pu(e[t]))}function Fi(e){if(e.length===0)return[];return[Re(),"ts_cloud_port_open() {",' if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | grep -qE "[:.]$1[[:space:]]" && return 0; fi',' if command -v netstat >/dev/null 2>&1; then netstat -ltn 2>/dev/null | grep -qE "[:.]$1[[:space:]]" && return 0; fi'," (exec 3<>/dev/tcp/127.0.0.1/$1) 2>/dev/null && { exec 3<&- 3>&-; return 0; }"," return 1","}","ts_cloud_probe() {",' TS_CLOUD_SVC="$1"; TS_CLOUD_PORT="$2"; shift 2',' for TS_CLOUD_BIN in "$@"; do',` if command -v "$TS_CLOUD_BIN" >/dev/null 2>&1; then echo "${Qt}$TS_CLOUD_SVC:present"; return 0; fi`," done",` if ts_cloud_port_open "$TS_CLOUD_PORT"; then echo "${Qt}$TS_CLOUD_SVC:present"; return 0; fi`,` echo "${Qt}$TS_CLOUD_SVC:missing"`," return 0","}",...e.filter((t)=>Yr[t]).map((t)=>`ts_cloud_probe ${t} ${Yr[t].port} ${Yr[t].binaries.join(" ")}`),"exit 0"]}function Ki(e,t){if(!e)return[];let r=new Map;for(let n of e.split(`
455
- `)){let s=n.indexOf(Qt);if(s===-1)continue;let[i,a]=n.slice(s+Qt.length).trim().split(":");if(i)r.set(i,a)}return t.filter((n)=>r.get(n)==="missing")}function ji(e,t,r){let n=r?` on ${r}`:"",s=e.map((a)=>`managedServices.${a}`).join(", "),i=e.length===1?`no ${e[0]}`:`none of ${e.join(", ")}`;return`This project declares ${s}, but '${t}' has ${i}${n}. Attach mode does not provision services on the owner's box, so nothing will install ${e.length===1?"it":"them"}. Point the app at an external service, or ask the owner of '${t}' to add ${e.length===1?"it":"them"} to their own config and re-provision.`}var Gi="/var/lib/pantry/php-fpm/pool.d",Eu=9100,Ru=400;function Xr(e){return(e.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"site").slice(0,28)}function Vi(e){return`web_${Xr(e)}`}function Ou(e){let t=Xr(e),r=0;for(let n=0;n<t.length;n++)r=r*31+t.charCodeAt(n)>>>0;return Eu+r%Ru}function Qn(e){return`127.0.0.1:${Ou(e)}`}function Du(e){let t=Xr(e.siteName),r=Vi(e.siteName),n=e.maxChildren??10;return[`; ts-cloud-managed pool for ${e.siteName} (site isolation)`,`[${t}]`,`user = ${r}`,`group = ${r}`,`listen = ${Qn(e.siteName)}`,"pm = dynamic",`pm.max_children = ${n}`,"pm.start_servers = 2","pm.min_spare_servers = 1","pm.max_spare_servers = 3","pm.max_requests = 500","catch_workers_output = yes",`php_admin_value[open_basedir] = ${e.appBase}:/tmp`,""].join(`
456
- `)}function qi(e){let t=Vi(e.siteName),r=Xr(e.siteName),n=`${Gi}/${r}.conf`;return[`getent group ${t} >/dev/null || groupadd --system ${t}`,`id -u ${t} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin -g ${t} ${t}`,`usermod -aG ${t} www-data 2>/dev/null || true`,`chown -R ${t}:${t} ${e.appBase} || true`,`chmod -R g+rX ${e.appBase} || true`,`chmod 600 ${e.appBase}/shared/.env 2>/dev/null || true`,`mkdir -p ${Gi}`,`cat > ${n} <<'TS_CLOUD_FPMPOOL_EOF'`,Du(e).replace(/\n$/,""),"TS_CLOUD_FPMPOOL_EOF",`(cd ${u} && pantry restart php-fpm) 2>/dev/null || true`]}var Zr={info:()=>{},warn:()=>{},error:()=>{},step:()=>{},success:()=>{}};function Lu(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function Ao(e,t,r=Zr){let{config:n,environment:s,siteName:i,site:a,slug:c,sha:f,runtime:g,localTarballPath:h}=t,S=`releases/${i}/${f}.tar.gz`,_=m(n,s),v=await e.getComputeOutputs({config:n,environment:s}),b=await e.findComputeTargets({slug:c,environment:s,role:"app",stackName:_});if(b.length===0)return{success:!1,error:e.name==="aws"?`Stack '${_}' has no EC2 instances tagged Project=${c} Environment=${s} Role=app, and storage/cloud/state/${_}.json pins no live instance. For a shared box, record its instanceId there.`:`No Hetzner servers labeled ts-cloud/project=${c} ts-cloud/environment=${s} ts-cloud/role=app, and storage/cloud/state/${_}.json pins no live server. For a shared box, record its serverId there.`};let T=v.servicesPrivateIp?Zs(v.servicesPrivateIp,p(n)):ps(p(n)),L=Zi(C(n,{environment:s})),K=n.infrastructure?.compute,M=si(K?.hostCleanup),D=z({...K,proxy:R(n,{environment:s})})?{HOST:"127.0.0.1"}:{},N=s==="production"?{APP_ENV:"production",NODE_ENV:"production"}:{},k={...T,...L,...D,...a.env||{},...N},j=Rn(k,a.sharedPaths??[]);if(j.warning)r.warn(`Site '${i}': ${j.warning}`);else if(j.path)r.info(`Site '${i}': sharing '${j.path}' so the SQLite database survives deploys.`);if(Kr(a)){let E=a.phpVersion??K?.php?.default??K?.php?.versions?.[0],I=Q(c,i),w=Object.keys(T).length>0?{...a,env:k}:a,G=Mi({siteName:i,site:w,releaseId:f,appBase:I,defaultPhpVersion:E}),Z=!z(K),J=Jn(a)==="custom"&&a.ssl?.certPath&&a.ssl?.keyPath?{certPath:a.ssl.certPath,keyPath:a.ssl.keyPath}:void 0,te=a.isolation?qi({siteName:i,appBase:I}):[],O=Z?fn({siteName:i,domain:a.domain||i,aliases:a.aliases,type:a.type,appDir:`${I}/current`,webDirectory:a.webDirectory,phpVersion:E,fastcgiPass:a.isolation?Qn(i):void 0,redirects:a.redirects,ssl:J,auth:a.auth&&a.auth.enabled!==!1&&a.auth.password?{username:a.auth.username||"admin",password:a.auth.password,realm:a.auth.realm}:void 0,serverSnippet:mn(a.nginx,K?.nginxTemplates),clientMaxBodySize:a.nginx?.clientMaxBodySize,hsts:a.ssl?.hsts,tlsProtocols:a.ssl?.tlsProtocols,security:a.security}):[],A=Z?Zn(a):[],fe=_i(a)?Xn({slug:c,siteName:i,site:a,phpVersion:E,appBase:I}):[],le=Z?Ni(a):[];r.step(`Deploying PHP site '${i}' to ${b.length} target(s)...`);let be=await e.runRemoteDeploy({targets:b,commands:[...En(I,c),...Fr(I,c),...G,...te,...O,...A,...fe,...le,...M],comment:`ts-cloud deploy ${c}/${i}@${f}`,tags:{Project:c,Environment:s,Role:"app"}}),Ot=hs(n.notifications,a.notifications);if(!be.success)return await Wi(e,b,M,r),await Sn(Ot,"deploy-failed",`❌ Deploy of ${c}/${i}@${f} failed: ${be.error||"unknown error"}`),{success:!1,error:be.error||"Remote PHP deploy failed",instanceCount:be.instanceCount,perInstance:be.perInstance};let Qi=a.domain?` → https://${a.domain}`:"";return await Sn(Ot,"deploy",`✅ Deployed ${c}/${i}@${f}${Qi}`),{success:!0,instanceCount:be.instanceCount,perInstance:be.perInstance}}if(!h)return{success:!1,error:`Site '${i}' requires a release tarball but none was provided`};let q=await e.uploadRelease({config:n,environment:s,localPath:h,remoteKey:S,targets:b}),B=di(c,i,f),re=e.name==="aws"?zc(v.deployBucketName,S,n.project.region||"us-east-1",B):Kc(q.artifactRef,B),ee=d(a),ne=Q(c,i),oe=Xn({slug:c,siteName:i,site:a,appBase:ne,releaseId:f}),ie=ee==="server-static"?ci({siteName:i,slug:c,appDir:ne,artifactFetch:re,releaseId:f,preStartCommands:a.preStart,postActivateCommands:oe,beforeResetCommands:Yn({slug:c,siteName:i})}):ai({siteName:i,slug:c,appDir:ne,artifactFetch:re,releaseId:f,execStart:oi(a.start,g,`${ne.replace(/\/+$/,"")}/releases/%i`),envEntries:k,isolation:a.isolation,port:a.port,preStartCommands:a.preStart,postActivateCommands:oe,beforeResetCommands:Yn({slug:c,siteName:i}),sharedPaths:a.sharedPaths,zeroDowntime:a.zeroDowntime!==!1,healthCheckPath:a.healthCheck?.path,liveness:a.liveness,memoryHigh:a.memoryHigh,memoryMax:a.memoryMax,cpuWeight:a.cpuWeight,ioWeight:a.ioWeight,tasksMax:a.tasksMax,stopTimeout:a.stopTimeout}),se=ee==="server-static"&&!z(K)&&!!a.domain,de=se?fn({siteName:i,domain:a.domain,aliases:a.aliases,type:a.type==="spa"?"spa":"static",appDir:`${ne}/current`,webDirectory:"",redirects:a.redirects,auth:a.auth&&a.auth.enabled!==!1&&a.auth.password?{username:a.auth.username||"admin",password:a.auth.password,realm:a.auth.realm}:void 0,serverSnippet:mn(a.nginx,K?.nginxTemplates),clientMaxBodySize:a.nginx?.clientMaxBodySize,hsts:a.ssl?.hsts,tlsProtocols:a.ssl?.tlsProtocols,security:a.security}):[],me=se?Zn(a):[],X=[...br(ne,{releaseId:f,commit:f,branch:a.repository?.branch??"main"}),...En(ne,c),...Fr(ne,c),...ie,...de,...me,...M];r.step(`Deploying to ${b.length} target(s)...`);let P=await e.runRemoteDeploy({targets:b,commands:X,comment:`ts-cloud deploy ${c}/${i}@${f}`,tags:{Project:c,Environment:s,Role:"app"}});if(!P.success)return await Wi(e,b,M,r),{success:!1,error:P.error||"Remote deploy failed",instanceCount:P.instanceCount,perInstance:P.perInstance};return{success:!0,instanceCount:P.instanceCount,perInstance:P.perInstance}}async function Wi(e,t,r,n){if(r.length===0)return;try{let s=await e.runRemoteDeploy({targets:t,commands:r,comment:"ts-cloud host cleanup after failed deploy"});if(!s.success)n.warn(`Host cleanup after the failed deploy did not complete: ${s.error||"unknown error"}`)}catch(s){n.warn(`Host cleanup after the failed deploy did not run: ${s.message}`)}}function Jr(e){return`'${e.replaceAll("'","'\\''")}'`}function Uu(e,t,r=!1,n=[]){let s=`${e}-dashboard-`,i=t.map((c)=>`${e}-${c}.service`),a=r?[`export TS_CLOUD_DASHBOARD_KEEP_DOMAINS=${Jr(JSON.stringify(n))}`,"rm -f /run/ts-cloud-dashboard-routes-changed",`/usr/local/bin/bun --bun -e ${Jr(`
463
+ `}function Wn(e,t){return[`cat > /etc/systemd/system/${e}.service <<'TS_CLOUD_UNIT_EOF'`,t.replace(/\n$/,""),"TS_CLOUD_UNIT_EOF"]}function vu(e,t){return`/etc/cron.d/${e}-${t}-scheduler`}function Xn(e){let t=e.site.isolation&&!["laravel","php","statamic","wordpress"].includes(e.site.type||"")?pt(e.slug):void 0,{slug:r,siteName:n,site:s}=e,i=e.appBase??`/var/www/${n}`,a=`${i}/current`,c=Jo(qr(s)),f=c.id==="stacks"&&s.backgroundBun!==void 0;if(f&&(!e.releaseId||!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(e.releaseId)))throw Error("Stacks backgroundBun requires a safe physical releaseId");let h=f?`${i}/releases/${e.releaseId}`:a,g={current:h,backgroundBun:s.backgroundBun},v=c.executionEnvironment?.(g)??c.execEnv,_=[],b=[];(s.queues||[]).forEach((B,Z)=>{let ee=c.wrapExec(c.queueWorkerCommand(B,g)),Y=Math.max(1,B.processes??1);for(let se=0;se<Y;se++){let re=gu(r,n,b.length);b.push(re),_.push(...Wn(re,qn({description:`${n} queue worker ${Z}.${se} (managed by ts-cloud)`,workingDir:h,execStart:ee,environment:v,isolatedUser:t,isolationRoot:i,isolationSlug:r,stopWaitSecs:B.stopWaitSecs??90})))}}),(s.daemons||[]).forEach((B,Z)=>{if(t&&B.user&&B.user!==t)throw Error("An isolated runtime daemon cannot change its project identity");let ee=Math.max(1,B.processes??1),Y=B.directory||a,se=B.command.trim().replace(/^\.\//,()=>`${Y.replace(/\/+$/,"")}/`);for(let re=0;re<ee;re++){let oe=`${Su(r,n,B,Z)}-${re}`;b.push(oe),_.push(...Wn(oe,qn({description:`${n} daemon ${B.name||B.command} (managed by ts-cloud)`,workingDir:Y,execStart:c.wrapExec(se),environment:c.execEnv,environmentFile:`${a}/.env`,restart:B.restart,user:B.user,isolatedUser:t,isolationRoot:i,isolationSlug:r})))}});let M=s.scheduler,q=M===!0||typeof M==="object"&&M!==null,G=`${r}-${n}-scheduler`;if(q&&c.schedulerMode==="daemon"){b.push(G);let B=typeof M==="object"&&M!==null?M:{};_.push(...Wn(G,qn({description:`${n} scheduler (managed by ts-cloud)`,workingDir:h,execStart:c.wrapExec(c.schedulerCommand(g)),environment:v,isolatedUser:t,isolationRoot:i,isolationSlug:r,memoryHigh:B.memoryHigh,memoryMax:B.memoryMax})),...wr(`${G}.service`,{auto:!1,high:Boolean(B.memoryHigh),max:Boolean(B.memoryMax)}))}let L=b.map((B)=>`${B}.service`).join(" ");_.push("systemctl daemon-reload",`TS_CLOUD_DESIRED="${L}"`,_i(r,n),' case " $TS_CLOUD_DESIRED " in',' *" $unit "*) ;;',' *) systemctl stop "$unit"; systemctl disable "$unit"; rm -f "/etc/systemd/system/$unit" ;;'," esac","done","systemctl daemon-reload");for(let B of b)_.push(`systemctl enable ${B}.service`,`systemctl restart ${B}.service`);let j=vu(r,n);if(q&&c.schedulerMode==="cron"){let B=typeof M==="object"&&M!==null?M:void 0,Z=`${c.schedulerCommand(g)} >> /dev/null 2>&1`;if(B?.heartbeatUrl){let Y=B.heartbeatMethod||"GET",se=Y==="GET"?"":`-X ${Y} `;Z+=` && curl -fsS -m 10 ${se}${hu(B.heartbeatUrl)} >/dev/null 2>&1`}let ee=`* * * * * root ${Z}
464
+ `.replace(/%/g,"\\%");_.push(`cat > ${j} <<'TS_CLOUD_CRON_EOF'`,ee.replace(/\n$/,""),"TS_CLOUD_CRON_EOF",`chmod 644 ${j}`)}else _.push(`rm -f ${j}`);return _}function $i(e){return!!(e.queues?.length||e.daemons?.length||e.scheduler)}function Jn(e){if(e.ssl?.provider)return e.ssl.provider;return e.domain?"letsencrypt":"none"}var Oi={cloudflare:{pkg:"python3-certbot-dns-cloudflare",plugin:"dns-cloudflare"},route53:{pkg:"python3-certbot-dns-route53",plugin:"dns-route53"},digitalocean:{pkg:"python3-certbot-dns-digitalocean",plugin:"dns-digitalocean"},google:{pkg:"python3-certbot-dns-google",plugin:"dns-google"}};function xi(e){return`/etc/letsencrypt/ts-cloud-${e}.ini`}function yu(e){let t=e?Oi[e.provider]:void 0,r=["certbot","python3-certbot-nginx",...t?[t.pkg]:[]].join(" ");return["export DEBIAN_FRONTEND=noninteractive",t?`apt-get update -y && apt-get install -y ${r}`:`command -v certbot >/dev/null 2>&1 || { apt-get update -y && apt-get install -y ${r}; }`,"mkdir -p /etc/letsencrypt/renewal-hooks/deploy","cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'TS_CLOUD_HOOK_EOF'","#!/bin/sh","systemctl reload ts-cloud-nginx 2>/dev/null || systemctl reload nginx 2>/dev/null || true","TS_CLOUD_HOOK_EOF","chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh","systemctl enable certbot.timer 2>/dev/null || true","systemctl start certbot.timer 2>/dev/null || true"]}function _u(e){if(!e.credentials||Object.keys(e.credentials).length===0)return[];let t=xi(e.provider),r=Object.entries(e.credentials).map(([n,s])=>`${n} = ${s}`).join(`
465
+ `);return[`cat > ${t} <<'TS_CLOUD_DNSCREDS_EOF'`,r,"TS_CLOUD_DNSCREDS_EOF",`chmod 600 ${t}`]}function $u(e){let t=e.dns,r=e.wildcard?[`*.${e.domain}`,e.domain]:[e.domain,...e.aliases||[]].filter(Boolean),n=["certbot","--non-interactive","--agree-tos","--keep-until-expiring"];if(t){let s=Oi[t.provider].plugin;if(n.push(`--${s}`),t.provider!=="route53"&&t.credentials)n.push(`--${s}-credentials ${xi(t.provider)}`);if(typeof t.propagationSeconds==="number"&&t.provider!=="route53")n.push(`--${s}-propagation-seconds ${t.propagationSeconds}`);n.push("certonly")}else n.push("--nginx",`--nginx-ctl ${ze}`,e.redirect===!1?"--no-redirect":"--redirect");if(e.email)n.push(`-m ${e.email}`);else n.push("--register-unsafely-without-email");for(let s of r)n.push(`-d ${s}`);return[n.join(" ")]}function Zn(e){if(Jn(e)!=="letsencrypt"||!e.domain)return[];let t=e.ssl,r=t?.dns,n=t?.wildcard===!0;if(n&&!r)return[];return[...yu(r),...r?_u(r):[],...$u({domain:e.domain,aliases:e.aliases,email:t?.email,wildcard:n,dns:r})]}function Li(e){let{repository:t,releaseDir:r,commit:n}=e,s=t.branch||"main",i=t.url,a=[`rm -rf ${r}`,`mkdir -p ${r}`];if(t.strategy==="tag")if(t.tag)a.push(`git clone -q --depth 1 --branch ${Ee(t.tag)} ${Ee(i)} ${r}`),a.push(`printf '%s' ${Ee(t.tag)} > ${r}/.ts-cloud-tag`);else{let c=t.tagPattern||"v*";a.push(`TS_CLOUD_TAG="$(git ls-remote --tags --refs --sort=-v:refname ${Ee(i)} ${Ee(`refs/tags/${c}`)} | head -n1 | sed 's#.*refs/tags/##')"`,`test -n "$TS_CLOUD_TAG" || { echo "no tags matching" ${Ee(c)} "found in" ${Ee(i)} >&2; exit 1; }`,`git clone -q --depth 1 --branch "$TS_CLOUD_TAG" ${Ee(i)} ${r}`,`printf '%s' "$TS_CLOUD_TAG" > ${r}/.ts-cloud-tag`)}else if(n)a.push(`git -C ${r} init -q`,`git -C ${r} remote add origin ${Ee(i)}`,`git -C ${r} fetch -q --depth 1 origin ${Ee(n)}`,`git -C ${r} checkout -q FETCH_HEAD`);else a.push(`git clone -q --depth 1 --branch ${Ee(s)} ${Ee(i)} ${r}`);return a.push(`git -C ${r} rev-parse HEAD > ${r}/.ts-cloud-sha`),a}function Ee(e){return`'${e.split("'").join("'\\''")}'`}var Br="$CREATE_RELEASE",Wr="$ACTIVATE_RELEASE",Ni="$RESTART_QUEUES",Ui="composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev",bu=["php artisan migrate --force","php artisan config:cache","php artisan route:cache","php artisan view:cache","php artisan event:cache","php artisan storage:link"];function Tu(e){switch(e){case"laravel":case"statamic":return[Br,Ui,...bu,Wr,Ni];case"php":case"wordpress":return[Br,`${Ui} || true`,Wr];case"static":case"spa":return[Br,Wr]}}function wu(e,t){return e.replace(/^php\s+/,`${t} `).replace(/(\s)php\s+artisan\s+/g,`$1${t} artisan `)}function Cu(e,t){let r=gr(t);return[`cat > ${e} <<'TS_CLOUD_ENV_EOF'`,r,"TS_CLOUD_ENV_EOF",`chmod 600 ${e}`]}function Hi(e,t,r){return[`cat > ${e} <<'${r}'`,t.replace(/\n$/,""),r,`chmod 600 ${e}`]}function Pu(e,t){if(!t)return[];let r=[];if(t.composerAuth){let n=typeof t.composerAuth==="string"?t.composerAuth:JSON.stringify(t.composerAuth,null,2);r.push(...Hi(`${e}/auth.json`,n,"TS_CLOUD_AUTHJSON_EOF"))}if(t.npmrc)r.push(...Hi(`${e}/.npmrc`,t.npmrc,"TS_CLOUD_NPMRC_EOF"));return r}function Mi(e){let t=e.healthCheck?.path;if(!t||!e.domain)return[];let n=`http://127.0.0.1${t.startsWith("/")?t:`/${t}`}`;return[`echo "[ts-cloud] health check ${n} (Host: ${e.domain})"`,"TS_CLOUD_HC_OK=0",`for i in $(seq 1 10); do TS_CLOUD_HC=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: ${e.domain}" -m 10 "${n}" 2>/dev/null || echo 000); case "$TS_CLOUD_HC" in 2*|3*) TS_CLOUD_HC_OK=1; break ;; esac; sleep 3; done`,'[ "$TS_CLOUD_HC_OK" = 1 ] && echo "health check passed ($TS_CLOUD_HC)" || { echo "health check FAILED (last=$TS_CLOUD_HC)" >&2; exit 1; }']}function Fi(e){let{siteName:t,site:r,releaseId:n,commit:s}=e;if(!r.repository?.url)throw Error(`Site '${t}' is a PHP/git site but has no repository.url to clone`);let i=e.appBase??`/var/www/${t}`,a="php",c=er(i,n),f=r.sharedPaths??Sr,h=[...f,...Tr(r.env,f)],g=r.keepReleases??Mt,v=r.deployScript?.length?r.deployScript:Tu(r.type??"laravel"),_=["set -euo pipefail",'export HOME="${HOME:-/root}"','export COMPOSER_HOME="${COMPOSER_HOME:-/root/.composer}"',"export COMPOSER_ALLOW_SUPERUSER=1",Re()];if(_.push(...br(i,{releaseId:n,commit:s,branch:r.repository.branch,keepLogs:g})),_.push(...zt(c,h)),r.env&&Object.keys(r.env).length>0)_.push(...Cu(`${c.shared}/.env`,r.env));for(let b of v){let S=b.trim();if(S===Br)_.push(...Li({repository:r.repository,releaseDir:c.release,commit:s})),_.push(...yr(c,h)),_.push(`cd ${c.release}`),_.push(...Pu(c.release,r.credentials)),_.push(`chown -R www-data:www-data ${c.shared}/storage 2>/dev/null || true`,`[ -d ${c.release}/bootstrap/cache ] && chown -R www-data:www-data ${c.release}/bootstrap/cache 2>/dev/null || true`,`chmod -R ug+rwX ${c.shared}/storage 2>/dev/null || true`);else if(S===Wr)_.push(...dt(c)),_.push(...mt(c,g)),_.push(`(cd ${u} && pantry restart php-fpm) 2>/dev/null || true`);else if(S===Ni)_.push(`${a} artisan queue:restart || true`);else _.push(wu(b,a))}return _}var Yr={mysql:{port:3306,binaries:["mysqld","mysql"]},mariadb:{port:3306,binaries:["mariadbd","mariadb","mysqld"]},postgres:{port:5432,binaries:["postgres","psql"]},redis:{port:6379,binaries:["redis-server","redis-cli"]},memcached:{port:11211,binaries:["memcached"]},meilisearch:{port:7700,binaries:["meilisearch"]},vitess:{port:15306,binaries:["vtgate"]}},Qt="ts-cloud-service:";function Eu(e){return e===!0||typeof e==="object"&&e!=null}function zi(e){if(!e)return[];return Object.keys(Yr).filter((t)=>Eu(e[t]))}function Ki(e){if(e.length===0)return[];return[Re(),"ts_cloud_port_open() {",' if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | grep -qE "[:.]$1[[:space:]]" && return 0; fi',' if command -v netstat >/dev/null 2>&1; then netstat -ltn 2>/dev/null | grep -qE "[:.]$1[[:space:]]" && return 0; fi'," (exec 3<>/dev/tcp/127.0.0.1/$1) 2>/dev/null && { exec 3<&- 3>&-; return 0; }"," return 1","}","ts_cloud_probe() {",' TS_CLOUD_SVC="$1"; TS_CLOUD_PORT="$2"; shift 2',' for TS_CLOUD_BIN in "$@"; do',` if command -v "$TS_CLOUD_BIN" >/dev/null 2>&1; then echo "${Qt}$TS_CLOUD_SVC:present"; return 0; fi`," done",` if ts_cloud_port_open "$TS_CLOUD_PORT"; then echo "${Qt}$TS_CLOUD_SVC:present"; return 0; fi`,` echo "${Qt}$TS_CLOUD_SVC:missing"`," return 0","}",...e.filter((t)=>Yr[t]).map((t)=>`ts_cloud_probe ${t} ${Yr[t].port} ${Yr[t].binaries.join(" ")}`),"exit 0"]}function ji(e,t){if(!e)return[];let r=new Map;for(let n of e.split(`
466
+ `)){let s=n.indexOf(Qt);if(s===-1)continue;let[i,a]=n.slice(s+Qt.length).trim().split(":");if(i)r.set(i,a)}return t.filter((n)=>r.get(n)==="missing")}function Gi(e,t,r){let n=r?` on ${r}`:"",s=e.map((a)=>`managedServices.${a}`).join(", "),i=e.length===1?`no ${e[0]}`:`none of ${e.join(", ")}`;return`This project declares ${s}, but '${t}' has ${i}${n}. Attach mode does not provision services on the owner's box, so nothing will install ${e.length===1?"it":"them"}. Point the app at an external service, or ask the owner of '${t}' to add ${e.length===1?"it":"them"} to their own config and re-provision.`}var Vi="/var/lib/pantry/php-fpm/pool.d",Ru=9100,Ou=400;function Xr(e){return(e.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"site").slice(0,28)}function qi(e){return`web_${Xr(e)}`}function Du(e){let t=Xr(e),r=0;for(let n=0;n<t.length;n++)r=r*31+t.charCodeAt(n)>>>0;return Ru+r%Ou}function Qn(e){return`127.0.0.1:${Du(e)}`}function xu(e){let t=Xr(e.siteName),r=qi(e.siteName),n=e.maxChildren??10;return[`; ts-cloud-managed pool for ${e.siteName} (site isolation)`,`[${t}]`,`user = ${r}`,`group = ${r}`,`listen = ${Qn(e.siteName)}`,"pm = dynamic",`pm.max_children = ${n}`,"pm.start_servers = 2","pm.min_spare_servers = 1","pm.max_spare_servers = 3","pm.max_requests = 500","catch_workers_output = yes",`php_admin_value[open_basedir] = ${e.appBase}:/tmp`,""].join(`
467
+ `)}function Wi(e){let t=qi(e.siteName),r=Xr(e.siteName),n=`${Vi}/${r}.conf`;return[`getent group ${t} >/dev/null || groupadd --system ${t}`,`id -u ${t} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin -g ${t} ${t}`,`usermod -aG ${t} www-data 2>/dev/null || true`,`chown -R ${t}:${t} ${e.appBase} || true`,`chmod -R g+rX ${e.appBase} || true`,`chmod 600 ${e.appBase}/shared/.env 2>/dev/null || true`,`mkdir -p ${Vi}`,`cat > ${n} <<'TS_CLOUD_FPMPOOL_EOF'`,xu(e).replace(/\n$/,""),"TS_CLOUD_FPMPOOL_EOF",`(cd ${u} && pantry restart php-fpm) 2>/dev/null || true`]}var Zr={info:()=>{},warn:()=>{},error:()=>{},step:()=>{},success:()=>{}};function Uu(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function Ao(e,t,r=Zr){let{config:n,environment:s,siteName:i,site:a,slug:c,sha:f,runtime:h,localTarballPath:g}=t,v=`releases/${i}/${f}.tar.gz`,_=m(n,s),b=await e.getComputeOutputs({config:n,environment:s}),S=await e.findComputeTargets({slug:c,environment:s,role:"app",stackName:_});if(S.length===0)return{success:!1,error:e.name==="aws"?`Stack '${_}' has no EC2 instances tagged Project=${c} Environment=${s} Role=app, and storage/cloud/state/${_}.json pins no live instance. For a shared box, record its instanceId there.`:`No Hetzner servers labeled ts-cloud/project=${c} ts-cloud/environment=${s} ts-cloud/role=app, and storage/cloud/state/${_}.json pins no live server. For a shared box, record its serverId there.`};let P=b.servicesPrivateIp?Qs(b.servicesPrivateIp,p(n)):ps(p(n)),M=Zi(C(n,{environment:s})),q=n.infrastructure?.compute,G=si(q?.hostCleanup),L=z({...q,proxy:R(n,{environment:s})})?{HOST:"127.0.0.1"}:{},j=s==="production"?{APP_ENV:"production",NODE_ENV:"production"}:{},B={...P,...M,...L,...a.env||{},...j},Z=Rn(B,a.sharedPaths??[]);if(Z.warning)r.warn(`Site '${i}': ${Z.warning}`);else if(Z.path)r.info(`Site '${i}': sharing '${Z.path}' so the SQLite database survives deploys.`);if(Kr(a)){let w=a.phpVersion??q?.php?.default??q?.php?.versions?.[0],I=Q(c,i),O=Object.keys(P).length>0?{...a,env:B}:a,N=Fi({siteName:i,site:O,releaseId:f,appBase:I,defaultPhpVersion:w}),te=!z(q),J=Jn(a)==="custom"&&a.ssl?.certPath&&a.ssl?.keyPath?{certPath:a.ssl.certPath,keyPath:a.ssl.keyPath}:void 0,ne=a.isolation?Wi({siteName:i,appBase:I}):[],D=te?fn({siteName:i,domain:a.domain||i,aliases:a.aliases,type:a.type,appDir:`${I}/current`,webDirectory:a.webDirectory,phpVersion:w,fastcgiPass:a.isolation?Qn(i):void 0,redirects:a.redirects,ssl:J,auth:a.auth&&a.auth.enabled!==!1&&a.auth.password?{username:a.auth.username||"admin",password:a.auth.password,realm:a.auth.realm}:void 0,serverSnippet:mn(a.nginx,q?.nginxTemplates),clientMaxBodySize:a.nginx?.clientMaxBodySize,hsts:a.ssl?.hsts,tlsProtocols:a.ssl?.tlsProtocols,security:a.security}):[],k=te?Zn(a):[],ue=$i(a)?Xn({slug:c,siteName:i,site:a,phpVersion:w,appBase:I}):[],de=te?Mi(a):[];r.step(`Deploying PHP site '${i}' to ${S.length} target(s)...`);let be=await e.runRemoteDeploy({targets:S,commands:[...En(I,c),...Fr(I,c),...N,...ne,...D,...k,...ue,...de,...G],comment:`ts-cloud deploy ${c}/${i}@${f}`,tags:{Project:c,Environment:s,Role:"app"}}),Ot=gs(n.notifications,a.notifications);if(!be.success)return await Xi(e,S,G,r),await Sn(Ot,"deploy-failed",`❌ Deploy of ${c}/${i}@${f} failed: ${be.error||"unknown error"}`),{success:!1,error:be.error||"Remote PHP deploy failed",instanceCount:be.instanceCount,perInstance:be.perInstance};let ea=a.domain?` → https://${a.domain}`:"";return await Sn(Ot,"deploy",`✅ Deployed ${c}/${i}@${f}${ea}`),{success:!0,instanceCount:be.instanceCount,perInstance:be.perInstance}}if(!g)return{success:!1,error:`Site '${i}' requires a release tarball but none was provided`};let ee=await e.uploadRelease({config:n,environment:s,localPath:g,remoteKey:v,targets:S}),Y=di(c,i,f),se=e.name==="aws"?zc(b.deployBucketName,v,n.project.region||"us-east-1",Y):Kc(ee.artifactRef,Y),re=d(a),oe=Q(c,i),me=Xn({slug:c,siteName:i,site:a,appBase:oe,releaseId:f}),le=re==="server-static"?ci({siteName:i,slug:c,appDir:oe,artifactFetch:se,releaseId:f,preStartCommands:a.preStart,postActivateCommands:me,beforeResetCommands:Yn({slug:c,siteName:i})}):ai({siteName:i,slug:c,appDir:oe,artifactFetch:se,releaseId:f,execStart:oi(a.start,h,`${oe.replace(/\/+$/,"")}/releases/%i`),envEntries:B,isolation:a.isolation,port:a.port,preStartCommands:a.preStart,postActivateCommands:me,beforeResetCommands:Yn({slug:c,siteName:i}),sharedPaths:a.sharedPaths,zeroDowntime:a.zeroDowntime!==!1,healthCheckPath:a.healthCheck?.path,liveness:a.liveness,memoryHigh:a.memoryHigh,memoryMax:a.memoryMax,cpuWeight:a.cpuWeight,ioWeight:a.ioWeight,tasksMax:a.tasksMax,stopTimeout:a.stopTimeout}),ie=re==="server-static"&&!z(q)&&!!a.domain,E=ie?fn({siteName:i,domain:a.domain,aliases:a.aliases,type:a.type==="spa"?"spa":"static",appDir:`${oe}/current`,webDirectory:"",redirects:a.redirects,auth:a.auth&&a.auth.enabled!==!1&&a.auth.password?{username:a.auth.username||"admin",password:a.auth.password,realm:a.auth.realm}:void 0,serverSnippet:mn(a.nginx,q?.nginxTemplates),clientMaxBodySize:a.nginx?.clientMaxBodySize,hsts:a.ssl?.hsts,tlsProtocols:a.ssl?.tlsProtocols,security:a.security}):[],X=ie?Zn(a):[],A=[...br(oe,{releaseId:f,commit:f,branch:a.repository?.branch??"main"}),...En(oe,c),...Fr(oe,c),...le,...E,...X,...G];r.step(`Deploying to ${S.length} target(s)...`);let T=await e.runRemoteDeploy({targets:S,commands:A,comment:`ts-cloud deploy ${c}/${i}@${f}`,tags:{Project:c,Environment:s,Role:"app"}});if(!T.success)return await Xi(e,S,G,r),{success:!1,error:T.error||"Remote deploy failed",instanceCount:T.instanceCount,perInstance:T.perInstance};return{success:!0,instanceCount:T.instanceCount,perInstance:T.perInstance}}async function Xi(e,t,r,n){if(r.length===0)return;try{let s=await e.runRemoteDeploy({targets:t,commands:r,comment:"ts-cloud host cleanup after failed deploy"});if(!s.success)n.warn(`Host cleanup after the failed deploy did not complete: ${s.error||"unknown error"}`)}catch(s){n.warn(`Host cleanup after the failed deploy did not run: ${s.message}`)}}function Jr(e){return`'${e.replaceAll("'","'\\''")}'`}function ku(e,t,r=!1,n=[]){let s=`${e}-dashboard-`,i=t.map((c)=>`${e}-${c}.service`),a=r?[`export TS_CLOUD_DASHBOARD_KEEP_DOMAINS=${Jr(JSON.stringify(n))}`,"rm -f /run/ts-cloud-dashboard-routes-changed",`/usr/local/bin/bun --bun -e ${Jr(`
457
468
  const { chmodSync, existsSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } = require('node:fs')
458
469
  const { basename, join } = require('node:path')
459
470
  const root = '/etc/rpx/sites.d'
@@ -481,4 +492,4 @@ This password is now in your deploy log. Set TS_CLOUD_UI_PASSWORD to a value of
481
492
  changed = true
482
493
  }
483
494
  if (changed) writeFileSync('/run/ts-cloud-dashboard-routes-changed', '')
484
- `)}`,"if [ -e /run/ts-cloud-dashboard-routes-changed ]; then"," systemctl restart rpx-gateway.service"," rm -f /run/ts-cloud-dashboard-routes-changed","fi"]:[];return["set -euo pipefail",`TS_CLOUD_DASHBOARD_PREFIX=${Jr(s)}`,`TS_CLOUD_DASHBOARD_SERVER_OWNER=${r?"1":"0"}`,`TS_CLOUD_DASHBOARD_DESIRED=${Jr(` ${i.join(" ")} `)}`,"for TS_CLOUD_UNIT_FILE in /etc/systemd/system/*.service; do",' [ -e "$TS_CLOUD_UNIT_FILE" ] || continue',' TS_CLOUD_UNIT=$(basename "$TS_CLOUD_UNIT_FILE")',' case "$TS_CLOUD_UNIT" in'," *-dashboard-*.service)",' if [ "$TS_CLOUD_DASHBOARD_SERVER_OWNER" != 1 ]; then',' case "$TS_CLOUD_UNIT" in "$TS_CLOUD_DASHBOARD_PREFIX"*.service) ;; *) continue ;; esac'," fi",' case "$TS_CLOUD_DASHBOARD_DESIRED" in',' *" $TS_CLOUD_UNIT "*) ;;'," *)",' systemctl disable --now "$TS_CLOUD_UNIT" 2>/dev/null || true',' rm -f "$TS_CLOUD_UNIT_FILE"'," ;;"," esac"," ;;"," esac","done","systemctl daemon-reload","systemctl reset-failed",...a]}async function ku(e,t,r,n){let{config:s,environment:i}=t,a=s.project.slug,c=m(s,i),f=await e.findComputeTargets({slug:a,environment:i,role:"app",stackName:c});if(f.length===0)return!0;let g=await e.runRemoteDeploy({targets:f,commands:Uu(a,r,!s.cloud?.attachTo,r.map((h)=>s.sites?.[h]?.domain).filter((h)=>typeof h==="string")),comment:`ts-cloud reconcile management dashboard ${a}`,tags:{Project:a,Environment:i,Role:"app"}});if(!g.success)return n.error(`Management dashboard reconciliation failed: ${g.error||"unknown error"}`),!1;return!0}async function Hu(e,t,r){let{config:n}=t,s=n.cloud?.attachTo;if(!s||!e.listReachableResources)return;let i;try{i=await e.listReachableResources()}catch(f){r.warn(`Could not determine this credential's reach: ${f instanceof Error?f.message:String(f)}`);return}let a=Ho(i,{ownerSlug:s,selfSlug:n.project.slug}),c=zn(a)>0;for(let f of No(a,{ownerSlug:s,selfSlug:n.project.slug}))if(c)r.warn(f);else r.info(f)}async function Nu(e,t,r){let{config:n,environment:s}=t,i=n.cloud?.attachTo;if(!i)return!0;let a=zi(n.infrastructure?.compute?.managedServices);if(a.length===0)return!0;let c=n.project.slug,f=m(n,s),g=await e.findComputeTargets({slug:c,environment:s,role:"app",stackName:f});if(g.length===0)return!0;let h=await e.runRemoteDeploy({targets:g,commands:Fi(a),comment:`ts-cloud preflight managed services ${c}`,tags:{Project:c,Environment:s,Role:"app"}});if(!h.success)return r.warn(`Could not check '${i}' for the declared services: ${h.error||"unknown error"}`),!0;let S=new Set;for(let _ of h.perInstance)for(let v of Ki(_.output,a))S.add(v);if(S.size===0)return!0;return r.error(ji([...S],i,g[0]?.publicIp)),!1}async function Mu(e,t,r){let{config:n,environment:s}=t,i=n.infrastructure?.compute,a=p(n);if(n.cloud?.attachTo&&a?.requireLocalPasswordAuth)throw Error("PostgreSQL host authentication can only be managed by an owned compute installation");if(!n.cloud?.attachTo&&!a?.requireLocalPasswordAuth)return!0;let c=ct(a,i?.managedServices??{});if(c.length===0)return!0;let f=n.project.slug,g=m(n,s),h=await e.findComputeTargets({slug:f,environment:s,role:"app",stackName:g});if(h.length===0)return!0;r.step(`Ensuring database '${a?.name}' + role exist on the shared box...`);let S=await e.runRemoteDeploy({targets:h,commands:c,comment:`ts-cloud ensure database ${f}/${a?.name}`,tags:{Project:f,Environment:s,Role:"app"}});if(!S.success)return r.error(`Ensuring database '${a?.name}' failed: ${S.error||"unknown error"}`),!1;return r.success(`Database '${a?.name}' is ready on ${S.instanceCount} target(s)`),!0}async function hd(e){let{config:t,environment:r,driver:n,sha:s,runtime:i,tarballForSite:a,logger:c=Zr}=e,f=t.project.slug,g=e.cwd??process.cwd(),h=e.managementDashboard!==!1,S=mr(t);if(h)Pi(t,{cwd:g,logger:{info:c.info,warn:c.warn}});let _=!h||S?[]:Gn(t);if(h&&e.rpxConfig&&e.rpxConfig!==t&&!mr(e.rpxConfig))Pi(e.rpxConfig,{cwd:g,logger:{info:()=>{},warn:()=>{}}});if(h&&!await ku(n,e,Gn(t),c))return!1;let v=null,b=new Map;if(_.length>0)if(v=ho(t.sites?.[_[0]],{cwd:g,slug:f,sha:s,siteName:_[0],logger:{info:c.info,warn:c.warn}}),!v){c.warn("Management dashboard: no artifact available — skipping dashboard site(s) for this deploy.");for(let D of _)if(t.sites)delete t.sites[D]}else for(let D of _){if(D===_[0]){b.set(D,v);continue}try{let N=Iu(Au(),`${f}-${D}-${s}.tar.gz`);xu(v,N),b.set(D,N)}catch{b.set(D,v)}}let T=t.sites||{},L=Object.entries(T).filter(([D,N])=>{if(!N)return!1;let k=d(N);if(k==="bucket")return c.warn(`Site '${D}' targets a bucket — skipping (handled by the static-site path, not compute).`),!1;if(k==="redirect")return!1;if(k==="proxy")return!1;return!0});if(L.length===0)return ki(e);if(await Hu(n,e,c),!await Nu(n,e,c))return!1;if(!await Mu(n,e,c))return!1;let K=(D)=>b.get(D)??a(D);for(let[D,N]of L){c.step(`Deploying site: ${D}`);let k=Kr(N)?void 0:K(D),j=await Ao(n,{config:t,environment:r,siteName:D,site:N,slug:f,sha:s,runtime:i,localTarballPath:k},c);if(!j.success){if(c.error(`Deploy of '${D}' failed: ${j.error||"unknown error"}`),j.perInstance)for(let q of j.perInstance)c.error(` ${q.instanceId}: ${q.status}${q.error?` — ${q.error}`:""}`);return!1}if(c.success(`Deployed ${f}/${D}@${s} to ${j.instanceCount} target(s)`),j.perInstance)for(let q of j.perInstance)c.info(` ✓ ${q.instanceId}: ${q.status}`)}if(!await ki(e))return!1;return!0}async function zu(e,t,r){let n=Bp(e);if(!n||n.tls==="off")return n;let s=await Xe(t,r),i=s?.provider==="ssh"?s.lanIp:void 0;return i?{...n,ip:i}:n}async function ki(e){let{config:t,environment:r,driver:n,logger:s=Zr}=e,i=e.rpxConfig??t,a=R(i,{environment:r})??R(t,{environment:r});if(a?.engine!=="rpx")return!0;let c=i.sites||{},f=t.project.slug,g=m(t,r),h=await n.findComputeTargets({slug:f,environment:r,role:"lb",stackName:g});if(h.length>0){let M=(await n.findComputeTargets({slug:f,environment:r,role:"app",stackName:g})).map((k)=>({privateIp:k.privateIp,publicIp:k.publicIp})),D=nr(c,M,{proxy:a,slug:f});if(D.proxies.length===0)return s.warn("rpx gateway: no server sites with a domain to route — skipping gateway reload."),!0;s.step(`Reloading the load balancer's rpx gateway with ${D.proxies.length} route(s)...`);let N=await n.runRemoteDeploy({targets:h,commands:_e({config:D,slug:f,preserveManagementDashboardRoutes:e.managementDashboard===!1}),comment:`ts-cloud rpx gateway reload ${f}`,tags:{Project:f,Environment:r,Role:"lb"}});if(!N.success)return s.error(`rpx gateway reload failed: ${N.error||"unknown error"}`),!1;return s.success(`rpx gateway reloaded on ${N.instanceCount} load balancer(s)`),!0}let S=await zu(t,g,e.cwd),_=x(c,{proxy:a,slug:f,lan:S,mail:C(t,{environment:r})});if(_.proxies.length===0)return s.warn("rpx gateway: no server sites with a domain to route — skipping gateway reload."),!0;let v=await n.findComputeTargets({slug:f,environment:r,role:"app",stackName:g});if(v.length===0)return s.warn("rpx gateway: no compute targets found — skipping gateway reload."),!0;let b=Boolean(t.cloud?.attachTo);if(b&&(a.version||a.tlsxVersion))s.warn(`rpx gateway: ignoring proxy.version/tlsxVersion — this project attaches to '${t.cloud?.attachTo}' and the gateway belongs to that box's owner.`);s.step(`Reloading rpx gateway with ${_.proxies.length} route(s)...`);let T=V({proxy:a,config:_,slug:f,tenant:b,profile:xp(t),preserveManagementDashboardRoutes:e.managementDashboard===!1}),L=await n.runRemoteDeploy({targets:v,commands:T,comment:`ts-cloud rpx gateway reload ${f}`,tags:{Project:f,Environment:r,Role:"app"}});if(!L.success)return s.error(`rpx gateway reload failed: ${L.error||"unknown error"}`),!1;return s.success(`rpx gateway reloaded on ${L.instanceCount} target(s)`),!0}async function Ad(e){let{config:t,environment:r,driver:n,logger:s=Zr}=e,i=e.rpxConfig??t,a=R(i,{environment:r})??R(t,{environment:r});if(a?.engine!=="rpx"||!a.onDemandTls)return!0;let c=x(i.sites||{},{proxy:a,slug:t.project.slug,mail:C(t,{environment:r})});if(c.proxies.length===0)return!0;let f=Cd(c);if(f.length===0)return!0;let g=t.project.slug,h=m(t,r),S="lb",_=await n.findComputeTargets({slug:g,environment:r,role:S,stackName:h});if(_.length===0)S="app",_=await n.findComputeTargets({slug:g,environment:r,role:S,stackName:h});if(_.length===0)return s.warn("rpx TLS: no gateway targets found - skipping certificate issuance."),!0;s.step("Issuing rpx TLS certificates after DNS reconciliation...");let v=f.map((T)=>`test -s ${Lu(`${c.productionCerts.certsDir}/${T}.crt`)}`).join(" && "),b=await n.runRemoteDeploy({targets:_,commands:[`systemctl start ${bd(g)}`,v],comment:`ts-cloud rpx TLS issuance ${g}`,tags:{Project:g,Environment:r,Role:S}});if(!b.success)return s.error(`rpx TLS issuance failed: ${b.error||"unknown error"}`),!1;return s.success(`rpx TLS certificates reconciled on ${b.instanceCount} gateway target(s)`),!0}var Dp="658327ea-f89d-4fab-a63d-7e88639e58f6",Pp="4135ea2d-6df8-44a3-9df3-4b5a84be39ad",kp="216adef6-5c7f-47e4-b989-5492eafa07d3",es="origin",Xi="origin-failover";function Ji(e,t){let r={TargetOriginId:es,ViewerProtocolPolicy:"redirect-to-https",Compress:!0,CachePolicyId:t==="dynamic"?Pp:Dp,OriginRequestPolicyId:kp,SmoothStreaming:!1,FieldLevelEncryptionId:"",TrustedSigners:{Enabled:!1,Quantity:0},TrustedKeyGroups:{Enabled:!1,Quantity:0},LambdaFunctionAssociations:{Quantity:0},FunctionAssociations:{Quantity:0},AllowedMethods:t==="dynamic"?{Quantity:7,Items:["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"],CachedMethods:{Quantity:2,Items:["GET","HEAD"]}}:{Quantity:3,Items:["GET","HEAD","OPTIONS"],CachedMethods:{Quantity:2,Items:["GET","HEAD"]}}};if(e!==null)r.PathPattern=e;return r}function lA(e){let t=e.aliases;if(t.length===0)throw Error("buildCloudFrontOriginConfig: at least one alias is required");if(t.includes(e.originDomain))throw Error(`buildCloudFrontOriginConfig: originDomain ${e.originDomain} must not be one of the aliases (it would loop)`);if(e.originShield&&!e.originShieldRegion)throw Error("buildCloudFrontOriginConfig: originShieldRegion is required when originShield is enabled");let r=e.failoverOriginDomain;if(r!==void 0){if(t.includes(r))throw Error(`buildCloudFrontOriginConfig: failoverOriginDomain ${r} must not be one of the aliases (it would loop)`);if(r===e.originDomain)throw Error("buildCloudFrontOriginConfig: failoverOriginDomain must differ from originDomain")}else if(e.failoverStatusCodes)throw Error("buildCloudFrontOriginConfig: failoverStatusCodes needs failoverOriginDomain to fail over to");let n=e.originSecretHeader??"X-Origin-Verify",s=e.originSecret?{Quantity:1,Items:[{HeaderName:n,HeaderValue:e.originSecret}]}:{Quantity:0},i=[...e.behaviors??[]].sort((_,v)=>v.pathPattern.length-_.pathPattern.length),a=pe(e,"buildCloudFrontOriginConfig"),c=(_,v,b,T={})=>({Id:_,DomainName:v,OriginPath:"",CustomHeaders:s,CustomOriginConfig:{HTTPPort:80,HTTPSPort:443,OriginProtocolPolicy:"https-only",OriginSslProtocols:{Quantity:1,Items:["TLSv1.2"]},OriginReadTimeout:30,OriginKeepaliveTimeout:5},ConnectionAttempts:T.ConnectionAttempts??3,ConnectionTimeout:T.ConnectionTimeout??10,OriginShield:b,OriginAccessControlId:""}),f=[c(es,e.originDomain,e.originShield?{Enabled:!0,OriginShieldRegion:e.originShieldRegion}:{Enabled:!1},a)],g={Quantity:0},h=Ji(null,"static");if(r!==void 0)f.push(c(Xi,r,{Enabled:!1})),g=gn({primaryOriginId:es,secondaryOriginId:Xi,statusCodes:e.failoverStatusCodes}),Nt(h.AllowedMethods,"the default cache behavior"),h.TargetOriginId=g.Items[0].Id;let S={CallerReference:e.callerReference??e.originDomain,Comment:e.comment??`Origin-fronted distribution for ${t[0]} → ${e.originDomain}`,Enabled:!0,Aliases:{Quantity:t.length,Items:t},DefaultRootObject:"",Origins:{Quantity:f.length,Items:f},OriginGroups:g,DefaultCacheBehavior:h,CacheBehaviors:{Quantity:i.length,Items:i.map((_)=>Ji(_.pathPattern,_.kind))},ViewerCertificate:{CloudFrontDefaultCertificate:!1,ACMCertificateArn:e.viewerCertificateArn,Certificate:e.viewerCertificateArn,CertificateSource:"acm",SSLSupportMethod:"sni-only",MinimumProtocolVersion:"TLSv1.2_2021"},PriceClass:e.priceClass??"PriceClass_All",HttpVersion:"http2and3",IsIPV6Enabled:!0,Restrictions:{GeoRestriction:{RestrictionType:"none",Quantity:0}},Logging:{Enabled:!1,IncludeCookies:!1,Bucket:"",Prefix:""},WebACLId:"",CustomErrorResponses:{Quantity:0}};return Bt(S,"buildCloudFrontOriginConfig"),S}export{Qg,Vc,Hg,ae,qc,ei,Gg,jc,ti,Yt,_r,Zt,Vg,qg,Jh,u,yt,oo,Lr,Ke,$r,jg,ao,F,Mr,ri,ni,Fr,er,zg,Kg,Ur,Xh,Yh,Wg,Jg,Zh,eA,tA,Qr,ii,Xg,co,si,oi,ai,ci,di,zc,Kc,li,Hr,ve,Wc,St,Jc,ui,Xc,Yc,Gr,Zc,We,ce,tr,Yg,gi,ed,td,rd,nd,It,Je,Xe,bt,pi,mi,xe,lo,uo,Zg,ep,id,tp,Ct,fi,sd,rp,np,od,Et,Oe,ad,ip,sp,go,hi,vt,op,ap,cp,dp,lp,up,gp,pp,mp,Ai,yi,Si,Ii,bi,Ci,Ei,fp,hp,Vr,U,po,Ap,yp,Sp,cd,Ip,bp,rA,nA,iA,dd,Cp,sA,oA,aA,vi,Ti,wi,Ri,ld,ud,Di,gd,cA,mo,Ep,vp,Tp,wp,Rp,pd,dA,md,fo,fd,rr,Pi,ho,qr,Ao,hd,ki,Ad,Dp,Pp,kp,lA};
495
+ `)}`,"if [ -e /run/ts-cloud-dashboard-routes-changed ]; then"," systemctl restart rpx-gateway.service"," rm -f /run/ts-cloud-dashboard-routes-changed","fi"]:[];return["set -euo pipefail",`TS_CLOUD_DASHBOARD_PREFIX=${Jr(s)}`,`TS_CLOUD_DASHBOARD_SERVER_OWNER=${r?"1":"0"}`,`TS_CLOUD_DASHBOARD_DESIRED=${Jr(` ${i.join(" ")} `)}`,"for TS_CLOUD_UNIT_FILE in /etc/systemd/system/*.service; do",' [ -e "$TS_CLOUD_UNIT_FILE" ] || continue',' TS_CLOUD_UNIT=$(basename "$TS_CLOUD_UNIT_FILE")',' case "$TS_CLOUD_UNIT" in'," *-dashboard-*.service)",' if [ "$TS_CLOUD_DASHBOARD_SERVER_OWNER" != 1 ]; then',' case "$TS_CLOUD_UNIT" in "$TS_CLOUD_DASHBOARD_PREFIX"*.service) ;; *) continue ;; esac'," fi",' case "$TS_CLOUD_DASHBOARD_DESIRED" in',' *" $TS_CLOUD_UNIT "*) ;;'," *)",' systemctl disable --now "$TS_CLOUD_UNIT" 2>/dev/null || true',' rm -f "$TS_CLOUD_UNIT_FILE"'," ;;"," esac"," ;;"," esac","done","systemctl daemon-reload","systemctl reset-failed",...a]}async function Hu(e,t,r,n){let{config:s,environment:i}=t,a=s.project.slug,c=m(s,i),f=await e.findComputeTargets({slug:a,environment:i,role:"app",stackName:c});if(f.length===0)return!0;let h=await e.runRemoteDeploy({targets:f,commands:ku(a,r,!s.cloud?.attachTo,r.map((g)=>s.sites?.[g]?.domain).filter((g)=>typeof g==="string")),comment:`ts-cloud reconcile management dashboard ${a}`,tags:{Project:a,Environment:i,Role:"app"}});if(!h.success)return n.error(`Management dashboard reconciliation failed: ${h.error||"unknown error"}`),!1;return!0}async function Nu(e,t,r){let{config:n}=t,s=n.cloud?.attachTo;if(!s||!e.listReachableResources)return;let i;try{i=await e.listReachableResources()}catch(f){r.warn(`Could not determine this credential's reach: ${f instanceof Error?f.message:String(f)}`);return}let a=No(i,{ownerSlug:s,selfSlug:n.project.slug}),c=Fn(a)>0;for(let f of Mo(a,{ownerSlug:s,selfSlug:n.project.slug}))if(c)r.warn(f);else r.info(f)}async function Mu(e,t,r){let{config:n,environment:s}=t,i=n.cloud?.attachTo;if(!i)return!0;let a=zi(n.infrastructure?.compute?.managedServices);if(a.length===0)return!0;let c=n.project.slug,f=m(n,s),h=await e.findComputeTargets({slug:c,environment:s,role:"app",stackName:f});if(h.length===0)return!0;let g=await e.runRemoteDeploy({targets:h,commands:Ki(a),comment:`ts-cloud preflight managed services ${c}`,tags:{Project:c,Environment:s,Role:"app"}});if(!g.success)return r.warn(`Could not check '${i}' for the declared services: ${g.error||"unknown error"}`),!0;let v=new Set;for(let _ of g.perInstance)for(let b of ji(_.output,a))v.add(b);if(v.size===0)return!0;return r.error(Gi([...v],i,h[0]?.publicIp)),!1}async function Fu(e,t,r){let{config:n,environment:s}=t,i=n.infrastructure?.compute,a=p(n);if(n.cloud?.attachTo&&a?.requireLocalPasswordAuth)throw Error("PostgreSQL host authentication can only be managed by an owned compute installation");if(!n.cloud?.attachTo&&!a?.requireLocalPasswordAuth)return!0;let c=ct(a,i?.managedServices??{});if(c.length===0)return!0;let f=n.project.slug,h=m(n,s),g=await e.findComputeTargets({slug:f,environment:s,role:"app",stackName:h});if(g.length===0)return!0;r.step(`Ensuring database '${a?.name}' + role exist on the shared box...`);let v=await e.runRemoteDeploy({targets:g,commands:c,comment:`ts-cloud ensure database ${f}/${a?.name}`,tags:{Project:f,Environment:s,Role:"app"}});if(!v.success)return r.error(`Ensuring database '${a?.name}' failed: ${v.error||"unknown error"}`),!1;return r.success(`Database '${a?.name}' is ready on ${v.instanceCount} target(s)`),!0}async function hd(e){let{config:t,environment:r,driver:n,sha:s,runtime:i,tarballForSite:a,logger:c=Zr}=e,f=t.project.slug,h=e.cwd??process.cwd(),g=e.managementDashboard!==!1,v=mr(t);if(g)Pi(t,{cwd:h,logger:{info:c.info,warn:c.warn}});let _=!g||v?[]:Gn(t);if(g&&e.rpxConfig&&e.rpxConfig!==t&&!mr(e.rpxConfig))Pi(e.rpxConfig,{cwd:h,logger:{info:()=>{},warn:()=>{}}});if(g&&!await Hu(n,e,Gn(t),c))return!1;let b=null,S=new Map;if(_.length>0)if(b=ho(t.sites?.[_[0]],{cwd:h,slug:f,sha:s,siteName:_[0],logger:{info:c.info,warn:c.warn}}),!b){c.warn("Management dashboard: no artifact available — skipping dashboard site(s) for this deploy.");for(let L of _)if(t.sites)delete t.sites[L]}else for(let L of _){if(L===_[0]){S.set(L,b);continue}try{let j=Lu(Iu(),`${f}-${L}-${s}.tar.gz`);Au(b,j),S.set(L,j)}catch{S.set(L,b)}}let P=t.sites||{},M=Object.entries(P).filter(([L,j])=>{if(!j)return!1;let B=d(j);if(B==="bucket")return c.warn(`Site '${L}' targets a bucket — skipping (handled by the static-site path, not compute).`),!1;if(B==="redirect")return!1;if(B==="proxy")return!1;return!0});if(M.length===0)return ki(e);if(await Nu(n,e,c),!await Mu(n,e,c))return!1;if(!await Fu(n,e,c))return!1;let q=(L)=>S.get(L)??a(L);for(let[L,j]of M){c.step(`Deploying site: ${L}`);let B=Kr(j)?void 0:q(L),Z=await Ao(n,{config:t,environment:r,siteName:L,site:j,slug:f,sha:s,runtime:i,localTarballPath:B},c);if(!Z.success){if(c.error(`Deploy of '${L}' failed: ${Z.error||"unknown error"}`),Z.perInstance)for(let ee of Z.perInstance)c.error(` ${ee.instanceId}: ${ee.status}${ee.error?` — ${ee.error}`:""}`);return!1}if(c.success(`Deployed ${f}/${L}@${s} to ${Z.instanceCount} target(s)`),Z.perInstance)for(let ee of Z.perInstance)c.info(` ✓ ${ee.instanceId}: ${ee.status}`)}if(!await ki(e))return!1;return!0}async function zu(e,t,r){let n=Bp(e);if(!n||n.tls==="off")return n;let s=await Xe(t,r),i=s?.provider==="ssh"?s.lanIp:void 0;return i?{...n,ip:i}:n}async function ki(e){let{config:t,environment:r,driver:n,logger:s=Zr}=e,i=e.rpxConfig??t,a=R(i,{environment:r})??R(t,{environment:r});if(a?.engine!=="rpx")return!0;let c=i.sites||{},f=t.project.slug,h=m(t,r),g=await n.findComputeTargets({slug:f,environment:r,role:"lb",stackName:h});if(g.length>0){let G=(await n.findComputeTargets({slug:f,environment:r,role:"app",stackName:h})).map((B)=>({privateIp:B.privateIp,publicIp:B.publicIp})),L=nr(c,G,{proxy:a,slug:f});if(L.proxies.length===0)return s.warn("rpx gateway: no server sites with a domain to route — skipping gateway reload."),!0;s.step(`Reloading the load balancer's rpx gateway with ${L.proxies.length} route(s)...`);let j=await n.runRemoteDeploy({targets:g,commands:_e({config:L,slug:f,preserveManagementDashboardRoutes:e.managementDashboard===!1}),comment:`ts-cloud rpx gateway reload ${f}`,tags:{Project:f,Environment:r,Role:"lb"}});if(!j.success)return s.error(`rpx gateway reload failed: ${j.error||"unknown error"}`),!1;return s.success(`rpx gateway reloaded on ${j.instanceCount} load balancer(s)`),!0}let v=await zu(t,h,e.cwd),_=x(c,{proxy:a,slug:f,lan:v,mail:C(t,{environment:r})});if(_.proxies.length===0)return s.warn("rpx gateway: no server sites with a domain to route — skipping gateway reload."),!0;let b=await n.findComputeTargets({slug:f,environment:r,role:"app",stackName:h});if(b.length===0)return s.warn("rpx gateway: no compute targets found — skipping gateway reload."),!0;let S=Boolean(t.cloud?.attachTo);if(S&&(a.version||a.tlsxVersion))s.warn(`rpx gateway: ignoring proxy.version/tlsxVersion — this project attaches to '${t.cloud?.attachTo}' and the gateway belongs to that box's owner.`);s.step(`Reloading rpx gateway with ${_.proxies.length} route(s)...`);let P=V({proxy:a,config:_,slug:f,tenant:S,profile:xp(t),preserveManagementDashboardRoutes:e.managementDashboard===!1}),M=await n.runRemoteDeploy({targets:b,commands:P,comment:`ts-cloud rpx gateway reload ${f}`,tags:{Project:f,Environment:r,Role:"app"}});if(!M.success)return s.error(`rpx gateway reload failed: ${M.error||"unknown error"}`),!1;return s.success(`rpx gateway reloaded on ${M.instanceCount} target(s)`),!0}async function Ad(e){let{config:t,environment:r,driver:n,logger:s=Zr}=e,i=e.rpxConfig??t,a=R(i,{environment:r})??R(t,{environment:r});if(a?.engine!=="rpx"||!a.onDemandTls)return!0;let c=x(i.sites||{},{proxy:a,slug:t.project.slug,mail:C(t,{environment:r})});if(c.proxies.length===0)return!0;let f=Cd(c);if(f.length===0)return!0;let h=t.project.slug,g=m(t,r),v="lb",_=await n.findComputeTargets({slug:h,environment:r,role:v,stackName:g});if(_.length===0)v="app",_=await n.findComputeTargets({slug:h,environment:r,role:v,stackName:g});if(_.length===0)return s.warn("rpx TLS: no gateway targets found - skipping certificate issuance."),!0;s.step("Issuing rpx TLS certificates after DNS reconciliation...");let b=f.map((P)=>`test -s ${Uu(`${c.productionCerts.certsDir}/${P}.crt`)}`).join(" && "),S=await n.runRemoteDeploy({targets:_,commands:[`systemctl start ${bd(h)}`,b],comment:`ts-cloud rpx TLS issuance ${h}`,tags:{Project:h,Environment:r,Role:v}});if(!S.success)return s.error(`rpx TLS issuance failed: ${S.error||"unknown error"}`),!1;return s.success(`rpx TLS certificates reconciled on ${S.instanceCount} gateway target(s)`),!0}var Dp="658327ea-f89d-4fab-a63d-7e88639e58f6",Pp="4135ea2d-6df8-44a3-9df3-4b5a84be39ad",kp="216adef6-5c7f-47e4-b989-5492eafa07d3",es="origin",Ji="origin-failover";function Qi(e,t){let r={TargetOriginId:es,ViewerProtocolPolicy:"redirect-to-https",Compress:!0,CachePolicyId:t==="dynamic"?Pp:Dp,OriginRequestPolicyId:kp,SmoothStreaming:!1,FieldLevelEncryptionId:"",TrustedSigners:{Enabled:!1,Quantity:0},TrustedKeyGroups:{Enabled:!1,Quantity:0},LambdaFunctionAssociations:{Quantity:0},FunctionAssociations:{Quantity:0},AllowedMethods:t==="dynamic"?{Quantity:7,Items:["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"],CachedMethods:{Quantity:2,Items:["GET","HEAD"]}}:{Quantity:3,Items:["GET","HEAD","OPTIONS"],CachedMethods:{Quantity:2,Items:["GET","HEAD"]}}};if(e!==null)r.PathPattern=e;return r}function lA(e){let t=e.aliases;if(t.length===0)throw Error("buildCloudFrontOriginConfig: at least one alias is required");if(t.includes(e.originDomain))throw Error(`buildCloudFrontOriginConfig: originDomain ${e.originDomain} must not be one of the aliases (it would loop)`);if(e.originShield&&!e.originShieldRegion)throw Error("buildCloudFrontOriginConfig: originShieldRegion is required when originShield is enabled");let r=e.failoverOriginDomain;if(r!==void 0){if(t.includes(r))throw Error(`buildCloudFrontOriginConfig: failoverOriginDomain ${r} must not be one of the aliases (it would loop)`);if(r===e.originDomain)throw Error("buildCloudFrontOriginConfig: failoverOriginDomain must differ from originDomain")}else if(e.failoverStatusCodes)throw Error("buildCloudFrontOriginConfig: failoverStatusCodes needs failoverOriginDomain to fail over to");let n=e.originSecretHeader??"X-Origin-Verify",s=e.originSecret?{Quantity:1,Items:[{HeaderName:n,HeaderValue:e.originSecret}]}:{Quantity:0},i=[...e.behaviors??[]].sort((_,b)=>b.pathPattern.length-_.pathPattern.length),a=pe(e,"buildCloudFrontOriginConfig"),c=(_,b,S,P={})=>({Id:_,DomainName:b,OriginPath:"",CustomHeaders:s,CustomOriginConfig:{HTTPPort:80,HTTPSPort:443,OriginProtocolPolicy:"https-only",OriginSslProtocols:{Quantity:1,Items:["TLSv1.2"]},OriginReadTimeout:30,OriginKeepaliveTimeout:5},ConnectionAttempts:P.ConnectionAttempts??3,ConnectionTimeout:P.ConnectionTimeout??10,OriginShield:S,OriginAccessControlId:""}),f=[c(es,e.originDomain,e.originShield?{Enabled:!0,OriginShieldRegion:e.originShieldRegion}:{Enabled:!1},a)],h={Quantity:0},g=Qi(null,"static");if(r!==void 0)f.push(c(Ji,r,{Enabled:!1})),h=gn({primaryOriginId:es,secondaryOriginId:Ji,statusCodes:e.failoverStatusCodes}),Nt(g.AllowedMethods,"the default cache behavior"),g.TargetOriginId=h.Items[0].Id;let v={CallerReference:e.callerReference??e.originDomain,Comment:e.comment??`Origin-fronted distribution for ${t[0]} → ${e.originDomain}`,Enabled:!0,Aliases:{Quantity:t.length,Items:t},DefaultRootObject:"",Origins:{Quantity:f.length,Items:f},OriginGroups:h,DefaultCacheBehavior:g,CacheBehaviors:{Quantity:i.length,Items:i.map((_)=>Qi(_.pathPattern,_.kind))},ViewerCertificate:{CloudFrontDefaultCertificate:!1,ACMCertificateArn:e.viewerCertificateArn,Certificate:e.viewerCertificateArn,CertificateSource:"acm",SSLSupportMethod:"sni-only",MinimumProtocolVersion:"TLSv1.2_2021"},PriceClass:e.priceClass??"PriceClass_All",HttpVersion:"http2and3",IsIPV6Enabled:!0,Restrictions:{GeoRestriction:{RestrictionType:"none",Quantity:0}},Logging:{Enabled:!1,IncludeCookies:!1,Bucket:"",Prefix:""},WebACLId:"",CustomErrorResponses:{Quantity:0}};return Bt(v,"buildCloudFrontOriginConfig"),v}export{Qg,Vc,Hg,ae,qc,ei,Gg,jc,ti,Yt,_r,Zt,Vg,qg,Jh,u,yt,oo,Lr,Ke,$r,jg,ao,F,Mr,ri,ni,Fr,er,zg,Kg,Ur,Xh,Yh,Wg,Jg,Zh,eA,tA,Qr,ii,Xg,co,si,oi,ai,ci,di,zc,Kc,li,Hr,ve,Wc,St,Jc,ui,Xc,Yc,Gr,Zc,We,ce,tr,Yg,gi,ed,td,rd,nd,It,Je,Xe,bt,pi,mi,xe,lo,uo,Zg,ep,id,tp,Ct,fi,sd,rp,np,od,Et,Oe,ad,ip,sp,go,hi,vt,op,ap,cp,dp,lp,up,gp,pp,mp,Ai,yi,Si,Ii,bi,Ci,Ei,fp,hp,Vr,U,po,Ap,yp,Sp,cd,Ip,bp,rA,nA,iA,dd,Cp,sA,oA,aA,vi,Ti,wi,Ri,ld,ud,Di,gd,cA,mo,Ep,vp,Tp,wp,Rp,pd,dA,md,fo,fd,rr,Pi,ho,qr,Ao,hd,ki,Ad,Dp,Pp,kp,lA};