Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions reference/cli/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ The CLI supports loading environment variables from your shell environment (or o
- `HARPER_CLI_TARGET` (or `CLI_TARGET`) - Sets the default `target` for CLI commands.
- `HARPER_CLI_USERNAME` (or `CLI_TARGET_USERNAME`) - Harper admin username for the target.
- `HARPER_CLI_PASSWORD` (or `CLI_TARGET_PASSWORD`) - Harper admin password for the target.
- `HARPER_CLI_REFRESH_TOKEN` (or `CLI_TARGET_REFRESH_TOKEN`) - Long-lived token the CLI exchanges for an operation token on each run. Available since v5.2.0.
- `HARPER_CLI_OPERATION_TOKEN` (or `CLI_TARGET_OPERATION_TOKEN`) - Short-lived operation token, if you would rather supply one directly. Available since v5.2.0.

**Prefer tokens over a password in CI.** A refresh token is scoped to authentication, can be revoked without changing the account password, and — unlike `HARPER_CLI_PASSWORD` — cannot be reused to log in interactively. See [Token credentials for CI/CD](#token-credentials-for-cicd) below.

**Example `.env` file**:

Expand Down Expand Up @@ -129,6 +133,51 @@ harper deploy target=https://prod-server.com:9925 replicated=true
harper restart target=https://prod-server.com:9925 replicated=true
```

##### Token credentials for CI/CD

Available since: v5.2.0

Rather than storing an admin password in your CI provider, log in once locally and hand CI a **refresh token**. The CLI mints a fresh, short-lived operation token from it on every run, so the only durable secret CI holds is a revocable token.

`harper login --for-ci` writes the two variables to **stdout** in `.env` format — and nothing else, so it pipes cleanly. Everything a human reads (the banner, prompts, status) goes to stderr:

```bash
# Set both GitHub Actions secrets in one command — the token is never displayed
harper login --for-ci | gh secret set --env-file -
Comment on lines +145 to +146

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The gh secret set command in the GitHub CLI does not support a --env-file flag. To set multiple secrets from an environment file, you typically need to loop over the file's contents in your shell script or use a custom gh extension.

Consider providing a standard shell loop or pointing to the correct syntax for setting individual secrets.

Suggested change
# Set both GitHub Actions secrets in one command — the token is never displayed
harper login --for-ci | gh secret set --env-file -
# Set GitHub Actions secrets by looping over the env-file output
harper login --for-ci | while read -r line; do gh secret set "${line%%=*}" -b "${line#*=}"; done

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying this one — gh secret set --env-file does exist, and - for stdin works. Verified on gh version 2.92.0:

$ gh secret set --help | grep env-file
  -f, --env-file file        Load secret names and values from a dotenv-formatted file

$ printf 'FOO=bar\nBAZ=qux\n' | gh secret set --env-file - --repo HarperFast/does-not-exist-xyz
failed to fetch public key: HTTP 404: Not Found (…/does-not-exist-xyz/actions/secrets/public-key)

The second command consumed stdin and got as far as the API call — it failed on the deliberately nonexistent repo, not on reading the file. A rejected - would have failed earlier with a file error.

Worth adding that the suggested loop would be a downgrade for this specific use: gh secret set NAME -b "$VALUE" puts the token in the process's argument list, where it's visible to ps for the life of the call and may land in shell history. Keeping the token on stdin and never in argv is precisely why --for-ci writes a dotenv block rather than printing a value to copy.

🤖 Reviewed by Claude Code


# Or copy them to the clipboard to paste in by hand
harper login --for-ci | pbcopy
```

The block it emits:

```bash
HARPER_CLI_TARGET=https://example.com:9925/
HARPER_CLI_REFRESH_TOKEN=eyJhbGciOi...
```

Because stdout carries only these lines, the token never appears on screen or in your shell history — which is not true of copying it out of terminal output by hand.

Expose the two values to the deploy step and no other credentials are needed:

```yaml
- name: Deploy
run: harper deploy project=my-app restart=true replicated=true
env:
HARPER_CLI_TARGET: ${{ secrets.HARPER_CLI_TARGET }}
HARPER_CLI_REFRESH_TOKEN: ${{ secrets.HARPER_CLI_REFRESH_TOKEN }}
```

**Precedence**: an explicitly supplied username (arguments or `HARPER_CLI_USERNAME`/`HARPER_CLI_PASSWORD`) wins; otherwise env-var tokens are used; otherwise the token saved by `harper login`. Env-var tokens deliberately outrank the saved credentials file so a runner that has both behaves predictably. A token refreshed from an env var is held in memory for that invocation only — nothing is written to `~/.harperdb/credentials.json`.

**Lifetimes**: operation tokens expire after `authentication.operationTokenTimeout` (default `1d`) and refresh tokens after `authentication.refreshTokenTimeout` (default `30d`). CI needs re-provisioning when the refresh token expires.

:::warning
**Each user has only one valid refresh token at a time.** Logging in again as that user issues a new one and invalidates the previous one, so a routine local `harper login` will break a pipeline holding an older token for the same account.

Create a **dedicated CI user** and run `harper login --for-ci` as that user. This also scopes the pipeline's permissions and lets you revoke its access — by logging in again as that user, or by changing its password — without disturbing anyone else.
:::

#### Method 3: Command Parameters

Provide credentials directly as command parameters:
Expand Down
15 changes: 15 additions & 0 deletions reference/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ harper login <URL>
**Optional Parameters**:

- `<URL>` - The URL of the Harper instance to log in to.
- `--for-ci` - Print CI/CD credentials to stdout after logging in. Available since v5.2.0.

**Prompts**:

Expand All @@ -164,6 +165,20 @@ You'll be asked to type in the following information:
- `<username>` - Harper admin username.
- `<password>` - Harper admin password.

**`--for-ci`**:

Prints `HARPER_CLI_TARGET` and `HARPER_CLI_REFRESH_TOKEN` to **stdout** in `.env` format — and nothing else, so the output pipes directly into a secret store without the token being displayed. Everything else (banner, prompts, status) goes to stderr:

```bash
# Set both GitHub Actions secrets in one command
harper login --for-ci | gh secret set --env-file -
Comment on lines +173 to +174

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The gh secret set command in the GitHub CLI does not support a --env-file flag. To set multiple secrets from an environment file, you typically need to loop over the file's contents in your shell script or use a custom gh extension.

Consider providing a standard shell loop or pointing to the correct syntax for setting individual secrets.

Suggested change
# Set both GitHub Actions secrets in one command
harper login --for-ci | gh secret set --env-file -
# Set GitHub Actions secrets by looping over the env-file output
harper login --for-ci | while read -r line; do gh secret set "${line%%=*}" -b "${line#*=}"; done

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying this one — gh secret set --env-file does exist, and - for stdin works. Verified on gh version 2.92.0:

$ gh secret set --help | grep env-file
  -f, --env-file file        Load secret names and values from a dotenv-formatted file

$ printf 'FOO=bar\nBAZ=qux\n' | gh secret set --env-file - --repo HarperFast/does-not-exist-xyz
failed to fetch public key: HTTP 404: Not Found (…/does-not-exist-xyz/actions/secrets/public-key)

The second command consumed stdin and got as far as the API call — it failed on the deliberately nonexistent repo, not on reading the file. A rejected - would have failed earlier with a file error.

Worth adding that the suggested loop would be a downgrade for this specific use: gh secret set NAME -b "$VALUE" puts the token in the process's argument list, where it's visible to ps for the life of the call and may land in shell history. Keeping the token on stdin and never in argv is precisely why --for-ci writes a dotenv block rather than printing a value to copy.

🤖 Reviewed by Claude Code


# Or copy them to paste in by hand
harper login --for-ci | pbcopy
```

See [Token credentials for CI/CD](authentication.md#token-credentials-for-cicd) for how the CLI consumes these variables, and for why a pipeline should use a dedicated user.

### `harper logout`

Available since: v5.0.0
Expand Down
73 changes: 73 additions & 0 deletions reference/components/applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,79 @@ Harper generates a `package.json` from component configurations and uses a form

For SSH-based private repos, use the [Add SSH Key](#add_ssh_key) operation to register keys first.

### Deploying by Reference

Available since: v5.2.0

Omitting `package` uploads a snapshot of your working directory. The result is an anonymous artifact: nothing records _which_ commit it came from, so reproducing it later — or stepping back to a previous release — means finding those exact files again.

Deploying by **reference** sends a pinned git reference instead, and the cluster fetches that exact commit. Redeploying the same reference is an exact redeploy, and rolling back is deploying an older one.

`harper deploy by_ref=true` builds that reference from the local git repository, so you don't assemble the URL yourself:

```sh
harper deploy by_ref=true restart=true replicated=true
```

This resolves the repository's `origin` remote and the current commit, then deploys `package=git+https://github.com/<owner>/<repo>.git#<full commit SHA>`.

**Parameters**:

- `by_ref` - Build the package reference from the local repository.
- `ref` _(optional)_ - Deploy a specific commit, tag, or branch instead of `HEAD`. Implies `by_ref`.
- `credential` _(optional)_ - Git host whose stored credential authenticates the clone, e.g. `github.com`. Omit for public repositories.

```sh
# Deploy a specific tag
harper deploy ref=v1.2.0 restart=true replicated=true

# Roll back by deploying an older commit
harper deploy ref=9f8c2a1 restart=true replicated=true
```

**A reference is pinned to a SHA, not to the name you typed.** Tags and branches are resolved locally and the full commit SHA is what ships. This matters on a cluster: peers resolve the package independently, so a tag that moves mid-deploy — or a branch that advances — could otherwise leave nodes running different code.

**Commit and push first.** The cluster clones from the remote, so it only sees commits that have been pushed. `by_ref` warns when the working tree is dirty, since uncommitted changes won't be part of the deploy.

#### Private repositories

Pass `credential=<host>` for a private repository. The CLI attaches a `credentials` reference naming a secret that the cluster resolves in memory at clone time, so no token travels in the operation body or lands on disk:

```sh
harper deploy by_ref=true credential=github.com restart=true replicated=true
```

Provision that credential once with [`harper deploy setup=true`](#provisioning-a-deploy-credential). See [Private-source deploy credentials](../security/secrets.md#private-source-deploy-credentials) for how the secret is named and resolved, and [`add_ssh_key`](#add_ssh_key) for the SSH-key alternative.

:::note
Deploying by reference means the **cluster** installs and builds the component from source. If your application needs a build step that can't run on the node, keep shipping the built output as a payload deploy instead.
:::

### Provisioning a Deploy Credential

Available since: v5.2.0

`harper deploy setup=true` provisions the credential a private deploy needs. It's interactive, and runs once per component and source:

```sh
harper deploy setup=true
```

It asks which private source needs a credential (a GitHub repository or an npm registry) and sources a token — from your `gh` CLI session, or one you paste — then:

1. Fetches the cluster's public key with `get_secrets_public_key`.
2. **Encrypts the token locally** into an `enc:v1:` envelope.
3. Stores only the ciphertext via `set_secret`, granted to the component.
4. Prints the `credentials` reference for the deploy to use.

The plaintext never leaves your machine: the operations API, its logs, and replication only ever carry the envelope, and the cluster decrypts it in memory at deploy time. This requires a cluster with secrets custody (Harper Pro / Fabric) — see [Client-side encryption](../security/secrets.md#client-side-encryption-encrypt-before-it-leaves-the-client).

Because the stored token is durable, later deploys — including re-fetching an older reference — reuse it without re-entering anything.

:::note
Rolling back to the **immediately previous** version needs no credential at all: [`revert_component`](../operations-api/operations.md#revert_component) swaps in the retained previous build without re-fetching from the source.
:::

## Dependency Management

Harper uses `npm` and `package.json` for dependency management.
Expand Down
2 changes: 2 additions & 0 deletions reference/security/secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ function encryptSecret(plaintext, publicKeyPem, kid) {

`deploy_component` accepts a `credentials` array so a component installed from a private **npm registry** or private **git repository** can authenticate. A provided token is ingested into the secrets store (as a reference, encrypted) rather than travelling in the operation body, persisting as a plaintext `.npmrc`, or being written to disk for git — so package-reference deploys survive rollback, reboot, and new peers joining. Ingested tokens are stored under a derived name (`deploy.<component>.<registry>` or `deploy.<component>.git.<host>`) granted to the component. See [`deploy_component`](../operations-api/operations.md#deploy_component).

To provision one of these without handing the cluster a plaintext token at all, `harper deploy setup=true` (v5.2.0+) runs the [client-side encryption](#client-side-encryption-encrypt-before-it-leaves-the-client) flow above for you: it fetches the public key, seals the token locally into an `enc:v1:` envelope, stores only the ciphertext under that same derived name, and prints the `credentials` reference for the deploy to use. See [Provisioning a Deploy Credential](../components/applications.md#provisioning-a-deploy-credential).

## Threat model

**Protects against:** theft of on-disk config/`.env` files, the editor/operations read surface, secrets appearing in operations logs and replication payloads, and an operator observing traffic at the TLS-terminating layer. Client-side encryption additionally keeps plaintext off the operations API entirely.
Expand Down
Loading