# Phase 1 — Site identity and signing-key lifecycle

## Status

Implemented and verified on 2026-07-19 for `absolutems`.

## Control-plane records

Phase 1 adds:

- `platform_sites`: immutable `site_id` binding to tenant, issuer and audience.
- `site_signing_keys`: versioned RS256 public keys with active/revoked state.
- `jwt_replay_records`: consumed JWT IDs retained until token expiry.

Apply the additive schema with:

```bash
cd /var/www/html/flask_server
venv/bin/python scripts/migrate_control_plane.py
```

## Bootstrap

The initial public-key PEM file is an explicit trust anchor. A valid token
signed by that key may create its site/key binding only when:

- issuer and audience match the Flask allowlist;
- the referenced tenant already exists and is active;
- `site_id`, tenant and key ID are not bound differently.

After bootstrap, authentication resolves the key from the control plane.
A key registered as revoked or inactive can never fall back to its bootstrap
PEM file.

## Rotation

WordPress:

1. Generates a new RSA key pair locally.
2. Signs the rotation request with its current key.
3. Sends only the new public key and new `kid`.
4. Adopts the new private key only after Flask confirms persistence.
5. Authenticates with the new key and revokes the previous key.

Both keys remain active during the brief handover so a lost HTTP response
cannot lock the site out.

Admin UI:

`WP Flask Bridge → Settings → Rotación de clave`

CLI:

```bash
wp ams-bridge rotate-key --path=/var/www/html/absolutems --allow-root
wp ams-bridge site-status --path=/var/www/html/absolutems --allow-root
```

## Replay protection

Every external control-plane JWT must contain a unique `jti`. On its first use,
the ID is persisted with the token expiry. A second request using the same JWT
returns HTTP 401 with `JWT replay detected`.

Verification:

```bash
wp ams-bridge replay-test --path=/var/www/html/absolutems --allow-root
```

Observed result:

- first request: HTTP 200
- same JWT reused: HTTP 401

## Tenant resolution

The gateway derives tenant identity from the verified JWT and resolves the
tenant through the control-plane `tenants` table. The previous hardcoded
`absolutems` routing resolver is now only a compatibility import to the
canonical database-backed resolver.

## Current AbsoluteMS state

- Site status: active
- Tenant: `absolutems`
- Issuer: `https://absolutems.com.au`
- Audience: `absolutems-api`
- Original development key: revoked
- Rotated key: active
- Authentication key source: control plane

## Deferred beyond Phase 1

- Subscription provider and signed billing webhooks.
- Distributed quota counters.
- App and feature entitlement synchronization into the WordPress Apps UI.
- Managed private-key storage instead of a WordPress database option.
