selfhosting: p3 cleanup docs + small fixes again (#2904)

* fix: Perms and configs

* rm broken editor for now

* selfhosting cleanup
This commit is contained in:
Daniel Salazar
2026-05-04 18:47:37 -07:00
committed by GitHub
parent 50679e6a01
commit a24246bfbc
11 changed files with 320 additions and 1847 deletions
+151 -116
View File
@@ -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 (`*.<domain>`) 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 `/<user>/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\" <no-reply@puter.com>",
"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
}
-816
View File
@@ -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: [],
},
},
},
];
-761
View File
@@ -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**:
@@ -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',
},
},
},
];
@@ -112,7 +112,9 @@ cat > puter/config/config.json <<EOF
"providers": {
"ollama": { "enabled": false }
}
},
"trust_proxy": 1
}
EOF
```
@@ -128,6 +130,7 @@ Why these knobs:
- `providers.ollama.enabled: false` — Puter auto-probes a local Ollama at `127.0.0.1:11434` by default; without one running you'd see `ECONNREFUSED` on every boot. To run a bundled Ollama, see [Optional: local LLM (Ollama)](#optional-local-llm-ollama) below.
- `s3.s3Config.forcePathStyle: true` — RustFS / MinIO / fauxqs need path-style URLs (`<endpoint>/<bucket>`). Real AWS S3 wants virtual-hosted (`<bucket>.<endpoint>`) — 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.<domain>` 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.<your-domain>` 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\" <no-reply@puter.example.com>",
"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.<your-domain>/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
@@ -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');
@@ -15,33 +15,7 @@
-- You should have received a copy of the GNU Affero General Public License
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
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`
@@ -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<string, Record<string, unknown>>
>
).system ?? {};
const groupByUid: Record<string, { id: number; uid: string }> = {};
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<string, unknown> = {
...(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<string, Record<string, unknown>>
>;
const hasRuntimeGrants =
Object.keys(this.systemGrantsByGroupUid).length > 0;
const byIssuer: Record<
string,
Record<string, Record<string, unknown>>
> = 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,
});
}
}