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:
Keysat
2026-06-13 14:25:05 -05:00
parent db580abad7
commit 0ae59f3550
176 changed files with 23823 additions and 803 deletions
+64 -8
View File
@@ -26,7 +26,7 @@ import { createWhisperProvider } from "./whisper.js";
import { createRelayProvider } from "./relay.js";
import { getInstallId } from "../install-id.js";
import { getRawLicenseKey } from "../license.js";
import { getRelayBaseURL } from "../relay-default.js";
import { getRelayBaseURL, getRelayOperatorKey } from "../relay-default.js";
export const PROVIDER_NAMES = [
"gemini",
@@ -94,10 +94,14 @@ export function getProvider(name, opts = {}) {
// `config` is the parsed startos-config.json snapshot.
// `clientOpts` is { apiKey?, baseURL? } for THIS provider only —
// typically a value out of req.body.providerOpts[name].
// `req` is the Express request — only used for the relay provider in
// multi-tenant mode, where the relay's install_id + license depend
// on WHICH user is making the call. Pass it through whenever a
// request is in scope. Single-mode + non-relay providers ignore it.
//
// Resolution priority for each field: client opt → config opt.
// Returns { apiKey?, baseURL? } as appropriate for the provider.
export function resolveProviderOpts(name, { config = {}, clientOpts = {} } = {}) {
export function resolveProviderOpts(name, { config = {}, clientOpts = {}, req = null } = {}) {
const fields = PROVIDER_KEY_FIELDS[name];
if (!fields) {
throw new Error(`Unknown provider: ${name}`);
@@ -141,14 +145,66 @@ export function resolveProviderOpts(name, { config = {}, clientOpts = {} } = {})
}
}
// Relay-specific injections: baseURL (hardcoded constant or env
// override) + install-id (always) + license key (when present).
// None of these come from clientOpts — relay identity + endpoint
// must not be spoofable from a request body.
// override) + install-id + license key. None of these come from
// clientOpts — relay identity + endpoint must not be spoofable from
// a request body.
//
// Identity rules:
// - single mode: always the operator install + operator license
// - multi mode + signed-in user WITH their own keysat_license:
// user's synthetic_install_id + user's license. The relay's
// license-keyed credit ledger (Path 3) routes consumption to
// the right user-pool.
// - multi mode + free / trial / Core user OR signed-in user with
// no license: operator's install + license. Their relay calls
// are paid out of the operator's credit pool (tenant_credits
// gates them locally to control fan-out).
if (name === "relay") {
opts.baseURL = getRelayBaseURL();
opts.installId = getInstallId();
const rawKey = getRawLicenseKey();
if (rawKey) opts.licenseKey = rawKey;
const ident = pickRelayIdentity(req);
if (ident.cloud) {
// Core-decoupling cloud identity: authenticate the server with the
// operator key + name the user; no per-user Keysat license.
opts.cloud = true;
opts.userId = ident.userId;
opts.operatorKey = ident.operatorKey;
} else {
opts.installId = ident.installId;
if (ident.licenseKey) opts.licenseKey = ident.licenseKey;
}
}
return opts;
}
// pickRelayIdentity(req) — single source of truth for "which (install_id,
// license) do we present to the relay for THIS request". Centralized so
// the rule doesn't drift across the four resolveProviderOpts callsites.
function pickRelayIdentity(req) {
// Single mode (or no request in scope, e.g. boot-time relay capability
// probe): operator identity, period.
if (!req || req.recapMode !== "multi") {
return { installId: getInstallId(), licenseKey: getRawLicenseKey() || null };
}
// Multi mode + PAID cloud user (core-decoupling): cloud identity —
// authenticate the server with the operator key and name the user by
// their Recaps account id. NO Keysat license; the relay owns the
// tier, keyed by user-id. `req.user.tier` is the Recaps-side cache of
// that relay tier (kept in sync by the operator grant flow). Falls
// back to the operator pool when the operator key isn't configured.
const tier = req.user?.tier;
if (
req.user &&
req.user.id &&
!req.user.is_admin &&
(tier === "pro" || tier === "max")
) {
const operatorKey = getRelayOperatorKey();
if (operatorKey) {
return { cloud: true, userId: req.user.id, operatorKey };
}
}
// Multi mode + everyone else (admin, anon trial, signed-in free user,
// family-share tenant on a self-hosted multi-tenant operator's box):
// pay out of the operator's pool.
return { installId: getInstallId(), licenseKey: getRawLicenseKey() || null };
}