enclave-artifacts
v0.4.2
Published
Publish and manage artifacts on a self-hosted enclave instance
Maintainers
Readme
enclave-artifacts
Command-line client for enclave — self-hostable artifact generation and hosting. Publish a local directory to your instance and manage what you have published, without opening a browser.
This is the client. It talks to an enclave server you run yourself; it is not useful on its own.
Early software. First release. The server it talks to has never run anywhere but its author's machine and CI.
Install
npm install -g enclave-artifactsOr run it without installing:
npx enclave-artifacts push ./distThe command is enclave either way.
Getting started
enclave login --host enclave.example.com
enclave push ./dist --title "Kanban board"login prints where to mint a token and reads it with the input masked (* per character,
nothing echoed to the terminal). The token needs three scopes — artifacts:read,
artifacts:write and shares:write — and is stored per host in
~/.config/enclave/credentials.json with mode 0600. ENCLAVE_TOKEN overrides the file, which is
what you want in CI. --token <token> skips the interactive prompt entirely — the other option
for CI, or for a terminal that cannot mask input correctly.
push writes .enclave.json next to the directory it published, recording which artifact the
directory maps to and which version it last pushed. Commit that file — it holds no secret, and
committing it is what lets a second machine or a CI job target the same artifact.
--host accepts a bare authority (enclave.example.com, 127.0.0.1:3000, [::1]:3000) or a
full http/https origin, with or without a trailing slash. Only scheme://host[:port] is
accepted — a path suffix (e.g. enclave.example.com/settings/tokens) is rejected rather than
silently truncated, since that can retarget every authenticated request to the wrong origin. An
explicit scheme is always honoured; otherwise enclave uses http for localhost, 127.0.0.1
and [::1], and https everywhere else.
An explicit http:// origin that isn't loopback is refused, since it would send the bearer token
in cleartext. Pass --insecure to opt in anyway.
Republishing
A second push against a directory that already has .enclave.json appends a new version to
the same artifact. The id and the URL do not change, so a link you already shared keeps working and
starts serving the new content:
$ enclave push ./dist
✓ 2 files, 40 KB
✓ updated 3f2a91c4 v2
→ https://enclave.example.com/a/3f2a91c4-2f1e-4a0b-9d43-5c9d0f0a1b2c
share it: enclave share create 3f2a91c4 --expires 7d--new opts out and publishes the directory as a separate artifact instead.
If the server holds a version newer than the one .enclave.json records — someone pushed from
another machine — the push is refused before anything uploads:
$ enclave push ./dist
✗ server is at v5, you last pushed v2
refusing to overwrite a newer version
re-run with --force to publish anyway--force drops that guard and publishes regardless, taking the next version number. Under --json
the same refusal is {"error":{"code":"VERSION_CONFLICT","details":{"expectedVersionNo":2,"currentVersionNo":5}}}
on stderr, with stdout left empty.
title and visibility belong to the artifact, not to a version — a republish leaves both alone.
Change them with enclave rename and enclave privacy.
A share link pinned to a version keeps serving that version after a newer one is published.
Publishing from CI, where there is no state file
.enclave.json lives inside the directory you push, which is usually a build output — gitignored,
and wiped by the build step that recreates it. A fresh checkout therefore has no state file, and a
plain push would create a duplicate artifact at a new address rather than a new version.
--artifact names the target directly, so nothing has to survive the build:
npm run build
enclave push ./dist --artifact 3f2a91c4-2f1e-4a0b-9d43-5c9d0f0a1b2cWith no state file there is no local version to compare against, so that push appends
unconditionally — the guard has nothing to guard. When a state file is present and names the same
artifact, the guard applies as usual and --force still overrides it. Naming a different
artifact than the state file is refused rather than guessed.
A full uuid is used as-is and costs no extra request, so this path needs only artifacts:write.
A prefix of eight characters or more is matched against your own artifacts, which additionally needs
artifacts:read.
--artifact and --new contradict each other and are rejected as a pair.
Commands
enclave version [--json] (also -v, -V, --version)
enclave login [--host <host>] [--token <token>]
enclave logout [--host <host>]
enclave push <dir> [--title <t>] [--visibility private|org|public]
[--artifact <id>] [--new] [--force] [--dry-run] [--json]
enclave list [--limit <n>] [--cursor <c>] [--json]
enclave show <id> [--json]
enclave rename <id> <title> [--json]
enclave privacy <id> private|org|public [--json]
enclave rm <id> [--json]
enclave restore <id> [--json]
enclave share create <id> [--version <versionId>] [--expires <7d|2026-08-10T23:59:00+07:00>] [--json]
enclave share list <id> [--json]
enclave share revoke <shareId><id> takes a full artifact uuid or any unambiguous prefix of eight characters or more.
The host resolves from --host, then ENCLAVE_HOST. push also falls back to .enclave.json.
The token resolves from --token, then ENCLAVE_TOKEN, then the stored credentials — login
included, so it runs unattended in CI.
Flags are scoped to the command that declares them. A flag a command does not take exits 2
rather than being silently discarded — enclave rm <id> --dry-run refuses instead of deleting.
--expires takes a duration (7d, 12h, 2w); a date (2026-08-10) or a date-time
(2026-08-10T14:30), both resolved in this machine's local timezone; or an ISO-8601 instant with
an explicit zone (2026-08-10T23:59:00+07:00, 2026-08-10T16:59:00Z), taken exactly as given —
including fractional seconds of any length and lowercase z. A bare date means local end of that
day (23:59:59.999 local), not UTC midnight (breaking vs early scripts that assumed UTC start of
day). Anything else is refused. The resolved instant, in both frames (UTC and local date+time), is
printed to stderr before the share link is created.
What gets uploaded
The server accepts thirteen file types — html css js mjs json svg png jpg jpeg webp woff2 txt md
— with paths matching [a-zA-Z0-9._-/] and no spaces. A bundle is validated as a unit, so one
disallowed file rejects the whole upload.
Rather than let that happen, push drops what the server would refuse and prints every file it
skipped, with the reason. A typical dist/ loses its sourcemaps and favicon and uploads fine:
$ enclave push ./dist
skipped 3 files:
app.js.map unsupported (.map)
favicon.ico unsupported (.ico)
fonts/Inter.ttf unsupported (.ttf)
✓ 2 files, 40 KB
✓ created 3f2a91c4 v1
→ https://enclave.example.com/a/3f2a91c4-2f1e-4a0b-9d43-5c9d0f0a1b2c
private — only you can open that link
share it: enclave share create 3f2a91c4 --expires 7d
or open to the instance: enclave privacy 3f2a91c4 orgUse .enclaveignore (gitignore syntax) to drop more. --dry-run shows the split without
uploading anything.
The client's copy of these rules is a convenience so a push does not spend an upload learning a file was never going to be accepted. The server is authoritative and enforces them regardless.
Scripting
Every command that returns an object takes --json, which puts the raw API object on stdout and
nothing else — errors and diagnostics always go to stderr, so | jq is safe on every path.
login, logout and share revoke have no object to return and reject the flag with exit 2.
| Exit code | Meaning |
| --------- | ---------------------------------------------------------------------------- |
| 0 | Success |
| 1 | Ran, and the answer was no — not found, refused, unreachable, token rejected |
| 2 | Malformed invocation; the command never ran |
Not included
enclave token create does not exist, deliberately. The server refuses to let an API token mint
another token, so a leaked token cannot outlive its own revocation. Mint tokens in the browser.
Generating an artifact from a prompt, and administering users, invites and the audit log, are browser-only for now.
Licence
Apache-2.0. Source: github.com/datj9/enclave
