khalidadil
  • Joined on 2025-08-25

@khalidadil/dbbot-ops-cli (0.1.0)

Published 2026-10-19 15:32:02 +00:00 by khalidadil

Installation

@khalidadil:registry=
npm install @khalidadil/dbbot-ops-cli@0.1.0
"@khalidadil/dbbot-ops-cli": "0.1.0"

About this package

@khalidadil/dbbot-ops-cli

The unified dbbot CLI (Deployment & CLI): one binary over the catalog (read-only), the manager's operator API and local laptop key material. From a checkout: pnpm --filter ./packages/ops-cli build, then node packages/ops-cli/dist/cli.js help.

Install (laptop)

The published package is one self-contained file (dist/dbbot.mjs, no runtime dependencies) on the Gitea npm registry. Put the scope line in ~/.npmrc, then:

@khalidadil:registry=https://git.kadil.dev/api/packages/khalidadil/npm/
npm i -g @khalidadil/dbbot-ops-cli

It needs Node >= 24 (refused at startup otherwise), age >= 1.1 on PATH for the escrow verbs and a Docker socket for discover; dbbot --version prints the version. Each verb is box, laptop or host (Where each verb runs): the same file runs in the manager image as the box CLI (bin/dbbot admin, box verbs only).

Release

Bump version in package.json and CLI_VERSION in src/version.ts together (a published version is never republished), then publish only through node packages/ops-cli/scripts/publish.mjs from a pushed integration or main (a dry run; --apply --yes publishes and verifies).

