@crvouga/mockingbird-service-aha 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +173 -0
- package/dist/chunk-HEWJBQGW.js +3241 -0
- package/dist/chunk-HEWJBQGW.js.map +7 -0
- package/dist/chunk-VJVM4I3P.js +398 -0
- package/dist/chunk-VJVM4I3P.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1025 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1328 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +7 -0
- package/dist/sftp.d.ts +78 -0
- package/dist/sftp.js +356 -0
- package/dist/sftp.js.map +7 -0
- package/package.json +97 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @crvouga/mockingbird-service-aha
|
|
2
|
+
|
|
3
|
+
Stateful mock of the **AHA (Advanced Health Academy) at-home phlebotomy** partner API for test
|
|
4
|
+
suites: HMAC-signed create-order and cancel, and — its main job — the order-status webhooks AHA
|
|
5
|
+
posts back. The vendor has no pull API, so every downstream effect (EMR appointment booking,
|
|
6
|
+
storefront status, "blood drawn") starts with a webhook; the mock emits one on demand, with every
|
|
7
|
+
field our handler reads, so the ZIP-routed bloodwork path can finally be tested.
|
|
8
|
+
|
|
9
|
+
- Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/aha/SUPPORT.md)
|
|
10
|
+
- The vendor publishes no spec: the contract (`openapi.yaml`) is hand-authored from our
|
|
11
|
+
consumers' zod schemas and wire shapes.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -D @crvouga/mockingbird-service-aha
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
|
|
20
|
+
`npx mockingbird-aha serve`, `createServer` from `./server` (Node), or `createRuntime` with any
|
|
21
|
+
Fetch server.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Point the app at the mock:
|
|
26
|
+
|
|
27
|
+
| Env | Value |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `AHA_API_URL` | `http://127.0.0.1:8799` (the lab-provider path already allows loopback; `AhaService` needs an http exception, see G-A1 / S10.2) |
|
|
30
|
+
| `AHA_API_KEY` / `AHA_API_SECRET` | anything, or the pair passed as `--api-key` / `--api-secret` to verify signatures exactly |
|
|
31
|
+
| `AHA_USE_LEGACY_AUTH` | `true` switches to `X-Geviti-Auth-Key`; both modes are accepted |
|
|
32
|
+
| `AHA_WEBHOOK_SECRET` | the same value as `--webhook-secret` |
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx mockingbird-aha serve --port 8799 \
|
|
36
|
+
--webhook-url http://127.0.0.1:3000/bloodwork/aha-webhook \
|
|
37
|
+
--webhook-secret "$AHA_WEBHOOK_SECRET" \
|
|
38
|
+
--api-key "$AHA_API_KEY" --api-secret "$AHA_API_SECRET" \
|
|
39
|
+
--envelope raw --auto-schedule 2000
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createRuntime } from "@crvouga/mockingbird-service-aha"
|
|
44
|
+
|
|
45
|
+
const aha = createRuntime({
|
|
46
|
+
webhooks: { url: "http://127.0.0.1:3000/bloodwork/aha-webhook", secret: "aha-webhook-secret" },
|
|
47
|
+
})
|
|
48
|
+
const admin = (path: string, body: unknown) =>
|
|
49
|
+
aha.fetch(
|
|
50
|
+
new Request(`http://aha.test/__admin${path}`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "content-type": "application/json" },
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// …the app's checkout calls POST /v1/geviti/create-order for GV-101…
|
|
58
|
+
|
|
59
|
+
// AHA books the draw: our handler books the EMR appointment for 10:30 Denver time.
|
|
60
|
+
await admin("/orders/GV-101/transition", {
|
|
61
|
+
status: "Scheduled",
|
|
62
|
+
scheduledAt: "2026-10-01T16:30:00Z",
|
|
63
|
+
timeZone: "America/Denver",
|
|
64
|
+
})
|
|
65
|
+
// The phlebotomist checks out with a sample: our handler sets vitalBloodDrawn.
|
|
66
|
+
await admin("/orders/GV-101/transition", { status: "Check Out", drawStatus: "Sample Collected" })
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Routes
|
|
70
|
+
|
|
71
|
+
| Route | Behaviour |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `POST /v1/geviti/create-order` | Validates the body (`partner_order_id`, patient fields, `biological_sex`, `service_type`, `npi`, `ordering_physician`, `test_codes`, optional `preferred_schedule_date/time`, `patient_timezone`). Create **or update**: a repeated `partner_order_id` keeps its `order_number`. Answers `{content: {partner_order_id, order_number}, message, status: "SUCCESS"}`. |
|
|
74
|
+
| `POST /v1/geviti/cancel` | `{partner_order_id, notes: [{note_type: "CANCELLATION", notes}]}` → `{message, status}`. Emits a `Cancelled` webhook (turn off with `cancelWebhook: false`). Unknown id → 404; an order whose sample was collected → 200 with `status: "ERROR"`. The AHA `order_number` is accepted in `partner_order_id` too, because our lab-provider client sends it there (G-A1). |
|
|
75
|
+
|
|
76
|
+
**Auth.** HMAC mode: `X-API-KEY`, `X-TIMESTAMP` (epoch ms, within ±5 min of wall-clock time),
|
|
77
|
+
`X-SIGNATURE` = base64 HMAC-SHA256(secret, `"<apiKey>:<path>:<timestamp>"`), where `path` is the
|
|
78
|
+
request path without host, body or `/ns/<name>` prefix. With a known key + secret
|
|
79
|
+
(`--api-key/--api-secret` or `credentials` in settings) the signature is verified exactly;
|
|
80
|
+
with none configured any key is accepted and the signature is checked for shape only. Legacy
|
|
81
|
+
mode: `X-Geviti-Auth-Key` (+ `X-API-Version: 1.0`). Failures are 401 `{status: "ERROR", message}`.
|
|
82
|
+
|
|
83
|
+
**Envelope (G-A1).** `AhaService` expects the raw `{content, message, status}`;
|
|
84
|
+
`AhaLabProvider` expects `{success: true, data: {…}}`. Raw is the default; choose with
|
|
85
|
+
`--envelope raw|wrapped` or per namespace with `PUT /__admin/settings {"envelope": "wrapped"}`.
|
|
86
|
+
|
|
87
|
+
**Idempotency.** `X-Idempotency-Key` (the lab-provider path): the same key and body replays the
|
|
88
|
+
stored response (`idempotent-replayed: true`); the same key with a different body is 409.
|
|
89
|
+
|
|
90
|
+
### Webhooks
|
|
91
|
+
|
|
92
|
+
`POST <webhook-url>` (our route: `POST /bloodwork/aha-webhook`) with
|
|
93
|
+
`Authorization: Token <AHA_WEBHOOK_SECRET>`. Every body has `status`, `partnerOrderId`, `ahaOrderId`,
|
|
94
|
+
plus, by status (all local times in the order's IANA zone):
|
|
95
|
+
|
|
96
|
+
| `status` | Extra fields |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `Scheduled`, `Rescheduled` | **`scheduleServiceTime`** (`YYYY-MM-DDTHH:mm:ss`, moment-parsable) and **`scheduleServiceTimeZone`** (IANA) — required by our handler though absent from the DTO — plus `scheduledServiceDate/Time/TimeZone` and `scheduleConfirmationDate/Time/TimeZone` |
|
|
99
|
+
| `Check In` | `checkInDate`, `checkInTime`, `checkInTimeZone` |
|
|
100
|
+
| `Check Out` | `drawStatus` (default `Sample Collected`), `drawStatusDate/Time/TimeZone` |
|
|
101
|
+
| `Lab Testing In Progress` | `dropOffDate`, `dropOffTime`, `dropOffTimeZone` |
|
|
102
|
+
| `Cancelled`, `Non Scheduled Update` | — |
|
|
103
|
+
|
|
104
|
+
`drawStatus` values: `Sample Collected`, `Completed` (drawn), `Patient Refused`, `UTO`,
|
|
105
|
+
`Patient Not Home`, `Patient Rescheduled`, `Order Cancelled`, `Others`,
|
|
106
|
+
`Patient Asked to Reschedule` (draw failed). Non-2xx answers are retried (immediately, 5 s,
|
|
107
|
+
5 min, 30 min, 2 h); `GET /__admin/webhooks`, `…/events`, `…/replay`, `…/flush` as usual.
|
|
108
|
+
|
|
109
|
+
### Admin (beyond the standard contract)
|
|
110
|
+
|
|
111
|
+
| Route | Effect |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `POST /__admin/orders/:partnerOrderId/transition` | `{status, drawStatus?, scheduledAt?, timeZone?}` emits the webhook. `status` is any value above (case and `_` forgiven; unknown values are sent verbatim). `scheduledAt` (ISO or epoch ms) defaults to the order's preferred slot, else the next hour 24 h out; `Rescheduled` defaults to one day later. `timeZone` defaults to the order's `patient_timezone`, else `America/New_York`. `:partnerOrderId` may also be the `order_number`. |
|
|
114
|
+
| `PUT /__admin/settings` | `{envelope?, credentials?: [{apiKey, apiSecret?}], allowLegacy?, timestampToleranceMs?, defaultTimeZone?, cancelWebhook?, autoSchedule?: {afterMs, leadMs?} \| ms \| null}` for the calling namespace. `GET` shows them with secrets masked. |
|
|
115
|
+
| `POST /__admin/tick` | Emit every `autoSchedule` webhook that is due on the mock clock (the served mock ticks every 100 ms). |
|
|
116
|
+
| `GET /__admin/orders` | The namespace's orders (ids, status, appointment, zone — no patient data). |
|
|
117
|
+
|
|
118
|
+
Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`): `bad_signature` (401),
|
|
119
|
+
`rate_limited` (429 → `rate_limit`), `server_error` (500), `order_error` (200 with inner
|
|
120
|
+
`status: "ERROR"`), `invalid_response` (200 with a body neither zod schema accepts),
|
|
121
|
+
`webhook_duplicate`, `webhook_reorder`, `webhook_drop`.
|
|
122
|
+
|
|
123
|
+
### Namespaces
|
|
124
|
+
|
|
125
|
+
`x-mockingbird-namespace`, a `/ns/<name>` prefix on `AHA_API_URL` (the signature still covers
|
|
126
|
+
only the path after it), or by API key:
|
|
127
|
+
`PUT /__admin/credentials {"credentials": {"<AHA_API_KEY>": "<namespace>"}}`.
|
|
128
|
+
|
|
129
|
+
### SFTP result delivery
|
|
130
|
+
|
|
131
|
+
`createAhaSftpServer` from `./sftp` starts a real SSH/SFTP server on an ephemeral port and shares order state with `createRuntime`. It supports password or public-key authentication, host-key verification, `list`/`stat`, binary upload/download, atomic temp-file rename, delete, nested directories, stable POSIX permissions and mock-clock timestamps. The deterministic Ed25519 host key is stable between runs.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { createRuntime } from "@crvouga/mockingbird-service-aha"
|
|
135
|
+
import { createAhaSftpServer } from "@crvouga/mockingbird-service-aha/sftp"
|
|
136
|
+
|
|
137
|
+
const runtime = createRuntime()
|
|
138
|
+
const sftp = await createAhaSftpServer({
|
|
139
|
+
runtime,
|
|
140
|
+
accounts: [{ username: "aha", password: "local-test-password" }],
|
|
141
|
+
})
|
|
142
|
+
console.log(sftp.host, sftp.port, sftp.hostPublicKey)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use `seed()` to install arbitrary binary fixtures or `publishResult(orderId, bytes)` to place a stable `AHA-…_result.pdf` in `/outbox`. The first complete download moves the linked HTTP order to `Lab Testing In Progress` and emits its webhook; repeat polling/download does not repeat that transition. `fault()` forces the next operation to disconnect, deny permission, report disk-full, or accept only part of a write. `journal()` exposes paths, operation names, byte counts, and outcomes—never credentials or file contents. `reset(namespace?)` clears deterministic filesystem state. Duplicate destination names fail, so `.tmp` → final rename is atomic.
|
|
146
|
+
|
|
147
|
+
### Deliberately not modelled
|
|
148
|
+
|
|
149
|
+
- Downstream S3 ingestion and `aha_results_queue` processing after the SFTP handoff.
|
|
150
|
+
- Real scheduling: AHA contacts the patient; nothing moves unless a test transitions the order
|
|
151
|
+
or sets `autoSchedule`.
|
|
152
|
+
- The serviceable-ZIP list (our app's own fixture decides eligibility before calling AHA).
|
|
153
|
+
- Patient details are validated, never stored or echoed.
|
|
154
|
+
- Which envelope the real vendor uses (G-A1): both are served, one per namespace.
|
|
155
|
+
|
|
156
|
+
## API
|
|
157
|
+
|
|
158
|
+
| Export | Kind | Description |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `AhaAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `transition(id, {status, drawStatus?, scheduledAt?, timeZone?})`, `tick()`, `orders()`. Options: `sqlite`, `now`, `namespace`, `settings`, `onWebhook`, `wallClock`. |
|
|
161
|
+
| `createRuntime` | function | The mock with the full service contract. Options: `webhooks: {url, secret, retryDelaysMs?, fetch?}`, `settings`, `tickMs`, `wallClock`, `clock`, `seed`, `adminKey`, `onLog`. |
|
|
162
|
+
| `AHA_PRESETS` | object | Every named fault preset. |
|
|
163
|
+
| `AHA_NAMESPACE` | string | The service name, `"aha"`. |
|
|
164
|
+
| `WEBHOOK_PATH` | string | `"/bloodwork/aha-webhook"`, our receiver's route. |
|
|
165
|
+
| `ORDER_STATUSES`, `DRAW_STATUSES` | arrays | The vendor status spellings the webhooks use. |
|
|
166
|
+
| `verifyAuth` | function | The HMAC / legacy verification the mock applies (an error message, or `undefined`). |
|
|
167
|
+
| `apiKeyCredential` | function | The API key a request carries (how credentials map to namespaces). |
|
|
168
|
+
| `isTimeZone`, `zonedParts`, `zonedToEpoch` | functions | IANA-zone helpers used to fill the local date/time fields. |
|
|
169
|
+
| `document`, `operationIds`, `supportedOperationIds` | values | The vendored OpenAPI contract and its operation ids. |
|
|
170
|
+
| `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http` (autoSchedule ticks every 100 ms); the `serve` CLI target; port 8799. |
|
|
171
|
+
| `createAhaSftpServer` (`./sftp`) | Node | Real SSH/SFTP endpoint with deterministic host key/filesystem, shared order state, transfer controls, and an ephemeral port. |
|
|
172
|
+
|
|
173
|
+
Part of [mockingbird](https://github.com/crvouga/mockingbird).
|