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
111 lines
6.1 KiB
TypeScript
111 lines
6.1 KiB
TypeScript
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.77–0.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(''),
|
||
}),
|
||
)
|