@factoidal/core 0.31.1 → 0.33.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/xmpp/server.mjs CHANGED
@@ -37,10 +37,13 @@
37
37
  // server-to-server federation, and any limit on how many bytes
38
38
  // a client may send without completing a unit beyond `maxBuffered`,
39
39
  // which is a transport limit this host applies by closing the socket.
40
+ // The other transport limits are `maxConnections`, `loginTimeoutMs` and
41
+ // `maxQueuedBytes`. There is no connection limit per address: behind a
42
+ // proxy, every client has the same one.
40
43
 
41
- import { randomBytes } from 'node:crypto'
44
+ import { createHash, randomBytes } from 'node:crypto'
42
45
  import { StringDecoder } from 'node:string_decoder'
43
- import { mkdir, readFile, writeFile, readdir, unlink, rename } from 'node:fs/promises'
46
+ import { mkdir, readFile, writeFile, readdir, unlink, rename, stat } from 'node:fs/promises'
44
47
  import { join } from 'node:path'
45
48
  import { loadEngine } from '../bin/engine.mjs'
46
49
  import { createXmppRouter } from './router.mjs'
@@ -49,7 +52,7 @@ import { createXmppRouter } from './router.mjs'
49
52
  export const XMPP_SERVER_OPS = [
50
53
  'xmppOpen', 'xmppClose',
51
54
  'xmppSessionOpen', 'xmppSessionFeed', 'xmppSessionDeliver', 'xmppSessionClose',
52
- 'xmppSessionTlsUp', 'xmppRosterGet', 'xmppRosterSet', 'xmppWsOpen', 'xmppStateLoad'
55
+ 'xmppSessionTlsUp', 'xmppRosterGet', 'xmppRosterSet', 'xmppWsOpen', 'xmppStateLoad', 'xmppClock'
53
56
  ]
54
57
 
