mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-01 09:38:21 +00:00
Members can already enumerate each other: `/teams/:uid/members` needs a user actor and nothing more. The only thing this adds is admitting an app actor to the same names, so an app can offer colleagues without the member driving it. That is the whole risk, so it is off until the team owner turns it on. `group.directory_enabled` defaults to 0, and a team that has not opted in answers 404 rather than 403 -- whether a team has this on is not something an app should be able to probe for either. Three things bound what an app sees. The membership tested is always the person's, never the app's, so an app installed by a member of one team can never read another's. The page carries username and uuid and nothing else -- no email, activation state, usage or role. And suspended accounts and ones that never took up their credential are left out, since offering someone who cannot sign in is noise and their existence is not this list's to disclose. Activation is the forced-change flag clearing, not the password existing: a provisioned seat holds its temporary password from birth, so testing `password IS NOT NULL` would have leaked exactly the accounts meant to be excluded. A test covers that distinction. Turning the directory on or off writes an audit row, because it changes who can read the member list and that is not something a team should be able to alter silently. Setting it to the value it already has records nothing. The toggle lives in TabTeams, and turning it on asks for confirmation while turning it off does not -- one grants access, the other only takes it away. Closes PUT-1736.
1265 lines
49 KiB
TypeScript
1265 lines
49 KiB
TypeScript
/*
|
|
* Copyright (C) 2024-present Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import type { PuterRouter } from './core/http/PuterRouter';
|
|
|
|
export interface IAWSCredentials {
|
|
access_key?: string;
|
|
secret_key?: string;
|
|
region?: string;
|
|
}
|
|
|
|
export interface IDynamoConfig {
|
|
aws?: IAWSCredentials;
|
|
endpoint?: string;
|
|
/**
|
|
* Filesystem path for the local dynalite store. Defaults to
|
|
* `./volatile/runtime/puter-ddb`. Pass `':memory:'` (or set `inMemory:
|
|
* true`) to run dynalite without persistence — the recommended setup for
|
|
* unit/integration tests.
|
|
*/
|
|
path?: string;
|
|
/**
|
|
* Run dynalite in-memory with no on-disk state. Equivalent to `path:
|
|
* ':memory:'`. Intended for tests so each suite gets a pristine in-process
|
|
* DynamoDB.
|
|
*/
|
|
inMemory?: boolean;
|
|
/**
|
|
* Create required tables on startup if they don't exist. Off by default
|
|
* because real-AWS deployments provision tables externally (Terraform /
|
|
* IaC). Set to `true` when pointing at a local DynamoDB emulator so
|
|
* self-hosters don't have to bootstrap by hand.
|
|
*/
|
|
bootstrapTables?: boolean;
|
|
}
|
|
|
|
export interface IRedisConfig {
|
|
startupNodes?: Array<{
|
|
host: string;
|
|
port: number;
|
|
}>;
|
|
/**
|
|
* Use TLS for cluster connections. Defaults to `true` (matches prod
|
|
* ElastiCache). Set `false` for self-host plain-TCP Valkey/Redis.
|
|
*/
|
|
tls?: boolean;
|
|
/**
|
|
* Use ioredis-mock instead of a real Redis cluster — fully in-process, no
|
|
* network. Defaults to `true` when `startupNodes` is empty (so tests with
|
|
* no redis config get a mock for free). Intended for unit/integration
|
|
* tests.
|
|
*/
|
|
useMock?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Redis-backed read cache in front of the KV store's point reads (`get`, and
|
|
* the per-key half of a batch `get`). Off unless `enabled` is set.
|
|
*
|
|
* Only user/app namespaces are cached; internal state under the system
|
|
* namespace is always read through, as are consistent reads.
|
|
*
|
|
* Turning the cache off does not clear what it already holds. Entries stop
|
|
* being read but also stop being invalidated, so a disable followed by a
|
|
* re-enable inside `ttlSeconds` can serve values written in between — wait out
|
|
* `ttlSeconds` before switching back on.
|
|
*/
|
|
export interface IKvCacheConfig {
|
|
/** Master switch. Default false. */
|
|
enabled?: boolean;
|
|
/** Seconds a cached value is served for. Default 60. */
|
|
ttlSeconds?: number;
|
|
/**
|
|
* Seconds a cached absence is served for. Shorter than `ttlSeconds` because
|
|
* a key that doesn't exist yet is the one most likely to appear. Default
|
|
* 10.
|
|
*/
|
|
missTtlSeconds?: number;
|
|
/**
|
|
* Seconds after a write during which that key's reads bypass the cache.
|
|
* Must comfortably exceed how long a mutation takes to become visible to
|
|
* every reader, or a read that raced the write can re-cache the old value.
|
|
* Default 5.
|
|
*/
|
|
blockSeconds?: number;
|
|
/**
|
|
* Largest cached entry, in bytes of serialized envelope. Bigger values are
|
|
* read through — they earn the least per byte of cache memory. Default
|
|
* 32768.
|
|
*/
|
|
maxEntryBytes?: number;
|
|
/**
|
|
* Milliseconds invalidations accumulate for before one broadcast carries
|
|
* them all. Local invalidation is always immediate; this only batches the
|
|
* message to peers. 0 sends one per write. Default 250.
|
|
*/
|
|
broadcastCoalesceMs?: number;
|
|
}
|
|
|
|
/**
|
|
* Alert severity. Ordered `info` < `warning` < `error` < `critical`; each alert
|
|
* transport takes everything at or above its own `minSeverity`, so the severity
|
|
* a call site picks is what decides where the alarm lands.
|
|
*/
|
|
export type PagerSeverity = 'critical' | 'error' | 'warning' | 'info';
|
|
|
|
/** A severity, or `mute` to drop the alarm before any transport sees it. */
|
|
export type SeverityRule = PagerSeverity | 'mute';
|
|
|
|
export interface IPagerDutyConfig {
|
|
enabled?: boolean;
|
|
routingKey?: string;
|
|
/**
|
|
* Lowest severity that reaches PagerDuty. Default `warning`, which keeps
|
|
* `info` alarms out of the paging system entirely.
|
|
*/
|
|
minSeverity?: PagerSeverity;
|
|
}
|
|
|
|
export interface ISlackAlertConfig {
|
|
enabled?: boolean;
|
|
/** Incoming-webhook URL to post alerts to. */
|
|
webhookUrl?: string;
|
|
/** Channel override (e.g. `#alerts`). Defaults to the webhook's own. */
|
|
channel?: string;
|
|
/** Bot display name on the posted message. */
|
|
username?: string;
|
|
/** Lowest severity posted to Slack. Default `info`. */
|
|
minSeverity?: PagerSeverity;
|
|
/**
|
|
* Highest severity posted to Slack. Defaults to `info` when PagerDuty is
|
|
* configured — anything that pages belongs in the paging system, not in
|
|
* chat — and to `critical` (everything) when Slack is the only transport.
|
|
*/
|
|
maxSeverity?: PagerSeverity;
|
|
/**
|
|
* Don't repost the same alarm id within this window. The first occurrence
|
|
* always posts; repeats inside the window only bump the occurrence count
|
|
* that the next post reports. Default 15 minutes; `0` disables throttling.
|
|
*/
|
|
repeatThrottleMs?: number;
|
|
}
|
|
|
|
export interface IPagerConfig {
|
|
/** Severity used when a call site doesn't pass one. Default `critical`. */
|
|
defaultSeverity?: PagerSeverity;
|
|
/**
|
|
* Operator overrides keyed by alarm id, or by prefix with a trailing `*`
|
|
* (`cronMonitor:*`). Exact ids beat patterns and the longest matching
|
|
* prefix wins. Applied after the call site's severity and any known-error
|
|
* rule, so this is the final say — it can retier or mute a noisy alarm
|
|
* without a deploy.
|
|
*/
|
|
severityOverrides?: Record<string, SeverityRule>;
|
|
pagerduty?: IPagerDutyConfig;
|
|
slack?: ISlackAlertConfig;
|
|
}
|
|
|
|
export interface ICfFileCacheConfig {
|
|
/**
|
|
* POST endpoint that accepts batched `{ site, path }[]` invalidation
|
|
* payloads.
|
|
*/
|
|
endpoint: string;
|
|
/** Flush cadence in ms. Default 500. */
|
|
throttle_ms?: number;
|
|
}
|
|
|
|
export interface IClickhouseConfig {
|
|
url: string;
|
|
username?: string;
|
|
password?: string;
|
|
/** Milliseconds. Default 15000. */
|
|
request_timeout?: number;
|
|
/** Max pending rows before backpressure drops oldest. Default 100000. */
|
|
max_buffer_size?: number;
|
|
/** Rows per flush. Default 500. */
|
|
batch_size?: number;
|
|
/** Flush cadence in ms. Default 5000. */
|
|
flush_interval_ms?: number;
|
|
}
|
|
|
|
export interface IEmailConfig {
|
|
/** "From" address used when callers don't override. */
|
|
from?: string;
|
|
// nodemailer transport options (passed through as-is)
|
|
host?: string;
|
|
port?: number;
|
|
secure?: boolean;
|
|
auth?: {
|
|
user?: string;
|
|
pass?: string;
|
|
};
|
|
service?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/** Prelude (https://prelude.so) Verify v2 — SMS phone verification provider. */
|
|
export interface IPreludeConfig {
|
|
/** Prelude v2 API key (sent as `Authorization: Bearer <apiKey>`). */
|
|
apiKey?: string;
|
|
/** Default region for parsing local-format phone numbers (e.g. 'US'). */
|
|
defaultCountry?: string;
|
|
/** Per-SMS cost ceiling in EUR. */
|
|
maxSmsCostEur?: number;
|
|
/**
|
|
* Verification template id (from the Prelude dashboard) that controls the
|
|
* SMS wording — e.g. a "Your Puter verification code is {{code}}" template.
|
|
* The message text itself is authored in Prelude, not here; this just
|
|
* selects it. Omit to use the dashboard default.
|
|
*/
|
|
templateId?: string;
|
|
/**
|
|
* Alphanumeric Sender ID to brand who the SMS is "from" (e.g. "Puter").
|
|
* Must be pre-enabled by Prelude and isn't supported by all
|
|
* carriers/regions (notably US long/short codes). Omit to use Prelude's
|
|
* default sender.
|
|
*/
|
|
senderId?: string;
|
|
/**
|
|
* Channel Prelude prioritizes for delivery. Defaults to 'rcs' (much cheaper
|
|
* than SMS); Prelude falls back to SMS when RCS isn't reachable. Requires
|
|
* an RCS agent provisioned in the Prelude account to actually use RCS.
|
|
*/
|
|
preferredChannel?:
|
|
| 'sms'
|
|
| 'rcs'
|
|
| 'whatsapp'
|
|
| 'viber'
|
|
| 'zalo'
|
|
| 'telegram';
|
|
}
|
|
|
|
/**
|
|
* S3-compatible bucket the thumbnails extension uses for storing generated
|
|
* thumbnails. When unset, the extension falls back to the main `S3Client`
|
|
* (fauxqs locally, real S3 in prod) and writes into the default bucket.
|
|
*/
|
|
export interface IThumbnailStoreConfig {
|
|
/** Bucket name. Default: `puter-local`. */
|
|
name?: string;
|
|
/** Endpoint URL — unset forces the fallback. */
|
|
endpoint?: string;
|
|
credentials?: {
|
|
accessKeyId: string;
|
|
secretAccessKey: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Shape of an entry under `config.providers.*` — each AI / integration driver
|
|
* reads a slightly different subset of these keys. Kept permissive so new
|
|
* providers don't have to touch the root type.
|
|
*/
|
|
export interface IAIProviderConfig {
|
|
/**
|
|
* API key. Sole canonical name — drivers no longer accept
|
|
* `secret_key`/`api_key`/`key` aliases.
|
|
*/
|
|
apiKey?: string;
|
|
/**
|
|
* Cloudflare API token (semantically distinct from a regular key).
|
|
* Cloudflare-only.
|
|
*/
|
|
apiToken?: string;
|
|
/**
|
|
* Override the provider's HTTP base URL (OpenRouter, Cloudflare,
|
|
* ElevenLabs, Ollama).
|
|
*/
|
|
apiBaseUrl?: string;
|
|
/**
|
|
* Azure AI Foundry deployment endpoint (azure-openai). Required alongside
|
|
* `apiKey`.
|
|
*/
|
|
apiURL?: string;
|
|
/** Cloudflare account id. */
|
|
accountId?: string;
|
|
/** ElevenLabs default voice id. */
|
|
defaultVoiceId?: string;
|
|
/** ElevenLabs speech-to-speech model id. */
|
|
speechToSpeechModelId?: string;
|
|
/** Ollama toggle — defaults true; set `false` to disable. */
|
|
enabled?: boolean;
|
|
/** AWS credentials for AWS-backed providers (Polly, Textract). */
|
|
aws?: IAWSCredentials;
|
|
/** Escape hatch — providers often expose additional tuning knobs. */
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* OIDC provider sub-config (google, custom, …). `google` uses discovery, so
|
|
* only `client_id` + `client_secret` are required; custom providers must also
|
|
* supply the three endpoint URLs explicitly.
|
|
*/
|
|
export interface IOIDCProviderConfig {
|
|
client_id?: string;
|
|
client_secret?: string;
|
|
authorization_endpoint?: string;
|
|
token_endpoint?: string;
|
|
userinfo_endpoint?: string;
|
|
/** Space-separated OAuth scopes. Default depends on provider. */
|
|
scopes?: string;
|
|
/** Apple Developer Team ID (apple provider only). */
|
|
team_id?: string;
|
|
/** Key ID for the Sign in with Apple private key (apple provider only). */
|
|
key_id?: string;
|
|
/** PKCS#8 PEM private key content from Apple (apple provider only). */
|
|
private_key?: string;
|
|
/** Azure AD tenant ID (microsoft provider only). Defaults to "common". */
|
|
tenant_id?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface IOIDCConfig {
|
|
providers?: Record<string, IOIDCProviderConfig>;
|
|
}
|
|
|
|
export interface IPeersConfig {
|
|
/** WebRTC signaller URL returned to clients. */
|
|
signaller_url?: string;
|
|
/** Fallback ICE server list when TURN credential generation fails. */
|
|
fallback_ice?: unknown[];
|
|
/** TURN credential generation config (Cloudflare-backed). */
|
|
turn?: {
|
|
cloudflare_turn_service_id?: string;
|
|
cloudflare_turn_api_token?: string;
|
|
/** Credential TTL in seconds. Default 86400. */
|
|
ttl?: number;
|
|
};
|
|
/**
|
|
* Relay access for guests of an authenticated host, who mint credentials
|
|
* against a signed grant instead of an account of their own.
|
|
*/
|
|
guest_turn?: {
|
|
/**
|
|
* HMAC key for guest grants. Absent disables the guest routes with a
|
|
* 503 — a deployment opts into guest relay access by setting this. Must
|
|
* not be shared with any other secret.
|
|
*/
|
|
grant_secret?: string;
|
|
/** Grant lifetime in seconds. Default 3600. */
|
|
grant_ttl?: number;
|
|
/** Guest credential TTL in seconds, clamped to `turn.ttl`. Default 3600. */
|
|
credential_ttl?: number;
|
|
};
|
|
/** Shared secret for the internal `/turn/ingest-usage` endpoint. */
|
|
internal_auth_secret?: string;
|
|
}
|
|
|
|
export interface IBroadcastPeerConfig {
|
|
/** Stable id of the peer (also sent as `X-Broadcast-Peer-Id`). */
|
|
peerId?: string;
|
|
/** Whether this peer should receive webhooks. Non-webhook peers are skipped. */
|
|
webhook?: boolean;
|
|
/** HTTPS endpoint to POST broadcast events to. */
|
|
webhook_url?: string;
|
|
/** HMAC-SHA256 secret shared with the peer for signing. */
|
|
webhook_secret?: string;
|
|
}
|
|
|
|
export interface IBroadcastConfig {
|
|
peers?: IBroadcastPeerConfig[];
|
|
webhook?: {
|
|
/**
|
|
* This server's peerId, sent in outbound POSTs as
|
|
* `X-Broadcast-Peer-Id`.
|
|
*/
|
|
peerId?: string;
|
|
/** Secret used to sign OUTBOUND POSTs. */
|
|
secret?: string;
|
|
};
|
|
/**
|
|
* Reject webhooks whose timestamp is more than this many seconds in the
|
|
* past. Default 300.
|
|
*/
|
|
webhook_replay_window_seconds?: number;
|
|
/**
|
|
* Time to wait coalescing outbound events into a single peer POST. Default
|
|
* 2000ms.
|
|
*/
|
|
outbound_flush_ms?: number;
|
|
}
|
|
|
|
/** Cloudflare Workers deployment config used by `WorkerDriver`. */
|
|
export interface IWorkersConfig {
|
|
XAUTHKEY?: string;
|
|
ACCOUNTID?: string;
|
|
/**
|
|
* Optional dispatch namespace — when set, scripts deploy under
|
|
* `/dispatch/namespaces/<ns>`.
|
|
*/
|
|
namespace?: string;
|
|
/**
|
|
* Base URL included as the `puter_endpoint` binding. Default
|
|
* `https://api.puter.com`.
|
|
*/
|
|
internetExposedUrl?: string;
|
|
/**
|
|
* URL returned by `getLoggingUrl()` — surfaced to clients that render
|
|
* worker logs.
|
|
*/
|
|
loggingUrl?: string;
|
|
[key: string]: string | undefined;
|
|
}
|
|
|
|
/**
|
|
* Optional outbound-fetch proxy used by `secureFetch()` when the backend has to
|
|
* fetch a user-supplied URL (e.g. image-gen `input_image`). Requests get
|
|
* prefixed with `url` and sent through the Worker with `x-cors-proxy-auth-
|
|
* secret: <secret>`; the Worker authenticates the secret, fetches the real URL,
|
|
* and strips CORS on the response. Unset → fetches go direct (still guarded by
|
|
* the URL/redirect/DNS checks in secureFetch).
|
|
*/
|
|
export interface ISecureCorsProxyConfig {
|
|
url: string;
|
|
secret: string;
|
|
}
|
|
|
|
export interface IWispConfig {
|
|
/** WISP relay server address returned to clients on token create. */
|
|
server?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface IServerHealthConfig {
|
|
/** DB liveness latency threshold (ms). Default 1500. */
|
|
db_liveness_latency_fail_ms?: number;
|
|
/**
|
|
* Latency threshold for the primary-pinned probe (ms). Separate from
|
|
* `db_liveness_latency_fail_ms` because that one measures the local read
|
|
* path while this one crosses to whichever region holds the primary.
|
|
* Default 3000. Values at or above the 4000ms per-check timeout are moot —
|
|
* the check runner gives up first.
|
|
*/
|
|
db_primary_liveness_latency_fail_ms?: number;
|
|
/**
|
|
* Consecutive over-threshold runs before the primary probe reports
|
|
* unhealthy. Default 2, i.e. sustained slowness rather than one slow round
|
|
* trip. A primary that errors or hangs fails on the first run regardless.
|
|
*/
|
|
db_primary_liveness_breaches_to_fail?: number;
|
|
/** Staleness threshold for the health-check loop itself (ms). */
|
|
stale_health_loop_fail_ms?: number;
|
|
/**
|
|
* Cadence for the external-dependency probes (redis, dynamo, object store,
|
|
* primary database). Deliberately slower than the 5s check loop so probing
|
|
* a paid, rate-limited backing service stays a rounding error against real
|
|
* traffic. Default 30000.
|
|
*/
|
|
dependency_check_interval_ms?: number;
|
|
/** Redis liveness latency threshold (ms). Default 1000. */
|
|
redis_liveness_latency_fail_ms?: number;
|
|
/** Dynamo liveness latency threshold (ms). Default 1500. */
|
|
dynamo_liveness_latency_fail_ms?: number;
|
|
/** Object-store liveness latency threshold (ms). Default 2000. */
|
|
s3_liveness_latency_fail_ms?: number;
|
|
/**
|
|
* Check names to skip registering entirely — an operator kill switch for a
|
|
* probe that turns out to be noisy, without waiting on a deploy.
|
|
*/
|
|
disabled_checks?: string[];
|
|
}
|
|
|
|
export interface IS3LocalConfig {
|
|
/**
|
|
* Run fauxqs entirely in-memory: random port on `127.0.0.1`, no `dataDir` /
|
|
* `s3StorageDir`. Intended for tests so each suite gets a pristine
|
|
* in-process S3.
|
|
*/
|
|
inMemory?: boolean;
|
|
host?: string;
|
|
port?: number;
|
|
dataDir?: string;
|
|
s3StorageDir?: string;
|
|
}
|
|
|
|
export interface IS3RemoteConfig {
|
|
useCredentialChain?: boolean;
|
|
endpoint: string;
|
|
/**
|
|
* Endpoint used when generating presigned URLs handed to clients (browser
|
|
* uploads/downloads). Defaults to `endpoint`. Set this when the server-side
|
|
* S3 endpoint isn't reachable from the browser — e.g. self-host with
|
|
* `endpoint: http://s3:9000` (docker-internal) and `publicEndpoint:
|
|
* http://localhost:9000` (host-published port).
|
|
*/
|
|
publicEndpoint?: string;
|
|
accessKeyId: string;
|
|
secretAccessKey: string;
|
|
region?: string;
|
|
/**
|
|
* Use path-style URLs (`<endpoint>/<bucket>`) instead of virtual-hosted
|
|
* style (`<bucket>.<endpoint>`). Defaults to AWS SDK's default (virtual-
|
|
* hosted, which only works on real AWS S3). Set `true` for S3-compatible
|
|
* servers (RustFS, MinIO, fauxqs) where DNS-style addressing fails.
|
|
*/
|
|
forcePathStyle?: boolean;
|
|
}
|
|
|
|
export interface IS3Config {
|
|
localConfig?: IS3LocalConfig;
|
|
s3Config?: IS3RemoteConfig;
|
|
}
|
|
|
|
export interface IDatabaseConfig {
|
|
engine: 'sqlite' | 'mysql' | 'postgres';
|
|
// sqlite
|
|
/**
|
|
* SQLite database file path. Defaults to `':memory:'` (the better-sqlite3
|
|
* in-memory mode), which is also what tests should use. `inMemory: true` is
|
|
* an explicit alias for the same.
|
|
*/
|
|
path?: string;
|
|
/**
|
|
* Force in-memory SQLite (ignores `path`). Equivalent to `path:
|
|
* ':memory:'`. Intended for tests so each suite gets a pristine in-process
|
|
* database. Test utilities also use `engine: 'postgres'` with `inMemory:
|
|
* true` to run against pgmock.
|
|
*/
|
|
inMemory?: boolean;
|
|
targetVersion?: number;
|
|
// mysql
|
|
host?: string;
|
|
port?: number;
|
|
user?: string;
|
|
password?: string;
|
|
database?: string;
|
|
connectionString?: string;
|
|
url?: string;
|
|
replica?: {
|
|
host?: string;
|
|
port?: number;
|
|
user?: string;
|
|
password?: string;
|
|
database?: string;
|
|
connectionString?: string;
|
|
url?: string;
|
|
};
|
|
/**
|
|
* Server-side execution cap for SELECT statements in ms (mysql engine;
|
|
* applies to both pools — MySQL only enforces it on SELECTs, so writes are
|
|
* unaffected). 0 disables. Default 30000.
|
|
*/
|
|
selectTimeoutMs?: number;
|
|
/**
|
|
* Max time to wait for a pooled connection before the query batcher treats
|
|
* acquisition as failed, in ms (mysql engine). 0 disables the bound.
|
|
* Default 5000.
|
|
*/
|
|
acquireTimeoutMs?: number;
|
|
/**
|
|
* Ordered list of directories whose `.sql` files are run sequentially at
|
|
* server start (mysql/postgres engines). Numbered migration filenames sort
|
|
* numerically; directories are processed in array order. Files MUST be
|
|
* idempotent — there is no per-file applied-state tracking. Relative paths
|
|
* resolve from `process.cwd()`.
|
|
*/
|
|
migrationPaths?: string[];
|
|
}
|
|
|
|
/**
|
|
* Bucket of pass-through values surfaced to the client-side `gui()` boot
|
|
* function. Known fields are declared for lookup hygiene; unknown keys are
|
|
* still tolerated so product teams can add one-off flags without churn.
|
|
*/
|
|
export interface IGuiParams {
|
|
title?: string;
|
|
short_description?: string;
|
|
social_media_image?: string;
|
|
/**
|
|
* Public SDK key for Prelude's JS Signals SDK. When set, the phone-
|
|
* verification window collects a `dispatch_id` (browser signals) and
|
|
* forwards it to /send-confirm-phone, which passes it to Prelude's Verify
|
|
* API so its abuse model can weigh the device. Omit to disable collection.
|
|
*/
|
|
preludeSdkKey?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface IDevWatcherConfig {
|
|
/** Force-enable/disable the dev watcher. Defaults to enabled in dev. */
|
|
enabled?: boolean;
|
|
/** Root path for watcher entries. Relative paths resolve from package root. */
|
|
root?: string;
|
|
/** Delay after watcher startup before boot continues. Default: 5000. */
|
|
ready_delay_ms?: number;
|
|
/** Optional extra child processes to start with the dev watcher. */
|
|
commands?: Array<{
|
|
name: string;
|
|
directory: string;
|
|
command: string;
|
|
args?: string[];
|
|
env?: Record<string, string>;
|
|
}>;
|
|
/**
|
|
* Optional webpack watcher entries. Omit to use the built-in GUI/puter.js
|
|
* watchers.
|
|
*/
|
|
webpack?: Array<{
|
|
name?: string;
|
|
directory: string;
|
|
env?: Record<string, string>;
|
|
}>;
|
|
}
|
|
|
|
/**
|
|
* Complete shape of Puter's root config. Everything is optional here —
|
|
* mandatory fields (only `port` + `extensions`) are pulled out of the
|
|
* `Partial<...>` below and listed after it.
|
|
*
|
|
* When adding a new config field, declare it here with a doc comment so there's
|
|
* a single discoverable reference for every config-driven switch.
|
|
*
|
|
* One value, one location: each setting lives at exactly one key. There are no
|
|
* legacy aliases or fallback paths — older configs that relied on them need to
|
|
* migrate.
|
|
*/
|
|
interface IConfigOptional {
|
|
// -- Environment / identity --------------------------------------
|
|
|
|
/**
|
|
* Environment marker. `dev` disables blocked-email checks, opens
|
|
* auto-browser, etc.
|
|
*/
|
|
env: 'dev' | 'prod';
|
|
/**
|
|
* Free-form name of the config profile (e.g. `oss-default`). Surfaced in
|
|
* logs.
|
|
*/
|
|
config_name: string;
|
|
/**
|
|
* Console output format. `json` replaces the global console so every call
|
|
* emits one structured JSON line (`level`, `timestamp`, `msg`, and the
|
|
* active `traceId`) — one event per call, so a line-oriented log collector
|
|
* can't split stack traces across events, and level filtering works. `text`
|
|
* (the default) leaves console output human-readable for local/dev.
|
|
*/
|
|
log_format: 'json' | 'text';
|
|
/**
|
|
* Keep serving after an uncaught exception instead of exiting. Uncaught
|
|
* exceptions are always logged either way; this only decides whether one
|
|
* ends the process. Default: false, matching Node's own behavior. Set it
|
|
* where losing the node costs more than running a possibly-degraded one — a
|
|
* small pool behind a health check that replaces bad nodes anyway.
|
|
*/
|
|
keep_alive_on_uncaught: boolean;
|
|
/** Server version. Falls back to `npm_package_version`. */
|
|
version: string;
|
|
/**
|
|
* Stable identity for this server node. Enables pager alerts + graceful
|
|
* shutdown delay.
|
|
*/
|
|
serverId: string;
|
|
|
|
// -- Networking / URLs -------------------------------------------
|
|
|
|
/**
|
|
* Protocol used for the externally-visible origin ('http' or 'https').
|
|
* Default: 'http'.
|
|
*/
|
|
protocol: string;
|
|
/** Primary domain for Puter (e.g., `puter.localhost`, `puter.com`). */
|
|
domain: string;
|
|
/**
|
|
* Externally-visible port. Defaults to `port`. Behind a reverse proxy, set
|
|
* this to the public port.
|
|
*/
|
|
pub_port: number;
|
|
/**
|
|
* Teams and teams. Off means `/teams` 404s and the schema is inert, so the
|
|
* tables can ship to production before anything can create a team. It is
|
|
* also the backout: turning it off removes the feature without touching
|
|
* data.
|
|
*/
|
|
teams_enabled: boolean;
|
|
/** Live teams one user may own. Default 1. */
|
|
max_teams_per_user?: number;
|
|
/** Seats one team may provision. Default 50. */
|
|
max_seats_per_team?: number;
|
|
/**
|
|
* Fully-qualified externally-visible URL (protocol + domain + port).
|
|
* Computed from `protocol`/`domain`/`pub_port` if unset.
|
|
*/
|
|
origin: string;
|
|
/**
|
|
* Public base URL for the API subdomain, e.g. `https://api.puter.com`. Used
|
|
* to build signed URLs.
|
|
*/
|
|
api_base_url: string;
|
|
/** Static hosting domain for user sites (e.g., `puter.site`). */
|
|
static_hosting_domain: string;
|
|
/** Alt static hosting domain. */
|
|
static_hosting_domain_alt: string;
|
|
/** Private app hosting domain (e.g., `app.puter.localhost`). */
|
|
private_app_hosting_domain: string;
|
|
/** Alt private app hosting domain. */
|
|
private_app_hosting_domain_alt: string;
|
|
/**
|
|
* Groups of equivalent app index_url hosts. Each group lists hosts that
|
|
* should resolve to the same canonical app: `appUidFromOrigin` looks up any
|
|
* DB row whose `index_url` is one of the group's hosts and returns that
|
|
* row's UID for every host in the group.
|
|
*
|
|
* Hosts listed here are also reserved — `apps.create` / `apps.update`
|
|
* reject any attempt to register a different app under one of these hosts,
|
|
* so the group is owned by exactly one app row.
|
|
*
|
|
* Entries are bare hosts (no scheme), lowercased. Example: [
|
|
* ["camera.puter.com", "camera.puter.site", "camera.ca"],
|
|
* ["player.puter.com", "player.puter.site"], ]
|
|
*/
|
|
app_origin_aliases?: string[][];
|
|
/** When true, accept any Host header value. Dev/testing only. */
|
|
allow_all_host_values: boolean;
|
|
/** When true, accept requests without a Host header. */
|
|
allow_no_host_header: boolean;
|
|
/** When true, allow nip.io wildcard domains. */
|
|
allow_nipio_domains: boolean;
|
|
/** When true, support custom domain resolution for hosted sites. */
|
|
custom_domains_enabled: boolean;
|
|
/** When true, enable IP validation via event bus. */
|
|
enable_ip_validation: boolean;
|
|
/**
|
|
* Express `trust proxy` setting — controls how `req.ip` is derived from
|
|
* `X-Forwarded-For`. Set to the number of reverse-proxy hops in front of
|
|
* the server (e.g. `1` for a single Cloudflare or nginx hop, `2` for
|
|
* Cloudflare → ALB → app), or to a CIDR / IP / list of trusted proxy
|
|
* addresses. `false` (default) disables XFF parsing — `req.ip` returns the
|
|
* direct socket peer, which is the safe choice when no proxy is in front.
|
|
* Never set to `true` in production: it trusts _every_ hop and makes XFF
|
|
* forgeable. See https://expressjs.com/en/guide/behind-proxies.html.
|
|
*/
|
|
trust_proxy: boolean | number | string | string[];
|
|
/** Don't launch browser when starting. */
|
|
no_browser_launch: boolean;
|
|
/** Disable dev-time frontend webpack watchers. */
|
|
no_devwatch: boolean;
|
|
/**
|
|
* Skip first-boot bootstrap of the `admin` user and the credentials banner
|
|
* that DefaultUserService prints. Intended for tests.
|
|
*/
|
|
no_default_user: boolean;
|
|
/**
|
|
* Import `.ts` extension sources instead of built `.js`. Only for
|
|
* transform-capable runtimes (the test harness sets this); plain node
|
|
* cannot execute the TypeScript sources.
|
|
*/
|
|
import_ts_extensions?: boolean;
|
|
/** Optional dev-time frontend watcher overrides. */
|
|
devwatch: IDevWatcherConfig;
|
|
|
|
// -- Auth / session ----------------------------------------------
|
|
|
|
/** HMAC secret used to sign and verify auth JWTs (`kid: 'v2'`). */
|
|
jwt_secret_v2: string;
|
|
/**
|
|
* Optional extra `Origin` header values allowed to call the routes that
|
|
* hand back a session credential (`/login`, `/signup`,
|
|
* `/session/sync-cookie` — see `guiOriginGate`). The main `origin` is
|
|
* always allowed, and callers with no `Origin` at all (CLI, mobile,
|
|
* server-side, tests) are never gated.
|
|
*
|
|
* Only for deployments that genuinely serve their GUI from a different
|
|
* origin than `config.origin`. Do NOT list loopback origins in production:
|
|
* `http://localhost:4000` is not an authenticatable origin, it's whatever
|
|
* is listening on that port on the visitor's machine. A local GUI should
|
|
* obtain its token via the AuthMe flow instead.
|
|
*/
|
|
allow_gui_origins?: string[];
|
|
/** HMAC secret for signed file URLs (/file, /writeFile, /sign). */
|
|
url_signature_secret: string;
|
|
/** Name of the session cookie the auth probe reads. */
|
|
cookie_name: string;
|
|
/** Minimum password length for login/signup validation. */
|
|
min_pass_length: number;
|
|
/** When true, allow the 'system' user to log in. */
|
|
allow_system_login: boolean;
|
|
/**
|
|
* When true, anonymous users cannot create new accounts or temporary
|
|
* sessions. Existing accounts can still log in, and pre-existing
|
|
* placeholder rows may still be claimed.
|
|
*/
|
|
disable_user_signup: boolean;
|
|
/** Reject auth-gated routes unless the user has confirmed their email. */
|
|
strict_email_verification_required: boolean;
|
|
/**
|
|
* Force SMS phone verification on every new signup, regardless of abuse
|
|
* reputation. Off by default; mainly a test/QA switch so the phone gate can
|
|
* be exercised on demand (it otherwise only triggers for low-reputation
|
|
* signups). Requires `prelude.apiKey` to actually deliver codes.
|
|
*/
|
|
always_require_phone_verification: boolean;
|
|
/**
|
|
* Force credit-card verification on every new signup, regardless of abuse
|
|
* reputation. Off by default; mainly a test/QA switch so the card gate can
|
|
* be exercised on demand (it otherwise only triggers for low-reputation
|
|
* signups). Requires a payments extension to actually run the $0 auth.
|
|
*/
|
|
always_require_card_verification: boolean;
|
|
/**
|
|
* Let a user who keeps getting blocked on SMS phone verification fall back
|
|
* to credit-card verification, which clears the phone gate (and the card
|
|
* gate too, when one is set).
|
|
*
|
|
* The fallback opens once the user has made `after_attempts` SMS _send_
|
|
* attempts inside the send rate-limit window, which by default means only
|
|
* after they have used up the window's entire send allowance — the card
|
|
* option is an escape hatch for a phone that isn't working, not a choice
|
|
* offered alongside a working SMS flow. Successful sends count too, so this
|
|
* trades the phone signal for a card signal; it does NOT guarantee SMS
|
|
* actually failed. Once open, the fallback stays open for 24 hours so the
|
|
* user can finish the card flow. Requires a payments extension to run the
|
|
* actual card check.
|
|
*/
|
|
phone_verification_card_fallback: {
|
|
/**
|
|
* Tri-state. Set it and that wins, either way — this is the opt-out.
|
|
* Omit it and the fallback is on wherever both gates it bridges
|
|
* actually work: an SMS provider is configured _and_ an installed
|
|
* extension reports card verification enabled. On a build with no card
|
|
* gate behind it the fallback stays off, since taking the offer there
|
|
* could only strand the user.
|
|
*/
|
|
enabled?: boolean;
|
|
/**
|
|
* SMS send attempts (within the send rate-limit window) before the card
|
|
* fallback opens. Defaults to the send route's full rate limit
|
|
* (10/hour), i.e. the fallback appears only once the user is out of SMS
|
|
* attempts. Values above that limit are clamped down to it — requests
|
|
* past the route limit never reach the attempt counter, so a higher
|
|
* threshold could never be crossed. Lower it (e.g. 2) to reach the card
|
|
* path without burning the whole allowance, which is mainly useful for
|
|
* QA.
|
|
*/
|
|
after_attempts?: number;
|
|
};
|
|
/** Captcha configuration. */
|
|
captcha: { enabled: boolean; difficulty?: 'easy' | 'medium' | 'hard' };
|
|
/** OIDC / OAuth2 providers (google + custom). */
|
|
oidc: IOIDCConfig;
|
|
|
|
// -- Groups / provisioning ---------------------------------------
|
|
|
|
/**
|
|
* UID of the persistent group that non-temp users are enrolled in at
|
|
* signup.
|
|
*/
|
|
default_user_group: string;
|
|
/**
|
|
* UID of the persistent group that temporary users are enrolled in at
|
|
* signup.
|
|
*/
|
|
default_temp_group: string;
|
|
/** When true, ACL grants read/list/see on `/<user>/Public` to any actor. */
|
|
enable_public_folders: boolean;
|
|
|
|
/**
|
|
* Whether a recipient who already has an account is emailed about a share
|
|
* as well as notified in-app. **On unless set to false**; recipients
|
|
* decline with the unsubscribe link the mail carries, or by blocking a
|
|
* sender. An invite to an address with no account is emailed regardless.
|
|
*/
|
|
share_email_notifications?: boolean;
|
|
|
|
/**
|
|
* Ceiling on how many shares one user may create per UTC day. An abuse
|
|
* bound, not an accounting one — it exists so a script can't blanket other
|
|
* accounts with unwanted items and the notifications that follow. Omit to
|
|
* use the built-in default.
|
|
*/
|
|
share_daily_limit?: number;
|
|
|
|
/**
|
|
* How often a share may interrupt its recipient — the notification pushed
|
|
* to their screen and the email that goes with it. The share itself is
|
|
* never refused for being over budget; only the announcement is dropped.
|
|
*
|
|
* Both axes are needed: the pair bounds hold one sharer back, and the
|
|
* recipient bounds are what stop many senders from burying one person
|
|
* between them. Omit any field for the built-in default; a non-positive
|
|
* value removes that bound.
|
|
*/
|
|
share_notify_limits?: {
|
|
/**
|
|
* Quiet period after one sharer reaches a recipient, in seconds. Also
|
|
* how long their notification keeps absorbing new shares.
|
|
*/
|
|
pairWindowSeconds?: number;
|
|
/** Interruptions one sharer may cause a recipient per day. */
|
|
pairDaily?: number;
|
|
/** Interruptions a recipient may receive per hour, from anyone. */
|
|
recipientHourly?: number;
|
|
/** Same, per day. */
|
|
recipientDaily?: number;
|
|
/**
|
|
* How long emails to one recipient are held and merged into a single
|
|
* digest, in seconds. Default 90; non-positive sends immediately.
|
|
*/
|
|
emailBatchSeconds?: number;
|
|
};
|
|
|
|
/**
|
|
* Ceiling on recipients, and on items, in a single share request. Bounds
|
|
* the fan-out one call can trigger; the daily limit bounds the total.
|
|
*/
|
|
share_max_recipients?: number;
|
|
share_max_items?: number;
|
|
|
|
/**
|
|
* How long a notification is kept, in days from creation — the mailbox is
|
|
* not an archive, so acknowledged or not, a row past this goes. Omit it, or
|
|
* set 0, and nothing is ever swept.
|
|
*/
|
|
notificationRetentionDays?: number;
|
|
|
|
// -- Storage / S3 ------------------------------------------------
|
|
|
|
/** S3 storage config (local fauxqs or remote). */
|
|
s3: IS3Config;
|
|
/** Default S3 bucket for file storage. */
|
|
s3_bucket: string;
|
|
/** Default S3 region. */
|
|
s3_region: string;
|
|
/** Fallback AWS region. */
|
|
region: string;
|
|
/**
|
|
* Where each node stores files, keyed by node id - the same ids `user.home`
|
|
* names. A write may pass one to place its bytes there; absent, or absent
|
|
* an entry, every write uses `s3_bucket` / `s3_region`.
|
|
*/
|
|
servers?: Record<string, { bucket: string; bucketRegion: string }>;
|
|
/** Default storage capacity per user (bytes). */
|
|
storage_capacity: number;
|
|
/** When false, storage is effectively unlimited (bounded by device space). */
|
|
is_storage_limited: boolean;
|
|
/** Bytes of device storage available (used when is_storage_limited=false). */
|
|
available_device_storage: number;
|
|
/** Optional dedicated S3-compatible bucket used by the thumbnails extension. */
|
|
thumbnailStore: IThumbnailStoreConfig;
|
|
|
|
// -- Database ----------------------------------------------------
|
|
|
|
database: IDatabaseConfig;
|
|
|
|
// -- Clients / infra ---------------------------------------------
|
|
|
|
dynamo: IDynamoConfig;
|
|
redis: IRedisConfig;
|
|
/** Read cache in front of KV point reads. Off unless `enabled` is set. */
|
|
kvCache: IKvCacheConfig;
|
|
pager: IPagerConfig;
|
|
email: IEmailConfig;
|
|
/** Optional — only set when SMS phone verification (Prelude) is wired in. */
|
|
prelude: IPreludeConfig;
|
|
/** Optional — only set when a ClickHouse analytics client is wired in. */
|
|
clickhouse?: IClickhouseConfig;
|
|
cf_file_cache: ICfFileCacheConfig;
|
|
|
|
// -- Rate limiting -----------------------------------------------
|
|
|
|
rate_limit: {
|
|
/**
|
|
* Rate limiter backend selection.
|
|
*
|
|
* - `memory`: per-node in-memory counters.
|
|
* - `redis`: sorted-sets in Redis — shared state across nodes (default).
|
|
* - `kv`: per-hit rows in the system KV store (DynamoDB), with TTL.
|
|
*/
|
|
backend?: 'memory' | 'redis' | 'kv';
|
|
};
|
|
|
|
// -- AI / integration providers ----------------------------------
|
|
//
|
|
// All AI providers — chat, image, video, TTS, OCR, speech-to-text,
|
|
// speech-to-speech — are configured under `providers[<provider-id>]`.
|
|
// Provider ids match the driver-side identifier (e.g. `claude`,
|
|
// `openai-image-generation`, `aws-textract`). There is no `services`
|
|
// bag and no top-level `openai`/`gemini`/`mistral`/`elevenlabs`/`aws`
|
|
// shortcut.
|
|
providers: Record<string, IAIProviderConfig | undefined>;
|
|
|
|
// -- Cross-node / external integrations --------------------------
|
|
|
|
/** Cross-node event replication config. */
|
|
broadcast: IBroadcastConfig;
|
|
/** WebRTC signalling + TURN. */
|
|
peers: IPeersConfig;
|
|
/** WISP relay proxy. */
|
|
wisp: IWispConfig;
|
|
/** Cloudflare Workers driver config. */
|
|
workers: IWorkersConfig;
|
|
/** Optional CORS-stripping signed-Worker proxy used by `secureFetch`. */
|
|
secureCorsProxy: ISecureCorsProxyConfig;
|
|
/** Legacy Stripe billing extension. */
|
|
|
|
// -- GUI / static mounts -----------------------------------------
|
|
|
|
/** Absolute path to the GUI assets root. */
|
|
gui_assets_root: string;
|
|
/** Which profile in `puter-gui.json` to load. Default: `development`. */
|
|
gui_profile: string;
|
|
/** Map of built-in app name → local directory served at `/builtin/<name>`. */
|
|
builtin_apps: Record<string, string>;
|
|
/** Force the bundled GUI even in dev. Default: false. */
|
|
use_bundled_gui: boolean;
|
|
/** Override the GUI bundle JS path. Default: `/dist/bundle.min.js`. */
|
|
gui_bundle: string;
|
|
/** Override the GUI CSS path when bundled. Default: `/dist/bundle.min.css`. */
|
|
gui_css: string;
|
|
/**
|
|
* Override the puter.js preload URL when bundled. Default:
|
|
* `https://js.puter.com/v2/`.
|
|
*/
|
|
gui_puterjs_bundle: string;
|
|
/**
|
|
* Free-form bag of values passed through to the client-side `gui()`
|
|
* function.
|
|
*/
|
|
gui_params: IGuiParams;
|
|
/**
|
|
* Absolute path to the directory holding native app bundles, each in a
|
|
* subdirectory matching its subdomain (e.g. `<root>/editor/`).
|
|
*/
|
|
native_apps_root: string;
|
|
/**
|
|
* Absolute path to a directory holding `puter.js`/`putility.js` version
|
|
* bundles.
|
|
*/
|
|
client_libs_root: string;
|
|
/** Path to the puter-js SDK root (serves `/sdk/*` and `/puter.js/v{1,2}`). */
|
|
puterjs_root: string;
|
|
|
|
// -- Extension-specific ------------------------------------------
|
|
|
|
/**
|
|
* Flat `{ flag_name: boolean }` bag of feature toggles. Non-boolean values
|
|
* are coerced before use.
|
|
*
|
|
* Server-only by default. Flags are surfaced to clients via `/whoami` only
|
|
* if their key is on the allowlist in `extensions/whoami.ts`
|
|
* (`CLIENT_VISIBLE_FEATURE_FLAGS`). New flags should be assumed internal —
|
|
* add them to the allowlist explicitly if (and only if) the client needs to
|
|
* read them.
|
|
*/
|
|
feature_flags: Record<string, boolean | string | number>;
|
|
/** Blocked email TLDs / domains — checked in `prod` only. */
|
|
blockedEmailDomains: string[];
|
|
/** Contact-form recipient. Default `support@puter.com`. */
|
|
support_email: string;
|
|
/** Worker / subdomain names that cannot be allocated by users. */
|
|
reserved_words: string[];
|
|
/** Max subdomains a single user may own. Default 10. */
|
|
max_subdomains_per_user: number;
|
|
/** Health-check tuning. */
|
|
server_health: IServerHealthConfig;
|
|
|
|
//Metering
|
|
unlimitedMetering?: boolean;
|
|
/**
|
|
* Fleet-wide spend rate, in micro-cents per minute, past which the metering
|
|
* service raises an alarm. There is no defensible default here — the right
|
|
* number is a multiple of what this deployment's own traffic normally
|
|
* costs, so set it from observed rate and revisit it as traffic grows. An
|
|
* unset or non-positive value turns the check off rather than guessing.
|
|
*/
|
|
maxGlobalUsagePerMinute?: number;
|
|
/**
|
|
* How long per-request usage (response bytes, object-store requests) is
|
|
* held in memory before being written, in milliseconds. This is the dial
|
|
* between how promptly that usage lands and how many writes it costs: every
|
|
* actor active in a window settles once per window, so halving it doubles
|
|
* the write rate. Unset uses the service default; a non-positive value is
|
|
* ignored.
|
|
*/
|
|
meteringUsageBufferFlushMs?: number;
|
|
/**
|
|
* Whether recorded usage is also enforced: an account with nothing left of
|
|
* its budget is turned away from the operations that spend it (file
|
|
* transfers, KV calls) with a 402. Metadata reads and deletions stay open,
|
|
* as does serving a hosted site — those bytes are billed to the account
|
|
* hosting it but requested by visitors who have no say in its balance.
|
|
*
|
|
* - `enabled` — defaults to on, and covers the plan gates below as well. The
|
|
* switch to reach for if enforcement is turning away traffic it
|
|
* shouldn't; usage is still recorded either way.
|
|
* - `workers` — extend enforcement to worker-driven calls. Off by default: a
|
|
* worker has no prompt to show and nobody watching, so being cut off
|
|
* presents as a program that started failing.
|
|
* - `subscriptions` — whether surfaces that declare a plan requirement
|
|
* (`requireSubscription`) enforce it. Defaults to on, and off wherever no
|
|
* paid plan exists to be on (self-hosted installs ship it off). Turning
|
|
* it off opens those surfaces to every account without unpicking the
|
|
* declarations; `enabled: false` turns it off along with everything
|
|
* else.
|
|
*/
|
|
meteringEnforcement?: {
|
|
enabled?: boolean;
|
|
workers?: boolean;
|
|
subscriptions?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Reputation gating: what the tiers named by `requireReputation` on a route
|
|
* or a driver method actually take.
|
|
*
|
|
* - `enabled` — the one switch that stops every declared gate enforcing,
|
|
* without unpicking the declarations. Defaults to on, which by itself
|
|
* gates nothing: a tier only bites once `tiers` gives it a score.
|
|
* - `tiers` — tier name → the minimum score (0-100) an account needs to pass
|
|
* it. The numbers live here rather than in the declarations because what
|
|
* counts as trusted enough is a per-deployment call, retuned far more
|
|
* often than the surfaces it protects. A tier with no entry is inert: an
|
|
* install that doesn't score its accounts must not turn traffic away on a
|
|
* score it never computed.
|
|
*/
|
|
reputationGate?: {
|
|
enabled?: boolean;
|
|
tiers?: Record<string, number>;
|
|
};
|
|
|
|
/**
|
|
* Subscribable events over filesystem and key-value changes.
|
|
*
|
|
* - `enabled` — the master switch for the whole surface. Absent means off:
|
|
* the dispatch hooks short-circuit on a boolean before resolving
|
|
* anything, and the subscribe verbs reject with `events_disabled`. An
|
|
* install that has never heard of events pays nothing on its write path.
|
|
* - `crossAppKv` — whether a subscription may name another app's KV
|
|
* namespace. Absent means off, and subscribing to one rejects with
|
|
* `events_cross_app_disabled`: a standing cross-app push leans entirely
|
|
* on revocation settling, so it turns on only once that has run on the
|
|
* simpler surface. The gate itself is enforced either way.
|
|
* - `notificationsFoldIn` — whether notification delivery runs through events
|
|
* dispatch. Absent means off, and `enabled` gates it: the fold-in has
|
|
* nowhere to dispatch from with the surface itself switched off. The
|
|
* socket wire is identical either way — the flag decides which layer
|
|
* produced the delivery, not what the desktop receives.
|
|
* - `kvHandles` — whether one user may hand another a watchable region of
|
|
* their key-value namespace. Absent means off: minting and subscribing
|
|
* through a handle both reject with `events_kv_handles_disabled`, and a
|
|
* handle row already made stops delivering. Nothing on the write path
|
|
* reads it.
|
|
* - `workerRuntime` — whether publishing handlers deploys a per-app events
|
|
* worker and delivery invokes it. Absent means off: publish stores rows
|
|
* and nothing is deployed, and the invoker keeps its null resolver, so
|
|
* worker-target deliveries stay retriable until something answers.
|
|
* - `forwardSession` — whether `onLocal` (session) subscriptions are
|
|
* forwarded across regions. On unless set to `false`, which holds back
|
|
* the per-token remote-watch index: a write in another region then never
|
|
* reaches a session subscription here. Durable (`onPersistent`)
|
|
* subscriptions and the rest of the addressed channel are unaffected
|
|
* either way.
|
|
*/
|
|
events?: {
|
|
enabled?: boolean;
|
|
crossAppKv?: boolean;
|
|
notificationsFoldIn?: boolean;
|
|
kvHandles?: boolean;
|
|
workerRuntime?: boolean;
|
|
forwardSession?: boolean;
|
|
/** How long a handler has to answer an invocation. Default 30 s. */
|
|
invokeTimeoutMs?: number;
|
|
/**
|
|
* Dispatch namespace events workers are deployed into. Must not be the
|
|
* one the public worker dispatcher serves — that namespace is reachable
|
|
* at `<script>.puter.work`, and an events worker must not be. No
|
|
* default: deploys refuse rather than land somewhere public.
|
|
*/
|
|
workerNamespace?: string;
|
|
/**
|
|
* Where the events dispatcher answers `POST /invoke` — its own
|
|
* hostname, since it carries no zone route. Unset means invocations
|
|
* have nowhere to go and stay retriable.
|
|
*/
|
|
dispatcherUrl?: string;
|
|
/**
|
|
* Shared secret between this backend and the events dispatcher, in both
|
|
* directions, and the master the per-script invoke keys derive from.
|
|
* Rotating it rotates every key at the next deploy.
|
|
*/
|
|
internalSecret?: string;
|
|
};
|
|
|
|
/**
|
|
* Display multiplier converting metered amounts into the "credits" clients
|
|
* show. Applied server-side by the usage-reporting endpoints, so raw
|
|
* metered amounts never leave the API; purely presentational, so it can
|
|
* change without a data migration. No default — when unset, the endpoints
|
|
* report raw amounts and clients render dollars.
|
|
*/
|
|
creditMultiplier?: number;
|
|
}
|
|
|
|
/**
|
|
* Extension-augmentable config surface. Extensions add their own config keys
|
|
* via TypeScript declaration merging:
|
|
*
|
|
* declare module '@heyputer/backend/types' {
|
|
* interface IExtensionConfig {
|
|
* myExtension?: { foo: string };
|
|
* }
|
|
* }
|
|
*
|
|
* Augmentations flow into `IConfig`, which is what `this.config` /
|
|
* `extension.config` are typed as everywhere.
|
|
*/
|
|
export interface IExtensionConfig {
|
|
/**
|
|
* Open index signature so config reads of extension-only keys return
|
|
* `unknown` (not a type error). Extensions that declare-merge concrete keys
|
|
* (`myExt?: { foo: string }`) override this for the named key — the
|
|
* concrete property type wins over the index signature.
|
|
*/
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export type IConfig = Partial<IConfigOptional> & {
|
|
extensions: string[];
|
|
port: number;
|
|
} & IExtensionConfig;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
|
|
export interface WithLifecycle extends Object {
|
|
onServerStart?: () => Promise<void> | void;
|
|
onServerShutdown?: () => Promise<void> | void;
|
|
onServerPrepareShutdown?: () => Promise<void> | void;
|
|
}
|
|
|
|
export interface WithCostsReporting extends WithLifecycle {
|
|
getReportedCosts?: () => // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
| Promise<Record<string, any>[]>
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
| Record<string, any>[];
|
|
}
|
|
|
|
export interface WithControllerRegistration extends WithCostsReporting {
|
|
registerRoutes: (router: PuterRouter) => void;
|
|
}
|
|
|
|
export type LayerInstances<
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
T extends Record<string, (new (...args: any[]) => any) | any>,
|
|
> = {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
[K in keyof T]: T[K] extends new (...args: any[]) => any
|
|
? InstanceType<T[K]>
|
|
: T[K];
|
|
};
|