# Adding a carrier

A carrier is a `CarrierAdapter` (`packages/carriers/src/types.ts`). Everything above the adapter —
API, events, SDK, docs — is carrier-neutral, so a new carrier is one adapter, one fake, and one test file.

## 1. Research first
Write `docs/research/<carrier>.md` from the carrier's **official** docs: auth, SIP connectivity
(does it REGISTER? IP auth? digest on INVITE? origination URI?), number search/order/release,
regulatory requirements and documents, webhook signature scheme, health APIs. Cite a URL for every
fact. Anything you could not confirm becomes a `// TODO(verify): …` comment in code and a line in
`STATUS.md`.

## 2. Implement the adapter
`packages/carriers/src/adapters/<carrier>.ts`, modelled on `mock.ts`:

| Member | Notes |
|---|---|
| `settingsSchema` | Non-secret settings (Zod, with defaults). Returned by the API. |
| `credentialsSchema` | Secrets. Encrypted at rest (AES-256-GCM envelope), never returned or logged. |
| `capabilities(settings)` | `authMode` (`register`/`ip_auth`/`credentials`), `inboundMode`, and one flag per provider-API feature. The CRM hides features whose flag is false. |
| `validateCredentials` | Cheapest authenticated call. Throw `CarrierAuthError` on 401/403. |
| `provision` / `deprovision` | Create/remove what the carrier needs (connection, trunk, credential list, ACL, origination URI pointing at `ctx.platform.carrierIngress`, webhook URL `ctx.platform.webhookUrl`). **Idempotent**: when `providerRefs` exist and still resolve, return them unchanged. Deprovision must tolerate missing resources. |
| `gateway` | Provider-neutral `GatewaySpec`; core renders the FreeSWITCH gateway XML. Use `ctx.http.baseUrl('<carrier>-sip', default)` for the SIP host so tests can point it at the simulator. |
| `providerHealth` | Only when the provider exposes registration/trunk status. |
| Numbers, requirements, documents | Map provider statuses onto the normalized enums in `types.ts`. Put provider-only data in `providerExtras`. Throw `CapabilityNotSupportedError` for anything the carrier cannot do — the API turns it into `CAPABILITY_NOT_SUPPORTED`. |
| `verifyWebhook` / `normalizeWebhook` | Verify over the raw body; never throw on attacker input. Emit `order.updated`, `requirement_group.updated`, `document.updated` or `ignored`. |

Rules: every HTTP call goes through `providerRequest()` (errors are built from the provider's
error body only, so credentials cannot leak); base URLs go through `ctx.http.baseUrl(key, default)`.

Register the adapter in `packages/carriers/src/registry.ts` and add the type to `CarrierType`
in `types.ts` and to the `CarrierType` enum in `apps/api/src/schemas/carriers.ts`.

## 3. Emulate it
Add `tools/fake-provider/src/<carrier>.ts` with the subset of the provider API the adapter uses
(same paths, field names, error shapes as the research). SIP credentials it issues go into
`sipRegistry` so the simulator FreeSWITCH accepts the platform's REGISTER/INVITEs. `advance()`
completes orders, decides reviews and sends signed webhooks.

## 4. Test it
`packages/carriers/test/<carrier>.test.ts`:
```ts
runAdapterContract('<carrier>', { adapter, ctx, badCtx, secrets, signWebhook, advance, regulated });
```
The shared contract (`packages/carriers/test/contract.ts`) checks capabilities/gateway consistency,
credential validation, idempotent provisioning, numbers, requirements, webhook verification and that
no secret ever appears in an error.

Then add the carrier to `tests/e2e/lib/carriers.ts`. The end-to-end suite connects it, places an
outbound call from a browser agent through it to the simulator, receives an inbound call from the
simulator on a DID, revokes it, and checks the service logs for leaked secrets
(`tests/e2e/lib/secrets.txt`). Point the carrier's API and SIP host at the fakes with
`PROVIDER_API_OVERRIDES` in `.env`.

## 5. Live test (optional)
Clear `PROVIDER_API_OVERRIDES`, connect a real account through `POST /v1/carriers`, and follow
`docs/operations.md` → "Live carrier check".
