# Business Metrics credentials — AdCenter keys + RO DB users (TW-295)

**Audience:** Platform, Infra/Security, AdCenter ops  
**Service:** `api.lucos.com`  
**Related:** [TW-295](https://admedia-jira.atlassian.net/browse/TW-295) · [TW-284](https://admedia-jira.atlassian.net/browse/TW-284) · [TW-301](https://admedia-jira.atlassian.net/browse/TW-301) (AdCenter key minting) · [TW-291](https://admedia-jira.atlassian.net/browse/TW-291)–[TW-294](https://admedia-jira.atlassian.net/browse/TW-294)  
**TW-301 ops runbook:** [ADCENTER-LEVEL1-OPS.md](./ADCENTER-LEVEL1-OPS.md)  
**Staging fixtures (IDs only):** [fixtures/adcenter-staging.json](./fixtures/adcenter-staging.json)

---

## Goal

Lucos connectors resolve **vaulted downstream credentials** only **after** tool RBAC allows a call. Users never supply AdCenter or DB passwords. Staging and production use separate secrets.

Current backend: **env-backed adapter** (host `.env` + dotenv / PM2), matching existing Lucos deploy. The service API (`getAdCenterLevel1Key`, `getRoDbCredential`) is stable so Infra can later swap in HashiCorp Vault or AWS Secrets Manager without changing connectors.

---

## Credential types

| Type | Purpose | Env vars |
| --- | --- | --- |
| AdCenter level-1 `X-API-KEY` | Map authorized `advertiser_id` → key (AdCenter does not accept `advertiser_id` as a query param) | `ADCENTER_LEVEL1_KEYS_STAGING` / `ADCENTER_LEVEL1_KEYS_PROD` |
| AdCenter base URL | Future HTTP connector | `ADCENTER_API_BASE_URL_STAGING` / `_PROD` |
| RO DB users | SELECT-only on approved MNGT / cake connections | `RO_DB_CREDENTIALS_STAGING` / `RO_DB_CREDENTIALS_PROD` |

### AdCenter key JSON shape (`advertiser_id` → level-1 key)

This is the locked mapping contract for `adcenter_get_report` (and other AdCenter tools):

```json
{ "19880": "<level-1-key-for-advertiser>" }
```

Canonical staging synthetic advertiser id: **19880** (see fixtures file). Prod map stays `{}` until staging exit.

### RO DB credentials JSON shape

```json
{
  "mngt_admin_ro": {
    "host": "db.example.internal",
    "port": 3306,
    "user": "lucos_mngt_ro_staging",
    "password": "<secret>",
    "database": "mngt"
  },
  "cake_ro": {
    "host": "db.example.internal",
    "port": 3306,
    "user": "lucos_cake_ro_staging",
    "password": "<secret>",
    "database": "cake"
  }
}
```

**Approved connection names:** `mngt_admin_ro`, `cake_ro`.  
**Reserved:** `mngt_harvest_ro` (metadata-only harvest in P1 — separate from row-query RO users).

Placeholders live in [`.env.example`](../.env.example). Never commit real secrets.

---

## Ownership

| Step | Owner |
| --- | --- |
| Mint dedicated Lucos AdCenter **level-1** keys (staging first; prod after staging exit) | AdCenter ops ([TW-301](https://admedia-jira.atlassian.net/browse/TW-301) / [ADCENTER-LEVEL1-OPS.md](./ADCENTER-LEVEL1-OPS.md)) |
| Synthetic advertiser + campaign/creative fixture IDs | AdCenter ops (document IDs in [fixtures/adcenter-staging.json](./fixtures/adcenter-staging.json)) |
| Create RO SQL users / grants / views | DBA (P4/P5 tickets) |
| Inject JSON into Lucos host env (staging ≠ prod) | Platform / Infra |
| Call credential helpers only after RBAC allow | Gateway / future connectors |

### Staging fixtures (non-secret)

| Field | Staging value |
| --- | --- |
| `advertiser_id` | `19880` |
| `campaign_id` / `creative_id` | Fill after mint via smoke discovery — see fixtures JSON |
| Key map env | `ADCENTER_LEVEL1_KEYS_STAGING` |
| Base URL env | `ADCENTER_API_BASE_URL_STAGING` |

Operator scripts (secrets via env only):

- `scripts/mint-adcenter-level1-key.sh`
- `scripts/smoke-adcenter-level1.sh`

---

## Deploy injection (staging / prod)

1. On the Lucos API host, edit the process `.env` (same path used by dotenv / PM2 today).
2. Set staging maps only on the staging host; prod maps only on the prod host. Do **not** copy staging keys into prod.
3. Restart the gateway process so env is reloaded, e.g. `pm2 restart` for the `api.lucos.com` app.
4. Confirm connectors fail closed when a key/connection is missing (no Basic-auth fallback).

Docker Compose uses `env_file: .env` — same JSON vars apply.

---

## Rotation

1. Mint a replacement level-1 key (AdCenter ops) or rotate the RO DB password (DBA).
2. Update the corresponding JSON value in host `.env` (`ADCENTER_LEVEL1_KEYS_*` or `RO_DB_CREDENTIALS_*`).
3. Restart the gateway process.
4. Revoke / drop the old key or password after verifying tool calls succeed.
5. Keep staging and prod rotations independent.

---

## Forbidden for Lucos

- Admin **Basic** authentication with **`X-API-ADVID`** (grants elevated AdCenter access)
- UI-generated **level-2** keys
- Sharing staging credentials in production env
- Writing keys/passwords into git, structured logs, or `tool_audit_logs` params
- Falling back to admin credentials when a vault lookup fails

Connectors must use `buildAdCenterAuthHeaders(level1Key)` (emits only `X-API-KEY`) and `assertSafeAdCenterAuth(headers)` before outbound calls. See `src/services/tool-credentials.service.js`.

---

## Code API (for future connectors)

```js
const {
  getAdCenterLevel1Key,
  getRoDbCredential,
  buildAdCenterAuthHeaders,
  assertSafeAdCenterAuth
} = require('../services/tool-credentials.service');

// After requireToolAccess allow:
const key = getAdCenterLevel1Key({ environment: 'staging', advertiserId: 19880 });
const headers = buildAdCenterAuthHeaders(key);
assertSafeAdCenterAuth(headers);

const ro = getRoDbCredential({ connection: 'mngt_admin_ro', environment: 'staging' });
// use ro.host / ro.user / ro.password — never pass into writeToolAudit params
```

---

## Isolation checklist

- [ ] Staging host has only `*_STAGING` secrets populated with staging values
- [ ] Prod host has only `*_PROD` secrets; values differ from staging
- [ ] No Basic / `X-API-ADVID` / level-2 material in env
- [ ] `.env` is not committed; `.env.example` has placeholders only
- [ ] Audit and logs never contain raw keys or DB passwords
