mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-24 15:07:17 +00:00
* refactor(share): move share notifications into their own service
* feat(share): invite an address with no account, and email it
* feat(share): surface pending invites in the SDK and share dialog
* fix(share): unreachable revoke confirmation, and double-encoded labels
* feat(share): budget share announcements, group them, and let people block senders
Sharing had one defence against noise: a 15-minute quiet window per (sender,
recipient) pair, which dropped the second share rather than folding it in.
Twenty senders each under their own window could still bury someone, and there
was no way to make one of them stop.
Announcements are now budgeted on two axes through the existing sliding-window
limiter: 1 per 15 minutes and 20 per day from one sender, and 10 per hour /
50 per day to one recipient from anyone. Over budget the share still succeeds
and the recipient's notification is still brought up to date — only the
interruption is dropped. Invite email to an address with no account is budgeted
the same way, keyed on a hash of the canonical address.
Notifications now fold across senders: a new share rewrites the notification
the recipient hasn't dismissed, so "alice and bob shared 5 items with you"
replaces a stack of five. The record is written even when suppressed, so the
count is right whenever they next look.
Blocking is a new `user_block` table with enforcement in ShareService: a blocked
sender's share is refused with `recipient_not_accepting_shares`, spends no
quota, and writes no row, and their unclaimed invite is dropped when the address
is confirmed. Existing access is untouched — that is what revoke is for.
Managed from a Blocked people card in the dashboard's Security tab.
Also publishes the sharing limits, including the ones already on this branch
that were never documented.
* fix(share): name the item in share email, instead of 'an item'
* fix(share): make the invite lifecycle canonical, authorized, and race-safe
* refactor(email): drop EmailClient.isConfigured; callers read config.email
* feat(share): batch share email into a per-recipient digest, durably
* docs(share): document the share error codes; steady the disk migration tests
* fix(share): log why a digest wasn't sent, and recover orphaned ones
* feat(share): email recipients about shares by default, with a way to decline
Share email was off unless a deployment opted in, which meant an account
holder was told about a share in the app only. It is now on unless
`share_email_notifications` is set to false.
The reason it defaulted off was that nobody could decline. So this also
honors `user.unsubscribed` — the account-wide opt-out the /unsubscribe page
already writes and app feedback already respects, which share email ignored —
and the digest carries that link. Sharing and the in-app notification are
unaffected by it; only the mail stops.
The link is composed in the template around an interpolated uuid rather than
passed pre-built: Handlebars escapes interpolated values, so a whole URL came
out as `user_uuid=…`, which browsers decode but link scanners and older
mail clients need not.
* fix(share): count every shared file in the digest, not just the first
* feat(share): let a recipient refuse shares from everyone
Blocking answered "not from this person" but had no answer to "not from
anyone", so the only way to stop a stream of unwanted shares was to name
each sender after they had already reached you.
Stored as a key in the user row's existing `metadata` blob rather than a
column: the share path already holds the recipient's row by the time it
asks, so reading it costs nothing, and a one-bit preference doesn't earn
a migration per dialect. `updateMetadata` merges and refreshes the cached
row, so the switch bites on the very next share.
Refusing everyone reports the same code as refusing one person — which of
the two it is is the recipient's business, not the sender's. Enforced at
both moments the per-sender block is: when the share is issued, and when
a pending invite is claimed. The per-sender list is untouched while the
blanket switch is on, so turning it off restores what it hid.
`GET /share/blocks` now carries `all`; `POST`/`DELETE` take `{ all: true }`
beside the existing `{ username }`. Managed from the same Blocked people
card in the dashboard's Security tab.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(share): keep the digest sweep off a window that still has a timer
The sweep treated an entry as orphaned the moment its window closed, which
is also the moment the node that armed it fires. Claiming an entry is only
exclusive among flushers that can see each other's deletes, so the two
could each claim a share of the same digest and both send. It now waits
out a grace period first, which costs a genuinely stranded digest that
much delay and nothing else.
Both digest listings were capped at 200 with no word when they hit it — a
truncated flush sends a digest that undercounts and reads as complete.
The cap is named and logged.
Also: `#emailHolder` still described share email as off by default, which
it stopped being; the config doc said the batch window defaults to 60s
when it is 90; and the two tests that need several calls inside one window
were racing a 50ms window across four sequential round trips, so they
failed under full-suite load rather than on the behaviour they cover.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(share): stop double-encoding the recipient in two dialog messages
`i18n()` encodes what it returns, replacements included, so encoding the
recipient first showed the entities to anyone whose address or username
contains one. Same pattern already fixed two lines above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(notification): widen the mysql shown/acknowledged columns
Both arrived from the v1 schema as `tinyint(1)`, where they were flags.
The backend rework changed the writes to a unix second; sqlite (`INTEGER`)
and postgres (`bigint`) took it and mysql did not, so on mysql every
`markShown` and `markAcknowledged` has failed with
ER_WARN_DATA_OUT_OF_RANGE and left the column NULL. Dismissing a
notification never stuck — the unacknowledged count never moved and one
already delivered came back on every reconnect.
No backfill: every reader tests `IS NULL` / `IS NOT NULL` only, so a
legacy `1` keeps meaning "yes" once widened. Guarded on the current type,
because changing a column type copies the table and this directory
replays on every boot.
Not reachable from the test suite — it runs against sqlite and postgres,
both of which already have the right type. Verified by hand against mysql:
`/notif/mark-read` and `/notif/mark-ack` now persist, and a dismissed
share notification is no longer the one a later share folds into.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
496 lines
22 KiB
JSON
496 lines
22 KiB
JSON
{
|
|
// 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",
|
|
// Console output format. `json` replaces the global console so every call
|
|
// emits one structured JSON line (level, timestamp, msg, and the active
|
|
// request's trace id) — one event per call, so a line-oriented log
|
|
// collector can't split stack traces across events and level filtering
|
|
// works. Unset (the default) leaves console output human-readable.
|
|
"log_format": "text",
|
|
"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_v2": "change-me",
|
|
"url_signature_secret": "change-me",
|
|
"cookie_name": "puter_auth_token",
|
|
"min_pass_length": 6,
|
|
// When true, anonymous users must log in instead of creating temp or
|
|
// permanent accounts.
|
|
"disable_user_signup": false,
|
|
"allow_system_login": false,
|
|
"strict_email_verification_required": false,
|
|
"captcha": {
|
|
"enabled": false,
|
|
"difficulty": "medium"
|
|
},
|
|
"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": "",
|
|
"authorization_endpoint": "",
|
|
"token_endpoint": "",
|
|
"userinfo_endpoint": "",
|
|
"scopes": "openid email profile"
|
|
}
|
|
}
|
|
},
|
|
|
|
// ── 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",
|
|
"port": 4566,
|
|
"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",
|
|
// 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": "",
|
|
"credentials": {
|
|
"accessKeyId": "",
|
|
"secretAccessKey": ""
|
|
}
|
|
},
|
|
|
|
// ── Database ────────────────────────────────────────────────────────
|
|
"database": {
|
|
// `sqlite` for single-node/dev, `mysql` for MariaDB/MySQL,
|
|
// or `postgres` for PostgreSQL.
|
|
//
|
|
// NOTE: `postgres` support is a community contribution and is not
|
|
// exercised by Puter.com production. It boots, passes its own
|
|
// integration tests against pgmock, and runs the common user/app/
|
|
// fsentry/session/permission/OIDC flows — but less-traveled code
|
|
// paths may surface MySQL/SQLite-isms that haven't been ported yet.
|
|
// Expect rough edges and please file issues if you hit one. For
|
|
// production self-hosting today, `mysql` (MariaDB) and `sqlite` are
|
|
// the supported defaults.
|
|
"engine": "sqlite",
|
|
// sqlite — file path on disk
|
|
"path": "volatile/runtime/puter-database.sqlite",
|
|
"targetVersion": 0,
|
|
// mysql/postgres — connection details. PostgreSQL defaults to port
|
|
// 5432 when `engine` is `postgres`; MySQL/MariaDB normally use 3306.
|
|
"host": "",
|
|
"port": 3306,
|
|
"user": "",
|
|
"password": "",
|
|
"database": "",
|
|
// postgres may also use a URL instead of host/user/password fields:
|
|
// "connectionString": "postgres://puter:secret@localhost:5432/puter",
|
|
// Optional read-replica pool. Reads route here when populated.
|
|
"replica": {
|
|
"host": "",
|
|
"port": 3306,
|
|
"user": "",
|
|
"password": "",
|
|
"database": ""
|
|
}
|
|
// mysql/postgres 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"]
|
|
// "migrationPaths": ["./src/backend/clients/database/migrations/postgres"]
|
|
},
|
|
|
|
// ── 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
|
|
},
|
|
|
|
// ── Email (transactional) ───────────────────────────────────────────
|
|
// Nodemailer transport — used for password resets, email confirmation, etc.
|
|
"email": {
|
|
"from": "\"Puter\" <no-reply@puter.com>",
|
|
"host": "smtp.example.com",
|
|
"port": 587,
|
|
"secure": false,
|
|
"service": "",
|
|
"auth": {
|
|
"user": "",
|
|
"pass": ""
|
|
}
|
|
},
|
|
|
|
// ── Sharing ─────────────────────────────────────────────────────────
|
|
// Email a recipient who already has an account about a new share. On
|
|
// unless set to false; they opt out via the unsubscribe link, or by
|
|
// blocking a sender. An invite to an address with no account always goes.
|
|
// "share_email_notifications": false,
|
|
//
|
|
// New shares one account may create per UTC day. Default 200.
|
|
// "share_daily_limit": 200,
|
|
//
|
|
// How often a share may interrupt its recipient — the notification pushed
|
|
// to their screen and the email with it. Over budget the share still
|
|
// succeeds and their notification is still brought up to date; only the
|
|
// interruption is dropped. The recipient bounds are what stop many senders
|
|
// burying one person between them. A non-positive value removes a bound.
|
|
// "share_notify_limits": {
|
|
// "pairWindowSeconds": 900,
|
|
// "pairDaily": 20,
|
|
// "recipientHourly": 10,
|
|
// "recipientDaily": 50,
|
|
// "emailBatchSeconds": 90
|
|
// },
|
|
|
|
// ── Alarms / alerting ───────────────────────────────────────────────
|
|
// Where system alarms go. Severity is the routing decision — each
|
|
// transport takes everything at or above its own `minSeverity`:
|
|
//
|
|
// critical — an unhandled server error; pages on-call.
|
|
// error — pages as well; prefer critical or warning.
|
|
// warning — look at it today; no page.
|
|
// info — a record in the chat channel only.
|
|
//
|
|
// Both transports are off unless enabled, so a self-hosted node just
|
|
// logs its alarms to the console.
|
|
"pager": {
|
|
// Severity for call sites that don't pick one. Default "critical".
|
|
"defaultSeverity": "critical",
|
|
|
|
// Retier or silence an alarm without a deploy. Keys are alarm ids,
|
|
// or a prefix ending in `*`; the exact id wins over a pattern, and
|
|
// the longest matching pattern wins among patterns. Values are a
|
|
// severity or "mute". This is applied last, so it overrides both the
|
|
// call site and any known-error rule.
|
|
"severityOverrides": {
|
|
// "cronMonitor:*": "info",
|
|
// "http_500:GET:/some/flapping/route:*": "mute"
|
|
},
|
|
|
|
"pagerduty": {
|
|
"enabled": false,
|
|
"routingKey": "",
|
|
// Lowest severity that reaches PagerDuty. Default "warning",
|
|
// which keeps `info` out of the paging system entirely.
|
|
"minSeverity": "warning"
|
|
},
|
|
|
|
// Slack incoming webhook — the low-noise destination for everything
|
|
// that shouldn't page.
|
|
"slack": {
|
|
"enabled": false,
|
|
"webhookUrl": "",
|
|
// Optional; defaults to the channel the webhook was created for.
|
|
"channel": "#alerts",
|
|
"username": "puter-alarms",
|
|
// Severity window posted to Slack. The ceiling defaults to
|
|
// "info" when PagerDuty is configured — what pages belongs in
|
|
// the pager, not in chat — and to "critical" when Slack is the
|
|
// only transport.
|
|
"minSeverity": "info",
|
|
"maxSeverity": "info",
|
|
// Don't repost the same alarm id within this window. The first
|
|
// occurrence always posts; the next post that gets through
|
|
// reports how many occurrences piled up. 0 disables throttling.
|
|
"repeatThrottleMs": 900000
|
|
}
|
|
},
|
|
|
|
// ── 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": {
|
|
// ─ Chat / completion ─
|
|
"claude": { "apiKey": "" },
|
|
"openai-completion": { "apiKey": "" },
|
|
"azure-openai": {
|
|
"apiKey": "",
|
|
"apiURL": ""
|
|
},
|
|
"gemini": { "apiKey": "" },
|
|
"groq": { "apiKey": "" },
|
|
"deepseek": { "apiKey": "" },
|
|
"mistral": { "apiKey": "" },
|
|
"xai": { "apiKey": "" },
|
|
"moonshot": { "apiKey": "" },
|
|
"minimax": {
|
|
"apiKey": "",
|
|
"apiBaseUrl": "https://api.minimax.io/v1"
|
|
},
|
|
"openrouter": {
|
|
"apiKey": "",
|
|
"apiBaseUrl": "https://openrouter.ai/api/v1"
|
|
},
|
|
"infron": {
|
|
"apiKey": "",
|
|
"apiBaseUrl": "https://llm.onerouter.pro/v1"
|
|
},
|
|
// BytePlus ModelArk. One key powers chat (Seed/GLM/DeepSeek), image
|
|
// generation (Seedream) and video generation (Seedance); `apiBaseUrl`
|
|
// selects the region; see
|
|
// https://docs.byteplus.com/en/docs/ModelArk/1330310 for options.
|
|
"byteplus": {
|
|
"apiKey": "",
|
|
"apiBaseUrl": "https://ark.ap-southeast.bytepluses.com/api/v3"
|
|
},
|
|
"zai": { "apiKey": "" },
|
|
"alibaba": { "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"
|
|
},
|
|
|
|
// ─ 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": "" },
|
|
|
|
// ─ 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",
|
|
"defaultVoiceId": "",
|
|
"speechToSpeechModelId": ""
|
|
},
|
|
"aws-polly": {
|
|
"access_key": "",
|
|
"secret_key": "",
|
|
"region": "us-west-2"
|
|
},
|
|
"speechify": { "apiKey": "" },
|
|
"aws-textract": {
|
|
"access_key": "",
|
|
"secret_key": "",
|
|
"region": "us-west-2"
|
|
},
|
|
"mistral-ocr": { "apiKey": "" }
|
|
},
|
|
|
|
// ── 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",
|
|
"gui_puterjs_bundle": "https://js.puter.com/v2/",
|
|
"gui_params": {
|
|
"title": "Puter",
|
|
"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, every account resolves to an unlimited policy: usage is still
|
|
// recorded, but nothing is ever refused for lack of budget. This is the
|
|
// setting for a deployment with no way to buy more — without it, accounts
|
|
// are held to the free monthly allowance and start getting 402s from the AI
|
|
// surfaces, file transfers and KV once they pass it.
|
|
"unlimitedMetering": false,
|
|
|
|
// Whether an account that has spent its whole allowance is refused the
|
|
// operations that spend it — file transfers, KV calls. Recording is
|
|
// unaffected either way. `workers` extends the same refusal to
|
|
// worker-driven calls, which are exempt by default because a deployed
|
|
// worker has nowhere to surface a payment prompt. `subscriptions` covers
|
|
// the separate question of surfaces reserved for paid plans (the
|
|
// vendor-compatible AI endpoints): turn it off on a deployment with no
|
|
// paid plans, or every account is refused them.
|
|
// "meteringEnforcement": { "enabled": true, "workers": false, "subscriptions": true },
|
|
|
|
// Fleet-wide spend rate, in micro-cents per minute, past which metering
|
|
// raises the `metering:excessiveGlobalUsageRate` alarm. Omit it (the
|
|
// default) to leave the check off: the only useful value is a multiple of
|
|
// what this deployment's normal traffic costs, so it has to be measured
|
|
// rather than guessed, and a stale number here alarms on healthy growth.
|
|
// "maxGlobalUsagePerMinute": 200000000
|
|
}
|