# Client integration

A walkthrough for the team running an external ERP that needs to consume
configs from this service.

## Issuing a token

1. Sign in to your SGApps.IO desk and launch the **ERP System Config**
   app.
2. Open the project you want the ERP to read (or create one — projects
   are folders under `/apps/site-builder/user/{you}/`).
3. In the **Tokens** panel of the project overview, click **+ New token**.
   Fill in:
   - **Name** — a label so you can revoke the right one later
     (e.g. `cluj-erp-prod`).
   - **Allowed repos** — comma-separated `repoId` list. Leave empty to
     grant access to all your projects (not recommended).
   - **Expires at** — optional date. Leave empty for non-expiring tokens.
   - **Require fingerprint** — turn on for production ERPs (see below).
4. The token value appears once in a green box. **Copy it now** — the
   server does not store the plain value retrievably; if you lose it, you
   have to issue a new one.

## Polling pattern

Use `commit.short_id` as an ETag-style cache key. Pseudocode:

```javascript
let lastShortId = null;
let cachedTree = null;
let cachedFiles = {};

async function refreshConfigIfChanged() {
    const branches = await fetch(
        '/site-builder/api/erp-config/projects/' + REPO_ID +
        '/repository/branches',
        { headers: { 'PRIVATE-TOKEN': TOKEN } }
    ).then(r => r.json());

    const master = branches.find(b => b.name === 'master');
    if (master.commit.short_id === lastShortId) {
        return cachedFiles; // no change
    }

    // Content changed — re-fetch tree + files.
    lastShortId = master.commit.short_id;
    cachedTree = await fetch(
        '/site-builder/api/erp-config/projects/' + REPO_ID +
        '/repository/tree?ref=master&recursive=1',
        { headers: { 'PRIVATE-TOKEN': TOKEN } }
    ).then(r => r.json());

    cachedFiles = {};
    for (const entry of cachedTree.filter(e => e.type === 'blob')) {
        cachedFiles[entry.path] = await fetch(
            '/site-builder/api/erp-config/projects/' + REPO_ID +
            '/repository/files/' + encodeURIComponent(entry.path) +
            '/raw?ref=master',
            { headers: { 'PRIVATE-TOKEN': TOKEN } }
        ).then(r => r.text());
    }
    return cachedFiles;
}

// Call from your existing refresh tick (every N seconds is fine — the
// branches endpoint is cheap; full re-fetch only fires when short_id changes).
```

### Recommended cadence

| ERP role | Refresh interval | Why |
|---|---|---|
| User-facing app (pricing, taxonomy) | 30s – 5min | Quick reaction to operator changes |
| Background batch (BOM expansion) | At job start + every 15min | Cold reads, no need to react fast |
| Reporting / analytics | At job start only | Configs are reference data; one read per run is enough |

## Fingerprint binding

For production ERPs, turn on **Require X-Instance-Id fingerprint match**
when issuing the token. Then on the client:

1. Compute a stable `instanceId` for the ERP host. Good sources:
   - Container ID + image digest (Docker).
   - Hardware-bound UUID (`/etc/machine-id` on Linux).
   - Cloud instance ID (AWS instance-id, GCP instance UUID).
2. Send it on every request as `X-Instance-Id: <id>`.
3. The first request binds the fingerprint server-side. Subsequent
   requests must match `crypto.timingSafeEqual` — a leaked token used
   from any other host returns `401 FINGERPRINT_MISMATCH`.

If you migrate the ERP to a new host, you have two options:
- **Easy path** — revoke the old token and issue a new one with the new
  fingerprint. Trades downtime for simplicity.
- **Maintained path** — disable `fingerprint_required` on the token
  (`PATCH /site-builder/api/tokens/{id}`), let the new instance bind, then
  re-enable. Requires a session with the owner's credentials.

## Error recovery

| Status | Recovery |
|---|---|
| `401` (token expired / fingerprint mismatch) | Page the on-call. Don't retry — won't change. Issue a fresh token from the webapp. |
| `403` (token doesn't cover repo) | The webapp operator added a new repo but forgot to extend the token. `PATCH /tokens/{id}` with the new `repos[]`. |
| `404` (project missing) | Webapp operator deleted the project. Don't auto-create — alert. |
| `5xx` | Transient. Retry with exponential backoff. Keep serving the last-known-good cache to keep the ERP alive. |

> **Rule of thumb.** Treat the config-store as eventually-consistent
> reference data. Always have a last-known-good in-memory cache so a brief
> service outage on this side doesn't take your ERP down.

## Token rotation

Periodic rotation (e.g. every 90 days):

1. Issue a new token via the webapp. Same name + suffix `-v2` is fine
   for tracking.
2. Roll the new value into your ERP's secret store. Restart the ERP — it
   now uses the new token.
3. Verify in the webapp Tokens list that the old token's last-use
   timestamp is stale, then revoke it.

If you need zero-downtime rotation across many ERPs:

1. Issue the new token. Keep the old one active.
2. Roll new value to all ERPs over a deployment window.
3. After the rollout, revoke the old token. Any ERP still on the old
   value gets `401` and pages the operator (your detection signal).
