Add multi-tenant cloud mode: self-serve purchase, credit metering, core-decoupling
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
This commit is contained in:
@@ -1,10 +1,74 @@
|
||||
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,
|
||||
@@ -23,6 +87,9 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
||||
'--',
|
||||
'/usr/local/bin/docker_entrypoint.sh',
|
||||
],
|
||||
env: {
|
||||
RECAP_MODE: recapMode,
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
display: i18n('Web Interface'),
|
||||
|
||||
Reference in New Issue
Block a user