# FormaPrint OEM — Automated Setup Runbook (for Claude Code)

You are an AI coding agent setting up **FormaPrint OEM** for a user, on
their machine, from this distribution kit. Follow these steps in order.
Each step says whether **YOU do it autonomously** or whether it **NEEDS
THE USER**. Do not skip the safety rules.

## Safety rules (read first)

- **Never ask the user to paste secrets into the chat / shared output.**
  License keys and DB passwords go **directly into `.env`** by the user (or
  by you writing a value the user hands you privately) — never echoed back.
- **Generate the internal secrets yourself** (JWT signing key, audit HMAC)
  with strong randomness and write them straight to `.env`. **Do not print
  them** in your responses.
- **Never commit `.env`** or show its contents. The kit's `.gitignore`
  already excludes it — do not override that.
- Operate only in this `distribution/formaprint/` directory.
- Treat the connection string as a secret (it contains a password).

## Step 1 — Verify Docker (YOU)

```bash
docker --version
docker compose version
docker info            # confirms the daemon is running
```

If any fail: tell the user to install Docker Desktop / start the Docker
daemon, then stop. Do not proceed without a running daemon.

## Step 2 — Collect the values only a human can provide (NEEDS THE USER)

Ask the user for these four values, which come from their Celestryll
sign-up at https://celestryll.com/get-started plus their own database.
**Tell them to place these directly into `.env` themselves** (you will
create `.env` in step 3 first), or to give them to you only if they are
comfortable — do **not** insist they paste secrets into the chat:

1. `FORMAPRINT__LICENTIA__TENANTID` — from Celestryll.
2. `FORMAPRINT__LICENTIA__LICENSEKEY` — from Celestryll.
3. `FORMAPRINT__LICENTIA__APIKEY` — from Celestryll.
4. `FORMAPRINT_CONNECTION` — their SQL Server / Azure SQL connection
   string (contains a password → secret).

If the user has **no** database of their own, offer the optional bundled
`sql` service: have them uncomment it in `docker-compose.yml`, set
`MSSQL_SA_PASSWORD` and a matching `FORMAPRINT_CONNECTION` in `.env`
(`Server=sql;Database=FormaPrint;...`). Note this is for evaluation; a
managed, backed-up database is better for production.

## Step 3 — Create and populate `.env` (YOU + USER)

Create `.env` from the template:

```bash
cp .env.template .env
```

Then fill it in:

- **You write** the prefilled + generated values (steps 3a, 4).
- **The user writes** (or privately hands you) the four secrets from
  step 2. Prefer editing `.env` directly over echoing values.

### 3a. Confirm the prefilled values (YOU)

Ensure `.env` has:

```
FORMAPRINT__LICENTIA__ENDPOINT=https://api.celestryll.com/licensing
FORMAPRINT__LICENTIA__FAILOPEN=false
FORMAPRINT__SCHEMANAME=forma
```

## Step 4 — Generate the internal secrets (YOU, autonomously)

Generate two strong random values and write them into `.env`. **Do not
print them.** Use whichever is available:

```bash
# Preferred: write directly into .env without displaying the values.
python - <<'PY'
import secrets, pathlib, re
env = pathlib.Path(".env")
text = env.read_text()
def set_key(text, key):
    val = secrets.token_urlsafe(48)          # ~64 chars, >32 bytes entropy
    if re.search(rf"(?m)^{re.escape(key)}=", text):
        return re.sub(rf"(?m)^{re.escape(key)}=.*$", f"{key}={val}", text)
    return text + f"\n{key}={val}\n"
text = set_key(text, "FORMAPRINT__JWT__SIGNINGKEY")
text = set_key(text, "FORMAPRINT__AUDIT__HMACKEY")
env.write_text(text)
print("JWT signing key and audit HMAC key written to .env")
PY
```

(If Python isn't available, use `openssl rand -base64 48` twice and write
each into `.env` with an editor — still without printing the values.)

Remind the user to also copy these two values into their secret manager;
losing the audit HMAC key means past audit rows can't be re-verified.

## Step 5 — Apply the database schema (YOU, with the admin credential)

The schema (14 ordered scripts) is **shipped in this kit's `./sql/`
directory**. Apply it with the bundled **apply-schema helper** — do **not**
hand-run raw `sqlcmd -i` (several scripts create filtered indexes that need
`SET QUOTED_IDENTIFIER ON`, which plain `sqlcmd` leaves OFF; the helper's `-I`
fixes this). Run as a DB admin/migrator (`sa` / owner); the helper creates the
database if absent and passes `SchemaName` (default `forma`):

```bash
./apply-schema.sh --database FormaPrint --schema forma \
  --server <server> --user sa --password '<sa-password>'
```

```powershell
./apply-schema.ps1 -Database FormaPrint -Schema forma `
  -Server <server> -User sa -Password '<sa-password>'
```

Needs `sqlcmd` on PATH (mssql-tools18) or `--sqlcmd <path>`.

Decision points:

- **Azure SQL?** SKIP `12_AppSecurity.sql` (it runs `CREATE LOGIN`, which
  Azure SQL rejects) — move it out of `./sql/` before running the helper, then
  create a contained user and grant it
  `SELECT/INSERT/UPDATE/DELETE/EXECUTE` on `SCHEMA::forma` only.
- **Self-hosted SQL Server?** Run all 14. `12_AppSecurity.sql` creates the
  least-privilege `formaprint_app` login (dev password `FormaPrint!App#Dev1`);
  set a strong password and use that (**not** `sa`) in `FORMAPRINT_CONNECTION`
  — Production refuses an `sa` runtime connection at boot.
