morpho-socket 0.0.0-stage → 0.2.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.
Files changed (59) hide show
  1. package/README.md +155 -2
  2. package/docs/building-a-bot.md +402 -0
  3. package/package.json +25 -4
  4. package/packages/commands/README.md +78 -0
  5. package/packages/commands/antiflood.js +39 -0
  6. package/packages/commands/index.js +116 -0
  7. package/packages/commands/package.json +1 -0
  8. package/packages/context/README.md +97 -0
  9. package/packages/context/group.js +245 -0
  10. package/packages/context/index.js +132 -0
  11. package/packages/context/package.json +1 -0
  12. package/packages/context/participants.js +12 -0
  13. package/packages/context/send.js +127 -0
  14. package/packages/core/README.md +50 -0
  15. package/packages/core/baileys.js +25 -0
  16. package/packages/core/codec.js +17 -0
  17. package/packages/core/index.js +86 -0
  18. package/packages/core/package.json +7 -0
  19. package/packages/core/store.js +11 -0
  20. package/packages/groups/README.md +42 -0
  21. package/packages/groups/index.js +205 -0
  22. package/packages/groups/package.json +7 -0
  23. package/packages/interactive/README.md +41 -0
  24. package/packages/interactive/index.js +92 -0
  25. package/packages/interactive/package.json +1 -0
  26. package/packages/jid/README.md +51 -0
  27. package/packages/jid/index.js +285 -0
  28. package/packages/jid/package.json +7 -0
  29. package/packages/kv/README.md +45 -0
  30. package/packages/kv/index.js +164 -0
  31. package/packages/kv/package.json +8 -0
  32. package/packages/media/README.md +70 -0
  33. package/packages/media/cache.js +116 -0
  34. package/packages/media/erros.js +45 -0
  35. package/packages/media/find.js +51 -0
  36. package/packages/media/index.js +261 -0
  37. package/packages/media/package.json +8 -0
  38. package/packages/message/README.md +53 -0
  39. package/packages/message/index.js +317 -0
  40. package/packages/message/package.json +7 -0
  41. package/packages/pool/README.md +75 -0
  42. package/packages/pool/index.js +369 -0
  43. package/packages/pool/package.json +8 -0
  44. package/packages/presence/README.md +40 -0
  45. package/packages/presence/index.js +80 -0
  46. package/packages/presence/package.json +1 -0
  47. package/packages/reply/README.md +36 -0
  48. package/packages/reply/index.js +126 -0
  49. package/packages/reply/package.json +7 -0
  50. package/packages/reply/voice.js +32 -0
  51. package/packages/sent/README.md +27 -0
  52. package/packages/sent/index.js +65 -0
  53. package/packages/sent/package.json +1 -0
  54. package/packages/store-file/README.md +28 -0
  55. package/packages/store-file/index.js +63 -0
  56. package/packages/store-file/package.json +8 -0
  57. package/packages/store-sqlite/README.md +25 -0
  58. package/packages/store-sqlite/index.js +98 -0
  59. package/packages/store-sqlite/package.json +9 -0
