@ubercode/channelvault
v0.2.1
Published
Git-style pull/push/diff for Mirth Connect (NextGen Connect / OIE / BridgeLink). Explodes a server config into a git-friendly directory tree and back.
Maintainers
Readme
channelvault
Git-style pull / push / diff for Mirth Connect (NextGen Connect, Open Integration Engine, BridgeLink).
channelvault explodes a Mirth server configuration into a git-friendly directory tree (channel config as JSON, each transformer, filter, connector and code-template script as its own .js file) and reassembles it losslessly, so Mirth changes can be edited in an IDE, reviewed as diffs and moved between servers.
Where it is going: docs/roadmap.md. Changes: CHANGELOG.md.
Install
Node 20.18.1 or later.
npm install -g @ubercode/channelvault # then: channelvault --help
npx @ubercode/channelvault --help # or run it without installingHow it works
Everything centers on one in-memory representation, the canonical config: a plain JSON object mirroring Mirth's serverConfiguration document. Two adapters produce it:
| Path | Source | Module |
| ------------- | -------------------------------------- | ------------ |
| Dev / offline | Administrator Backup Config .xml | src/xml |
| Live server | GET /server/configuration (JSON) | src/client |
The explode engine (src/explode) projects a canonical config onto a directory tree and reverses it. Script bodies move to sidecar .js files, leaving { "@file": "..." } in their place; split-out resources leave { "@ref": "..." }.
The two adapters produce different shapes for the same server (XML: @_version, all-string values; live: @version, native types), and nothing converts between them yet. push refuses an XML-exploded tree and implode refuses a live-pulled one unless you pass --force.
Round-trip fidelity
The exploded tree is a projection, not a re-derivation: config is stored verbatim and code is patched back at its exact location. Unknown elements, attributes, plugin step types, comments and interleaved sibling order all survive. The contract lives in the tests:
test/xml.test.ts,test/xml.robustness.test.ts: XML parse/build fixed point, entity and whitespace handling, document order, unexpected XMLtest/explode.test.ts,test/explode.paths.test.ts:implode(explode(c))deep-equalsc, byte-identical sidecars, file namingtest/e2e.test.ts: the whole pipeline on a Mirth-exported fixture (test/fixtures, synthetic;test/fixtures.guard.test.tsrejects real exports)test/third-party-fixtures.test.tsandtest/third-party-corpus.test.ts: the same pipeline on public exports from Mirth 3.0 to OIE 4.6 (mirthsync, Open Integration Engine, NextGen's FHIR examples and six other projects;test/fixtures/third-party/<source>/, each pinned by hash in itsSOURCE.mdwith its license)test/mesh.test.ts: a synthetic, production-shaped routing mesh (test/fixtures/serverConfiguration.mesh.xml: 26 channels, an 18-destination hub, a 10,000-line code template, attachment handlers), generated byscripts/fixtures/generate-mesh.tsand exported by Mirth after every channel deployed (node scripts/fixtures/build-mesh.mjs)
Layout produced by explode / pull
<root>/
channelvault.json # provenance: source, pulledAt, engine version
.env # secret values (git-ignored; see below)
server/configuration.json # everything not split out below
channelGroups/<group>.json
codeTemplates/<library>/
library.json
<template>.js
channels/<channel>/
channel.json
scripts/{preprocessor,postprocessor,deploy,undeploy}.js
source/receiver.js # JavaScript Reader body
source/{transformer,filter}/<n>.<step>.js
destinations/<dest>/writer.js # JavaScript Writer body
destinations/<dest>/{transformer,responseTransformer,filter}/<n>.<step>.jsGlobal scripts, server settings, alerts and the configuration map stay inline in server/configuration.json.
Commands
channelvault explode <backup.xml> <dir> # XML -> tree
channelvault implode <dir> <backup.xml> # tree -> XML
channelvault pull <dir> # live server -> tree
channelvault push <dir> # changed channels/templates -> live server
channelvault diff <dir> # tree vs live server
channelvault status <dir> # summary of a tree
channelvault backup # live server -> .backup/<server>-<UTC time>.xml
channelvault restore [file] # backup -> live server (default: newest of this server)channelvault <command> --help lists the flags. Server connection comes from flags or MIRTH_HOST, MIRTH_PORT, MIRTH_USER, MIRTH_PASS; prefer the env var over --pass, which shows up in process listings. Mirth's default certificate is self-signed; --insecure accepts it by turning verification off.
diff exits 0 when the tree matches the server, 1 when they differ, and 2 on any error (including a bad flag), so a scheduled drift check can tell drift from an outage. It compares content: Mirth bumps revision numbers and timestamps on any save (even an unchanged one in the Administrator) and on a restore, so those are reported as a note ("pull to update the tree's baseline") and don't count as differences.
For scripts, CI and AI agents, status, diff and push --plan-only take --json and print one JSON document on stdout; errors still go to stderr with the usual exit code.
status --json:{ root, source, pulledAt, engineVersion, counts }.diff --json:{ clean, files: [{ path, change }], secretDrift, staleRevisions, notes, patch }, wherechangeismodified,only-in-treeoronly-on-serverandpatchis the redacted unified diff.push --plan-only --json:{ target, mode, changes: [{ kind, op, id, label }], conflicts, serverOnly, notPushed, wouldStop }(plusredeployandnotRedeployedwith--deploy).wouldStoplists what would refuse a real push with the same flags, such as unconfirmed deletions or conflicts.
pull and explode replace server/, channels/, codeTemplates/ and channelGroups/ in <dir>, so they refuse a directory that has any of those but no channelvault.json, an unreadable channelvault.json, or a --dotenv file inside one of those directories. All of this is checked before anything is written.
Secrets and per-environment values
explode and pull keep the credentials they detect out of the tree. Detection is heuristic (see below), so review a first pull of a real server before committing it. Fields named like credentials (passwords, passphrases, passcodes, tokens, secrets, API/access/private keys, and DICOM's keyPW, keyStorePW and trustStorePW), and every configuration-map value, become {{env:NAME}} placeholders, and their values go to <dir>/.env, which is added to the tree's .gitignore. push and implode fill the placeholders back in and refuse to run if any are missing, naming each one.
- You can add placeholders yourself anywhere, in JSON or in a
.jsfile (for example"host": "{{env:DB_HOST}}"). A re-pull keeps them as long as they still resolve to what the server holds. --dotenv .env.prodselects another environment. Variables already set in the process environment take precedence over the file, so CI can supply them directly.- A server's error response can echo the credentials that were sent, so error messages leave it out and show only the HTTP status.
CHANNELVAULT_DEBUG=1includes it, decoded from JSON or XML, with credential fields and the env file's values redacted. That redaction matches known values and field names, so a secret the server transformed some other way (for example base64) is not recognised: don't useCHANNELVAULT_DEBUGwhere output is logged or shared. - If git would commit the env file (for example
--dotenvpointing outside the tree, into a repository that doesn't ignore it),pullandexplodewarn. - A password rotated on the server updates
.envon the nextpull, anddiffreports it by name only. The previous env file is kept in the tree's.secrets/(git-ignored, newest 5 only) whenever a value in it changes. An extracted secret inside a script survives server-side edits to the rest of that script.
Secrets inside values are caught too: credentials in URLs and connection strings, Authorization headers, createDatabaseConnection(…, 'password') and setPassword('…') calls, password/key assignments in scripts (password, dbPass, DB_PASS, apiKey…), private keys, and AWS, GitHub, Slack and JWT tokens. If pull or explode finds one, it writes nothing and lists each finding by location and kind (never the value). Then either:
- rerun with
--extract-secrets, which replaces just the secret part with a placeholder, or - list a false positive in
channelvault.allow.json(committed):{ "ignore": [{ "location": "<as printed>", "kind": "assignment", "context": "<as printed>", "note": "why" }] }.contextidentifies the surrounding text, so the entry stops applying if that text changes.
diff redacts any such secret the server holds that the tree hasn't extracted. After extraction, a known secret value (8+ characters) that still appears in plain text elsewhere, where no rule matched, is reported as a warning naming the variable and location.
Pushing
push sends only what changed, one resource at a time: channels, code templates (and library membership), and global scripts. It prints the plan and asks before applying it.
channelvault push ./mirth # everything that changed
channelvault push ./mirth --channel "ADT Router" # just this channel (repeat --channel for more)
channelvault push ./mirth --library Formatting --deploy # one library, then redeploy the channels using it- Copies: every channel, library and code template is saved by its
id, and a copied directory keeps its source's.pushrefuses a tree where two resources share an id, and a save under a name another channel holds (case-insensitive), even one a delete or rename in the same push frees: push that delete or rename first. Give a copy a new UUID. - Deletions (a channel directory or template you removed) need
--allow-deletes. Something created on the server since your last pull is never treated as a deletion;pushleaves it alone and says so. - Conflicts: if the server's copy has a newer revision than your tree (someone saved it in the Administrator since your last pull),
pushrefuses. Pull, merge in git, and push again, or pass--force. --deployredeploys the channels that changed and the channels a changed code-template library is enabled for, but only those deployed on the server right now; it never starts a channel someone took down. Failures are reported per channel.- After a push the tree's revision numbers are updated from the server, so
diffand the nextpushstay clean. - Server settings, the configuration map, channel groups and tags are not pushed;
pushnames them if they differ.--whole-serverreplaces the entire server configuration instead. It shows the same change list and needs the same--allow-deletes/--force, and it can't be combined with--channelor--library. - A tree exploded from an XML backup is refused (its shape differs from the live API's);
--ignore-originoverrides that. - Before it changes anything,
pushbacks up the server (see below) and prints therestorecommand that undoes it.--no-backupskips that. - Without a terminal,
pushneeds--yes. --plan-onlyshows the plan and what would stop it, and changes nothing: no prompt, no backup, no writes. It works without a terminal.
Backups
backup saves the server's configuration exactly as the Administrator's Backup Config does, so the Administrator can restore it too. restore puts one back. They are for recovery, outside the git workflow: the tree is what you review and promote.
channelvault backup # .backup/vns-gov-20260925T143012Z.xml
channelvault backup --out before-upgrade.xml # exactly this file
channelvault restore # the newest backup of this server
channelvault restore .backup/vns-gov-20260925T143012Z.xml --deploy- Names. The file is named after the server name in Server Settings, else the environment name, else the host and port, followed by the time in UTC.
.backupis relative to the current directory;--backup-dirpicks another. - Which server. That name is part of the configuration, so a restore or
push --whole-servercarries it to another server. Which server a backup came from is therefore recorded by Mirth's server ID, which belongs to the installation, in the directory'schannelvault-backups.json. Choosing, checking and rotating backups go by that ID. - Plain-text credentials. A backup holds every connector password and configuration-map value; channelvault's
{{env:…}}placeholders don't apply. Files are readable by their owner only (on Windows they get the folder's permissions instead), and a backup directory channelvault creates contains a.gitignorethat ignores everything in it. If git would still commit a backup, you get a warning. Keep backups off shared drives and out of CI artifacts. - Rotation. The newest 10 backups of each server are kept (
--keepchanges that); older ones are deleted. Files the manifest doesn't attribute to the server, such as ones copied in by hand, are never deleted. - Restore replaces the entire server, like
push --whole-server:- Without a file it takes the newest backup of the server it is connected to, never another server's. A backup taken from another server is refused unless you pass
--force. For a file from outside the backup directory only the server names can be compared, and it says so. - The preview says when the restore changes the server's name.
push --whole-serverdoes too. - A backup from a newer Mirth version is refused; an older one is converted by the server.
- It previews the channels and templates it creates, changes and deletes, and asks before continuing (
--yeswithout a terminal). - It saves the server's current configuration first, as a new backup, and prints the command that undoes the restore. That backup is the newest, so a second plain
restorereverts the first. - If the server changes while you confirm, nothing is restored; run it again to review the new state.
--deployredeploys every channel afterwards;--overwrite-config-mapalso replaces the configuration map.
- Without a file it takes the newest backup of the server it is connected to, never another server's. A backup taken from another server is refused unless you pass
Using channelvault with an AI agent
An agent can safely edit a tree and check its work. Pushing, restoring and anything that needs --force should stay with a person, who reads the plan first. Paste this into the agent instructions of your configuration repository (AGENTS.md, CLAUDE.md or similar) and adjust the paths:
## Mirth configuration (channelvault)
`mirth/` is a channelvault tree of our Mirth Connect server. Channel scripts are the `.js` files under `mirth/channels/<channel>/`, settings are in `channel.json`, and code templates are under `mirth/codeTemplates/<library>/`.
- Edit the `.js` and `.json` files; leave `id`, `revision`, `channelvault.json` and the `{"@file": …}` / `{"@ref": …}` markers alone. A new channel or template needs its own new UUID for `id`; a copied directory keeps the old one.
- Mirth runs these scripts in Rhino, not Node: no `async`/`await`, optional chaining (`?.`), classes, `require`, `import` or shorthand object properties (`{ status }`; write `{ status: status }`). `let`, `const` and arrow functions work.
- `{{env:NAME}}` is a placeholder for a secret kept outside git. Never replace one with a real value, and never read, print or copy `.env`, `.secrets/` or `.backup/`.
- Check your work with `channelvault diff mirth --json` (exit 0: matches the server, 1: differs) and `channelvault push mirth --plan-only --json`, which shows what a push would change and `wouldStop` reasons.
- Do not run `channelvault push` (other than with `--plan-only`), `restore` or `pull`, and never pass `--force`, `--allow-deletes` or `--yes`. Summarise the plan and let a person push.Local test server
docker compose up -d # nextgenhealthcare/connect:4.5.2, https://localhost:8443, admin/admin
MIRTH_HOST=localhost MIRTH_PORT=8443 MIRTH_USER=admin MIRTH_PASS=admin \
channelvault pull ./work --insecureTo try channelvault on a real configuration, use docker compose -f docker-compose.isolated.yml up -d instead: the same server with no outbound network (a deployed channel cannot reach anything), startup deploy off, and port 8443 on localhost only. down deletes it and everything in it.
pnpm test:integration creates two disposable Mirth 4.5.2 servers on random localhost ports and runs two suites. The mesh suite loads the production-shaped mesh and checks that a pull converges (nothing to push, no diff) before and after scoped pushes, a redeploy and a restore. The promotion suite imports the synthetic fixture and drives the CLI through a promotion with different destination credentials and independent revision history. It verifies conflict refusal, an explicit override, preserved tags and unrelated channels, and a clean repeat push. It removes its containers and network on success or failure. Docker is required; unavailable servers fail the command.
test/integration/live.test.ts is an optional additional suite for a disposable server you manage yourself. It runs when MIRTH_HOST is set; connection failures fail the suite. It writes the server configuration, so do not point it at a production or shared server.
Promoting between environments
Use a separate working copy and env file for each server. Resource revisions and the sync baseline belong to the server that supplied them; they are not comparable version numbers across independent servers. Do not alternate a single working copy between destinations.
For an initial promotion, copy the reviewed source tree into a destination working copy and supply all of its placeholders in a destination env file. The example below assumes the MIRTH_* connection variables point to the destination:
channelvault diff ./destination --dotenv .env.destination
channelvault push ./destination --dotenv .env.destination --channel "Report Distributor"Review the destination differences before overriding an independent revision history with --force. The initial source baseline cannot establish whether an independently managed destination changed since its last review. After the first successful push, keep the destination working copy and its refreshed baseline for subsequent changes. --force permits overwriting concurrent edits, while deletions still require --allow-deletes. A scoped push does not promote the configuration map or other server settings; manage those separately.
Checks
pnpm typecheck, pnpm lint, pnpm test, and pnpm build run the local checks. GitHub Actions defines those checks and packaging on Windows and Linux with Node 20.18.1, 22, and 24, plus the disposable two-server promotion on Linux. CLI tests cover partial saves and retries, failed refreshes, deployment failures, confirmation cancellation and EOF, and concurrent edits. File-symlink write protection is tested on Linux; directory junction protection is also tested on Windows.
License
MIT
