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
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
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
|
||
},
|
||
)
|