@crvouga/mockingbird-service-slack 1.5.1 → 1.6.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 +10 -4
- package/README.md +24 -7
- package/dist/{chunk-3NVCQXOC.js → chunk-GPFJICAV.js} +299 -3
- package/dist/chunk-GPFJICAV.js.map +7 -0
- package/dist/{chunk-OVHCSYG6.js → chunk-XNPGYYVE.js} +253 -20
- package/dist/{chunk-OVHCSYG6.js.map → chunk-XNPGYYVE.js.map} +4 -4
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +34 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +41 -9
- package/dist/server.js +2 -2
- package/package.json +15 -13
- package/dist/chunk-3NVCQXOC.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-slack
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
3
|
+
## 1.6.0 (2026-09-30)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- boot Bolt over Socket Mode ([01322df](https://github.com/crvouga/mockingbird/commit/01322df315c66b1f2f5b53094cbbeeeef6d3ae6c))
|
|
4
8
|
|
|
5
9
|
### Fixes and improvements
|
|
6
10
|
|
|
7
|
-
- keep
|
|
11
|
+
- keep merged kill-bill and slack checks green ([5de12bf](https://github.com/crvouga/mockingbird/commit/5de12bf20b6b209769c632ccd4b9ec24b680a07a))
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## 1.5.1 (2026-09-29)
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
### Fixes and improvements
|
|
16
|
+
|
|
17
|
+
- keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
|
|
12
18
|
|
|
13
19
|
## 1.5.0 (2026-09-29)
|
|
14
20
|
|
package/README.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> Familiar calls. Faithful echoes. Part of [Mockingbird](https://github.com/crvouga/mockingbird).
|
|
4
4
|
|
|
5
|
-
Stateful mock of **Slack** for test suites: incoming webhooks
|
|
6
|
-
call,
|
|
5
|
+
Stateful mock of **Slack** for test suites: incoming webhooks, the Web API methods our apps
|
|
6
|
+
call, and the Socket Mode control plane Bolt needs in order to boot, with an **outbox** of
|
|
7
|
+
everything the app "sent". A suite asserts that an alert fired
|
|
7
8
|
(`GET /__admin/outbox?webhook=…` or `?channel=…`) without a real workspace, and drives the
|
|
8
9
|
retry paths (429 with `retry-after`, 5xx) and the terminal ones (`no_text`, `no_service`,
|
|
9
10
|
`channel_not_found`) with named presets.
|
|
@@ -30,6 +31,9 @@ Point every `SLACK_*_WEBHOOK_URL` (`SLACK_PIPELINE_ALERT_WEBHOOK_URL`,
|
|
|
30
31
|
`http://127.0.0.1:8808/services/T000/B000/<anything>`. Any path is accepted until you register
|
|
31
32
|
hooks. The Web API callers hardcode `https://slack.com/api/…`; point them at
|
|
32
33
|
`http://127.0.0.1:8808/api/…` (for `@slack/web-api`, `slackApiUrl: "http://127.0.0.1:8808/api/"`).
|
|
34
|
+
Bolt in Socket Mode uses that same `clientOptions.slackApiUrl` (Bolt copies it into
|
|
35
|
+
`SocketModeReceiver`) plus an `xapp-` app token. `apps.connections.open` returns a `ws://` URL
|
|
36
|
+
on this process.
|
|
33
37
|
|
|
34
38
|
```bash
|
|
35
39
|
npx mockingbird-slack serve --port 8808
|
|
@@ -61,7 +65,7 @@ const outbox = await slack.fetch(
|
|
|
61
65
|
| Route | Behaviour |
|
|
62
66
|
| --- | --- |
|
|
63
67
|
| `POST /services/{T}/{B}/{X}` | Incoming webhook. JSON `{text, blocks?, attachments?, thread_ts?}` (or form `payload=<json>`) → `200 ok` (text/plain). `400 invalid_payload` (not a JSON object), `400 no_text` (no text, blocks or attachments), `400 invalid_blocks`, `400 too_many_attachments`, `404 no_service` (unregistered hook, once any hook is registered), `404 channel_not_found`, `410 channel_is_archived`. |
|
|
64
|
-
| `POST /api/chat.postMessage` | `{channel, text?, blocks?, attachments?, thread_ts?}` → `{ok, channel, ts, message}`. `channel` is an id or `#name`; unknown channels are created on first use unless `strictChannels`. Errors: `channel_not_found`, `no_text`, `invalid_blocks`, `invalid_blocks_format`, `msg_too_long`, `is_archived`. |
|
|
68
|
+
| `POST /api/chat.postMessage` | `{channel, text?, blocks?, attachments?, thread_ts?, unfurl_links?}` → `{ok, channel, ts, message}`. `channel` is an id or `#name`; unknown channels are created on first use unless `strictChannels`. The outbox keeps `text`, `blocks`, `unfurl_links`, `thread_ts`, and `workspace` (the team id). Errors: `channel_not_found`, `no_text`, `invalid_blocks`, `invalid_blocks_format`, `msg_too_long`, `is_archived`. |
|
|
65
69
|
| `POST /api/chat.update` | `{channel, ts, text?, blocks?}` → `{ok, channel, ts, text, message}` with `edited`; `message_not_found`, `cant_update_message` (webhook posts). Omitted blocks are kept. |
|
|
66
70
|
| `POST /api/chat.postEphemeral` | `{channel, user, text}` → `{ok, message_ts}`; `user_not_found`. Recorded in the outbox with `ephemeral: true`. |
|
|
67
71
|
| `GET\|POST /api/chat.getPermalink` | `channel`, `message_ts` → `{ok, channel, permalink}` (`https://<domain>.slack.com/archives/C…/p…`); `message_not_found`. |
|
|
@@ -71,6 +75,8 @@ const outbox = await slack.fetch(
|
|
|
71
75
|
| `GET\|POST /api/users.info`, `/users.lookupByEmail` | `{ok, user}` with `profile.email`/`real_name`; `user_not_found`, `users_not_found`. Seeded: `U0ADA` (`ada@example.com`) and the bot. |
|
|
72
76
|
| `GET\|POST /api/files.info` | `{ok, file}` with `url_private_download` on `files.slack.com`; `file_not_found`. Seeded: `F0REPORT`. |
|
|
73
77
|
| `POST /api/views.open` | `{trigger_id, view}` → `{ok, view: {id: "V…", hash, state, …}}`; `invalid_arguments`. |
|
|
78
|
+
| `POST /api/apps.connections.open` | `Authorization: Bearer xapp-…` (header only) → `{ok, url}` where `url` is `ws://<this host>/link/?ticket=…&app_id=…`. One WebSocket per ticket; a second upgrade or a closed ticket is rejected. Missing token → `not_authed`; a bot token → `not_allowed_token_type`; malformed → `invalid_auth`; a token in `revokedAppTokens` → `token_revoked`; an app token that is not in this namespace's `appTokens` (when that list is non-empty) → `invalid_auth`. |
|
|
79
|
+
| WebSocket `/link/?ticket=` | On upgrade, one JSON `hello` (`type`, `connection_info.app_id`, `num_connections`, `debug_info.approximate_connection_time`). No events are pushed. Client pings are ponged; the server pings every 2s. `{envelope_id}` acks are accepted and not answered. `disconnect` reasons `warning`, `refresh_requested`, and `link_disabled` match Slack's envelope (no reconnect URL: `@slack/socket-mode@2.0.4` calls `apps.connections.open` again). |
|
|
74
80
|
|
|
75
81
|
Web API bodies may be JSON or form-encoded (structures JSON-encoded in form fields), as Slack
|
|
76
82
|
accepts both; read methods also take query arguments. Auth is `Authorization: Bearer xox…` (or a
|
|
@@ -82,10 +88,12 @@ a JSON post without `charset` succeeds with Slack's `missing_charset` warning.
|
|
|
82
88
|
|
|
83
89
|
| Route | Effect |
|
|
84
90
|
| --- | --- |
|
|
85
|
-
| `GET /__admin/outbox?webhook=<path>&channel=<id\|#name>&thread_ts=&source=webhook\|api&since=&limit=` | Every message sent, oldest first: `text`, `blocks`, `attachments`, `thread_ts`, `ts`, `channel`, `webhook`, `method`, `ephemeral`, `edited`, `reactions`. `webhook` takes `/services/T/B/X`, `T/B/X` or a full URL. `GET /__admin/outbox/:id` reads one. |
|
|
91
|
+
| `GET /__admin/outbox?webhook=<path>&channel=<id\|#name>&thread_ts=&source=webhook\|api&since=&limit=` | Every message sent, oldest first: `text`, `blocks`, `attachments`, `thread_ts`, `ts`, `channel`, `webhook`, `method`, `ephemeral`, `edited`, `reactions`, `unfurl_links`, `workspace`. `webhook` takes `/services/T/B/X`, `T/B/X` or a full URL. `GET /__admin/outbox/:id` reads one. |
|
|
86
92
|
| `POST /__admin/hooks` | `{path, channel?}` or `{hooks: [...]}`: register webhooks (and route them to a channel). Once any hook exists, unknown hooks answer `404 no_service`. `GET` lists, `DELETE [?path=]` removes. |
|
|
87
93
|
| `POST /__admin/channels`, `/users`, `/files` | Seed workspace records (`{id?, name, is_private?, is_archived?, is_member?}`, `{id, name?, real_name?, email?}`, `{id, name?, mimetype?, size?}`). `GET /__admin/channels`, `/users`, `/views` list them. |
|
|
88
|
-
| `PUT /__admin/settings` | `{teamId?, teamName?, teamDomain?, botUserId?, botId?, appId?, tokens?, strictChannels?}` for the calling namespace. |
|
|
94
|
+
| `PUT /__admin/settings` | `{teamId?, teamName?, teamDomain?, botUserId?, botId?, appId?, tokens?, appTokens?, revokedAppTokens?, socketLifetimeMs?, strictChannels?}` for the calling namespace. `socketLifetimeMs` closes an open Socket Mode connection with `reason: "warning"`. |
|
|
95
|
+
| `GET /__admin/socket/connections` | Open and closed Socket Mode sockets for the calling namespace: `namespace`, `appId`, `ticket`, `connectedAt`, `state`, `reconnectCount`. Never tokens. |
|
|
96
|
+
| `POST /__admin/socket/disconnect` | `{reason?: "warning"\|"refresh_requested"\|"link_disabled", code?: 1000}`. Sends Slack's `disconnect` envelope, then closes. `code: 1006` drops the TCP connection instead. |
|
|
89
97
|
|
|
90
98
|
Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n, "params"?: {...}}`):
|
|
91
99
|
|
|
@@ -97,6 +105,12 @@ Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n, "params"?
|
|
|
97
105
|
| `channel_not_found` | Webhooks `404 channel_not_found`; Web API channel methods `{ok: false, error: "channel_not_found"}`. |
|
|
98
106
|
| `no_service` | Webhooks `404 no_service` (a revoked hook). |
|
|
99
107
|
| `invalid_auth` | Web API `{ok: false, error: "invalid_auth"}` (a revoked token). |
|
|
108
|
+
| `connections_open_invalid_auth` | `apps.connections.open` answers `{ok:false, error:"invalid_auth"}` and mints no ticket. |
|
|
109
|
+
| `connections_open_429` | `apps.connections.open` answers `429 {ok:false, error:"ratelimited"}` with `retry-after` (`params.retryAfter`, default 1). |
|
|
110
|
+
| `connections_open_5xx` | `apps.connections.open` answers `500 internal_error` (`params.status: 503` gives `service_unavailable`). |
|
|
111
|
+
| `socket_close_after_hello` | The next Socket Mode socket sends `hello`, then a normal close. |
|
|
112
|
+
| `socket_abnormal_close` | The next Socket Mode socket sends `hello`, then drops the connection (code 1006). |
|
|
113
|
+
| `socket_hello_latency` | Delay `hello` by `params.latencyMs` milliseconds. The HTTP response itself is not delayed. |
|
|
100
114
|
|
|
101
115
|
### Namespaces
|
|
102
116
|
|
|
@@ -106,8 +120,11 @@ maps a bot token or a webhook's `T/B/X` path to a namespace.
|
|
|
106
120
|
|
|
107
121
|
### Deliberately not modelled
|
|
108
122
|
|
|
109
|
-
-
|
|
110
|
-
|
|
123
|
+
- Inbound Events API and interactivity delivery. Socket Mode's connection lifecycle is
|
|
124
|
+
modelled (`apps.connections.open`, `hello`, ping/pong, `disconnect`) so Bolt can boot and
|
|
125
|
+
shut down; the mock does not push events on connect. A `disconnect` envelope has no
|
|
126
|
+
reconnect URL, matching Slack's documented shape and `@slack/socket-mode@2.0.4`, which
|
|
127
|
+
calls `apps.connections.open` again.
|
|
111
128
|
- File uploads and downloads: `files.info` answers seeded metadata; the `files.slack.com` URLs
|
|
112
129
|
are not served.
|
|
113
130
|
- Real per-method rate-limit tiers: 429s come only from the `rate_limited` preset.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
bindSocketTransport,
|
|
3
|
+
claimSocketTicket,
|
|
4
|
+
createRuntime,
|
|
5
|
+
markSocketClosed,
|
|
6
|
+
openSocketCount,
|
|
7
|
+
unbindSocketTransport
|
|
8
|
+
} from "./chunk-XNPGYYVE.js";
|
|
4
9
|
|
|
5
10
|
// ../../adapters/node/dist/cli.js
|
|
6
11
|
import { readFile } from "node:fs/promises";
|
|
@@ -235,6 +240,7 @@ var start = async (target, values, config) => {
|
|
|
235
240
|
onLog: formatLog(config.log)
|
|
236
241
|
});
|
|
237
242
|
const listening = await listen(runtime, { port: config.port, host: config.host });
|
|
243
|
+
target.listening?.(listening);
|
|
238
244
|
console.log(`${target.name} mock listening on ${listening.url}`);
|
|
239
245
|
console.log(`${target.name} health: GET ${listening.url}/health`);
|
|
240
246
|
console.log(`${target.name} admin: ${listening.url}/__admin (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
|
|
@@ -302,6 +308,291 @@ var serveCommand = (target) => ({
|
|
|
302
308
|
}
|
|
303
309
|
});
|
|
304
310
|
|
|
311
|
+
// src/socket-server.ts
|
|
312
|
+
import { createHash } from "node:crypto";
|
|
313
|
+
var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
314
|
+
var SERVER_PING_MS = 2e3;
|
|
315
|
+
var MAX_FRAME = 1e6;
|
|
316
|
+
var HOST = "applink-mockingbird";
|
|
317
|
+
var attached = /* @__PURE__ */ new WeakSet();
|
|
318
|
+
var encodeFrame = (opcode, payload) => {
|
|
319
|
+
const length = payload.length;
|
|
320
|
+
let header;
|
|
321
|
+
if (length < 126) {
|
|
322
|
+
header = Buffer.alloc(2);
|
|
323
|
+
header[1] = length;
|
|
324
|
+
} else if (length < 65536) {
|
|
325
|
+
header = Buffer.alloc(4);
|
|
326
|
+
header[1] = 126;
|
|
327
|
+
header.writeUInt16BE(length, 2);
|
|
328
|
+
} else {
|
|
329
|
+
header = Buffer.alloc(10);
|
|
330
|
+
header[1] = 127;
|
|
331
|
+
header.writeBigUInt64BE(BigInt(length), 2);
|
|
332
|
+
}
|
|
333
|
+
header[0] = 128 | opcode;
|
|
334
|
+
return Buffer.concat([header, payload]);
|
|
335
|
+
};
|
|
336
|
+
var acceptKey = (key) => createHash("sha1").update(key + GUID).digest("base64");
|
|
337
|
+
var rejectHttp = (socket, status, reason) => {
|
|
338
|
+
const body = Buffer.from(reason);
|
|
339
|
+
socket.end(
|
|
340
|
+
`HTTP/1.1 ${status} ${reason}\r
|
|
341
|
+
Connection: close\r
|
|
342
|
+
Content-Type: text/plain\r
|
|
343
|
+
Content-Length: ${body.length}\r
|
|
344
|
+
\r
|
|
345
|
+
${reason}`
|
|
346
|
+
);
|
|
347
|
+
};
|
|
348
|
+
var startedAt = (ms) => new Date(ms).toISOString().replace("T", " ").replace("Z", "");
|
|
349
|
+
var live = /* @__PURE__ */ new Map();
|
|
350
|
+
var createParser = (onFrame, onError) => {
|
|
351
|
+
let buffer = Buffer.alloc(0);
|
|
352
|
+
let fragments = [];
|
|
353
|
+
let fragmentOpcode = 0;
|
|
354
|
+
return {
|
|
355
|
+
push(chunk) {
|
|
356
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
357
|
+
if (buffer.length > MAX_FRAME) {
|
|
358
|
+
onError();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
for (; ; ) {
|
|
362
|
+
if (buffer.length < 2) return;
|
|
363
|
+
const first = buffer[0] ?? 0;
|
|
364
|
+
const second = buffer[1] ?? 0;
|
|
365
|
+
if ((first & 112) !== 0) {
|
|
366
|
+
onError();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const fin = (first & 128) !== 0;
|
|
370
|
+
const opcode = first & 15;
|
|
371
|
+
const masked = (second & 128) !== 0;
|
|
372
|
+
let length = second & 127;
|
|
373
|
+
let offset = 2;
|
|
374
|
+
if (length === 126) {
|
|
375
|
+
if (buffer.length < 4) return;
|
|
376
|
+
length = buffer.readUInt16BE(2);
|
|
377
|
+
offset = 4;
|
|
378
|
+
} else if (length === 127) {
|
|
379
|
+
if (buffer.length < 10) return;
|
|
380
|
+
const wide = buffer.readBigUInt64BE(2);
|
|
381
|
+
if (wide > BigInt(MAX_FRAME)) {
|
|
382
|
+
onError();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
length = Number(wide);
|
|
386
|
+
offset = 10;
|
|
387
|
+
}
|
|
388
|
+
if (length > MAX_FRAME) {
|
|
389
|
+
onError();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const maskLength = masked ? 4 : 0;
|
|
393
|
+
if (buffer.length < offset + maskLength + length) return;
|
|
394
|
+
const raw = buffer.subarray(offset + maskLength, offset + maskLength + length);
|
|
395
|
+
let payload;
|
|
396
|
+
if (masked) {
|
|
397
|
+
const mask = buffer.subarray(offset, offset + 4);
|
|
398
|
+
payload = Buffer.alloc(length);
|
|
399
|
+
for (let i = 0; i < length; i++) payload[i] = (raw[i] ?? 0) ^ (mask[i % 4] ?? 0);
|
|
400
|
+
} else payload = Buffer.from(raw);
|
|
401
|
+
buffer = Buffer.from(buffer.subarray(offset + maskLength + length));
|
|
402
|
+
if (opcode === 0) {
|
|
403
|
+
if (fragmentOpcode === 0) {
|
|
404
|
+
onError();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
fragments.push(payload);
|
|
408
|
+
if (fin) {
|
|
409
|
+
const message = Buffer.concat(fragments);
|
|
410
|
+
const op = fragmentOpcode;
|
|
411
|
+
fragments = [];
|
|
412
|
+
fragmentOpcode = 0;
|
|
413
|
+
onFrame(op, message);
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (opcode === 1 || opcode === 2) {
|
|
418
|
+
if (!fin) {
|
|
419
|
+
fragmentOpcode = opcode;
|
|
420
|
+
fragments = [payload];
|
|
421
|
+
} else onFrame(opcode, payload);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
onFrame(opcode, payload);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
430
|
+
var writeFrame = (socket, opcode, payload) => {
|
|
431
|
+
if (socket.destroyed || !socket.writable) return;
|
|
432
|
+
socket.write(encodeFrame(opcode, payload));
|
|
433
|
+
};
|
|
434
|
+
var helloBody = (record, approximate) => JSON.stringify({
|
|
435
|
+
type: "hello",
|
|
436
|
+
num_connections: openSocketCount(record.namespace),
|
|
437
|
+
debug_info: {
|
|
438
|
+
host: HOST,
|
|
439
|
+
started: startedAt(Date.now()),
|
|
440
|
+
build_number: 1,
|
|
441
|
+
approximate_connection_time: approximate
|
|
442
|
+
},
|
|
443
|
+
connection_info: { app_id: record.appId }
|
|
444
|
+
});
|
|
445
|
+
var disconnectBody = (reason) => JSON.stringify({ type: "disconnect", reason, debug_info: { host: HOST } });
|
|
446
|
+
var acceptUpgrade = (req, socket, head, owned) => {
|
|
447
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
448
|
+
if (url.pathname !== "/link" && url.pathname !== "/link/") {
|
|
449
|
+
rejectHttp(socket, 404, "not_found");
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const ticket = url.searchParams.get("ticket");
|
|
453
|
+
const key = req.headers["sec-websocket-key"];
|
|
454
|
+
if (!ticket || typeof key !== "string" || key.length === 0) {
|
|
455
|
+
rejectHttp(socket, 400, "bad_request");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const record = claimSocketTicket(ticket);
|
|
459
|
+
if (!record) {
|
|
460
|
+
rejectHttp(socket, 404, "ticket_used");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
socket.setNoDelay(true);
|
|
464
|
+
socket.write(
|
|
465
|
+
`HTTP/1.1 101 Switching Protocols\r
|
|
466
|
+
Upgrade: websocket\r
|
|
467
|
+
Connection: Upgrade\r
|
|
468
|
+
Sec-WebSocket-Accept: ${acceptKey(key)}\r
|
|
469
|
+
\r
|
|
470
|
+
`
|
|
471
|
+
);
|
|
472
|
+
const session = {
|
|
473
|
+
ticket,
|
|
474
|
+
socket,
|
|
475
|
+
done: false,
|
|
476
|
+
ping: void 0,
|
|
477
|
+
lifetime: void 0,
|
|
478
|
+
hello: void 0
|
|
479
|
+
};
|
|
480
|
+
live.set(ticket, session);
|
|
481
|
+
owned.add(ticket);
|
|
482
|
+
const cleanup = () => {
|
|
483
|
+
if (session.done) return;
|
|
484
|
+
session.done = true;
|
|
485
|
+
if (session.ping) clearInterval(session.ping);
|
|
486
|
+
if (session.lifetime) clearTimeout(session.lifetime);
|
|
487
|
+
if (session.hello) clearTimeout(session.hello);
|
|
488
|
+
live.delete(ticket);
|
|
489
|
+
owned.delete(ticket);
|
|
490
|
+
unbindSocketTransport(ticket);
|
|
491
|
+
markSocketClosed(ticket);
|
|
492
|
+
};
|
|
493
|
+
const finish = (code) => {
|
|
494
|
+
if (session.done) return;
|
|
495
|
+
if (code === 1006) socket.destroy();
|
|
496
|
+
else {
|
|
497
|
+
const payload = Buffer.alloc(2);
|
|
498
|
+
payload.writeUInt16BE(code, 0);
|
|
499
|
+
writeFrame(socket, 8, payload);
|
|
500
|
+
socket.end();
|
|
501
|
+
}
|
|
502
|
+
cleanup();
|
|
503
|
+
};
|
|
504
|
+
bindSocketTransport(ticket, {
|
|
505
|
+
disconnect(reason, code) {
|
|
506
|
+
if (session.done) return;
|
|
507
|
+
writeFrame(socket, 1, Buffer.from(disconnectBody(reason)));
|
|
508
|
+
const timer = setTimeout(() => finish(code), 20);
|
|
509
|
+
timer.unref();
|
|
510
|
+
},
|
|
511
|
+
destroy() {
|
|
512
|
+
socket.destroy();
|
|
513
|
+
cleanup();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
const parser = createParser(
|
|
517
|
+
(opcode, data) => {
|
|
518
|
+
if (session.done) return;
|
|
519
|
+
if (opcode === 8) {
|
|
520
|
+
finish(1e3);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (opcode === 9) {
|
|
524
|
+
writeFrame(socket, 10, data);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (opcode !== 1) return;
|
|
528
|
+
let parsed;
|
|
529
|
+
try {
|
|
530
|
+
parsed = JSON.parse(data.toString("utf8"));
|
|
531
|
+
} catch {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (isRecord(parsed) && typeof parsed.envelope_id === "string" && parsed.type === void 0)
|
|
535
|
+
return;
|
|
536
|
+
},
|
|
537
|
+
() => finish(1002)
|
|
538
|
+
);
|
|
539
|
+
socket.on("data", (chunk) => {
|
|
540
|
+
if (session.done) return;
|
|
541
|
+
parser.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
542
|
+
});
|
|
543
|
+
socket.on("close", cleanup);
|
|
544
|
+
socket.on("error", cleanup);
|
|
545
|
+
socket.on("end", cleanup);
|
|
546
|
+
if (head.length > 0) parser.push(head);
|
|
547
|
+
const approximate = url.searchParams.get("debug_reconnects") === "true" ? 360 : record.lifetimeMs !== null && record.lifetimeMs > 0 ? Math.max(1, Math.ceil(record.lifetimeMs / 1e3)) : 3600;
|
|
548
|
+
const deliverHello = () => {
|
|
549
|
+
if (session.done) return;
|
|
550
|
+
const frame = Buffer.from(helloBody(record, approximate));
|
|
551
|
+
socket.write(encodeFrame(1, frame), () => {
|
|
552
|
+
if (session.done) return;
|
|
553
|
+
if (record.abnormalClose) {
|
|
554
|
+
socket.destroy();
|
|
555
|
+
cleanup();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (record.closeAfterHello) {
|
|
559
|
+
finish(1e3);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
session.ping = setInterval(() => {
|
|
563
|
+
if (session.done) return;
|
|
564
|
+
writeFrame(socket, 9, Buffer.from("ping"));
|
|
565
|
+
}, SERVER_PING_MS);
|
|
566
|
+
session.ping.unref();
|
|
567
|
+
if (record.lifetimeMs !== null && record.lifetimeMs > 0) {
|
|
568
|
+
session.lifetime = setTimeout(() => {
|
|
569
|
+
if (session.done) return;
|
|
570
|
+
writeFrame(socket, 1, Buffer.from(disconnectBody("warning")));
|
|
571
|
+
const timer = setTimeout(() => finish(1e3), 20);
|
|
572
|
+
timer.unref();
|
|
573
|
+
}, record.lifetimeMs);
|
|
574
|
+
session.lifetime.unref();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
};
|
|
578
|
+
if (record.helloLatencyMs > 0) {
|
|
579
|
+
session.hello = setTimeout(deliverHello, record.helloLatencyMs);
|
|
580
|
+
session.hello.unref();
|
|
581
|
+
} else deliverHello();
|
|
582
|
+
};
|
|
583
|
+
var attachSocketServer = (server) => {
|
|
584
|
+
if (attached.has(server)) return;
|
|
585
|
+
attached.add(server);
|
|
586
|
+
const owned = /* @__PURE__ */ new Set();
|
|
587
|
+
server.on("upgrade", (req, socket, head) => {
|
|
588
|
+
acceptUpgrade(req, socket, head, owned);
|
|
589
|
+
});
|
|
590
|
+
server.on("close", () => {
|
|
591
|
+
for (const ticket of owned) live.get(ticket)?.socket.destroy();
|
|
592
|
+
owned.clear();
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
|
|
305
596
|
// src/server.ts
|
|
306
597
|
var DEFAULT_PORT = 8808;
|
|
307
598
|
var createServer2 = async (options = {}) => {
|
|
@@ -311,6 +602,7 @@ var createServer2 = async (options = {}) => {
|
|
|
311
602
|
port: port ?? 0,
|
|
312
603
|
...host !== void 0 ? { host } : {}
|
|
313
604
|
});
|
|
605
|
+
attachSocketServer(listening.server);
|
|
314
606
|
return { ...listening, runtime };
|
|
315
607
|
};
|
|
316
608
|
var text = (value) => typeof value === "string" ? value : void 0;
|
|
@@ -340,9 +632,13 @@ var serveTarget = {
|
|
|
340
632
|
...common.onLog ? { onLog: common.onLog } : {}
|
|
341
633
|
});
|
|
342
634
|
},
|
|
635
|
+
listening: (listening) => {
|
|
636
|
+
attachSocketServer(listening.server);
|
|
637
|
+
},
|
|
343
638
|
banner: () => [
|
|
344
639
|
"webhooks: point SLACK_*_WEBHOOK_URL at <this>/services/T000/B000/XXXX (any path works until you POST /__admin/hooks)",
|
|
345
640
|
"web api: Authorization: Bearer xoxb-\u2026; @slack/web-api: new WebClient(token, {slackApiUrl: '<this>/api/'})",
|
|
641
|
+
"socket mode: Authorization: Bearer xapp-\u2026 on POST <this>/api/apps.connections.open; Bolt: socketMode + clientOptions.slackApiUrl '<this>/api/'",
|
|
346
642
|
"outbox: GET /__admin/outbox?webhook=/services/T/B/X or ?channel=C\u2026"
|
|
347
643
|
]
|
|
348
644
|
};
|
|
@@ -354,4 +650,4 @@ export {
|
|
|
354
650
|
createServer2 as createServer,
|
|
355
651
|
serveTarget
|
|
356
652
|
};
|
|
357
|
-
//# sourceMappingURL=chunk-
|
|
653
|
+
//# sourceMappingURL=chunk-GPFJICAV.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/socket-server.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 /** After the HTTP server is bound. A service that speaks more than HTTP attaches it here. */\n listening?(server: Listening): void\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 // A rejection says what arrived (never the body) and why the service refused it.\n const received = entry.request\n ? ` received=${entry.request.bodyBytes === null ? \"?\" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? \"-\"}${\n entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : \"\"\n }`\n : \"\"\n const issues = entry.issues?.length\n ? ` issues=${entry.issues\n .slice(0, 3)\n .map((issue) => `${issue.path || \"$\"}: ${issue.message}`)\n .join(\"; \")}${entry.issues.length > 3 ? ` (+${entry.issues.length - 3})` : \"\"}`\n : \"\"\n console.log(\n `${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`,\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 target.listening?.(listening)\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 console.log(`${target.name} admin ui: ${listening.url}/__admin/ui`)\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 { createHash } from \"node:crypto\"\nimport type { IncomingMessage, Server } from \"node:http\"\nimport type { Socket } from \"node:net\"\nimport {\n bindSocketTransport,\n claimSocketTicket,\n markSocketClosed,\n openSocketCount,\n type SocketRecord,\n unbindSocketTransport,\n} from \"./sockets.js\"\n\nconst GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n/** Under Socket Mode's default 30s server-ping timeout, and frequent enough to test quickly. */\nconst SERVER_PING_MS = 2_000\nconst MAX_FRAME = 1_000_000\nconst HOST = \"applink-mockingbird\"\n\nconst attached = new WeakSet<Server>()\n\nconst encodeFrame = (opcode: number, payload: Buffer): Buffer => {\n const length = payload.length\n let header: Buffer\n if (length < 126) {\n header = Buffer.alloc(2)\n header[1] = length\n } else if (length < 65_536) {\n header = Buffer.alloc(4)\n header[1] = 126\n header.writeUInt16BE(length, 2)\n } else {\n header = Buffer.alloc(10)\n header[1] = 127\n header.writeBigUInt64BE(BigInt(length), 2)\n }\n header[0] = 0x80 | opcode\n return Buffer.concat([header, payload])\n}\n\nconst acceptKey = (key: string): string =>\n createHash(\"sha1\")\n .update(key + GUID)\n .digest(\"base64\")\n\nconst rejectHttp = (socket: Socket, status: number, reason: string): void => {\n const body = Buffer.from(reason)\n socket.end(\n `HTTP/1.1 ${status} ${reason}\\r\\nConnection: close\\r\\nContent-Type: text/plain\\r\\nContent-Length: ${body.length}\\r\\n\\r\\n${reason}`,\n )\n}\n\nconst startedAt = (ms: number): string =>\n new Date(ms).toISOString().replace(\"T\", \" \").replace(\"Z\", \"\")\n\ntype Session = {\n ticket: string\n socket: Socket\n done: boolean\n ping: ReturnType<typeof setInterval> | undefined\n lifetime: ReturnType<typeof setTimeout> | undefined\n hello: ReturnType<typeof setTimeout> | undefined\n}\n\nconst live = new Map<string, Session>()\n\nconst createParser = (onFrame: (opcode: number, data: Buffer) => void, onError: () => void) => {\n let buffer = Buffer.alloc(0) as Buffer\n let fragments: Buffer[] = []\n let fragmentOpcode = 0\n return {\n push(chunk: Buffer) {\n buffer = Buffer.concat([buffer, chunk])\n if (buffer.length > MAX_FRAME) {\n onError()\n return\n }\n for (;;) {\n if (buffer.length < 2) return\n const first = buffer[0] ?? 0\n const second = buffer[1] ?? 0\n if ((first & 0x70) !== 0) {\n onError()\n return\n }\n const fin = (first & 0x80) !== 0\n const opcode = first & 0x0f\n const masked = (second & 0x80) !== 0\n let length = second & 0x7f\n let offset = 2\n if (length === 126) {\n if (buffer.length < 4) return\n length = buffer.readUInt16BE(2)\n offset = 4\n } else if (length === 127) {\n if (buffer.length < 10) return\n const wide = buffer.readBigUInt64BE(2)\n if (wide > BigInt(MAX_FRAME)) {\n onError()\n return\n }\n length = Number(wide)\n offset = 10\n }\n if (length > MAX_FRAME) {\n onError()\n return\n }\n const maskLength = masked ? 4 : 0\n if (buffer.length < offset + maskLength + length) return\n const raw = buffer.subarray(offset + maskLength, offset + maskLength + length)\n let payload: Buffer\n if (masked) {\n const mask = buffer.subarray(offset, offset + 4)\n payload = Buffer.alloc(length)\n for (let i = 0; i < length; i++) payload[i] = (raw[i] ?? 0) ^ (mask[i % 4] ?? 0)\n } else payload = Buffer.from(raw)\n buffer = Buffer.from(buffer.subarray(offset + maskLength + length))\n if (opcode === 0x0) {\n if (fragmentOpcode === 0) {\n onError()\n return\n }\n fragments.push(payload)\n if (fin) {\n const message = Buffer.concat(fragments)\n const op = fragmentOpcode\n fragments = []\n fragmentOpcode = 0\n onFrame(op, message)\n }\n continue\n }\n if (opcode === 0x1 || opcode === 0x2) {\n if (!fin) {\n fragmentOpcode = opcode\n fragments = [payload]\n } else onFrame(opcode, payload)\n continue\n }\n onFrame(opcode, payload)\n }\n },\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value)\n\nconst writeFrame = (socket: Socket, opcode: number, payload: Buffer): void => {\n if (socket.destroyed || !socket.writable) return\n socket.write(encodeFrame(opcode, payload))\n}\n\nconst helloBody = (record: SocketRecord, approximate: number) =>\n JSON.stringify({\n type: \"hello\",\n num_connections: openSocketCount(record.namespace),\n debug_info: {\n host: HOST,\n started: startedAt(Date.now()),\n build_number: 1,\n approximate_connection_time: approximate,\n },\n connection_info: { app_id: record.appId },\n })\n\nconst disconnectBody = (reason: string) =>\n JSON.stringify({ type: \"disconnect\", reason, debug_info: { host: HOST } })\n\nconst acceptUpgrade = (\n req: IncomingMessage,\n socket: Socket,\n head: Buffer,\n owned: Set<string>,\n): void => {\n const url = new URL(req.url ?? \"/\", \"http://localhost\")\n if (url.pathname !== \"/link\" && url.pathname !== \"/link/\") {\n rejectHttp(socket, 404, \"not_found\")\n return\n }\n const ticket = url.searchParams.get(\"ticket\")\n const key = req.headers[\"sec-websocket-key\"]\n if (!ticket || typeof key !== \"string\" || key.length === 0) {\n rejectHttp(socket, 400, \"bad_request\")\n return\n }\n const record = claimSocketTicket(ticket)\n if (!record) {\n rejectHttp(socket, 404, \"ticket_used\")\n return\n }\n socket.setNoDelay(true)\n socket.write(\n \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n \"Upgrade: websocket\\r\\n\" +\n \"Connection: Upgrade\\r\\n\" +\n `Sec-WebSocket-Accept: ${acceptKey(key)}\\r\\n` +\n \"\\r\\n\",\n )\n const session: Session = {\n ticket,\n socket,\n done: false,\n ping: undefined,\n lifetime: undefined,\n hello: undefined,\n }\n live.set(ticket, session)\n owned.add(ticket)\n const cleanup = () => {\n if (session.done) return\n session.done = true\n if (session.ping) clearInterval(session.ping)\n if (session.lifetime) clearTimeout(session.lifetime)\n if (session.hello) clearTimeout(session.hello)\n live.delete(ticket)\n owned.delete(ticket)\n unbindSocketTransport(ticket)\n markSocketClosed(ticket)\n }\n const finish = (code: number) => {\n if (session.done) return\n if (code === 1006) socket.destroy()\n else {\n const payload = Buffer.alloc(2)\n payload.writeUInt16BE(code, 0)\n writeFrame(socket, 0x8, payload)\n socket.end()\n }\n cleanup()\n }\n bindSocketTransport(ticket, {\n disconnect(reason, code) {\n if (session.done) return\n writeFrame(socket, 0x1, Buffer.from(disconnectBody(reason)))\n const timer = setTimeout(() => finish(code), 20)\n timer.unref()\n },\n destroy() {\n socket.destroy()\n cleanup()\n },\n })\n const parser = createParser(\n (opcode, data) => {\n if (session.done) return\n if (opcode === 0x8) {\n finish(1000)\n return\n }\n if (opcode === 0x9) {\n writeFrame(socket, 0xa, data)\n return\n }\n if (opcode !== 0x1) return\n let parsed: unknown\n try {\n parsed = JSON.parse(data.toString(\"utf8\"))\n } catch {\n return\n }\n // An acknowledgement. Socket Mode does not reply, and a stray ack must not drop the socket.\n if (isRecord(parsed) && typeof parsed.envelope_id === \"string\" && parsed.type === undefined)\n return\n },\n () => finish(1002),\n )\n socket.on(\"data\", (chunk: Buffer | string) => {\n if (session.done) return\n parser.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk)\n })\n socket.on(\"close\", cleanup)\n socket.on(\"error\", cleanup)\n socket.on(\"end\", cleanup)\n if (head.length > 0) parser.push(head)\n\n const approximate =\n url.searchParams.get(\"debug_reconnects\") === \"true\"\n ? 360\n : record.lifetimeMs !== null && record.lifetimeMs > 0\n ? Math.max(1, Math.ceil(record.lifetimeMs / 1000))\n : 3600\n const deliverHello = () => {\n if (session.done) return\n const frame = Buffer.from(helloBody(record, approximate))\n socket.write(encodeFrame(0x1, frame), () => {\n if (session.done) return\n if (record.abnormalClose) {\n socket.destroy()\n cleanup()\n return\n }\n if (record.closeAfterHello) {\n finish(1000)\n return\n }\n session.ping = setInterval(() => {\n if (session.done) return\n writeFrame(socket, 0x9, Buffer.from(\"ping\"))\n }, SERVER_PING_MS)\n session.ping.unref()\n if (record.lifetimeMs !== null && record.lifetimeMs > 0) {\n session.lifetime = setTimeout(() => {\n if (session.done) return\n writeFrame(socket, 0x1, Buffer.from(disconnectBody(\"warning\")))\n const timer = setTimeout(() => finish(1000), 20)\n timer.unref()\n }, record.lifetimeMs)\n session.lifetime.unref()\n }\n })\n }\n if (record.helloLatencyMs > 0) {\n session.hello = setTimeout(deliverHello, record.helloLatencyMs)\n session.hello.unref()\n } else deliverHello()\n}\n\n/** Accept Socket Mode WebSocket upgrades on a Node HTTP server. Idempotent per server. */\nexport const attachSocketServer = (server: Server): void => {\n if (attached.has(server)) return\n attached.add(server)\n const owned = new Set<string>()\n server.on(\"upgrade\", (req, socket, head) => {\n acceptUpgrade(req, socket as Socket, head, owned)\n })\n server.on(\"close\", () => {\n for (const ticket of owned) live.get(ticket)?.socket.destroy()\n owned.clear()\n })\n}\n", "/// <reference types=\"node\" />\nimport { type Listening, listen, type ServeTarget } from \"@crvouga/mockingbird-adapter-node\"\nimport { createRuntime, type SlackRuntime, type SlackRuntimeOptions } from \"./runtime.js\"\nimport { attachSocketServer } from \"./socket-server.js\"\n\n/** Port `mockingbird-slack serve` listens on when none is given. */\nexport const DEFAULT_PORT = 8808\n\nexport type SlackServerOptions = SlackRuntimeOptions & {\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 SlackServer = Listening & { runtime: SlackRuntime }\n\n/** Serve the Slack mock over `node:http`. */\nexport const createServer = async (options: SlackServerOptions = {}): Promise<SlackServer> => {\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 attachSocketServer(listening.server)\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 Slack mock from flags. */\nexport const serveTarget: ServeTarget = {\n name: \"slack\",\n defaultPort: DEFAULT_PORT,\n options: {\n token: {\n type: \"string\",\n value: \"<xoxb-\u2026>\",\n description:\n \"Only accept this bot token on the Web API (default: any xoxb-/xoxp- token is accepted)\",\n },\n \"strict-channels\": {\n type: \"boolean\",\n description: \"Answer channel_not_found for channels not created through /__admin/channels\",\n },\n },\n create: (values, common) => {\n const token = text(values.token)\n return createRuntime({\n settings: {\n ...(token ? { tokens: [token] } : {}),\n ...(values[\"strict-channels\"] === true ? { strictChannels: true } : {}),\n },\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n ...(common.seed !== undefined ? { seed: common.seed } : {}),\n ...(common.onLog ? { onLog: common.onLog } : {}),\n })\n },\n listening: (listening) => {\n attachSocketServer(listening.server)\n },\n banner: () => [\n \"webhooks: point SLACK_*_WEBHOOK_URL at <this>/services/T000/B000/XXXX (any path works until you POST /__admin/hooks)\",\n \"web api: Authorization: Bearer xoxb-\u2026; @slack/web-api: new WebClient(token, {slackApiUrl: '<this>/api/'})\",\n \"socket mode: Authorization: Bearer xapp-\u2026 on POST <this>/api/apps.connections.open; Bolt: socketMode + clientOptions.slackApiUrl '<this>/api/'\",\n \"outbox: GET /__admin/outbox?webhook=/services/T/B/X or ?channel=C\u2026\",\n ],\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["createServer"]
|
|
7
|
+
}
|