Files
recap/startos/file-models/config.json.ts
T
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

111 lines
6.1 KiB
TypeScript
Raw 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 { FileHelper } from '@start9labs/start-sdk'
import { Volume } from '@start9labs/start-sdk/package/lib/util/Volume'
import { z } from 'zod'
const mainVolume = new Volume('main')
export const configFile = FileHelper.json(
{
base: mainVolume,
subpath: 'config/startos-config.json',
},
z.object({
gemini_api_key: z.string().default(''),
anthropic_api_key: z.string().default(''),
openai_api_key: z.string().default(''),
openai_compatible_base_url: z.string().default(''),
openai_compatible_api_key: z.string().default(''),
ollama_base_url: z.string().default(''),
whisper_base_url: z.string().default(''),
whisper_api_key: z.string().default(''),
// NOTE: relay_base_url was removed in 0.2.34. The relay endpoint
// is hardcoded in server/relay-default.js and updated via Recap
// version releases — never exposed to end users.
recap_license_key: z.string().default(''),
recap_admin_username: z.string().default(''),
recap_admin_password_hash: z.string().default(''),
recap_admin_password_salt: z.string().default(''),
recap_admin_session_secret: z.string().default(''),
// PodcastIndex credentials — used to resolve Spotify share URLs to
// their underlying RSS audio enclosure. Free tier signup at
// api.podcastindex.org. Apple Podcasts URLs resolve without auth.
podcastindex_api_key: z.string().default(''),
podcastindex_api_secret: z.string().default(''),
// ── Multi-tenant cloud-mode fields (added 0.2.77) ──
// recap_mode: "single" → existing self-hosted, one operator, no auth
// "multi" → cloud mode with email + magic-link auth,
// per-user library, BTCPay subscriptions
// The .s9pk defaults to single. Operators flip to multi via the
// "Enable multi-tenant mode" StartOS action.
recap_mode: z.enum(['single', 'multi']).default('single'),
// Public URL used in magic-link sign-in emails. Must be the
// ClearNet URL pointing at the Recap UI (typically a domain
// routed through Start Tunnel). Without this set, magic-link
// emails won't have a working sign-in link.
recap_public_url: z.string().default(''),
// Shared "operator key" for the core-decoupling cloud path. The secret
// that authenticates THIS Recap server to the Recap Relay so it can
// vouch for its signed-in users by account-id (X-Recap-User-Id) instead
// of attaching a per-user Keysat license. Must EXACTLY match the relay's
// own relay_cloud_operator_key. Empty = the cloud user-id path is
// disabled and paid users fall back to the operator's relay pool.
// Set via the "Set Relay Operator Key" StartOS action. Server-side
// only — never sent to the browser. Picked up live by the config poll
// (no restart needed), same as the provider API keys.
recap_relay_operator_key: z.string().default(''),
// Per-tenant default credit allowance — only used when running in
// multi mode on a self-hosted operator's StartOS server. New
// tenants (family members who sign up on the operator's domain)
// start with this many credits. The operator's relay-credit pool
// is debited as their tenants summarize. Doesn't apply to the
// canonical cloud deployment because cloud users have their own
// keysat licenses + relay-side credit pools.
tenant_default_credits: z.number().int().nonnegative().default(5),
// How often a tenant's "replenishable" credit bucket is refilled
// to tenant_default_credits.
// "off" — no refill; the signup-grant is one-time
// "daily" — anniversary-aligned 24h
// "weekly" — anniversary-aligned 7d
// "monthly" — anniversary-aligned calendar month
// Anniversary-aligned = anchored to each tenant's last_replenish_at
// (set when they signed up or when the operator first turned the
// period on). Purchased credits + admin grants are persisted in
// tenant_credits.purchased_balance and NEVER affected by refills —
// only the replenish_balance bucket gets reset to the configured N.
tenant_credit_replenish_period: z
.enum(["off", "daily", "weekly", "monthly"])
.default("off"),
// Anonymous-trial knobs (multi-mode only). Visitors who land on
// recapapp.xyz without an account get N free summaries gated by a
// browser cookie before being prompted to sign up. Set
// trial_credits_per_visitor=0 to disable trials (return to an
// auth-wall landing). trials_per_ip_per_day caps how many distinct
// trial cookies one IP can mint in 24h — anti-script-abuse floor.
trial_credits_per_visitor: z.number().int().nonnegative().default(1),
// Lifetime cap on the number of distinct trial cookies one IP can
// mint. Was previously rolling-24h; switched to lifetime so a user
// who clears cookies can't replay the trial every day. The legacy
// field name `trials_per_ip_per_day` is preserved below as a
// read-only alias so installs that already have it set don't lose
// their value during the rename — anon-trial.js prefers
// _lifetime if set, falls back to _per_day if not.
trials_per_ip_lifetime: z.number().int().positive().default(5),
// Legacy alias — read for backward compatibility with v0.2.770.2.83
// installs that wrote the per-day variant. Will be removed once
// all known installs have re-saved their config under the new key.
trials_per_ip_per_day: z.number().int().positive().default(5),
// ── SMTP — synced from StartOS System SMTP via the SDK ──
// main.ts subscribes to effects.getSystemSmtp() and writes these
// fields here whenever the operator changes the system SMTP. The
// server reads them through the same file-poll as everything else
// and (re)builds its nodemailer transport. NEVER edit these
// fields directly — they get overwritten on the next sync.
smtp_host: z.string().default(''),
smtp_port: z.number().int().default(0),
smtp_security: z.enum(['starttls', 'tls']).default('tls'),
smtp_username: z.string().default(''),
smtp_password: z.string().default(''),
smtp_from: z.string().default(''),
}),
)