api #

convergent machine-state management: set up and ship to Linux machines from TypeScript

28 modules · 145 declarations

Modules
#

alpine_certbot_reload_nginx_hook
#

recipe_alpine_certbot.ts view source

(options?: CertbotReloadNginxHookOptions | undefined): RunPartsFile import {alpine_certbot_reload_nginx_hook} from '@fuzdev/zap/recipe_alpine_certbot.js';

Build the certbot deploy hook that reloads nginx via OpenRC after a renewal, ready to spread into a zap.file({...}).

rc-service --ifstarted exits 0 without running the command when the service isn't started, so the hook is a successful no-op while nginx is stopped (the first --standalone issuance). It is named by its full path, /sbin/rc-service, so the hook doesn't rest on the PATH certbot was started with (BusyBox crond's holds /sbin; a hand-run certbot's may not). A failing hook is reported by certbot and doesn't undo the renewal.

options?

optional

returns

RunPartsFile

a spreadable {path, mode, content} for an inline file resource.

throws

  • when - `options.name` holds a character outside `[A-Za-z0-9_-]`

alpine_certbot_renew_cron
#

recipe_alpine_certbot.ts view source

(): string import {alpine_certbot_renew_cron} from '@fuzdev/zap/recipe_alpine_certbot.js';

Build the daily certbot-renewal script for /etc/periodic/daily/certbot-renew.

BusyBox crond runs every executable in /etc/periodic/daily, so this stands in for Debian's certbot.timer. certbot renew no-ops when nothing is due. It reloads nothing itself: pair it with alpine_certbot_reload_nginx_hook, which certbot runs after each renewal.

returns

string

the script body (write it mode 755; the caller also needs a running crond).

alpine_certbot_standalone_issue
#

recipe_alpine_certbot.ts view source

(options?: CertbotStandaloneIssueOptions | undefined): CertbotScript import {alpine_certbot_standalone_issue} from '@fuzdev/zap/recipe_alpine_certbot.js';

Build the ssl-certificate bootstrap script for Alpine (fresh-host bootstrap only).

The check short-circuits once the cert exists; run passes the DNS preflight, stops nginx via OpenRC (best effort) so certbot certonly --standalone can bind port 80, and issues the first certificate. A refusal leaves nginx running.

options?

optional

returns

CertbotScript

the script check/run pair.

throws

  • when - `options.expected_addresses` is empty

alpine_nginx_catch_all
#

recipe_alpine_nginx.ts view source

(options?: NginxCatchAllOptions | undefined): NginxCatchAll import {alpine_nginx_catch_all} from '@fuzdev/zap/recipe_alpine_nginx.js';

Build the Alpine catch-all: the removal of the stock http.d/default.conf the nginx package ships, and the catch-all default_server file at /etc/nginx/http.d/00-catch-all.conf (the auto-included http.d/). See nginx_catch_all_core: order the file after the removal and after the SSL parameters, and give http2 and ssl as the site recipe has them.

options?

optional

returns

NginxCatchAll

the removal script payload and the catch-all file payload.

alpine_nginx_spa_site
#

recipe_alpine_nginx.ts view source

(options: NginxSpaSiteOptions): NginxSiteContent import {alpine_nginx_spa_site} from '@fuzdev/zap/recipe_alpine_nginx.js';

Build the full Alpine nginx site config for a single-page app.

Delegates to nginx_site_core with the SSL-snippet path SSL_PARAMS_PATH (outside the auto-included http.d/, see the module doc). The caller gives the result as the content of a file at /etc/nginx/http.d/<domain>.conf (no symlink — http.d/ is auto-included).

options

returns

NginxSiteContent

the site's template content, domain and port its vars, plus any redirect_domain_<i> (see nginx_site_core).

ALPINE_PERIODIC_DAILY
#

recipe_alpine_certbot.ts view source

"/etc/periodic/daily" import {ALPINE_PERIODIC_DAILY} from '@fuzdev/zap/recipe_alpine_certbot.js';

The BusyBox crond daily-jobs directory — every executable here runs once a day.

alpine_periodic_daily_file
#

recipe_alpine_certbot.ts view source

(options: AlpinePeriodicDailyFileOptions): RunPartsFile import {alpine_periodic_daily_file} from '@fuzdev/zap/recipe_alpine_certbot.js';

Build the path / mode / content for a BusyBox crond daily job, ready to spread into a zap.file({...}).

Encodes the /etc/periodic/daily convention and the executable 755 mode once, so neither is hand-typed at the callsite (getting the dir wrong — daily vs hourly, say — silently changes the schedule). The job only fires if a crond service is also declared and running. name is letters, digits, _ and - only: BusyBox run-parts skips any other name.

options

returns

RunPartsFile

a spreadable {path, mode, content} for an inline file resource.

throws

  • when - `name` holds a character outside `[A-Za-z0-9_-]`

alpine_postgres_setup
#

recipe_alpine_postgres.ts view source

(options: AlpinePostgresSetupOptions): AlpinePostgresSetupScript import {alpine_postgres_setup} from '@fuzdev/zap/recipe_alpine_postgres.js';

Build the postgres-cluster-init script's check/run for Alpine.

run initializes the data cluster via the OpenRC setup action (Alpine's apk install does not, unlike Debian's apt), then closes the trust rules that action's initdb writes into the new pg_hba.conf: local lines become peer (the OS account named like the role) and host lines scram-sha-256 (a password), so no account on the machine connects as any role, postgres included, without a credential. Debian's cluster starts with those methods. The rewrite runs as postgres, which owns the file and its directory (sed -i stages its copy there), and the script fails if a trust rule is left. A server already running is told to reload (rc-service --ifstarted), so the new rules are in force.

check passes once the cluster's PG_VERSION marker exists and its pg_hba.conf holds no trust rule. So a run that failed after setup is retried on the next apply, and that run skips setup (the cluster is there) and goes straight to the rewrite; a cluster set up before this recipe, trust rules and all, is rewritten the same way.

A client that relied on a trust rule stops connecting. Every trust line in the file is rewritten, not only initdb's: a service that reaches PostgreSQL over TCP with no password is refused once the rewrite is in force, and one on the socket is admitted only as the OS account named like its role. Move such a client to the socket as its own account (fuz_app_env_base's DATABASE_URL), or give it a password, before this runs on an existing cluster. Sequence it before the postgresql service (the setup action requires the server stopped) and before any zap.database.

options

returns

AlpinePostgresSetupScript

the script check/run pair.

alpine_ssh_service
#

recipe_alpine_ssh.ts view source

(options: SshServiceOptions): Required<Pick<ServiceResource, "service" | "enabled" | "running" | "reload_on">> import {alpine_ssh_service} from '@fuzdev/zap/recipe_alpine_ssh.js';

Build the sshd service payload for Alpine: OpenRC's sshd service, enabled, running, and reloaded when any of options.reload_on changes.

options

returns

Required<Pick<ServiceResource, "service" | "enabled" | "running" | "reload_on">>

the service payload.

alpine_web_firewall
#

recipe_alpine_firewall.ts view source

(options?: WebFirewallOptions | undefined): { firewall: Pick<FirewallResource, "allow" | "backend">; service: Pick<ServiceResource, "service" | ... 1 more ... | "running">; } import {alpine_web_firewall} from '@fuzdev/zap/recipe_alpine_firewall.js';

Build the nftables firewall payload and its companion persistence service for a web host (SSH + HTTP + HTTPS).

Spread each piece into its own resource, and make the service depend on the firewall so the ruleset file is written before the service loads it:

const fw = alpine_web_firewall(); zap.firewall({id: 'firewall', ...fw.firewall}); zap.service({id: 'nftables', depends_on: ['firewall'], ...fw.service});

options?

optional

returns

{ firewall: Pick<FirewallResource, "allow" | "backend">; service: Pick<ServiceResource, "service" | "enabled" | "running">; }

{firewall, service} — the payloads for a firewall and a service resource.

AlpinePeriodicDailyFileOptions
#

recipe_alpine_certbot.ts view source

AlpinePeriodicDailyFileOptions import type {AlpinePeriodicDailyFileOptions} from '@fuzdev/zap/recipe_alpine_certbot.js';

name

The job's file name under /etc/periodic/daily (e.g. 'certbot-renew').

type string

body

The script body — a #!/bin/sh executable (e.g. alpine_certbot_renew_cron()).

type string

AlpinePostgresSetupOptions
#

recipe_alpine_postgres.ts view source

AlpinePostgresSetupOptions import type {AlpinePostgresSetupOptions} from '@fuzdev/zap/recipe_alpine_postgres.js';

version

The installed PostgreSQL major version — matches apk add postgresql<version> and the /var/lib/postgresql/<version>/data cluster path. Pin it to the major your other hosts run, for replication and restore compatibility.

type number | string

AlpinePostgresSetupScript
#

recipe_alpine_postgres.ts view source

AlpinePostgresSetupScript import type {AlpinePostgresSetupScript} from '@fuzdev/zap/recipe_alpine_postgres.js';

The check/run pair alpine_postgres_setup returns (caller supplies id/name/depends_on).

check

Check command — exit 0 means the script is already applied (skip).

type string

run

The command the planner emits when the check fails.

type string

AptRepoResource
#

schema.ts view source

AptRepoResource import type {AptRepoResource} from '@fuzdev/zap/schema.js';

apt_repo resource payload — a GPG key + sources list.

key_url

The URL zap fetches the GPG signing key from, on the operator's machine: https://, or http:// to a loopback host; any other scheme is rejected by the structural gate. It may carry a reference. The key is pinned by the blake3 of its bytes, and only bytes that hash to the pin are installed.

type string

key_blake3?

The key's vendored pin: the blake3 of its bytes, 64 lowercase hex. Absent, the pin is tracked in zap.lock (zap lock / zap update mint and advance it).

type string

key_path?

Where the GPG key is stored; default /usr/share/keyrings/{id}.asc. It takes no reference (rejected when the config is read): it is written into the source line, inside double quotes.

type string

source

The deb source line (signed-by is auto-injected from key_path). It is expanded by the target shell inside double quotes, so $VERSION_CODENAME resolves there; it takes no reference (rejected when the config is read).

type string

list_file?

The .list file path; default /etc/apt/sources.list.d/{id}.list. It takes no reference (rejected when the config is read): detection reads it as declared.

type string

BuildResource
#

schema.ts view source

BuildResource import type {BuildResource} from '@fuzdev/zap/schema.js';

build resource payload — fetch a verified source, build it, install to output.

source

Where the source comes from + how its integrity is verified.

type BuildSource

run

The build command (e.g. zig build -p $HOME/.local -Doptimize=ReleaseFast).

type string

output

Path that exists iff the build is installed — the detection probe.

type string

cwd?

Working directory for run: absolute, or relative to the extracted source dir, which is the default. Takes no reference: zap rejects one here.

type string

env?

