mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-10 07:15:53 +00:00
Covers PUT-1708, PUT-1709 and PUT-1743. Twelve routes, every one setting requireUserActor -- that option is what installs requireAuthGate, requireVerifiedAccount and requireNonAccessTokenGate, because server.ts derives `needsAuth` from the route options. Reads need it as much as writes: without an auth option a route gets no suspension check and admits access tokens, so a just-disabled member could still read the roster and a scoped third-party token could read the audit log. Authority is checked before anything observable. Validating the body first made POST /members answer 400 before 403, and resolving :username first turned the member routes into a global username-existence oracle. Provisioning applies the same username and email rules as signup rather than its own -- USERNAME_REGEX, USERNAME_MAX_LENGTH, RESERVED_USERNAMES and validator.isEmail, now exported from AuthController. Without them a workspace could mint accounts signup would refuse, claim unregistered reserved names, and mail arbitrary unvalidated addresses. Handle problems are 400 or 409 rather than a bare Error, which the server turns into a 500 and a deduped critical alarm -- an uppercase handle should not page on-call. Disable drops sessions through SessionStore.removeByUuid rather than a raw DELETE. The store invalidates every composite cache key; without that a disabled member kept authenticating from cache for the session TTL, which is exactly the "takes effect on the next request, not after a cache TTL" property disable is supposed to have. Revoking also preserves last_ip/last_user_agent, which the member-facing audit view reads. Audit writes live in TeamService at the point of each action rather than in the route, so a caller reaching the service directly cannot skip them, and the SQL lives in TeamStore. Audit reads map internal user ids to usernames, and remain readable by the owner after the workspace is soft-deleted -- otherwise the delete_team entry was written and immediately unreachable. teams_enabled gates route registration through an optional isEnabled() the server honours, so with it off the paths do not exist rather than existing and refusing. It does not gate DDL. TeamIsolation.http.test.ts asserts the negative the feature rests on: the workspace manages accounts and cannot read them, including through a full-access token and after the member is disabled. It asserts outcomes rather than the absence of an implicator.
517 lines
24 KiB
JSON
517 lines
24 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
|
|
// },
|
|
|
|
// ── Teams / workspaces ──────────────────────────────────────────────
|
|
// Off by default, and off is the state every existing install stays in.
|
|
// The tables ship either way and sit inert; this gates whether `/teams`
|
|
// is registered at all, so with it off the paths do not exist rather than
|
|
// existing and refusing. It is also the backout: turning it off removes
|
|
// the feature without touching data.
|
|
"teams_enabled": false,
|
|
|
|
// ── Notifications ───────────────────────────────────────────────────
|
|
// How long a notification is kept, in days from creation. Acknowledged or
|
|
// not, a row past this is swept. Set 0 to keep everything forever.
|
|
"notificationRetentionDays": 14,
|
|
|
|
// ── 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": "" },
|
|
// Meta Model API (Muse Spark). `muse-spark-1.2-contributor`
|
|
// trades a ~12x discount for Meta training on its prompts and
|
|
// completions, so it is only served to callers that name it.
|
|
"meta": { "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": "" },
|
|
"hoonify": {
|
|
"apiKey": "",
|
|
"apiBaseUrl": "https://api.hoonify.ai/v1"
|
|
},
|
|
"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
|
|
}
|