mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-06 14:07:51 +00:00
* test(ai): end every realtime relay test's upstream before the next The fake upstream now answers response.create with response.created first, as OpenAI does, so closing those sessions no longer waits out the settle window. * feat(ai): ask realtime clients to reconnect before a restart Open realtime sessions get a puter.reconnect event when the drain starts, with close_in_ms set to the drain window, and new sessions are still refused until the 1012 close at shutdown. A host extension can announce a restart early by emitting serverRestartAnnounced; that only sends the notice, so a false alarm leaves the server working normally. A session gets the notice at most once a minute. onServerDrainStart now receives the drain duration. * fix(ai): retry realtime reconnects for about a minute and wait out offline browsers Five attempts gave up after about 15 seconds, shorter than a server restart. The conversation makes ten attempts with the backoff capped at 10 seconds, and while the browser reports being offline it waits for the online event, up to two minutes per reconnect, instead of spending attempts. close() ends either wait and removes its listener. * feat(ai): restore recent conversation context after a realtime reconnect The provider starts a fresh session on every reconnect, so the conversation lost all earlier items. The SDK now records finished items it can recreate from the provider's own events: user text, user speech with a transcript, assistant replies as text, and function calls with their outputs. After the new session acknowledges its settings, and before ready fires, it adds back the newest of them, up to 40 items and 48 KiB, which stays under the relay's upload burst. Assistant replies are cut to what the user saw or heard: the deltas shown before an interruption, and the share of the transcript matching the audio played before a truncation or a drop. Deleted items, unfinished replies, images, speech without a transcript and incomplete function calls are left out, as are function outputs whose call falls outside the window. Restored copies take over their new item IDs. A refused item is reported once as context_restore_failed without failing the reconnect. * feat(ai): move a realtime conversation to a new session on the restart notice On puter.reconnect the conversation waits until it is idle: no reply requested, generating or playing, no user turn being spoken or transcribed, no function call awaiting its output and no acknowledgment pending. Then it reconnects through the usual path, emitting reconnecting (code 1012) and ready with reconnected: true, without an error. Until then the session keeps working, and if the server closes it first the normal 1012 reconnect takes over. close() at any point stops the wait. * docs(ai): realtime restart notice, context restore and reconnect limits * test(ai): realtime reconnects against the local server The mock provider answers with item events and whole replies like OpenAI. New cases cover a dropped connection (cut through a TCP proxy so the client sees 1006), a host-announced restart, and a draining server refusing the reconnect; the first two check the provider receives the restored items before the next turn. The browser media test waits for captured audio and the reply instead of a fixed sleep. The live suite gains a tool and text context round trip across a restart notice. * ci: run the live realtime conversation tests with the AI provider suite They need the OpenAI key, which only this workflow has, and skip themselves without it. * fix(ai): open the replacement before leaving a realtime session on the restart notice The handover closed the current session and then reconnected, so every notice cost a second or two in which sends failed and microphone audio was dropped. On a single server that refuses new sessions while it drains, the retries ran out during the drain and the conversation closed sooner than if it had stayed. The conversation now opens a replacement session as soon as the notice arrives and switches only once it is open and the conversation is idle. A refused replacement is retried with backoff while the current session keeps working; when the server closes it first, the held replacement is used right away. The idle check also missed some in-progress work: - a reply created while the user was still speaking ended the user's turn; speech now lasts until its audio is committed or cleared, and a committed turn waits for the reply server turn detection creates - only the newest function call was checked; every finished call since the last user message needs its output, and calls cut off by a cancellation don't count - native session.update and conversation.item.create events the provider hadn't acknowledged could be lost; the switch waits for them, up to the acknowledgment timeout for items - only the newest transcription was waited on; every committed item is - clearing playback didn't recheck the switch Restored items stay internal: the new session's copies of them no longer reach listeners. At a reconnect, every queued item the user didn't hear is cut, not just the one playing. A notice from a failed reconnect attempt is dropped. * test(ai): realtime replay after split settings, and a draining server refusing the replacement The relay accepts the context replay right after two full-size settings updates. Against the local server, a draining relay refuses the replacement session while the current one keeps answering. * ci: build the SDK in the live realtime step so it stands on its own * fix(ai): a failed realtime reconnect attempt no longer poisons the next An attempt that dropped after sending its session.update but before the acknowledgment left that event ID queued, so the next attempt's acknowledgment was matched to it and timed out, and so on until the conversation closed. Turn state is now reset after each failed attempt. * fix(ai): bound the realtime handover's waits and stop retries that can't succeed - The first replacement opens within a randomized 3 seconds of the notice, so sessions notified together don't all open at once. - A replacement refused with a code retrying can't fix, such as insufficient_funds, ends the attempts; the server's close still falls back to the usual reconnect. - A held replacement that gets an error or its own server's notice is closed and replaced. - A held replacement used after a drop gets a 3 second acknowledgment timeout, since its socket may have died with the network. - close() during the switch closes the held replacement too. - The reply to a committed turn and each transcription hold the switch for at most 10 and 30 seconds, like app items already did, and an app item is released only by an added item of its type, role and call ID. * docs(ai): the realtime restart notice isn't guaranteed before every restart
1369 lines
53 KiB
TypeScript
1369 lines
53 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;
|
|
/** Use the default AWS credential chain instead of the keys. */
|
|
useCredentialChain?: boolean;
|
|
}
|
|
|
|
export interface IDynamoConfig {
|
|
aws?: IAWSCredentials;
|
|
endpoint?: string;
|
|
/**
|
|
* Local dynalite store path. Default `./volatile/runtime/puter-ddb`;
|
|
* `':memory:'` for tests.
|
|
*/
|
|
path?: string;
|
|
/** Same as `path: ':memory:'`. */
|
|
inMemory?: boolean;
|
|
/** Create missing tables at startup. Off by default; for local emulators. */
|
|
bootstrapTables?: boolean;
|
|
}
|
|
|
|
export interface IRedisConfig {
|
|
startupNodes?: Array<{
|
|
host: string;
|
|
port: number;
|
|
}>;
|
|
/** Default `true`; set `false` for plain-TCP Valkey/Redis. */
|
|
tls?: boolean;
|
|
/**
|
|
* Use ioredis-mock in-process. Defaults to `true` when `startupNodes` is
|
|
* empty.
|
|
*/
|
|
useMock?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Redis read cache in front of KV point reads for user/app namespaces. Off
|
|
* unless `enabled`. Disabling does not clear entries, so wait out `ttlSeconds`
|
|
* before re-enabling or stale values can be served.
|
|
*/
|
|
export interface IKvCacheConfig {
|
|
/** Default false. */
|
|
enabled?: boolean;
|
|
/** Seconds a cached value is served for. Default 60. */
|
|
ttlSeconds?: number;
|
|
/** Seconds a cached absence is served for. Default 10. */
|
|
missTtlSeconds?: number;
|
|
/**
|
|
* Seconds after a write during which that key's reads bypass the cache.
|
|
* Must exceed mutation visibility lag or a racing read re-caches the old
|
|
* value. Default 5.
|
|
*/
|
|
blockSeconds?: number;
|
|
/**
|
|
* Largest cached entry in bytes; bigger values are read through. Default
|
|
* 32768.
|
|
*/
|
|
maxEntryBytes?: number;
|
|
/**
|
|
* Milliseconds to batch peer invalidation broadcasts; 0 sends 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;
|
|
}
|
|
|
|
/**
|
|
* Puter-addressed user mailboxes: the shared secret the ingress endpoint
|
|
* checks, and the optional local SMTP receiver that feeds it.
|
|
*
|
|
* The receiver is the self-hosted stand-in for an externally-hosted inbound
|
|
* mail path — `localServer` switches it on the same way `workers.localServer`
|
|
* switches workers to an in-process implementation. It runs as its own process,
|
|
* so nothing here affects the main server when it is off.
|
|
*/
|
|
export interface IUserEmailConfig {
|
|
/** Shared secret the ingress endpoint requires. Unset disables ingress. */
|
|
secret?: string;
|
|
/** Run the local SMTP receiver. Absent or falsy and nothing binds. */
|
|
localServer?: boolean;
|
|
/** Bind port. Default 2525 — set 25 only where the process may bind it. */
|
|
localPort?: number;
|
|
/** Bind address. Default '0.0.0.0'. */
|
|
localHost?: string;
|
|
/** Domains to accept recipients for. Required with `localServer`. */
|
|
localDomains?: string[];
|
|
/** Greeting and EHLO name. Defaults to the first accepted domain. */
|
|
localHostname?: string;
|
|
/** Ingress URL. Defaults to `<api_base_url>/email/ingress`. */
|
|
localIngressUrl?: string;
|
|
/**
|
|
* Host header to send. The ingress route is served on the `api` subdomain,
|
|
* so this must name it when `localIngressUrl` points at an internal
|
|
* address. Defaults to the host of `api_base_url`.
|
|
*/
|
|
localIngressHost?: string;
|
|
/** Envelope recipients accepted per message. Default 50. */
|
|
localMaxRecipients?: number;
|
|
/**
|
|
* Local parts that take feedback instead of naming a mailbox. Default
|
|
* `['fbl', 'abuse', 'postmaster']`.
|
|
*/
|
|
feedbackAddresses?: string[];
|
|
/**
|
|
* Concurrent client connections. Default 20. Each message is held in memory
|
|
* while it is delivered, so this also bounds memory use.
|
|
*/
|
|
localMaxClients?: number;
|
|
[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, the canonical name. `readProviderKey` also takes `secret_key`,
|
|
* `api_key` or `key` from older configs, the same way in every AI driver.
|
|
*/
|
|
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;
|
|
/**
|
|
* Largest outbound POST body in bytes; a flush over it goes out as several
|
|
* POSTs, and a peer that is down is owed at most four bodies' worth.
|
|
* Default 4 MiB.
|
|
*/
|
|
outboundMaxBodyBytes?: 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;
|
|
/**
|
|
* Let locally run workers reach private-network (LAN) addresses. Off by
|
|
* default; public addresses and this machine are always reachable.
|
|
*/
|
|
allowLanAccess?: boolean;
|
|
[key: string]: string | boolean | 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>;
|
|
}>;
|
|
}
|
|
|
|
/**
|
|
* Root config. Only `port` and `extensions` are mandatory; they are added in
|
|
* `IConfig` below. Every config field is declared here with a doc comment so
|
|
* this is the one reference. Each setting has exactly one key, no aliases.
|
|
*/
|
|
interface IConfigOptional {
|
|
// -- Environment / identity --------------------------------------
|
|
|
|
/** `dev` disables blocked-email checks, opens the browser, etc. */
|
|
env: 'dev' | 'prod';
|
|
/** Config profile name (e.g. `oss-default`), surfaced in logs. */
|
|
config_name: string;
|
|
/**
|
|
* `json` emits one structured line per console call (`level`, `timestamp`,
|
|
* `msg`, `traceId`); `text` (default) stays human-readable.
|
|
*/
|
|
log_format: 'json' | 'text';
|
|
/**
|
|
* Keep serving after an uncaught exception instead of exiting. Default
|
|
* false. For nodes behind a health check that replaces bad ones anyway.
|
|
*/
|
|
keep_alive_on_uncaught: boolean;
|
|
/** Server version. Falls back to `npm_package_version`. */
|
|
version: string;
|
|
/** Stable node identity. Enables pager alerts and graceful shutdown delay. */
|
|
serverId: string;
|
|
|
|
// -- Networking / URLs -------------------------------------------
|
|
|
|
/** Externally-visible protocol. 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;
|
|
/**
|
|
* Idle keep-alive timeout for the HTTP server, in ms. Must stay above the
|
|
* idle timeout of any proxy in front of it, or the proxy reuses connections
|
|
* this server has already closed. Default 620000.
|
|
*/
|
|
keep_alive_timeout: 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. On, boot refuses unless an extension registers the `team` store and
|
|
* service.
|
|
*/
|
|
teams_enabled: boolean;
|
|
/** Live teams one user may own. Default 1. */
|
|
max_teams_per_user?: number;
|
|
/** Seats a team whose owner pays nothing may provision. Default 3. */
|
|
max_seats_per_team_free?: number;
|
|
/**
|
|
* Seats a paying owner's team may provision. Default 40; a plan's
|
|
* `teamSeatCap` wins.
|
|
*/
|
|
max_seats_per_team_paid?: number;
|
|
/** One flat cap whatever the owner pays; overrides both of the above. */
|
|
max_seats_per_team?: number;
|
|
/**
|
|
* Only these email domains may enter the teams surface; members of an
|
|
* existing team always pass. Unset means everyone.
|
|
*/
|
|
teams_allowed_email_domains?: string[];
|
|
/**
|
|
* 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;
|
|
/**
|
|
* Content-Security-Policy applied to active documents served from hosted
|
|
* sites. Unset means no header at all, which is the default: hosted sites
|
|
* are arbitrary third-party apps, so an enforcing policy has to be rolled
|
|
* out with evidence rather than switched on blind.
|
|
* `RECOMMENDED_HOSTING_CSP` in `middleware/puterSite.ts` is the suggested
|
|
* value.
|
|
*/
|
|
hosting_csp?: string;
|
|
/**
|
|
* Send `hosting_csp` as `Content-Security-Policy-Report-Only` instead of
|
|
* enforcing it. Cannot break a page, so it is the safe first step when
|
|
* introducing a policy.
|
|
*/
|
|
hosting_csp_report_only?: boolean;
|
|
/**
|
|
* 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;
|
|
/**
|
|
* Accept the pre-owner-binding signature format on signed file URLs
|
|
* alongside the current owner-bound one. Default true, so URLs minted
|
|
* before owner binding shipped keep working. Set false once those have aged
|
|
* out — the old format has no owner check, so a signed folder URL keeps
|
|
* authorizing reads after the folder moves to another owner.
|
|
*/
|
|
legacy_file_signatures?: boolean;
|
|
/** 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;
|
|
/** Mode changes on existing shares; defaults to ten times the above. */
|
|
share_remode_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;
|
|
|
|
/** Ms a live notification waits for sockets to acknowledge it; 5000. */
|
|
notificationAckTimeoutMs?: number;
|
|
|
|
/**
|
|
* How many `app.message` notifications one app may create for one user.
|
|
* Omit a field for the built-in default (30 per minute, 300 per day); a
|
|
* non-positive value removes that bound.
|
|
*/
|
|
notificationAppMessageLimits?: {
|
|
perMinute?: number;
|
|
perDay?: 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;
|
|
/** Puter-addressed user mailboxes and the optional local SMTP receiver. */
|
|
userEmail: IUserEmailConfig;
|
|
/** 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;
|
|
/** OpenAI Realtime WebSocket relay. */
|
|
realtime: {
|
|
/** Concurrent realtime sessions per user. Default 20. */
|
|
maxConnectionsPerUser?: number;
|
|
/**
|
|
* Upstream WebSocket URL. Default OpenAI's; point at a mock to
|
|
* load-test.
|
|
*/
|
|
upstreamUrl?: string;
|
|
};
|
|
/** 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 `util/userDetails.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;
|
|
/**
|
|
* How many full months past its own a monthly metering record is kept
|
|
* before it expires, on top of the month it belongs to. Unset defaults to
|
|
* 3; `0` keeps records forever.
|
|
*/
|
|
meteringRetentionMonths?: 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>;
|
|
};
|
|
|
|
/**
|
|
* The verified-factor requirement some routes declare
|
|
* (`requireAnyVerified`: a verified phone or card). `enabled: false` is the
|
|
* one switch that stops every declared gate enforcing without unpicking the
|
|
* declarations — for an SMS provider outage, say. Defaults to on; a
|
|
* deployment that can verify neither factor gates nothing regardless.
|
|
*/
|
|
verifiedFactorGate?: {
|
|
enabled?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Whether a user profile is public only while its owner is on a paid plan.
|
|
* `enabled: false` makes every profile public; a deployment with plan gates
|
|
* switched off (`meteringEnforcement.subscriptions`) already does. Defaults
|
|
* to on.
|
|
*/
|
|
profileGate?: {
|
|
enabled?: boolean;
|
|
};
|
|
|
|
/**
|
|
* 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;
|
|
};
|
|
|
|
/**
|
|
* Key-value sharing beyond watching.
|
|
*
|
|
* - `shareHandles` — whether a share handle may carry `read` or `write`
|
|
* access, and be used for key-value calls. Absent means off: those mints
|
|
* and upgrades reject with `kv_handles_disabled`. Watch-only handles stay
|
|
* under `events.kvHandles`, and revoking, unsharing and listing are never
|
|
* gated.
|
|
*/
|
|
kv?: {
|
|
shareHandles?: boolean;
|
|
};
|
|
|
|
/**
|
|
* 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 keys, declaration-merged from
|
|
* `@heyputer/backend/types`; see `IExtensionClientInstances` for the pattern.
|
|
*/
|
|
export interface IExtensionConfig {
|
|
[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;
|
|
/**
|
|
* When the drain starts, before `onServerPrepareShutdown`, which waits for
|
|
* every connection (upgraded sockets included) to close or the drain to
|
|
* end.
|
|
*/
|
|
onServerDrainStart?: (info?: ServerDrainInfo) => Promise<void> | void;
|
|
}
|
|
|
|
export interface ServerDrainInfo {
|
|
/** The longest the drain runs before teardown, when known. */
|
|
drainMs?: number;
|
|
}
|
|
|
|
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];
|
|
};
|