Build environment. Keys are POSIX names ([A-Za-z_][A-Za-z0-9_]*), validated at config-eval; a name beginning with __zap_ is rejected by the structural gate (zap's own wrapper variables). A value's references resolve before it is quoted, so it can hold any text but a NUL and the reference delimiters U+FDD0 and U+FDD1.

type Record<string, string>

BuildSource
#

schema.ts view source

BuildSource

type { type: "remote"; } & RemoteSource

import type {BuildSource} from '@fuzdev/zap/schema.js';

The source a build fetches + verifies. Internally tagged on type (only the remote-tarball flavor exists). Mirrors the Rust BuildSource.

type

A fetched source archive — fetched, verified, and content-addressed.

type "remote"

url

Fetch location: an https:// URL with no user:password@ (http:// only for a loopback host), or the structural gate rejects it. It may carry a reference, resolved when zap fetches (the lock addresses a secret reference by its token, any other by its value).

type string

extract?

How the fetched source is unpacked before run.

type ExtractMode

auth?

Optional fetch credential for a private byte-host (never hashed).

type RemoteAuth

verify?

Optional upstream-signature verification, run when a fetch fills the content cache, before the bytes are pinned or built. A cache hit is not re-verified: it is re-hashed against the pin.

type SourceVerify

blake3?

Vendored source pin. A declared pin self-pins; absent, the lock pins it. Mirrors ContentHash — exactly 64 lowercase hex on the wire.

type string

certbot_deploy_hook_core
#

recipe_certbot.ts view source

(command: string, name: string): RunPartsFile import {certbot_deploy_hook_core} from '@fuzdev/zap/recipe_certbot.js';

Build the path / mode / content of a certbot deploy hook, ready to spread into a zap.file({...}): an executable #!/bin/sh script at CERTBOT_DEPLOY_HOOKS_DIR/<name> that execs command.

A hook file rather than a --deploy-hook flag, so the renewal schedule (certbot.timer, a cron job) needs no arguments and any renewal, scheduled or run by hand, picks it up. On a certbot that also runs deploy hooks after certonly, the hook fires during the first --standalone issuance, while nginx is stopped, so command must succeed as a no-op when the service isn't running; the distro wrappers' commands do. A failing hook is reported by certbot but doesn't undo the renewal.

command

the command the hook runs, as shell words by design (the hook is a shell script); the caller's own literal, never a value from elsewhere

type string

name

the hook's file name: letters, digits, _ and - only, so run-parts-style runners don't skip it and it can't leave the directory

type string

returns

RunPartsFile

a spreadable {path, mode, content} for an inline file resource.

throws

  • when - `name` holds any other character

CERTBOT_DEPLOY_HOOKS_DIR
#

recipe_certbot.ts view source

"/etc/letsencrypt/renewal-hooks/deploy" import {CERTBOT_DEPLOY_HOOKS_DIR} from '@fuzdev/zap/recipe_certbot.js';

The directory certbot runs deploy hooks from: every executable here runs after a certificate is renewed (with certbot renew, the command both distros schedule). Since certbot 5.3.0 they also run after certonly gets a new certificate; certbot 4.0.0, which Debian 13 and Alpine 3.22 ship, runs them for renew only, so certbot_webroot_switch runs its hook itself.

certbot_standalone_issue_core
#

recipe_certbot.ts view source

(stop_command: string, options?: CertbotStandaloneIssueOptions | undefined): CertbotScript import {certbot_standalone_issue_core} from '@fuzdev/zap/recipe_certbot.js';

The neutral core of the ssl-certificate bootstrap scripts ({debian,alpine}_certbot_standalone_issue), which differ only in stop_command.

Issues the first certificate via certbot certonly --standalone. The run passes the DNS preflight, then stops nginx with stop_command (best effort, || true) to free port 80, then runs certbot; the stop and the request take their names from the one options, and a refusal stops nothing. The check short-circuits once the cert exists.

stop_command

the service manager's stop command, as shell words by design; the caller's own literal, never a value from elsewhere

type string

options?

the certificate's names, its registration email, and the addresses the names must resolve to

optional

returns

CertbotScript

the script check/run pair.

throws

  • when - `options.expected_addresses` is empty

certbot_webroot_switch
#

recipe_certbot.ts view source

(options: CertbotWebrootSwitchOptions): CertbotScript import {certbot_webroot_switch} from '@fuzdev/zap/recipe_certbot.js';

Build the certbot-renewal-webroot script.

Re-issues once via --webroot to pin the unattended renewal authenticator to webroot, then runs deploy_hook; the check short-circuits once the renewal config records webroot (and the live cert covers each of extra_domains). The re-issue is a CA request, for every name the certificate covers, so the run passes the DNS preflight first.

options

returns

CertbotScript

the script check/run pair.

throws

  • when - `options.expected_addresses` is empty

CertbotDnsPreflightOptions
#

recipe_certbot.ts view source

CertbotDnsPreflightOptions import type {CertbotDnsPreflightOptions} from '@fuzdev/zap/recipe_certbot.js';

The DNS preflight's option, shared by every certbot script that leads to a CA request.

expected_addresses?

The IP addresses every name the certificate covers must resolve to, on the target. Before the script changes anything, the target resolves each name with getent ahosts and getent hosts — through its own resolver, so /etc/hosts first — and refuses when a name doesn't resolve, when no address can be read, or when any A/AAAA address either prints is outside this set: Let's Encrypt validates whichever address it picks, so an undeclared AAAA would fail the validation. The two lookups together see every A and AAAA but in one case: on a musl host (Alpine) with IPv6 disabled, ahosts drops the AAAA records and hosts prints only the first, so a name with several AAAA, the first of them declared, passes unseen.

The guard reads the target's resolver, not the CA's. An /etc/hosts line, a split-horizon answer, or a cached one can pass while public DNS doesn't point at the host yet, or refuse while it does. A name /etc/hosts maps to a loopback address refuses, the safe direction. That includes the 127.0.1.1 <name> line a Debian install writes for the host's own name, when that name is the domain: remove the line, or give the host a name that isn't the domain (a tool that manages /etc/hosts may write the line back).

Each value is an IP literal, or a reference resolving to one at apply time; the target checks it and refuses anything else (a host name, say), asking for this option. An IPv6 address is compared as text, lowercased, so write it as getent prints it (the compressed form). Defaults to a reference to DEPLOY_HOST, the target's host; declare the addresses when the host is reached by name or the names also carry an AAAA.

type readonly string[]

CertbotReloadNginxHookOptions
#

recipe_certbot.ts view source

CertbotReloadNginxHookOptions import type {CertbotReloadNginxHookOptions} from '@fuzdev/zap/recipe_certbot.js';

Options for the distro *_certbot_reload_nginx_hook wrappers — neutral; the wrappers differ in the service-manager reload command.

name?

The hook's file name under CERTBOT_DEPLOY_HOOKS_DIR. Defaults to RELOAD_NGINX_HOOK_NAME ('reload-nginx').

type string

CertbotScript
#

recipe_certbot.ts view source

CertbotScript import type {CertbotScript} from '@fuzdev/zap/recipe_certbot.js';

The check/run pair the certbot recipes return (caller supplies id/name/depends_on).

check

Check command — exit 0 means the script is already applied (skip).

type string

run

The command the planner emits when the check fails.

type string

CertbotStandaloneIssueOptions
#

recipe_certbot.ts view source

CertbotStandaloneIssueOptions import type {CertbotStandaloneIssueOptions} from '@fuzdev/zap/recipe_certbot.js';

Options for certbot_standalone_issue_core and its distro wrappers ({debian,alpine}_certbot_standalone_issue) — neutral; the wrappers differ in the service-manager stop command.

inheritance

domain?

The certificate domain. Defaults to a reference to DEPLOY_DOMAIN.

type string

extra_domains?

Further names the certificate covers beside domain, one -d each — e.g. the www. name a site's redirect_domains 301s to domain. The cert stays named for domain. Defaults to none.

type readonly string[]

email?

The registration email. Defaults to a reference to DEPLOY_CERTBOT_EMAIL.

type string

CertbotWebrootSwitchOptions
#

recipe_certbot.ts view source

CertbotWebrootSwitchOptions import type {CertbotWebrootSwitchOptions} from '@fuzdev/zap/recipe_certbot.js';

inheritance

domain?

The certificate domain. Defaults to a reference to DEPLOY_DOMAIN.

type string

extra_domains?

Further names the certificate covers beside domain (see CertbotStandaloneIssueOptions.extra_domains). When set, the check also requires each name in the live cert's subjectAltName, so adding a name to a host whose cert predates it re-issues the cert to cover it. Defaults to none.

type readonly string[]

email?

The registration email. Defaults to a reference to DEPLOY_CERTBOT_EMAIL.

type string

webroot?

The ACME challenge webroot. Defaults to /var/www/certbot.

type string

deploy_hook

The deploy hook the run executes after certonly succeeds — the file a *_certbot_reload_nginx_hook returns, or any with its path — so the server loads the re-issued certificate: certbot before 5.3.0 runs deploy hooks for renew only. On a later certbot the hook runs twice, which a reload tolerates. The switch's script must depend on the hook's file.

type Pick<RunPartsFile, "path">

CommandAssertion
#

schema.ts view source

CommandAssertion import type {CommandAssertion} from '@fuzdev/zap/schema.js';

The payload of a command VerifyAssertion. Mirrors CommandAssertion.

run

The command to run.

type string

expect?

A substring that must appear in stdout.

type string

DatabaseAuth
#

schema.ts view source

DatabaseAuth import type {DatabaseAuth} from '@fuzdev/zap/schema.js';

PostgreSQL auth config for a database resource's pg_hba.conf rules: one host line per entry of hosts. A database without one declares no rule, so the role is reached over the Unix socket by the OS account of the same name (the stock peer rule). Mirrors the Rust DatabaseAuth.

method?

Auth method (default scram-sha-256).

type PgAuthMethod

hosts

Allowed client addresses, each an IP address with its prefix length (127.0.0.1/32); a keyword (all, samehost, samenet) or a host name is refused. At least one: an auth that names none is refused, since it would write no rule.

type string[]

DatabaseEngine
#

schema.ts view source

"postgresql" import type {DatabaseEngine} from '@fuzdev/zap/schema.js';

Database engine for a database resource (only postgresql).

DatabaseResource
#

schema.ts view source

DatabaseResource import type {DatabaseResource} from '@fuzdev/zap/schema.js';

database resource payload (PostgreSQL only).

engine

Database engine (only postgresql).

type "postgresql"

database_name

Database name to create. Taken literally (never shell-expanded); a reference resolves on the operator's machine.

type string

username

Role/user to create and own the database. Taken literally (never shell-expanded); zap.env('USER') declares "the operator's OS user".

type string

auth?

pg_hba.conf auth config. Absent, zap writes no rule, and the role is reached over the Unix socket by the OS account of the same name. A rule zap adds is put in force with pg_reload_conf(); the server isn't restarted, and the target needs no service_manager for it.

type DatabaseAuth

debian_certbot_reload_nginx_hook
#

recipe_debian_certbot.ts view source

(options?: CertbotReloadNginxHookOptions | undefined): RunPartsFile import {debian_certbot_reload_nginx_hook} from '@fuzdev/zap/recipe_debian_certbot.js';

Build the certbot deploy hook that reloads nginx via systemd after a renewal, ready to spread into a zap.file({...}).

systemctl try-reload-or-restart acts only on a running unit, so the hook is a successful no-op while nginx is stopped (the first --standalone issuance).

options?

optional

returns

RunPartsFile

a spreadable {path, mode, content} for an inline file resource.

throws

  • when - `options.name` holds a character outside `[A-Za-z0-9_-]`

debian_certbot_standalone_issue
#

recipe_debian_certbot.ts view source

(options?: CertbotStandaloneIssueOptions | undefined): CertbotScript import {debian_certbot_standalone_issue} from '@fuzdev/zap/recipe_debian_certbot.js';

Build the ssl-certificate bootstrap script for Debian (fresh-host bootstrap only).

The check short-circuits once the cert exists; run passes the DNS preflight, stops nginx via systemd (best effort) so certbot certonly --standalone can bind port 80, and issues the first certificate. A refusal leaves nginx running.

options?

optional

returns

CertbotScript

the script check/run pair.

throws

  • when - `options.expected_addresses` is empty

debian_nginx_catch_all
#

recipe_debian_nginx.ts view source

(options?: NginxCatchAllOptions | undefined): NginxCatchAll import {debian_nginx_catch_all} from '@fuzdev/zap/recipe_debian_nginx.js';

Build the Debian catch-all: the removal of the stock sites-enabled/default symlink, and the catch-all default_server file at /etc/nginx/conf.d/00-catch-all.conf (conf.d/, which Debian's nginx.conf includes in its http block). See nginx_catch_all_core: order the file after the removal and after the SSL parameters, and give http2 and ssl as the site recipe has them.

options?

optional

returns

NginxCatchAll

the removal script payload and the catch-all file payload.

debian_nginx_spa_site
#

recipe_debian_nginx.ts view source

(options: NginxSpaSiteOptions): NginxSiteContent import {debian_nginx_spa_site} from '@fuzdev/zap/recipe_debian_nginx.js';

Build the full Debian nginx site config for a single-page app.

Delegates to nginx_site_core with the SSL-snippet path SSL_PARAMS_PATH. The caller gives the result as the content of a file at /etc/nginx/sites-available/<domain> and symlinks it into sites-enabled.

options

returns

NginxSiteContent

the site's template content, domain and port its vars, plus any redirect_domain_<i> (see nginx_site_core).

debian_ssh_service
#

recipe_debian_ssh.ts view source

(options: SshServiceOptions): Required<Pick<ServiceResource, "service" | "enabled" | "running" | "reload_on">> import {debian_ssh_service} from '@fuzdev/zap/recipe_debian_ssh.js';

Build the sshd service payload for Debian: systemd's ssh unit, enabled, running, and reloaded when any of options.reload_on changes.

It targets ssh.service, which Debian runs by default. On Ubuntu 22.10 and later, where ssh.socket starts it, ssh.service is the daemon that takes the socket from systemd, so enabling it only starts sshd at boot as well. The one conflict is a hand-enabled Accept=yes socket on Debian (ssh@.service per connection), which contends with ssh.service for port 22: disable that socket first.

options

returns

Required<Pick<ServiceResource, "service" | "enabled" | "running" | "reload_on">>

the service payload.

debian_web_firewall
#

recipe_debian_firewall.ts view source

(options?: WebFirewallOptions | undefined): Pick<FirewallResource, "allow" | "backend"> import {debian_web_firewall} from '@fuzdev/zap/recipe_debian_firewall.js';

Build the ufw firewall payload for a web host (SSH + HTTP + HTTPS).

Spread into a zap.firewall({id, ...debian_web_firewall()}). Leaves default_incoming / default_outgoing at the schema defaults (deny in, allow out).

options?

optional

returns

Pick<FirewallResource, "backend" | "allow">

the backend + allow for a ufw firewall resource.

deno_install
#

recipe_deno.ts view source

(options: DenoInstallOptions): DenoScript import {deno_install} from '@fuzdev/zap/recipe_deno.js';

Build the check/run for a pinned-Deno install script resource.

check passes once the pinned version is present at bin_path (so a re-apply is a no-op until the version bumps); run downloads the pinned release zip, extracts it, and installs the binary to bin_path per the destination mode (user / privileged / same-user).

options

returns

DenoScript

the script check/run pair.

throws

  • Error - if `bin_path` isn't absolute, `user` and `privileged` are both given, or `user` is empty, starts with `#`, or is a reference

DenoInstallOptions
#

recipe_deno.ts view source

DenoInstallOptions import type {DenoInstallOptions} from '@fuzdev/zap/recipe_deno.js';

Options for deno_install.

version

Pinned Deno version, e.g. '2.8.2' (no leading v).

type string

bin_path

Absolute path the deno executable is installed to — e.g. '/usr/local/bin/deno' (system) or '/home/svc/.deno/bin/deno' (user-home). Parent dirs are created (install -D). Passed single-quoted, so it is taken literally: no ~ or $HOME expansion. Refused at eval unless it begins /.

type string

user?

Install as this user via sudo -u <user>, so bin_path (under their home) lands owned by them even when the apply runs as root. The check and the closing --version run the binary as this user too, never as the connecting user: the account owns the file, and could replace it. A user name written out: not empty, not #<uid>, and not a reference. Omit for a same-user or privileged system install. Mutually exclusive with privileged.

type string

privileged?

sudo the install into a root-owned system dir (e.g. /usr/local/bin). Defaults to false. Mutually exclusive with user.

type boolean

target?

Release-asset target triple. Defaults to 'x86_64-unknown-linux-gnu' (glibc). On an Alpine/musl host pass 'x86_64-unknown-linux-musl' — a glibc Deno binary won't run under musl without gcompat (this recipe is neutral, so the distro is the caller's to declare).

type string

DenoScript
#

recipe_deno.ts view source

DenoScript import type {DenoScript} from '@fuzdev/zap/recipe_deno.js';

The check/run pair deno_install returns (caller supplies id/name/depends_on).

check

Check command — exit 0 means the script is already applied (skip).

type string

run

The command the planner emits when the check fails.

type string

DeployHealth
#

schema.ts view source

DeployHealth import type {DeployHealth} from '@fuzdev/zap/schema.js';

Post-restart HTTP health check for a deploy. Mirrors the Rust DeployHealth.

domain lives inside this block, so declaring a health gate with nothing to poll is unrepresentable rather than a mistake caught at run time. Omit the whole block to deploy without a health gate.

domain

Host polled over HTTPS (e.g. app.example.com, or a zap.env('DEPLOY_DOMAIN') reference resolved at execution).

type string

path

Request path polled over HTTPS against domain, beginning with / (e.g. /health). Takes no reference: zap rejects one here.

type string

attempts?

How many times to poll before failing the deploy, at least 1 (default 15).

type number

interval_ms?

Delay between poll attempts, in milliseconds (default 2000).

type number

DeploySpec
#

schema.ts view source

DeploySpec import type {DeploySpec} from '@fuzdev/zap/schema.js';

The deploy() release-management block — what zap deploy packages and ships. Mirrors the Rust DeploySpec.

build?

Build command run locally before packaging (e.g. gro build).

type string

artifacts

Files/directories (relative to the operator cwd) to package into the tarball. An entry can't begin with - (write ./-name) or with @; either is rejected by the structural gate.

type string[]

path

Remote application root — holds releases/, shared/, and the current symlink. An absolute path, written out: no reference (rejected when the config is read) and no ~ (rejected by the structural gate).

type string

releases?

Number of releases to retain on the target, the live one included (default 5). Below 2 is rejected by the structural gate: the release just deployed and the one it replaced, which a failed deploy rolls back to, are always kept.

type number

service?

Service to restart after the symlink swap. Requires target.service_manager. Takes no reference (rejected when the config is read).

type string

shared?

Files under the remote shared/ directory to symlink into each release root. Each is a relative name of a file: no .. component, no control character, not ending in / or in a . component (anything else is rejected by the structural gate).

type string[]

prepare?

Post-extract commands run on the target, in the new release directory, before swap, each in a shell of its own (sh -ec): an exit in one ends that command, not the deploy, and one that fails stops the deploy before the swap. The release is still under its hidden staging name then, so a command shouldn't record its absolute path.

type string[]

health?

Optional post-restart HTTP health check.

type DeployHealth

DirectoryResource
#

schema.ts view source

DirectoryResource import type {DirectoryResource} from '@fuzdev/zap/schema.js';

directory resource payload — a directory with managed permissions.

path

Directory path, absolute or under ~ (may carry a reference).

type string

mode?

Desired octal mode (e.g. "755"). Mirrors Mode — octal, 7777 at most.

type string

owner?

Desired owner. Mirrors AccountName — a portable Unix name ([a-z_][a-z0-9_-]*, optional trailing $; anything else is rejected at parse).

type string

group?

Desired group. Mirrors AccountName — a portable Unix name ([a-z_][a-z0-9_-]*, optional trailing $; anything else is rejected at parse).

type string

env_ref
#

env_ref.ts view source

(name: string): string import {env_ref} from '@fuzdev/zap/env_ref.js';

A reference to the operator environment variable name, as a string a recipe places in a field that takes a reference — whole, interpolated, or concatenated.

name

the variable's name: a letter or _, then letters, digits, or _

type string

returns

string

the marked string zap.env(name) returns

throws

  • Error - if `name` is not an environment variable's name

ExtractMode
#

schema.ts view source

ExtractMode

type { type: "none"; } | { type: "tar.xz"; strip_components?: number | undefined; } | { type: "tar.gz"; strip_components?: number | undefined; }

import type {ExtractMode} from '@fuzdev/zap/schema.js';

How a fetched remote file / build source is materialized after fetch. Internally tagged on type. Mirrors the Rust ExtractMode.

fail2ban_jail
#

recipe_fail2ban.ts view source

(options?: Fail2banJailOptions | undefined): string import {fail2ban_jail} from '@fuzdev/zap/recipe_fail2ban.js';

Build the fail2ban jail.local text for options.

Writes a [DEFAULT] section with the global ban/find/retry tunables and the banaction, and a stricter [sshd] jail reading the journal, or sshd_logpath when one is given. jail.local is read after the distro's jail.d/*.conf, so its values win over a packaged default, such as Debian's nftables banaction or Alpine's alpine-sshd filter.

options?

optional

returns

string

the complete jail.local text, terminated by a trailing newline.

Fail2banJailOptions
#

recipe_fail2ban.ts view source

Fail2banJailOptions import type {Fail2banJailOptions} from '@fuzdev/zap/recipe_fail2ban.js';

Options for fail2ban_jail.

Every field defaults to the shared baseline, so fail2ban_jail() takes no argument for the common case: a Debian or Ubuntu host whose sshd logs to the journal and whose firewall is ufw (iptables).

ban_time?

[DEFAULT] bantime — how long a banned host stays banned. Defaults to '1h'.

type string

find_time?

[DEFAULT] findtime — the window failures are counted within. Defaults to '10m'.

type string

max_retry?

[DEFAULT] maxretry — failures within find_time before a ban. Defaults to 5.

type number

sshd_max_retry?

[sshd] maxretry — sshd-specific failure threshold. Defaults to 3.

type number

sshd_ban_time?

[sshd] bantime — sshd-specific ban duration. Defaults to '24h'.

type string

banaction?

[DEFAULT] banaction — the fail2ban action that blocks a banned address, which has to drive the firewall the host has. Defaults to 'iptables-multiport', for a ufw host (ufw depends on iptables, not on the nft command Debian's own nftables default needs). Pass 'nftables' on an nftables host such as the one alpine_web_firewall sets up, which has no iptables. Without the command the action names, fail2ban counts and lists a ban but blocks nothing.

type string

sshd_logpath?

The file fail2ban watches for sshd auth failures. Unset, the jail reads the systemd journal (backend = systemd), where sshd logs on Debian and Ubuntu with or without rsyslog. On Alpine there is no journal: BusyBox syslog writes everything to '/var/log/messages', so pass that (and ensure the syslog service is enabled); the jail then reads the file with the sshd filter set to read BusyBox's lines. fail2ban refuses to start a jail whose logpath doesn't exist ("Have not found any log file").

type string

FileContent
#

schema.ts view source

FileContent

type { type: "inline"; content: string; } | TemplateContent | { type: "source"; path: string; } | ({ type: "remote"; } & RemoteContent)

import type {FileContent} from '@fuzdev/zap/schema.js';

The content field of a file resource. Internally tagged on type. Mirrors the Rust FileContent.

FileResource
#

schema.ts view source

FileResource import type {FileResource} from '@fuzdev/zap/schema.js';

file resource payload — a file with managed content.

path

Target path (may carry a reference / ~).

type string

content

Content source.

type FileContent

mode?

Desired octal mode (e.g. "600"). Mirrors Mode — octal, 7777 at most.

type string

owner?

Desired owner. Mirrors AccountName — a portable Unix name ([a-z_][a-z0-9_-]*, optional trailing $; anything else is rejected at parse).

type string

group?

Desired group. Mirrors AccountName — a portable Unix name ([a-z_][a-z0-9_-]*, optional trailing $; anything else is rejected at parse).

type string

triggers?

Ids of service resources notified when this file changes.

type string[]

validate?

Command that must exit 0 after a write (e.g. NGINX_VALIDATE), or the write is rolled back — the previous bytes, owner, group, and mode restored, less any set-id or sticky bit the declared mode lacks (or a new file removed) — and the change fails. Runs against the file in place, so it suits a file some larger config includes. Text content only — rejected on remote.

type string

FileType
#

schema.ts view source

FileType

type "file" | "directory"

import type {FileType} from '@fuzdev/zap/schema.js';

The required type of a path_exists verify assertion's target. Mirrors FileType.

FirewallAllowRule
#

schema.ts view source

FirewallAllowRule

type string | { port: number; proto?: Protocol | undefined; }

import type {FirewallAllowRule} from '@fuzdev/zap/schema.js';

A firewall allow rule: a named ufw application profile (e.g. OpenSSH, ufw only — rejected under the nftables backend), or a port/proto pair (both backends). A named profile takes no reference (rejected when the config is read: detection reads it as declared) and can't begin with - (rejected by the structural gate). Untagged. Mirrors the Rust FirewallAllowRule.

FirewallBackend
#

schema.ts view source

FirewallBackend

type "ufw" | "nftables"

import type {FirewallBackend} from '@fuzdev/zap/schema.js';

Firewall backend for a firewall resource: ufw (Debian/Ubuntu — app profiles + port rules) or nftables (the Alpine-native declarative inet filter backend — port/proto rules only, no application profiles). Mirrors the Rust FirewallBackend.

FirewallPolicy
#

schema.ts view source

FirewallPolicy

type "allow" | "deny" | "reject"

import type {FirewallPolicy} from '@fuzdev/zap/schema.js';

A firewall default-policy verdict / allow disposition, on either backend. Mirrors FirewallPolicy.

FirewallResource
#

schema.ts view source

FirewallResource import type {FirewallResource} from '@fuzdev/zap/schema.js';

firewall resource payload — host firewall configuration (ufw or nftables).

backend

Firewall backend (ufw or nftables).

type FirewallBackend

default_incoming?

Default policy for incoming traffic (default deny).

type FirewallPolicy

default_outgoing?

Default policy for outgoing traffic (default allow).

type FirewallPolicy

allow

Allow rules (named profiles or port/proto pairs).

type FirewallAllowRule[]

fuz_app_bootstrap_token
#

recipe_bootstrap.ts view source

(options: FuzAppBootstrapTokenOptions): FuzAppBootstrapTokenScript import {fuz_app_bootstrap_token} from '@fuzdev/zap/recipe_bootstrap.js';

Build the check/run for a fuz_app bootstrap-token script.

check passes when the token path is a non-empty regular file (not a symlink to one) or the bootstrap_lock row already marks the server bootstrapped; run mints a token of 32 random bytes as 64 lowercase hex characters at <config_dir>/secret_bootstrap_token (mode 600, owned by user), ending in one LF, which fuz_app's reader strips.

options

returns

FuzAppBootstrapTokenScript

the script check/run payload, ready to spread.

throws

  • Error - if `user` is empty, starts with `#`, or is a reference, or `config_dir` isn't absolute

fuz_app_env_base
#

recipe_env.ts view source

(options: FuzAppEnvBaseOptions): TemplateContent import {fuz_app_env_base} from '@fuzdev/zap/recipe_env.js';

Build a fuz_app server's .env.production content: the shared header, then the caller's extra lines.

The header emits NODE_ENV/PORT/HOST, the DATABASE_URL over the PostgreSQL Unix socket in /run/postgresql (where Debian and Alpine both put it), SECRET_FUZ_COOKIE_KEYS, and FUZ_BOOTSTRAP_TOKEN_PATH — the section identical across fuz_app server deploys. {{port}} and {{cookie_keys}} are placeholders whose values are port and cookie_keys, held to port and env_value.

options

returns

TemplateContent

the template content: the header (ending in a newline) and extra's lines, the variables of both, and their kinds.

throws

  • when - `user`, `service`, or `config_dir` can't be written as they must be, or `extra` names a variable of the base's

fuz_app_trusted_proxies
#

recipe_env.ts view source

(options: FuzAppTrustedProxiesOptions): string import {fuz_app_trusted_proxies} from '@fuzdev/zap/recipe_env.js';

Build the FUZ_<infix>_TRUSTED_PROXIES env stanza (comment + var line).

The comment is the load-bearing rationale: without the var, the backend attributes every request to 127.0.0.1 (nginx's loopback address), so per-IP rate limits collapse into one shared bucket and audit_log.ip is useless.

options

returns

string

the stanza text, terminated by a trailing newline.

FuzAppBootstrapTokenOptions
#

recipe_bootstrap.ts view source

FuzAppBootstrapTokenOptions import type {FuzAppBootstrapTokenOptions} from '@fuzdev/zap/recipe_bootstrap.js';

user

The service user that owns config_dir and writes the token file. run runs as them (sudo -u <user>), so the connecting user's sudo rule must allow running as them, even when it is the same user. A user name written out: not empty, not #<uid>, and not a reference.

type string

service

Database name probed by the bootstrap_lock fallback check (the psql_args default is -d <service>).

type string

config_dir

Absolute directory holding secret_bootstrap_token, e.g. /home/<user>/.myapp. The token path is <config_dir>/secret_bootstrap_token. Must already exist and be writable by user (declare the config-dir directory resource, owned by user, as a depends_on).

type string

psql_args?

psql connection args for the bootstrap_lock fallback probe. Defaults to -d <service>: the Unix socket, as the connecting user, whom the stock peer rule admits when the user is the role (the deploy user running the service, as fuz_app_env_base's DATABASE_URL assumes). These are shell words, spliced into the check as written: quote a value in them yourself. Every other option is passed as one single-quoted word.

type string

FuzAppBootstrapTokenScript
#

recipe_bootstrap.ts view source

FuzAppBootstrapTokenScript import type {FuzAppBootstrapTokenScript} from '@fuzdev/zap/recipe_bootstrap.js';

The check/run fuz_app_bootstrap_token returns (caller supplies id/name/depends_on).

check

Check command — exit 0 means the script is already applied (skip).

type string

run

The command the planner emits when the check fails.

type string

FuzAppEnvBaseOptions
#

recipe_env.ts view source

FuzAppEnvBaseOptions import type {FuzAppEnvBaseOptions} from '@fuzdev/zap/recipe_env.js';

Options for fuz_app_env_base.

user

The DB role in DATABASE_URL. The server connects over the Unix socket, where the stock peer rule admits only the OS account of the same name, so this is the account the service runs as. Letters, digits, _, ., and -, written out (no reference): it is a name in DATABASE_URL, refused at eval otherwise.

type string

service

The database name in DATABASE_URL. Letters, digits, _, ., and -, written out (no reference), refused at eval otherwise.

type string

config_dir

Directory holding secret_bootstrap_token, e.g. /home/<user>/.myapp. The emitted FUZ_BOOTSTRAP_TOKEN_PATH is <config_dir>/secret_bootstrap_token.

type string

port

The port the server listens on, {{port}}'s value (PORT). Held to port.

type string | number

cookie_keys

The cookie-signing keys, {{cookie_keys}}'s value (SECRET_FUZ_COOKIE_KEYS) — a reference, zap.env('SECRET_FUZ_COOKIE_KEYS'). Held to env_value: it sits inside single quotes.

type string

extra?

The caller's own lines, appended after the base, with their variables.

type FuzAppEnvExtra

FuzAppEnvExtra
#

recipe_env.ts view source

FuzAppEnvExtra import type {FuzAppEnvExtra} from '@fuzdev/zap/recipe_env.js';

A caller's own env-file lines, appended to fuz_app_env_base's: their text, the values of the placeholders they add, and those values' kinds.

The lines may use the base's {{port}} and {{cookie_keys}}, but vars and kinds can't name them: the base's values and kinds are its own. Used whole, the content carries its kinds; overriding kinds or rebuilding the content drops them. A variable vars adds and kinds leaves out declares no kind (a reference in its value is still held to text; a value written out is substituted unchecked), so give each the kind of the place it lands (env_value inside single quotes).

template

The lines, quoted by the format recipe_alpine_openrc.ts documents.

type string

vars?

The values of the placeholders the lines add.

type Record<string, unknown> & { port?: undefined; cookie_keys?: undefined; }

kinds?

The kinds of the variables vars adds.

type Record<string, TemplateKind> & { port?: undefined; cookie_keys?: undefined; }

FuzAppTrustedProxiesOptions
#

recipe_env.ts view source

FuzAppTrustedProxiesOptions import type {FuzAppTrustedProxiesOptions} from '@fuzdev/zap/recipe_env.js';

env_prefix

The service infix in FUZ_<infix>_TRUSTED_PROXIES, e.g. 'FORGE'.

type string

value?

The trusted proxy address(es). Defaults to '127.0.0.1' (the loopback nginx hop).

type string

hardened_openrc_service
#

recipe_alpine_openrc.ts view source

(options: HardenedOpenrcServiceOptions): string import {hardened_openrc_service} from '@fuzdev/zap/recipe_alpine_openrc.js';

Build the hardened OpenRC init-script text for options.

Produces an openrc-run script supervised by supervise-daemon, with the Tier-A privilege hardening (drop to command_user, the capability bounding set kept to what capabilities names, no_new_privs, umask, rc_ulimit, the service user's check of env_file, and the log-directory guard) and respawn-on-failure. Given a service's inputs it reproduces that service's init script deterministically.

options

returns

string

the complete init-script text, terminated by a trailing newline.

hardened_systemd_unit
#

recipe_debian_systemd.ts view source

(options: HardenedSystemdUnitOptions): string import {hardened_systemd_unit} from '@fuzdev/zap/recipe_debian_systemd.js';

Build the hardened systemd unit-file text for options.

Produces a canonical [Unit] / [Service] / [Install] unit with the standard security-hardening block (introduced by a # Security hardening comment), from a service's inputs, including its per-host hardening_comment.

options

returns

string

the complete unit-file text, terminated by a trailing newline.

HardenedOpenrcIsolation
#

recipe_alpine_openrc.ts view source

HardenedOpenrcIsolation import type {HardenedOpenrcIsolation} from '@fuzdev/zap/recipe_alpine_openrc.js';

Tier-B bubblewrap filesystem-isolation options for hardened_openrc_service.

read_write_paths

Absolute paths bind-mounted read-write inside the otherwise read-only root — everything the service writes at runtime (its app/data directory, a database socket directory, …). The working_directory should fall under one of these. /tmp is always a private tmpfs and need not be listed.

Each entry must be an absolute path of [A-Za-z0-9_./-] only — no whitespace or shell metacharacters — validated at eval. They're embedded unquoted into the bwrap command line, so an unsafe path would word-split and silently break the sandbox.

type string[]

bwrap_path?

The bwrap binary path. Defaults to /usr/bin/bwrap.

type string

HardenedOpenrcServiceOptions
#

recipe_alpine_openrc.ts view source

HardenedOpenrcServiceOptions import type {HardenedOpenrcServiceOptions} from '@fuzdev/zap/recipe_alpine_openrc.js';

Options for hardened_openrc_service.

The required fields are the per-service variations (name, description, command, user); everything else defaults to the shared hardened baseline. description / command / user / env_file are the subset shared in spirit with HardenedSystemdUnitOptions, so a deploy can present one options shape and pick the artifact by import.

name

The init-script name (/etc/init.d/<name>); also the default log basename.

type string

description

Human-readable description= (e.g. the domain).

type string

command

Absolute path to the executable (command=).

type string

command_args?

Arguments string passed as command_args=: shell words, which OpenRC splits and whose quotes it reads (see the module doc). Omitted when unset.

type string

user

The service account (command_user=<user>), by name: a numeric uid and root are refused. Also the default for group.

type string

group?

The service group (command_user=<user>:<group>), by name, not root. Defaults to user.

type string

working_directory?

directory= — the process working directory (typically the current release symlink). Omitted when unset.

type string

env_file?

Read as data into the daemon's environment — OpenRC's stand-in for systemd EnvironmentFile=. start_pre() has the service user check it, and the daemon's process, as command_user, exports it before it execs the command, so it must be readable by the service user. It must be an absolute path outside the log directory (which is chowned to the service user), or it is refused at eval. Omitted means no environment file. The format, and where it differs from systemd's, is in the module doc.

type string

capabilities?

The capabilities= value, in libcap's IAB text, which supervise-daemon applies: ^cap_x raises cap_x as an ambient capability, !cap_x drops it from the bounding set. Defaults to '^', no ambient capability (a daemon listening on a high port behind nginx needs none); pass '^cap_net_bind_service' for one that binds below 1024. Whatever it names, the recipe appends !cap_… for every other capability (CAPABILITY_NAMES by name, then every higher number up to 63, so one a newer kernel adds is dropped too; the names need a libcap that knows cap_checkpoint_restore), so the bounding set holds only the capabilities the value names without !: a '^' alone leaves it empty. libcap's IAB text has no word for "all", so each is named. Under isolation the command holds none.

type string

umask?

umask=, which supervise-daemon sets for the daemon (--umask): four octal digits. Defaults to '0077', as the systemd unit does, so what the service creates is its own; pass '0022' (or '0027' for a group) when another account reads what it writes, such as nginx serving a store through X-Accel-Redirect.

type string

ulimit_nofile?

rc_ulimit="-n <n>" open-file cap. Omitted when unset.

type number

respawn_delay?

respawn_delay seconds between supervise-daemon restarts. Defaults to 5.

type number

needs?

depend() need entries. Defaults to ['net'].

type string[]

uses?

depend() use entries (soft deps). Defaults to none; add 'postgresql' for an app on a local database.

type string[]

log_file?

Combined stdout+stderr log path (output_log/error_log). Defaults to /var/log/<name>/<name>.log. supervise-daemon opens this as command_user (after the privilege drop), so for a non-root service the path's directory must be writable by that user. The recipe handles this: it creates and chowns log_file's parent directory to the service user in a start_pre() checkpath hook (which runs as root, before the privilege drop), so the default works for a non-root service out of the box with no external directory resource. Because the parent is chowned to the service user, it must be the service's own: at or under a directory that sits directly under /var/log, /var/lib, /srv, /opt or /run and is named name or user, alone or followed by one of _, -, . (/var/log/<name>/server.log, /srv/<user>/logs/server.log), and env_file must not sit inside it, nor may command or, under isolation, the bwrap binary, which the service user could otherwise replace. Any other path is refused at eval.

At start, the hook also refuses (eerror, start_pre fails) a log directory that already exists unless it is a directory, not a symlink, owned by the service user: an absent one is created, and anything else (root's, another daemon's — a name nginx meeting /var/log/nginx) is someone else's, which the hook never re-owns. A root-owned directory is refused too, since the hook didn't make it; chown it to the service user, or remove it, to adopt it. The residual: only the log directory itself is checked, and checkpath follows a symlink in the path's intermediate components, creating through it and chowning where it points; a symlink as the last component it refuses, failing the start. So below an owner-named directory the service user already owns, a symlink it plants at an intermediate component (/srv/<user>/a/logs, replacing a) could redirect the root-run create and chown, while one swapped in for the log directory itself between the check and checkpath is refused. Where that matters, make the log directory the owner-named directory itself (/var/log/<user>).

type string

isolation?

Tier-B filesystem isolation via bubblewrap. When set, the service command runs inside a bwrap sandbox — a read-only root, a private /tmp (tmpfs), and the declared read_write_paths bound read-write — the OpenRC stand-in for systemd's ProtectSystem=strict + PrivateTmp + ReadWritePaths. It layers over the Tier-A privilege hardening (the sandboxed process still drops to command_user, holds no capabilities, whatever capabilities names (bwrap empties the bounding set), and sets no_new_privs). Requires bubblewrap on the target (declare it as a zap.pkg) and a kernel permitting unprivileged user namespaces (Alpine's default). Omit for Tier-A only.

type HardenedOpenrcIsolation

HardenedSystemdUnitOptions
#

recipe_debian_systemd.ts view source

HardenedSystemdUnitOptions import type {HardenedSystemdUnitOptions} from '@fuzdev/zap/recipe_debian_systemd.js';

Options for hardened_systemd_unit.

Required fields are the real per-service variations across deploys (description, exec_start, user, working_directory, read_write_paths); everything else defaults to the shared baseline.

description

Description= — human-readable unit description (e.g. the domain).

type string

exec_start

ExecStart= — the full command line, already absolute and quoted as needed. It begins with the absolute path of the program, optionally after systemd's @ (the next word is argv[0]) or : (no $VAR expansion) prefix. Every other prefix is refused: +, ! and !! run the command outside User= or the sandbox, and - reads a failing exit as success, so Restart=on-failure never restarts it.

type string

user

User= — the service account, by name. Also the default for group. Not root: the unit is for an unprivileged account.

type string

group?

Group= — the service group, by name, not root. Defaults to user.

type string

working_directory

WorkingDirectory= — the process working directory (typically the current release symlink).

type string

read_write_paths

ReadWritePaths= — the read-write holes punched through ProtectSystem=strict + ProtectHome=read-only. Space-joined into a single directive; every path must already exist or systemd namespace setup fails with status 226.

type string[]

env_file?

EnvironmentFile= — emitted only when set. Leave unset when exec_start injects env itself (e.g. Deno's --env=); see the module doc.

type string

after?

After= units. Defaults to ['network.target']; add 'postgresql.service' for an app on a local database.

type string[]

wants?

Wants= units, the line emitted only when non-empty. Defaults to none.

type string[]

restart?

Restart= policy. Defaults to 'on-failure'.

type string

restart_sec?

RestartSec= seconds. Defaults to 5.

type number

restrict_address_families?

RestrictAddressFamilies= — space-joined. Defaults to ['AF_UNIX', 'AF_INET', 'AF_INET6'], which covers a TCP listener, the Postgres socket, and loopback callbacks.

type string[]

wanted_by?

WantedBy= install target. Defaults to 'multi-user.target'.

type string

capabilities?

The capabilities the service holds, as systemd names them (CAP_NET_BIND_SERVICE), written both as the CapabilityBoundingSet= and as AmbientCapabilities=. Defaults to none: an empty CapabilityBoundingSet= and no AmbientCapabilities= line.

type string[]

system_call_filter?

SystemCallFilter= — the syscall sets or names the service may call (an allow-list), written with SystemCallErrorNumber=EPERM. Defaults to ['@system-service']; widen it for a workload that needs a set outside it, such as @privileged, @mount, @debug or @pkey (`systemd-analyze syscall-filter

type string[] | false

umask?

UMask= — four octal digits. Defaults to '0077'; pass '0022' (or '0027' for a group) when another account reads what the service writes.

type string

hardening_comment?

Host-specific sandboxing-rationale comment, inserted verbatim after PrivateTmp=true and before PrivateDevices=true. Pass the full comment lines (each already prefixed with # , newline-separated, no trailing newline); omitted means no comment. A line that doesn't begin with # would be a directive, and one ending in \ would continue onto the next directive, so both are refused. This is where a deploy documents why its address-family / syscall-filter choices are safe for its workload — genuinely per-host prose (a git-backend service explains its subprocesses; a pure-API service notes it has none), so it can't be a shared constant.

type string

InstallerResource
#

schema.ts view source

InstallerResource import type {InstallerResource} from '@fuzdev/zap/schema.js';

installer resource payload — software via a curl-piped script.

url

The install script's https:// URL, piped into shell; any other scheme is rejected by the structural gate.

type string

check

Path checked for existence to confirm installation (may carry ~).

type string

shell?

Command the fetched install script is piped into (default sh), run as written. The script is fetched whole first; a failed fetch, or an empty script, fails the step.

type string

letsencrypt_live_path
#

recipe_certbot.ts view source

(domain: string, file: "fullchain.pem" | "privkey.pem"): string import {letsencrypt_live_path} from '@fuzdev/zap/recipe_certbot.js';

The path of one of the files certbot keeps for domain's certificate, under /etc/letsencrypt/live/. domain is spliced in as given, so a reference or a {{domain}} template placeholder composes as a literal name does.

domain

the certificate's name, its first -d domain

type string

file

the certificate chain or its private key

type "fullchain.pem" | "privkey.pem"

returns

string

the path

LINK_PROTECTION_SYSCTL_PATH
#

LINK_PROTECTION_SYSCTLS
#

LinkResource
#

schema.ts view source

LinkResource import type {LinkResource} from '@fuzdev/zap/schema.js';

link resource payload — a symbolic link.

path

Symlink path (may carry a reference / ~). A real directory at the path fails the step.

type string

target

The target the symlink should point to (resolved the same way).

type string

logo_zap
#

MinisignVerify
#

schema.ts view source

MinisignVerify import type {MinisignVerify} from '@fuzdev/zap/schema.js';

The payload of a minisign SourceVerify. Mirrors the Rust MinisignVerify.

public_key

The pinned minisign public key: the 56 base64 characters beginning RW (the key line of a .pub file, without its comment). Mirrors the Rust MinisignKey; any other shape is rejected at config eval.

type string

signature_url?

Explicit signature URL, held to the rule url is; it may carry a reference. Defaults to the source URL with .minisig appended to its path, before any query or fragment.

type string

nginx_catch_all_core
#

recipe_nginx.ts view source

(default_path: string, catch_all_path: string, options?: NginxCatchAllOptions | undefined): NginxCatchAll import {nginx_catch_all_core} from '@fuzdev/zap/recipe_nginx.js';

The neutral core of the *_nginx_catch_all recipes: the stock-default removal at default_path and the catch-all server at catch_all_path.

The site recipes declare no default_server, so without a catch-all nginx answers a request for any other name (or none) with the first server on the port, the site, and the proxied locations forward that name to the backend as $host. The catch-all closes those requests instead. Order its file after the removal (the stock default site is itself a default_server, and two fail nginx -t, the file's validate) and after the SSL parameters it includes. A site meant to answer any host name declares only the removal.

default_path

type string

catch_all_path

type string

options?

optional

returns

NginxCatchAll

the removal script payload and the catch-all file payload.

nginx_catch_all_server
#

recipe_nginx.ts view source

(options: NginxCatchAllOptions & { ssl_snippet_path: string; }): string import {nginx_catch_all_server} from '@fuzdev/zap/recipe_nginx.js';

Build the catch-all server text: the default_server on every socket the site recipe listens on (port 80 and 443, IPv4 and IPv6), answering any request for a name no other server has with nginx's 444 (the connection closed, no response), and refusing the TLS handshake (ssl_reject_handshake), so it needs no certificate and shows none. It carries the site's SSL parameters (see NginxCatchAllOptions.ssl), ssl_snippet_path being where the 'snippet' ones are, as the site recipe takes it.

options

type NginxCatchAllOptions & { ssl_snippet_path: string; }

returns

string

the server block text, terminated by a trailing newline.

nginx_config_verify
#

recipe_nginx.ts view source

(): VerifyResource import {nginx_config_verify} from '@fuzdev/zap/recipe_nginx.js';

The verify payload that re-checks the on-disk nginx config with NGINX_VALIDATE on every plan (detection reads every resource live).

The files' validate gates each write, but a config the running nginx can't reload onto can still reach disk — a hand edit, an apply killed mid-rollback, a site that only becomes active when its sites-enabled link lands — and its bytes match the declared ones, so the files alone read as converged. A failing check shows as a pending change and blocks zap deploy's preflight. Spread it into a zap.verify that depends on the nginx service.

returns

VerifyResource

the verify assertions.

nginx_remove_default_core
#

recipe_nginx.ts view source

(default_path: string): NginxRemoveDefaultScript import {nginx_remove_default_core} from '@fuzdev/zap/recipe_nginx.js';

The neutral core of the stock-default removal: remove the default-site file at default_path (so it can't shadow the real site, or claim default_server beside the catch-all) and validate the config with nginx -t. The check reads converged once nginx is installed (its nginx.conf is on disk) and nothing is at the path, not even a dangling link; before nginx is installed it reads pending, so a fresh host plans the removal and runs it after the package it is ordered behind, instead of reading the not-yet-installed default as already gone. The distro wrappers differ only in that path — Debian's sites-enabled/default symlink vs Alpine's http.d/default.conf.

default_path

type string

returns

NginxRemoveDefaultScript

the script check/run pair.

nginx_site_core
#

recipe_nginx.ts view source

(options: NginxSiteCoreOptions): NginxSiteContent import {nginx_site_core} from '@fuzdev/zap/recipe_nginx.js';

Build the full nginx site config for a single-page app behind nginx — the neutral core the distro wrappers call.

Composes the HTTPS server block — SSL cert + the SSL params (snippet or certbot), HTTP/2 unless http2 is off, server_tokens off, an optional server-level client_max_body_size, the document root, the security-header set with the CSP (repeated in the /_app and location / blocks, which set headers of their own and so inherit none; the proxied locations inherit it), a rate-limited /api reverse proxy (with WebSocket upgrade headers + Authorization forward + timeouts), /health, the /ready deploy-readiness gate (zap's post-deploy poll target), the immutable /_app static block, any extra_locations, an optional error_page 404, and the SPA catch-all location / (no-cache + the try_files fallback) — plus the HTTP→HTTPS redirect server block (ACME challenge served plain, everything else 301'd), and, for any redirect_domains, an HTTPS server that 301s them to {{domain}}.

Neither server is a default_server: the *_nginx_catch_all recipes add one that gives a request for any other host name no answer, so the proxied locations forward only this site's names to the backend as $host.

{{domain}} / {{port}} are placeholders for zap's template engine, their values the options' domain and port; root/csp/etc. are interpolated from the options. Prefer the debian_nginx_spa_site / alpine_nginx_spa_site wrappers, which supply ssl_snippet_path for you.

options

returns

NginxSiteContent

the template content: the complete site config (terminated by a trailing newline), domain and port as its vars with a redirect_domain_<i> for each redirect_domains entry carrying a reference, and their kinds.

throws

  • Error - if `csp` holds a `"`, a `\`, a `$`, or a line break, if an `extra_locations` block sets `add_header` with no non-empty `Content-Security-Policy`, if a `redirect_domains` entry carrying a reference holds a placeholder, or if another input writes a `redirect_domain_<i>` placeholder such an entry fills

NGINX_VALIDATE
#

recipe_nginx.ts view source

"sudo nginx -t" import {NGINX_VALIDATE} from '@fuzdev/zap/recipe_nginx.js';

The validate command for a file holding nginx config — a site, a snippet, or an http-context include. nginx -t checks the whole tree the running config loads, so a write that would make the next reload fail is rolled back instead of left on disk. sudo because the check reads the TLS private keys.

NginxCatchAll
#

recipe_nginx.ts view source

NginxCatchAll import type {NginxCatchAll} from '@fuzdev/zap/recipe_nginx.js';

The two payloads the *_nginx_catch_all recipes return, each spread into its resource.

remove_default

The stock-default removal, for a zap.script.

type NginxRemoveDefaultScript

catch_all

The catch-all server, for a zap.file ordered after the removal.

type Required<Pick<FileResource, "path" | "content" | "validate">>

NginxCatchAllOptions
#

recipe_nginx.ts view source

NginxCatchAllOptions import type {NginxCatchAllOptions} from '@fuzdev/zap/recipe_nginx.js';

Options for the *_nginx_catch_all recipes.

http2?

Whether the site serves HTTP/2: pass the site recipe's http2. The catch-all's listen 443 sockets carry the same http2 parameter as the site's, so the two servers on one address and port agree on it. Defaults to true, the site's default.

type boolean

ssl?

How the site wires its SSL parameters: pass the site recipe's ssl. Defaults to 'snippet', the site's default. The catch-all includes the same parameters, because as the default_server it is the server whose SSL settings nginx starts every handshake with — its protocols, ciphers and session cache apply to the site's handshakes too, and without them a distro's own defaults would (Debian's allow TLS 1.2 ciphers without forward secrecy). That file must be on disk before the catch-all is written, or its nginx -t fails: order the catch-all after the ssl_params file ('snippet') or the certificate issuance ('certbot').

type "snippet" | "certbot"

NginxRedirectDomainVar
#

recipe_nginx.ts view source

`redirect_domain_${number}` import type {NginxRedirectDomainVar} from '@fuzdev/zap/recipe_nginx.js';

The variable a redirect_domains entry carrying a reference becomes, named for the entry's index.

NginxRemoveDefaultScript
#

recipe_nginx.ts view source

NginxRemoveDefaultScript import type {NginxRemoveDefaultScript} from '@fuzdev/zap/recipe_nginx.js';

The check/run pair of the stock-default removal nginx_catch_all_core returns.

check

Check command — exit 0 means the script is already applied (skip).

type string

run

The command the planner emits when the check fails.

type string

NginxSiteContent
#

recipe_nginx.ts view source

NginxSiteContent import type {NginxSiteContent} from '@fuzdev/zap/recipe_nginx.js';

The template content a site recipe returns: the body, its values, their kinds — domain and port, plus one redirect_domain_<i> per redirect_domains entry carrying a reference.

inheritance

vars

type { domain: string; port: string | number; } & Record<NginxRedirectDomainVar, string>

kinds

type { readonly domain: "hostname"; readonly port: "port"; } & Record<NginxRedirectDomainVar, "hostname">

NginxSiteCoreOptions
#

recipe_nginx.ts view source

NginxSiteCoreOptions import type {NginxSiteCoreOptions} from '@fuzdev/zap/recipe_nginx.js';

Options for nginx_site_core — the public-recipe inputs plus the path the wrapper supplies.

inheritance

ssl_snippet_path

The on-disk path the ssl: 'snippet' include points at — the one bit of the site config the distro wrappers supply, SSL_PARAMS_PATH on both. Unused when ssl === 'certbot'.

type string

NginxSpaSiteOptions
#

recipe_nginx.ts view source

NginxSpaSiteOptions import type {NginxSpaSiteOptions} from '@fuzdev/zap/recipe_nginx.js';

Options for the public site recipes (debian_nginx_spa_site / alpine_nginx_spa_site) — the portable, distro-agnostic inputs. The ssl_snippet_path is supplied by the wrapper, not here.

domain

The site's host name, {{domain}}'s value — typically a reference, zap.env('DEPLOY_DOMAIN'). Held to hostname once it resolves.

type string

port

The backend's loopback port, {{port}}'s value. Held to port.

type string | number

root

Document root for the static frontend, e.g. `` ${CURRENT_LINK}/build ``.

type string

csp

The Content-Security-Policy header value (interpolated, not a placeholder). One holding a ", a \, a $, or a line break is refused: inside the quoted header value the first three end it or read as an escape or an nginx variable.

type string

ssl?

How SSL is wired into the HTTPS server.

  • 'snippet' (default) — include <ssl_snippet_path>; (paired with the ssl_params recipe written to that path; zap owns the full config).
  • 'certbot' — include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; (certbot-managed params).

type "snippet" | "certbot"

http2?

Serve HTTP/2 in the HTTPS server. Defaults to true.

On by default because the alternative is a worse baseline for every site: without it a browser is capped at six connections per origin and repeats every request and response header uncompressed, which dominates the wire cost of any page serving many small assets. HPACK plus multiplexing removes both, and ALPN falls back to HTTP/1.1 for clients that can't negotiate h2, so there is no client-compatibility cost.

Emitted as the listen ... http2 parameter rather than the http2 on; directive. The directive is the modern spelling, but it only exists from nginx 1.25.1; on anything older it is an unknown directive that fails nginx -t outright, and Ubuntu 24.04 LTS ships nginx 1.24. The parameter form works on both sides of 1.25.1 (deprecated above it, still honored, one warning logged at start), so it is the spelling that delivers h2 on every supported host.

One thing to know before turning it off — and the reason this is an option at all rather than unconditional:

  • limit_req burst= absorbs arrival pattern, and h2 changes it. The /api burst was sized against requests trickling in six at a time; over one multiplexed connection a page's whole RPC fan-out can arrive at once. The zone rate is unaffected (limit_req counts requests, not connections), but a site with a heavy fan-out on load may need a larger api_burst than it did on HTTP/1.1.

type boolean

client_max_body_size?

Server-level client_max_body_size cap. Emitted as its own line (with the surrounding blank lines) only when set; omit for nginx's default.

type string

client_max_body_size_comment?

App-specific comment line rendered directly above client_max_body_size (without the leading # , which is added here). Ignored unless client_max_body_size is set; omit for no comment.

type string

api_burst?

/api limit_req burst=. Defaults to 50.

type number

spa_try_files?

The location / try_files arguments (the SPA fallback chain). Defaults to '$uri $uri/index.html /200.html'.

type string

error_page_404?

error_page 404 <target>; in the HTTPS server, e.g. '/404.html'. Emitted only when set.

type string

extra_locations?

App-specific location blocks injected into the HTTPS server verbatim (already indented to the server-block level, no leading/trailing blank lines), placed just before the /_app static block. A block with no add_header inherits the server's security headers and CSP; one with its own inherits none, so it renders site_security_header_directives(csp) beside them, or a Content-Security-Policy of its own. A block (a location, or an if in one) that sets add_header with no Content-Security-Policy is refused.

type string

acme_webroot?

ACME HTTP-01 challenge webroot for the redirect server. Defaults to /var/www/certbot.

type string

redirect_domains?

Further host names that 301 to https://{{domain}} — e.g. ['www.{{domain}}'] to send www. to the apex. Each joins the HTTP server's server_name (so the ACME challenge is served for it too) and gets a lean HTTPS server that only redirects, on the site's certificate, so the cert must cover them: pass the same names to the certbot recipes' extra_domains. Defaults to none.

A literal name may use the {{domain}} placeholder, substituted with the rest of the template. A name that carries a reference (zap.env('MIRROR_DOMAIN'), whole or interpolated) becomes its own variable, redirect_domain_<i> for the entry at index i, held to hostname once it resolves; it takes no placeholder, since a variable's value is never read for one.

type readonly string[]

PackageManager
#

schema.ts view source

PackageManager

type "apt" | "pacman" | "brew" | "apk"

import type {PackageManager} from '@fuzdev/zap/schema.js';

Package managers a target may declare. Mirrors the Rust PackageManager.

PackageResource
#

schema.ts view source

PackageResource import type {PackageResource} from '@fuzdev/zap/schema.js';

package resource payload — a system package via a package manager.

from

Package-manager → package-name mapping (e.g. {apt: "curl"}). A name takes no reference (rejected when the config is read): it is put into the manager's command unquoted, and detection asks the manager about it (under apt by the name alone, for name=version at exactly that version, and for name:arch by that architecture). So a name is one word in the manager's syntax — a letter or digit, then letters, digits, and `+ . _ - = ~ : /

type Partial<Record<PackageManager, string>>

check?

Optional custom check (a path, or a command); defaults to the PM query. A check beginning /, ~, or $HOME is a path and its references resolve; anything else is a command, run as written.

type string

PathExistsAssertion
#

schema.ts view source

PathExistsAssertion import type {PathExistsAssertion} from '@fuzdev/zap/schema.js';

The payload of a path_exists VerifyAssertion. Mirrors PathExistsAssertion.

path

The absolute path to check, as written: no reference; a ~ or relative path is rejected. A link is followed: type, mode and owner are its target's.

type string

file_type?

Required type; absent accepts either.

type FileType

mode?

Required octal mode (e.g. "600"). Mirrors Mode — octal, 7777 at most. 0644 and 644 compare the same.

type string

owner?

Required owner. Mirrors AccountName — a portable Unix user/group name ([a-z_][a-z0-9_-]*, optional trailing $; a numeric uid/gid or any other character is rejected at parse).

type string

not_empty?

Whether the path's size must be above zero (test -s). A directory's size depends on its filesystem, so this doesn't reliably check that a directory has entries.

type boolean

PgAuthMethod
#

schema.ts view source

PgAuthMethod

type "reject" | "scram-sha-256" | "md5" | "password" | "gss" | "ident" | "pam"

import type {PgAuthMethod} from '@fuzdev/zap/schema.js';

A PostgreSQL pg_hba.conf auth method — the set PostgreSQL documents, but for those that can't work on a plain TCP host line with no options, the only kind zap writes: trust (it admits any account on the machine as the role), peer (accepted only on a local line), cert (only on a hostssl line), ldap and radius (they need options zap doesn't write), and sspi and bsd (not built on Linux). Mirrors PgAuthMethod.

PGDG_KEY_BLAKE3
#

recipe_debian_postgres.ts view source

"bb84ad7105529a9bf29a01dca1d2e400576031b070e396a9f0c669bf75457192" import {PGDG_KEY_BLAKE3} from '@fuzdev/zap/recipe_debian_postgres.js';

The blake3 of the PGDG signing key postgres_apt_repo fetches, vendored as its key_blake3: zap installs only these bytes, so a config needs no lock entry for it. When PGDG rotates the key, its fetch no longer matches and the apply fails naming both digests; moving to the new key is an edit of this constant.

PipelineResource
#

schema.ts view source

PipelineResource import type {PipelineResource} from '@fuzdev/zap/schema.js';

pipeline resource payload — imperative multi-step execution.

env?

Environment variables shared across all steps. Rejected by the structural gate — the sequential executor doesn't apply it; inline the vars into each step's run.

type Record<string, string>

steps

The pipeline steps.

type PipelineStep[]

max_concurrency?

Max steps to run concurrently, at least 1. Any value above 1 is rejected by the structural gate: steps run sequentially.

type number

PipelineStep
#

schema.ts view source

PipelineStep import type {PipelineStep} from '@fuzdev/zap/schema.js';

One step in a pipeline resource. Mirrors the Rust PipelineStep.

id

Identifier for this step, unique within the pipeline: a repeated id is rejected by the structural gate.

type string

name?

Human-readable name for the step, printed under its id in a plan.

type string

run

The command this step runs.

type string

cwd?

Working directory for this step. Rejected by the structural gate — the sequential executor doesn't apply it; inline a cd into run.

type string

env?

Environment variables for this step. Rejected by the structural gate — the sequential executor doesn't apply it; inline the vars into run.

type Record<string, string>

depends_on?

Step ids this depends on (within the pipeline). Rejected by the structural gate: steps run sequentially in declaration order.

type string[]

postgres_apt_repo
#

recipe_debian_postgres.ts view source

(options?: PostgresAptRepoOptions | undefined): PostgresAptRepoFields import {postgres_apt_repo} from '@fuzdev/zap/recipe_debian_postgres.js';

Build the PostgreSQL apt_repo fields for options.

The key_url (the PGDG signing key), its pin, and source (the pgdg main deb line, with ${VERSION_CODENAME} left for the target's shell to expand, /etc/os-release sourced, when zap writes the line) are fixed; the key + list paths come from inputs, defaulting to the /usr/share/keyrings convention.

options?

optional

returns

PostgresAptRepoFields

the apt_repo payload minus id/name, ready to spread.

PostgresAptRepoFields
#

recipe_debian_postgres.ts view source

PostgresAptRepoFields import type {PostgresAptRepoFields} from '@fuzdev/zap/recipe_debian_postgres.js';

The apt_repo fields postgres_apt_repo returns (caller supplies id/name).

key_url

The URL zap fetches the GPG signing key from, on the operator's machine: https://, or http:// to a loopback host; any other scheme is rejected by the structural gate. It may carry a reference. The key is pinned by the blake3 of its bytes, and only bytes that hash to the pin are installed.

type string

key_blake3?

The key's vendored pin: the blake3 of its bytes, 64 lowercase hex. Absent, the pin is tracked in zap.lock (zap lock / zap update mint and advance it).

type string

source

The deb source line (signed-by is auto-injected from key_path). It is expanded by the target shell inside double quotes, so $VERSION_CODENAME resolves there; it takes no reference (rejected when the config is read).

type string

key_path?

Where the GPG key is stored; default /usr/share/keyrings/{id}.asc. It takes no reference (rejected when the config is read): it is written into the source line, inside double quotes.

type string

list_file?

The .list file path; default /etc/apt/sources.list.d/{id}.list. It takes no reference (rejected when the config is read): detection reads it as declared.

type string

PostgresAptRepoOptions
#

recipe_debian_postgres.ts view source

PostgresAptRepoOptions import type {PostgresAptRepoOptions} from '@fuzdev/zap/recipe_debian_postgres.js';

Options for postgres_apt_repo.

Both fields default to the /usr/share/keyrings convention; pass them to match a host that stores the key + list elsewhere (e.g. PGDG's own /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc).

key_path?

Where the GPG signing key is stored. Defaults to /usr/share/keyrings/postgresql-pgdg.asc.

type string

list_file?

The .list file path. Defaults to /etc/apt/sources.list.d/pgdg.list.

type string

Protocol
#

schema.ts view source

Protocol

type "tcp" | "udp" | "any"

import type {Protocol} from '@fuzdev/zap/schema.js';

The transport protocol of a firewall port rule. Mirrors Protocol.

rate_limit_zone
#

recipe_nginx.ts view source

(options?: RateLimitZoneOptions | undefined): string import {rate_limit_zone} from '@fuzdev/zap/recipe_nginx.js';

Build the http-context rate-limit zone line (rate_limit.conf, which the caller writes under conf.d/ on Debian and http.d/ on Alpine).

Emits the single directive the /api and /ready limit_req directives reference, limit_req_zone $binary_remote_addr zone=<zone>:<size> rate=<rate>;, keyed on the client IP; the defaults suit a single-page app's API.

options?

optional

returns

string

the directive text, terminated by a trailing newline.

RATE_LIMIT_ZONE
#

recipe_nginx.ts view source

"global" import {RATE_LIMIT_ZONE} from '@fuzdev/zap/recipe_nginx.js';

The single rate-limit zone name, shared by rate_limit_zone (which defines the zone) and nginx_site_core (whose /api + /ready limit_req directives reference it). One constant so the definition and the references can't drift — a limit_req naming a zone no limit_req_zone defined fails nginx -t. (This is why the zone name isn't an option: it would let the two sides diverge silently.)

A fixed name assumes one zap-managed site per host. Two of them would write the same rate_limit.conf path with each other's rate — fighting on every apply — and co-declaring the zone in one http context fails nginx -t on the duplicate. Co-hosting needs a per-site zone name (and path) before it needs anything else here.

Exported so a config's own limit_req (in an extra_locations block) references the zone by this constant rather than repeating its name.

RateLimitZoneOptions
#

recipe_nginx.ts view source

RateLimitZoneOptions import type {RateLimitZoneOptions} from '@fuzdev/zap/recipe_nginx.js';

Options for rate_limit_zone.

size?

Shared-memory zone size. Defaults to '10m'.

type string

rate?

rate= cap. Defaults to '10r/s'.

type string

RELOAD_NGINX_HOOK_NAME
#

recipe_certbot.ts view source

"reload-nginx" import {RELOAD_NGINX_HOOK_NAME} from '@fuzdev/zap/recipe_certbot.js';

The default file name of the distro *_certbot_reload_nginx_hook hooks.

RemoteAuth
#

schema.ts view source

RemoteAuth import type {RemoteAuth} from '@fuzdev/zap/schema.js';

A credential for fetching a remote file's bytes from a private host. Tagged on scheme (not type). Mirrors the Rust RemoteAuth.

scheme

Authorization: Bearer <token>.

type "bearer"

token

The bearer token; typically a reference, never a literal.

type string

RemoteContent
#

schema.ts view source

RemoteContent import type {RemoteContent} from '@fuzdev/zap/schema.js';

The payload of a remote FileContent. Mirrors the Rust RemoteContent.

url

Fetch location: an https:// URL with no user:password@ (http:// only for a loopback host), or the structural gate rejects it. It may carry a reference, resolved when zap fetches (the lock addresses a secret reference by its token, any other by its value).

type string

blake3?

Vendored content pin. A declared pin self-pins (skips the lock); absent, the lock pins the resolved hash. Mirrors ContentHash — exactly 64 lowercase hex on the wire.

type string

extract?

How the fetched bytes are materialized on the target.

type ExtractMode

auth?

Optional fetch credential for a private byte-host (never hashed).

type RemoteAuth

RemoteSource
#

schema.ts view source

RemoteSource import type {RemoteSource} from '@fuzdev/zap/schema.js';

The payload of a remote BuildSource. Mirrors the Rust RemoteSource.

url

Fetch location: an https:// URL with no user:password@ (http:// only for a loopback host), or the structural gate rejects it. It may carry a reference, resolved when zap fetches (the lock addresses a secret reference by its token, any other by its value).

type string

extract?

How the fetched source is unpacked before run.

type ExtractMode

auth?

Optional fetch credential for a private byte-host (never hashed).

type RemoteAuth

verify?

Optional upstream-signature verification, run when a fetch fills the content cache, before the bytes are pinned or built. A cache hit is not re-verified: it is re-hashed against the pin.

type SourceVerify

blake3?

Vendored source pin. A declared pin self-pins; absent, the lock pins it. Mirrors ContentHash — exactly 64 lowercase hex on the wire.

type string

ResourceIdentity
#

schema.ts view source

ResourceIdentity import type {ResourceIdentity} from '@fuzdev/zap/schema.js';

The common identity flattened onto every resource arg (id / name? / depends_on?). Mirrors the flat fields of the Rust Resource.

Each builder method takes ResourceIdentity intersected with the kind-specific payload; the builder shim injects the type discriminant, so authors never write type.

id

Stable identity within the config. Mirrors ResourceId (a plain string on the wire). A name: a letter, a digit, or _, then any of those and . + : @ -; anything else is rejected by the structural gate.

type string

name?

A label for whoever reads the config. A plan prints it under the change's header; everything else (apply's progress, events, triggers) names a resource by its id. It is carried in a change, so changing it changes the plan_hash of a plan that changes this resource, but never the lock entry's config_hash.

type string

depends_on?

Resource ids this depends on (the resource-graph edges).

type string[]

RunPartsFile
#

run_parts.ts view source

RunPartsFile import type {RunPartsFile} from '@fuzdev/zap/run_parts.js';

The spreadable file fields of an executable placed in a run-parts-style directory — a certbot deploy hook, a BusyBox crond daily job — ready to spread into a zap.file({...}).

path

type string

mode

type string

content

type { type: "inline"; content: string; }

ScriptResource
#

schema.ts view source

ScriptResource import type {ScriptResource} from '@fuzdev/zap/schema.js';

script resource payload — an idempotent command (check / run).

check

Check command — exit 0 means the script is already applied (skip).

type string

run

The command the planner emits when the check fails.

type string

cwd?

Working directory for the check and for run. An absolute path, taken verbatim: no reference (rejected when the config is read) and no ~ (rejected by the structural gate). A check that can't enter it reads as not applied; a run that can't enter it fails without running.

type string

env?

Extra environment, exported to the check and to run (every command of a compound sees it). Keys are POSIX names ([A-Za-z_][A-Za-z0-9_]*), validated at config-eval; a name beginning with __zap_ is rejected by the structural gate (zap's own wrapper variables). A value's references resolve before it is quoted, so it can hold any text but a NUL and the reference delimiters U+FDD0 and U+FDD1 (a value with a ' goes here, not in the command text).

type Record<string, string>

SECURITY_HEADER_DIRECTIVES
#

recipe_nginx.ts view source

readonly string[] import {SECURITY_HEADER_DIRECTIVES} from '@fuzdev/zap/recipe_nginx.js';

The site-independent security-header add_header … always; directives: HSTS, X-Frame-Options, nosniff, and Referrer-Policy, in render order, unindented. The Content-Security-Policy is per site, so it is not here: site_security_header_directives appends it.

nginx drops inherited add_header directives in any block that sets its own, so the set can't be declared once at the server level. A block that sets its own headers repeats the whole set, the CSP included, or it serves without them: an extra_locations block renders site_security_header_directives(csp). A block with a policy of its own (a sandboxing CSP for user-uploaded bytes) renders these and its own Content-Security-Policy instead.

ServiceManager
#

schema.ts view source

ServiceManager

type "systemd" | "launchd" | "openrc"

import type {ServiceManager} from '@fuzdev/zap/schema.js';

Service managers a target may declare. Mirrors the Rust ServiceManager.

ServiceResource
#

schema.ts view source

ServiceResource import type {ServiceResource} from '@fuzdev/zap/schema.js';

service resource payload — service lifecycle management.

service

Service name (interpreted by the target's service manager). Takes no reference (rejected when the config is read): it is put into the manager's command unquoted, and detection reads it as declared. So a name is one word — a letter or digit, then letters, digits, and _ . @ : - — and anything else is rejected when the config is read.

type string

enabled?

Whether the service should be enabled at boot (default true).

type boolean

running?

Whether the service should be running now; absent = don't manage runtime.

type boolean

restart_on?

Resource ids whose change triggers a restart of this service, if it is running (try-restart on systemd, rc-service --ifstarted on OpenRC). A trigger on a service with running: false is a config error.

type string[]

reload_on?

Resource ids whose change triggers a reload of this service, if it is running: try-reload-or-restart on systemd, rc-service --ifstarted <svc> reload on OpenRC (a service that can't reload fails the step).

type string[]

single_quote
#

shell.ts view source

(value: string): string import {single_quote} from '@fuzdev/zap/shell.js';

value as one single-quoted shell word, each ' in it written '\''.

The twin of zap's own quoting for the values it composes into commands, so a recipe's command quotes the way zap's do.

value

the text, literal or holding references

type string

returns

string

the quoted word

site_security_header_directives
#

recipe_nginx.ts view source

(csp: string): string[] import {site_security_header_directives} from '@fuzdev/zap/recipe_nginx.js';

The full security-header set a site's server block declares, as the directives a block that sets its own add_header repeats: SECURITY_HEADER_DIRECTIVES and the site's Content-Security-Policy, in render order, unindented.

csp

the site's Content-Security-Policy value, the csp the site recipe is given

type string

returns

string[]

the directives, one per entry.

SourceVerify
#

schema.ts view source

SourceVerify

type { scheme: "minisign"; } & MinisignVerify

import type {SourceVerify} from '@fuzdev/zap/schema.js';

Upstream-signature verification for a build source. Tagged on scheme (not type); the one scheme is minisign. Mirrors the Rust SourceVerify.

scheme

minisign -V against public_key.

type "minisign"

public_key

The pinned minisign public key: the 56 base64 characters beginning RW (the key line of a .pub file, without its comment). Mirrors the Rust MinisignKey; any other shape is rejected at config eval.

type string

signature_url?

Explicit signature URL, held to the rule url is; it may carry a reference. Defaults to the source URL with .minisig appended to its path, before any query or fragment.

type string

spa_csp
#

recipe_csp.ts view source

(options?: SpaCspOptions | undefined): string import {spa_csp} from '@fuzdev/zap/recipe_csp.js';

Build the SPA Content-Security-Policy header value for options.

Assembles the fixed directive order — default-src / script-src / style-src / img-src / font-src / connect-src / (optional worker-src) / object-src / base-uri / form-action / frame-ancestors — joined by '; ' with no trailing separator.

options?

optional

returns

string

the single-line CSP header value (no trailing semicolon).

SpaCspOptions
#

recipe_csp.ts view source

SpaCspOptions import type {SpaCspOptions} from '@fuzdev/zap/recipe_csp.js';

Options for spa_csp.

Every field defaults to the locked-down baseline, so spa_csp() with no argument is the locked-down baseline; pass sources to widen a single directive (a site that uses blob: images and a web worker).

websocket?

The connect-src WebSocket endpoint, appended after 'self'. Defaults to the zap template placeholder wss://{{domain}} (substituted at apply time).

type string

connect_src_extra?

Extra connect-src sources appended after 'self' and websocket.

type string[]

img_src_extra?

Extra img-src sources appended after the 'self' data: baseline (e.g. ['blob:']).

type string[]

worker_src_extra?

worker-src sources beyond the implicit 'self'. Emits a worker-src directive (placed after connect-src) only when set (e.g. ['blob:']).

type string[]

ssh_hardening
#

recipe_ssh.ts view source

(options: SshHardeningOptions): string import {ssh_hardening} from '@fuzdev/zap/recipe_ssh.js';

Build the hardened sshd_config drop-in text for options.

The hardened baseline: root login disabled, password + keyboard-interactive auth off, X11 forwarding off. Only AllowUsers and the tunables (max_auth_tries, login_grace_time) come from inputs. It leaves UsePAM to the distro: Debian's and Ubuntu's stock sshd_config sets it, and Alpine's sshd is built without PAM and warns on the keyword; with password and keyboard-interactive auth off, PAM authenticates nobody either way.

sshd keeps the first value it reads for most keywords, and the stock sshd_config includes sshd_config.d/*.conf in name order before its own lines. So name the drop-in to sort first (00-hardened.conf): a later drop-in, such as a cloud image's 50-cloud-init.conf setting PasswordAuthentication yes, then can't override it.

options

returns

string

the complete drop-in text, terminated by a trailing newline.

ssh_hardening_verify
#

recipe_ssh.ts view source

(options: SshHardeningOptions): VerifyResource import {ssh_hardening_verify} from '@fuzdev/zap/recipe_ssh.js';

Build the verify payload that re-asserts the hardening in sshd's effective config for the allowed user on every plan, from the options ssh_hardening was given: root login, password auth, keyboard-interactive auth, and X11 forwarding off, AllowUsers the user and no one else, and the tunables at their values. Pass both the same options object.

sshd prints each keyword lowercase with its value, one AllowUsers entry per line and LoginGraceTime in seconds. Each assertion but one matches one whole line, fixed-string (grep -qxF), the expected line one single-quoted word, so a value is matched as text. sshd adds up AllowUsers across every file it reads, so a presence check would miss a user another drop-in allows: that one compares every allowusers line together with the one expected (test "$(…)" = '…').

It reads what sshd would run with, not the drop-in's bytes, so it catches a drop-in that sorts earlier and overrides the hardening, a hand edit, or a config that no longer parses. A failing check shows as a pending change and blocks zap deploy's preflight. Spread it into a zap.verify that depends on the ssh service.

The config is read for a connection, sshd -T -C user=<user>,host=…,addr=…,lport=22, so a Match block that applies to the allowed user (Match User, Match Group, Match All, an address list that holds every client) and re-enables password auth fails the verify; without -C, sshd prints only the global values. The client address is 192.0.2.1, a documentation address, so a Match Address block naming one real network (an office, a VPN) isn't exercised: the other users it could admit are refused by AllowUsers, but the allowed user from that network is not checked. Nor are Match LocalAddress and Match RDomain blocks: the connection names no local address or routing domain.

options

the options given ssh_hardening

returns

VerifyResource

the verify assertions.

ssh_service_core
#

recipe_ssh.ts view source

(service: string, options: SshServiceOptions): Required<Pick<ServiceResource, "service" | "enabled" | "running" | "reload_on">> import {ssh_service_core} from '@fuzdev/zap/recipe_ssh.js';

Build the sshd service payload: enabled, running, and reloaded when any of options.reload_on changes. The service name is the one distro-varying value; prefer the debian_ssh_service / alpine_ssh_service wrappers, which pass it.

zap's reload is systemctl try-reload-or-restart on systemd and rc-service --ifstarted <name> reload on OpenRC. Both distros' sshd reload runs sshd -t first and leaves the running sshd untouched when the config is invalid.

service

the service manager's name for sshd

type string

options

the ids that reload sshd (reload_on)

returns

Required<Pick<ServiceResource, "service" | "enabled" | "running" | "reload_on">>

the service payload.

SSHD_VALIDATE
#

recipe_ssh.ts view source

"sudo sshd -t" import {SSHD_VALIDATE} from '@fuzdev/zap/recipe_ssh.js';

The drop-in file's validate command: sshd -t checks the whole config, drop-ins and the body of every Match block included, so a write sshd would reject is rolled back before the service reloads onto it. An unsupported keyword is a warning to it, not an error.

SshHardeningOptions
#

recipe_ssh.ts view source

SshHardeningOptions import type {SshHardeningOptions} from '@fuzdev/zap/recipe_ssh.js';

Options for ssh_hardening and ssh_hardening_verify.

user is the per-deploy variation (the account allowed to log in); max_auth_tries and login_grace_time default to the shared baseline.

user

AllowUsers — the sole account permitted to SSH in (the deploy user). One name, so no whitespace (sshd would read each word as another user) and no , (the verify names the user in sshd -C's comma-separated connection spec).

type string

max_auth_tries?

MaxAuthTries — auth attempts before the connection is dropped. Defaults to 3.

type number

login_grace_time?

LoginGraceTime — seconds to complete authentication. Defaults to 20.

type number

SshKeys
#

schema.ts view source

SshKeys

type { copy_from: string; } | { keys: string[]; }

import type {SshKeys} from '@fuzdev/zap/schema.js';

SSH key configuration for a user resource: copy another user's authorized_keys, or supply explicit keys. Untagged — distinguished by which field is present. Exactly one of the two: an object with both copy_from and keys type-checks here and is rejected when the config is read. Mirrors the Rust SshKeys.

SshService
#

recipe_ssh.ts view source

Required<Pick<ServiceResource, "service" | "enabled" | "running" | "reload_on">>

type Required<Pick<ServiceResource, "service" | "enabled" | "running" | "reload_on">>

import type {SshService} from '@fuzdev/zap/recipe_ssh.js';

The service fields the *_ssh_service recipes return (caller supplies id/name/depends_on).

service

Service name (interpreted by the target's service manager). Takes no reference (rejected when the config is read): it is put into the manager's command unquoted, and detection reads it as declared. So a name is one word — a letter or digit, then letters, digits, and _ . @ : - — and anything else is rejected when the config is read.

type string

enabled

Whether the service should be enabled at boot (default true).

type boolean

running

Whether the service should be running now; absent = don't manage runtime.

type boolean

reload_on

Resource ids whose change triggers a reload of this service, if it is running: try-reload-or-restart on systemd, rc-service --ifstarted <svc> reload on OpenRC (a service that can't reload fails the step).

type string[]

SshServiceOptions
#

recipe_ssh.ts view source

SshServiceOptions import type {SshServiceOptions} from '@fuzdev/zap/recipe_ssh.js';

Options for the distro *_ssh_service wrappers.

reload_on

The ids whose change reloads sshd — the hardening drop-in, at least. Required: without it the service would never pick up a config change.

type string[]

ssl_params
#

recipe_nginx.ts view source

(): string import {ssl_params} from '@fuzdev/zap/recipe_nginx.js';

Build the shared SSL-parameters snippet content.

The ssl_protocols/ssl_ciphers/ssl_prefer_server_ciphers + ssl_session_* block both server contexts include to avoid duplication. The value is fixed across distros, and so is its path (SSL_PARAMS_PATH), so this takes no parameters.

The session-cache zone is named fuz_app_SSL, not the conventional SSL, because Alpine's stock nginx.conf already declares ssl_session_cache shared:SSL:2m in the http block — a second SSL zone at a different size makes nginx -t fail with a zone-size conflict. A unique name sidesteps any distro's built-in zone (Debian's nginx.conf declares none, so it's harmless there).

returns

string

the complete snippet text, terminated by a trailing newline.

SSL_PARAMS_PATH
#

recipe_nginx.ts view source

"/etc/nginx/snippets/ssl_params.conf" import {SSL_PARAMS_PATH} from '@fuzdev/zap/recipe_nginx.js';

The on-disk path of the ssl_params snippet, the same on Debian and Alpine: the distro site wrappers pass it as ssl_snippet_path, and a config writes the ssl_params file here. Under snippets/, never Alpine's auto-included http.d/, which would declare the session zone a second time.

sudo_as_user
#

shell.ts view source

(recipe: string, user: string): string import {sudo_as_user} from '@fuzdev/zap/shell.js';

The sudo -u <user> prefix of a command a recipe runs as user, refusing a user that wouldn't name the account the recipe means.

sudo -u reads a user starting with # as a uid (#0 is root), and an empty one as nothing at all. A reference (env_ref / zap.env) is refused too: its value isn't known until the command runs, so a recipe can't check it, and a value of #0 would run the command as root.

recipe

the recipe's name, for the error

type string

user

the account's name

type string

returns

string

sudo -u and the quoted name, then a space

throws

  • Error - if `user` is empty, starts with `#`, or holds a reference

SudoMode
#

schema.ts view source

SudoMode

type "nopasswd" | "password"

import type {SudoMode} from '@fuzdev/zap/schema.js';

Sudo access mode for a user resource. Mirrors SudoMode.

SwapResource
#

schema.ts view source

SwapResource import type {SwapResource} from '@fuzdev/zap/schema.js';

swap resource payload — a swap file.

size

Swap size (e.g. "2G").

type string

path?

Swap file path (default /swapfile). Used verbatim — no ~ expansion, and no reference (rejected when the config is read: detection reads it as declared).

type string

sysctl_apply
#

recipe_sysctl.ts view source

(path: string, values: Readonly<Record<string, string>>): SysctlApplyScript import {sysctl_apply} from '@fuzdev/zap/recipe_sysctl.js';

Build the script that puts values in force from the file at path.

check reads each parameter's live value from /proc/sys, readable by any account, with its tabs and runs of blanks read as one space (a multi-value parameter such as net.ipv4.ip_local_port_range reads 32768<tab>60999 there), and passes when every one is the declared value; run loads the file with sysctl -p and fails when a value still differs. Pass the same values the file was written from.

path

the absolute path of the sysctl.d file

type string

values

the parameters the file holds

type Readonly<Record<string, string>>

returns

SysctlApplyScript

the script check/run pair.

sysctl_conf
#

recipe_sysctl.ts view source

(values: Readonly<Record<string, string>>): string import {sysctl_conf} from '@fuzdev/zap/recipe_sysctl.js';

Build the sysctl.d file text for values, one name = value line each, in the order given.

values

type Readonly<Record<string, string>>

returns

string

the file text, terminated by a trailing newline.

SysctlApplyScript
#

recipe_sysctl.ts view source

SysctlApplyScript import type {SysctlApplyScript} from '@fuzdev/zap/recipe_sysctl.js';

The check/run sysctl_apply returns (caller supplies id/name/depends_on).

check

Check command — exit 0 means the script is already applied (skip).

type string

run

The command the planner emits when the check fails.

type string

SysctlValues
#

recipe_sysctl.ts view source

Readonly<Record<string, string>>

type Readonly<Record<string, string>>

import type {SysctlValues} from '@fuzdev/zap/recipe_sysctl.js';

Kernel parameters by sysctl name, each with the value it is set to.

[key: string]

type string

readonly

TargetInput
#

schema.ts view source

TargetInput

type { local: true; package_manager?: PackageManager | undefined; service_manager?: ServiceManager | undefined; } | { local?: false | undefined; host?: string | undefined; user?: string | undefined; bootstrap_user?: string | undefined; port?: number | undefined; key_path?: string | undefined; home?: string | undefined; package_manager?: PackageManager | undefined; service_manager?: ServiceManager | undefined; }

import type {TargetInput} from '@fuzdev/zap/schema.js';

The zap.target({...}) machine descriptor. Modeled as a union so the contradictory local: true with remote connection fields is unrepresentable — a local target rejects host / user / bootstrap_user / port / key_path / home. Mirrors the Rust Target / TargetWire (local-vs-remote TargetLocation).

TemplateContent
#

schema.ts view source

TemplateContent import type {TemplateContent} from '@fuzdev/zap/schema.js';

template file content: a {{key}} template with per-variable substitution. Mirrors the Rust TemplateContent. The site and env recipes return one whole, with the kinds of the variables their templates place.

type

A {{key}} template with per-variable substitution.

type "template"

template

The template body, used as declared. It takes no reference. A placeholder is a variable's name between {{ and }} and nothing else; one that names no variable is rejected by the structural gate.

type string

vars?

Variable values substituted for {{key}}, in one pass: a value is never read for placeholders. A string value may be or carry a reference, resolved before it is substituted. A value that isn't a string is substituted as its compact JSON text and takes no reference at any depth. A variable no placeholder names is rejected by the structural gate. A reference whose value holds a line break (CR or LF) or a NUL holds the file and refuses its write: it would add a line.

type Record<string, unknown>

kinds?

The kind of value each named variable must render to (TemplateKind). Each key names a variable in vars, or the structural gate rejects it. A declared kind is checked on a value written out when the config is read, and on one carrying a reference once it resolves, which holds the file and refuses its write when it isn't its kind. A variable not named here declares no kind: a reference in its value is still held to 'text' once it resolves, and a value written out is substituted unchecked.

type Record<string, TemplateKind>

TemplateKind
#

schema.ts view source

TemplateKind

type "text" | "hostname" | "port" | "path" | "email" | "env_value"

import type {TemplateKind} from '@fuzdev/zap/schema.js';

The kind of value a template variable renders to, checked on its whole rendered value. Mirrors the Rust TemplateKind.

  • text — any one line: no line break, NUL, or reference delimiter. Every kind keeps this rule. A reference's value is held to it whether or not its variable declares a kind; a value written out is held to it only when its variable declares one.
  • hostname — a DNS host name (RFC 1123): dot-separated labels of ASCII letters, digits, and -, each 1 to 63 bytes, no leading or trailing -, at most 253 bytes, with no trailing dot, no wildcard, and no _.
  • port — a decimal number from 1 to 65535, with no sign or leading zero.
  • path — an absolute path whose names are ASCII letters, digits, ., _, -, +, and @, with no empty, ., or .. name.
  • email — a local part of ASCII letters, digits, ., _, %, +, and - (no leading, trailing, or doubled .), an @, and a hostname.
  • env_value — a value written inside single quotes in an env file: text with no ', which would end the quoting.

None of the hostname, port, path, or email alphabets holds a character a config grammar reads as structure (;, {, }, a quote, a blank), which is what a kind is for: a {{domain}} in an nginx server_name declared hostname can't end the directive.

UserResource
#

schema.ts view source

UserResource import type {UserResource} from '@fuzdev/zap/schema.js';

user resource payload — a Unix user (shell, home mode, SSH keys, sudoers).

username

Unix username to create/manage. Mirrors AccountName — a portable Unix name ([a-z_][a-z0-9_-]*, optional trailing $; anything else is rejected at parse). zap manages the user's home as /home/<username>, so 'root' (home /root) is rejected by the structural gate, and an existing account whose /etc/passwd home is anything else is held at detection when home_mode or ssh_keys is declared. With either declared, a /home/<username> directory root owns is given to the user when zap creates the account, and left as it is for an existing one; one another uid owns, a link, or a non-directory there holds the user.

type string

shell?

Login shell (e.g. /bin/bash); system default when absent. It takes no reference (rejected when the config is read): detection reads it as declared.

type string

home_mode?

Home-directory mode (e.g. "711"). Mirrors Mode — octal, 7777 at most.

type string

ssh_keys?

SSH key configuration, if managed.

type SshKeys

sudo?

Sudo access: a grant, or false to remove. Absent = unmanaged.

type UserSudo

groups?

Supplementary groups the user must belong to (additive — listed groups are added, never removed). Mirrors AccountName[] — portable Unix names (see AccountName). Absent/empty = unmanaged.

type string[]

UserSudo
#

schema.ts view source

UserSudo

type false | SudoMode | { mode: SudoMode; commands: string[]; }

import type {UserSudo} from '@fuzdev/zap/schema.js';

A user resource's sudo field: false to remove sudo, a bare mode string to grant unrestricted, or {mode, commands} to grant command-restricted. Untagged. Mirrors the Rust UserSudo.

validate_run_parts_name
#

run_parts.ts view source

(name: string, recipe: string): void import {validate_run_parts_name} from '@fuzdev/zap/run_parts.js';

Throw unless name is a file name a run-parts-style runner runs: letters, digits, _ and - only. BusyBox run-parts (and Debian's, by default) skips any other name, so a .sh job would never run, and a / would leave the directory. The one check for the hook and job file names the recipes place in such a directory.

name

the file name

type string

recipe

the recipe asking, named in the error

type string

returns

void

throws

  • when - `name` holds any other character, or is empty

VerifyAssertion
#

VerifyResource
#

schema.ts view source

VerifyResource import type {VerifyResource} from '@fuzdev/zap/schema.js';

verify resource payload — assertions checked at detection and, on a wetrun, checked again at its place in dependency order; one still failing there fails the run: changes ordered before it have run, the rest are skipped.

assertions

Assertions to check (all must pass for the resource to be met).

type VerifyAssertion[]

WEB_FIREWALL_ALLOW
#

recipe_firewall.ts view source

readonly FirewallAllowRule[]

type readonly FirewallAllowRule[]

import {WEB_FIREWALL_ALLOW} from '@fuzdev/zap/recipe_firewall.js';

The standard web-host inbound allow set: SSH (22), HTTP (80), HTTPS (443), all tcp.

Expressed as port/proto rules rather than ufw named profiles ('OpenSSH', 'Nginx Full') so the identical set works on both backends — nftables has no application-profile concept (a named rule under it is a config error zap rejects at plan). One source for "what a web host accepts," shared by both wrappers.

web_firewall_allow_rules
#

recipe_firewall.ts view source

(options?: WebFirewallOptions | undefined): FirewallAllowRule[] import {web_firewall_allow_rules} from '@fuzdev/zap/recipe_firewall.js';

The baseline allow set plus any allow_extra, as a fresh mutable array (the shared firewall.allow both wrappers build on).

options?

optional

returns

FirewallAllowRule[]

a new FirewallAllowRule[] — the baseline is never mutated.

WebFirewallOptions
#

ZapBuilder
#

zap.ts view source

ZapBuilder import type {ZapBuilder} from '@fuzdev/zap/zap.js';

The builder a config declares against. Every method returns the builder for chaining; the resource methods take ResourceIdentity intersected with the kind-specific payload (the shim adds type).

target

Declare the zap.target({...}) machine descriptor (local or remote SSH).

type (config: TargetInput): ZapBuilder

config

returns ZapBuilder

deploy

Declare the zap.deploy({...}) release-management block zap deploy ships with.

type (config: DeploySpec): ZapBuilder

config

returns ZapBuilder

env

A reference to the operator environment variable name: the value is read from the environment of the machine zap runs on, when the field holding the reference is used — never while the config is evaluated, which has no environment.

The result is a string to place in a field that takes a reference — whole (host: zap.env('DEPLOY_HOST')), interpolated into a template literal, or concatenated (path: '/srv/' + zap.env('APP') + '/data'). It is opaque: it holds a marker, not the value, so slicing, splitting, case-changing, or encoding it leaves something that is no longer a reference: the config fails to evaluate when part of the marker survives, and otherwise the result is ordinary text. A field that takes no reference rejects one, naming the field. A recipe, which has no builder, calls env_ref from @fuzdev/zap/env_ref.ts for the same string.

type (name: string): string

name

the variable's name: a letter or _, then letters, digits, or _

type string
returns string

a marked string standing for the variable's value

throws

  • Error - if `name` is not an environment variable's name

mask

Declare env-var names secret: ones zap doesn't recognize by where they are used, their name, or the form of their value. Each is a variable's name — 'LICENSE_CODE', not zap.env('LICENSE_CODE') — and POSIX ([A-Za-z_][A-Za-z0-9_]*); anything else throws when called.

type (...vars: string[]): ZapBuilder

vars

type string[]
returns ZapBuilder

unmask

Declare env-var names not secret, where zap reads one as a secret by its name or the form of its value and it isn't. Each is a variable's name, POSIX ([A-Za-z_][A-Za-z0-9_]*); anything else throws when called. Unmasking a variable the config uses as a fetch credential (auth) is rejected when the config is read.

type (...vars: string[]): ZapBuilder

vars

type string[]
returns ZapBuilder

pkg

Declare a package resource — a system package via a package manager.

type (config: ResourceIdentity & PackageResource): ZapBuilder

config

returns ZapBuilder

apt_repo

Declare an apt_repo resource — a GPG key + sources list.

type (config: ResourceIdentity & AptRepoResource): ZapBuilder

config

returns ZapBuilder

script

Declare a script resource — an idempotent command (check / run).

type (config: ResourceIdentity & ScriptResource): ZapBuilder

config

returns ZapBuilder

installer

Declare an installer resource — software via a curl-piped script.

type (config: ResourceIdentity & InstallerResource): ZapBuilder

config

returns ZapBuilder

user

Declare a user resource — a Unix user (shell, home mode, SSH keys, sudoers).

type (config: ResourceIdentity & UserResource): ZapBuilder

config

returns ZapBuilder

directory

Declare a directory resource — a directory with managed permissions.

type (config: ResourceIdentity & DirectoryResource): ZapBuilder

config

returns ZapBuilder

file

Declare a file resource — a file with managed content.

type (config: ResourceIdentity & FileResource): ZapBuilder

config

returns ZapBuilder

link

Declare a link resource — a symbolic link.

type (config: ResourceIdentity & LinkResource): ZapBuilder

config

returns ZapBuilder

service

Declare a service resource — service lifecycle management.

type (config: ResourceIdentity & ServiceResource): ZapBuilder

config

returns ZapBuilder

database

Declare a database resource (PostgreSQL only).

type (config: ResourceIdentity & DatabaseResource): ZapBuilder

config

returns ZapBuilder

swap

Declare a swap resource — a swap file.

type (config: ResourceIdentity & SwapResource): ZapBuilder

config

returns ZapBuilder

firewall

Declare a firewall resource — firewall rules (ufw or nftables).

type (config: ResourceIdentity & FirewallResource): ZapBuilder

config

returns ZapBuilder

verify

Declare a verify resource — assertions checked at detection and again at its place in dependency order on a wetrun; one still failing there fails the run.

type (config: ResourceIdentity & VerifyResource): ZapBuilder

config

returns ZapBuilder

pipeline

Declare a pipeline resource — imperative multi-step execution.

type (config: ResourceIdentity & PipelineResource): ZapBuilder

config

returns ZapBuilder

build

Declare a build resource — fetch a verified source, build it, install to output.

type (config: ResourceIdentity & BuildResource): ZapBuilder

config

returns ZapBuilder

ZapConfigModule
#

zap.ts view source

ZapConfigModule import type {ZapConfigModule} from '@fuzdev/zap/zap.js';

The default-export signature of a zap.ts config — a pure (zap, ctx) function with no ambient authority. It may be async: zap waits for the promise, and a builder call made after it settles fails the evaluation, even when the config catches the error (a guard against a forgotten await).

(call)

type (zap: ZapBuilder, ctx: ZapContext): void | Promise<void>

zap

ctx

returns void | Promise<void>

ZapContext
#

zap.ts view source

ZapContext import type {ZapContext} from '@fuzdev/zap/zap.js';

Plan-time inputs to a config's default export. There are none, so it is empty: the same zap.ts always evals to the same declared state (the changeset built from it then depends on what detection finds on the target).