# Licentia Core OEM — Automated Setup Runbook (for Claude Code)

You are an AI coding agent setting up **Licentia Core OEM** for a user, on
their machine, from this distribution kit. Licentia is the license **server**
the user runs to license **their own** products. 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.**
  The database connection string (it contains a password) goes **directly
  into `.env`** by the user, or is handed to you privately — never echoed
  back.
- **Generate the internal secrets yourself** (JWT signing key, webhook
  signing secret, audit HMAC key, and the offline RSA keypair) with strong
  randomness and write them straight to `.env` / `./secrets`. **Do not print
  them** in your responses.
- **Never commit `.env` or `secrets/`.** The kit's `.gitignore` already
  excludes them — do not override that.
- Operate only in this `distribution/licentia/` directory.
- Treat the connection string as a secret (it contains a password).

## What is NOT needed (Licentia is different)

Licentia does **not** phone home. There are **no** license credentials
(TenantId / LicenseKey / ApiKey) to collect — the user IS the license
authority. Whether the instance is Demo or Production is decided by the SQL
**edition** it runs against (Express = demo caps; Standard/Enterprise/Azure
SQL = production). So the only human-provided value is the database.

## 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 value only a human can provide (NEEDS THE USER)

Ask the user for **one** value (plus, for schema apply, an admin DB
credential):

1. `LICENTIA_CONNECTION` — their SQL Server / Azure SQL connection string
   for the **runtime app login** (contains a password → secret). In
   Production it must **not** be `sa`; it should be the least-privilege
   `licentia_app` login (created in step 5).
2. The **admin** DB credential (`sa` or a DB owner) — needed only to create
   the database and apply the schema in step 5. May differ from the runtime
   login. Keep it out of chat output.

Ask the user which posture they want:
- **Evaluation** → SQL Express is fine (demo caps apply).
- **Production** → SQL Standard/Enterprise or Azure SQL.

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 `LICENTIA_CONNECTION` in `.env`
(`Server=sql;Database=LicentiaCoreOEM;...`). Note the bundled image runs
Express (demo caps) — for production, managed SQL is better.

## 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) `LICENTIA_CONNECTION` from
  step 2. Prefer editing `.env` directly over echoing the value.
- Leave section (a) empty — there are no Celestryll license credentials.

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

Ensure `.env` has:

```
LICENTIA__SCHEMANAME=licentia
LICENTIA_VERSION=latest
LICENTIA_API_PORT=8080
```

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

Generate the three required secrets and the optional offline keypair, and
write them into `.env` / `./secrets`. **Do not print any of them.**

```bash
# Three required HMAC secrets -> written straight into .env, never displayed.
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"
for k in ("JWT__SIGNINGKEY", "LICENTIA__WEBHOOKS__SIGNINGSECRET", "LICENTIA__AUDIT__HMACKEY"):
    text = set_key(text, k)
env.write_text(text)
print("JWT signing key, webhook secret, and audit HMAC key written to .env")
PY
```

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

