# usage-report

Read month-to-date workspace usage from `GET /v1/usage` and print a
per-resource table with a coverage banner. Refuses to present partial or
unknown coverage as a settled amount: non-complete totals are labeled
provisional, and absent `billing` is reported as "no billing contract data",
never as zero.

Standalone: Node 22 with no runtime dependencies. Copy this directory anywhere
and run `npm test` (or `npm ci && npm test` from the published archive).

## Fixture mode (no cloud)

```bash
BEX_USAGE_FIXTURE=1 node report.mjs
BEX_USAGE_FIXTURE=1 BEX_FIXTURE_SCENARIO=partial node report.mjs
BEX_USAGE_FIXTURE=1 BEX_FIXTURE_SCENARIO=unknown node report.mjs
npm run demo   # all three scenarios back to back
```

Scenarios: `complete`, `partial` (degraded sources + `through` watermark),
`unknown`. The fixture also answers `400` for a malformed `period` and `401`
for a missing or wrong token, mirroring the server.

## Authorized mode (live API)

```bash
export BEX_API_ORIGIN='https://api.bex.co'
export BEX_TOKEN='<short-lived bearer from the OAuth exchange>'
node report.mjs [--period YYYY-MM]
```

Authenticate as in [REST & GraphQL API](/docs/platform/api): an API key
exchanged for a short-lived Bearer [REDACTED] The token travels in an
environment variable only — never a flag — and is never printed. The read is
one bounded `GET /v1/usage` (finite timeout, no retries). `BEX_OWNER_ID`
optionally names another workspace; `--period` reads a past month (malformed
values fail locally before any request, matching the server's `400`).

Any workspace member can read usage. The `billing` projection (real cost,
invoices) appears only for billing/admin callers; everyone else sees the
advisory `estimatedCost` alone. See [Month-to-date usage](/docs/platform/usage)
for coverage states and estimate semantics.

## Files

| Path | Purpose |
| --- | --- |
| `report.mjs` | CLI entry: fixture/live mode, one read, one table |
| `lib/config.mjs` | Env + `--period` validation (`ConfigError`, exit 2) |
| `lib/http.mjs` | Bounded `apiGet` (`TransportError`, finite timeout, size cap) |
| `lib/coverage.mjs` | Pure coverage classifier — only `complete` is settled |
| `lib/report.mjs` | Usage fetch + table formatting |
| `fixtures/usage.mjs` | Local `GET /v1/usage` server (three scenarios + 400/401) |
| `scripts/demo.mjs` | Runs all three fixture scenarios |
| `test/report.test.mjs` | Period, coverage, fixture, CLI, and no-leak tests |
| `source-review.md` | Bex source map this example reproduces |

## Environment

| Variable | Required | Meaning |
| --- | --- | --- |
| `BEX_API_ORIGIN` | live mode | API base URL (`https://api.bex.co`; plain HTTP only for localhost fixtures) |
| `BEX_TOKEN` | live mode | Bearer [REDACTED] never logged |
| `BEX_OWNER_ID` | no | Query another workspace (403 unless a member) |
| `BEX_USAGE_FIXTURE=1` | fixture mode | Read from the local fixture server instead |
| `BEX_FIXTURE_SCENARIO` | no | `complete` (default), `partial`, or `unknown` |
| `BEX_REQUEST_TIMEOUT_MS` | no | Single-request bound (default `15000`) |
| `BEX_MAX_BYTES` | no | Response size cap (default `1048576`) |