Command groups

  • Discovery / status
    • dbbot discover [--socket <path> | --tcp host:port] [--all] [--probe] [--sqlite <project>/<service>=<path>[,...]]: list the database containers a host runs, over the dockerode-free socket-proxy client (the same classifier the agent uses). The DUMP column shows what the agent would dump (the database the image created, by the agent's own resolver) or refused:<reason> for a site it will not back up (e.g. refused:mariadb-no-database-env); STATUS is supported, unsupported (MySQL / Percona, D2, with the reason in DETAIL) or sqlite-opt-in: each path the agent-side SQLite opt-in (--sqlite, else DBBOT_SQLITE) names on a listed container, with the mount it maps through or why it is unmapped (no realpath: the CLI may run off-host). An invalid opt-in is a usage error. Read-only; never touches a DB.
    • dbbot dbs / dbbot status / dbbot checkpoints <db>: read the catalog (--catalog-url <conn> / DBBOT_CATALOG_URL / DATABASE_URL). Read-only.
  • Session (the manager's operator API; the manager is --manager <url>, else DBBOT_PUBLIC_URL)
    • dbbot login [--manager <url>] [--email <addr>]: the manager's email-code login. The code is typed at the prompt (or read from the first line of stdin); the session is saved 0600 under $DBBOT_HOME, bound to that manager's origin, and its token is never printed.
    • dbbot logout [--local]: revoke it on the manager and delete the file (--local: delete only).
    • dbbot auth break-glass --email <addr> [--manager <url>] [--colocated] [--deploy-dir <dir>]: when login mail is down, on the manager's box. It runs deploy/break-glass.sh, which mints one session inside the manager container (the CLI never holds a catalog write credential; the email must be on the operator allowlist, the session is capped at 12 h and recorded as break_glass). The script's stdout must be exactly one 64-hex line; anything else is "unexpected output" and is never echoed. The bearer is saved (0600, bound to --manager, on the box http://127.0.0.1:8080) only after GET /api/auth/session confirms it. From a laptop: ssh <box> /path/to/deploy/break-glass.sh <addr> | dbbot auth break-glass --stdin --manager "$DBBOT_PUBLIC_URL". There is no dry run, and it is a console session only: live-data changes still need a signed intent.
    • dbbot kek status [--candidate <copy>] | new [--from <copy>] [--replace-unusable] | recover --lost-kek <ref>[,<ref>...] [--by <who>] [--confirm <phrase>] | reissue-ca [--by <who>] [--confirm <phrase>], each with [--colocated] [--deploy-dir <dir>] [--env-file <file>]: on the manager's box, for a manager KEK lost with no escrowed copy or an enrolment CA that is missing or unusable. It runs deploy/kek-recover.sh with the terminal attached, so the work runs in the manager image with the manager's own catalog credential (the CLI holds none); --candidate/--from files reach the container on stdin only. status is the dry run and prints the exact next command and phrase; recover and reissue-ca need the manager stopped and that phrase (typed, or --confirm), and --by defaults to DBBOT_OPERATOR_EMAIL. Exit 3 is status FOREIGN, 4 status CA_REISSUE (deploy/docs/scripts.md).
  • Run (through the operator session; "queued" is printed only after the manager answered 2xx)
    • dbbot backup <db> [--wait [--wait-timeout 90s|30m|2h]] [--idempotency-key <k>]: queue a backup now. The same key never queues a second one (a retry after a lost connection is safe); a failure says backup NOT queued, why, and the key. --wait polls until the checkpoint is stored (default timeout 2h).
    • dbbot restore <checkpoint> --db <db> [--wait ...]: restore-to-NEW. The agent names the copy when it accepts the job (the CLI never sends or invents a name); --wait ends when the preview is ready. dbbot restore status <job> shows the state, the copy's name once chosen, and any error.
    • In-place restore is not offered: restore to a new copy, then promote it with dbbot intent sign --kind live-name-promote and dbbot intent submit.
    • dbbot intent submit <file>: hand a signed bucket-rebind or live-name-promote intent to the manager. Other agent kinds are refused by the manager (not executed in v1); erasure-purge intents and no-personal-data confirmations are uploaded in the console.
  • Operator admin (hosts, agents and enrolment tokens; the same operator session)
    • dbbot host register <label>: the manager mints the host's UUID (the identity every dbId is namespaced by) and the label is renameable; registration is always explicit. dbbot host rename <host> <new-label> keeps the UUID. dbbot host list [--record | --local] shows the manager's hosts and whether this laptop has each recorded; --record records them, --local lists only this laptop's record without contacting the manager.
    • dbbot agent list: the enrolled agents, their certificate state and health.
    • dbbot agent token mint --host <label|uuid> [--ttl <minutes>] [--replace-existing] [--out <file>]: a single-use enrolment token bound to one registered host (30 minutes by default). It is shown once; --out writes the agent env fragment (DBBOT_HOST_ID, DBBOT_ENROLMENT_TOKEN, the manager CA on one line, the enrol and channel endpoints) at 0600, never over an existing file, and keeps the token off the terminal. Every value of the answer is shape-checked before it reaches a file, and a token that cannot be delivered is revoked. --replace-existing re-enrols a rebuilt host onto the same UUID.
    • dbbot agent token list [--host <h>] (never the secret) and dbbot agent token revoke <token-id> (idempotent).
    • dbbot agent retire <host> [--force] [--yes]: revokes the agent's certificate and orphans its databases (their backups are untouched). It needs --yes, or the host label typed at a terminal; unattended without --yes it exits 2 before any request. An agent that still has work in flight is refused with the database ids unless --force. A retired host re-enrols with a plain agent token mint; --replace-existing is only for a rebuilt host that is still enrolled.
  • Native-deploy wrappers: dbbot setup | up | down | update. Each runs its deploy/ script (setup.sh, up.sh, stop.sh, update.sh) and forwards exactly the flags that script accepts: --dry-run, --colocated, --ref, --timeout, --recovery-env and --seal-catalog-cred (setup), --drain-timeout and --volumes (down; it drops the catalog, so it needs --yes, or the project name typed at a terminal). Any other option is a usage error. --env-file becomes the script's DBBOT_ENV_FILE. The scripts are found beside the dbbot checkout, or at --deploy-dir / DBBOT_DEPLOY_DIR (two different deploy/ directories in reach is refused). The exit status is the script's (update exit 5: updated, catalog-backup unhealthy, no rollback), a signal kill is 128 + the signal, and under --json stdout carries only the result. These manage the manager stack on its box only: the agent composes (agent.compose.yaml, and agent-a.compose.yaml on host A) and the proxy-*.compose.yaml files run on each app host, not through setup / up.
  • Laptop security (only where key material lives: the laptop):
    • dbbot enrol [--force]: generate and persist the 0600 ed25519 signing key; prints the public key to pin at each agent.
    • dbbot host pin --whoami <json|file> --label <name>: record a host from the --whoami line its agent printed on the host itself. The host record ($DBBOT_HOME/hosts.json, 0600) is append-only: a UUID is never recorded under two labels, a label never names two UUIDs, and a --whoami that disagrees with the record (another agent, an older identity generation, another key at the same generation) is refused. host register and host list --record write it too.
    • dbbot intent sign --kind <k> --target-host <host-uuid> [kind flags…] [--out <file>] [--yes] mints an ed25519 T2 intent (exp ≤ 15 min) the agent-transport gate accepts. --target-host is the host's UUID (a label is refused) and must be in this laptop's host record. At a terminal the summary (kind, host and the label this laptop recorded, database, names, expiry) is shown and you confirm, or pass --yes, before anything is written; without a terminal it proceeds and the summary goes to stderr. --out is created 0600 and never overwrites a file. in-place, cross-host and sqlite-force take --file-id --bucket --sha256, so the signature pins the exact blob (D15 v2). --kind erasure-purge --plan <plan.json> signs the manager-only intent for a GDPR erasure purge: erasureRequestId, dbId, targetHost and itemCount come from the downloaded plan and planSha256 = sha256(canonicalBytes(plan)); it prints the plan summary to stderr first. Agents refuse this vocabulary. --kind pii-none-confirm --target-host <recorded host-uuid> --db-id --project --service --dbname --engine --acknowledge-irreversible signs the laptop's no-personal-data confirmation (a T2 attestation the manager's console ingress and the verifier check against their own pinned keys; never relayed to an agent). It refuses without the bare acknowledgement (a value after it is a UsageError), a --db-id that is not computeDbId over the named fields, or a TTL over 15 min, and prints the statement plus "delete the file if you do not submit it": once accepted, weekly+ snapshots become compliance-locked (undeletable until expiry).
  • GDPR erasure (the B2 erasure break-glass key comes only from env DBBOT_B2_KEY_ID / DBBOT_B2_APP_KEY, never a flag, and never reaches the manager):
    • dbbot erasure purge --plan <plan.json> --endpoint https://s3.<region>.backblazeb2.com --bucket <b> [--prefix p/] [--region r] --confirm <databaseLabel> --receipt <file> [--dry-run]: for each planned blob it lists every version and delete marker of o/<oid>, r/<oid>, _t/<oid> and _t/<oid>.r, deletes GOVERNANCE versions with the bypass header (others without), never deletes a COMPLIANCE or legal-hold version, deletes markers last (only on keys with nothing kept), re-lists and writes a 0600 receipt to upload on the console's purge panel. Plain http is accepted only for localhost endpoints (tests).
    • dbbot erasure sweep-hidden --endpoint <url> --bucket <b> [--prefix p/] [--min-hidden-age-hours 24] --confirm <b> [--dry-run]: for keys under o/ and _t/ hidden by a marker older than the threshold, deletes noncurrent versions whose retention is absent or lapsed (never with bypass), then the marker: the noncurrent-version expiry Filebot does not provision; it covers flagged rows GFS already pruned.
    • dbbot erasure check-key: calls b2_authorize_account and fails unless the key has listFiles, readFileRetentions, readFileLegalHolds, deleteFiles and bypassGovernance.
    • dbbot escrow init | verify | rotate | show: the recovery escrow (Key Escrow & Recovery). No command ever prints an identity, and escrow never writes or prints the secret sets: it only proves copies of them.
      • init generates each slot as a new x25519 identity file (--recoveryN-out <new file>: written exclusively at 0600 and passphrase-armored, so age -d -i prompts; the passphrase, at least 12 characters, comes from a 0600 --recoveryN-passphrase-file or is typed twice at a hidden prompt; --no-passphrase opts out) or accepts an existing one (--recoveryN-recipient <age1… | age1tag1…> --recoveryN-identity <file or device stub>). A recipient is x25519 or a hardware tag recipient (age1tag1…: age-plugin-tpm >= 1.0.0 and age-plugin-se >= 0.2.0 print one; age-plugin-yubikey will from its first release after 0.5.1), accepted with the device plugin's identity stub, recorded hardware-bound, and proven by decrypting through the device (the plugin's touch or PIN prompts are relayed to stderr). At least one slot stays x25519 with an offline copy. Post-quantum, plugin and SSH recipients are refused before anything is written, and --allow-nonx25519-recipient is retired. It re-reads every file from disk and proves each slot (a canary the Go age binary seals to the slot opens in-process; one sealed only to the other slot does not) and an agent-shaped [DEK, R1, R2] blob, then links the public record ($DBBOT_HOME/escrow-record.json, or --record) into place: the single commit point, which never replaces a record that appeared meanwhile. It refuses to run without a plan for both slots, to re-mint over an existing record, and to overwrite any file. RECOVERY2 is --recovery2-custody hardware-held (its armored file on offline media plus paper): init and rotate warn, without refusing, when RECOVERY2 is recorded as software custody, when an identity file is not passphrase-armored, and when an accepted file's passphrase is under 12 characters, each naming a fix the CLI accepts; they also warn for each hardware-bound slot (no paper copy).
      • verify re-proves the record at any time, one custodian at a time: --agent-env <dotenv> (only the two recipient keys are read), --recovery1-identity and/or --recovery2-identity <file>, and --blob <file.age> to drill a real sealed checkpoint (streamed, nothing written). It also proves the custody copies, each against a public value only: --manager-kek <copy> with --kek-recipient <age1…> (the "manager KEK recipient" setup.sh prints) or, at cold start, --kek-seal-ref <16 hex> (the seal_ref in the catalog dump); the copy must be the bare identity line the manager loads, and an armored copy prints its age -d -o manager_kek restore step. --verifier-identity <copy> --verifier-recipient <VERIFIER_PUBLIC_IDENTITY> proves the verifier copy (warning when it is not armored). Either is refused if it is a recovery, retired or (given the other recipient) the other key. --laptop-key <copy> checks the escrowed laptop signing key against the live keystore's public key, or --laptop-public-key <key> (the DBBOT_TRUSTED_LAPTOP_KEYS value); it needs no record, and naming the live keystore itself is refused. One set per run: a WRITE/COMMAND copy (--manager-kek, --laptop-key) is never loaded beside a READ copy (--recoveryN-identity, --verifier-identity, --blob). Any failed check exits 1.
      • rotate --slot N replaces one slot (new-slot flags as for init) after proving the surviving one (--recoveryM-identity, required), committing by renaming over the record it started from; the old entry is retired in the record and its file is left untouched.
      • show prints the public record and the two DBBOT_RECOVERY{1,2}_RECIPIENT lines every agent seals to. init, rotate and show take --out-env <new file> to write exactly those two lines, and print the custody checklist rendered from key-escrow's escrow sets: the READ set, the separate WRITE/COMMAND set (with the manager KEK copy), what is not escrowed and what is kept with both, each item with its proof.
    • dbbot names check --file <f>: validates a local dbId↔name JSON file and reports every problem (nothing is synced: the manager stores no friendly names; rename a host with host rename).
  • Fleet rollout plan (print-only): dbbot rollout --dry-run --manager-image <img@sha256:…> --host-b-agent-image … --host-b-socket-proxy-image … --host-a-agent-image … --host-a-socket-proxy-image … prints the ordered compose updates (manager, host B canary, host A). Every image must be digest-pinned (a moving tag exits 2 and prints no plan); nothing is deployed.
  • Project mapping: which Filebot project backs up each Dokploy compose project, over the manager's operator API (--manager <url>, else DBBOT_PUBLIC_URL; https://, or http:// only on loopback). A Dokploy project is owned by its active link, else by the one live project whose Filebot name equals it exactly.
    • dbbot project list: LINKS, PROJECTS (how each is matched, and why one backs up nothing) and UNMATCHED DOKPLOY PROJECTS, each reason with a one-line hint. Faults (!) come first; the rows under not backed up (no enabled Filebot project or link) are informational: agents watch every Dokploy project, and an app whose Filebot project has database backups off is expected there.
    • dbbot project link <filebot-project> <dokploy-project>: the Filebot project by Filebot id, DBBot id or exact name; the Dokploy project is the name project list shows unmatched (the com.docker.compose.project label its databases report). A link adopts only databases not yet backed up under another project and prints every adopted dbId; one already backed up elsewhere stays there (no re-homing in v1, so there is no --move).
    • dbbot project unlink <dokploy-project>: retires the link and never stops a backup: its databases stay pinned to, and backed up under, the project; new ones fall back to exact-name matching. To correct a wrong link, unlink it and link the right project.
    • Login: the first run at a terminal asks for the operator email (or --email, or DBBOT_OPERATOR_EMAIL) and the emailed code, then saves the session at $DBBOT_HOME/session.json (0600 in a 0700 directory) bound to that manager origin; the token is never printed. Off a terminal it fails with no request; a 401 deletes the session. --json prints the API body. Exit codes: 0 ok, 1 refused or failed (with the reason and request id), 2 usage.

Design notes

  • Reads, never writes, the catalog. dbs/status/checkpoints open a read-only view; catalog owns the schema, the manager owns writes. Project links change only through the manager's operator API, which holds the locks, adopts and records who linked.
  • The agent never live-swaps on manager authority alone. Destructive / trust-widening verbs (in-place, live-name-promote, cross-host, bucket-rebind, re-wrap, un-enrol, sqlite-force) are ed25519-signed T2 intents minted here on the laptop; the private key never leaves it. intent sign --kind re-wrap takes an x25519 --new-verifier-recipient (the verifier unwraps dek.age unattended, in process).
  • No identity in any output. Every byte the binary prints passes src/render.ts: a result whose data or lines carry an age identity (native, post-quantum or plugin) is refused in --json and text mode alike, and identities in error messages are masked before stderr.
  • The library surface (@khalidadil/dbbot-ops-cli) exports every command's logic so it is testable without a subprocess (see test/); test/cli-binary.test.ts also runs the built dist/cli.js itself.

Depends on protocol, key-escrow (escrow record, identity files, proofs, the output guard), filebot-client, catalog (also the reason hints), discovery-core and agent-lifecycle (rollout planning, retire). Tests cross-check agent-transport's real authorization gate, envelope-crypto's real agent sealing (Go age, [DEK, R1, R2]) and a real manager (test/support/manager.ts: session, run, admin, break-glass and project verbs; an agent enrols with a minted token); test/cli.subprocess.test.ts runs the shipped binary.

Details
npm
2026-10-19 15:32:02 +00:00
3
UNLICENSED
latest
261 KiB
Assets (1)
Versions (1) View all
0.1.0 2026-10-19