@stacksjs/buddy 0.72.67 → 0.72.69

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.
@@ -45,7 +45,7 @@ rm -f /etc/systemd/system/mail-health.service /etc/systemd/system/mail-health.ti
45
45
  rm -f /usr/local/sbin/mail-health-check /etc/systemd/system/mail.service.d/reliability.conf
46
46
  rmdir /etc/systemd/system/mail.service.d 2>/dev/null || true
47
47
  systemctl daemon-reload
48
- systemctl reset-failed`;try{execSync(`ssh ${sshArgs.map((a)=>`'${a}'`).join(" ")} bash -s`,{input:script,encoding:"utf8",stdio:["pipe","pipe","pipe"]});logger.success("Mail: removed the detached health timer from the application box")}catch(err){logger.warn(`Mail: could not remove the detached health timer: ${getErrorMessage(err)}`)}}export function resolveHetznerApiToken(tsCloudConfig){return tsCloudConfig?.hetzner?.apiToken||process.env.HCLOUD_TOKEN||process.env.HETZNER_API_TOKEN}export function describeAttachLookupFailure(owner,environment,failure){if(failure?.kind==="no-token")return`Attach target '${owner}': no Hetzner API token to look it up with, so no lookup was attempted. Set HCLOUD_TOKEN (or HETZNER_API_TOKEN, or hetzner.apiToken in config/cloud.ts).`;if(failure?.kind==="request-failed"){const where=failure.status>0?`returned HTTP ${failure.status}`:"could not be reached";return`Attach target '${owner}': the Hetzner API ${where}, so whether the box exists is unknown.`+(failure.detail?` ${failure.detail}`:"")+(failure.status===401||failure.status===403?" That is an auth failure, not a missing server: check the token is valid for this Hetzner project.":"")}return`Attach target '${owner}' has no reachable box for '${environment}'. Nothing matched ts-cloud/project=${owner},ts-cloud/environment=${environment},ts-cloud/role=app, and no server is named '${owner}-${environment}-app'. Is it provisioned (by its owner)?`}export async function resolveAttachTargetBox(owner,environment,tsCloudConfig){const token=resolveHetznerApiToken(tsCloudConfig);if(!token)return{box:null,failure:{kind:"no-token"}};const pick=(servers)=>servers.find((s)=>s?.status!=="off"&&s?.public_net?.ipv4?.ip)||servers[0];let requestFailure;const req=async(qs)=>{try{const res=await fetch(`https://api.hetzner.cloud/v1/servers?${qs}`,{headers:{Authorization:`Bearer ${token}`}});if(!res.ok){const body=await res.text().catch(()=>"");requestFailure??={kind:"request-failed",status:res.status,detail:pollFailureDetail(body)};return[]}return(await res.json()).servers||[]}catch(err){requestFailure??={kind:"request-failed",status:0,detail:pollFailureDetail(getErrorMessage(err))};return[]}},byLabel=await req(`label_selector=${encodeURIComponent(`ts-cloud/project=${owner},ts-cloud/environment=${environment},ts-cloud/role=app`)}`),chosen=pick(byLabel)||pick(await req(`name=${encodeURIComponent(`${owner}-${environment}-app`)}`));if(!chosen)return{box:null,failure:requestFailure??{kind:"no-match"}};const{normalizePublicIpv6}=await import("@stacksjs/ts-cloud");if(typeof normalizePublicIpv6!=="function")log.warn("DNS: @stacksjs/ts-cloud does not export normalizePublicIpv6 - AAAA records will be skipped. Upgrade ts-cloud.");const reportedIpv6=chosen.public_net?.ipv6?.ip;return{box:{serverId:chosen.id,serverName:chosen.name,publicIp:chosen.public_net?.ipv4?.ip,publicIpv6:typeof normalizePublicIpv6==="function"?normalizePublicIpv6(reportedIpv6):void 0}}}export async function provisionMailTenant(ip,logger){if(!hasExplicitEmailConfig())return null;const cfg=emailConfig||{};if(cfg.server?.enabled===!1)return null;const domain=cfg.domain||(typeof cfg.from?.address==="string"&&cfg.from.address.includes("@")?cfg.from.address.split("@")[1]:void 0),declaredForwards=cfg.forwards&&typeof cfg.forwards==="object"?cfg.forwards:{},forwards={...declaredForwards},declaredBoxes=new Set(domain?resolveMailboxes(cfg.mailboxes,domain).map((b)=>b.address):[]);for(const[key,targets]of Object.entries(declaredForwards)){const at=key.indexOf("@");if(at===-1||declaredBoxes.has(key))continue;const localPart=key.slice(0,at);if(key.slice(at+1)!==domain||forwards[localPart])continue;forwards[localPart]=targets}const hasForwards=Object.keys(forwards).length>0,generatePasswords=cfg.server?.generatePasswords===!0,resolved=domain?resolveMailboxesWithSkipped(cfg.mailboxes,domain,generatePasswords):{boxes:[],skipped:[]},boxes=resolved.boxes,generated=boxes.filter((box)=>box.generated);if(generated.length>0)try{const{setEnv}=await import("@stacksjs/env");for(const box of generated)await setEnv(`MAIL_PASSWORD_${box.localPart.replace(/[^A-Z0-9]/g,"_")}`,box.password,{file:".env.production",encrypt:!0});logger.success(`Mail: generated and saved ${generated.length} mailbox password(s) to .env.production (encrypted)`)}catch(err){logger.warn(`Mail: could not save the generated password(s) to .env.production - they are printed below and will otherwise be regenerated next deploy: ${getErrorMessage(err)}`)}if(resolved.skipped.length>0){logger.warn(`Mail: ${resolved.skipped.length} declared mailbox(es) were not created because no password was supplied: ${resolved.skipped.join(", ")}`);logger.info(`Set MAIL_PASSWORD_<LOCALPART> in the target environment (e.g. ${resolved.skipped[0]?.split("@")[0]?.toUpperCase().replace(/[^A-Z0-9]/g,"_")}) and run this again.`)}if(!domain&&!hasForwards)return null;const{execSync}=await import("node:child_process"),sshArgs=["-o","StrictHostKeyChecking=accept-new","-o","BatchMode=yes","-o","ConnectTimeout=20",`root@${ip}`],forwardsB64=hasForwards?Buffer.from(JSON.stringify(forwards)).toString("base64"):"",readme="Auto-forwarding rules, re-read on every message (edits take effect immediately, no restart). KEY = the delivered mailbox: the FULL address for per-domain isolated mailboxes (e.g. no-reply@app.com), or a bare local-part for legacy role mailboxes. VALUE = list of destination addresses; targets on a local domain are written straight to that mailbox Maildir, external targets are relayed. Managed by buddy deploy from config/email.ts (merge-based - hand edits to other keys are preserved).",readmeB64=Buffer.from(readme).toString("base64"),boxesB64=boxes.length?Buffer.from(`${boxes.map((b)=>`${b.address} ${b.password}`).join(`
48
+ systemctl reset-failed`;try{execSync(`ssh ${sshArgs.map((a)=>`'${a}'`).join(" ")} bash -s`,{input:script,encoding:"utf8",stdio:["pipe","pipe","pipe"]});logger.success("Mail: removed the detached health timer from the application box")}catch(err){logger.warn(`Mail: could not remove the detached health timer: ${getErrorMessage(err)}`)}}export function resolveHetznerApiToken(tsCloudConfig){return tsCloudConfig?.hetzner?.apiToken||process.env.HCLOUD_TOKEN||process.env.HETZNER_API_TOKEN}export function describeAttachLookupFailure(owner,environment,failure){if(failure?.kind==="no-token")return`Attach target '${owner}': no Hetzner API token to look it up with, so no lookup was attempted. Set HCLOUD_TOKEN (or HETZNER_API_TOKEN, or hetzner.apiToken in config/cloud.ts).`;if(failure?.kind==="request-failed"){const where=failure.status>0?`returned HTTP ${failure.status}`:"could not be reached";return`Attach target '${owner}': the Hetzner API ${where}, so whether the box exists is unknown.`+(failure.detail?` ${failure.detail}`:"")+(failure.status===401||failure.status===403?" That is an auth failure, not a missing server: check the token is valid for this Hetzner project.":"")}return`Attach target '${owner}' has no reachable box for '${environment}'. Nothing matched ts-cloud/project=${owner},ts-cloud/environment=${environment},ts-cloud/role=app, and no server is named '${owner}-${environment}-app'. Is it provisioned (by its owner)?`}export async function resolveAttachTargetBox(owner,environment,tsCloudConfig){const token=resolveHetznerApiToken(tsCloudConfig);if(!token)return{box:null,failure:{kind:"no-token"}};const pick=(servers)=>servers.find((s)=>s?.status!=="off"&&s?.public_net?.ipv4?.ip)||servers[0];let requestFailure;const req=async(qs)=>{try{const res=await fetch(`https://api.hetzner.cloud/v1/servers?${qs}`,{headers:{Authorization:`Bearer ${token}`}});if(!res.ok){const body=await res.text().catch(()=>"");requestFailure??={kind:"request-failed",status:res.status,detail:pollFailureDetail(body)};return[]}return(await res.json()).servers||[]}catch(err){requestFailure??={kind:"request-failed",status:0,detail:pollFailureDetail(getErrorMessage(err))};return[]}},byLabel=await req(`label_selector=${encodeURIComponent(`ts-cloud/project=${owner},ts-cloud/environment=${environment},ts-cloud/role=app`)}`),chosen=pick(byLabel)||pick(await req(`name=${encodeURIComponent(`${owner}-${environment}-app`)}`));if(!chosen)return{box:null,failure:requestFailure??{kind:"no-match"}};const{normalizePublicIpv6}=await import("@stacksjs/ts-cloud");if(typeof normalizePublicIpv6!=="function")log.warn("DNS: @stacksjs/ts-cloud does not export normalizePublicIpv6 - AAAA records will be skipped. Upgrade ts-cloud.");const reportedIpv6=chosen.public_net?.ipv6?.ip;return{box:{serverId:chosen.id,serverName:chosen.name,publicIp:chosen.public_net?.ipv4?.ip,publicIpv6:typeof normalizePublicIpv6==="function"?normalizePublicIpv6(reportedIpv6):void 0}}}export async function provisionMailTenant(ip,logger){if(!hasExplicitEmailConfig())return null;const cfg=emailConfig||{};if(cfg.server?.enabled===!1)return null;const domain=cfg.domain||(typeof cfg.from?.address==="string"&&cfg.from.address.includes("@")?cfg.from.address.split("@")[1]:void 0),declaredForwards=cfg.forwards&&typeof cfg.forwards==="object"?cfg.forwards:{},forwards={...declaredForwards},declaredBoxes=new Set(domain?resolveMailboxes(cfg.mailboxes,domain).map((b)=>b.address):[]);for(const[key,targets]of Object.entries(declaredForwards)){const at=key.indexOf("@");if(at===-1||declaredBoxes.has(key))continue;const localPart=key.slice(0,at);if(key.slice(at+1)!==domain||forwards[localPart])continue;forwards[localPart]=targets}const hasForwards=Object.keys(forwards).length>0,generatePasswords=cfg.server?.generatePasswords===!0,resolved=domain?resolveMailboxesWithSkipped(cfg.mailboxes,domain,generatePasswords):{boxes:[],skipped:[]},boxes=resolved.boxes,generated=boxes.filter((box)=>box.generated);if(generated.length>0)try{const{setEnv}=await import("@stacksjs/env");for(const box of generated)await setEnv(`MAIL_PASSWORD_${box.localPart.replace(/[^A-Z0-9]/g,"_")}`,box.password,{file:".env.production"});logger.success(`Mail: generated and saved ${generated.length} mailbox password(s) to .env.production (encrypted)`)}catch(err){logger.warn(`Mail: could not save the generated password(s) to .env.production - they are printed below and will otherwise be regenerated next deploy: ${getErrorMessage(err)}`)}if(resolved.skipped.length>0){logger.warn(`Mail: ${resolved.skipped.length} declared mailbox(es) were not created because no password was supplied: ${resolved.skipped.join(", ")}`);logger.info(`Set MAIL_PASSWORD_<LOCALPART> in the target environment (e.g. ${resolved.skipped[0]?.split("@")[0]?.toUpperCase().replace(/[^A-Z0-9]/g,"_")}) and run this again.`)}if(!domain&&!hasForwards)return null;const{execSync}=await import("node:child_process"),sshArgs=["-o","StrictHostKeyChecking=accept-new","-o","BatchMode=yes","-o","ConnectTimeout=20",`root@${ip}`],forwardsB64=hasForwards?Buffer.from(JSON.stringify(forwards)).toString("base64"):"",readme="Auto-forwarding rules, re-read on every message (edits take effect immediately, no restart). KEY = the delivered mailbox: the FULL address for per-domain isolated mailboxes (e.g. no-reply@app.com), or a bare local-part for legacy role mailboxes. VALUE = list of destination addresses; targets on a local domain are written straight to that mailbox Maildir, external targets are relayed. Managed by buddy deploy from config/email.ts (merge-based - hand edits to other keys are preserved).",readmeB64=Buffer.from(readme).toString("base64"),boxesB64=boxes.length?Buffer.from(`${boxes.map((b)=>`${b.address} ${b.password}`).join(`
49
49
  `)}
50
50
  `).toString("base64"):"",script=`set -e
51
51
  DOMAIN=${domain?`'${domain}'`:"''"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.67",
5
+ "version": "0.72.69",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,62 +95,62 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "^0.72.67",
99
- "@stacksjs/ai": "^0.72.67",
100
- "@stacksjs/alias": "^0.72.67",
101
- "@stacksjs/analytics": "^0.72.67",
102
- "@stacksjs/arrays": "^0.72.67",
103
- "@stacksjs/auth": "^0.72.67",
104
- "@stacksjs/browser-extension": "^0.72.67",
105
- "@stacksjs/build": "^0.72.67",
106
- "@stacksjs/cache": "^0.72.67",
107
- "@stacksjs/chat": "^0.72.67",
98
+ "@stacksjs/actions": "^0.72.69",
99
+ "@stacksjs/ai": "^0.72.69",
100
+ "@stacksjs/alias": "^0.72.69",
101
+ "@stacksjs/analytics": "^0.72.69",
102
+ "@stacksjs/arrays": "^0.72.69",
103
+ "@stacksjs/auth": "^0.72.69",
104
+ "@stacksjs/browser-extension": "^0.72.69",
105
+ "@stacksjs/build": "^0.72.69",
106
+ "@stacksjs/cache": "^0.72.69",
107
+ "@stacksjs/chat": "^0.72.69",
108
108
  "@stacksjs/clapp": "^0.2.12",
109
- "@stacksjs/cli": "^0.72.67",
110
- "@stacksjs/cloud": "^0.72.67",
111
- "@stacksjs/cms": "^0.72.67",
112
- "@stacksjs/collections": "^0.72.67",
113
- "@stacksjs/config": "^0.72.67",
114
- "@stacksjs/database": "^0.72.67",
115
- "@stacksjs/desktop-build": "^0.72.67",
116
- "@stacksjs/dns": "^0.72.67",
109
+ "@stacksjs/cli": "^0.72.69",
110
+ "@stacksjs/cloud": "^0.72.69",
111
+ "@stacksjs/cms": "^0.72.69",
112
+ "@stacksjs/collections": "^0.72.69",
113
+ "@stacksjs/config": "^0.72.69",
114
+ "@stacksjs/database": "^0.72.69",
115
+ "@stacksjs/desktop-build": "^0.72.69",
116
+ "@stacksjs/dns": "^0.72.69",
117
117
  "@stacksjs/dnsx": "^0.2.3",
118
- "@stacksjs/email": "^0.72.67",
119
- "@stacksjs/enums": "^0.72.67",
120
- "@stacksjs/env": "^0.72.67",
121
- "@stacksjs/error-handling": "^0.72.67",
122
- "@stacksjs/events": "^0.72.67",
123
- "@stacksjs/git": "^0.72.67",
118
+ "@stacksjs/email": "^0.72.69",
119
+ "@stacksjs/enums": "^0.72.69",
120
+ "@stacksjs/env": "^0.72.69",
121
+ "@stacksjs/error-handling": "^0.72.69",
122
+ "@stacksjs/events": "^0.72.69",
123
+ "@stacksjs/git": "^0.72.69",
124
124
  "@stacksjs/gitit": "^0.2.5",
125
- "@stacksjs/health": "^0.72.67",
125
+ "@stacksjs/health": "^0.72.69",
126
126
  "@stacksjs/httx": "^0.1.10",
127
- "@stacksjs/image": "^0.72.67",
128
- "@stacksjs/lint": "^0.72.67",
129
- "@stacksjs/logging": "^0.72.67",
130
- "@stacksjs/notifications": "^0.72.67",
131
- "@stacksjs/objects": "^0.72.67",
132
- "@stacksjs/orm": "^0.72.67",
133
- "@stacksjs/path": "^0.72.67",
134
- "@stacksjs/payments": "^0.72.67",
135
- "@stacksjs/realtime": "^0.72.67",
136
- "@stacksjs/router": "^0.72.67",
127
+ "@stacksjs/image": "^0.72.69",
128
+ "@stacksjs/lint": "^0.72.69",
129
+ "@stacksjs/logging": "^0.72.69",
130
+ "@stacksjs/notifications": "^0.72.69",
131
+ "@stacksjs/objects": "^0.72.69",
132
+ "@stacksjs/orm": "^0.72.69",
133
+ "@stacksjs/path": "^0.72.69",
134
+ "@stacksjs/payments": "^0.72.69",
135
+ "@stacksjs/realtime": "^0.72.69",
136
+ "@stacksjs/router": "^0.72.69",
137
137
  "@stacksjs/rpx": "^0.11.42",
138
- "@stacksjs/scheduler": "^0.72.67",
139
- "@stacksjs/search-engine": "^0.72.67",
140
- "@stacksjs/security": "^0.72.67",
141
- "@stacksjs/server": "^0.72.67",
142
- "@stacksjs/sites": "^0.72.67",
143
- "@stacksjs/skills": "^0.72.67",
144
- "@stacksjs/storage": "^0.72.67",
145
- "@stacksjs/strings": "^0.72.67",
146
- "@stacksjs/testing": "^0.72.67",
147
- "@stacksjs/tinker": "^0.72.67",
148
- "@stacksjs/ts-cloud": "^0.11.4",
149
- "@stacksjs/tunnel": "^0.72.67",
150
- "@stacksjs/types": "^0.72.67",
151
- "@stacksjs/ui": "^0.72.67",
152
- "@stacksjs/utils": "^0.72.67",
153
- "@stacksjs/validation": "^0.72.67",
138
+ "@stacksjs/scheduler": "^0.72.69",
139
+ "@stacksjs/search-engine": "^0.72.69",
140
+ "@stacksjs/security": "^0.72.69",
141
+ "@stacksjs/server": "^0.72.69",
142
+ "@stacksjs/sites": "^0.72.69",
143
+ "@stacksjs/skills": "^0.72.69",
144
+ "@stacksjs/storage": "^0.72.69",
145
+ "@stacksjs/strings": "^0.72.69",
146
+ "@stacksjs/testing": "^0.72.69",
147
+ "@stacksjs/tinker": "^0.72.69",
148
+ "@stacksjs/ts-cloud": "^0.12.3",
149
+ "@stacksjs/tunnel": "^0.72.69",
150
+ "@stacksjs/types": "^0.72.69",
151
+ "@stacksjs/ui": "^0.72.69",
152
+ "@stacksjs/utils": "^0.72.69",
153
+ "@stacksjs/validation": "^0.72.69",
154
154
  "ajv": "^8.20.0",
155
155
  "ajv-formats": "^3.0.1",
156
156
  "ts-pantry": "^0.11.35"