55
58
  /** An error raised by this host, as opposed to one the engine reported.
@@ -116,10 +119,19 @@ function randomToken (bytes = 18) {
116
119
  * @param {object} options
117
120
  * @param {string} [options.domain] the domain the server is authoritative for (default `localhost`)
118
121
  * @param {Array<{username: string, password: string}>} [options.accounts]
119
- * @param {string} [options.accountsText] `user:password` lines, the harness's accounts-file format
120
- * @param {string} [options.saltB64] the SCRAM salt, base64
122
+ * @param {string} [options.accountsText] the accounts file's text, one account per line:
123
+ * `name:password`, or the keys `factoidal xmpp-serve --verifier NAME` writes. A line that is
124
+ * neither stops the start (`Server.accountsOfText`)
125
+ * @param {string} [options.saltB64] one SCRAM salt for every name, base64; absent, each name has its own
126
+ * @param {string} [options.saltSeed] the seed of the salts; absent, a random one for this process
127
+ * @param {string} [options.occupantSeed] the key of the rooms' occupant ids; absent, a random one
121
128
  * @param {number} [options.iterationCount] the SCRAM iteration count
122
129
  * @param {boolean} [options.tlsCarrier] whether the carrier terminates TLS
130
+ * @param {boolean} [options.disableRateLimits] no failed SASL login limit and no sender rate on
131
+ * held stanzas (`--dangerously-disable-ratelimits`)
132
+ * @param {object} [options.heldLimits] caps of the stanzas held for offline accounts, each a
133
+ * number: `perAccount`, `perAccountBytes`, `total`, `totalBytes`, `senderPerWindow`,
134
+ * `senderWindow` (seconds); what is absent keeps the engine's default
123
135
  * @param {object} [options.engine] an engine handle to reuse
124
136
  * @returns {Promise<{engine: object, handle: string, domain: string,
125
137
  * openSession: Function, roster: Function, setRoster: Function, close: Function}>}
@@ -130,11 +142,20 @@ export async function openXmppServer (options = {}) {
130
142
  if (typeof options.domain === 'string') config.domain = options.domain
131
143
  if (Array.isArray(options.accounts)) config.accounts = options.accounts
132
144
  if (typeof options.accountsText === 'string') config.accountsText = options.accountsText
145
+ // One salt per name, from a seed the engine cannot make; a fixed `saltB64`
146
+ // (a transcript that must replay) is one salt for all.
133
147
  if (typeof options.saltB64 === 'string') config.saltB64 = options.saltB64
148
+ else config.saltSeed = typeof options.saltSeed === 'string' ? options.saltSeed : randomToken(32)
149
+ // The key of the XEP-0421 occupant ids of the rooms.
150
+ config.occupantSeed = typeof options.occupantSeed === 'string' ? options.occupantSeed : randomToken(32)
134
151
  if (typeof options.iterationCount === 'number') config.iterationCount = options.iterationCount
135
152
  if (typeof options.tlsCarrier === 'boolean') config.tlsCarrier = options.tlsCarrier
136
153
  // XEP-0045 room service (L4Factoidal/XMPP/Muc.lean); off unless named.
137
154
  if (typeof options.mucDomain === 'string') config.mucDomain = options.mucDomain
155
+ // Members-only rooms: [{room, members: [bare JID]}]; no list, an open room.
156
+ if (Array.isArray(options.mucMembers)) config.mucMembers = options.mucMembers
157
+ // Nicks reserved for a JID: [{nick, jid}]. Each account's name is reserved for it anyway.
158
+ if (Array.isArray(options.mucReservedNicks)) config.mucReservedNicks = options.mucReservedNicks
138
159
  // XEP-0369 channels (L4Factoidal/XMPP/Mix.lean), joined through MIX-PAM; off unless named.
139
160
  if (typeof options.mixDomain === 'string') config.mixDomain = options.mixDomain
140
161
  // GC3 prototype groups (L4Factoidal/XMPP/Gc3.lean; no published spec); off unless named.
@@ -142,6 +163,10 @@ export async function openXmppServer (options = {}) {
142
163
  // The host keeps the state across restarts (a state key and a DIR): only
143
164
  // then does the engine produce `store`/`unstore`.
144
165
  if (options.persist === true) config.persist = true
166
+ // The failed SASL login limit is on unless the operator turns it off
167
+ // (https://github.com/danbri/factoidal/issues/1128); the engine decides.
168
+ if (options.disableRateLimits === true) config.disableRateLimits = true
169
+ if (options.heldLimits && typeof options.heldLimits === 'object') config.heldLimits = options.heldLimits
145
170
  let opened
146
171
  try {
147
172
  opened = engine.call('xmppOpen', [JSON.stringify(config)])
@@ -157,20 +182,45 @@ export async function openXmppServer (options = {}) {
157
182
  throw new XmppHostError('xmppOpen answered with no handle')
158
183
  }
159
184
  const handle = opened.handle
185
+ // The rate limits read the host's clock: tell the engine when the second changes.
186
+ let lastSecond = 0
187
+ const tick = () => {
188
+ const now = Math.floor(Date.now() / 1000)
189
+ if (now === lastSecond) return
190
+ lastSecond = now
191
+ try {
192
+ engine.call('xmppClock', [handle, String(now)])
193
+ } catch (error) {
194
+ // An older engine has no such op, and no limit that reads it.
195
+ if (!isUnknownOp(error)) throw error
196
+ }
197
+ }
198
+ // An older engine ignores the list, and the rooms would be open: do not start.
199
+ if (Array.isArray(options.mucMembers) && opened.mucMembers !== options.mucMembers.length) {
200
+ engine.call('xmppClose', [handle])
201
+ throw new XmppHostError('this engine does not carry members-only rooms; rebuild the engine')
202
+ }
160
203
  return {
161
204
  engine,
162
205
  handle,
163
206
  domain: typeof opened.domain === 'string' ? opened.domain : (config.domain || 'localhost'),
164
207
  accounts: typeof opened.accounts === 'number' ? opened.accounts : undefined,
208
+ /** The engine's start warnings (--dangerously-disable-ratelimits). */
209
+ warnings: Array.isArray(opened.warnings) ? opened.warnings : [],
210
+ tick,
165
211
  /**
166
212
  * One client connection.
167
- * @param {{streamId?: string, nonce?: string, tlsCarrier?: boolean, startTls?: boolean, maxUnits?: number}} [sessionOptions]
213
+ * `remoteAddress` (the socket's) and the clock go to the engine for the
214
+ * failed SASL login limit (https://github.com/danbri/factoidal/issues/1128).
215
+ * @param {{streamId?: string, nonce?: string, tlsCarrier?: boolean, startTls?: boolean, maxUnits?: number, remoteAddress?: string}} [sessionOptions]
168
216
  */
