# API reference

Two HTTP surfaces, mounted on the same SGApps.IO host:

| Surface | Prefix | Auth | Audience |
|---|---|---|---|
| **Read API** | `/site-builder/api/erp-config/` | `PRIVATE-TOKEN` header | External ERPs |
| **Admin API** | `/site-builder/api/` | Session cookie | The webapp UI |

## Read API (external — ERP clients)

GitLab v4-compatible shape — existing GitLab API clients work unmodified.

<div class="endpoint-table">

| Method | Endpoint | Returns |
|---|---|---|
| `GET` | `/site-builder/api/erp-config/projects/{repoId}/repository/branches?search=` | `[{name, commit:{short_id}}]` |
| `GET` | `/site-builder/api/erp-config/projects/{repoId}/repository/tree?ref=&path=&recursive=1` | `[{id, name, type, path, mode}]` |
| `GET` | `/site-builder/api/erp-config/projects/{repoId}/repository/files/{rawencoded-path}/raw?ref=` | Raw file body |
| `GET` | `/site-builder/api/erp-config/health` | `{status:"ok", time}` |

</div>

### Example — fetching a config

```bash
curl -H "PRIVATE-TOKEN: abc...xyz" \
     "https://sgapps.io/site-builder/api/erp-config/projects/b2b-cnc.erp-config/repository/files/operations.config.json/raw?ref=master"
```

### Example — listing branches

```bash
curl -H "PRIVATE-TOKEN: abc...xyz" \
     "https://sgapps.io/site-builder/api/erp-config/projects/b2b-cnc.erp-config/repository/branches"
```

Returns:

```json
[
  {
    "name": "master",
    "commit": { "short_id": "ac800ee0" }
  }
]
```

## Admin API (internal — webapp)

All endpoints are gated by the SGApps session cookie. `{owner}` is implicit
(taken from `session.user._id`); requests trying to touch another user's
projects return `403`.

<div class="endpoint-table">

| Method | Endpoint | Body / Query | Description |
|---|---|---|---|
| `GET` | `/site-builder/api/projects` | — | List your projects |
| `POST` | `/site-builder/api/projects` | `{name, type}` | Create new project |
| `GET` | `/site-builder/api/projects/{repoId}/branches` | — | List branches |
| `POST` | `/site-builder/api/projects/{repoId}/branches` | `{name}` | Create branch (empty folder) |
| `GET` | `/site-builder/api/projects/{repoId}/branches/{ref}/tree?path=&recursive=1` | — | Tree listing |
| `GET` | `/site-builder/api/projects/{repoId}/branches/{ref}/files/{path}` | — | Raw file body |
| `PUT` | `/site-builder/api/projects/{repoId}/branches/{ref}/files/{path}` | Raw body (octet-stream) | Save file, snapshot first |
| `GET` | `/site-builder/api/projects/{repoId}/branches/{ref}/history` | — | List snapshots |
| `POST` | `/site-builder/api/projects/{repoId}/branches/{ref}/rollback?to=v{NNNN}` | — | Restore snapshot (creates pre-rollback snapshot too) |
| `GET` | `/site-builder/api/tokens` | — | List your tokens (suffix only — full value never re-exposed) |
| `POST` | `/site-builder/api/tokens` | `{name, repos[], expires_at, fingerprint_required}` | Create token (returns `tokenString` **once**) |
| `PATCH` | `/site-builder/api/tokens/{tokenId}` | partial body | Update repos / expiry / fingerprint flag |
| `DELETE` | `/site-builder/api/tokens/{tokenId}` | — | Revoke token |

</div>

## Authentication

### `PRIVATE-TOKEN` (external read API)

Set on every request:

```
PRIVATE-TOKEN: <64-char hex token>
```

The server resolves the token to a record `{owner, repos[], expires_at,
fingerprint_required}`. Expired tokens reject with `401`. Requests for a
`repoId` not in the token's `repos[]` list reject with `403`.

### `X-Instance-Id` fingerprint binding

Optional defense against token leaks. When a token has `fingerprint_required:
true`:

1. On first use, the client sends `X-Instance-Id: <stable-id>` along
   with `PRIVATE-TOKEN`. The server stores the fingerprint.
2. On subsequent requests, the same `X-Instance-Id` must be sent. The
   server compares with `crypto.timingSafeEqual`. Mismatches reject with
   `401 FINGERPRINT_MISMATCH`.

A leaked token is then useless without the matching fingerprint. Pick a
fingerprint that's stable per instance (machine UUID, container ID, etc.)
but unguessable from outside.

### Session cookie (admin API)

The webapp authenticates via the standard SGApps session cookie set by
`/user/login`. There's nothing the API has to do — the existing
`SessionManager` middleware drops `req.session.data.loginData.local` into
the request before the handler runs.

## Cache versioning

The read API exposes `commit.short_id` per branch in the branches listing
response. It's the first 8 chars of `sha256(concat sortat după path de
"{path}|{size}|{md5}\n")` over all files in the live branch.

| Property | Value |
|---|---|
| Stability | Identical for unchanged content (deterministic) |
| Invalidation | Recomputed on next read after a `PUT` |
| Cost | One GridFS list per branch — no content reads |

Clients should use this as an ETag-like cache key — fetch the tree once,
remember the `short_id`, and skip re-fetching files until the `short_id`
changes.

## Error codes

| Status | When | Body |
|---|---|---|
| `400` | Malformed path / branch name / missing required field | `{message}` |
| `401` | Missing / invalid / expired token; session not logged in | `{message}` |
| `403` | Token doesn't cover requested repo; owner mismatch on admin route | `{message}` |
| `404` | Project / branch / file / snapshot not found | `{message}` |
| `409` | (Reserved for future optimistic-lock writes) | `{message}` |
| `500` | Storage error | `{message}` |

Errors are always JSON. The `message` field is human-readable but stable
enough to switch on (e.g. `401 FINGERPRINT_MISMATCH` for the fingerprint case).
