# Idle-service wake client

Wait for an **identifiable application response** after a free-tier Bex web
service wakes from idle hibernation. The first public request hits the
activator and receives `503` + `Retry-After: 5` +
`{"error":"service hibernated","retryAfter":5}` — it is not forwarded to your
app. The activator's own `GET /healthz` returns `200` even while application
routes are still waking.

Source pin: Bex `0b2421de955618e334936ede506b502f1413cad1`.

Public download (no private CMS checkout):
[`/examples/idle-service-client/`](https://bex.co/examples/idle-service-client/README.md).

Guide: [Recover when an idle web service wakes](https://bex.co/docs/platform/idle-services).

## Files

| Path | Role |
| --- | --- |
| `wake.mjs` | CLI entry: load config, wait, print JSON, exit codes |
| `lib/client.mjs` | Bounded GET loop, wake classification, cancellation |
| `lib/wake-contract.mjs` | Source-shaped wake JSON/HTML strings |
| `fixtures/stack.mjs` | **Protocol fixtures** (local HTTP, not a live Bex service) |
| `test/client.test.mjs` | Node test runner for the client against fixtures |

## Offline protocol fixtures

```bash
# Eventual readiness after hibernated wake responses
BEX_IDLE_FIXTURE=1 node wake.mjs

# Activator /healthz stays 200 while /ready still wakes — client targets /ready
BEX_IDLE_FIXTURE=1 BEX_IDLE_FIXTURE_MODE=healthz-always-ok node wake.mjs

# Permanent unrelated 503
BEX_IDLE_FIXTURE=1 BEX_IDLE_FIXTURE_MODE=permanent-unrelated-503 node wake.mjs

# Auth refusal
BEX_IDLE_FIXTURE=1 BEX_IDLE_FIXTURE_MODE=auth-401 node wake.mjs

# Malformed 503 body
BEX_IDLE_FIXTURE=1 BEX_IDLE_FIXTURE_MODE=malformed-wake node wake.mjs
```

These fixtures only prove client/protocol handling. They do not operate a Bex
cluster.

Run the bundled checks:

```bash
node --test test/client.test.mjs
```

## Live usage

```bash
BEX_APP_URL='https://YOUR_SERVICE.host/ready' \
  BEX_EXPECT_JSON='{"ok":true}' \
  node wake.mjs
```

Optional:

| Env | Default | Meaning |
| --- | --- | --- |
| `BEX_EXPECT_BODY` | — | Substring match when not using JSON |
| `BEX_EXPECT_STATUS` | `200` | Required status |
| `BEX_IDLE_DEADLINE_MS` | `120000` | Overall budget |
| `BEX_IDLE_REQUEST_TIMEOUT_MS` | `10000` | Per-request cap |
| `BEX_IDLE_MAX_ATTEMPTS` | `40` | Attempt cap |
| `BEX_IDLE_MAX_BYTES` | `65536` | Response size cap |
| `BEX_AUTHORIZATION` | — | Optional `Authorization` header (never printed) |

Bare `/healthz` URLs are refused unless `BEX_ALLOW_HEALTHZ=1` **and** you still
set a body predicate you control.

Redirects are not followed (`redirect: "error"`); authorization is never
forwarded to another origin.

## Exit codes

| Code | Meaning |
| --- | --- |
| 0 | Application matched the success predicate |
| 1 | Persistent unavailability / transport |
| 2 | Configuration error |
| 3 | HTTP 401/403 |
| 4 | Deadline or cancellation |
| 5 | Malformed wake or unexpected response |
