Files
puter/src/backend/types.ts
T
Daniel Salazar d8f9aff3dd fix: follow-ups to #4161–#4203 (rate limits, broadcast, AI routing and billing, fs subtree walks) (#4205)
* fix(rate-limit): count the newest KV rate rows first

The KV backend's rate check, peek and concurrent acquire list the key's
rows with a limit + 1 cap that the store applies before dropping expired
rows, so unswept rows from past windows (oldest keys) could fill the fetch
and the live count read as zero. They now list newest first.

* fix(broadcast): split webhook flushes under a byte budget, per-peer backlogs

Flushes go out in bodies of at most `broadcast.outboundMaxBodyBytes` (default
4 MiB) instead of one POST, and each peer keeps its own backlog of what it
missed, capped by count and four bodies' worth of bytes, oldest dropped first,
so one failing peer can no longer grow every peer's body past their 50 MB limit.

* fix(ai-chat): classify dropped upstream connections as provider failures

A connection reset or refusal (SDK APIConnectionError, Mistral's
ConnectionError, an undici reset on the cause) carries no status, so a chain
of them fell through to 500 internal_error. Attempts now record
connectionFailed; a chain of only resets and 5xx answers 400
upstream_provider_unavailable, and a reset counts as an upstream signal in a
mixed chain.

* fix(ai-chat): keep SDK retries on Anthropic count_tokens

The Claude client is built with SDK retries off so the driver can fall back,
but count_tokens has no fallback route. It now passes sdkRetryOptions, like
the OpenAI moderation call, so a transient 429 or reset is retried.

* fix(ai-chat): mark a route unhealthy for 60 s after a transient failure

With SDK retries off, one 5xx, 429, timeout or dropped connection kept a
route marked for 10 minutes, sending that model's traffic to resellers. Those
failures now mark the route for 60 s; bad credentials, an exhausted account
and other route-level failures keep 10 minutes.

* fix(metering): don't memoise spent addons inside the force-exact window

The spent-addons memo could cache a pre-grant record read from a lagging
replica right after a credits-changed announcement, so overage skipped the
new credit and requireCredits kept answering 402 for up to 15 s. A spent
record is now only cached once the actor's force-exact window has ended.

* fix(share): send the digest entries a flush claimed when another take fails

Digest entries are taken concurrently, and a single rejected take threw
away every entry the other takes had already removed from KV. The flush
now logs the failed takes and sends what it claimed; an entry whose take
failed stays queued for a later flush.

* fix(fs): announce an overwriting move's replaced entry only when it took its name

/move and v2 /fs/move looked up the entry at the destination name before
moving and always announced its removal. A move into someone else's Trash
renames instead of replacing, so the owner's desktop hid a live entry and
legacy /move reported it as `overwritten`. Both routes now pass the target
on only when the moved entry kept the name it was looked up by.

* fix(ai): name the vendor in upstream error fields from media providers

upstreamFetch now takes the provider id plus a display label, and
UpstreamResponseError carries the id. translateProviderError copies a
string `provider` off the error into `fields`, and the BytePlus video and
Cloudflare image errors set one, so upstream_auth_failed and
upstream_credits_exhausted alarms say which vendor failed again.

* fix(teams): delete a provisioned seat whose home folders could not be created

provisionUser logs a failed default-folder creation and still returns the
account, so provisionAccount went on to add the membership, issue a password,
mail the seat and emit team.account.created for an account with no home. It
now cascade-deletes the new user and returns 500 internal_error before any of
that.

* fix(ai-chat): load model catalogs without holding up server start

onServerStart awaited every gateway catalog (up to its 10s fetch timeout)
while HTTP was already listening, so drivers started after it had no
providers yet and chat requests found an empty model map. The first build
now runs in the background; complete(), models() and token counting wait
on it instead of failing.

* fix(ai-chat): serve direct models while gateway catalogs load at boot

The first model-map build now publishes a fresh map as each provider's
catalog lands, so static catalogs are routable at once. complete() and
token counting wait for the full first build only when the model, or the
provider asked for, isn't in the map yet. Refreshes still swap once.

* fix(ai-chat): split gateway-billed cost across input and output

A completion billed at the cost OpenRouter, Infron or Neuralwatt reports
was quoted to `ai.prompt.cost-calculated` as all input and no output. The
ledger still records one `billedUsage` line; the reported per-key costs now
spread that bill over the usage keys at the catalog's relative rates, in
whole µ¢ that sum to it. Neuralwatt energy units take no share.

* fix(ai-image): hold Replicate credits for the whole prediction

The output and input-surcharge holds took the default 10-minute TTL, the
same as the prediction window alone, so a slow prediction could outlive
them once creation and input measurement were added. They now last
15 minutes, like the video job holds.

* feat(ai-chat): report what a completion was charged as usage.cost

Chat usage (the non-stream result and the stream's final usage line) now
carries `cost`: the µ¢ the ledger records for the request after the cost
factor, from the request's recorded charges and the gate's factor lookup.
`usd_cents` is unchanged. A hooked recording now copies the usage object
first, so keys the driver adds later never land in the ledger.

* fix(ai): charge streamed speech once its first byte arrives

Per-character TTS and the voice changer recorded their charge when the
upstream's response headers arrived, so a body that failed before any audio
was billed in full. A shared relay now charges on the first byte, or when
the caller hangs up, and charges nothing for an upstream that fails or ends
before sending audio.

* fix(ai): release credit holds only after the request's usage lands

A hold was released as soon as its operation returned, while the usage it
stood for was still being written (behind the cost-factor lookup), so a
concurrent request briefly saw neither. AI metering now tracks the
request's usage writes and its holds (chat included) release once those
settle, without delaying the response; a streamed charge chains its own.
A failed write still releases the hold, and the hold's TTL backstops it.

* test(ai-chat): wait for a stream's credit hold to come back after its usage lands

* perf(fs): page subtree delete and copy by parent link, not by depth over the path range

Each page of a recursive delete or copy sorted the whole remaining subtree by a
computed depth, so a walk cost grew with the square of its size. It now pages
the children of up to 100 sibling directories at a time by parent_uid, depth
first, keyset on (parent_uid, id), so a page reads only its own rows; rows with
no stored path are reached too. SQLite gets the parent_uid index the others have.

* fix(fs): find children by parent_uid in the empty check and supplementary-name lookup

Rows the old backend wrote carry parent_uid and no parent_id, so a non-recursive
delete of a directory holding only such rows passed the empty check and orphaned
them, and on MySQL the supplementary-character child lookup missed them, leaving
their subtrees out of listings, counts and renames. Both now key on parent_uid;
the empty check still probes parent_id too, which the delete cascade follows.

* docs(fs): drop the single-POST note from the entry broadcast cap
2026-10-12 01:10:54 -07:00

1323 lines
52 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;
/**
* 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.
*/
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;
// -- 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;
/** 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;
};
/**
* 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;
}
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];
};