169
217
  openSession (sessionOptions = {}) {
170
218
  const record = {
171
219
  streamId: typeof sessionOptions.streamId === 'string' ? sessionOptions.streamId : randomToken(),
172
- nonce: typeof sessionOptions.nonce === 'string' ? sessionOptions.nonce : randomToken()
220
+ nonce: typeof sessionOptions.nonce === 'string' ? sessionOptions.nonce : randomToken(),
221
+ now: Math.floor(Date.now() / 1000)
173
222
  }
223
+ if (typeof sessionOptions.remoteAddress === 'string') record.remoteAddress = sessionOptions.remoteAddress
174
224
  if (typeof sessionOptions.tlsCarrier === 'boolean') record.tlsCarrier = sessionOptions.tlsCarrier
175
225
  if (typeof sessionOptions.startTls === 'boolean') record.startTls = sessionOptions.startTls
176
226
  if (typeof sessionOptions.maxUnits === 'number') record.maxUnits = sessionOptions.maxUnits
@@ -182,6 +232,7 @@ export async function openXmppServer (options = {}) {
182
232
  handle: session,
183
233
  /** Feed received text; returns the engine's answer document. */
184
234
  feed (text) {
235
+ tick()
185
236
  return engine.call('xmppSessionFeed', [session, text])
186
237
  },
187
238
  /** The TLS handshake a feed asked for (`startTls: true`) finished. */
@@ -228,6 +279,14 @@ async function readAccountsFile (path) {
228
279
  }
229
280
  }
230
281
 
282
+ /** A warning when the accounts file can be read by anyone but its owner. */
283
+ async function accountsFileWarnings (path) {
284
+ if (process.platform === 'win32') return []
285
+ const { mode } = await stat(path)
286
+ if ((mode & 0o077) === 0) return []
287
+ return [`${path} can be read by other users of this machine: chmod 600 it`]
288
+ }
289
+
231
290
  /**
232
291
  * A `net.Server` (or `tls.Server`, when `cert` and `key` are given —
233
292
  * XEP-0368 direct TLS) that runs one engine session per connection.
@@ -235,6 +294,7 @@ async function readAccountsFile (path) {
235
294
  * The server is returned unlistened: the caller chooses the port.
236
295
  *
237
296
  * @param {object} options as `openXmppServer`, plus:
297
+ * @param {string} [options.accountsFile] the path `accountsText` came from, to check its mode
238
298
  * @param {string} [options.dir] state directory: `accounts` is read from it when
239
299
  * no accounts are given; rosters are persisted under `roster/`; `spool/` is
240
300
  * polled for stanzas written by other processes
@@ -243,10 +303,20 @@ async function readAccountsFile (path) {
243
303
  * @param {boolean} [options.startTls] listen in clear and use `cert` and
244
304
  * `key` for STARTTLS (RFC 6120 section 5), which the engine then requires
245
305
  * before anything else; without it, `cert` and `key` mean direct TLS
306
+ * @param {number} [options.directTlsPort] with `startTls`, `listen` also serves
307
+ * XEP-0368 direct TLS on this port, in the same server: one set of rooms
246
308
  * @param {number} [options.pollMs] spool poll interval (default 250; 0 disables)
247
309
  * @param {number} [options.maxBuffered] residual UTF-8 bytes held by the engine
248
310
  * after a feed before the socket is closed (default 1 MiB); the decoder
249
311
  * may separately hold up to three bytes of an incomplete UTF-8 sequence
312
+ * @param {number} [options.maxConnections] open connections, of all kinds, before
313
+ * a new one is closed at once (default 1024)
314
+ * @param {number} [options.loginTimeoutMs] how long a connection may stay
315
+ * without a bound resource before it is closed (default 30000; 0 disables)
316
+ * @param {number} [options.maxQueuedBytes] bytes waiting to be written to a
317
+ * client that is not reading, before the socket is closed (default 8 MiB)
318
+ * @param {boolean} [options.addressLimit] false: count failed logins per account name only, for a
319
+ * listener behind a proxy, where every client has one address (default true)
250
320
  * @param {Function} [options.onError]
251
321
  * @param {Function} [options.onLog]
252
322
  * @returns {Promise<{server: object, xmpp: object, close: Function, sessions: Function}>}
@@ -259,6 +329,14 @@ export async function createXmppServer (options = {}) {
259
329
  const dir = typeof options.dir === 'string' ? options.dir : null
260
330
  const pollMs = typeof options.pollMs === 'number' ? options.pollMs : 250
261
331
  const maxBuffered = typeof options.maxBuffered === 'number' ? options.maxBuffered : 1024 * 1024
332
+ const limit = (name, fallback) => {
333
+ const value = typeof options[name] === 'number' ? options[name] : fallback
334
+ if (!Number.isSafeInteger(value) || value < 0) throw new XmppHostError(`${name} must be a nonnegative safe integer`)
335
+ return value
336
+ }
337
+ const maxConnections = limit('maxConnections', 1024)
338
+ const loginTimeoutMs = limit('loginTimeoutMs', 30000)
339
+ const maxQueuedBytes = limit('maxQueuedBytes', 8 * 1024 * 1024)
262
340
  if (!Number.isSafeInteger(maxBuffered) || maxBuffered < 0) {
263
341
  throw new XmppHostError('maxBuffered must be a nonnegative safe integer')
264
342
  }
@@ -267,9 +345,19 @@ export async function createXmppServer (options = {}) {
267
345
  }
268
346
 
269
347
  const serverOptions = { ...options }
348
+ let accountsFile = typeof options.accountsFile === 'string' ? options.accountsFile : null
270
349
  if (dir !== null && !Array.isArray(options.accounts) && typeof options.accountsText !== 'string') {
271
350
  const text = await readAccountsFile(join(dir, 'accounts'))
272
- if (text !== null) serverOptions.accountsText = text
351
+ if (text !== null) { serverOptions.accountsText = text; accountsFile = join(dir, 'accounts') }
352
+ }
353
+ // Salts that survive a restart when the state does: a client that keeps a
354
+ // salted password goes on working, and a name's salt does not change.
355
+ if (typeof options.saltSeed !== 'string' && typeof options.stateKey === 'string') {
356
+ serverOptions.saltSeed = createHash('sha256').update('xmpp salt seed\0' + options.stateKey).digest('base64url')
357
+ }
358
+ // Occupant ids that survive a restart, for the same reason.
359
+ if (typeof options.occupantSeed !== 'string' && typeof options.stateKey === 'string') {
360
+ serverOptions.occupantSeed = createHash('sha256').update('xmpp occupant seed\0' + options.stateKey).digest('base64url')
273
361
  }
274
362
  const haveKey = options.cert !== undefined && options.key !== undefined
275
363
  if (options.startTls === true && !haveKey) {
@@ -280,6 +368,7 @@ export async function createXmppServer (options = {}) {
280
368
  if (typeof serverOptions.tlsCarrier !== 'boolean') serverOptions.tlsCarrier = useTls
281
369
  if (dir !== null && options.stateKey !== undefined) serverOptions.persist = true
282
370
  const xmpp = await openXmppServer(serverOptions)
371
+ if (accountsFile !== null) xmpp.warnings.push(...await accountsFileWarnings(accountsFile))
283
372
 
284
373
  // Rosters the previous run persisted.
285
374
  try {
@@ -337,6 +426,10 @@ export async function createXmppServer (options = {}) {
337
426
  if (got.status !== 200) throw new XmppHostError(`state: ${path} answered ${got.status}`)
338
427
  const loaded = xmpp.engine.call('xmppStateLoad', [xmpp.handle, path, new TextDecoder().decode(got.body)])
339
428
  if (loaded === null || typeof loaded !== 'object') throw new XmppHostError(`state: ${path} was refused`)
429
+ if (loaded.stale === true) {
430
+ const gone = await state.step({ method: 'DELETE', target: path, headers: [], body: new Uint8Array(0) })
431
+ if (gone.status >= 300 && gone.status !== 404) throw new XmppHostError(`state: DELETE ${path} answered ${gone.status}`)
432
+ }
340
433
  }
341
434
  }
342
435
  } catch (error) {
@@ -385,8 +478,44 @@ export async function createXmppServer (options = {}) {
385
478
  // connection callback and therefore have no engine session yet.
386
479
  const transports = new Set()
387
480
 
481
+ // The client's address for the login limit. Behind a proxy every client has
482
+ // the proxy's address, and a limit on it would stop them all: `addressLimit: false`.
483
+ const addressOf = (socket) =>
484
+ options.addressLimit !== false && typeof socket.remoteAddress === 'string' ? socket.remoteAddress : ''
485
+
486
+ // Transport limits, like `maxBuffered`: bytes and time, not protocol.
487
+ function closeIfBacklogged (c) {
488
+ if (c.socket.destroyed || !(c.socket.writableLength > maxQueuedBytes)) return
489
+ onError(new XmppHostError(`connection exceeded ${maxQueuedBytes} queued bytes`))
490
+ c.socket.destroy()
491
+ }
492
+
388
493
  function writeAll (c, lines) {
389
494
  for (const line of lines) if (!c.socket.destroyed) c.socket.write(line)
495
+ closeIfBacklogged(c)
496
+ }
497
+
498
+ /** A connection gets `loginTimeoutMs` to bind a resource. */
499
+ function startLoginTimer (c) {
500
+ if (loginTimeoutMs === 0) return
501
+ c.loginTimer = setTimeout(() => {
502
+ if (c.full === null && connections.has(c)) {
503
+ onLog('closing a connection that did not log in in time')
504
+ c.socket.destroy()
505
+ }
506
+ }, loginTimeoutMs)
507
+ c.loginTimer.unref()
508
+ }
509
+
510
+ /** Count a new socket, or close it at once if the server is full. */
511
+ function admit (socket) {
512
+ if (transports.size >= maxConnections) {
513
+ onError(new XmppHostError(`refused a connection: ${maxConnections} are open`))
514
+ socket.destroy()
515
+ return
516
+ }
517
+ transports.add(socket)
518
+ socket.once('close', () => transports.delete(socket))
390
519
  }
391
520
 
392
521
  let rosterWrites = Promise.resolve()
@@ -471,6 +600,7 @@ export async function createXmppServer (options = {}) {
471
600
  function endSession (c) {
472
601
  if (!connections.has(c)) return
473
602
  connections.delete(c)
603
+ clearTimeout(c.loginTimer)
474
604
  let answer
475
605
  try { answer = c.session.close() } catch (error) { onError(error); answer = { send: [] } }
476
606
  // The session left its MUC rooms (Muc.leaveAll): tell the other occupants.
@@ -482,10 +612,10 @@ export async function createXmppServer (options = {}) {
482
612
  }
483
613
  }
484
614
 
485
- function onConnection (socket) {
615
+ function onConnection (socket, direct = false) {
486
616
  let session
487
617
  try {
488
- session = xmpp.openSession({ tlsCarrier: useTls, startTls })
618
+ session = xmpp.openSession({ tlsCarrier: useTls || direct, startTls: startTls && !direct, remoteAddress: addressOf(socket) })
489
619
  } catch (error) {
490
620
  onError(error)
491
621
  socket.destroy()
@@ -505,6 +635,7 @@ export async function createXmppServer (options = {}) {
505
635
  }
506
636
  }
507
637
  connections.add(c)
638
+ startLoginTimer(c)
508
639
  onLog(`connection from ${socket.remoteAddress}:${socket.remotePort}`)
509
640
  c.onData = (chunk) => {
510
641
  if (!connections.has(c) || c.closing || c.upgrading) return
@@ -552,7 +683,7 @@ export async function createXmppServer (options = {}) {
552
683
  let handle
553
684
  try {
554
685
  const opened = xmpp.engine.call('xmppWsOpen', [xmpp.handle, JSON.stringify({
555
- streamId: randomToken(), nonce: randomToken(),
686
+ streamId: randomToken(), nonce: randomToken(), remoteAddress: addressOf(socket),
556
687
  path: typeof ws.path === 'string' ? ws.path : '/xmpp-websocket',
557
688
  allowedOrigins: Array.isArray(ws.allowedOrigins) ? ws.allowedOrigins : [],
558
689
  allowNoOrigin: ws.allowNoOrigin === true,
@@ -579,15 +710,17 @@ export async function createXmppServer (options = {}) {
579
710
  },
580
711
  deliver (text) {
581
712
  const got = wsCall('xmppWsDeliver', [handle, text], new Uint8Array(0))
582
- if (got.envelope.delivered === true) { if (!socket.destroyed) socket.write(got.bytes); return true }
713
+ if (got.envelope.delivered === true) { if (!socket.destroyed) socket.write(got.bytes); closeIfBacklogged(c); return true }
583
714
  return false
584
715
  }
585
716
  }
586
717
  connections.add(c)
718
+ startLoginTimer(c)
587
719
  socket.on('data', (chunk) => {
588
720
  if (!connections.has(c) || c.closing) return
589
721
  let got
590
722
  try {
723
+ xmpp.tick()
591
724
  got = wsCall('xmppWsFeed', [handle], new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength))
592
725
  } catch (error) {
593
726
  onError(error)
@@ -623,29 +756,32 @@ export async function createXmppServer (options = {}) {
623
756
  const net = await import('node:net')
624
757
  wsServer = net.createServer(onWsConnection)
625
758
  }
626
- wsServer.on('connection', socket => {
627
- transports.add(socket)
628
- socket.once('close', () => transports.delete(socket))
629
- })
759
+ wsServer.on('connection', admit)
630
760
  }
631
761
 
632
762
  let server
633
763
  try {
634
764
  if (useTls) {
635
765
  const tls = await import('node:tls')
636
- server = tls.createServer({ cert: options.cert, key: options.key }, onConnection)
766
+ server = tls.createServer({ cert: options.cert, key: options.key }, (socket) => onConnection(socket))
637
767
  } else {
638
768
  const net = await import('node:net')
639
- server = net.createServer(onConnection)
769
+ server = net.createServer((socket) => onConnection(socket))
640
770
  }
641
771
  } catch (error) {
642
772
  xmpp.close()
643
773
  throw error
644
774
  }
645
- server.on('connection', socket => {
646
- transports.add(socket)
647
- socket.once('close', () => transports.delete(socket))
648
- })
775
+ server.on('connection', admit)
776
+
777
+ // XEP-0368 direct TLS beside a STARTTLS listener: the engine takes its
778
+ // answer per session, so this differs only in what `onConnection` passes.
779
+ let directServer = null
780
+ if (startTls && options.directTlsPort !== undefined) {
781
+ const tls = await import('node:tls')
782
+ directServer = tls.createServer({ cert: options.cert, key: options.key }, (socket) => onConnection(socket, true))
783
+ directServer.on('connection', admit)
784
+ }
649
785
 
650
786
  // The harness's mailbox: `<dir>/spool/<bare jid>/<name>` holds one
651
787
  // stanza's bytes for that account. Polled for every bound session,
@@ -693,6 +829,7 @@ export async function createXmppServer (options = {}) {
693
829
  return {
694
830
  server,
695
831
  wsServer,
832
+ directServer,
696
833
  xmpp,
697
834
  /** The live connections' `{bare, full}` pairs, for a host's own view. */
698
835
  sessions () {
@@ -714,6 +851,7 @@ export async function createXmppServer (options = {}) {
714
851
  try {
715
852
  await new Promise(resolve => server.close(resolve))
716
853
  if (wsServer !== null) await new Promise(resolve => wsServer.close(resolve))
854
+ if (directServer !== null) await new Promise(resolve => directServer.close(resolve))
717
855
  await pollTask
718
856
  await rosterWrites
719
857
  await stateWrites
@@ -765,7 +903,20 @@ export async function listen (options = {}) {
765
903
  const path = typeof options.websocket.path === 'string' ? options.websocket.path : '/xmpp-websocket'
766
904
  wsOrigin = `${wsScheme}://${host}:${created.wsServer.address().port}${path}`
767
905
  }
906
+ let directOrigin = null
907
+ if (created.directServer !== null) {
908
+ try {
909
+ await new Promise((resolve, reject) => {
910
+ created.directServer.once('error', reject)
911
+ created.directServer.listen(options.directTlsPort, host, resolve)
912
+ })
913
+ } catch (error) {
914
+ await created.close()
915
+ throw error
916
+ }
917
+ directOrigin = `xmpps://${host}:${created.directServer.address().port}`
918
+ }
768
919
  const bound = created.server.address()
769
920
  const scheme = options.cert !== undefined && options.key !== undefined ? 'xmpps' : 'xmpp'
770
- return { ...created, port: bound.port, host, origin: `${scheme}://${host}:${bound.port}`, wsOrigin }
921
+ return { ...created, port: bound.port, host, origin: `${scheme}://${host}:${bound.port}`, wsOrigin, directOrigin }
771
922
  }