0ae59f3550
Introduces RECAP_MODE=multi alongside single-mode self-host: - Tenant auth + accounts (magic-link via System SMTP), per-tenant credit pool, anonymous trial minting with per-IP/-64 caps - Self-serve Pro/Max purchase: inline Lightning (BTCPay) + card (Zaprite), prepaid 30-day periods, expiry-reminder emails - Core-decoupling: relay owns cloud tier/expiry keyed by Recaps user-id - SQLite (better-sqlite3) schema for multi-mode; filesystem unchanged for single - StartOS actions/versions through 0.2.155
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
import { i18n } from './i18n'
|
|
import { sdk } from './sdk'
|
|
import { uiPort } from './utils'
|
|
import { configFile } from './file-models/config.json'
|
|
|
|
// ── System SMTP → config.json sync ───────────────────────────────────────
|
|
// The StartOS SDK exposes shared SMTP credentials via effects.getSystemSmtp.
|
|
// Passing a callback subscribes us — StartOS re-invokes the callback when
|
|
// the operator changes their System → SMTP settings. We mirror the values
|
|
// into our own config.json so the server (which polls the JSON via
|
|
// server/config.js) can pick them up without needing direct SDK access.
|
|
//
|
|
// `password` can be null in the StartOS payload — coerce to empty string
|
|
// so the Zod schema (z.string()) accepts it. The server treats "" the
|
|
// same as "auth disabled".
|
|
//
|
|
// Only meaningful in multi-tenant cloud mode (recap_mode === 'multi'),
|
|
// where the server sends magic-link sign-in emails. In single mode the
|
|
// fields exist in config.json but nothing reads them.
|
|
async function syncSystemSmtpToConfig(effects: any) {
|
|
try {
|
|
const smtp = await effects.getSystemSmtp({
|
|
callback: () => {
|
|
// Re-fire ourselves on change. Effects callbacks fire on every
|
|
// mutation of the underlying value, so this stays in sync.
|
|
syncSystemSmtpToConfig(effects).catch((err) => {
|
|
console.warn('[smtp] sync callback failed:', err)
|
|
})
|
|
},
|
|
})
|
|
if (!smtp) {
|
|
// No System SMTP configured — clear stale values so the server
|
|
// doesn't try to send mail through a transport we no longer have
|
|
// credentials for.
|
|
await configFile.merge(effects, {
|
|
smtp_host: '',
|
|
smtp_port: 0,
|
|
smtp_security: 'tls',
|
|
smtp_username: '',
|
|
smtp_password: '',
|
|
smtp_from: '',
|
|
})
|
|
return
|
|
}
|
|
await configFile.merge(effects, {
|
|
smtp_host: smtp.host || '',
|
|
smtp_port: smtp.port || 0,
|
|
smtp_security: smtp.security || 'tls',
|
|
smtp_username: smtp.username || '',
|
|
smtp_password: smtp.password || '',
|
|
smtp_from: smtp.from || '',
|
|
})
|
|
} catch (err) {
|
|
console.warn('[smtp] initial sync failed:', err)
|
|
}
|
|
}
|
|
|
|
export const main = sdk.setupMain(async ({ effects }) => {
|
|
console.info(i18n('Starting Recap...'))
|
|
|
|
// Subscribe to System SMTP changes before the daemon starts so the
|
|
// server boots with credentials already in config.json (no race where
|
|
// a magic-link request arrives before the first sync).
|
|
await syncSystemSmtpToConfig(effects)
|
|
|
|
// Read current config to determine which mode to boot in. The
|
|
// RECAP_MODE env var is passed to the container so the Node server
|
|
// can branch on it without re-reading the config file at startup.
|
|
const cfg = await configFile.read().once()
|
|
const recapMode = cfg?.recap_mode === 'multi' ? 'multi' : 'single'
|
|
|
|
return sdk.Daemons.of(effects).addDaemon('primary', {
|
|
subcontainer: await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'main' },
|
|
sdk.Mounts.of().mountVolume({
|
|
volumeId: 'main',
|
|
subpath: null,
|
|
mountpoint: '/data',
|
|
readonly: false,
|
|
}),
|
|
'recap-sub',
|
|
),
|
|
exec: {
|
|
command: [
|
|
'dumb-init',
|
|
'--',
|
|
'/usr/local/bin/docker_entrypoint.sh',
|
|
],
|
|
env: {
|
|
RECAP_MODE: recapMode,
|
|
},
|
|
},
|
|
ready: {
|
|
display: i18n('Web Interface'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
|
successMessage: i18n('Recap is ready'),
|
|
errorMessage: i18n('Recap is not responding'),
|
|
}),
|
|
},
|
|
requires: [],
|
|
})
|
|
})
|