Files
recap/startos/actions/setTrialsPerIpPerDay.ts
Keysat 0ae59f3550 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
2026-06-13 14:25:05 -05:00

80 lines
3.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.770.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.770.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
},
)