# Terraform

A bank can manage its Mirror posture as code:

- the corridors it proposes and ratifies;
- the witnesses it vouches for;
- the https endpoints it wants pushed to.

There are two ways, both shipped as source: a **module** (`terraform/`, with `examples/bank-a`) that needs nothing but Terraform and Node, and a compiled **provider** (`terraform/provider`, Go) that adds drift detection and import.

## The module

```hcl
module "mirror" {
  source            = "./terraform"
  bander_command    = ["/opt/mirror/mirror-bander"]
  signer            = { custody_file = "/etc/mirror/member-signer.json" }
  propose_corridors = { cu_v1 = { product = "cu", version = 1, edges = [7800, 8200, 8600, 9000], tolerance = 1 } }
  webhooks          = { gateway = { url = "https://mirror-hooks.bank.example/deliveries" } }
}
```

Each resource runs a helper (`terraform/mirror-tf.js`) that does only the HTTP. Before sending an event, the helper reads the live registry and skips anything this member already proposed or ratified, so re-applying changes nothing. The module does not notice a subscription removed outside Terraform; the provider does.

## The provider

```hcl
terraform {
  required_providers {
    mirror = { source = "terraform.local/mirror/mirror" }
  }
}

provider "mirror" {
  bander_command = ["/opt/mirror/mirror-bander"]          # signs; has no network
  custody_file   = "/etc/mirror/member-signer.json"       # or key_file, for a trial
}

resource "mirror_webhook_subscription" "gateway" {
  url = "https://mirror-hooks.bank.example/deliveries"
}

resource "mirror_governance_event" "copper" {
  type    = "corridor.propose"
  payload = jsonencode({ product = "cu", version = 1, edges = [7800, 8200, 8600, 9000], closed = true, max_ratio = 1.2 })
}

data "mirror_registry" "live" {}                          # members, ratified corridors, active witnesses
```

- **`mirror_webhook_subscription`** reads the operator's live list on every plan. A subscription removed outside Terraform shows up as drift and is recreated. An existing one can be adopted with `terraform import mirror_webhook_subscription.gateway wh_...`. Ids derive from (member, url, events), so a recreated subscription gets the same id.
- **`mirror_governance_event`** sends a signed corridor, witness or member proposal or ratification. It reports `applied = false` when its effect is already on the registry for this member.
- **Building it:** `go build -o terraform-provider-mirror .` in `terraform/provider`, using `terraform-plugin-framework` 1.19. Then point Terraform at it with a CLI configuration `dev_overrides` block for `terraform.local/mirror/mirror`.

## How both keep their promises

- **The key never enters Terraform.** The bander signs every event and every login (`sign-event`, `sign-login`), from a key file or the HSM, and the bander has no network. Neither the configuration nor the state holds key material; both test suites check the state.
- **Signed answers are verified** against the operator's JWKS. The helper refuses to run without the verifier, and the provider rejects a subscription receipt that does not verify.
- **Governance is append-only.** `terraform destroy` removes webhook subscriptions. It never un-ratifies anything, and the provider says so in a warning. Retiring a corridor or suspending a witness is its own signed event, decided by quorum.

## What is proven

Both suites were run with Terraform 1.16.4, against a running operator in production posture.

**The module:** `server/terraform-e2e.test.js`, 12 checks, drives a real `terraform` binary:

- `init` and `validate` on the shipped example and on a bank module;
- `apply`, and a second plan that wants no changes;
- no key material in the state;
- `destroy`, which leaves governance on the record.

**The provider:** `server/terraform-provider.test.js`, 19 checks. It builds the provider with Go 1.27, then checks:

- `validate` and `apply`;
- drift: a subscription removed behind Terraform's back is recreated;
- `import` by id, followed by a plan with no changes;
- no private key in the state;
- the destroy warning.

The helper alone is also tested by `server/terraform-helper.test.js`: propose, ratify and an idempotent re-apply; subscribe and unsubscribe; the bander refusing an event that names another member; loud configuration errors.

## What is not done

- **Not on the Terraform Registry.** Publishing needs a registry namespace and a signing key for releases; that is the maintainer's decision. Until then, build it from source.
- **Only `dev_overrides` was tested.** A filesystem mirror for air-gapped installs is standard Terraform, but it has not been exercised here.
