diff --git a/config.template.jsonc b/config.template.jsonc index 19b6897ff..8d761c2f7 100644 --- a/config.template.jsonc +++ b/config.template.jsonc @@ -1,34 +1,72 @@ { - // Comprehensive template — every key the backend or shipped extensions read. - // Copy to `config.json` and trim what you don't need; unset keys fall back to - // documented defaults (see src/backend/types.ts for per-field comments). + // Comprehensive template — every key the OSS backend or shipped (non-prod) + // extensions read. Copy to `config.json` and trim what you don't need; + // unset keys fall back to documented defaults. See + // `src/backend/types.ts` for the per-field source of truth. + // // Each setting lives at exactly one canonical key — there are no fallback // aliases. Values shown are illustrative, not production secrets. - + // + // Keys consumed only by closed-source / hosted-prod extensions (clickhouse, + // cacheUpdateHandler, pages, prodMeteringAndBilling, …) are intentionally + // omitted. + + // ── Environment / identity ────────────────────────────────────────── "config_name": "template", + // `dev` opens a browser on boot, skips blocked-email checks, and runs the + // dev-time webpack watcher; `prod` serves pre-built bundles. "env": "dev", "version": "0.0.0", + // Stable identity for this server node — used by pager alerts and + // graceful-shutdown coordination. "serverId": "node-1", + + // ── Networking / URLs ─────────────────────────────────────────────── + // Port Puter listens on internally. "port": 4100, + // Externally-visible port (set this when behind a reverse proxy on 80/443). "pub_port": 4100, "protocol": "http", "domain": "puter.localhost", + // Fully-qualified externally-visible URL. Computed from protocol/domain/ + // pub_port if unset. "origin": "http://puter.localhost:4100", + // Public base URL for the API subdomain (used to build signed URLs and + // surfaced to the client by the `installedApps` and `whoami` extensions). "api_base_url": "http://api.puter.localhost:4100", + // Subdomains Puter routes on. Wildcard DNS (`*.`) must point at + // this server for site/app hosting to work. "static_hosting_domain": "site.puter.localhost", "static_hosting_domain_alt": "host.puter.localhost", "private_app_hosting_domain": "app.puter.localhost", "private_app_hosting_domain_alt": "dev.puter.localhost", + // Host-header / domain handling. Defaults below are the dev-friendly + // settings; tighten for any public install. "allow_all_host_values": true, "allow_no_host_header": true, "allow_nipio_domains": false, "custom_domains_enabled": false, "enable_ip_validation": false, + // Express `trust proxy` setting — set to the number of reverse-proxy + // hops in front of the server (1 = nginx/Cloudflare, 2 = CF→ALB→app), + // or to a CIDR / IP / list. `false` (safe default) makes `req.ip` the + // direct socket peer. NEVER set to `true` in prod — it trusts every hop + // and makes X-Forwarded-For forgeable. + "trust_proxy": false, "no_browser_launch": false, + + // ── Dev watcher (devWatcher extension) ────────────────────────────── + // Rebuilds GUI + puter.js on file changes when running from source. + // Ignored when `env: "prod"` unless `devwatch.enabled: true`. "no_devwatch": false, "devwatch": { + // Delay after watcher startup before boot continues. Lets webpack + // emit its first build so the homepage doesn't 404 on bundle.min.js. "ready_delay_ms": 5000 }, + + // ── Auth / session ────────────────────────────────────────────────── + // ALWAYS replace these for any public install — `openssl rand -hex 64`. "jwt_secret": "change-me", "url_signature_secret": "change-me", "cookie_name": "puter_auth_token", @@ -41,11 +79,13 @@ }, "oidc": { "providers": { + // Google uses OIDC discovery — only ids are required. "google": { "client_id": "", "client_secret": "", "scopes": "openid email profile" }, + // Custom OIDC providers must also supply the three endpoints. "custom-oidc": { "client_id": "", "client_secret": "", @@ -56,10 +96,18 @@ } } }, + + // ── Groups / provisioning ─────────────────────────────────────────── + // UIDs of the persistent groups new users are auto-enrolled in. "default_user_group": "78b1b1dd-c959-44d2-b02c-8735671f9997", "default_temp_group": "b7220104-7905-4985-b996-649fdcdb3c8f", + // When true, ACL grants read/list on `//Public` to any actor. "enable_public_folders": true, + + // ── Storage / S3 ──────────────────────────────────────────────────── "s3": { + // Local fauxqs (in-process S3-compatible) — used in dev and the + // bundled-defaults Docker mode. Files land under `dataDir`. "localConfig": { "inMemory": false, "host": "127.0.0.1", @@ -67,22 +115,35 @@ "dataDir": "volatile/runtime/fauxqs-data", "s3StorageDir": "volatile/runtime/fauxqs-s3-data" }, + // For real / external S3, replace `localConfig` above with `s3Config`. "_remote_example": { "s3Config": { "useCredentialChain": false, "endpoint": "https://s3.example.com", + // Endpoint used in presigned URLs handed to the browser. Set + // this when the server-side endpoint isn't reachable from the + // browser (e.g. docker-internal `http://s3:9000`). + "publicEndpoint": "", "accessKeyId": "", "secretAccessKey": "", - "region": "us-west-2" + "region": "us-west-2", + // Set true for RustFS / MinIO / fauxqs (path-style URLs). + // Real AWS S3 wants virtual-hosted — leave unset / false. + "forcePathStyle": false } } }, "s3_bucket": "puter-local", "s3_region": "us-west-2", "region": "us-west-2", + // Default per-user storage cap (bytes). 100 MB. "storage_capacity": 104857600, "is_storage_limited": false, "available_device_storage": 0, + // ── Thumbnails (thumbnails extension) ─────────────────────────────── + // Optional dedicated S3-compatible bucket for generated thumbnails. + // When unset (or `endpoint` empty), the extension falls back to the + // main S3 client / bucket above. "thumbnailStore": { "name": "puter-local", "endpoint": "", @@ -91,15 +152,21 @@ "secretAccessKey": "" } }, + + // ── Database ──────────────────────────────────────────────────────── "database": { + // `sqlite` for single-node / dev; `mysql` for self-host with MariaDB. "engine": "sqlite", + // sqlite — file path on disk "path": "volatile/runtime/puter-database.sqlite", "targetVersion": 0, + // mysql — connection details "host": "", "port": 3306, "user": "", "password": "", "database": "", + // Optional read-replica pool. Reads route here when populated. "replica": { "host": "", "port": 3306, @@ -107,31 +174,48 @@ "password": "", "database": "" } + // mysql self-host bootstrap: set `migrationPaths` to apply the bundled + // schema on first boot. Idempotent — safe to leave on. + // "migrationPaths": ["./src/backend/clients/database/migrations/mysql"] }, + + // ── DynamoDB (KV store) ───────────────────────────────────────────── "dynamo": { + // Local emulator (dynamodb-local) endpoint. Drop this field for real + // AWS DynamoDB. "endpoint": "http://localhost:8000", + // Set true when pointing at a local emulator so Puter creates the KV + // table on boot. NEVER set against real AWS — provision via IaC. + // "bootstrapTables": true, "path": "", + // Credentials. NOTE: snake_case here, unlike `s3.s3Config` below. + // For dynamodb-local, any non-empty values work. "aws": { "access_key": "", "secret_key": "", "region": "us-west-2" } }, + + // ── Redis / Valkey (cache + cross-node rate limit) ────────────────── "redis": { + // True → in-process redis-mock (dev / single-node). "useMock": true, + // Cluster nodes for ioredis. For a single Valkey/Redis container, + // run it in cluster mode (one node, all slots). "startupNodes": [ { "host": "127.0.0.1", "port": 7000 } ] + // Defaults to true (matches prod ElastiCache). Set false for plain-TCP + // self-host Valkey/Redis. + // "tls": false }, - "pager": { - "pagerduty": { - "enabled": false, - "routingKey": "" - } - }, + + // ── Email (transactional) ─────────────────────────────────────────── + // Nodemailer transport — used for password resets, email confirmation, etc. "email": { "from": "\"Puter\" ", "host": "smtp.example.com", @@ -143,84 +227,58 @@ "pass": "" } }, - "clickhouse": { - "url": "http://127.0.0.1:8123", - "username": "", - "password": "", - "request_timeout": 15000, - "max_buffer_size": 100000, - "batch_size": 500, - "flush_interval_ms": 5000 - }, + + // ── Rate limiting ─────────────────────────────────────────────────── + // `memory` for single-node, `redis` for multi-node (default), `kv` for + // dynamo-backed counters. "rate_limit": { "backend": "redis" }, + + // ── AI / integration providers ────────────────────────────────────── + // All AI drivers (chat, image, video, TTS, OCR, STT) read from here. + // Provider id == driver-side identifier. Leave empty / omit to disable. "providers": { - "_": "All AI / integration drivers read from here. Provider id = driver-side identifier.", - "claude": { - "apiKey": "" - }, - "openai-completion": { - "apiKey": "" - }, - "gemini": { - "apiKey": "" - }, - "groq": { - "apiKey": "" - }, - "deepseek": { - "apiKey": "" - }, - "mistral": { - "apiKey": "" - }, - "xai": { - "apiKey": "" - }, - "moonshot": { - "apiKey": "" - }, + // ─ Chat / completion ─ + "claude": { "apiKey": "" }, + "openai-completion": { "apiKey": "" }, + "gemini": { "apiKey": "" }, + "groq": { "apiKey": "" }, + "deepseek": { "apiKey": "" }, + "mistral": { "apiKey": "" }, + "xai": { "apiKey": "" }, + "moonshot": { "apiKey": "" }, "openrouter": { "apiKey": "", "apiBaseUrl": "https://openrouter.ai/api/v1" }, - "together-ai": { - "apiKey": "" - }, + "together-ai": { "apiKey": "" }, + // Local Ollama. `enabled: false` skips the auto-probe at startup + // (otherwise Puter logs ECONNREFUSED on every boot when no Ollama + // is running). "ollama": { "enabled": false, "apiBaseUrl": "http://localhost:11434" }, - "openai-image-generation": { - "apiKey": "" - }, - "gemini-image-generation": { - "apiKey": "" - }, - "together-image-generation": { - "apiKey": "" - }, + + // ─ Image generation ─ + "openai-image-generation": { "apiKey": "" }, + "gemini-image-generation": { "apiKey": "" }, + "together-image-generation": { "apiKey": "" }, "cloudflare-image-generation": { "apiToken": "", "accountId": "", "apiBaseUrl": "https://api.cloudflare.com/client/v4" }, - "xai-image-generation": { - "apiKey": "" - }, - "openai-video-generation": { - "apiKey": "" - }, - "together-video-generation": { - "apiKey": "" - }, - "gemini-video-generation": { - "apiKey": "" - }, - "openai": { - "apiKey": "" - }, + "xai-image-generation": { "apiKey": "" }, + + // ─ Video generation ─ + "openai-video-generation": { "apiKey": "" }, + "together-video-generation": { "apiKey": "" }, + "gemini-video-generation": { "apiKey": "" }, + + // ─ Speech / OCR ─ + "openai": { "apiKey": "" }, "elevenlabs": { "apiKey": "", "apiBaseUrl": "https://api.elevenlabs.io", @@ -237,56 +295,17 @@ "secret_key": "", "region": "us-west-2" }, - "mistral-ocr": { - "apiKey": "" - } + "mistral-ocr": { "apiKey": "" } }, - "broadcast": { - "peers": [ - { - "peerId": "peer-a", - "webhook": true, - "webhook_url": "https://peer-a.example.com/broadcast/webhook", - "webhook_secret": "shared-secret" - } - ], - "webhook": { - "peerId": "this-node", - "secret": "shared-secret" - }, - "webhook_replay_window_seconds": 300, - "outbound_flush_ms": 2000 - }, - "peers": { - "signaller_url": "wss://signaller.example.com", - "fallback_ice": [], - "turn": { - "cloudflare_turn_service_id": "", - "cloudflare_turn_api_token": "", - "ttl": 86400 - }, - "internal_auth_secret": "" - }, - "wisp": { - "server": "wss://wisp.example.com" - }, - "workers": { - "XAUTHKEY": "", - "ACCOUNTID": "", - "namespace": "", - "internetExposedUrl": "https://api.puter.com", - "loggingUrl": "" - }, - "entri": { - "applicationId": "", - "secret": "" - }, - + + // ── GUI / static mounts ───────────────────────────────────────────── "gui_assets_root": "./src/gui", "gui_profile": "development", "builtin_apps": { "dev-center": "./src/dev-center" }, + // Force the bundled GUI even in dev — set true when running from a + // pre-built tree without webpack-dev-server. "use_bundled_gui": false, "gui_bundle": "/dist/bundle.min.js", "gui_css": "/dist/bundle.min.css", @@ -296,22 +315,38 @@ "short_description": "Your personal cloud computer", "social_media_image": "" }, + // Optional roots for native app bundles and custom puter.js builds. "native_apps_root": "", "client_libs_root": "", "puterjs_root": "./src/puter-js/dist", + + // ── Feature flags (whoami extension) ──────────────────────────────── + // Flat `{ flag_name: boolean }` bag. Server-only by default — flags are + // only surfaced to the client if their key is on the allowlist in + // `extensions/whoami.ts` (CLIENT_VISIBLE_FEATURE_FLAGS). "feature_flags": { "example_flag": false }, + + // ── Misc / safety ─────────────────────────────────────────────────── + // TLDs / domains rejected at signup (prod only). "blockedEmailDomains": [], "support_email": "support@puter.com", + // Worker / subdomain names users can't claim. "reserved_words": [], "max_subdomains_per_user": 10, "server_health": { "db_liveness_latency_fail_ms": 1500, "stale_health_loop_fail_ms": 0 }, + + // ── Extensions ────────────────────────────────────────────────────── + // Directories scanned for extension entrypoints (`*.ts` / subdirs). "extensions": [ "./extensions" ], + + // ── Metering ──────────────────────────────────────────────────────── + // When true, all metering checks pass — no per-actor limits enforced. "unlimitedMetering": false } diff --git a/doc/contributors/extensions/events.json.js b/doc/contributors/extensions/events.json.js deleted file mode 100644 index 502016800..000000000 --- a/doc/contributors/extensions/events.json.js +++ /dev/null @@ -1,816 +0,0 @@ -export default [ - { - properties: { - completionId: { - type: 'any', - mutability: 'mutable', - summary: 'completionId', - notes: [], - }, - allow: { - type: 'boolean', - mutability: 'mutable', - summary: 'whether the operation is allowed', - notes: [], - }, - intended_service: { - type: 'any', - mutability: 'mutable', - summary: 'intended service', - notes: [], - }, - parameters: { - type: 'any', - mutability: 'mutable', - summary: 'parameters', - notes: [], - }, - }, - }, - { - id: 'ai.prompt.complete', - description: ` - This event is emitted for ai prompt complete operations. - `, - properties: { - intended_service: { - type: 'any', - mutability: 'mutable', - summary: 'intended service', - notes: [], - }, - parameters: { - type: 'any', - mutability: 'mutable', - summary: 'parameters', - notes: [], - }, - result: { - type: 'any', - mutability: 'mutable', - summary: 'result', - notes: [], - }, - model_used: { - type: 'any', - mutability: 'mutable', - summary: 'model used', - notes: [], - }, - service_used: { - type: 'any', - mutability: 'mutable', - summary: 'service used', - notes: [], - }, - }, - }, - { - id: 'ai.prompt.cost-calculated', - description: ` - This event is emitted for ai prompt cost calculated operations. - `, - }, - { - id: 'ai.prompt.validate', - description: ` - This event is emitted when a validate is being validated. - The event can be used to block certain validates from being validated. - `, - properties: { - completionId: { - type: 'any', - mutability: 'mutable', - summary: 'completionId', - notes: [], - }, - allow: { - type: 'boolean', - mutability: 'mutable', - summary: 'whether the operation is allowed', - notes: [ - 'If set to false, the ai will be considered invalid.', - ], - }, - intended_service: { - type: 'any', - mutability: 'mutable', - summary: 'intended service', - notes: [], - }, - parameters: { - type: 'any', - mutability: 'mutable', - summary: 'parameters', - notes: [], - }, - }, - }, - { - id: 'app.new-icon', - description: ` - This event is emitted for app new icon operations. - `, - properties: { - data_url: { - type: 'any', - mutability: 'no-effect', - summary: 'data url', - notes: [], - }, - }, - }, - { - id: 'app.rename', - description: ` - This event is emitted for app rename operations. - `, - properties: { - data_url: { - type: 'any', - mutability: 'no-effect', - summary: 'data url', - notes: [], - }, - }, - }, - { - id: 'apps.invalidate', - description: ` - This event is emitted when a invalidate is being validated. - The event can be used to block certain invalidates from being validated. - `, - properties: { - apps: { - type: 'any', - mutability: 'no-effect', - summary: 'apps', - notes: [], - }, - }, - }, - { - id: 'captcha.check', - description: ` - This event is emitted for captcha check operations. - `, - properties: { - required: { - type: 'any', - mutability: 'no-effect', - summary: 'required', - notes: [], - }, - }, - }, - { - id: 'core.email.validate', - description: ` - This event is emitted when an email is being validated. - The event can be used to block certain emails from being validated. - `, - properties: { - email: { - type: 'string', - mutability: 'no-effect', - summary: 'the email being validated', - notes: [ - 'The email may have already been cleaned.', - ], - }, - allow: { - type: 'boolean', - mutability: 'mutable', - summary: 'whether the email is allowed', - notes: [ - 'If set to false, the email will be considered invalid.', - ], - }, - }, - }, - { - id: 'core.fs.create.directory', - description: ` - This event is emitted when a directory is created. - `, - properties: { - node: { - type: 'FSNodeContext', - mutability: 'no-effect', - summary: 'the directory that was created', - }, - context: { - type: 'Context', - mutability: 'no-effect', - summary: 'current context', - }, - }, - }, - { - id: 'core.request.measured', - description: ` - This event is emitted when a requests incoming and outgoing bytes - have been measured. - `, - example: { - language: 'javascript', - code: /*javascript*/` - extension.on('core.request.measured', data => { - const measurements = data.measurements; - // measurements = { sz_incoming: integer, sz_outgoing: integer } - - const actor = data.actor; // instance of Actor - - console.log('\x1B[36;1m === MEASUREMENT ===\x1B[0m\n', { - actor: data.actor.uid, - measurements: data.measurements - }); - }); - `, - }, - }, - { - id: 'credit.check-available', - description: ` - This event is emitted for credit check available operations. - `, - properties: { - available: { - type: 'any', - mutability: 'no-effect', - summary: 'available', - notes: [], - }, - cost_uuid: { - type: 'string', - mutability: 'no-effect', - summary: 'cost uuid', - notes: [], - }, - }, - }, - { - id: 'credit.funding-update', - description: ` - This event is emitted when a funding-update is updated. - `, - properties: { - available: { - type: 'any', - mutability: 'no-effect', - summary: 'available', - notes: [], - }, - cost_uuid: { - type: 'string', - mutability: 'no-effect', - summary: 'cost uuid', - notes: [], - }, - }, - }, - { - id: 'credit.record-cost', - description: ` - This event is emitted for credit record cost operations. - `, - properties: { - available: { - type: 'any', - mutability: 'no-effect', - summary: 'available', - notes: [], - }, - cost_uuid: { - type: 'string', - mutability: 'no-effect', - summary: 'cost uuid', - notes: [], - }, - }, - }, - { - id: 'driver.create-call-context', - description: ` - This event is emitted when a create-call-context is created. - `, - properties: { - usages: { - type: 'any', - mutability: 'no-effect', - summary: 'usages', - notes: [], - }, - }, - }, - { - id: 'email.validate', - description: ` - This event is emitted when a validate is being validated. - The event can be used to block certain validates from being validated. - `, - properties: { - allow: { - type: 'boolean', - mutability: 'mutable', - summary: 'whether the operation is allowed', - notes: [ - 'If set to false, the email will be considered invalid.', - ], - }, - email: { - type: 'any', - mutability: 'mutable', - summary: 'email', - notes: [ - 'The email may have already been cleaned.', - ], - }, - }, - }, - { - id: 'fs.create.directory', - description: ` - This event is emitted when a directory is created. - `, - }, - { - id: 'fs.create.file', - description: ` - This event is emitted when a file is created. - `, - properties: { - context: { - type: 'Context', - mutability: 'no-effect', - summary: 'current context', - notes: [], - }, - }, - }, - { - id: 'fs.create.shortcut', - description: ` - This event is emitted when a shortcut is created. - `, - }, - { - id: 'fs.create.symlink', - description: ` - This event is emitted when a symlink is created. - `, - }, - { - id: 'fs.move.file', - description: ` - This event is emitted for fs move file operations. - `, - properties: { - moved: { - type: 'any', - mutability: 'no-effect', - summary: 'moved', - notes: [], - }, - old_path: { - type: 'string', - mutability: 'no-effect', - summary: 'path to the affected resource', - notes: [], - }, - }, - }, - { - id: 'fs.pending.file', - description: ` - This event is emitted for fs pending file operations. - `, - }, - { - id: 'fs.storage.progress.copy', - description: ` - This event reports progress of a copy operation. - `, - properties: { - context: { - type: 'Context', - mutability: 'no-effect', - summary: 'current context', - notes: [], - }, - meta: { - type: 'object', - mutability: 'no-effect', - summary: 'additional metadata for the operation', - notes: [], - }, - item_path: { - type: 'string', - mutability: 'no-effect', - summary: 'path to the affected resource', - notes: [], - }, - }, - }, - { - id: 'fs.storage.upload-progress', - description: ` - This event reports progress of a upload-progress operation. - `, - }, - { - id: 'fs.write.file', - description: ` - This event is emitted when a file is updated. - `, - properties: { - context: { - type: 'Context', - mutability: 'no-effect', - summary: 'current context', - notes: [], - }, - }, - }, - { - id: 'ip.validate', - description: ` - This event is emitted when a validate is being validated. - The event can be used to block certain validates from being validated. - `, - properties: { - res: { - type: 'any', - mutability: 'mutable', - summary: 'res', - notes: [], - }, - end_: { - type: 'any', - mutability: 'mutable', - summary: 'end ', - notes: [], - }, - end: { - type: 'any', - mutability: 'mutable', - summary: 'end', - notes: [], - }, - }, - }, - { - id: 'outer.fs.write-hash', - description: ` - This event is emitted when a write-hash is updated. - `, - properties: { - uuid: { - type: 'string', - mutability: 'no-effect', - summary: 'uuid', - notes: [], - }, - }, - }, - { - id: 'outer.gui.item.added', - description: ` - This event is emitted for outer gui item added operations. - `, - properties: { - response: { - type: 'any', - mutability: 'no-effect', - summary: 'response', - notes: [], - }, - }, - }, - { - id: 'outer.gui.item.moved', - description: ` - This event is emitted for outer gui item moved operations. - `, - properties: { - response: { - type: 'any', - mutability: 'no-effect', - summary: 'response', - notes: [], - }, - }, - }, - { - id: 'outer.gui.item.pending', - description: ` - This event is emitted for outer gui item pending operations. - `, - properties: { - response: { - type: 'any', - mutability: 'no-effect', - summary: 'response', - notes: [], - }, - }, - }, - { - id: 'outer.gui.item.updated', - description: ` - This event is emitted when a updated is updated. - `, - properties: { - response: { - type: 'any', - mutability: 'no-effect', - summary: 'response', - notes: [], - }, - }, - }, - { - id: 'outer.gui.notif.ack', - description: ` - This event is emitted for outer gui notif ack operations. - `, - properties: { - response: { - type: 'any', - mutability: 'no-effect', - summary: 'response', - notes: [], - }, - }, - }, - { - id: 'outer.gui.notif.message', - description: ` - This event is emitted for outer gui notif message operations. - `, - properties: { - response: { - type: 'any', - mutability: 'no-effect', - summary: 'response', - notes: [], - }, - notification: { - type: 'any', - mutability: 'no-effect', - summary: 'notification', - notes: [], - }, - }, - }, - { - id: 'outer.gui.notif.persisted', - description: ` - This event is emitted for outer gui notif persisted operations. - `, - properties: { - response: { - type: 'any', - mutability: 'no-effect', - summary: 'response', - notes: [], - }, - }, - }, - { - id: 'outer.gui.notif.unreads', - description: ` - This event is emitted for outer gui notif unreads operations. - `, - properties: { - response: { - type: 'any', - mutability: 'no-effect', - summary: 'response', - notes: [], - }, - }, - }, - { - id: 'outer.gui.submission.done', - description: ` - This event is emitted for outer gui submission done operations. - `, - properties: { - response: { - type: 'any', - mutability: 'no-effect', - summary: 'response', - notes: [], - }, - }, - }, - { - id: 'outer.gui.usage.update', - description: ` - This event is emitted when a update is updated. - `, - }, - { - id: 'outer.thread.notify-subscribers', - description: ` - This event is emitted for outer thread notify subscribers operations. - `, - properties: { - uid: { - type: 'string', - mutability: 'no-effect', - summary: 'uid', - notes: [], - }, - action: { - type: 'any', - mutability: 'no-effect', - summary: 'action', - notes: [], - }, - data: { - type: 'any', - mutability: 'no-effect', - summary: 'data', - notes: [], - }, - }, - }, - { - id: 'puter.signup', - description: ` - This event is emitted for puter signup operations. - `, - properties: { - ip: { - type: 'any', - mutability: 'mutable', - summary: 'ip', - notes: [], - }, - user_agent: { - type: 'any', - mutability: 'mutable', - summary: 'user agent', - notes: [], - }, - body: { - type: 'any', - mutability: 'mutable', - summary: 'body', - notes: [], - }, - }, - }, - { - id: 'request.measured', - description: ` - This event is emitted for request measured operations. - `, - properties: { - req: { - type: 'any', - mutability: 'no-effect', - summary: 'req', - notes: [], - }, - res: { - type: 'any', - mutability: 'no-effect', - summary: 'res', - notes: [], - }, - }, - }, - { - id: 'request.will-be-handled', - description: ` - This event is emitted for request will be handled operations. - `, - properties: { - res: { - type: 'any', - mutability: 'mutable', - summary: 'res', - notes: [], - }, - end_: { - type: 'any', - mutability: 'mutable', - summary: 'end ', - notes: [], - }, - end: { - type: 'any', - mutability: 'mutable', - summary: 'end', - notes: [], - }, - }, - }, - { - id: 'template-service.hello', - description: ` - This event is emitted for template-service hello operations. - `, - }, - { - id: 'usages.query', - description: ` - This event is emitted for usages query operations. - `, - properties: { - usages: { - type: 'any', - mutability: 'no-effect', - summary: 'usages', - notes: [], - }, - }, - }, - { - id: 'user.email-changed', - description: ` - This event is emitted for user email changed operations. - `, - properties: { - new_email: { - type: 'any', - mutability: 'no-effect', - summary: 'new email', - notes: [], - }, - }, - }, - { - id: 'user.email-confirmed', - description: ` - This event is emitted for user email confirmed operations. - `, - properties: { - email: { - type: 'any', - mutability: 'no-effect', - summary: 'email', - notes: [], - }, - }, - }, - { - id: 'user.save_account', - description: ` - This event is emitted for user save_account operations. - `, - properties: { - user: { - type: 'User', - mutability: 'no-effect', - summary: 'user associated with the operation', - notes: [], - }, - }, - }, - { - id: 'web.socket.connected', - description: ` - This event is emitted for web socket connected operations. - `, - properties: { - user: { - type: 'User', - mutability: 'mutable', - summary: 'user associated with the operation', - notes: [], - }, - }, - }, - { - id: 'web.socket.user-connected', - description: ` - This event is emitted for web socket user connected operations. - `, - properties: { - user: { - type: 'User', - mutability: 'mutable', - summary: 'user associated with the operation', - notes: [], - }, - }, - }, - { - id: 'wisp.get-policy', - description: ` - This event is emitted for wisp get policy operations. - `, - properties: { - policy: { - type: 'Policy', - mutability: 'mutable', - summary: 'policy information for the operation', - notes: [], - }, - }, - }, -]; diff --git a/doc/contributors/extensions/events.md b/doc/contributors/extensions/events.md deleted file mode 100644 index 40c1cbd70..000000000 --- a/doc/contributors/extensions/events.md +++ /dev/null @@ -1,761 +0,0 @@ -#### Property `completionId` - -completionId -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `allow` - -whether the operation is allowed -- **Type**: boolean -- **Mutability**: mutable -- **Notes**: - -#### Property `intended_service` - -intended service -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `parameters` - -parameters -- **Type**: any -- **Mutability**: mutable -- **Notes**: - - -### `ai.prompt.complete` - -This event is emitted for ai prompt complete operations. - -#### Property `intended_service` - -intended service -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `parameters` - -parameters -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `result` - -result -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `model_used` - -model used -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `service_used` - -service used -- **Type**: any -- **Mutability**: mutable -- **Notes**: - - -### `ai.prompt.cost-calculated` - -This event is emitted for ai prompt cost calculated operations. - - -### `ai.prompt.validate` - -This event is emitted when a validate is being validated. -The event can be used to block certain validates from being validated. - -#### Property `completionId` - -completionId -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `allow` - -whether the operation is allowed -- **Type**: boolean -- **Mutability**: mutable -- **Notes**: - - If set to false, the ai will be considered invalid. - -#### Property `intended_service` - -intended service -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `parameters` - -parameters -- **Type**: any -- **Mutability**: mutable -- **Notes**: - - -### `app.new-icon` - -This event is emitted for app new icon operations. - -#### Property `data_url` - -data url -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `app.rename` - -This event is emitted for app rename operations. - -#### Property `data_url` - -data url -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `apps.invalidate` - -This event is emitted when a invalidate is being validated. -The event can be used to block certain invalidates from being validated. - -#### Property `apps` - -apps -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `captcha.check` - -This event is emitted for captcha check operations. - -#### Property `required` - -required -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `core.email.validate` - -This event is emitted when an email is being validated. -The event can be used to block certain emails from being validated. - -#### Property `email` - -the email being validated -- **Type**: string -- **Mutability**: no-effect -- **Notes**: - - The email may have already been cleaned. - -#### Property `allow` - -whether the email is allowed -- **Type**: boolean -- **Mutability**: mutable -- **Notes**: - - If set to false, the email will be considered invalid. - - -### `core.fs.create.directory` - -This event is emitted when a directory is created. - -#### Property `node` - -the directory that was created -- **Type**: FSNodeContext -- **Mutability**: no-effect - -#### Property `context` - -current context -- **Type**: Context -- **Mutability**: no-effect - - -### `core.request.measured` - -This event is emitted when a requests incoming and outgoing bytes -have been measured. - -#### Example - -```javascript -extension.on('core.request.measured', data => { - const measurements = data.measurements; - // measurements = { sz_incoming: integer, sz_outgoing: integer } - - const actor = data.actor; // instance of Actor - - console.log(' === MEASUREMENT === -', { - actor: data.actor.uid, - measurements: data.measurements - }); -}); -``` - -### `credit.check-available` - -This event is emitted for credit check available operations. - -#### Property `available` - -available -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - -#### Property `cost_uuid` - -cost uuid -- **Type**: string -- **Mutability**: no-effect -- **Notes**: - - -### `credit.funding-update` - -This event is emitted when a funding-update is updated. - -#### Property `available` - -available -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - -#### Property `cost_uuid` - -cost uuid -- **Type**: string -- **Mutability**: no-effect -- **Notes**: - - -### `credit.record-cost` - -This event is emitted for credit record cost operations. - -#### Property `available` - -available -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - -#### Property `cost_uuid` - -cost uuid -- **Type**: string -- **Mutability**: no-effect -- **Notes**: - - -### `driver.create-call-context` - -This event is emitted when a create-call-context is created. - -#### Property `usages` - -usages -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `email.validate` - -This event is emitted when a validate is being validated. -The event can be used to block certain validates from being validated. - -#### Property `allow` - -whether the operation is allowed -- **Type**: boolean -- **Mutability**: mutable -- **Notes**: - - If set to false, the email will be considered invalid. - -#### Property `email` - -email -- **Type**: any -- **Mutability**: mutable -- **Notes**: - - The email may have already been cleaned. - - -### `fs.create.directory` - -This event is emitted when a directory is created. - - -### `fs.create.file` - -This event is emitted when a file is created. - -#### Property `context` - -current context -- **Type**: Context -- **Mutability**: no-effect -- **Notes**: - - -### `fs.create.shortcut` - -This event is emitted when a shortcut is created. - - -### `fs.create.symlink` - -This event is emitted when a symlink is created. - - -### `fs.move.file` - -This event is emitted for fs move file operations. - -#### Property `moved` - -moved -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - -#### Property `old_path` - -path to the affected resource -- **Type**: string -- **Mutability**: no-effect -- **Notes**: - - -### `fs.pending.file` - -This event is emitted for fs pending file operations. - - -### `fs.storage.progress.copy` - -This event reports progress of a copy operation. - -#### Property `context` - -current context -- **Type**: Context -- **Mutability**: no-effect -- **Notes**: - -#### Property `meta` - -additional metadata for the operation -- **Type**: object -- **Mutability**: no-effect -- **Notes**: - -#### Property `item_path` - -path to the affected resource -- **Type**: string -- **Mutability**: no-effect -- **Notes**: - - -### `fs.storage.upload-progress` - -This event reports progress of a upload-progress operation. - - -### `fs.write.file` - -This event is emitted when a file is updated. - -#### Property `context` - -current context -- **Type**: Context -- **Mutability**: no-effect -- **Notes**: - - -### `ip.validate` - -This event is emitted when a validate is being validated. -The event can be used to block certain validates from being validated. - -#### Property `res` - -res -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `end_` - -end -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `end` - -end -- **Type**: any -- **Mutability**: mutable -- **Notes**: - - -### `outer.fs.write-hash` - -This event is emitted when a write-hash is updated. - -#### Property `uuid` - -uuid -- **Type**: string -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.item.added` - -This event is emitted for outer gui item added operations. - -#### Property `response` - -response -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.item.moved` - -This event is emitted for outer gui item moved operations. - -#### Property `response` - -response -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.item.pending` - -This event is emitted for outer gui item pending operations. - -#### Property `response` - -response -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.item.updated` - -This event is emitted when a updated is updated. - -#### Property `response` - -response -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.notif.ack` - -This event is emitted for outer gui notif ack operations. - -#### Property `response` - -response -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.notif.message` - -This event is emitted for outer gui notif message operations. - -#### Property `response` - -response -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - -#### Property `notification` - -notification -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.notif.persisted` - -This event is emitted for outer gui notif persisted operations. - -#### Property `response` - -response -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.notif.unreads` - -This event is emitted for outer gui notif unreads operations. - -#### Property `response` - -response -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.submission.done` - -This event is emitted for outer gui submission done operations. - -#### Property `response` - -response -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `outer.gui.usage.update` - -This event is emitted when a update is updated. - - -### `outer.thread.notify-subscribers` - -This event is emitted for outer thread notify subscribers operations. - -#### Property `uid` - -uid -- **Type**: string -- **Mutability**: no-effect -- **Notes**: - -#### Property `action` - -action -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - -#### Property `data` - -data -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `puter.signup` - -This event is emitted for puter signup operations. - -#### Property `ip` - -ip -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `user_agent` - -user agent -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `body` - -body -- **Type**: any -- **Mutability**: mutable -- **Notes**: - - -### `request.measured` - -This event is emitted for request measured operations. - -#### Property `req` - -req -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - -#### Property `res` - -res -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `request.will-be-handled` - -This event is emitted for request will be handled operations. - -#### Property `res` - -res -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `end_` - -end -- **Type**: any -- **Mutability**: mutable -- **Notes**: - -#### Property `end` - -end -- **Type**: any -- **Mutability**: mutable -- **Notes**: - - -### `sns` - -This event is emitted for sns operations. - -#### Property `message` - -message -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `template-service.hello` - -This event is emitted for template-service hello operations. - - -### `usages.query` - -This event is emitted for usages query operations. - -#### Property `usages` - -usages -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `user.email-changed` - -This event is emitted for user email changed operations. - -#### Property `new_email` - -new email -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `user.email-confirmed` - -This event is emitted for user email confirmed operations. - -#### Property `email` - -email -- **Type**: any -- **Mutability**: no-effect -- **Notes**: - - -### `user.save_account` - -This event is emitted for user save_account operations. - -#### Property `user` - -user associated with the operation -- **Type**: User -- **Mutability**: no-effect -- **Notes**: - - -### `web.socket.connected` - -This event is emitted for web socket connected operations. - -#### Property `user` - -user associated with the operation -- **Type**: User -- **Mutability**: mutable -- **Notes**: - - -### `web.socket.user-connected` - -This event is emitted for web socket user connected operations. - -#### Property `user` - -user associated with the operation -- **Type**: User -- **Mutability**: mutable -- **Notes**: - - -### `wisp.get-policy` - -This event is emitted for wisp get policy operations. - -#### Property `policy` - -policy information for the operation -- **Type**: Policy -- **Mutability**: mutable -- **Notes**: - - diff --git a/doc/contributors/extensions/manual_overrides.json.js b/doc/contributors/extensions/manual_overrides.json.js deleted file mode 100644 index fedfca263..000000000 --- a/doc/contributors/extensions/manual_overrides.json.js +++ /dev/null @@ -1,68 +0,0 @@ -export default [ - { - id: 'core.email.validate', - description: ` - This event is emitted when an email is being validated. - The event can be used to block certain emails from being validated. - `, - properties: { - email: { - type: 'string', - mutability: 'no-effect', - summary: 'the email being validated', - notes: [ - 'The email may have already been cleaned.', - ], - }, - allow: { - type: 'boolean', - mutability: 'mutable', - summary: 'whether the email is allowed', - notes: [ - 'If set to false, the email will be considered invalid.', - ], - }, - }, - }, - { - id: 'core.request.measured', - description: ` - This event is emitted when a requests incoming and outgoing bytes - have been measured. - `, - example: { - language: 'javascript', - code: /*javascript*/` - extension.on('core.request.measured', data => { - const measurements = data.measurements; - // measurements = { sz_incoming: integer, sz_outgoing: integer } - - const actor = data.actor; // instance of Actor - - console.log('\\x1B[36;1m === MEASUREMENT ===\\x1B[0m\\n', { - actor: data.actor.uid, - measurements: data.measurements - }); - }); - `, - }, - }, - { - id: 'core.fs.create.directory', - description: ` - This event is emitted when a directory is created. - `, - properties: { - node: { - type: 'FSNodeContext', - mutability: 'no-effect', - summary: 'the directory that was created', - }, - context: { - type: 'Context', - mutability: 'no-effect', - summary: 'current context', - }, - }, - }, -]; \ No newline at end of file diff --git a/selfhosted/README.md b/doc/selfhosting/README.md similarity index 100% rename from selfhosted/README.md rename to doc/selfhosting/README.md diff --git a/selfhosted/docker.md b/doc/selfhosting/docker.md similarity index 100% rename from selfhosted/docker.md rename to doc/selfhosting/docker.md diff --git a/selfhosted/full-stack.md b/doc/selfhosting/full-stack.md similarity index 82% rename from selfhosted/full-stack.md rename to doc/selfhosting/full-stack.md index 4ebcd4da3..7167d8d40 100644 --- a/selfhosted/full-stack.md +++ b/doc/selfhosting/full-stack.md @@ -112,7 +112,9 @@ cat > puter/config/config.json </`). Real AWS S3 wants virtual-hosted (`.`) — drop this flag (or set `false`) when you swap to real S3. - `s3.s3Config.publicEndpoint` — `endpoint` (`http://s3:9000`) only resolves inside the docker network; presigned upload/download URLs handed to the browser need a host-reachable URL. nginx routes the `s3.` subdomain to RustFS internally and preserves the Host header end-to-end (required for S3 signature validation), so the browser hits the same port/protocol as the rest of the app — no separate published port, no mixed-content surprises when you turn on TLS. Switch to `https://s3.` once you enable TLS in Step 3. Real AWS S3 doesn't need this — its endpoint is already public; drop the field entirely. +- `trust_proxy: 1` — nginx terminates TLS and forwards `X-Forwarded-For`. Without this, `req.ip` is the docker-network address of the nginx container instead of the real client IP, which breaks rate limiting and IP-based audit logs. `1` = one trusted hop (nginx). Bump to `2` if you put Cloudflare in front of nginx; never set `true` (it trusts every hop and makes XFF forgeable). > If you ever change `MARIADB_PASSWORD` after first boot, `.env` alone won't update MariaDB — its credentials are baked into `./puter/data/mariadb/` on first init. Either rotate the password inside MariaDB by hand or `docker compose down && rm -rf ./puter/data/mariadb` to start fresh. @@ -219,6 +222,100 @@ docker compose -f docker-compose.full.yml logs puter | grep tmp_password Change it in Settings after first login. +## Additional configuration + +All optional. Drop any of the blocks below into `puter/config/config.json` and `docker compose -f docker-compose.full.yml restart puter`. See [config.template.jsonc](../config.template.jsonc) for the full list. Per-key documentation lives in [src/backend/types.ts](../src/backend/types.ts). + +### Email (SMTP) + +Used for password resets, email confirmation, and notifications. Without it those flows silently fail. + +```json +"email": { + "from": "\"Puter\" ", + "host": "smtp.example.com", + "port": 587, + "secure": false, + "auth": { "user": "...", "pass": "..." } +} +``` + +To require email confirmation before login, also set `"strict_email_verification_required": true`. + +### Sign in with Google (or another OIDC provider) + +```json +"oidc": { + "providers": { + "google": { + "client_id": "...apps.googleusercontent.com", + "client_secret": "...", + "scopes": "openid email profile" + } + } +} +``` + +Add `https://puter./auth/oidc/callback/login` to the OAuth client's authorized redirect URIs in the Google Cloud Console. For non-Google providers, replace `google` with a custom id and supply `authorization_endpoint` / `token_endpoint` / `userinfo_endpoint` explicitly. + +### AI providers + +Any provider with a key set is auto-enabled. Same shape as `ollama` above: + +```json +"providers": { + "claude": { "apiKey": "sk-ant-..." }, + "openai-completion": { "apiKey": "sk-..." }, + "gemini": { "apiKey": "..." }, + "openai-image-generation": { "apiKey": "sk-..." } +} +``` + +Full provider list (chat, image, video, TTS, OCR) is in the template. + +### Per-user storage quota + +Default is 100 MB per user. + +```json +"storage_capacity": 5368709120, // 5 GB +"is_storage_limited": true +``` + +Set `is_storage_limited: false` for unlimited (bounded by host disk). + +### Captcha on signup / login + +Built-in proof-of-work captcha — no external service needed. + +```json +"captcha": { "enabled": true, "difficulty": "medium" } +``` + +`difficulty` is one of `easy` / `medium` / `hard`. + +### Block disposable email TLDs + +Only enforced when `env: "prod"`. + +```json +"blockedEmailDomains": ["mailinator.com", "tempmail.com", "guerrillamail.com"] +``` + +### Password policy + +```json +"min_pass_length": 12 +``` + +### Contact-form recipient + +Where the in-app contact form posts. Defaults to `support@puter.com`. + +```json +"support_email": "support@puter.example.com" +``` + ## Optional: local LLM (Ollama) The `ollama` and `ollama-init` services live behind a compose profile so they don't run unless you ask for them. By default, `puter/config/config.json` has `"ollama": { "enabled": false }` — Puter skips the auto-probe entirely. To run a local model: @@ -256,7 +353,7 @@ docker compose -f docker-compose.full.yml up -d --build --- -## Re-starting backend +## Managing running backend ```bash # update diff --git a/selfhosted/npm.md b/doc/selfhosting/npm.md similarity index 100% rename from selfhosted/npm.md rename to doc/selfhosting/npm.md diff --git a/src/backend/clients/database/migrations/mysql/mysql_mig_2.sql b/src/backend/clients/database/migrations/mysql/mysql_mig_2.sql index 0f930cf78..883268122 100644 --- a/src/backend/clients/database/migrations/mysql/mysql_mig_2.sql +++ b/src/backend/clients/database/migrations/mysql/mysql_mig_2.sql @@ -14,7 +14,7 @@ /*!40014 SET @OLD_FK = @@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-838dfbc4-bf8b-48c2-b47b-c4adc77fab58', 1, 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIGJhc2VQcm9maWxlPSJ0aW55LXBzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij4KCTx0aXRsZT5hcHAtaWNvbi1lZGl0b3Itc3ZnPC90aXRsZT4KCTxkZWZzPgoJCTxsaW5lYXJHcmFkaWVudCBpZD0iZ3JkMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiICB4MT0iNDciIHkxPSIzOS41MTQiIHgyPSIxIiB5Mj0iOC40ODYiPgoJCQk8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM3MTAxZTgiICAvPgoJCQk8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM5MTY3YmUiICAvPgoJCTwvbGluZWFyR3JhZGllbnQ+Cgk8L2RlZnM+Cgk8c3R5bGU+CgkJdHNwYW4geyB3aGl0ZS1zcGFjZTpwcmUgfQoJCS5zaHAwIHsgZmlsbDogdXJsKCNncmQxKSB9IAoJCS5zaHAxIHsgZmlsbDogI2ZmZmZmZiB9IAoJPC9zdHlsZT4KCTxnIGlkPSJMYXllciI+CgkJPHBhdGggaWQ9IkxheWVyIiBjbGFzcz0ic2hwMCIgZD0iTTQ3IDNMNDcgNDVDNDcgNDYuMSA0Ni4xIDQ3IDQ1IDQ3TDMgNDdDMS45IDQ3IDEgNDYuMSAxIDQ1TDEgM0MxIDEuOSAxLjkgMSAzIDFMNDUgMUM0Ni4xIDEgNDcgMS45IDQ3IDNaIiAvPgoJCTxwYXRoIGlkPSJMYXllciIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGFzcz0ic2hwMSIgZD0iTTI4LjYyIDQwTDI4LjYyIDM3LjYxTDMyLjI1IDM3LjIyTDI5Ljg2IDMwTDE3LjUzIDMwTDE1LjE4IDM3LjIyTDE4Ljc2IDM3LjYxTDE4Ljc2IDQwTDguNiA0MEw4LjYgMzcuNjZMMTAuNSAzNy4xN0MxMS4yMSAzNi45OSAxMS40MyAzNi44NiAxMS42IDM2LjMzTDIxLjMzIDhMMjYuNDUgOEwzNi4zNiAzNi4zOEMzNi41MyAzNi45MSAzNi44OCAzNi45OSAzNy40MiAzNy4xM0wzOS40IDM3LjYxTDM5LjQgNDBMMjguNjIgNDBaTTIzLjc2IDExLjQ1TDE4LjU0IDI3TDI4Ljg4IDI3TDIzLjc2IDExLjQ1WiIgLz4KCTwvZz4KPC9zdmc+', 'editor', 'Editor', 'A simple text editor', 'https://editor.puter.com/index.html', 0, 0, 0, 1, 1, 0, NULL, '2020-01-01 00:00:00'); +-- TEMP: editor app insert removed — broken, will fix later INSERT IGNORE INTO `apps` (`uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `index_url`, `godmode`, `maximize_on_start`, `background`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `tags`, `timestamp`) VALUES ('app-7870be61-8dff-4a99-af64-e9ae6811e367', 1, 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIGJhc2VQcm9maWxlPSJ0aW55LXBzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij4KCTx0aXRsZT5hcHAtaWNvbi12aWV3ZXItc3ZnPC90aXRsZT4KCTxkZWZzPgoJCTxsaW5lYXJHcmFkaWVudCBpZD0iZ3JkMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiICB4MT0iNDciIHkxPSIzOS41MTQiIHgyPSIxIiB5Mj0iOC40ODYiPgoJCQk8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwMzYzYWQiICAvPgoJCQk8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM1Njg0ZjUiICAvPgoJCTwvbGluZWFyR3JhZGllbnQ+Cgk8L2RlZnM+Cgk8c3R5bGU+CgkJdHNwYW4geyB3aGl0ZS1zcGFjZTpwcmUgfQoJCS5zaHAwIHsgZmlsbDogdXJsKCNncmQxKSB9IAoJCS5zaHAxIHsgZmlsbDogI2ZmZDc2NCB9IAoJCS5zaHAyIHsgZmlsbDogI2NiZWFmYiB9IAoJPC9zdHlsZT4KCTxnIGlkPSJMYXllciI+CgkJPHBhdGggaWQ9IlNoYXBlIDEiIGNsYXNzPSJzaHAwIiBkPSJNMSAxTDQ3IDFMNDcgNDdMMSA0N0wxIDFaIiAvPgoJCTxwYXRoIGlkPSJMYXllciIgY2xhc3M9InNocDEiIGQ9Ik0xOCAxOEMxNS43OSAxOCAxNCAxNi4yMSAxNCAxNEMxNCAxMS43OSAxNS43OSAxMCAxOCAxMEMyMC4yMSAxMCAyMiAxMS43OSAyMiAxNEMyMiAxNi4yMSAyMC4yMSAxOCAxOCAxOFoiIC8+CgkJPHBhdGggaWQ9IkxheWVyIiBjbGFzcz0ic2hwMiIgZD0iTTM5Ljg2IDM2LjUxQzM5LjgyIDM2LjU4IDM5Ljc3IDM2LjY1IDM5LjcgMzYuNzFDMzkuNjQgMzYuNzcgMzkuNTcgMzYuODIgMzkuNSAzNi44N0MzOS40MiAzNi45MSAzOS4zNCAzNi45NCAzOS4yNiAzNi45N0MzOS4xNyAzNi45OSAzOS4wOSAzNyAzOSAzN0w5IDM3QzguODIgMzcgOC42NCAzNi45NSA4LjQ5IDM2Ljg2QzguMzMgMzYuNzYgOC4yIDM2LjYzIDguMTIgMzYuNDdDOC4wMyAzNi4zMSA3Ljk5IDM2LjEzIDggMzUuOTVDOC4wMSAzNS43NyA4LjA3IDM1LjYgOC4xNyAzNS40NEwxNC4xNyAyNi40NUMxNC4yNCAyNi4zNCAxNC4zMyAyNi4yNCAxNC40NCAyNi4xN0MxNC41NSAyNi4xIDE0LjY4IDI2LjA0IDE0LjggMjYuMDJDMTQuOTMgMjUuOTkgMTUuMDcgMjUuOTkgMTUuMTkgMjYuMDJDMTUuMzIgMjYuMDQgMTUuNDUgMjYuMSAxNS41NSAyNi4xN0MxNS41NyAyNi4xOCAxNS41OCAyNi4xOSAxNS42IDI2LjJDMTUuNjEgMjYuMjEgMTUuNjIgMjYuMjIgMTUuNjMgMjYuMjNDMTUuNjUgMjYuMjQgMTUuNjYgMjYuMjUgMTUuNjcgMjYuMjZDMTUuNjggMjYuMjcgMTUuNyAyNi4yOCAxNS43MSAyNi4yOUwyMC44NiAzMS40NUwyOS4xOCAxOS40M0MyOS4yMyAxOS4zNiAyOS4yOCAxOS4zIDI5LjM1IDE5LjI0QzI5LjQxIDE5LjE5IDI5LjQ4IDE5LjE0IDI5LjU2IDE5LjFDMjkuNjMgMTkuMDYgMjkuNzEgMTkuMDQgMjkuNzkgMTkuMDJDMjkuODggMTkgMjkuOTYgMTkgMzAuMDUgMTlDMzAuMTMgMTkgMzAuMjEgMTkuMDIgMzAuMjkgMTkuMDRDMzAuMzggMTkuMDcgMzAuNDUgMTkuMSAzMC41MiAxOS4xNUMzMC42IDE5LjE5IDMwLjY2IDE5LjI1IDMwLjcyIDE5LjMxQzMwLjc4IDE5LjM3IDMwLjgzIDE5LjQ0IDMwLjg3IDE5LjUxTDM5Ljg3IDM1LjUxQzM5LjkxIDM1LjU5IDM5Ljk1IDM1LjY3IDM5Ljk3IDM1Ljc1QzM5Ljk5IDM1Ljg0IDQwIDM1LjkyIDQwIDM2LjAxQzQwIDM2LjEgMzkuOTkgMzYuMTggMzkuOTYgMzYuMjdDMzkuOTQgMzYuMzUgMzkuOTEgMzYuNDMgMzkuODYgMzYuNTFaIiAvPgoJPC9nPgo8L3N2Zz4=', 'viewer', 'Viewer', '', 'https://viewer.puter.com/index.html', 0, 1, 0, 1, 0, 0, NULL, '2020-01-01 00:00:00'); diff --git a/src/backend/clients/database/migrations/sqlite/0002_add-default-apps.sql b/src/backend/clients/database/migrations/sqlite/0002_add-default-apps.sql index c648ed3c1..ecbde1c93 100644 --- a/src/backend/clients/database/migrations/sqlite/0002_add-default-apps.sql +++ b/src/backend/clients/database/migrations/sqlite/0002_add-default-apps.sql @@ -15,33 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see . -INSERT INTO `apps` ( - `uid`, - `owner_user_id`, - `icon`, - `name`, - `title`, - `description`, - `index_url`, - `approved_for_listing`, - `approved_for_opening_items`, - `approved_for_incentive_program`, - `timestamp`, - `last_review` -) VALUES ( - 'app-838dfbc4-bf8b-48c2-b47b-c4adc77fab58', - 1, - 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIGJhc2VQcm9maWxlPSJ0aW55LXBzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij4KCTx0aXRsZT5hcHAtaWNvbi1lZGl0b3Itc3ZnPC90aXRsZT4KCTxkZWZzPgoJCTxsaW5lYXJHcmFkaWVudCBpZD0iZ3JkMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiICB4MT0iNDciIHkxPSIzOS41MTQiIHgyPSIxIiB5Mj0iOC40ODYiPgoJCQk8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM3MTAxZTgiICAvPgoJCQk8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM5MTY3YmUiICAvPgoJCTwvbGluZWFyR3JhZGllbnQ+Cgk8L2RlZnM+Cgk8c3R5bGU+CgkJdHNwYW4geyB3aGl0ZS1zcGFjZTpwcmUgfQoJCS5zaHAwIHsgZmlsbDogdXJsKCNncmQxKSB9IAoJCS5zaHAxIHsgZmlsbDogI2ZmZmZmZiB9IAoJPC9zdHlsZT4KCTxnIGlkPSJMYXllciI+CgkJPHBhdGggaWQ9IkxheWVyIiBjbGFzcz0ic2hwMCIgZD0iTTQ3IDNMNDcgNDVDNDcgNDYuMSA0Ni4xIDQ3IDQ1IDQ3TDMgNDdDMS45IDQ3IDEgNDYuMSAxIDQ1TDEgM0MxIDEuOSAxLjkgMSAzIDFMNDUgMUM0Ni4xIDEgNDcgMS45IDQ3IDNaIiAvPgoJCTxwYXRoIGlkPSJMYXllciIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGFzcz0ic2hwMSIgZD0iTTI4LjYyIDQwTDI4LjYyIDM3LjYxTDMyLjI1IDM3LjIyTDI5Ljg2IDMwTDE3LjUzIDMwTDE1LjE4IDM3LjIyTDE4Ljc2IDM3LjYxTDE4Ljc2IDQwTDguNiA0MEw4LjYgMzcuNjZMMTAuNSAzNy4xN0MxMS4yMSAzNi45OSAxMS40MyAzNi44NiAxMS42IDM2LjMzTDIxLjMzIDhMMjYuNDUgOEwzNi4zNiAzNi4zOEMzNi41MyAzNi45MSAzNi44OCAzNi45OSAzNy40MiAzNy4xM0wzOS40IDM3LjYxTDM5LjQgNDBMMjguNjIgNDBaTTIzLjc2IDExLjQ1TDE4LjU0IDI3TDI4Ljg4IDI3TDIzLjc2IDExLjQ1WiIgLz4KCTwvZz4KPC9zdmc+', - 'editor', - 'Editor', - 'A simple text editor', - 'https://editor.puter.com/index.html', - 1, 1, 0, - '2020-01-01 00:00:00', - NULL -); - - +-- TEMP: editor app insert removed — broken, will fix later INSERT INTO `apps` ( `id`, `uid`, `owner_user_id`, `icon`, `name`, `title`, `description`, `godmode`, `maximize_on_start`, `index_url`, `approved_for_listing`, `approved_for_opening_items`, `approved_for_incentive_program`, `timestamp`, `last_review`, `tags`, `app_owner` diff --git a/src/backend/services/permission/PermissionService.ts b/src/backend/services/permission/PermissionService.ts index e989151d3..58a946559 100644 --- a/src/backend/services/permission/PermissionService.ts +++ b/src/backend/services/permission/PermissionService.ts @@ -469,22 +469,6 @@ export class PermissionService extends PuterService { * `hardcoded-permissions.js`, merged with any runtime grants registered * through `registerSystemGrantForEveryone` / `registerSystemGrantForUsers`. */ - /** - * Hardcoded user-group permissions, granted by `system` (the only - * issuer). DB-free: the default groups are static fixtures we never - * assign at runtime via DB, so we infer membership from the actor: - * - `username === 'admin'` → admin group - * - `email_confirmed === true` → `default_user_group` - * - otherwise → `default_temp_group` - * - * The admin username + group UID are matched verbatim against - * `DefaultUserService` and the seed migration; if either is renamed, - * update both ends. - * - * Permissions for non-default groups (custom operator-managed - * groups) still flow through `#scanUserGroup`, which reads - * `user_to_group_permissions` directly. - */ async #scanHcUserGroupUser( actor: Actor, options: string[], @@ -493,50 +477,78 @@ export class PermissionService extends PuterService { if (actor.app || actor.accessToken) return; if (!actor.user?.id) return; - const userGroupUid = this.config.default_user_group; - const tempGroupUid = this.config.default_temp_group; - const isAdmin = actor.user.username === 'admin'; - const inferredGroupUid = isAdmin - ? 'ca342a5e-b13d-4dee-9048-58b11a57cc55' // admin group - : actor.user.email_confirmed - ? userGroupUid - : tempGroupUid; - if (!inferredGroupUid) return; + const memberGroups = await this.stores.group.listGroupsWithMember( + actor.user.id, + ); + if (memberGroups.length === 0) return; - const hcSystem = - ( - hardcoded_user_group_permissions as Record< - string, - Record> - > - ).system ?? {}; + const groupByUid: Record = {}; + for (const g of memberGroups) { + groupByUid[g.uid] = { id: g.id, uid: g.uid }; + } - // Hardcoded grants + runtime `registerSystemGrant*` additions. - // Runtime grants always shadow the static map for the same key. - const groupPerms: Record = { - ...(hcSystem[inferredGroupUid] ?? {}), - ...(this.systemGrantsByGroupUid[inferredGroupUid] ?? {}), - }; - if (Object.keys(groupPerms).length === 0) return; + // Compose the effective issuer → group → permission → data map by + // merging the imported hardcoded data with runtime-registered system + // grants. Runtime grants are always attributed to the `system` issuer. + const hcMap = hardcoded_user_group_permissions as Record< + string, + Record> + >; + const hasRuntimeGrants = + Object.keys(this.systemGrantsByGroupUid).length > 0; + const byIssuer: Record< + string, + Record> + > = hasRuntimeGrants + ? { ...hcMap, system: { ...(hcMap.system ?? {}) } } + : hcMap; + if (hasRuntimeGrants) { + for (const [gUid, perms] of Object.entries( + this.systemGrantsByGroupUid, + )) { + byIssuer.system[gUid] = { + ...(byIssuer.system[gUid] ?? {}), + ...perms, + }; + } + } - for (const permission of options) { - if (!Object.prototype.hasOwnProperty.call(groupPerms, permission)) { - continue; + for (const issuerUsername of Object.keys(byIssuer)) { + const issuerUser = + await this.stores.user.getByUsername(issuerUsername); + if (!issuerUser) continue; + const issuerActor = this.#userToActor(issuerUser); + const issuerGroups = byIssuer[issuerUsername]; + + for (const groupUid of Object.keys(issuerGroups)) { + if (!groupByUid[groupUid]) continue; + const issuerGroupPerms = issuerGroups[groupUid]; + + for (const permission of options) { + if ( + !Object.prototype.hasOwnProperty.call( + issuerGroupPerms, + permission, + ) + ) + continue; + const issuerReading = await this.scan( + issuerActor, + permission, + ); + reading.push({ + $: 'path', + via: 'hc-user-group', + has_terminal: readingHasTerminal(issuerReading), + permission, + data: issuerGroupPerms[permission], + holder_username: actor.user.username, + issuer_username: issuerUsername, + reading: issuerReading, + group_id: groupByUid[groupUid].id, + }); + } } - reading.push({ - $: 'path', - via: 'hc-user-group', - // `system` is the issuer; `isSystemActor` short-circuits - // any scan to grant, so the chain is terminal by - // definition — no recursive verify needed. - has_terminal: true, - permission, - data: groupPerms[permission], - holder_username: actor.user.username, - issuer_username: 'system', - reading: null, - vgroup_id: inferredGroupUid, - }); } }