@@ -0,0 +1,317 @@
1
+ // @morpho/message
2
+ //
3
+ // Problema que isto resolve: o objeto cru do Baileys (msg.message) tem a mensagem de verdade escondida
4
+ // dentro de "embrulhos" (ephemeralMessage, viewOnceMessageV2, editedMessage...) e cada coisa mora num
5
+ // lugar diferente (texto em .conversation OU .extendedTextMessage.text OU .imageMessage.caption; menções
6
+ // e citação em .contextInfo de qualquer tipo). No Morvyn isso estava refeito em 4 arquivos, cada um
7
+ // com um pedaço da lógica. Aqui vira UMA função: parse(msg) → objeto simples e sempre do mesmo formato.
8
+ //
9
+ // const { parse, findMedia } = require("@morpho/message")
10
+ // const m = parse(msg)
11
+ // m.type // "text" | "image" | "video" | "gif" | "videoNote" | "audio" | "voice" | "sticker" | "document" | ...
12
+ // m.text // texto ou legenda ("" se não tem)
13
+ // m.mentions // jids marcados (em QUALQUER tipo de mensagem, não só texto)
14
+ // m.media // { type, mimetype, seconds, size, ptt, animated, viewOnce, node... } ou null
15
+ // m.quoted // a mensagem citada, já parseada do mesmo jeito (ou null)
16
+ //
17
+ // Nunca lança erro: msg nula, sem descriptografar ou de tipo desconhecido vira type "empty"/"unknown".
18
+
19
+ // "embrulhos": a mensagem de verdade está dentro de .message
20
+ const WRAPPERS = [
21
+ "ephemeralMessage", // chat com mensagens temporárias
22
+ "viewOnceMessage", // visualização única
23
+ "viewOnceMessageV2",
24
+ "viewOnceMessageV2Extension",
25
+ "documentWithCaptionMessage", // documento com legenda
26
+ "editedMessage", // mensagem editada
27
+ "deviceSentMessage", // enviada por outro aparelho do próprio bot
28
+ "groupMentionedMessage",
29
+ "botInvokeMessage",
30
+ "lottieStickerMessage" // figurinha lottie: o stickerMessage está dentro
31
+ ]
32
+
33
+ // chaves que não são o conteúdo
34
+ const IGNORE = new Set(["messageContextInfo", "senderKeyDistributionMessage"])
35
+
36
+ // chave do Baileys → tipo simples (função = depende do conteúdo)
37
+ const TYPE_OF_KIND = {
38
+ conversation: "text",
39
+ extendedTextMessage: "text",
40
+ imageMessage: "image",
41
+ videoMessage: n => (n?.gifPlayback ? "gif" : "video"),
42
+ ptvMessage: "videoNote", // vídeo redondo
43
+ audioMessage: n => (n?.ptt ? "voice" : "audio"),
44
+ stickerMessage: "sticker",
45
+ documentMessage: "document",
46
+ contactMessage: "contact",
47
+ contactsArrayMessage: "contact",
48
+ locationMessage: "location",
49
+ liveLocationMessage: "location",
50
+ pollCreationMessage: "poll",
51
+ pollCreationMessageV2: "poll",
52
+ pollCreationMessageV3: "poll",
53
+ pollUpdateMessage: "pollVote",
54
+ reactionMessage: "reaction",
55
+ buttonsMessage: "interactive",
56
+ listMessage: "interactive",
57
+ templateMessage: "interactive",
58
+ interactiveMessage: "interactive",
59
+ buttonsResponseMessage: "reply",
60
+ listResponseMessage: "reply",
61
+ templateButtonReplyMessage: "reply",
62
+ interactiveResponseMessage: "reply",
63
+ groupInviteMessage: "invite",
64
+ eventMessage: "event",
65
+ albumMessage: "album",
66
+ productMessage: "other",
67
+ orderMessage: "other",
68
+ requestPaymentMessage: "other",
69
+ sendPaymentMessage: "other",
70
+ callLogMesssage: "other", // (sic) o nome vem assim do WhatsApp
71
+ newsletterAdminInviteMessage: "other"
72
+ }
73
+
74
+ // chave do Baileys → tipo de mídia (o mesmo nome que o downloadContentFromMessage espera)
75
+ const MEDIA_OF_KIND = {
76
+ imageMessage: "image",
77
+ videoMessage: "video",
78
+ ptvMessage: "video",
79
+ audioMessage: "audio",
80
+ stickerMessage: "sticker",
81
+ documentMessage: "document"
82
+ }
83
+
84
+ const MEDIA_TYPES = ["image", "video", "audio", "sticker", "document"]
85
+
86
+ // número, texto, bigint ou Long do protobuf (fileLength, seconds, messageTimestamp...) → number
87
+ function toNumber(v) {
88
+ if (v == null) return 0
89
+ if (typeof v === "number") return Number.isFinite(v) ? v : 0
90
+ if (typeof v === "bigint") return Number(v)
91
+ if (typeof v === "string") { const n = Number(v); return Number.isFinite(n) ? n : 0 }
92
+ if (typeof v === "object") {
93
+ if (typeof v.toNumber === "function") return toNumber(v.toNumber())
94
+ if (typeof v.low === "number" && typeof v.high === "number") return (v.high >>> 0) * 4294967296 + (v.low >>> 0)
95
+ }
96
+ return 0
97
+ }
98
+
99
+ const isObj = x => x !== null && typeof x === "object"
100
+
101
+ // tira os embrulhos. { message, viewOnce, edited, ephemeral } — message é o que está dentro (ou null)
102
+ function unwrap(message) {
103
+ let m = isObj(message) ? message : null
104
+ let viewOnce = false, edited = false, ephemeral = false
105
+ for (let i = 0; i < 10 && m; i++) {
106
+ const k = WRAPPERS.find(w => m[w])
107
+ if (!k) break
108
+ if (k.startsWith("viewOnce")) viewOnce = true
109
+ else if (k === "editedMessage") edited = true
110
+ else if (k === "ephemeralMessage") ephemeral = true
111
+ m = isObj(m[k]) && isObj(m[k].message) ? m[k].message : null
112
+ }
113
+ return { message: m, viewOnce, edited, ephemeral }
114
+ }
115
+
116
+ // acha o conteúdo de verdade. Edição vira o conteúdo NOVO com edited=true; apagar vira { revoked }.
117
+ function resolve(message) {
118
+ let { message: m, viewOnce, edited, ephemeral } = unwrap(message)
119
+ let editTarget = null, revoked = null, protocol = null
120
+
121
+ for (let i = 0; i < 3 && m; i++) {
122
+ const kind = Object.keys(m).find(k => !IGNORE.has(k) && m[k] != null)
123
+ if (!kind) return { kind: null, node: null, viewOnce, edited, ephemeral, editTarget, revoked, protocol }
124
+ const node = m[kind]
125
+
126
+ if (kind === "protocolMessage") {
127
+ const t = node?.type
128
+ if ((t === 14 || t === "MESSAGE_EDIT") && isObj(node.editedMessage)) {
129
+ edited = true
130
+ editTarget = node.key || null
131
+ const inner = unwrap(node.editedMessage)
132
+ viewOnce = viewOnce || inner.viewOnce
133
+ ephemeral = ephemeral || inner.ephemeral
134
+ m = inner.message
135
+ continue
136
+ }
137
+ if (t === 0 || t === "REVOKE") revoked = node?.key || null
138
+ else if (t === 3 || t === "EPHEMERAL_SETTING") protocol = "ephemeralSetting"
139
+ else protocol = String(t ?? "unknown")
140
+ }
141
+ return { kind, node, viewOnce, edited, ephemeral, editTarget, revoked, protocol }
142
+ }
143
+ return { kind: null, node: null, viewOnce, edited, ephemeral, editTarget, revoked, protocol }
144
+ }
145
+
146
+ function textOf(kind, node) {
147
+ if (kind === "conversation") return typeof node === "string" ? node : ""
148
+ if (!isObj(node)) return ""
149
+ switch (kind) {
150
+ case "extendedTextMessage": return String(node.text || "")
151
+ case "imageMessage":
152
+ case "videoMessage":
153
+ case "documentMessage": return String(node.caption || "")
154
+ default: return ""
155
+ }
156
+ }
157
+
158
+ // resposta de botão / lista / modelo / botão nativo → { id, text } (id null se não deu para ler)
159
+ function selectionOf(kind, node) {
160
+ if (!isObj(node)) return null
161
+ switch (kind) {
162
+ case "buttonsResponseMessage":
163
+ return { id: node.selectedButtonId ?? null, text: String(node.selectedDisplayText || "") }
164
+ case "listResponseMessage":
165
+ return { id: node.singleSelectReply?.selectedRowId ?? null, text: String(node.title || "") }
166
+ case "templateButtonReplyMessage":
167
+ return { id: node.selectedId ?? null, text: String(node.selectedDisplayText || "") }
168
+ case "interactiveResponseMessage": {
169
+ let id = null
170
+ const json = node.nativeFlowResponseMessage?.paramsJson
171
+ if (json) { try { id = JSON.parse(json)?.id ?? null } catch { /* json inválido: sem id */ } }
172
+ return { id, text: String(node.body?.text || "") }
173
+ }
174
+ default: return null
175
+ }
176
+ }
177
+
178
+ function mediaOf(kind, node, viewOnce) {
179
+ const type = MEDIA_OF_KIND[kind]
180
+ if (!type || !isObj(node)) return null
181
+ const mimetype = String(node.mimetype || "")
182
+ let family = type
183
+ if (type === "document") {
184
+ const f = mimetype.split("/")[0]
185
+ family = f === "image" || f === "video" || f === "audio" ? f : "other"
186
+ }
187
+ return {
188
+ type, // "image" | "video" | "audio" | "sticker" | "document"
189
+ kind, // chave original (ptvMessage continua type "video")
190
+ family, // igual a type, exceto documento: "image"/"video"/"audio" pelo mimetype, ou "other"
191
+ mimetype,
192
+ seconds: toNumber(node.seconds),
193
+ size: toNumber(node.fileLength),
194
+ ptt: !!node.ptt,
195
+ gif: !!node.gifPlayback,
196
+ round: kind === "ptvMessage",
197
+ animated: !!node.isAnimated,
198
+ fileName: node.fileName || null,
199
+ caption: String(node.caption || ""),
200
+ width: toNumber(node.width),
201
+ height: toNumber(node.height),
202
+ viewOnce: !!viewOnce || node.viewOnce === true,
203
+ node // o que vai para o downloadContentFromMessage(node, type)
204
+ }
205
+ }
206
+
207
+ function lazy(obj, name, fn) {
208
+ let done = false, val
209
+ Object.defineProperty(obj, name, {
210
+ enumerable: true, configurable: true,
211
+ get() { if (!done) { val = fn(); done = true } return val }
212
+ })
213
+ }
214
+
215
+ const cache = new WeakMap()
216
+
217
+ function build(msg) {
218
+ const key = isObj(msg?.key) ? msg.key : {}
219
+ const chat = key.remoteJid || null
220
+ const participant = key.participant || null
221
+ const r = resolve(msg?.message)
222
+
223
+ let type
224
+ if (!msg?.message) type = msg?.messageStubType ? "system" : "empty"
225
+ else if (!r.kind) type = "empty"
226
+ else if (r.revoked) type = "revoke"
227
+ else if (r.kind === "protocolMessage") type = "system"
228
+ else {
229
+ const t = TYPE_OF_KIND[r.kind]
230
+ type = typeof t === "function" ? t(r.node) : (t || "unknown")
231
+ }
232
+
233
+ const node = r.node
234
+ const context = isObj(node) && isObj(node.contextInfo) ? node.contextInfo : null
235
+ const media = mediaOf(r.kind, node, r.viewOnce)
236
+ const reaction = r.kind === "reactionMessage" && isObj(node) ? { emoji: String(node.text || ""), key: node.key || null } : null
237
+
238
+ const m = {
239
+ // quem / onde (ainda crus: o @morpho/jid é que resolve LID → número)
240
+ id: key.id || null,
241
+ chat,
242
+ sender: participant || chat,
243
+ senderAlt: key.participantAlt || key.remoteJidAlt || null,
244
+ fromMe: !!key.fromMe,
245
+ isGroup: !!chat && chat.endsWith("@g.us"),
246
+ isStatus: chat === "status@broadcast",
247
+ isNewsletter: !!chat && chat.endsWith("@newsletter"),
248
+ pushName: msg?.pushName || "",
249
+ timestamp: toNumber(msg?.messageTimestamp),
250
+ key: msg?.key || null,
251
+
252
+ // o que é
253
+ type, // ver lista no topo; "unknown" = chave que o Morpho ainda não conhece (veja m.kind)
254
+ kind: r.kind, // chave original do Baileys, ex.: "imageMessage"
255
+ text: textOf(r.kind, node),
256
+ media,
257
+ selection: r.kind && type === "reply" ? selectionOf(r.kind, node) : null,
258
+ reaction,
259
+ pollKey: r.kind === "pollUpdateMessage" ? (node?.pollCreationMessageKey || null) : null,
260
+
261
+ // detalhes
262
+ mentions: Array.isArray(context?.mentionedJid) ? context.mentionedJid.slice() : [],
263
+ forwarded: !!context?.isForwarded,
264
+ viewOnce: r.viewOnce || (isObj(node) && node.viewOnce === true),
265
+ edited: r.edited,
266
+ editTarget: r.editTarget, // key da mensagem que foi editada (só quando edited)
267
+ revoked: r.revoked, // key da mensagem apagada (só quando type "revoke")
268
+ ephemeral: r.ephemeral, // chegou num chat de mensagens temporárias
269
+ expiration: toNumber(context?.expiration), // segundos até sumir (0 = não some)
270
+ stub: msg?.messageStubType ?? null,
271
+ protocol: r.protocol, // "ephemeralSetting" | código do protocolo | null
272
+
273
+ context, // contextInfo cru
274
+ node // conteúdo cru (string no caso de conversation)
275
+ }
276
+
277
+ lazy(m, "quoted", () => {
278
+ if (!context?.quotedMessage) return null
279
+ return parse({
280
+ key: { id: context.stanzaId || null, remoteJid: context.remoteJid || chat, participant: context.participant || undefined },
281
+ message: context.quotedMessage
282
+ })
283
+ })
284
+
285
+ Object.defineProperty(m, "is", { value: (...types) => types.includes(m.type), enumerable: false })
286
+ return m
287
+ }
288
+
289
+ // Parseia uma mensagem do Baileys (o item de messages.upsert). Parsear a mesma msg de novo devolve o mesmo objeto.
290
+ function parse(msg) {
291
+ if (!isObj(msg)) return build(null)
292
+ let p = cache.get(msg)
293
+ if (!p) { p = build(msg); cache.set(msg, p) }
294
+ return p
295
+ }
296
+
297
+ // Acha a mídia que o comando deve usar. Por padrão a da mensagem MARCADA tem prioridade (responder uma figurinha
298
+ // com /s), senão a da própria mensagem — igual ao utils/midia.js do Morvyn.
299
+ // types: ["image", "video", ...] (padrão: todos)
300
+ // families: ["image", "video", "audio"] (para pegar também documento que é, na verdade, imagem/vídeo/áudio)
301
+ // quoted: "prefer" (padrão) | "only" | "never"
302
+ // Devolve a mídia (ver m.media) com fromQuoted: true/false, ou null.
303
+ // accept: (media) => boolean filtro extra (ex.: ignorar documento que não é imagem/vídeo/áudio)
304
+ function findMedia(msg, { types, families, quoted = "prefer", accept } = {}) {
305
+ const m = parse(msg)
306
+ const q = quoted === "never" ? null : m.quoted?.media || null
307
+ const own = quoted === "only" ? null : m.media
308
+ const ok = a => a && (!types || types.includes(a.type)) && (!families || families.includes(a.family)) && (!accept || accept(a))
309
+ if (ok(q)) return { ...q, fromQuoted: true }
310
+ if (ok(own)) return { ...own, fromQuoted: false }
311
+ return null
312
+ }
313
+
314
+ // atalho: só o texto (ou legenda) de uma mensagem
315
+ const textOfMessage = msg => parse(msg).text
316
+
317
+ module.exports = { parse, findMedia, unwrap, textOf: textOfMessage, toNumber, WRAPPERS, MEDIA_TYPES }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@morpho/message",
3
+ "version": "0.1.0",
4
+ "description": "Mensagem do WhatsApp em formato simples no Morpho: desembrulha (visualização única, temporária, editada...), descobre o tipo, texto/legenda, menções, mídia, resposta de botão/lista e a mensagem citada. Sem dependências. Junta o que no Morvyn estava espalhado em tipoMensagem.js, midia.js, global.js e handler.js.",
5
+ "main": "index.js",
6
+ "license": "MIT"
7
+ }
@@ -0,0 +1,75 @@
1
+ # morpho-socket/pool
2
+
3
+ Runs **several WhatsApp numbers in one process**: connect, pair by code, reconnect with backoff, stop, reset. No business rules (plans, limits, who is the owner) — those plug in through events.
4
+
5
+ ```js
6
+ const { MorphoPool } = require("morpho-socket/pool")
7
+ const { initAuthCreds } = require("@whiskeysockets/baileys")
8
+
9
+ const pool = new MorphoPool({ store, initCreds: initAuthCreds, maxAttemptsWithoutPairing: 8 })
10
+
11
+ pool.on("socket", (id, sock, { listen }) => listen("messages.upsert", data => handle(id, sock, data)))
12
+ pool.on("pairing-code", (id, code) => console.log(id, "→", code))
13
+ pool.on("connect", (id, sock) => { /* socket is open */ })
14
+
15
+ await pool.startAll(["client-a", { sessionId: "client-b", phoneNumber: "5511999999999" }], { intervalMs: 1500 })
16
+ ```
17
+
18
+ ## Pairing
19
+
20
+ If a session has no saved credentials and you pass `phoneNumber`, the pool requests a pairing code and emits `pairing-code`. The code is requested only after WhatsApp answers the handshake (the `qr` event) — asking earlier makes the server reject the session. A session that started pairing but never finished is discarded on the next connect, so a failed attempt does not leave the number stuck in a 401 loop.
21
+
22
+ ## What happens when the connection drops
23
+
24
+ | Code | Reason | What the pool does | Extra event |
25
+ |---|---|---|---|
26
+ | 401 | logged out | **stops** (always) | `logged-out` |
27
+ | 440 | replaced: another instance opened the same session | **stops** (reconnecting would start a fight) | `replaced` |
28
+ | 403 | forbidden: number restricted or banned | **stops** | `forbidden` |
29
+ | 515 | restart required (normal right after pairing) | reconnects **immediately** (up to 3 in a row, then backoff) | — |
30
+ | anything else | network drop, timeout… | reconnects with backoff (2 s … 60 s) | `reconnecting` |
31
+
32
+ Every terminal stop also emits `stopped` with `{ reason }`, and rejects the pending `start()` if the session never opened. Override per code with `policy`:
33
+
34
+ ```js
35
+ new MorphoPool({ store, initCreds, policy: { 440: "reconnect", 515: "immediate" } }) // "reconnect" | "immediate" | "stop"
36
+ ```
37
+
38
+ ## API
39
+
40
+ **Constructor options**
41
+
42
+ | Option | Meaning |
43
+ |---|---|
44
+ | `store`, `initCreds` | required — a [`MorphoAuthStore`](../core) and Baileys' `initAuthCreds` |
45
+ | `socketConfig(sessionId, state)` | extra `makeWASocket` options (`getMessage`, `cachedGroupMetadata`, `browser`…) merged into the default factory |
46
+ | `makeSocket({ sessionId, state, saveCreds })` | replace the socket factory entirely (also how you test without WhatsApp). `socketConfig` is only used by the default factory |
47
+ | `backoff(attempt)` | milliseconds to wait; default exponential, capped at 60 s |
48
+ | `policy` | per-close-code action (see above). 401 always stops |
49
+ | `maxAttemptsWithoutPairing` | a number that was **never paired** gives up after N consecutive drops and emits `gave-up`. A paired session never gives up. Default `null` (try forever) |
50
+ | `skipNetworkVersion`, `versionTimeoutMs` | the protocol version is fetched once and cached for 1 h, with a timeout; if the lookup fails the last good version (or Baileys' bundled one) is used and a warning is printed |
51
+
52
+ **Methods**
53
+
54
+ | | |
55
+ |---|---|
56
+ | `start(id, { phoneNumber })` | connects; resolves the first time the connection opens |
57
+ | `startAll(items, { intervalMs })` | starts in sequence with a gap instead of opening every connection at once; does not wait for pairing. Items: `"id"`, `{ sessionId, phoneNumber }` or `{ sessionId, opts }` |
58
+ | `stop(id)` | disconnects but **keeps** the saved session (`start()` again, no pairing) |
59
+ | `reset(id)` | disconnects **and deletes** the saved session (next `start()` asks for pairing) |
60
+ | `get(id)` | the current socket, or `null` |
61
+ | `status(id)` / `info(id)` / `list()` | `"connecting" \| "open" \| "reconnecting" \| "stopped"` · `{ status, attempts, paired, stopReason }` · all sessions |
62
+
63
+ **Events**
64
+
65
+ | Event | Arguments |
66
+ |---|---|
67
+ | `socket` | `(id, sock, { listen })` — one per (re)connection. `listen` is `sock.ev.on` that ignores events from a replaced/stopped socket and routes listener errors (sync or async) to `error` |
68
+ | `connect` | `(id, sock)` |
69
+ | `pairing-code` | `(id, code)` |
70
+ | `disconnect` | `(id, { code, message, loggedOut, reason, action })` |
71
+ | `reconnecting` | `(id, { attempt, delayMs, reason, immediate })` |
72
+ | `logged-out` · `replaced` · `forbidden` · `gave-up` · `stopped` | `(id, info)` |
73
+ | `error` | `(id, error)` — without a listener the error is printed instead of crashing the process |
74
+
75
+ Also exported: `STATUS`, `CLOSE_REASONS`, `DEFAULT_POLICY`.