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:
@@ -0,0 +1,79 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { configFile } from '../file-models/config.json'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
// Lifetime cap on how many distinct anonymous trial cookies one IP
|
||||
// address can mint, FOR THE LIFETIME OF THE INSTALL — not a rolling
|
||||
// daily window. Was previously per-day; switched in 0.2.84 so a user
|
||||
// who clears cookies can't simply wait 24h and replay the trial.
|
||||
//
|
||||
// Anti-abuse model:
|
||||
// - Each minted cookie carries trial_credits_per_visitor credits
|
||||
// - Each IP can mint at most `trials_per_ip_lifetime` cookies, ever
|
||||
// - Combined effect: an IP's total free credits =
|
||||
// trial_credits_per_visitor × trials_per_ip_lifetime
|
||||
// - Once spent, the visitor must sign up (which grants
|
||||
// tenant_default_credits) or pay for more
|
||||
//
|
||||
// IP rotation via VPN / proxy pool defeats this, same as before. The
|
||||
// goal isn't to be unbypassable — it's to raise the floor for casual
|
||||
// scripted abuse and give the operator forensic data (IP + UA logged
|
||||
// on every trial) to manually ban any sophisticated abuser.
|
||||
//
|
||||
// Defaults to 5 — generous enough that a family on one NAT all get
|
||||
// trials, tight enough that 50 trials/IP from one address looks like
|
||||
// scripted abuse in the admin dashboard.
|
||||
//
|
||||
// Legacy field name `trials_per_ip_per_day` is preserved on the
|
||||
// config schema as a read-only alias so installs upgrading from
|
||||
// 0.2.77–0.2.83 don't lose their existing setting.
|
||||
const inputSpec = InputSpec.of({
|
||||
limit: Value.number({
|
||||
name: 'Max trial cookies per IP (lifetime)',
|
||||
description:
|
||||
'How many anonymous trial cookies can be issued from a single IP, FOR THE LIFE OF THIS INSTALL. Not a rolling daily window — once the IP hits this cap, no more trial cookies from that address ever. Higher = friendlier to shared networks (offices, families). Lower = tighter against scripted abuse + cookie-clearing replay.',
|
||||
required: true,
|
||||
default: 5,
|
||||
integer: true,
|
||||
min: 1,
|
||||
max: 50,
|
||||
}),
|
||||
})
|
||||
|
||||
export const setTrialsPerIpPerDay = sdk.Action.withInput(
|
||||
'set-trials-per-ip-per-day',
|
||||
|
||||
async ({ effects }) => ({
|
||||
name: 'Set Trial Cookies per IP (Lifetime)',
|
||||
description:
|
||||
'Anti-abuse cap on how many trial cookies a single IP can mint over the life of this install. Default: 5. Was per-day in 0.2.77–0.2.83 and is now lifetime — see release notes for 0.2.84.',
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: 'Multi-Tenant',
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
inputSpec,
|
||||
|
||||
async ({ effects }) => {
|
||||
const config = await configFile.read().once()
|
||||
// Prefer the new field; fall back to the legacy per_day key for
|
||||
// operators whose StartOS-managed config still has the old name.
|
||||
const current =
|
||||
config?.trials_per_ip_lifetime ??
|
||||
config?.trials_per_ip_per_day ??
|
||||
5
|
||||
return { limit: current }
|
||||
},
|
||||
|
||||
async ({ effects, input }) => {
|
||||
// Write to BOTH keys so any code path still reading the legacy name
|
||||
// gets a sane value too. anon-trial.js prefers the new key.
|
||||
await configFile.merge(effects, {
|
||||
trials_per_ip_lifetime: input.limit,
|
||||
trials_per_ip_per_day: input.limit,
|
||||
})
|
||||
return null
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user