/** * Copyright (C) 2024-present Puter Technologies Inc. * * This file is part of Puter. * * Puter is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import type { PuterRouter } from './core/http/PuterRouter'; export interface IAWSCredentials { access_key?: string; secret_key?: string; region?: string; } export interface IDynamoConfig { aws?: IAWSCredentials; endpoint?: string; /** * Filesystem path for the local dynalite store. Defaults to * `./volatile/runtime/puter-ddb`. Pass `':memory:'` (or set * `inMemory: true`) to run dynalite without persistence — the * recommended setup for unit/integration tests. */ path?: string; /** * Run dynalite in-memory with no on-disk state. Equivalent to * `path: ':memory:'`. Intended for tests so each suite gets a * pristine in-process DynamoDB. */ inMemory?: boolean; /** * Create required tables on startup if they don't exist. Off by * default because real-AWS deployments provision tables externally * (Terraform / IaC). Set to `true` when pointing at a local * DynamoDB emulator so self-hosters don't have to bootstrap by hand. */ bootstrapTables?: boolean; } export interface IRedisConfig { startupNodes?: Array<{ host: string; port: number; }>; /** * Use TLS for cluster connections. Defaults to `true` (matches prod * ElastiCache). Set `false` for self-host plain-TCP Valkey/Redis. */ tls?: boolean; /** * Use ioredis-mock instead of a real Redis cluster — fully * in-process, no network. Defaults to `true` when `startupNodes` * is empty (so tests with no redis config get a mock for free). * Intended for unit/integration tests. */ useMock?: boolean; } export interface IPagerConfig { pagerduty?: { enabled?: boolean; routingKey?: string; }; } export interface ICfFileCacheConfig { /** POST endpoint that accepts batched `{ site, path }[]` invalidation payloads. */ endpoint: string; /** Flush cadence in ms. Default 500. */ throttle_ms?: number; } export interface IClickhouseConfig { url: string; username?: string; password?: string; /** Milliseconds. Default 15000. */ request_timeout?: number; /** Max pending rows before backpressure drops oldest. Default 100000. */ max_buffer_size?: number; /** Rows per flush. Default 500. */ batch_size?: number; /** Flush cadence in ms. Default 5000. */ flush_interval_ms?: number; } export interface IEmailConfig { /** "From" address used when callers don't override. */ from?: string; // nodemailer transport options (passed through as-is) host?: string; port?: number; secure?: boolean; auth?: { user?: string; pass?: string; }; service?: string; [key: string]: unknown; } /** * S3-compatible bucket the thumbnails extension uses for storing generated * thumbnails. When unset, the extension falls back to the main `S3Client` * (fauxqs locally, real S3 in prod) and writes into the default bucket. */ export interface IThumbnailStoreConfig { /** Bucket name. Default: `puter-local`. */ name?: string; /** Endpoint URL — unset forces the fallback. */ endpoint?: string; credentials?: { accessKeyId: string; secretAccessKey: string; }; } /** * Shape of an entry under `config.providers.*` — each AI / integration driver * reads a slightly different subset of these keys. Kept permissive so new * providers don't have to touch the root type. */ export interface IAIProviderConfig { /** API key. Sole canonical name — drivers no longer accept `secret_key`/`api_key`/`key` aliases. */ apiKey?: string; /** Cloudflare API token (semantically distinct from a regular key). Cloudflare-only. */ apiToken?: string; /** Override the provider's HTTP base URL (OpenRouter, Cloudflare, ElevenLabs, Ollama). */ apiBaseUrl?: string; /** Cloudflare account id. */ accountId?: string; /** ElevenLabs default voice id. */ defaultVoiceId?: string; /** ElevenLabs speech-to-speech model id. */ speechToSpeechModelId?: string; /** Ollama toggle — defaults true; set `false` to disable. */ enabled?: boolean; /** AWS credentials for AWS-backed providers (Polly, Textract). */ aws?: IAWSCredentials; /** Escape hatch — providers often expose additional tuning knobs. */ [key: string]: unknown; } /** * OIDC provider sub-config (google, custom, …). `google` uses discovery, so * only `client_id` + `client_secret` are required; custom providers must * also supply the three endpoint URLs explicitly. */ export interface IOIDCProviderConfig { client_id?: string; client_secret?: string; authorization_endpoint?: string; token_endpoint?: string; userinfo_endpoint?: string; /** Space-separated OAuth scopes. Default depends on provider. */ scopes?: string; /** Apple Developer Team ID (apple provider only). */ team_id?: string; /** Key ID for the Sign in with Apple private key (apple provider only). */ key_id?: string; /** PKCS#8 PEM private key content from Apple (apple provider only). */ private_key?: string; [key: string]: unknown; } export interface IOIDCConfig { providers?: Record; } export interface IPeersConfig { /** WebRTC signaller URL returned to clients. */ signaller_url?: string; /** Fallback ICE server list when TURN credential generation fails. */ fallback_ice?: unknown[]; /** TURN credential generation config (Cloudflare-backed). */ turn?: { cloudflare_turn_service_id?: string; cloudflare_turn_api_token?: string; /** Credential TTL in seconds. Default 86400. */ ttl?: number; }; /** Shared secret for the internal `/turn/ingest-usage` endpoint. */ internal_auth_secret?: string; } export interface IBroadcastPeerConfig { /** Stable id of the peer (also sent as `X-Broadcast-Peer-Id`). */ peerId?: string; /** Whether this peer should receive webhooks. Non-webhook peers are skipped. */ webhook?: boolean; /** HTTPS endpoint to POST broadcast events to. */ webhook_url?: string; /** HMAC-SHA256 secret shared with the peer for signing. */ webhook_secret?: string; } export interface IBroadcastConfig { peers?: IBroadcastPeerConfig[]; webhook?: { /** This server's peerId, sent in outbound POSTs as `X-Broadcast-Peer-Id`. */ peerId?: string; /** Secret used to sign OUTBOUND POSTs. */ secret?: string; }; /** Reject webhooks whose timestamp is more than this many seconds in the past. Default 300. */ webhook_replay_window_seconds?: number; /** Time to wait coalescing outbound events into a single peer POST. Default 2000ms. */ outbound_flush_ms?: number; } /** * Cloudflare Workers deployment config used by `WorkerDriver`. */ export interface IWorkersConfig { XAUTHKEY?: string; ACCOUNTID?: string; /** Optional dispatch namespace — when set, scripts deploy under `/dispatch/namespaces/`. */ namespace?: string; /** Base URL included as the `puter_endpoint` binding. Default `https://api.puter.com`. */ internetExposedUrl?: string; /** URL returned by `getLoggingUrl()` — surfaced to clients that render worker logs. */ loggingUrl?: string; [key: string]: string | undefined; } /** * Optional outbound-fetch proxy used by `secureFetch()` when the backend has * to fetch a user-supplied URL (e.g. image-gen `input_image`). Requests get * prefixed with `url` and sent through the Worker with `x-cors-proxy-auth- * secret: `; the Worker authenticates the secret, fetches the real * URL, and strips CORS on the response. Unset → fetches go direct (still * guarded by the URL/redirect/DNS checks in secureFetch). */ export interface ISecureCorsProxyConfig { url: string; secret: string; } export interface IWispConfig { /** WISP relay server address returned to clients on token create. */ server?: string; [key: string]: unknown; } export interface IServerHealthConfig { /** DB liveness latency threshold (ms). Default 1500. */ db_liveness_latency_fail_ms?: number; /** Staleness threshold for the health-check loop itself (ms). */ stale_health_loop_fail_ms?: number; } export interface IS3LocalConfig { /** * Run fauxqs entirely in-memory: random port on `127.0.0.1`, no * `dataDir` / `s3StorageDir`. Intended for tests so each suite * gets a pristine in-process S3. */ inMemory?: boolean; host?: string; port?: number; dataDir?: string; s3StorageDir?: string; } export interface IS3RemoteConfig { useCredentialChain?: boolean; endpoint: string; /** * Endpoint used when generating presigned URLs handed to clients * (browser uploads/downloads). Defaults to `endpoint`. Set this when * the server-side S3 endpoint isn't reachable from the browser — e.g. * self-host with `endpoint: http://s3:9000` (docker-internal) and * `publicEndpoint: http://localhost:9000` (host-published port). */ publicEndpoint?: string; accessKeyId: string; secretAccessKey: string; region?: string; /** * Use path-style URLs (`/`) instead of virtual-hosted * style (`.`). Defaults to AWS SDK's default (virtual- * hosted, which only works on real AWS S3). Set `true` for S3-compatible * servers (RustFS, MinIO, fauxqs) where DNS-style addressing fails. */ forcePathStyle?: boolean; } export interface IS3Config { localConfig?: IS3LocalConfig; s3Config?: IS3RemoteConfig; } export interface IDatabaseConfig { engine: 'sqlite' | 'mysql'; // sqlite /** * SQLite database file path. Defaults to `':memory:'` (the * better-sqlite3 in-memory mode), which is also what tests should * use. `inMemory: true` is an explicit alias for the same. */ path?: string; /** * Force in-memory SQLite (ignores `path`). Equivalent to * `path: ':memory:'`. Intended for tests so each suite gets a * pristine in-process database. */ inMemory?: boolean; targetVersion?: number; // mysql host?: string; port?: number; user?: string; password?: string; database?: string; replica?: { host?: string; port?: number; user?: string; password?: string; database?: string; }; /** * Ordered list of directories whose `.sql` files are run sequentially at * server start (mysql engine only). Files within a directory are sorted * lexically; directories are processed in array order. Files MUST be * idempotent — there is no per-file applied-state tracking. * Relative paths resolve from `process.cwd()`. */ migrationPaths?: string[]; } /** * Bucket of pass-through values surfaced to the client-side `gui()` boot * function. Known fields are declared for lookup hygiene; unknown keys are * still tolerated so product teams can add one-off flags without churn. */ export interface IGuiParams { title?: string; short_description?: string; social_media_image?: string; [key: string]: unknown; } export interface IDevWatcherConfig { /** Force-enable/disable the dev watcher. Defaults to enabled in dev. */ enabled?: boolean; /** Root path for watcher entries. Relative paths resolve from package root. */ root?: string; /** Delay after watcher startup before boot continues. Default: 5000. */ ready_delay_ms?: number; /** Optional extra child processes to start with the dev watcher. */ commands?: Array<{ name: string; directory: string; command: string; args?: string[]; env?: Record; }>; /** * Optional webpack watcher entries. * Omit to use the built-in GUI/puter.js watchers. */ webpack?: Array<{ name?: string; directory: string; env?: Record; }>; } /** * Complete shape of Puter's root config. Everything is optional here — * mandatory fields (only `port` + `extensions`) are pulled out of the * `Partial<...>` below and listed after it. * * When adding a new config field, declare it here with a doc comment so * there's a single discoverable reference for every config-driven switch. * * One value, one location: each setting lives at exactly one key. There are * no legacy aliases or fallback paths — older configs that relied on them * need to migrate. */ interface IConfigOptional { // ── Environment / identity ────────────────────────────────────── /** Environment marker. `dev` disables blocked-email checks, opens auto-browser, etc. */ env: 'dev' | 'prod'; /** Free-form name of the config profile (e.g. `oss-default`). Surfaced in logs. */ config_name: string; /** Server version. Falls back to `npm_package_version`. */ version: string; /** Stable identity for this server node. Enables pager alerts + graceful shutdown delay. */ serverId: string; // ── Networking / URLs ─────────────────────────────────────────── /** Protocol used for the externally-visible origin ('http' or 'https'). Default: 'http'. */ protocol: string; /** Primary domain for Puter (e.g., `puter.localhost`, `puter.com`). */ domain: string; /** Externally-visible port. Defaults to `port`. Behind a reverse proxy, set this to the public port. */ pub_port: number; /** Fully-qualified externally-visible URL (protocol + domain + port). Computed from `protocol`/`domain`/`pub_port` if unset. */ origin: string; /** Public base URL for the API subdomain, e.g. `https://api.puter.com`. Used to build signed URLs. */ api_base_url: string; /** Static hosting domain for user sites (e.g., `puter.site`). */ static_hosting_domain: string; /** Alt static hosting domain. */ static_hosting_domain_alt: string; /** Private app hosting domain (e.g., `app.puter.localhost`). */ private_app_hosting_domain: string; /** Alt private app hosting domain. */ private_app_hosting_domain_alt: string; /** * Groups of equivalent app index_url hosts. Each group lists hosts that * should resolve to the same canonical app: `appUidFromOrigin` looks up * any DB row whose `index_url` is one of the group's hosts and returns * that row's UID for every host in the group. * * Hosts listed here are also reserved — `apps.create` / `apps.update` * reject any attempt to register a different app under one of these * hosts, so the group is owned by exactly one app row. * * Entries are bare hosts (no scheme), lowercased. Example: * [ * ["camera.puter.com", "camera.puter.site", "camera.ca"], * ["player.puter.com", "player.puter.site"], * ] */ app_origin_aliases?: string[][]; /** When true, accept any Host header value. Dev/testing only. */ allow_all_host_values: boolean; /** When true, accept requests without a Host header. */ allow_no_host_header: boolean; /** When true, allow nip.io wildcard domains. */ allow_nipio_domains: boolean; /** When true, support custom domain resolution for hosted sites. */ custom_domains_enabled: boolean; /** When true, enable IP validation via event bus. */ enable_ip_validation: boolean; /** * Express `trust proxy` setting — controls how `req.ip` is derived from * `X-Forwarded-For`. Set to the number of reverse-proxy hops in front of * the server (e.g. `1` for a single Cloudflare or nginx hop, `2` for * Cloudflare → ALB → app), or to a CIDR / IP / list of trusted proxy * addresses. `false` (default) disables XFF parsing — `req.ip` returns * the direct socket peer, which is the safe choice when no proxy is in * front. Never set to `true` in production: it trusts *every* hop and * makes XFF forgeable. See https://expressjs.com/en/guide/behind-proxies.html. */ trust_proxy: boolean | number | string | string[]; /** Don't launch browser when starting. */ no_browser_launch: boolean; /** Disable dev-time frontend webpack watchers. */ no_devwatch: boolean; /** * Skip first-boot bootstrap of the `admin` user and the credentials * banner that DefaultUserService prints. Intended for tests. */ no_default_user: boolean; /** Optional dev-time frontend watcher overrides. */ devwatch: IDevWatcherConfig; // ── Auth / session ────────────────────────────────────────────── /** HMAC secret used to sign auth JWTs. */ jwt_secret: string; /** HMAC secret for signed file URLs (/file, /writeFile, /sign). */ url_signature_secret: string; /** Name of the session cookie the auth probe reads. */ cookie_name: string; /** Minimum password length for login/signup validation. */ min_pass_length: number; /** When true, allow the 'system' user to log in. */ allow_system_login: boolean; /** Reject auth-gated routes unless the user has confirmed their email. */ strict_email_verification_required: boolean; /** Captcha configuration. */ captcha: { enabled: boolean; difficulty?: 'easy' | 'medium' | 'hard' }; /** OIDC / OAuth2 providers (google + custom). */ oidc: IOIDCConfig; // ── Groups / provisioning ─────────────────────────────────────── /** UID of the persistent group that non-temp users are enrolled in at signup. */ default_user_group: string; /** UID of the persistent group that temporary users are enrolled in at signup. */ default_temp_group: string; /** When true, ACL grants read/list/see on `//Public` to any actor. */ enable_public_folders: boolean; // ── Storage / S3 ──────────────────────────────────────────────── /** S3 storage config (local fauxqs or remote). */ s3: IS3Config; /** Default S3 bucket for file storage. */ s3_bucket: string; /** Default S3 region. */ s3_region: string; /** Fallback AWS region. */ region: string; /** Default storage capacity per user (bytes). */ storage_capacity: number; /** When false, storage is effectively unlimited (bounded by device space). */ is_storage_limited: boolean; /** Bytes of device storage available (used when is_storage_limited=false). */ available_device_storage: number; /** Optional dedicated S3-compatible bucket used by the thumbnails extension. */ thumbnailStore: IThumbnailStoreConfig; // ── Database ──────────────────────────────────────────────────── database: IDatabaseConfig; // ── Clients / infra ───────────────────────────────────────────── dynamo: IDynamoConfig; redis: IRedisConfig; pager: IPagerConfig; email: IEmailConfig; clickhouse: IClickhouseConfig; cf_file_cache: ICfFileCacheConfig; // ── Rate limiting ─────────────────────────────────────────────── rate_limit: { /** * Rate limiter backend selection. * - `memory`: per-node in-memory counters. * - `redis`: sorted-sets in Redis — shared state across nodes (default). * - `kv`: per-hit rows in the system KV store (DynamoDB), with TTL. */ backend?: 'memory' | 'redis' | 'kv'; }; // ── AI / integration providers ────────────────────────────────── // // All AI providers — chat, image, video, TTS, OCR, speech-to-text, // speech-to-speech — are configured under `providers[]`. // Provider ids match the driver-side identifier (e.g. `claude`, // `openai-image-generation`, `aws-textract`). There is no `services` // bag and no top-level `openai`/`gemini`/`mistral`/`elevenlabs`/`aws` // shortcut. providers: Record; // ── Cross-node / external integrations ────────────────────────── /** Cross-node event replication config. */ broadcast: IBroadcastConfig; /** WebRTC signalling + TURN. */ peers: IPeersConfig; /** WISP relay proxy. */ wisp: IWispConfig; /** Cloudflare Workers driver config. */ workers: IWorkersConfig; /** Optional CORS-stripping signed-Worker proxy used by `secureFetch`. */ secureCorsProxy: ISecureCorsProxyConfig; /** Legacy Stripe billing extension. */ // ── GUI / static mounts ───────────────────────────────────────── /** Absolute path to the GUI assets root. */ gui_assets_root: string; /** Which profile in `puter-gui.json` to load. Default: `development`. */ gui_profile: string; /** * Map of built-in app name → local directory served at `/builtin/`. */ builtin_apps: Record; /** Force the bundled GUI even in dev. Default: false. */ use_bundled_gui: boolean; /** Override the GUI bundle JS path. Default: `/dist/bundle.min.js`. */ gui_bundle: string; /** Override the GUI CSS path when bundled. Default: `/dist/bundle.min.css`. */ gui_css: string; /** Override the puter.js preload URL when bundled. Default: `https://js.puter.com/v2/`. */ gui_puterjs_bundle: string; /** Free-form bag of values passed through to the client-side `gui()` function. */ gui_params: IGuiParams; /** * Absolute path to the directory holding native app bundles, each in a * subdirectory matching its subdomain (e.g. `/editor/`). */ native_apps_root: string; /** Absolute path to a directory holding `puter.js`/`putility.js` version bundles. */ client_libs_root: string; /** Path to the puter-js SDK root (serves `/sdk/*` and `/puter.js/v{1,2}`). */ puterjs_root: string; // ── Extension-specific ────────────────────────────────────────── /** * Flat `{ flag_name: boolean }` bag of feature toggles. Non-boolean values * are coerced before use. * * Server-only by default. Flags are surfaced to clients via `/whoami` only * if their key is on the allowlist in `extensions/whoami.ts` * (`CLIENT_VISIBLE_FEATURE_FLAGS`). New flags should be assumed internal — * add them to the allowlist explicitly if (and only if) the client needs * to read them. */ feature_flags: Record; /** Blocked email TLDs / domains — checked in `prod` only. */ blockedEmailDomains: string[]; /** Contact-form recipient. Default `support@puter.com`. */ support_email: string; /** Worker / subdomain names that cannot be allocated by users. */ reserved_words: string[]; /** Max subdomains a single user may own. Default 10. */ max_subdomains_per_user: number; /** Health-check tuning. */ server_health: IServerHealthConfig; //Metering unlimitedMetering?: boolean; } /** * Extension-augmentable config surface. Extensions add their own config keys * via TypeScript declaration merging: * * declare module '@heyputer/backend/types' { * interface IExtensionConfig { * myExtension?: { foo: string }; * } * } * * Augmentations flow into `IConfig`, which is what `this.config` / * `extension.config` are typed as everywhere. */ export interface IExtensionConfig { /** * Open index signature so config reads of extension-only keys return * `unknown` (not a type error). Extensions that declare-merge concrete * keys (`myExt?: { foo: string }`) override this for the named key — * the concrete property type wins over the index signature. */ [key: string]: unknown; } export type IConfig = Partial & { extensions: string[]; port: number; } & IExtensionConfig; // eslint-disable-next-line @typescript-eslint/no-wrapper-object-types export interface WithLifecycle extends Object { onServerStart?: () => Promise | void; onServerShutdown?: () => Promise | void; onServerPrepareShutdown?: () => Promise | void; } export interface WithCostsReporting extends WithLifecycle { getReportedCosts?: () => Promise[]>; } export interface WithControllerRegistration extends WithCostsReporting { registerRoutes: (router: PuterRouter) => void; } export type LayerInstances< // eslint-disable-next-line @typescript-eslint/no-explicit-any T extends Record any) | any>, > = { // eslint-disable-next-line @typescript-eslint/no-explicit-any [K in keyof T]: T[K] extends new (...args: any[]) => any ? InstanceType : T[K]; };