# Registry Migration v1/v2 → v3

The commercial platform moved from an Albanian domain model to an English one.
This document is about doing that to a registry that holds real, paying clients
without losing a single credential.

## You may not need to run it

The reader accepts **v1, v2 and v3** and normalises all of them to v3 in memory.
A legacy registry keeps working indefinitely:

```
READ      v1 · v2 · v3
INTERNAL  v3 only
WRITE     v3 only
```

Nothing is rewritten on read. Any *new* write produces v3. So the only reason to
run the migration is to have the file itself in the English shape.

## Field mapping

| v1 / v2 | v3 |
|---|---|
| `klientet` | `clients` |
| `emri` | `name` |
| `abonimet` | `subscriptions` |
| `produkti` | `product` |
| `vecorite` (v1, client-level) | one implicit `live_intelligence` subscription |
| `vecori_shtese` | `additional_features` |
| `origjinat` | `allowed_origins` |
| `gjendja` / `pezulluar` | `status` |
| `nga` | `starts_at` |
| `deri` / `skadon` | `expires_at` |
| `krijuar` | `created_at` |
| `rrotulluar` | `rotated_at` |
| `perdorur_here_fundit` | `last_used_at` |
| `prefiksi` | `token_prefix` |
| `kripe` | `token_salt` |
| `hash` | `token_hash` |
| `veprimet` | `operations` |

v1 held one implicit Live Intelligence entitlement on the client itself
(`vecorite` + `skadon`). It is read as a single subscription with
`id: "v1"` so a v1 client keeps exactly what it paid for.

## What must survive exactly

```
client ids · subscription ids · token salts · token hashes · token prefixes
allowed origins · expiry instants · statuses · additional features
created_at · rotated_at · audit history
```

Tokens are **never** regenerated, rehashed, rotated or re-salted. A client that
authenticated before the migration authenticates identically after it — and
that is checked, not assumed.

## Procedure

```
node live-intelligence/migrate-registry-v3.js --check     report only, writes nothing
node live-intelligence/migrate-registry-v3.js --apply     migrate after the checks pass
```

Nothing is replaced until the last step:

1. back up the existing file byte for byte, next to it, timestamped
2. hash the backup (sha256, printed)
3. read it with the legacy-aware reader
4. normalise to v3 in memory
5. write a v3 **candidate** to `clients.json.v3-candidate`
6. read the candidate back
7. compare field by field — client count, declared `schema_version`, the English
   collection, zero legacy keys, every client's fingerprint, audit entry count,
   and that the backup itself is unchanged
8. **only then** rename the candidate over the original

Any failure aborts before step 8 and leaves the original in place. The script
also refuses to write an empty v3 file if the reader found no clients — that
would be the one mistake you could not undo.

### A worked run

```
REGISTRY MIGRATION v1/v2 -> v3   (APPLY)
==================================================================
  registry : …/live-intelligence/var/clients.json
  backup   : clients.json.v2-backup-2026-10-08T08-01-31-215Z
  sha256   : 4f1c…
  detected : schema v2
  clients  : 1
  audit    : 12 entries (left untouched)

  SEMANTIC IDENTITY
    ok   client count
    ok   declared schema_version is 3
    ok   the English collection is used
    ok   zero legacy schema keys
    ok   client cl-1 identical
    ok   audit history still readable
    ok   the backup is untouched

  All identity checks passed.
  MIGRATED — clients.json is now schema v3.
```

Verified afterwards on a real v2 fixture: the same plaintext token still
authenticates, the client id and both subscription ids are unchanged, the salt
is byte-identical, `advanced_stats` is still granted, and
`market_engine` still reports `EXPIRED` while `live_intelligence` stays active.

## Rolling back

Copy the timestamped backup over `clients.json`. The reader accepts v2, so the
service works immediately with no other change.

## Deprecated shims — remove after the gates run in production

| file | replaced by |
|---|---|
| `klientet.js` | `clients.js` |
| `licenca.js` | `licensing.js` |

Both hold **no logic** — they re-export the real module under the old names so
an out-of-tree `require` does not break during the migration window. The gate
asserts they contain no control flow and no crypto, because a shim that grows
logic is the second implementation this migration removed.

### Deprecated routes

| old | new |
|---|---|
| `/admin/gjendja` | `/admin/state` |
| `/admin/hyrje` | `/admin/login` |
| `/admin/fjalekalimi` | `/admin/password` |
| `/admin/licenca` | `/admin/license` |
| `/admin/klientet` | `/admin/clients` |
| `/admin/klient/abono` | `/admin/clients/subscribe` |
| `/admin/klient/zgjat` | `/admin/clients/extend` |
| `/admin/klient/gjendja` | `/admin/clients/status` |
| `/admin/klient/vecori` | `/admin/clients/feature` |
| `/admin/klient/origjinat` | `/admin/clients/origins` |
| `/admin/klient/rrotullo` | `/admin/clients/rotate-token` |
| `/klient` | `/client` |
| `/klient/gjendja` | `/client/state` |

They resolve to the English handler rather than duplicating it. Request bodies
also accept the legacy field names for the same window.

`/admin/klient/fshi` has **no** replacement: v3 has no hard-delete flow.

### Environment variables

| legacy | canonical |
|---|---|
| `LI_LICENCA_TOKEN` | `LI_LICENSE_TOKEN` |
| `LI_LICENCA_ORIGJINAT` | `LI_LICENSE_ORIGINS` |
| `LI_LICENCA_SKADON` | `LI_LICENSE_EXPIRES_AT` |
| `LI_LICENCA_VECORITE` | `LI_LICENSE_FEATURES` |
| `LI_LICENCA_KLIENTI` | `LI_LICENSE_CLIENT` |
| `LI_KLIENTET_PATH` | `LI_CLIENTS_PATH` |

Both sets are read; only the English ones are written. When the admin panel
saves a license it **blanks the legacy keys in the same pass**, so a stale
Albanian token cannot keep granting access after a change.

Note for anyone writing a test: because both sets are read, a test that removes
only the English keys is still measuring a license it believed it had removed.

## The public API will not carry legacy names

Deprecated aliases exist for the admin and client panels during the migration
window only. Public API v1 exposes the v3 English vocabulary and nothing else.
