@crvouga/mockingbird-service-intercom 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 +218 -0
- package/dist/chunk-5RRE6WVT.js +364 -0
- package/dist/chunk-5RRE6WVT.js.map +7 -0
- package/dist/chunk-OSBG2XJ7.js +3835 -0
- package/dist/chunk-OSBG2XJ7.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1204 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1510 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +7 -0
- package/package.json +87 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# @crvouga/mockingbird-service-intercom
|
|
2
|
+
|
|
3
|
+
Stateful mock of the **Intercom REST API (version 2.11)** for test suites. It serves the calls
|
|
4
|
+
our backend makes:
|
|
5
|
+
|
|
6
|
+
- **contacts:** search, create (409 on duplicates, the way Intercom does), update and get;
|
|
7
|
+
- **conversations:** create (optional `Idempotency-Key`), update, reply as the member or as an
|
|
8
|
+
admin (JSON or multipart attachments), close/open/snooze/assign, get with
|
|
9
|
+
`?display_as=plaintext`, and cursor-paginated search;
|
|
10
|
+
- **identity:** `/admins` and `/me`.
|
|
11
|
+
|
|
12
|
+
Admin replies and close/open send Intercom's `notification_event` webhooks, signed with
|
|
13
|
+
`X-Hub-Signature: sha1=<hex HMAC-SHA1>`, to both of our receivers: the backend's
|
|
14
|
+
`POST /messaging/webhook` and the EMR's `POST /v1/webhooks/intercom`.
|
|
15
|
+
|
|
16
|
+
- Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/intercom/SUPPORT.md)
|
|
17
|
+
- The contract (`openapi.yaml`) is trimmed from Intercom's published 2.11 reference to what our
|
|
18
|
+
consumers use.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -D @crvouga/mockingbird-service-intercom
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
|
|
27
|
+
`npx mockingbird-intercom serve`, `createServer` from `./server` (Node), or `createRuntime` with
|
|
28
|
+
any Fetch server.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx mockingbird-intercom serve --port 8807 \
|
|
34
|
+
--webhook-url http://127.0.0.1:3000/messaging/webhook \
|
|
35
|
+
--emr-webhook-url http://127.0.0.1:4000/v1/webhooks/intercom \
|
|
36
|
+
--webhook-secret "$INTERCOM_WEBHOOK_SECRET"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Consumer | Setting |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| Member messaging adapter (`intercom-messaging.adapter.ts`) | `INTERCOM_API_BASE_URL=http://127.0.0.1:8807` (already read), `INTERCOM_ACCESS_TOKEN=<any>` |
|
|
42
|
+
| Intercom sync adapter (`intercom-api.adapter.ts`) | Seam **G-I1**: this adapter hardcodes `https://api.intercom.io` today, so it needs an env base URL. Its writes are also gated by `FEATURE_INTERCOM_SYNC_ENABLED`. |
|
|
43
|
+
| Both webhook receivers | `INTERCOM_WEBHOOK_SECRET` = `--webhook-secret` |
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createRuntime } from "@crvouga/mockingbird-service-intercom"
|
|
47
|
+
|
|
48
|
+
const intercom = createRuntime({
|
|
49
|
+
webhooks: {
|
|
50
|
+
urls: ["http://127.0.0.1:3000/messaging/webhook", "http://127.0.0.1:4000/v1/webhooks/intercom"],
|
|
51
|
+
secret: "whsec-test",
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
// …the member opens a conversation through the app (POST /conversations)…
|
|
55
|
+
|
|
56
|
+
// Support answers: appends an admin comment and fires the signed conversation.admin.replied.
|
|
57
|
+
await intercom.fetch(
|
|
58
|
+
new Request("http://intercom.test/__admin/conversations/215470000000001/admin-reply", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "content-type": "application/json" },
|
|
61
|
+
body: JSON.stringify({ adminId: "1000001", body: "Your results are in" }),
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Routes
|
|
67
|
+
|
|
68
|
+
Every route needs `Authorization: Bearer <token>`. Any non-empty token is accepted unless
|
|
69
|
+
`tokens` is set. `Intercom-Version` defaults to 2.11, and a malformed value answers 400
|
|
70
|
+
`intercom_version_invalid`. Every error uses Intercom's envelope:
|
|
71
|
+
`{type: "error.list", request_id, errors: [{code, message}]}`.
|
|
72
|
+
|
|
73
|
+
| Route | Behaviour |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `POST /contacts/search` | `{query, pagination?}` → `{type: "list", data, total_count, pages}`. See "Search" below. |
|
|
76
|
+
| `POST /contacts` | `{role, external_id?, email?, name?, phone?, signed_up_at?, custom_attributes?}` → the contact, whose `id` is 24 hex characters. A `user` needs an email or external_id. A second `user` with the same `external_id`, or the same email (case-insensitive), answers **409** `conflict`: "A contact matching those details already exists with id=…". The sync adapter then searches and PUTs. Leads never conflict. |
|
|
77
|
+
| `PUT /contacts/{id}` | Partial update. `custom_attributes` are merged. 404 `User Not Found`; 409 if the new identifiers collide. |
|
|
78
|
+
| `GET /contacts/{id}` | The contact (`name`, `email`, `external_id`, …), or 404. |
|
|
79
|
+
| `POST /conversations` | `{from: {type: "user", id}, body}` → `{type: "user_message", id, created_at, body, message_type: "inapp", conversation_id}`. The contact must exist (404 otherwise). An `Idempotency-Key` header replays the stored response for the same body. A different body with the same key is a 409. |
|
|
80
|
+
| `PUT /conversations/{id}` | `{read?, title?, custom_attributes?}` (for example `{custom_attributes: {chatbot_escalation: true}}` or `{read: true}`) → the conversation. |
|
|
81
|
+
| `POST /conversations/{id}/reply` | JSON `{message_type: "comment", type: "user", intercom_user_id \| user_id \| email, body, attachment_files?: [{content_type, name, data (base64)}], attachment_urls?}`, or `{type: "admin", admin_id, message_type: "comment" \| "note" \| "quick_reply", body}`. **Or multipart**, with the same fields plus `attachment_files[]` file parts. Returns the conversation. |
|
|
82
|
+
| `POST /conversations/{id}/parts` | `{message_type: "close" \| "open" \| "snoozed" \| "assignment", type: "admin", admin_id, snoozed_until?, assignee_id?}` → the conversation. |
|
|
83
|
+
| `GET /conversations/{id}?display_as=plaintext` | `id, state, open, read, created_at, source{id, body, author{type, id, name, email}, attachments, delivered_as}, contacts.contacts[0].external_id, conversation_parts.conversation_parts[]` (every part type, including close/open/note; our adapter keeps `comment`), teammates, custom_attributes. |
|
|
84
|
+
| `POST /conversations/search` | `{query, pagination: {per_page ≤ 150, starting_after}, sort_field, sort_order}` → `{type: "conversation.list", conversations, total_count, pages: {page, per_page, total_pages, next?: {page, starting_after}}}`. Like the real API, results carry **no `conversation_parts`**. |
|
|
85
|
+
| `GET /admins` | `{type: "admin.list", admins: [{id, name, email, …}]}`. The seeded admins are `1000001` (`support@mock.intercom.local`) and `1000002` (`clinician@mock.intercom.local`). |
|
|
86
|
+
| `GET /me` | The token's admin and app. |
|
|
87
|
+
|
|
88
|
+
Conversation state follows Intercom:
|
|
89
|
+
|
|
90
|
+
- Admin comments set `read: false`, meaning unread for the member.
|
|
91
|
+
- Member replies set `read: true`, set `waiting_since`, and reopen a closed or snoozed
|
|
92
|
+
conversation.
|
|
93
|
+
- `PUT {read: true}` marks the conversation read.
|
|
94
|
+
- Notes change nothing the member sees.
|
|
95
|
+
- Bodies are stored as HTML. Plain text is wrapped in `<p>`, one paragraph per line, and
|
|
96
|
+
`display_as=plaintext` turns it back into text.
|
|
97
|
+
|
|
98
|
+
Attachments keep their metadata only: name, content type, size, and a
|
|
99
|
+
`downloads.intercomcdn.com` URL that is not served. The uploaded bytes are never stored.
|
|
100
|
+
|
|
101
|
+
### Search
|
|
102
|
+
|
|
103
|
+
A query is either a filter `{field, operator, value}` or a compound
|
|
104
|
+
`{operator: "AND" | "OR", value: [query, …]}`, nested up to two levels.
|
|
105
|
+
|
|
106
|
+
- **Operators:** `=`, `!=`, `IN`, `NIN` (these two take an array), `<`, `>`, `~` (contains),
|
|
107
|
+
`!~`, `^` (starts with) and `$` (ends with). String comparisons are case-insensitive.
|
|
108
|
+
- **Contact fields:** `id`, `external_id`, `email`, `name`, `phone`, `role`, the timestamps,
|
|
109
|
+
and `custom_attributes.<name>`.
|
|
110
|
+
- **Conversation fields:** `id`, `contact_ids`, `teammate_ids`, `admin_assignee_id`,
|
|
111
|
+
`team_assignee_id`, `state`, `open`, `read`, `priority`, `title`, the timestamps, and
|
|
112
|
+
`source.{id,type,delivered_as,subject,body,author.id,author.type,author.name,author.email}`.
|
|
113
|
+
- **Unknown fields or operators** answer 400 `parameter_invalid`.
|
|
114
|
+
- **Sorting:** `updated_at` descending by default, with the id as tiebreaker.
|
|
115
|
+
- **Cursors:** opaque base64, 20 per page by default.
|
|
116
|
+
|
|
117
|
+
### Webhooks
|
|
118
|
+
|
|
119
|
+
The mock sends webhooks for these events:
|
|
120
|
+
|
|
121
|
+
| Event | Topic |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| Admin comment or quick reply (API or `POST /__admin/conversations/:id/admin-reply`) | `conversation.admin.replied` |
|
|
124
|
+
| Close (`/parts` or `POST /__admin/conversations/:id/close`) | `conversation.admin.closed` |
|
|
125
|
+
| Open (`/parts` or `POST /__admin/conversations/:id/open`) | `conversation.admin.opened` |
|
|
126
|
+
| Admin-initiated conversation (`POST /__admin/conversations`) | `conversation.admin.single.created` |
|
|
127
|
+
|
|
128
|
+
Snooze and assignment publish `conversation.admin.snoozed` and `.assigned`, but the default
|
|
129
|
+
endpoints do not subscribe to them. Member messages and notes send nothing: the EMR treats every
|
|
130
|
+
`notification_event` it receives as an admin event, so a real subscription must not include
|
|
131
|
+
member topics either.
|
|
132
|
+
|
|
133
|
+
Each delivery is a JSON body posted with `X-Hub-Signature: sha1=<hex HMAC-SHA1(secret, rawBody)>`:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{ "type": "notification_event", "app_id": "mockapp", "topic": "conversation.admin.replied",
|
|
137
|
+
"id": "notif_…", "created_at": 1760000000, "delivery_status": "pending", "delivery_attempts": 1,
|
|
138
|
+
"data": { "type": "notification_event_data", "item": { "type": "conversation", "id": "…",
|
|
139
|
+
"source": { "delivered_as": "customer_initiated", "author": { "email": "…" }, "body": "…" },
|
|
140
|
+
"contacts": { "contacts": [{ "id": "…", "external_id": "1652" }] },
|
|
141
|
+
"conversation_parts": { "conversation_parts": [ { "id": "…", "part_type": "comment", "body": "<p>…</p>", "created_at": 1760000000 } ] } } } }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **Only the new part.** `item.conversation_parts` holds just the part that caused the event,
|
|
145
|
+
as in Intercom's own payloads.
|
|
146
|
+
- **Wall-clock timestamps.** The part's timestamps and the envelope's `created_at` use real
|
|
147
|
+
time, never the mock clock, because the EMR rejects parts more than 5 minutes old against its
|
|
148
|
+
own clock.
|
|
149
|
+
- **Stable id.** The notification `id` stays the same on retries and on `webhook_duplicate`, so
|
|
150
|
+
the backend's `delivery_id ?? id` dedupe and the EMR's processed-marker dedupe both hold.
|
|
151
|
+
- **Retries.** A non-2xx answer is retried after 5 s, 5 min, 30 min and 2 h.
|
|
152
|
+
- **Admin plane.** `GET /__admin/webhooks`, `…/events`, `…/flush`, `…/:id/replay` and
|
|
153
|
+
`PUT /__admin/webhook-endpoints` are the standard webhook admin routes.
|
|
154
|
+
|
|
155
|
+
### Admin (beyond the standard contract)
|
|
156
|
+
|
|
157
|
+
| Route | Effect |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `POST /__admin/conversations/:id/admin-reply` | `{adminId? (default: first admin), body, messageType?: "comment" \| "note"}` appends an admin part and fires the webhook. |
|
|
160
|
+
| `POST /__admin/conversations/:id/close`, `…/open` | `{adminId?}` closes or reopens as an admin and fires the webhook. |
|
|
161
|
+
| `POST /__admin/conversations` | `{contactId \| externalId, adminId?, body}` creates an admin-initiated conversation. The EMR then looks the member up by `contacts[0].external_id`. |
|
|
162
|
+
| `GET /__admin/conversations`, `GET /__admin/contacts` | The namespace's records. |
|
|
163
|
+
| `PUT /__admin/admins` | Replace the admins: `[{id, name, email}]`. |
|
|
164
|
+
| `GET/PUT /__admin/settings` | `{tokens?: string[], customAttributes?: string[] \| null}`. With `customAttributes` set, writing an undefined attribute is a 400, as in a real workspace. |
|
|
165
|
+
|
|
166
|
+
Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`):
|
|
167
|
+
|
|
168
|
+
| Preset | Effect |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `rate_limited` | Every call answers 429 `rate_limit_exceeded`. The adapter maps it to 429. |
|
|
171
|
+
| `unauthorized` | Every call answers 401. |
|
|
172
|
+
| `server_error` | Every call answers 500. The adapter throws. |
|
|
173
|
+
| `service_unavailable` | Every call answers 503. The adapter throws. |
|
|
174
|
+
| `contact_stale_404` | The next conversation search answers 404. The adapter drops its cached contact id, re-resolves it and retries once. |
|
|
175
|
+
| `search_unavailable` | Search answers `conversations: null`. The admin inbox answers 503. |
|
|
176
|
+
| `repeated_cursor` | Search keeps answering the same `next.starting_after`. The admin inbox detects the loop and answers 503. |
|
|
177
|
+
| `webhook_duplicate` | Deliver the next webhook twice. |
|
|
178
|
+
| `webhook_reorder` | Swap the next two webhooks. |
|
|
179
|
+
| `webhook_drop` | Never deliver the next webhook. |
|
|
180
|
+
|
|
181
|
+
### Namespaces
|
|
182
|
+
|
|
183
|
+
- the `x-mockingbird-namespace` header;
|
|
184
|
+
- a `/ns/<name>` prefix on `INTERCOM_API_BASE_URL`;
|
|
185
|
+
- the access token: `PUT /__admin/credentials {"credentials": {"<INTERCOM_ACCESS_TOKEN>": "<namespace>"}}`.
|
|
186
|
+
|
|
187
|
+
The request journal records operation ids, statuses and contact, conversation and part ids. It
|
|
188
|
+
never records message bodies or contact details.
|
|
189
|
+
|
|
190
|
+
### Deliberately not modelled
|
|
191
|
+
|
|
192
|
+
- The member-app Messenger SDK and web widget (`@intercom/intercom-react-native`,
|
|
193
|
+
`@intercom/messenger-js-sdk`). They talk to Intercom's own hosts over their own protocol. The
|
|
194
|
+
web widget has no test-stage gate, so QA should block it.
|
|
195
|
+
- Attachment downloads: URLs are recorded but not served.
|
|
196
|
+
- Tags, notes, companies, segments, tickets, articles, data-attribute management, teams and
|
|
197
|
+
SLAs. These appear only as empty lists where a payload has them.
|
|
198
|
+
- Rate-limit headers on normal responses. Only `rate_limited` sends them.
|
|
199
|
+
- Intercom's exact wording for the idempotency conflict, which is unverified: the mock answers
|
|
200
|
+
409 `conflict`.
|
|
201
|
+
|
|
202
|
+
## API
|
|
203
|
+
|
|
204
|
+
| Export | Kind | Description |
|
|
205
|
+
| --- | --- | --- |
|
|
206
|
+
| `IntercomAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `appendPart(…)`, `manage(…)`, `startAdminConversation(…)`, `conversations()`, `contacts()`, `conversationBody(…)`, `contactBody(…)`, `error(…)`, `state`. Options: `sqlite`, `now`, `wallClock`, `namespace`, `admins`, `settings`, `onWebhook`. |
|
|
207
|
+
| `IntercomError` | class | A vendor error (status, code, message) raised by the admin-facing methods. |
|
|
208
|
+
| `createRuntime` | function | The mock with the full service contract (health, admin, namespaces, credentials, presets, webhooks). Options: `webhooks: {urls, secret?, events?, retryDelaysMs?, fetch?}`, `admins`, `settings`, `clock`, `wallClock`, `seed`, `adminKey`, `onLog`, `sqlite`. |
|
|
209
|
+
| `INTERCOM_PRESETS` | object | Every named fault preset. |
|
|
210
|
+
| `INTERCOM_NAMESPACE` | string | The service name, `"intercom"`. |
|
|
211
|
+
| `INTERCOM_TOPICS` | array | Every webhook topic the mock can send. |
|
|
212
|
+
| `HUB_SIGNATURE_HEADER`, `signHub` | values | `X-Hub-Signature` and the `sha1=<hex>` signer, for your own receivers. |
|
|
213
|
+
| `DEFAULT_ADMINS`, `DEFAULT_SETTINGS` | values | The seeded admins and default settings. |
|
|
214
|
+
| `toHtml`, `toPlaintext`, `encodeCursor`, `decodeCursor` | functions | Body rendering and the search cursor format. |
|
|
215
|
+
| `document`, `operationIds`, `supportedOperationIds` | values | The vendored OpenAPI contract and its operation ids. |
|
|
216
|
+
| `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target (`--webhook-url`, `--emr-webhook-url`, `--webhook-secret`, `--access-token`); port 8807. |
|
|
217
|
+
|
|
218
|
+
Part of [mockingbird](https://github.com/crvouga/mockingbird).
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuntime
|
|
3
|
+
} from "./chunk-OSBG2XJ7.js";
|
|
4
|
+
|
|
5
|
+
// ../../adapters/node/dist/cli.js
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
|
|
9
|
+
// ../../adapters/node/dist/serve.js
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
var serve = async (api, options = {}) => {
|
|
12
|
+
const server = createServer(async (req, res) => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of req) {
|
|
15
|
+
chunks.push(Buffer.from(chunk));
|
|
16
|
+
}
|
|
17
|
+
const body = Buffer.concat(chunks);
|
|
18
|
+
const address = server.address();
|
|
19
|
+
const port = typeof address === "object" && address !== null ? address.port : void 0;
|
|
20
|
+
const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`;
|
|
21
|
+
const raw = req.url ?? "/";
|
|
22
|
+
const url = new URL(raw.replace(/^\/+/, "/"), base);
|
|
23
|
+
const method = req.method ?? "GET";
|
|
24
|
+
const init = { method, headers: req.headers };
|
|
25
|
+
if (method !== "GET" && method !== "HEAD" && body.length > 0) {
|
|
26
|
+
init.body = body;
|
|
27
|
+
}
|
|
28
|
+
const aborted = new AbortController();
|
|
29
|
+
res.once("close", () => {
|
|
30
|
+
if (!res.writableFinished)
|
|
31
|
+
aborted.abort();
|
|
32
|
+
});
|
|
33
|
+
init.signal = aborted.signal;
|
|
34
|
+
const request = new Request(url, init);
|
|
35
|
+
let response;
|
|
36
|
+
try {
|
|
37
|
+
response = await api.fetch(request);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code === "MOCKINGBIRD_DROP") {
|
|
40
|
+
req.socket.destroy();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
44
|
+
res.end(JSON.stringify({
|
|
45
|
+
error: {
|
|
46
|
+
type: "mockingbird_internal",
|
|
47
|
+
message: error instanceof Error ? error.message : String(error)
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const headers = Object.fromEntries(response.headers);
|
|
53
|
+
const cookies = response.headers.getSetCookie();
|
|
54
|
+
if (cookies.length > 0)
|
|
55
|
+
headers["set-cookie"] = cookies;
|
|
56
|
+
if (!response.body) {
|
|
57
|
+
res.writeHead(response.status, headers);
|
|
58
|
+
res.end();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
res.writeHead(response.status, headers);
|
|
62
|
+
res.flushHeaders();
|
|
63
|
+
const reader = response.body.getReader();
|
|
64
|
+
try {
|
|
65
|
+
for (; ; ) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done)
|
|
68
|
+
break;
|
|
69
|
+
if (!res.write(value))
|
|
70
|
+
await new Promise((resolve) => res.once("drain", resolve));
|
|
71
|
+
}
|
|
72
|
+
res.end();
|
|
73
|
+
} catch {
|
|
74
|
+
res.destroy();
|
|
75
|
+
} finally {
|
|
76
|
+
reader.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
server.once("error", reject);
|
|
81
|
+
server.listen(options.port ?? 0, options.host, resolve);
|
|
82
|
+
});
|
|
83
|
+
return server;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ../../adapters/node/dist/listen.js
|
|
87
|
+
var listen = async (api, options = {}) => {
|
|
88
|
+
const host = options.host ?? "127.0.0.1";
|
|
89
|
+
const server = await serve(api, { port: options.port ?? 0, host });
|
|
90
|
+
const address = server.address();
|
|
91
|
+
const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
|
|
92
|
+
const shown = host.includes(":") ? `[${host}]` : host;
|
|
93
|
+
return {
|
|
94
|
+
url: `http://${shown}:${port}`,
|
|
95
|
+
port,
|
|
96
|
+
host,
|
|
97
|
+
server,
|
|
98
|
+
close: () => new Promise((resolve, reject) => {
|
|
99
|
+
server.close((error) => {
|
|
100
|
+
if (error && error.code !== "ERR_SERVER_NOT_RUNNING")
|
|
101
|
+
reject(error);
|
|
102
|
+
else
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
server.closeAllConnections?.();
|
|
106
|
+
})
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ../../adapters/node/dist/cli.js
|
|
111
|
+
var optionHelp = (options) => Object.entries(options).map(([name, option]) => {
|
|
112
|
+
const flag = `--${name}${option.type === "string" ? ` ${option.value ?? "<value>"}` : ""}`;
|
|
113
|
+
const fallback = option.default !== void 0 ? ` (default: ${String(option.default)})` : "";
|
|
114
|
+
return ` ${flag.padEnd(30)} ${option.description}${fallback}`;
|
|
115
|
+
});
|
|
116
|
+
var help = (spec) => [
|
|
117
|
+
`${spec.bin} \u2014 ${spec.description}`,
|
|
118
|
+
"",
|
|
119
|
+
"Usage:",
|
|
120
|
+
` ${spec.bin} <command> [options]`,
|
|
121
|
+
"",
|
|
122
|
+
"Commands:",
|
|
123
|
+
...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),
|
|
124
|
+
"",
|
|
125
|
+
`Run \`${spec.bin} <command> --help\` for a command's options.`
|
|
126
|
+
].join("\n");
|
|
127
|
+
var commandHelp = (spec, name, command) => [
|
|
128
|
+
`${spec.bin} ${name} \u2014 ${command.summary}`,
|
|
129
|
+
"",
|
|
130
|
+
"Usage:",
|
|
131
|
+
` ${command.usage ?? `${spec.bin} ${name} [options]`}`,
|
|
132
|
+
...command.options ? ["", "Options:", ...optionHelp(command.options)] : []
|
|
133
|
+
].join("\n");
|
|
134
|
+
var runCli = async (spec, argv) => {
|
|
135
|
+
const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : void 0;
|
|
136
|
+
const words = pair !== void 0 && spec.commands[pair] ? 2 : 1;
|
|
137
|
+
const name = words === 2 ? pair : argv[0];
|
|
138
|
+
const rest = argv.slice(words);
|
|
139
|
+
if (name === void 0 || name === "--help" || name === "-h" || name === "help") {
|
|
140
|
+
console.log(help(spec));
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
const command = spec.commands[name];
|
|
144
|
+
if (!command) {
|
|
145
|
+
console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}
|
|
146
|
+
|
|
147
|
+
${help(spec)}`);
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
|
150
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
151
|
+
console.log(commandHelp(spec, name, command));
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = parseArgs({
|
|
157
|
+
args: rest,
|
|
158
|
+
allowPositionals: true,
|
|
159
|
+
strict: true,
|
|
160
|
+
options: Object.fromEntries(Object.entries(command.options ?? {}).map(([key, option]) => [
|
|
161
|
+
key,
|
|
162
|
+
{
|
|
163
|
+
type: option.type,
|
|
164
|
+
...option.default !== void 0 ? { default: option.default } : {}
|
|
165
|
+
}
|
|
166
|
+
]))
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}
|
|
170
|
+
|
|
171
|
+
${commandHelp(spec, name, command)}`);
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
174
|
+
return command.run(parsed.values, parsed.positionals);
|
|
175
|
+
};
|
|
176
|
+
var COMMON_SERVE_OPTIONS = {
|
|
177
|
+
port: { type: "string", value: "<port>", description: "Port to listen on" },
|
|
178
|
+
host: { type: "string", value: "<host>", description: "Interface to bind", default: "127.0.0.1" },
|
|
179
|
+
"admin-key": {
|
|
180
|
+
type: "string",
|
|
181
|
+
value: "<key>",
|
|
182
|
+
description: "Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)"
|
|
183
|
+
},
|
|
184
|
+
seed: { type: "string", value: "<seed>", description: "Seed for every random choice" },
|
|
185
|
+
log: {
|
|
186
|
+
type: "string",
|
|
187
|
+
value: "<pretty|json|off>",
|
|
188
|
+
description: "Request log format",
|
|
189
|
+
default: "pretty"
|
|
190
|
+
},
|
|
191
|
+
"log-requests": {
|
|
192
|
+
type: "boolean",
|
|
193
|
+
description: "One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json"
|
|
194
|
+
},
|
|
195
|
+
config: {
|
|
196
|
+
type: "string",
|
|
197
|
+
value: "<file>",
|
|
198
|
+
description: "Serve every service in a mockingbird.json config instead"
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var formatLog = (format) => {
|
|
202
|
+
if (format === "off")
|
|
203
|
+
return void 0;
|
|
204
|
+
if (format === "json")
|
|
205
|
+
return (entry) => console.log(JSON.stringify(entry));
|
|
206
|
+
return (entry) => {
|
|
207
|
+
const op = entry.operationId ?? (entry.unmatched ? "UNMATCHED" : "-");
|
|
208
|
+
const ns = entry.namespace === "default" ? "" : ` [${entry.namespace}]`;
|
|
209
|
+
const fault = entry.faultId ? ` fault=${entry.faultId}` : "";
|
|
210
|
+
const adopted = entry.adopted ? " adopted" : "";
|
|
211
|
+
console.log(`${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}`);
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
var asString = (value) => typeof value === "string" ? value : void 0;
|
|
215
|
+
var loadTarget = async (name, own) => {
|
|
216
|
+
if (name === own.name)
|
|
217
|
+
return own;
|
|
218
|
+
const specifier = `@crvouga/mockingbird-service-${name}/server`;
|
|
219
|
+
try {
|
|
220
|
+
const mod = await import(specifier);
|
|
221
|
+
if (!mod.serveTarget)
|
|
222
|
+
throw new Error(`${specifier} exports no serveTarget`);
|
|
223
|
+
return mod.serveTarget;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
226
|
+
throw new Error(`cannot load service "${name}": ${reason}. Install @crvouga/mockingbird-service-${name}.`);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var start = async (target, values, config) => {
|
|
230
|
+
const runtime = await target.create(values, {
|
|
231
|
+
adminKey: config.adminKey,
|
|
232
|
+
seed: config.seed,
|
|
233
|
+
onLog: formatLog(config.log)
|
|
234
|
+
});
|
|
235
|
+
const listening = await listen(runtime, { port: config.port, host: config.host });
|
|
236
|
+
console.log(`${target.name} mock listening on ${listening.url}`);
|
|
237
|
+
console.log(`${target.name} health: GET ${listening.url}/health`);
|
|
238
|
+
console.log(`${target.name} admin: ${listening.url}/__admin (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
|
|
239
|
+
for (const line of target.banner?.(runtime) ?? [])
|
|
240
|
+
console.log(`${target.name} ${line}`);
|
|
241
|
+
return listening;
|
|
242
|
+
};
|
|
243
|
+
var untilSignal = async (servers) => new Promise((resolve) => {
|
|
244
|
+
const stop = async () => {
|
|
245
|
+
await Promise.allSettled(servers.map((s) => s.close()));
|
|
246
|
+
resolve(0);
|
|
247
|
+
};
|
|
248
|
+
process.once("SIGINT", stop);
|
|
249
|
+
process.once("SIGTERM", stop);
|
|
250
|
+
});
|
|
251
|
+
var serveCommand = (target) => ({
|
|
252
|
+
summary: `Serve the ${target.name} mock over HTTP`,
|
|
253
|
+
options: { ...COMMON_SERVE_OPTIONS, ...target.options },
|
|
254
|
+
async run(values) {
|
|
255
|
+
const log = values["log-requests"] === true ? "json" : asString(values.log) ?? "pretty";
|
|
256
|
+
if (!["pretty", "json", "off"].includes(log)) {
|
|
257
|
+
console.error(`--log must be pretty, json or off (got ${log})`);
|
|
258
|
+
return 2;
|
|
259
|
+
}
|
|
260
|
+
const configPath = asString(values.config);
|
|
261
|
+
if (configPath !== void 0) {
|
|
262
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
263
|
+
const servers = [];
|
|
264
|
+
try {
|
|
265
|
+
for (const [name, entry] of Object.entries(config.services ?? {})) {
|
|
266
|
+
const each = await loadTarget(name, target);
|
|
267
|
+
servers.push(await start(each, entry.options ?? {}, {
|
|
268
|
+
port: entry.port ?? each.defaultPort,
|
|
269
|
+
host: entry.host ?? "127.0.0.1",
|
|
270
|
+
...entry.adminKey !== void 0 ? { adminKey: entry.adminKey } : {},
|
|
271
|
+
...entry.seed !== void 0 ? { seed: entry.seed } : {},
|
|
272
|
+
log: config.log ?? log
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
await Promise.allSettled(servers.map((s) => s.close()));
|
|
277
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
return untilSignal(servers);
|
|
281
|
+
}
|
|
282
|
+
const port = asString(values.port);
|
|
283
|
+
const adminKey = asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
|
|
284
|
+
const seed = asString(values.seed);
|
|
285
|
+
let listening;
|
|
286
|
+
try {
|
|
287
|
+
listening = await start(target, values, {
|
|
288
|
+
port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
|
|
289
|
+
host: asString(values.host) ?? "127.0.0.1",
|
|
290
|
+
...adminKey !== void 0 ? { adminKey } : {},
|
|
291
|
+
...seed !== void 0 ? { seed } : {},
|
|
292
|
+
log
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
return untilSignal([listening]);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// src/server.ts
|
|
303
|
+
var DEFAULT_PORT = 8807;
|
|
304
|
+
var createServer2 = async (options = {}) => {
|
|
305
|
+
const { port, host, ...rest } = options;
|
|
306
|
+
const runtime = createRuntime(rest);
|
|
307
|
+
const listening = await listen(runtime, {
|
|
308
|
+
port: port ?? 0,
|
|
309
|
+
...host !== void 0 ? { host } : {}
|
|
310
|
+
});
|
|
311
|
+
return { ...listening, runtime };
|
|
312
|
+
};
|
|
313
|
+
var text = (value) => typeof value === "string" ? value : void 0;
|
|
314
|
+
var serveTarget = {
|
|
315
|
+
name: "intercom",
|
|
316
|
+
defaultPort: DEFAULT_PORT,
|
|
317
|
+
options: {
|
|
318
|
+
"webhook-url": {
|
|
319
|
+
type: "string",
|
|
320
|
+
value: "<url>",
|
|
321
|
+
description: "Backend webhook receiver (e.g. http://127.0.0.1:3000/messaging/webhook); comma-separate several"
|
|
322
|
+
},
|
|
323
|
+
"emr-webhook-url": {
|
|
324
|
+
type: "string",
|
|
325
|
+
value: "<url>",
|
|
326
|
+
description: "EMR webhook receiver (e.g. http://127.0.0.1:4000/v1/webhooks/intercom)"
|
|
327
|
+
},
|
|
328
|
+
"webhook-secret": {
|
|
329
|
+
type: "string",
|
|
330
|
+
value: "<secret>",
|
|
331
|
+
description: "Signs X-Hub-Signature (the app's INTERCOM_WEBHOOK_SECRET)"
|
|
332
|
+
},
|
|
333
|
+
"access-token": {
|
|
334
|
+
type: "string",
|
|
335
|
+
value: "<token>",
|
|
336
|
+
description: "Accept only this bearer token (the app's INTERCOM_ACCESS_TOKEN); default any"
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
create: (values, common) => {
|
|
340
|
+
const urls = [text(values["webhook-url"]), text(values["emr-webhook-url"])].filter((value) => value !== void 0).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean);
|
|
341
|
+
const secret = text(values["webhook-secret"]);
|
|
342
|
+
const token = text(values["access-token"]);
|
|
343
|
+
return createRuntime({
|
|
344
|
+
...urls.length > 0 ? { webhooks: { urls, ...secret ? { secret } : {} } } : {},
|
|
345
|
+
...token ? { settings: { tokens: [token] } } : {},
|
|
346
|
+
...common.adminKey !== void 0 ? { adminKey: common.adminKey } : {},
|
|
347
|
+
...common.seed !== void 0 ? { seed: common.seed } : {},
|
|
348
|
+
...common.onLog ? { onLog: common.onLog } : {}
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
banner: () => [
|
|
352
|
+
"auth: Authorization: Bearer <INTERCOM_ACCESS_TOKEN>, Intercom-Version: 2.11",
|
|
353
|
+
"namespaces: x-mockingbird-namespace, /ns/<name>/\u2026, or PUT /__admin/credentials {<token>: <ns>}"
|
|
354
|
+
]
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export {
|
|
358
|
+
runCli,
|
|
359
|
+
serveCommand,
|
|
360
|
+
DEFAULT_PORT,
|
|
361
|
+
createServer2 as createServer,
|
|
362
|
+
serveTarget
|
|
363
|
+
};
|
|
364
|
+
//# sourceMappingURL=chunk-5RRE6WVT.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../adapters/node/src/cli.ts", "../../../adapters/node/src/serve.ts", "../../../adapters/node/src/listen.ts", "../src/server.ts"],
|
|
4
|
+
"sourcesContent": ["import { readFile } from \"node:fs/promises\"\nimport { type ParseArgsConfig, parseArgs } from \"node:util\"\nimport type { RequestLog, ServiceInstance, ServiceRuntime } from \"@crvouga/mockingbird-service\"\nimport { type Listening, listen } from \"./listen.js\"\n\nexport type CliOption = {\n type: \"string\" | \"boolean\"\n description: string\n /** Shown in help; the value placeholder, e.g. `<port>`. */\n value?: string\n default?: string | boolean\n}\n\nexport type CliValues = Record<string, string | boolean | undefined>\n\nexport type CliCommand = {\n summary: string\n usage?: string\n options?: Record<string, CliOption>\n /** Resolves to an exit code; a server command resolves only when it stops. */\n run(values: CliValues, positionals: string[]): Promise<number>\n}\n\nexport type CliSpec = {\n bin: string\n description: string\n commands: Record<string, CliCommand>\n}\n\nconst optionHelp = (options: Record<string, CliOption>): string[] =>\n Object.entries(options).map(([name, option]) => {\n const flag = `--${name}${option.type === \"string\" ? ` ${option.value ?? \"<value>\"}` : \"\"}`\n const fallback = option.default !== undefined ? ` (default: ${String(option.default)})` : \"\"\n return ` ${flag.padEnd(30)} ${option.description}${fallback}`\n })\n\nconst help = (spec: CliSpec): string =>\n [\n `${spec.bin} \u2014 ${spec.description}`,\n \"\",\n \"Usage:\",\n ` ${spec.bin} <command> [options]`,\n \"\",\n \"Commands:\",\n ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),\n \"\",\n `Run \\`${spec.bin} <command> --help\\` for a command's options.`,\n ].join(\"\\n\")\n\nconst commandHelp = (spec: CliSpec, name: string, command: CliCommand): string =>\n [\n `${spec.bin} ${name} \u2014 ${command.summary}`,\n \"\",\n \"Usage:\",\n ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,\n ...(command.options ? [\"\", \"Options:\", ...optionHelp(command.options)] : []),\n ].join(\"\\n\")\n\n/** Parse `argv` against `spec` and run the chosen command. Resolves to an exit code. */\nexport const runCli = async (spec: CliSpec, argv: string[]): Promise<number> => {\n // Two-word commands (`corpus pull`) win over one-word ones.\n const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : undefined\n const words = pair !== undefined && spec.commands[pair] ? 2 : 1\n const name = words === 2 ? pair : argv[0]\n const rest = argv.slice(words)\n if (name === undefined || name === \"--help\" || name === \"-h\" || name === \"help\") {\n console.log(help(spec))\n return 0\n }\n const command = spec.commands[name]\n if (!command) {\n console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}\\n\\n${help(spec)}`)\n return 2\n }\n if (rest.includes(\"--help\") || rest.includes(\"-h\")) {\n console.log(commandHelp(spec, name, command))\n return 0\n }\n let parsed: { values: CliValues; positionals: string[] }\n try {\n parsed = parseArgs({\n args: rest,\n allowPositionals: true,\n strict: true,\n options: Object.fromEntries(\n Object.entries(command.options ?? {}).map(([key, option]) => [\n key,\n {\n type: option.type,\n ...(option.default !== undefined ? { default: option.default } : {}),\n },\n ]),\n ) as ParseArgsConfig[\"options\"],\n }) as { values: CliValues; positionals: string[] }\n } catch (error) {\n console.error(\n `${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}\\n\\n${commandHelp(spec, name, command)}`,\n )\n return 2\n }\n return command.run(parsed.values, parsed.positionals)\n}\n\n// \u2500\u2500 serve \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type LogFormat = \"pretty\" | \"json\" | \"off\"\n\nexport type CommonServeOptions = {\n adminKey: string | undefined\n seed: string | undefined\n onLog: ((entry: RequestLog) => void) | undefined\n}\n\n/**\n * What a service contributes to `serve`: how to build its runtime from CLI flags,\n * and what to say at startup. Every service's `./server` entry exports one as\n * `serveTarget`, which is also how `serve --config` finds services by name.\n */\nexport type ServeTarget = {\n name: string\n defaultPort: number\n /** Serve flags beyond the common ones. */\n options?: Record<string, CliOption>\n create(\n values: CliValues,\n common: CommonServeOptions,\n ): Promise<ServiceRuntime<ServiceInstance>> | ServiceRuntime<ServiceInstance>\n /** Startup lines after the listen address, e.g. the loaded corpus version. */\n banner?(runtime: ServiceRuntime<ServiceInstance>): string[]\n}\n\nconst COMMON_SERVE_OPTIONS: Record<string, CliOption> = {\n port: { type: \"string\", value: \"<port>\", description: \"Port to listen on\" },\n host: { type: \"string\", value: \"<host>\", description: \"Interface to bind\", default: \"127.0.0.1\" },\n \"admin-key\": {\n type: \"string\",\n value: \"<key>\",\n description: \"Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)\",\n },\n seed: { type: \"string\", value: \"<seed>\", description: \"Seed for every random choice\" },\n log: {\n type: \"string\",\n value: \"<pretty|json|off>\",\n description: \"Request log format\",\n default: \"pretty\",\n },\n \"log-requests\": {\n type: \"boolean\",\n description:\n \"One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json\",\n },\n config: {\n type: \"string\",\n value: \"<file>\",\n description: \"Serve every service in a mockingbird.json config instead\",\n },\n}\n\nconst formatLog = (format: LogFormat) => {\n if (format === \"off\") return undefined\n if (format === \"json\") return (entry: RequestLog) => console.log(JSON.stringify(entry))\n return (entry: RequestLog) => {\n const op = entry.operationId ?? (entry.unmatched ? \"UNMATCHED\" : \"-\")\n const ns = entry.namespace === \"default\" ? \"\" : ` [${entry.namespace}]`\n const fault = entry.faultId ? ` fault=${entry.faultId}` : \"\"\n const adopted = entry.adopted ? \" adopted\" : \"\"\n console.log(\n `${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}`,\n )\n }\n}\n\nconst asString = (value: string | boolean | undefined): string | undefined =>\n typeof value === \"string\" ? value : undefined\n\n/** A service entry in `mockingbird.json`. */\nexport type ConfigService = {\n port?: number\n host?: string\n adminKey?: string\n seed?: string\n /** Service-specific serve flags, by long name: `{ \"webhook-url\": \"\u2026\" }`. */\n options?: Record<string, string | boolean>\n}\n\nexport type MockingbirdConfig = {\n /** Keyed by service name: `junction` loads `@crvouga/mockingbird-service-junction`. */\n services: Record<string, ConfigService>\n log?: LogFormat\n}\n\nconst loadTarget = async (name: string, own: ServeTarget): Promise<ServeTarget> => {\n if (name === own.name) return own\n const specifier = `@crvouga/mockingbird-service-${name}/server`\n try {\n const mod = (await import(specifier)) as { serveTarget?: ServeTarget }\n if (!mod.serveTarget) throw new Error(`${specifier} exports no serveTarget`)\n return mod.serveTarget\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n throw new Error(\n `cannot load service \"${name}\": ${reason}. Install @crvouga/mockingbird-service-${name}.`,\n )\n }\n}\n\nconst start = async (\n target: ServeTarget,\n values: CliValues,\n config: { port: number; host: string; adminKey?: string; seed?: string; log: LogFormat },\n): Promise<Listening> => {\n const runtime = await target.create(values, {\n adminKey: config.adminKey,\n seed: config.seed,\n onLog: formatLog(config.log),\n })\n const listening = await listen(runtime, { port: config.port, host: config.host })\n console.log(`${target.name} mock listening on ${listening.url}`)\n console.log(`${target.name} health: GET ${listening.url}/health`)\n console.log(\n `${target.name} admin: ${listening.url}/__admin (${config.adminKey ? \"x-mockingbird-admin-key required\" : \"open \u2014 pass --admin-key to lock\"})`,\n )\n for (const line of target.banner?.(runtime) ?? []) console.log(`${target.name} ${line}`)\n return listening\n}\n\nconst untilSignal = async (servers: Listening[]): Promise<number> =>\n new Promise((resolve) => {\n const stop = async () => {\n await Promise.allSettled(servers.map((s) => s.close()))\n resolve(0)\n }\n process.once(\"SIGINT\", stop)\n process.once(\"SIGTERM\", stop)\n })\n\n/** The standard `serve` command for a service, including multi-service `--config`. */\nexport const serveCommand = (target: ServeTarget): CliCommand => ({\n summary: `Serve the ${target.name} mock over HTTP`,\n options: { ...COMMON_SERVE_OPTIONS, ...target.options },\n async run(values) {\n const log = (\n values[\"log-requests\"] === true ? \"json\" : (asString(values.log) ?? \"pretty\")\n ) as LogFormat\n if (![\"pretty\", \"json\", \"off\"].includes(log)) {\n console.error(`--log must be pretty, json or off (got ${log})`)\n return 2\n }\n const configPath = asString(values.config)\n if (configPath !== undefined) {\n const config = JSON.parse(await readFile(configPath, \"utf8\")) as MockingbirdConfig\n const servers: Listening[] = []\n try {\n for (const [name, entry] of Object.entries(config.services ?? {})) {\n const each = await loadTarget(name, target)\n servers.push(\n await start(each, entry.options ?? {}, {\n port: entry.port ?? each.defaultPort,\n host: entry.host ?? \"127.0.0.1\",\n ...(entry.adminKey !== undefined ? { adminKey: entry.adminKey } : {}),\n ...(entry.seed !== undefined ? { seed: entry.seed } : {}),\n log: config.log ?? log,\n }),\n )\n }\n } catch (error) {\n await Promise.allSettled(servers.map((s) => s.close()))\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal(servers)\n }\n const port = asString(values.port)\n const adminKey = asString(values[\"admin-key\"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY\n const seed = asString(values.seed)\n let listening: Listening\n try {\n listening = await start(target, values, {\n port: port === undefined ? target.defaultPort : Number.parseInt(port, 10),\n host: asString(values.host) ?? \"127.0.0.1\",\n ...(adminKey !== undefined ? { adminKey } : {}),\n ...(seed !== undefined ? { seed } : {}),\n log,\n })\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal([listening])\n },\n})\n", "import { createServer } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\n\n/** Options for {@link serve}. */\nexport type NodeServeOptions = {\n port?: number\n host?: string\n}\n\n/**\n * Serve any Mockingbird {@link FetchAPI} over `node:http`.\n * Port defaults to `0`, so the OS assigns an ephemeral port (read from `server.address()`).\n */\nexport const serve = async (api: FetchAPI, options: NodeServeOptions = {}) => {\n const server = createServer(async (req, res) => {\n const chunks: Buffer[] = []\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk))\n }\n const body = Buffer.concat(chunks)\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : undefined\n const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`\n const raw = req.url ?? \"/\"\n const url = new URL(raw.replace(/^\\/+/, \"/\"), base)\n const method = req.method ?? \"GET\"\n const init: RequestInit = { method, headers: req.headers as Record<string, string> }\n if (method !== \"GET\" && method !== \"HEAD\" && body.length > 0) {\n init.body = body\n }\n const aborted = new AbortController()\n res.once(\"close\", () => {\n if (!res.writableFinished) aborted.abort()\n })\n init.signal = aborted.signal\n const request = new Request(url, init)\n let response: Response\n try {\n response = await api.fetch(request)\n } catch (error) {\n // A `drop` fault: destroy the socket so the client sees the connection die.\n if ((error as { code?: string }).code === \"MOCKINGBIRD_DROP\") {\n req.socket.destroy()\n return\n }\n res.writeHead(500, { \"content-type\": \"application/json\" })\n res.end(\n JSON.stringify({\n error: {\n type: \"mockingbird_internal\",\n message: error instanceof Error ? error.message : String(error),\n },\n }),\n )\n return\n }\n // Headers#entries() joins repeated headers; Set-Cookie must stay one header per cookie.\n const headers: Record<string, string | string[]> = Object.fromEntries(response.headers)\n const cookies = response.headers.getSetCookie()\n if (cookies.length > 0) headers[\"set-cookie\"] = cookies\n if (!response.body) {\n res.writeHead(response.status, headers)\n res.end()\n return\n }\n // Stream the body chunk by chunk: event streams and long-polls must not be buffered.\n res.writeHead(response.status, headers)\n res.flushHeaders()\n const reader = response.body.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!res.write(value)) await new Promise<void>((resolve) => res.once(\"drain\", resolve))\n }\n res.end()\n } catch {\n res.destroy()\n } finally {\n reader.releaseLock()\n }\n })\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, options.host, resolve)\n })\n return server\n}\n", "import type { Server } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\nimport { serve } from \"./serve.js\"\n\nexport type ListenOptions = {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`: a mock should not be reachable off the machine by accident. */\n host?: string\n}\n\n/** A running server, with the address it actually bound. */\nexport type Listening = {\n url: string\n port: number\n host: string\n server: Server\n close(): Promise<void>\n}\n\nexport const listen = async (api: FetchAPI, options: ListenOptions = {}): Promise<Listening> => {\n const host = options.host ?? \"127.0.0.1\"\n const server = await serve(api, { port: options.port ?? 0, host })\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : (options.port ?? 0)\n const shown = host.includes(\":\") ? `[${host}]` : host\n return {\n url: `http://${shown}:${port}`,\n port,\n host,\n server,\n close: () =>\n new Promise<void>((resolve, reject) => {\n // Stop accepting first, then drop keep-alive sockets so close() can finish.\n server.close((error) => {\n if (error && (error as { code?: string }).code !== \"ERR_SERVER_NOT_RUNNING\") reject(error)\n else resolve()\n })\n server.closeAllConnections?.()\n }),\n }\n}\n", "/// <reference types=\"node\" />\nimport { type Listening, listen, type ServeTarget } from \"@crvouga/mockingbird-adapter-node\"\nimport { createRuntime, type IntercomRuntime, type IntercomRuntimeOptions } from \"./runtime.js\"\n\n/** Port `mockingbird-intercom serve` listens on when none is given. */\nexport const DEFAULT_PORT = 8807\n\nexport type IntercomServerOptions = IntercomRuntimeOptions & {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`. */\n host?: string\n}\n\nexport type IntercomServer = Listening & { runtime: IntercomRuntime }\n\n/** Serve the Intercom mock over `node:http`. */\nexport const createServer = async (\n options: IntercomServerOptions = {},\n): Promise<IntercomServer> => {\n const { port, host, ...rest } = options\n const runtime = createRuntime(rest)\n const listening = await listen(runtime, {\n port: port ?? 0,\n ...(host !== undefined ? { host } : {}),\n })\n return { ...listening, runtime }\n}\n\nconst text = (value: string | boolean | undefined) =>\n typeof value === \"string\" ? value : undefined\n\n/** How `serve` (and `serve --config`) builds the Intercom mock from flags. */\nexport const serveTarget: ServeTarget = {\n name: \"intercom\",\n defaultPort: DEFAULT_PORT,\n options: {\n \"webhook-url\": {\n type: \"string\",\n value: \"<url>\",\n description:\n \"Backend webhook receiver (e.g. http://127.0.0.1:3000/messaging/webhook); comma-separate several\",\n },\n \"emr-webhook-url\": {\n type: \"string\",\n value: \"<url>\",\n description: \"EMR webhook receiver (e.g. http://127.0.0.1:4000/v1/webhooks/intercom)\",\n },\n \"webhook-secret\": {\n type: \"string\",\n value: \"<secret>\",\n description: \"Signs X-Hub-Signature (the app's INTERCOM_WEBHOOK_SECRET)\",\n },\n \"access-token\": {\n type: \"string\",\n value: \"<token>\",\n description: \"Accept only this bearer token (the app's INTERCOM_ACCESS_TOKEN); default any\",\n },\n },\n create: (values, common) => {\n const urls = [text(values[\"webhook-url\"]), text(values[\"emr-webhook-url\"])]\n .filter((value): value is string => value !== undefined)\n .flatMap((value) => value.split(\",\"))\n .map((value) => value.trim())\n .filter(Boolean)\n const secret = text(values[\"webhook-secret\"])\n const token = text(values[\"access-token\"])\n return createRuntime({\n ...(urls.length > 0 ? { webhooks: { urls, ...(secret ? { secret } : {}) } } : {}),\n ...(token ? { settings: { tokens: [token] } } : {}),\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n ...(common.seed !== undefined ? { seed: common.seed } : {}),\n ...(common.onLog ? { onLog: common.onLog } : {}),\n })\n },\n banner: () => [\n \"auth: Authorization: Bearer <INTERCOM_ACCESS_TOKEN>, Intercom-Version: 2.11\",\n \"namespaces: x-mockingbird-namespace, /ns/<name>/\u2026, or PUT /__admin/credentials {<token>: <ns>}\",\n ],\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["createServer"]
|
|
7
|
+
}
|