- Scripts are idempotent — safe to re-run.
- `sa` is the **admin/migrator** credential for schema apply only; the **app**
  credential in `FORMAPRINT_CONNECTION` is the runtime one and must be the
  `formaprint_app` login. Ask the user for the admin credential if you don't
  have it — and keep it out of chat output.

## Step 6 — Bring up the stack (YOU)

```bash
docker compose pull
docker compose up -d
docker compose ps
```

If a service is unhealthy, read logs: `docker compose logs formaprint-api`.

## Step 7 — Poll health until ready (YOU)

Poll liveness, then readiness (readiness confirms the DB is reachable).
Use the port from `FORMAPRINT_API_PORT` (default 5000):

```bash
for i in $(seq 1 30); do
  curl -fsS http://localhost:5000/health/live >/dev/null 2>&1 && break
  sleep 2
done
curl -fsS http://localhost:5000/health/live      # {"status":"live"}
curl -fsS http://localhost:5000/health/ready      # healthy => DB reachable
```

If readiness never goes healthy, it's almost always `FORMAPRINT_CONNECTION`
or an un-applied schema. Diagnose from `docker compose logs formaprint-api`.

## Step 8 — Smoke render (YOU, autonomously)

Every request needs a bearer token signed with the JWT signing key you
generated in step 4. Mint a short-lived HS256 service token
(`iss=FormaPrint`, `aud=FormaPrint`, `scope=service`) using that key, then
render the seeded Work Order template for the seeded demo tenant. Read the
key from `.env` in-process — **do not print it or the token**.

```bash
python - <<'PY'
import os, re, json, base64, hmac, hashlib, time, pathlib, urllib.request
env = dict(re.findall(r"(?m)^([A-Z0-9_]+)=(.*)$", pathlib.Path(".env").read_text()))
key = env["FORMAPRINT__JWT__SIGNINGKEY"].encode()
port = env.get("FORMAPRINT_API_PORT", "5000")
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
now = int(time.time())
hdr = b64(json.dumps({"alg":"HS256","typ":"JWT"}).encode())
pl  = b64(json.dumps({"iss":"FormaPrint","aud":"FormaPrint","scope":"service",
                      "sub":"claude-setup-smoke","iat":now,"nbf":now,"exp":now+300}).encode())
sig = b64(hmac.new(key, hdr+b"."+pl, hashlib.sha256).digest())
token = (hdr+b"."+pl+b"."+sig).decode()
body = json.dumps({"templateId":"33333333-3333-3333-3333-333333333333",
  "documentType":"WorkOrder","outputFormat":"pdf",
  "data":{"workOrderNumber":"WO-SMOKE","status":"Released","priority":"High",
          "assignedTo":"Setup","factoryName":"MainPlant","customer":{"name":"Smoke Test"},
          "operations":[{"seq":10,"description":"Mill housing","machine":"MILL-04","standardHours":1.5}],
          "materials":[{"sku":"HSG-1001","description":"Housing blank","quantity":1,"uom":"EA"}]}}).encode()
req = urllib.request.Request(f"http://localhost:{port}/reports/generate", data=body, method="POST",
  headers={"Content-Type":"application/json","Authorization":f"Bearer {token}",
           "X-Tenant-Id":"11111111-1111-1111-1111-111111111111",
           "X-Factory-Id":"22222222-2222-2222-2222-222222222222"})
resp = json.load(urllib.request.urlopen(req))
print("Smoke render OK — reportId:", resp.get("reportId"), "pages:", resp.get("pageCount"))
PY
```

A successful response with a `reportId` means licensing validated and
rendering works end-to-end. (If you get `401`/`402` with a license reason,
the Licentia values in `.env` are wrong — go back to step 2/3. A plain
`401` means the token/key don't match — re-check step 4.)

## Step 9 — Report success (YOU)

Summarize for the user, **without printing any secret**:

- Docker services running (`docker compose ps`).
- Health: live + ready OK.
- Smoke render produced a `reportId`.
- API base URL (e.g. `http://localhost:5000`) and where to change the port.
- Reminders: `.env` holds secrets and is git-ignored; copy the generated
  JWT + audit keys into their secret manager; pin `FORMAPRINT_VERSION` for
  reproducible upgrades.

## Autonomous vs. user-provided — quick map

| Step | You (autonomous) | Needs the user |
|---|---|---|
| 1 Verify Docker | ✅ | install/start Docker if missing |
| 2 Collect license + DB values | — | ✅ TenantId, LicenseKey, ApiKey, connection string |
| 3 Create `.env` | ✅ create + prefilled values | ✅ writes the 4 secrets into `.env` |
| 4 Generate JWT + audit secrets | ✅ generate + write, never print | — |
| 5 Apply DB schema | ✅ run scripts | ✅ provide admin DB credential |
| 6 `docker compose up -d` | ✅ | — |
| 7 Poll health | ✅ | — |
| 8 Smoke render | ✅ mint token + render | — |
| 9 Report success | ✅ | — |
