@wacando/agent 2.3.0 → 2.4.1

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.
@@ -0,0 +1,811 @@
1
+ // Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
2
+ import {
3
+ XMPP_DOMAIN
4
+ } from "./chunk-4Z6TL6JY.mjs";
5
+
6
+ // packages/xmpp/src/scram.ts
7
+ import { client } from "@xmpp/client";
8
+ import xml from "@xmpp/xml";
9
+
10
+ // packages/xmpp/src/start.ts
11
+ import { promise, timeout } from "@xmpp/events";
12
+ var guarded = /* @__PURE__ */ new WeakSet();
13
+ function guardStart(entity) {
14
+ if (guarded.has(entity)) return entity;
15
+ const target = entity;
16
+ target.start = async function start() {
17
+ if (this.status !== "offline") {
18
+ throw new Error("Connection is not offline");
19
+ }
20
+ const { service, domain, lang } = this.options;
21
+ await this.connect(service);
22
+ const online = promise(this, "online");
23
+ online.catch(() => {
24
+ });
25
+ await this.open({ domain, lang });
26
+ return online;
27
+ };
28
+ guarded.add(entity);
29
+ return entity;
30
+ }
31
+
32
+ // packages/xmpp/src/scram.ts
33
+ var SASL1_NS = "urn:ietf:params:xml:ns:xmpp-sasl";
34
+ var SASL2_NS = "urn:xmpp:sasl:2";
35
+ var SCRAM_MIN_ITERATIONS = 4096;
36
+ var SCRAM_MAX_ITERATIONS = 1e5;
37
+ var HASH_BYTES = { "SHA-1": 20, "SHA-256": 32 };
38
+ var REASON_TEXT = {
39
+ "missing-server-signature": "server sent no ServerSignature (v=)",
40
+ "server-signature-mismatch": "server ServerSignature (v=) does not match",
41
+ "malformed-server-message": "malformed SCRAM server message",
42
+ "server-error": "server reported a SCRAM error (e=)",
43
+ "nonce-mismatch": "server nonce does not extend the client nonce",
44
+ "weak-iteration-count": `server iteration count below ${SCRAM_MIN_ITERATIONS}`,
45
+ "excessive-iteration-count": `server iteration count above ${SCRAM_MAX_ITERATIONS}`,
46
+ "sasl2-refused": "server offers SASL2, which this client refuses (SASL1 SCRAM only)",
47
+ "no-scram-mechanism": "server offers no supported SCRAM mechanism",
48
+ "out-of-order": "SCRAM message out of order"
49
+ };
50
+ var ScramAuthError = class extends Error {
51
+ reason;
52
+ mechanism;
53
+ constructor(reason, mechanism) {
54
+ super(
55
+ `SCRAM authentication refused${mechanism ? ` (${mechanism})` : ""}: ${REASON_TEXT[reason]}`
56
+ );
57
+ this.name = "ScramAuthError";
58
+ this.reason = reason;
59
+ this.mechanism = mechanism;
60
+ }
61
+ };
62
+ var encoder = new TextEncoder();
63
+ function utf8(text) {
64
+ return new Uint8Array(encoder.encode(text));
65
+ }
66
+ function bytesToBinary(bytes) {
67
+ let out = "";
68
+ for (const b of bytes) out += String.fromCharCode(b);
69
+ return out;
70
+ }
71
+ function binaryToBytes(binary) {
72
+ const out = new Uint8Array(binary.length);
73
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i) & 255;
74
+ return out;
75
+ }
76
+ function base64Encode(bytes) {
77
+ return globalThis.btoa(bytesToBinary(bytes));
78
+ }
79
+ function base64Decode(text) {
80
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(text)) {
81
+ return void 0;
82
+ }
83
+ try {
84
+ return binaryToBytes(globalThis.atob(text));
85
+ } catch {
86
+ return void 0;
87
+ }
88
+ }
89
+ function constantTimeEqual(a, b) {
90
+ let diff = a.length ^ b.length;
91
+ const n = Math.max(a.length, b.length);
92
+ for (let i = 0; i < n; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);
93
+ return diff === 0;
94
+ }
95
+ function xor(a, b) {
96
+ const out = new Uint8Array(a.length);
97
+ for (let i = 0; i < a.length; i++) out[i] = (a[i] ?? 0) ^ (b[i] ?? 0);
98
+ return out;
99
+ }
100
+ async function hmac(hash, key, data) {
101
+ const k = await globalThis.crypto.subtle.importKey("raw", key, { name: "HMAC", hash }, false, [
102
+ "sign"
103
+ ]);
104
+ return new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", k, data));
105
+ }
106
+ async function digest(hash, data) {
107
+ return new Uint8Array(await globalThis.crypto.subtle.digest(hash, data));
108
+ }
109
+ async function hi(hash, password, salt, iterations) {
110
+ const key = await globalThis.crypto.subtle.importKey("raw", utf8(password), "PBKDF2", false, [
111
+ "deriveBits"
112
+ ]);
113
+ return new Uint8Array(
114
+ await globalThis.crypto.subtle.deriveBits(
115
+ { name: "PBKDF2", hash, salt, iterations },
116
+ key,
117
+ HASH_BYTES[hash] * 8
118
+ )
119
+ );
120
+ }
121
+ function parseAttributes(message) {
122
+ const attrs = /* @__PURE__ */ new Map();
123
+ for (const part of message.split(",")) {
124
+ const m = /^([A-Za-z])=(.*)$/s.exec(part);
125
+ if (!m) return void 0;
126
+ const [, key, value] = m;
127
+ if (attrs.has(key)) return void 0;
128
+ attrs.set(key, value);
129
+ }
130
+ return attrs;
131
+ }
132
+ function saslname(name) {
133
+ return name.replace(/=/g, "=3D").replace(/,/g, "=2C");
134
+ }
135
+ async function scramProofs(hash, password, salt, iterations, authMessage) {
136
+ const salted = await hi(hash, password, salt, iterations);
137
+ const clientKey = await hmac(hash, salted, utf8("Client Key"));
138
+ const storedKey = await digest(hash, clientKey);
139
+ const am = utf8(authMessage);
140
+ const clientSignature = await hmac(hash, storedKey, am);
141
+ const serverKey = await hmac(hash, salted, utf8("Server Key"));
142
+ return {
143
+ clientProof: xor(clientKey, clientSignature),
144
+ serverSignature: await hmac(hash, serverKey, am)
145
+ };
146
+ }
147
+ function verifyServerFinal(serverFinal, expected, mechanism) {
148
+ if (serverFinal === void 0 || serverFinal === "") {
149
+ throw new ScramAuthError("missing-server-signature", mechanism);
150
+ }
151
+ const attrs = parseAttributes(serverFinal);
152
+ if (!attrs) throw new ScramAuthError("malformed-server-message", mechanism);
153
+ if (attrs.has("e")) throw new ScramAuthError("server-error", mechanism);
154
+ const v = attrs.get("v");
155
+ if (v === void 0) throw new ScramAuthError("missing-server-signature", mechanism);
156
+ const received = base64Decode(v);
157
+ if (!received) throw new ScramAuthError("malformed-server-message", mechanism);
158
+ if (!constantTimeEqual(received, expected)) {
159
+ throw new ScramAuthError("server-signature-mismatch", mechanism);
160
+ }
161
+ }
162
+ var ScramMechanism = class {
163
+ constructor(hash, genNonce = defaultNonce) {
164
+ this.hash = hash;
165
+ this.genNonce = genNonce;
166
+ this.name = hash === "SHA-1" ? "SCRAM-SHA-1" : "SCRAM-SHA-256";
167
+ }
168
+ hash;
169
+ genNonce;
170
+ name;
171
+ clientFirst = true;
172
+ stage = "initial";
173
+ clientNonce = "";
174
+ clientFirstBare = "";
175
+ serverFirst = "";
176
+ expectedServerSignature;
177
+ pendingServerFinal;
178
+ verifiedFlag = false;
179
+ /** True once the server's ServerSignature has been verified. */
180
+ get verified() {
181
+ return this.verifiedFlag;
182
+ }
183
+ /** True once the client-final-message was sent (a `v=` is now due). */
184
+ get awaitingServerFinal() {
185
+ return this.stage === "final";
186
+ }
187
+ async response(cred) {
188
+ switch (this.stage) {
189
+ case "initial":
190
+ return this.clientFirstMessage(cred);
191
+ case "challenge":
192
+ return this.clientFinalMessage(cred);
193
+ case "final":
194
+ await this.final(this.pendingServerFinal);
195
+ return "";
196
+ default:
197
+ throw new ScramAuthError("out-of-order", this.name);
198
+ }
199
+ }
200
+ challenge(chal) {
201
+ if (this.stage === "challenge") {
202
+ this.serverFirst = utf8Decode(chal);
203
+ return;
204
+ }
205
+ if (this.stage === "final") {
206
+ this.pendingServerFinal = chal;
207
+ return;
208
+ }
209
+ throw new ScramAuthError("out-of-order", this.name);
210
+ }
211
+ /**
212
+ * server-final-message (binary string, as decoded from base64) from
213
+ * <success/> (SASL1), <additional-data/> (SASL2) or a last <challenge/>.
214
+ */
215
+ async final(data) {
216
+ if (this.verifiedFlag) return;
217
+ if (this.stage !== "final" || !this.expectedServerSignature) {
218
+ throw new ScramAuthError("out-of-order", this.name);
219
+ }
220
+ this.stage = "done";
221
+ const expected = this.expectedServerSignature;
222
+ this.expectedServerSignature = void 0;
223
+ verifyServerFinal(data === void 0 ? void 0 : utf8Decode(data), expected, this.name);
224
+ this.verifiedFlag = true;
225
+ }
226
+ clientFirstMessage(cred) {
227
+ this.clientNonce = this.genNonce();
228
+ this.clientFirstBare = `n=${saslname(cred.username ?? "")},r=${this.clientNonce}`;
229
+ this.stage = "challenge";
230
+ return utf8Binary(`n,,${this.clientFirstBare}`);
231
+ }
232
+ async clientFinalMessage(cred) {
233
+ const attrs = parseAttributes(this.serverFirst);
234
+ const nonce = attrs?.get("r");
235
+ const saltText = attrs?.get("s");
236
+ const iterText = attrs?.get("i");
237
+ if (attrs?.has("e")) throw new ScramAuthError("server-error", this.name);
238
+ if (!attrs || attrs.has("m") || !nonce || !saltText || !iterText) {
239
+ throw new ScramAuthError("malformed-server-message", this.name);
240
+ }
241
+ if (!nonce.startsWith(this.clientNonce) || nonce.length <= this.clientNonce.length) {
242
+ throw new ScramAuthError("nonce-mismatch", this.name);
243
+ }
244
+ const salt = base64Decode(saltText);
245
+ if (!salt || salt.length === 0 || !/^[1-9][0-9]{0,9}$/.test(iterText)) {
246
+ throw new ScramAuthError("malformed-server-message", this.name);
247
+ }
248
+ const iterations = Number(iterText);
249
+ if (iterations < SCRAM_MIN_ITERATIONS) {
250
+ throw new ScramAuthError("weak-iteration-count", this.name);
251
+ }
252
+ if (iterations > SCRAM_MAX_ITERATIONS) {
253
+ throw new ScramAuthError("excessive-iteration-count", this.name);
254
+ }
255
+ const withoutProof = `c=biws,r=${nonce}`;
256
+ const authMessage = `${this.clientFirstBare},${this.serverFirst},${withoutProof}`;
257
+ const { clientProof, serverSignature } = await scramProofs(
258
+ this.hash,
259
+ cred.password ?? "",
260
+ salt,
261
+ iterations,
262
+ authMessage
263
+ );
264
+ this.expectedServerSignature = serverSignature;
265
+ this.stage = "final";
266
+ return utf8Binary(`${withoutProof},p=${base64Encode(clientProof)}`);
267
+ }
268
+ };
269
+ function utf8Binary(text) {
270
+ return bytesToBinary(utf8(text));
271
+ }
272
+ function utf8Decode(binary) {
273
+ return new TextDecoder().decode(binaryToBytes(binary));
274
+ }
275
+ function defaultNonce() {
276
+ const bytes = new Uint8Array(24);
277
+ globalThis.crypto.getRandomValues(bytes);
278
+ return base64Encode(bytes);
279
+ }
280
+ var SCRAM_HASHES = ["SHA-256", "SHA-1"];
281
+ var SCRAM_NAMES = SCRAM_HASHES.map(
282
+ (h) => h === "SHA-1" ? "SCRAM-SHA-1" : "SCRAM-SHA-256"
283
+ );
284
+ var NS_STREAM = "http://etherx.jabber.org/streams";
285
+ function saslFeaturesRefusal(features) {
286
+ if (features.getChild("authentication", SASL2_NS)) return "sasl2-refused";
287
+ const sasl1 = features.getChild("mechanisms", SASL1_NS);
288
+ if (!sasl1) return void 0;
289
+ const offered = sasl1.getChildren("mechanism").map((m) => m.text().trim());
290
+ return SCRAM_NAMES.some((name) => offered.includes(name)) ? void 0 : "no-scram-mechanism";
291
+ }
292
+ var states = /* @__PURE__ */ new WeakMap();
293
+ function scramClient(options) {
294
+ const { username, password, ...rest } = options;
295
+ let current;
296
+ let rejectFailure = () => {
297
+ };
298
+ const state = {
299
+ failure: new Promise((_, reject) => {
300
+ rejectFailure = reject;
301
+ })
302
+ };
303
+ state.failure.catch(() => {
304
+ });
305
+ const userAgent = xml("user-agent", { id: globalThis.crypto.randomUUID() });
306
+ const cut = (entity, err) => {
307
+ state.cutting ??= (async () => {
308
+ entity.reconnect?.stop();
309
+ await entity.stop().catch(() => {
310
+ });
311
+ rejectFailure(err);
312
+ })();
313
+ return state.cutting;
314
+ };
315
+ let refused;
316
+ const xmpp = client({
317
+ ...rest,
318
+ credentials: async (authenticate, mechanisms, _fast, entity) => {
319
+ const failClosed = async (err) => {
320
+ await cut(entity, err);
321
+ throw err;
322
+ };
323
+ if (refused) return failClosed(refused);
324
+ const mechanism = SCRAM_NAMES.find((name) => mechanisms.includes(name));
325
+ if (!mechanism) return failClosed(new ScramAuthError("no-scram-mechanism"));
326
+ let successData;
327
+ const onNonza = (el) => {
328
+ if (el.is("success", SASL1_NS)) successData = decodeSuccess(el.text());
329
+ };
330
+ entity.on("nonza", onNonza);
331
+ current = void 0;
332
+ try {
333
+ await authenticate({ username, password }, mechanism, userAgent);
334
+ const mech = current;
335
+ if (!mech) throw new ScramAuthError("out-of-order", mechanism);
336
+ if (!mech.verified) await mech.final(successData);
337
+ } catch (err) {
338
+ if (err instanceof ScramAuthError) return failClosed(err);
339
+ throw err;
340
+ } finally {
341
+ entity.removeListener("nonza", onNonza);
342
+ current = void 0;
343
+ }
344
+ }
345
+ });
346
+ const factory = xmpp.saslFactory;
347
+ const bound = SCRAM_HASHES.map((hash) => ({
348
+ name: hash === "SHA-1" ? "SCRAM-SHA-1" : "SCRAM-SHA-256",
349
+ mech: class extends ScramMechanism {
350
+ constructor() {
351
+ super(hash);
352
+ current = this;
353
+ }
354
+ }
355
+ }));
356
+ factory._mechs.splice(0, factory._mechs.length, ...bound);
357
+ xmpp.on("nonza", (el) => {
358
+ if (!el.is("features", NS_STREAM)) return;
359
+ const reason = saslFeaturesRefusal(el);
360
+ if (!reason) return;
361
+ refused = new ScramAuthError(reason);
362
+ void cut(xmpp, refused);
363
+ });
364
+ guardStart(xmpp);
365
+ states.set(xmpp, state);
366
+ return xmpp;
367
+ }
368
+ function decodeSuccess(text) {
369
+ const trimmed = text.trim();
370
+ if (trimmed === "" || trimmed === "=") return void 0;
371
+ try {
372
+ return globalThis.atob(trimmed);
373
+ } catch {
374
+ return void 0;
375
+ }
376
+ }
377
+ async function startScramClient(xmpp) {
378
+ const state = states.get(xmpp);
379
+ if (!state) return xmpp.start();
380
+ try {
381
+ return await Promise.race([xmpp.start(), state.failure]);
382
+ } catch (err) {
383
+ if (state.cutting) {
384
+ await state.cutting;
385
+ return state.failure;
386
+ }
387
+ throw err;
388
+ }
389
+ }
390
+
391
+ // packages/xmpp/src/muc.ts
392
+ import xml2 from "@xmpp/xml";
393
+
394
+ // packages/xmpp/src/jid.ts
395
+ var COMPONENT_JID = `policy.${XMPP_DOMAIN}`;
396
+ function occupantJid(room, nick) {
397
+ if (nick.length === 0) {
398
+ throw new Error("occupant nick must not be empty");
399
+ }
400
+ return `${room}/${nick}`;
401
+ }
402
+
403
+ // packages/xmpp/src/timeouts.ts
404
+ var BASE_TIMEOUT_MS = 1e4;
405
+ var scale = 1;
406
+ function scaledTimeoutMs(ms) {
407
+ return Math.round(ms * scale);
408
+ }
409
+ function defaultTimeoutMs() {
410
+ return scaledTimeoutMs(BASE_TIMEOUT_MS);
411
+ }
412
+
413
+ // packages/xmpp/src/stanza.ts
414
+ var StanzaWaitError = class extends Error {
415
+ constructor(reason, message, stanza) {
416
+ super(message);
417
+ this.reason = reason;
418
+ this.stanza = stanza;
419
+ this.name = "StanzaWaitError";
420
+ }
421
+ reason;
422
+ stanza;
423
+ };
424
+ function waitForElement(entity, event, predicate, timeoutMs = defaultTimeoutMs()) {
425
+ return new Promise((resolve, reject) => {
426
+ const seen = [];
427
+ const timer = setTimeout(() => {
428
+ cleanup();
429
+ const trail = seen.length ? `; last seen: ${seen.join(", ")}` : "; nothing arrived";
430
+ const status = entity.status;
431
+ const state = typeof status === "string" ? `; entity status: ${status}` : "";
432
+ reject(
433
+ new StanzaWaitError(
434
+ "timeout",
435
+ `no matching ${event} within ${timeoutMs}ms${trail}${state}`
436
+ )
437
+ );
438
+ }, timeoutMs);
439
+ const onElement = (el) => {
440
+ seen.push(describeElement(el));
441
+ if (seen.length > 8) seen.shift();
442
+ try {
443
+ if (predicate(el)) {
444
+ cleanup();
445
+ resolve(el);
446
+ }
447
+ } catch (err) {
448
+ cleanup();
449
+ reject(err);
450
+ }
451
+ };
452
+ const cleanup = () => {
453
+ clearTimeout(timer);
454
+ entity.off(event, onElement);
455
+ };
456
+ entity.on(event, onElement);
457
+ });
458
+ }
459
+ function waitForStanza(entity, predicate, timeoutMs = defaultTimeoutMs()) {
460
+ return waitForElement(entity, "stanza", predicate, timeoutMs);
461
+ }
462
+ function describeElement(el) {
463
+ const type = el.getAttr("type");
464
+ const from = el.getAttr("from");
465
+ const error = type === "error" ? ` error=${stanzaErrorCondition(el)}` : "";
466
+ return `<${el.name}${type ? ` type=${type}` : ""}${from ? ` from=${from}` : ""}${error}>`;
467
+ }
468
+ function stanzaErrorCondition(stanza) {
469
+ const error = stanza.getChild("error");
470
+ const condition = error?.children.find((c) => typeof c !== "string");
471
+ return condition?.getName() ?? "unknown";
472
+ }
473
+
474
+ // packages/xmpp/src/muc.ts
475
+ var NS_MUC = "http://jabber.org/protocol/muc";
476
+ var NS_MUC_USER = `${NS_MUC}#user`;
477
+ var NS_MUC_ADMIN = `${NS_MUC}#admin`;
478
+ var NS_MUC_OWNER = `${NS_MUC}#owner`;
479
+ var NS_DATA_FORMS = "jabber:x:data";
480
+ var MucError = class extends Error {
481
+ constructor(message, condition = "unknown", stanza) {
482
+ super(message);
483
+ this.condition = condition;
484
+ this.stanza = stanza;
485
+ this.name = "MucError";
486
+ }
487
+ condition;
488
+ stanza;
489
+ };
490
+ var MucJoinError = class extends MucError {
491
+ constructor(condition, stanza, room, nick) {
492
+ super(`join denied for ${occupantJid(room, nick)}: ${condition}`, condition, stanza);
493
+ this.room = room;
494
+ this.nick = nick;
495
+ this.name = "MucJoinError";
496
+ }
497
+ room;
498
+ nick;
499
+ };
500
+ function joinPresenceEl(room, nick) {
501
+ return xml2("presence", { to: occupantJid(room, nick) }, xml2("x", { xmlns: NS_MUC }));
502
+ }
503
+ async function joinRoom(entity, room, nick, timeoutMs = defaultTimeoutMs()) {
504
+ const self = occupantJid(room, nick);
505
+ const wait = waitForStanza(
506
+ entity,
507
+ (s) => s.is("presence") && s.getAttr("from") === self,
508
+ timeoutMs
509
+ );
510
+ try {
511
+ await entity.send(joinPresenceEl(room, nick));
512
+ } catch (err) {
513
+ void wait.catch(() => {
514
+ });
515
+ throw err;
516
+ }
517
+ const presence = await wait;
518
+ if (presence.getAttr("type") === "error") {
519
+ throw new MucJoinError(stanzaErrorCondition(presence), presence, room, nick);
520
+ }
521
+ return presence;
522
+ }
523
+
524
+ // packages/xmpp/src/origin.ts
525
+ import { jid } from "@xmpp/jid";
526
+ var NS_MUC_USER2 = "http://jabber.org/protocol/muc#user";
527
+ var NS_FORWARD = "urn:xmpp:forward:0";
528
+ var NS_WACANDO_POLICY = "urn:wacando:policy:0";
529
+ var origins = /* @__PURE__ */ new WeakMap();
530
+ function authenticatedMessageOrigin(stanza) {
531
+ return origins.get(stanza);
532
+ }
533
+ function itemJid(stanza) {
534
+ const xs = stanza.getChildren("x", NS_MUC_USER2);
535
+ if (xs.length !== 1) return;
536
+ const items = xs[0]?.getChildren("item");
537
+ if (items?.length !== 1) return;
538
+ const value = items[0]?.getAttr("jid");
539
+ return typeof value === "string" ? value : void 0;
540
+ }
541
+ function userBareJid(value) {
542
+ if (value === void 0) return;
543
+ try {
544
+ const parsed = jid(value);
545
+ return parsed.getLocal() ? parsed.bare().toString() : void 0;
546
+ } catch {
547
+ return;
548
+ }
549
+ }
550
+ function isTrustedRelay(value, trustedRelayJid) {
551
+ if (value === void 0 || trustedRelayJid === void 0) return false;
552
+ try {
553
+ const parsed = jid(value);
554
+ return !parsed.getLocal() && parsed.getDomain() === trustedRelayJid;
555
+ } catch {
556
+ return false;
557
+ }
558
+ }
559
+ function relaySender(stanza) {
560
+ const relays = stanza.getChildren("relay", NS_WACANDO_POLICY);
561
+ if (relays.length !== 1) return;
562
+ return userBareJid(relays[0]?.getAttr("jid"));
563
+ }
564
+ function recordMamOrigin(stanza, room, trustedRelayJid) {
565
+ if (!stanza.is("message") || stanza.getAttr("type") !== "groupchat" || !stanza.getAttr("from")?.startsWith(`${room}/`))
566
+ return;
567
+ const item = itemJid(stanza);
568
+ const senderBareJid = isTrustedRelay(item, trustedRelayJid) ? relaySender(stanza) : userBareJid(item);
569
+ if (senderBareJid) origins.set(stanza, { roomJid: room, senderBareJid });
570
+ }
571
+ function unwrapPolicyDelivery(stanza, policyJid) {
572
+ if (!stanza.is("message") || stanza.getAttr("type") === "error") return;
573
+ const from = stanza.getAttr("from");
574
+ if (typeof from !== "string" || !isTrustedRelay(from, policyJid)) return;
575
+ const deliveries = stanza.getChildren("delivery", NS_WACANDO_POLICY);
576
+ if (deliveries.length !== 1) return;
577
+ const delivery = deliveries[0];
578
+ const room = delivery?.getAttr("room");
579
+ const senderBareJid = userBareJid(delivery?.getAttr("sender"));
580
+ const inner = delivery?.getChild("forwarded", NS_FORWARD)?.getChild("message");
581
+ if (typeof room !== "string" || !senderBareJid || !inner) return;
582
+ if (inner.getAttr("type") !== "groupchat" || !inner.getAttr("from")?.startsWith(`${room}/`))
583
+ return;
584
+ origins.set(inner, { roomJid: room, senderBareJid });
585
+ return inner;
586
+ }
587
+ var MucOriginTracker = class {
588
+ /**
589
+ * `trustedRelayJid`: the policy component domain. When set, groupchat from
590
+ * an occupant whose real JID is exactly that domain is attributed to its
591
+ * `<relay jid>` (#39). Without it, relays carry no origin (fail closed).
592
+ */
593
+ constructor(entity, options = {}) {
594
+ this.entity = entity;
595
+ this.options = options;
596
+ entity.on("stanza", this.onStanza);
597
+ entity.on("offline", this.reset);
598
+ entity.on("disconnect", this.reset);
599
+ }
600
+ entity;
601
+ options;
602
+ rooms = /* @__PURE__ */ new Set();
603
+ occupants = /* @__PURE__ */ new Map();
604
+ /** Occupant full JIDs whose authenticated real JID is the policy component. */
605
+ relays = /* @__PURE__ */ new Set();
606
+ trackRoom(room) {
607
+ this.rooms.add(room);
608
+ }
609
+ reset = () => {
610
+ this.occupants.clear();
611
+ this.relays.clear();
612
+ };
613
+ onStanza = (stanza) => {
614
+ const from = stanza.getAttr("from");
615
+ if (typeof from !== "string") return;
616
+ const slash = from.indexOf("/");
617
+ if (slash < 1 || !this.rooms.has(from.slice(0, slash))) return;
618
+ if (stanza.is("presence")) {
619
+ const type = stanza.getAttr("type");
620
+ if (type === "unavailable" || type === "error") {
621
+ this.occupants.delete(from);
622
+ this.relays.delete(from);
623
+ if (stanza.getChild("x", NS_MUC_USER2)?.getChildren("status").some((s) => s.getAttr("code") === "110")) {
624
+ const prefix = `${from.slice(0, slash)}/`;
625
+ for (const occupant of this.occupants.keys()) {
626
+ if (occupant.startsWith(prefix)) this.occupants.delete(occupant);
627
+ }
628
+ for (const relay of [...this.relays]) {
629
+ if (relay.startsWith(prefix)) this.relays.delete(relay);
630
+ }
631
+ }
632
+ } else if (!type) {
633
+ const item = itemJid(stanza);
634
+ const sender = userBareJid(item);
635
+ if (sender) this.occupants.set(from, sender);
636
+ else this.occupants.delete(from);
637
+ if (isTrustedRelay(item, this.options.trustedRelayJid)) this.relays.add(from);
638
+ else this.relays.delete(from);
639
+ }
640
+ } else if (stanza.is("message") && stanza.getAttr("type") === "groupchat" && !stanza.getChild("delay", "urn:xmpp:delay")) {
641
+ const senderBareJid = this.relays.has(from) ? relaySender(stanza) : this.occupants.get(from);
642
+ if (senderBareJid) origins.set(stanza, { roomJid: from.slice(0, slash), senderBareJid });
643
+ }
644
+ };
645
+ close() {
646
+ this.entity.off("stanza", this.onStanza);
647
+ this.entity.off("offline", this.reset);
648
+ this.entity.off("disconnect", this.reset);
649
+ this.reset();
650
+ }
651
+ };
652
+
653
+ // packages/xmpp/src/mam.ts
654
+ import xml3 from "@xmpp/xml";
655
+ var NS_MAM = "urn:xmpp:mam:2";
656
+ var NS_FORWARD2 = "urn:xmpp:forward:0";
657
+ var NS_RSM = "http://jabber.org/protocol/rsm";
658
+ var queryCounter = 0;
659
+ function filterField(varName, value) {
660
+ return xml3("field", { var: varName }, xml3("value", {}, value));
661
+ }
662
+ function mamQueryEl(options) {
663
+ const fields = [xml3("field", { var: "FORM_TYPE", type: "hidden" }, xml3("value", {}, NS_MAM))];
664
+ if (options.withJid !== void 0) fields.push(filterField("with", options.withJid));
665
+ if (options.start !== void 0) fields.push(filterField("start", options.start));
666
+ if (options.end !== void 0) fields.push(filterField("end", options.end));
667
+ const children = [xml3("x", { xmlns: NS_DATA_FORMS, type: "submit" }, ...fields)];
668
+ if (options.max !== void 0) {
669
+ children.push(xml3("set", { xmlns: NS_RSM }, xml3("max", {}, String(options.max))));
670
+ }
671
+ return xml3("query", { xmlns: NS_MAM, queryid: options.queryId }, ...children);
672
+ }
673
+ async function queryMam(entity, options, timeoutMs = defaultTimeoutMs()) {
674
+ const queryId = options.queryId ?? `mam${++queryCounter}`;
675
+ const forwarded = [];
676
+ const onStanza = (stanza) => {
677
+ if (!stanza.is("message") || stanza.getAttr("from") !== options.to) return;
678
+ const result = stanza.getChild("result", NS_MAM);
679
+ if (!result || result.getAttr("queryid") !== queryId) return;
680
+ const fwd = result.getChild("forwarded", NS_FORWARD2);
681
+ if (fwd) {
682
+ const inner = forwardedStanza(fwd);
683
+ if (inner) recordMamOrigin(inner, options.to, options.trustedRelayJid);
684
+ forwarded.push(fwd);
685
+ }
686
+ };
687
+ entity.on("stanza", onStanza);
688
+ try {
689
+ const response = await entity.iqCaller.set(
690
+ mamQueryEl({ ...options, queryId }),
691
+ options.to,
692
+ timeoutMs
693
+ );
694
+ const fin = response?.getChild("fin", NS_MAM);
695
+ const rsmLast = fin?.getChild("set", NS_RSM)?.getChildText("last");
696
+ return {
697
+ queryId,
698
+ complete: fin?.getAttr("complete") === "true",
699
+ lastId: typeof rsmLast === "string" ? rsmLast : void 0,
700
+ messages: forwarded
701
+ };
702
+ } finally {
703
+ entity.off("stanza", onStanza);
704
+ }
705
+ }
706
+ function forwardedStanza(forwarded) {
707
+ return forwarded.children.find(
708
+ (c) => typeof c !== "string" && c.getName() !== "delay"
709
+ );
710
+ }
711
+
712
+ // packages/xmpp/src/roster.ts
713
+ import { jid as parseJid } from "@xmpp/jid";
714
+ import xml4 from "@xmpp/xml";
715
+ function subscriptionRefusal(stanza) {
716
+ if (!stanza.is("presence") || stanza.getAttr("type") !== "subscribe") return null;
717
+ const from = stanza.getAttr("from");
718
+ if (typeof from !== "string" || from === "") return null;
719
+ let bare;
720
+ try {
721
+ bare = parseJid(from).bare().toString();
722
+ } catch {
723
+ return null;
724
+ }
725
+ return xml4("presence", { to: bare, type: "unsubscribed" });
726
+ }
727
+ function refuseSubscriptionRequests(entity) {
728
+ const onStanza = (stanza) => {
729
+ const refusal = subscriptionRefusal(stanza);
730
+ if (refusal) void entity.send(refusal).catch(() => {
731
+ });
732
+ };
733
+ entity.on("stanza", onStanza);
734
+ return () => entity.off("stanza", onStanza);
735
+ }
736
+
737
+ // packages/xmpp/src/presence.ts
738
+ var UNKNOWN_PRESENCE = Object.freeze({
739
+ connection: "unknown",
740
+ availability: null
741
+ });
742
+
743
+ // packages/xmpp/src/pubsub.ts
744
+ import xml5 from "@xmpp/xml";
745
+ var NS_PUBSUB = "http://jabber.org/protocol/pubsub";
746
+ var NS_PUBSUB_PUBLISH_OPTIONS = "http://jabber.org/protocol/pubsub#publish-options";
747
+ function pepPublishEl(node, itemId, payload, accessModel = "open") {
748
+ return xml5(
749
+ "pubsub",
750
+ { xmlns: NS_PUBSUB },
751
+ xml5("publish", { node }, xml5("item", { id: itemId }, payload)),
752
+ xml5(
753
+ "publish-options",
754
+ {},
755
+ xml5(
756
+ "x",
757
+ { xmlns: NS_DATA_FORMS, type: "submit" },
758
+ xml5(
759
+ "field",
760
+ { var: "FORM_TYPE", type: "hidden" },
761
+ xml5("value", {}, NS_PUBSUB_PUBLISH_OPTIONS)
762
+ ),
763
+ xml5("field", { var: "pubsub#access_model" }, xml5("value", {}, accessModel))
764
+ )
765
+ )
766
+ );
767
+ }
768
+ function pubsubItemsEl(node, maxItems) {
769
+ const attrs = { node };
770
+ if (maxItems !== void 0) attrs.max_items = String(maxItems);
771
+ return xml5("pubsub", { xmlns: NS_PUBSUB }, xml5("items", attrs));
772
+ }
773
+ async function pepPublish(entity, to, node, itemId, payload, timeoutMs = defaultTimeoutMs()) {
774
+ await entity.iqCaller.set(pepPublishEl(node, itemId, payload), to, timeoutMs);
775
+ }
776
+ async function pepFetchItems(entity, to, node, timeoutMs = defaultTimeoutMs()) {
777
+ try {
778
+ const result = await entity.iqCaller.get(pubsubItemsEl(node), to, timeoutMs);
779
+ const pubsub = result?.getName() === "pubsub" ? result : result?.getChild("pubsub", NS_PUBSUB);
780
+ const items = pubsub?.getChild("items");
781
+ if (!items) return [];
782
+ return items.getChildren("item").map((item) => item.children.find((c) => typeof c !== "string")).filter((el) => el !== void 0);
783
+ } catch (err) {
784
+ if (isItemNotFound(err)) return [];
785
+ throw err;
786
+ }
787
+ }
788
+ function isItemNotFound(err) {
789
+ const cond = err?.condition;
790
+ if (cond === "item-not-found") return true;
791
+ const stanza = err?.stanza;
792
+ return stanza?.getChild("error")?.getChild("item-not-found") !== void 0;
793
+ }
794
+
795
+ export {
796
+ ScramAuthError,
797
+ scramClient,
798
+ startScramClient,
799
+ defaultTimeoutMs,
800
+ waitForStanza,
801
+ MucJoinError,
802
+ joinRoom,
803
+ authenticatedMessageOrigin,
804
+ unwrapPolicyDelivery,
805
+ MucOriginTracker,
806
+ queryMam,
807
+ forwardedStanza,
808
+ pepPublish,
809
+ pepFetchItems,
810
+ refuseSubscriptionRequests
811
+ };