**Offline activation keypair (generate it — it's harmless and enables
air-gapped licensing of the user's products later):**

```bash
mkdir -p secrets
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out secrets/offline-signing.key.pem
openssl rsa -in secrets/offline-signing.key.pem -pubout -out secrets/offline-verify.pub.pem
```

Then set the file references in `.env` (do not print the key contents):

```
LICENTIA__OFFLINE__SIGNINGKEYPEM=file:/secrets/offline-signing.key.pem
LICENTIA__OFFLINE__VERIFYKEYPEM=file:/secrets/offline-verify.pub.pem
```

Remind the user to copy the three HMAC secrets into their secret manager;
the **same** `LICENTIA__AUDIT__HMACKEY` must be used by the API, worker, and
optional dashboard, and losing it means past audit rows can't be re-verified.

## Step 5 — Create the DB and apply the schema (YOU, with the admin credential)

The schema 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 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 `LicentiaCoreOEM` if absent and passes `SchemaName` (default
`licentia`):

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

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

Needs `sqlcmd` on PATH (mssql-tools18) or `--sqlcmd <path>`. The helper applies
every `./sql/*.sql` in numeric order (including `14_UsageEventOwnership.sql` and
`22_UsageReportMarkers.sql` — both idempotent and harmless on a fresh DB).

Decision points:

- **Azure SQL?** SKIP `17_AppSecurity.sql` (it runs `CREATE LOGIN` and
  `GRANT VIEW SERVER STATE`, which Azure SQL rejects) — move it out of `./sql/`
  first, then create a contained user and grant it
  `SELECT/INSERT/UPDATE/DELETE/EXECUTE` on `SCHEMA::licentia` only.
- **Self-hosted SQL Server?** Run all of them. `17_AppSecurity.sql` creates
  the least-privilege `licentia_app` login (dev password `Licentia!App#Dev1`);
  set a strong password and use that (**not** `sa`) in `LICENTIA_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 `LICENTIA_CONNECTION` is the runtime one (`licentia_app`). Ask
  the user for the admin credential if you don't have it — keep it out of chat.

## 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 licentia-api`.
A common first-boot failure is the Production secret guard rejecting a
missing/default secret or an `sa` connection string — fix `.env` and retry.

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

The API exposes a public `GET /api/health` that includes a SQL readiness
check. Use the port from `LICENTIA_API_PORT` (default 8080):

```bash
for i in $(seq 1 30); do
  curl -fsS http://localhost:8080/api/health >/dev/null 2>&1 && break
  sleep 2
done
curl -fsS http://localhost:8080/api/health   # expect {"status":"Healthy","checks":{"sql":"Healthy"}}
```

If it never reports `Healthy` with `"sql":"Healthy"`, it's almost always
`LICENTIA_CONNECTION` or an un-applied schema. Diagnose from
`docker compose logs licentia-api`.

## Step 8 — Smoke test: issue a first license (YOU, autonomously)

Mint a short-lived **Admin** HS256 token (`iss=LicentiaCoreOEM`,
`aud=LicentiaCoreOEM`, `scope=Admin`) signed with the JWT signing key you
generated in step 4, then issue a license for the seeded demo partner/tenant
(`07_SeedData.sql` creates `PartnerId=1`, `TenantId=1` on a fresh DB). Read
the key from `.env` in-process — **do not print it or the token**.

```bash
python - <<'PY'
import 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["JWT__SIGNINGKEY"].encode()
port = env.get("LICENTIA_API_PORT", "8080")
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":"LicentiaCoreOEM","aud":"LicentiaCoreOEM","scope":"Admin",
                      "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({"PartnerId":1,"TenantId":1,"LicenseType":"Demo","Plan":"Evaluation",
                   "DurationDays":30,"MaxTenants":1}).encode()
req = urllib.request.Request(f"http://localhost:{port}/api/licenses", data=body, method="POST",
  headers={"Content-Type":"application/json","Authorization":f"Bearer {token}"})
resp = json.load(urllib.request.urlopen(req))
d = resp.get("data", resp)
print("Smoke issue OK — licenseId:", d.get("licenseId"), "status:", d.get("status"))
PY
```

A `201` with a `licenseId` / `licenseKey` means auth, the database, and the
audit chain all work end-to-end. (A `401` means the token/key don't match —
re-check step 4. If the seeded ids differ, list them with an admin
`GET /api/admin/tenants`.)

## Step 9 — Report success (YOU)

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

- Docker services running (`docker compose ps`).
- Health: `/api/health` reports `Healthy` with `sql` Healthy.
- Smoke test issued a license (`licenseId`).
- API base URL (e.g. `http://localhost:8080`) and where to change the port.
- Posture: which SQL edition they're on and therefore Demo vs Production.
- Reminders: `.env` + `secrets/` hold secrets and are git-ignored; copy the
  three HMAC secrets into their secret manager; the audit key must match
  across api/worker/dashboard; pin `LICENTIA_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 DB values | — | ✅ runtime connection string + admin DB credential; choose posture |
| 3 Create `.env` | ✅ create + prefilled values | ✅ writes `LICENTIA_CONNECTION` into `.env` |
| 4 Generate JWT + webhook + audit secrets + offline keypair | ✅ generate + write, never print | — |
| 5 Create DB + apply schema | ✅ run scripts | ✅ provide admin DB credential |
| 6 `docker compose up -d` | ✅ | — |
| 7 Poll health | ✅ | — |
| 8 Smoke test (issue license) | ✅ mint Admin token + issue | — |
| 9 Report success | ✅ | — |
