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(''), }), )