@wacando/agent 2.3.0 → 2.4.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/STATUS.md +21 -0
- package/bin/wacando-agent.mjs +25 -8
- package/docs/AGENT_TOOLS.md +12 -0
- package/docs/WACANDO_PACKAGES.md +12 -1
- package/package.json +1 -1
- package/packages/onboarding/service.mjs +25 -1
- package/packages/onboarding/version.mjs +1 -1
- package/packages/runtime/session.mjs +42 -6
- package/packages/wacando/chunks/{chunk-PHGOH4AU.mjs → chunk-F7J3K43U.mjs} +2 -2
- package/packages/wacando/chunks/{chunk-PZDQXGA6.mjs → chunk-FFLYSYV6.mjs} +1 -1
- package/packages/wacando/chunks/{chunk-53E7SSTF.mjs → chunk-FUNUGIGH.mjs} +384 -21
- package/packages/wacando/omemo-wire.mjs +3 -3
- package/packages/wacando/omemo.mjs +13 -8
- package/packages/wacando/xmpp.mjs +9 -3
- package/plugins/wacando/.codex-plugin/plugin.json +1 -1
- package/plugins/wacando-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/wacando-cursor/.cursor-plugin/plugin.json +1 -1
- package/provenance.json +20 -18
package/STATUS.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Handoff — October 15, 2026
|
|
2
2
|
|
|
3
|
+
## Runtime 2.4.0 — October 17, 2026
|
|
4
|
+
|
|
5
|
+
Verified XMPP server (lbframe/wacando-agent#26): every XMPP connection of
|
|
6
|
+
the runtime now goes through the embedded `scramClient` of lbframe/wacando#186.
|
|
7
|
+
Only SCRAM-SHA-256/SHA-1 over SASL1 is used (no PLAIN, no SASL2/FAST,
|
|
8
|
+
iteration count between 4096 and 100000), and the session is bound only after
|
|
9
|
+
the server's ServerSignature (`v=`) was verified. A forged or missing `v=`, a
|
|
10
|
+
SASL2 offer or a server without SCRAM closes the connection before any
|
|
11
|
+
session: state `refused`, error `XMPP_SERVER_UNVERIFIED`, no retry and no
|
|
12
|
+
reconnection in the process; the daemon exits with code 4 (code 3 for
|
|
13
|
+
`SESSION_REPLACED`), at the first connection as at a reconnection. An error while
|
|
14
|
+
the XMPP stream opens (a WebSocket cut on Node.js 24, a TCP reset) no longer
|
|
15
|
+
kills the process with an unhandled rejection: it is an ordinary, retried
|
|
16
|
+
connection failure. A repository-wide
|
|
17
|
+
test forbids any raw `@xmpp/client` `client()` outside the embedded code. See
|
|
18
|
+
[agent tools](docs/AGENT_TOOLS.md); evidence `docs/evidence/issue-26/`.
|
|
19
|
+
|
|
20
|
+
Embedded Wacando code rebuilt from lbframe/wacando main `40dd0c3`, which also
|
|
21
|
+
brings lbframe/wacando#166: `downloadFile` refuses more than 10 MiB and fails
|
|
22
|
+
closed on size ([embedded Wacando code](docs/WACANDO_PACKAGES.md)).
|
|
23
|
+
|
|
3
24
|
## Runtime 2.3.0 — October 17, 2026
|
|
4
25
|
|
|
5
26
|
Default origin (fix): the runtime now defaults to the production API
|
package/bin/wacando-agent.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { pluginVersion } from '../packages/onboarding/version.mjs';
|
|
8
8
|
import { configuredOrigin } from '../packages/onboarding/network.mjs';
|
|
9
9
|
import { SafeError } from '../packages/onboarding/storage.mjs';
|
|
10
|
-
import { SESSION_REPLACED } from '../packages/onboarding/service.mjs';
|
|
10
|
+
import { SESSION_REPLACED, XMPP_SERVER_UNVERIFIED } from '../packages/onboarding/service.mjs';
|
|
11
11
|
|
|
12
12
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
13
13
|
const self = fileURLToPath(import.meta.url);
|
|
@@ -174,28 +174,41 @@ commands.set('files', async flags => {
|
|
|
174
174
|
await withConversations(parseFlags(rest, conversationFlags), c => c.listFiles(id));
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
+
/** Daemon exit code of a terminal connection error: 3 replaced, 4 unverified server. */
|
|
178
|
+
function terminalExitCode(error) {
|
|
179
|
+
if (!(error instanceof SafeError)) return undefined;
|
|
180
|
+
if (error.message === SESSION_REPLACED) return 3;
|
|
181
|
+
if (error.message === XMPP_SERVER_UNVERIFIED) return 4;
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
177
185
|
commands.set('daemon', async flags => {
|
|
178
186
|
const options = parseFlags(flags, ['--agent', '--host', '--origin']);
|
|
179
187
|
// Keep the process alive between heartbeats/retries until a stop signal, or
|
|
180
|
-
// until the server hands this identity to a newer session
|
|
188
|
+
// until the server hands this identity to a newer session or fails SCRAM
|
|
189
|
+
// verification (both terminal).
|
|
181
190
|
let keepAlive;
|
|
182
|
-
let
|
|
191
|
+
let terminal;
|
|
183
192
|
const stop = new Promise(resolve => {
|
|
184
193
|
keepAlive = setInterval(() => {}, 3600000);
|
|
185
|
-
|
|
194
|
+
terminal = reason => resolve(reason);
|
|
186
195
|
process.once('SIGINT', resolve); process.once('SIGTERM', resolve);
|
|
187
196
|
});
|
|
188
197
|
const agent = await runtime(options, event => {
|
|
189
198
|
// Diagnostics carry only local enums and public ids (never content).
|
|
190
199
|
process.stderr.write(`${JSON.stringify({ component: 'wacando-connection', ...event })}\n`);
|
|
191
|
-
if (event.state === 'replaced')
|
|
200
|
+
if (event.state === 'replaced' || event.state === 'refused') terminal(event.state);
|
|
192
201
|
});
|
|
193
202
|
try {
|
|
194
203
|
try { print(await agent.connect(options.agent)); }
|
|
195
204
|
catch (error) {
|
|
196
205
|
// A retry stays scheduled unless the identity was rejected for good or
|
|
197
206
|
// another process already runs it (identity lock).
|
|
198
|
-
|
|
207
|
+
// The terminal exit codes hold whether the first connection or a later
|
|
208
|
+
// reconnection hits them (a supervisor must not restart into a loop).
|
|
209
|
+
const exitCode = terminalExitCode(error);
|
|
210
|
+
if (exitCode) throw Object.assign(error, { exitCode });
|
|
211
|
+
if (!(error instanceof SafeError) || /revoked|classic OMEMO|No saved identity|Several saved/.test(error.message)) throw error;
|
|
199
212
|
if (/Failed step: local identity/.test(error.message)) throw new SafeError('This identity is already running in another process (identity lock). Stop it first.');
|
|
200
213
|
console.error(error.message);
|
|
201
214
|
}
|
|
@@ -207,9 +220,13 @@ commands.set('daemon', async flags => {
|
|
|
207
220
|
decrypted: entry.text !== null || Boolean(entry.file), ...(entry.file ? { file: { fileId: entry.file.fileId ?? null, state: entry.file.state } } : {}) })));
|
|
208
221
|
} catch (error) { if (!(error instanceof SafeError)) throw error; console.error(error.message); }
|
|
209
222
|
console.error('Daemon running; stop with Ctrl-C or SIGTERM.');
|
|
210
|
-
|
|
223
|
+
const reason = await stop;
|
|
224
|
+
if (reason === 'replaced') {
|
|
211
225
|
console.error(SESSION_REPLACED);
|
|
212
226
|
process.exitCode = 3;
|
|
227
|
+
} else if (reason === 'refused') {
|
|
228
|
+
console.error(XMPP_SERVER_UNVERIFIED);
|
|
229
|
+
process.exitCode = 4;
|
|
213
230
|
}
|
|
214
231
|
} finally { clearInterval(keepAlive); await agent.close(); }
|
|
215
232
|
});
|
|
@@ -283,6 +300,6 @@ if (invoked === self) {
|
|
|
283
300
|
if (error instanceof UsageError) console.error(usage);
|
|
284
301
|
else if (error instanceof SafeError) console.error(error.message);
|
|
285
302
|
else console.error('Wacando command failed. Check arguments, local state permissions and service availability.');
|
|
286
|
-
process.exitCode = error instanceof UsageError ? 2 : 1;
|
|
303
|
+
process.exitCode = error instanceof UsageError ? 2 : (Number.isInteger(error?.exitCode) ? error.exitCode : 1);
|
|
287
304
|
});
|
|
288
305
|
}
|
package/docs/AGENT_TOOLS.md
CHANGED
|
@@ -96,6 +96,18 @@ Rules enforced by the runtime (and, independently, by the server):
|
|
|
96
96
|
identity back. After an ordinary network loss the conversation client moves
|
|
97
97
|
to the new session (one OMEMO provider per identity) and re-joins rooms on
|
|
98
98
|
demand.
|
|
99
|
+
- **Verified XMPP server** (lbframe/wacando-agent#26, lbframe/wacando#186):
|
|
100
|
+
every XMPP login of the runtime uses the embedded `scramClient`: SCRAM-SHA-256
|
|
101
|
+
or SCRAM-SHA-1 over SASL1 only (no PLAIN, no SASL2/FAST), iteration count
|
|
102
|
+
between 4096 and 100000, and the session is bound only after the server's
|
|
103
|
+
ServerSignature (`v=`) was verified. A forged or missing `v=`, a SCRAM `e=`
|
|
104
|
+
error, a SASL2 offer or a server without SCRAM closes the connection before
|
|
105
|
+
any session: state `refused`, error `XMPP_SERVER_UNVERIFIED`, no retry and no
|
|
106
|
+
reconnection in the process (a further connect is refused). The daemon exits
|
|
107
|
+
with code 4, whether the first connection or a reconnection is refused (and
|
|
108
|
+
with code 3 for `SESSION_REPLACED`, likewise). Ordinary failures (wrong or
|
|
109
|
+
expired grant, network cut, timeout) stay `retrying`. Check the configured
|
|
110
|
+
service, then restart the runtime.
|
|
99
111
|
- **Message text on the CLI**: pass `-` instead of the text to read it from
|
|
100
112
|
stdin, so it stays out of `ps` and shell history.
|
|
101
113
|
- **Presence**: after connecting, the runtime waits for the server's echo of
|
package/docs/WACANDO_PACKAGES.md
CHANGED
|
@@ -42,7 +42,18 @@ source repository; a URL is cloned into a temporary directory.
|
|
|
42
42
|
result is byte-identical to `packages/wacando/` and `provenance.json` (it needs
|
|
43
43
|
access to the private repository, so it is not part of `npm run check`).
|
|
44
44
|
|
|
45
|
-
Current source (2.
|
|
45
|
+
Current source (2.4.0): lbframe/wacando main
|
|
46
|
+
`40dd0c30b3653095f08994f9534fd5110e1d0925`. It brings lbframe/wacando#186:
|
|
47
|
+
`#wacando/xmpp` now also exports `scramClient`, `startScramClient` and
|
|
48
|
+
`ScramAuthError` (SCRAM client verifying the ServerSignature `v=`, SASL2
|
|
49
|
+
refused, iteration count capped at 100000), through which the runtime makes
|
|
50
|
+
every XMPP connection (`packages/runtime/session.mjs`; `@xmpp/client` is now
|
|
51
|
+
also a recorded external of the embedded code); and lbframe/wacando#166:
|
|
52
|
+
`downloadFile` refuses more than 10 MiB and fails closed on size.
|
|
53
|
+
`tests/scram-wiring.test.mjs` forbids any raw `@xmpp/client` `client()`
|
|
54
|
+
outside the embedded code.
|
|
55
|
+
|
|
56
|
+
2.2.0 and 2.3.0 embedded lbframe/wacando main
|
|
46
57
|
`618922ecaf2213da18949a8b27d343c8767b64c5`, which adds the review copy of
|
|
47
58
|
lbframe/wacando#169 (`fileReviewFor`, `formatFileReview`, `parseFileReview`,
|
|
48
59
|
`decryptFileReview`, `isFileReviewBody` in `#wacando/omemo`). Before it,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wacando/agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Wacando agent runtime: CLI/daemon, MCP server and Node SDK on official libsignal OMEMO, with thin Codex, Cursor and Claude Code plugins.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,13 @@ const identifier = value => typeof value === 'string' && /^[a-zA-Z0-9_-]{1,200}$
|
|
|
13
13
|
const secret = (value, prefix) => typeof value === 'string' && value.startsWith(prefix) && /^[a-zA-Z0-9_.-]{8,512}$/.test(value);
|
|
14
14
|
const lostEnrollment = 'Enrollment result was not safely saved. Check the agent status on the site. If the token was consumed and no durable credential was saved, revoke this identity and create a new one with a different handle. No automatic recovery exists in v1.';
|
|
15
15
|
export const SESSION_REPLACED = 'SESSION_REPLACED: this identity was taken over by a newer session elsewhere (one session per agent). Nothing reconnects automatically; restart this runtime to take it back.';
|
|
16
|
+
/**
|
|
17
|
+
* lbframe/wacando-agent#26: the server failed SCRAM verification (forged or
|
|
18
|
+
* missing ServerSignature `v=`, SASL2 offered, no SCRAM mechanism, iteration
|
|
19
|
+
* count out of bounds...). Terminal: never retried automatically.
|
|
20
|
+
*/
|
|
21
|
+
export const XMPP_SERVER_UNVERIFIED = 'XMPP_SERVER_UNVERIFIED: the XMPP server did not prove it knows this identity\'s credential (SCRAM-SHA-256/SHA-1 with a verified server signature over SASL1 is required). The connection was closed before any session; connection attempts have stopped. Check the configured service, then restart this runtime.';
|
|
22
|
+
export const SCRAM_REFUSED = 'SCRAM_REFUSED';
|
|
16
23
|
const retryConnection = 'Agent credential is saved, but connection was not confirmed. Automatic retry is scheduled while this MCP host stays open. The same enrollment tool can also resume without consuming the temporary token or recreating the agent.';
|
|
17
24
|
|
|
18
25
|
export class OnboardingService {
|
|
@@ -32,6 +39,9 @@ export class OnboardingService {
|
|
|
32
39
|
// Identities whose session the server replaced (<conflict/>): terminal for
|
|
33
40
|
// this process, so two runtimes never steal the session from each other.
|
|
34
41
|
this.replaced = new Set();
|
|
42
|
+
// Identities whose XMPP server failed SCRAM verification (#26): terminal
|
|
43
|
+
// for this process, never retried in a loop against that server.
|
|
44
|
+
this.refused = new Set();
|
|
35
45
|
this.schedule = schedule; this.cancel = cancel; this.random = random; this.report = report;
|
|
36
46
|
this.onStanza = onStanza;
|
|
37
47
|
this.onSession = onSession;
|
|
@@ -128,7 +138,7 @@ export class OnboardingService {
|
|
|
128
138
|
catch {
|
|
129
139
|
// open() schedules network recovery itself. Lock/storage failures also
|
|
130
140
|
// remain observable and retryable, except after terminal access rejection.
|
|
131
|
-
if (!this.closed && !this.timers.has(agent.agentId) && !['revoked', 'replaced'].includes(this.states.get(agent.agentId)?.state)) this.retry(agent, 'local operation');
|
|
141
|
+
if (!this.closed && !this.timers.has(agent.agentId) && !['revoked', 'replaced', 'refused'].includes(this.states.get(agent.agentId)?.state)) this.retry(agent, 'local operation');
|
|
132
142
|
}
|
|
133
143
|
finally { this.recoveries.delete(recovery); }
|
|
134
144
|
}, delay);
|
|
@@ -150,10 +160,18 @@ export class OnboardingService {
|
|
|
150
160
|
this.status(agent, 'replaced', phase);
|
|
151
161
|
await this.disconnect(agent.agentId);
|
|
152
162
|
}
|
|
163
|
+
/** The XMPP server failed SCRAM verification: stop for good in this process. */
|
|
164
|
+
async markRefused(agent, phase = 'XMPP connection') {
|
|
165
|
+
this.refused.add(agent.agentId);
|
|
166
|
+
this.cancel(this.timers.get(agent.agentId)); this.timers.delete(agent.agentId);
|
|
167
|
+
if (this.states.get(agent.agentId)?.state !== 'refused') this.status(agent, 'refused', phase);
|
|
168
|
+
await this.disconnect(agent.agentId);
|
|
169
|
+
}
|
|
153
170
|
async open(agent) {
|
|
154
171
|
this.cancel(this.timers.get(agent.agentId)); this.timers.delete(agent.agentId);
|
|
155
172
|
let release;
|
|
156
173
|
let phase = 'local identity';
|
|
174
|
+
if (this.refused.has(agent.agentId)) throw new SafeError(XMPP_SERVER_UNVERIFIED);
|
|
157
175
|
if (this.replaced.has(agent.agentId) || this.sessions.get(agent.agentId)?.session?.isReplaced?.()) {
|
|
158
176
|
await this.markReplaced(agent);
|
|
159
177
|
throw new SafeError(SESSION_REPLACED);
|
|
@@ -211,6 +229,12 @@ export class OnboardingService {
|
|
|
211
229
|
await this.store.write(agent.origin, this.host, 'agent', agent.agentId, { ...agent, revoked: true });
|
|
212
230
|
throw new SafeError('Agent revoked or durable access rejected. Connection attempts have stopped. Check the identity on the site.');
|
|
213
231
|
}
|
|
232
|
+
if (error?.code === SCRAM_REFUSED) {
|
|
233
|
+
// Forged/missing ServerSignature or SASL2 offer: the connection is
|
|
234
|
+
// already closed; no automatic retry against this server.
|
|
235
|
+
await this.markRefused(agent, phase);
|
|
236
|
+
throw new SafeError(XMPP_SERVER_UNVERIFIED);
|
|
237
|
+
}
|
|
214
238
|
const active = this.sessions.get(agent.agentId);
|
|
215
239
|
if (this.replaced.has(agent.agentId) || active?.session?.isReplaced?.()) {
|
|
216
240
|
// Another process took this identity over (newest session wins). Never
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Must match all distributed plugin manifests; checked by the test suite.
|
|
2
|
-
export const pluginVersion = '2.
|
|
2
|
+
export const pluginVersion = '2.4.0';
|
|
@@ -1,10 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { announceAvailable, refuseSubscriptionRequests, ScramAuthError, scramClient, startScramClient, xml } from '#wacando/xmpp';
|
|
2
|
+
import { SCRAM_REFUSED, XMPP_SERVER_UNVERIFIED } from '../onboarding/service.mjs';
|
|
3
3
|
import { SafeError } from '../onboarding/storage.mjs';
|
|
4
4
|
import { loadDevice } from './device.mjs';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @xmpp/connection 0.14.0 `start()` creates its "online" promise (rejected by
|
|
8
|
+
* any entity 'error') BEFORE awaiting `open()`. When an 'error' is emitted
|
|
9
|
+
* while the stream opens (Node.js 24's WebSocket reports a socket cut that
|
|
10
|
+
* way), `open()` rejects, `start()` throws without ever awaiting that promise
|
|
11
|
+
* and its rejection is unhandled: the whole process (daemon, MCP server)
|
|
12
|
+
* dies. Same steps, with the "online" promise always observed.
|
|
13
|
+
*/
|
|
14
|
+
function observeOnlinePromise(xmpp) {
|
|
15
|
+
xmpp.start = async function start() {
|
|
16
|
+
if (this.status !== 'offline') throw new Error('Connection is not offline');
|
|
17
|
+
const { service, domain, lang } = this.options;
|
|
18
|
+
await this.connect(service);
|
|
19
|
+
const online = new Promise((resolve, reject) => {
|
|
20
|
+
const settle = () => { this.removeListener('online', onOnline); this.removeListener('error', onError); };
|
|
21
|
+
const onOnline = value => { settle(); resolve(value); };
|
|
22
|
+
const onError = error => { settle(); reject(error); };
|
|
23
|
+
this.on('online', onOnline);
|
|
24
|
+
this.on('error', onError);
|
|
25
|
+
});
|
|
26
|
+
online.catch(() => {});
|
|
27
|
+
await this.open({ domain, lang });
|
|
28
|
+
return online;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
const unavailable = () => new SafeError('Secure messaging is unavailable. Retry the saved identity.');
|
|
7
33
|
|
|
34
|
+
/** SafeError for a ScramAuthError: the fixed message plus its local reason enum (no secret). */
|
|
35
|
+
const serverUnverified = error => Object.assign(new SafeError(XMPP_SERVER_UNVERIFIED), { code: SCRAM_REFUSED, reason: error.reason });
|
|
36
|
+
|
|
8
37
|
/**
|
|
9
38
|
* One XMPP c2s stream (WebSocket) + this identity's native OMEMO device.
|
|
10
39
|
*
|
|
@@ -13,12 +42,18 @@ const unavailable = () => new SafeError('Secure messaging is unavailable. Retry
|
|
|
13
42
|
* delivery addressed to the bare JID after that point reaches this resource
|
|
14
43
|
* and none can be missed between "online" and the caller subscribing.
|
|
15
44
|
* Raw XMPP errors, stanzas and credentials are never logged.
|
|
45
|
+
*
|
|
46
|
+
* The client is the embedded `scramClient` (lbframe/wacando#186): SCRAM only,
|
|
47
|
+
* SASL1 only, and authentication succeeds only once the server's
|
|
48
|
+
* ServerSignature (`v=`) was verified. A refusal closes the connection,
|
|
49
|
+
* stops @xmpp/client's reconnect and rejects with `code: 'SCRAM_REFUSED'`.
|
|
16
50
|
*/
|
|
17
51
|
export async function connectSession({ jid, password, service, stateBaseDir, onStanza, onReplaced, timeoutMs = 15000 }) {
|
|
18
52
|
const provider = await loadDevice(jid, stateBaseDir);
|
|
19
53
|
const at = jid.indexOf('@');
|
|
20
54
|
const domain = jid.slice(at + 1);
|
|
21
|
-
const xmpp =
|
|
55
|
+
const xmpp = scramClient({ service, domain, username: jid.slice(0, at), password });
|
|
56
|
+
observeOnlinePromise(xmpp);
|
|
22
57
|
// One server-enforced session per agent (lbframe/wacando #15): a newer login
|
|
23
58
|
// ends this one with <conflict/>. Remember it so nothing reconnects in a loop.
|
|
24
59
|
let replaced = false;
|
|
@@ -31,15 +66,16 @@ export async function connectSession({ jid, password, service, stateBaseDir, onS
|
|
|
31
66
|
let timer;
|
|
32
67
|
const deadline = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('timeout')), timeoutMs); });
|
|
33
68
|
try {
|
|
34
|
-
await Promise.race([xmpp
|
|
69
|
+
await Promise.race([startScramClient(xmpp), deadline]);
|
|
35
70
|
xmpp.reconnect?.stop();
|
|
36
71
|
// The control plane owns the roster (#13): never approve a subscription.
|
|
37
72
|
refuseSubscriptionRequests(xmpp);
|
|
38
73
|
if (onStanza) xmpp.on('stanza', stanza => onStanza(stanza, xmpp));
|
|
39
74
|
await Promise.race([announceAvailable(xmpp, timeoutMs), deadline]);
|
|
40
|
-
} catch {
|
|
75
|
+
} catch (error) {
|
|
76
|
+
xmpp.reconnect?.stop();
|
|
41
77
|
await xmpp.stop().catch(() => {});
|
|
42
|
-
throw unavailable();
|
|
78
|
+
throw error instanceof ScramAuthError ? serverUnverified(error) : unavailable();
|
|
43
79
|
} finally { clearTimeout(timer); }
|
|
44
80
|
return {
|
|
45
81
|
client: xmpp, provider, bareJid: provider.bareJid, domain,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
DomainError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FFLYSYV6.mjs";
|
|
5
5
|
|
|
6
6
|
// packages/omemo/src/ns.ts
|
|
7
7
|
var NS_OMEMO = "eu.siacs.conversations.axolotl";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
|
|
3
3
|
// packages/contracts/src/errors.ts
|
|
4
4
|
var DomainError = class extends Error {
|
|
@@ -1,10 +1,370 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
XMPP_DOMAIN
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FFLYSYV6.mjs";
|
|
5
5
|
|
|
6
|
-
// packages/xmpp/src/
|
|
6
|
+
// packages/xmpp/src/scram.ts
|
|
7
|
+
import { client } from "@xmpp/client";
|
|
7
8
|
import xml from "@xmpp/xml";
|
|
9
|
+
var SASL1_NS = "urn:ietf:params:xml:ns:xmpp-sasl";
|
|
10
|
+
var SASL2_NS = "urn:xmpp:sasl:2";
|
|
11
|
+
var SCRAM_MIN_ITERATIONS = 4096;
|
|
12
|
+
var SCRAM_MAX_ITERATIONS = 1e5;
|
|
13
|
+
var HASH_BYTES = { "SHA-1": 20, "SHA-256": 32 };
|
|
14
|
+
var REASON_TEXT = {
|
|
15
|
+
"missing-server-signature": "server sent no ServerSignature (v=)",
|
|
16
|
+
"server-signature-mismatch": "server ServerSignature (v=) does not match",
|
|
17
|
+
"malformed-server-message": "malformed SCRAM server message",
|
|
18
|
+
"server-error": "server reported a SCRAM error (e=)",
|
|
19
|
+
"nonce-mismatch": "server nonce does not extend the client nonce",
|
|
20
|
+
"weak-iteration-count": `server iteration count below ${SCRAM_MIN_ITERATIONS}`,
|
|
21
|
+
"excessive-iteration-count": `server iteration count above ${SCRAM_MAX_ITERATIONS}`,
|
|
22
|
+
"sasl2-refused": "server offers SASL2, which this client refuses (SASL1 SCRAM only)",
|
|
23
|
+
"no-scram-mechanism": "server offers no supported SCRAM mechanism",
|
|
24
|
+
"out-of-order": "SCRAM message out of order"
|
|
25
|
+
};
|
|
26
|
+
var ScramAuthError = class extends Error {
|
|
27
|
+
reason;
|
|
28
|
+
mechanism;
|
|
29
|
+
constructor(reason, mechanism) {
|
|
30
|
+
super(
|
|
31
|
+
`SCRAM authentication refused${mechanism ? ` (${mechanism})` : ""}: ${REASON_TEXT[reason]}`
|
|
32
|
+
);
|
|
33
|
+
this.name = "ScramAuthError";
|
|
34
|
+
this.reason = reason;
|
|
35
|
+
this.mechanism = mechanism;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var encoder = new TextEncoder();
|
|
39
|
+
function utf8(text) {
|
|
40
|
+
return new Uint8Array(encoder.encode(text));
|
|
41
|
+
}
|
|
42
|
+
function bytesToBinary(bytes) {
|
|
43
|
+
let out = "";
|
|
44
|
+
for (const b of bytes) out += String.fromCharCode(b);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function binaryToBytes(binary) {
|
|
48
|
+
const out = new Uint8Array(binary.length);
|
|
49
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i) & 255;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function base64Encode(bytes) {
|
|
53
|
+
return globalThis.btoa(bytesToBinary(bytes));
|
|
54
|
+
}
|
|
55
|
+
function base64Decode(text) {
|
|
56
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(text)) {
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return binaryToBytes(globalThis.atob(text));
|
|
61
|
+
} catch {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function constantTimeEqual(a, b) {
|
|
66
|
+
let diff = a.length ^ b.length;
|
|
67
|
+
const n = Math.max(a.length, b.length);
|
|
68
|
+
for (let i = 0; i < n; i++) diff |= (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
69
|
+
return diff === 0;
|
|
70
|
+
}
|
|
71
|
+
function xor(a, b) {
|
|
72
|
+
const out = new Uint8Array(a.length);
|
|
73
|
+
for (let i = 0; i < a.length; i++) out[i] = (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
async function hmac(hash, key, data) {
|
|
77
|
+
const k = await globalThis.crypto.subtle.importKey("raw", key, { name: "HMAC", hash }, false, [
|
|
78
|
+
"sign"
|
|
79
|
+
]);
|
|
80
|
+
return new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", k, data));
|
|
81
|
+
}
|
|
82
|
+
async function digest(hash, data) {
|
|
83
|
+
return new Uint8Array(await globalThis.crypto.subtle.digest(hash, data));
|
|
84
|
+
}
|
|
85
|
+
async function hi(hash, password, salt, iterations) {
|
|
86
|
+
const key = await globalThis.crypto.subtle.importKey("raw", utf8(password), "PBKDF2", false, [
|
|
87
|
+
"deriveBits"
|
|
88
|
+
]);
|
|
89
|
+
return new Uint8Array(
|
|
90
|
+
await globalThis.crypto.subtle.deriveBits(
|
|
91
|
+
{ name: "PBKDF2", hash, salt, iterations },
|
|
92
|
+
key,
|
|
93
|
+
HASH_BYTES[hash] * 8
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
function parseAttributes(message) {
|
|
98
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
99
|
+
for (const part of message.split(",")) {
|
|
100
|
+
const m = /^([A-Za-z])=(.*)$/s.exec(part);
|
|
101
|
+
if (!m) return void 0;
|
|
102
|
+
const [, key, value] = m;
|
|
103
|
+
if (attrs.has(key)) return void 0;
|
|
104
|
+
attrs.set(key, value);
|
|
105
|
+
}
|
|
106
|
+
return attrs;
|
|
107
|
+
}
|
|
108
|
+
function saslname(name) {
|
|
109
|
+
return name.replace(/=/g, "=3D").replace(/,/g, "=2C");
|
|
110
|
+
}
|
|
111
|
+
async function scramProofs(hash, password, salt, iterations, authMessage) {
|
|
112
|
+
const salted = await hi(hash, password, salt, iterations);
|
|
113
|
+
const clientKey = await hmac(hash, salted, utf8("Client Key"));
|
|
114
|
+
const storedKey = await digest(hash, clientKey);
|
|
115
|
+
const am = utf8(authMessage);
|
|
116
|
+
const clientSignature = await hmac(hash, storedKey, am);
|
|
117
|
+
const serverKey = await hmac(hash, salted, utf8("Server Key"));
|
|
118
|
+
return {
|
|
119
|
+
clientProof: xor(clientKey, clientSignature),
|
|
120
|
+
serverSignature: await hmac(hash, serverKey, am)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function verifyServerFinal(serverFinal, expected, mechanism) {
|
|
124
|
+
if (serverFinal === void 0 || serverFinal === "") {
|
|
125
|
+
throw new ScramAuthError("missing-server-signature", mechanism);
|
|
126
|
+
}
|
|
127
|
+
const attrs = parseAttributes(serverFinal);
|
|
128
|
+
if (!attrs) throw new ScramAuthError("malformed-server-message", mechanism);
|
|
129
|
+
if (attrs.has("e")) throw new ScramAuthError("server-error", mechanism);
|
|
130
|
+
const v = attrs.get("v");
|
|
131
|
+
if (v === void 0) throw new ScramAuthError("missing-server-signature", mechanism);
|
|
132
|
+
const received = base64Decode(v);
|
|
133
|
+
if (!received) throw new ScramAuthError("malformed-server-message", mechanism);
|
|
134
|
+
if (!constantTimeEqual(received, expected)) {
|
|
135
|
+
throw new ScramAuthError("server-signature-mismatch", mechanism);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
var ScramMechanism = class {
|
|
139
|
+
constructor(hash, genNonce = defaultNonce) {
|
|
140
|
+
this.hash = hash;
|
|
141
|
+
this.genNonce = genNonce;
|
|
142
|
+
this.name = hash === "SHA-1" ? "SCRAM-SHA-1" : "SCRAM-SHA-256";
|
|
143
|
+
}
|
|
144
|
+
hash;
|
|
145
|
+
genNonce;
|
|
146
|
+
name;
|
|
147
|
+
clientFirst = true;
|
|
148
|
+
stage = "initial";
|
|
149
|
+
clientNonce = "";
|
|
150
|
+
clientFirstBare = "";
|
|
151
|
+
serverFirst = "";
|
|
152
|
+
expectedServerSignature;
|
|
153
|
+
pendingServerFinal;
|
|
154
|
+
verifiedFlag = false;
|
|
155
|
+
/** True once the server's ServerSignature has been verified. */
|
|
156
|
+
get verified() {
|
|
157
|
+
return this.verifiedFlag;
|
|
158
|
+
}
|
|
159
|
+
/** True once the client-final-message was sent (a `v=` is now due). */
|
|
160
|
+
get awaitingServerFinal() {
|
|
161
|
+
return this.stage === "final";
|
|
162
|
+
}
|
|
163
|
+
async response(cred) {
|
|
164
|
+
switch (this.stage) {
|
|
165
|
+
case "initial":
|
|
166
|
+
return this.clientFirstMessage(cred);
|
|
167
|
+
case "challenge":
|
|
168
|
+
return this.clientFinalMessage(cred);
|
|
169
|
+
case "final":
|
|
170
|
+
await this.final(this.pendingServerFinal);
|
|
171
|
+
return "";
|
|
172
|
+
default:
|
|
173
|
+
throw new ScramAuthError("out-of-order", this.name);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
challenge(chal) {
|
|
177
|
+
if (this.stage === "challenge") {
|
|
178
|
+
this.serverFirst = utf8Decode(chal);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (this.stage === "final") {
|
|
182
|
+
this.pendingServerFinal = chal;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
throw new ScramAuthError("out-of-order", this.name);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* server-final-message (binary string, as decoded from base64) from
|
|
189
|
+
* <success/> (SASL1), <additional-data/> (SASL2) or a last <challenge/>.
|
|
190
|
+
*/
|
|
191
|
+
async final(data) {
|
|
192
|
+
if (this.verifiedFlag) return;
|
|
193
|
+
if (this.stage !== "final" || !this.expectedServerSignature) {
|
|
194
|
+
throw new ScramAuthError("out-of-order", this.name);
|
|
195
|
+
}
|
|
196
|
+
this.stage = "done";
|
|
197
|
+
const expected = this.expectedServerSignature;
|
|
198
|
+
this.expectedServerSignature = void 0;
|
|
199
|
+
verifyServerFinal(data === void 0 ? void 0 : utf8Decode(data), expected, this.name);
|
|
200
|
+
this.verifiedFlag = true;
|
|
201
|
+
}
|
|
202
|
+
clientFirstMessage(cred) {
|
|
203
|
+
this.clientNonce = this.genNonce();
|
|
204
|
+
this.clientFirstBare = `n=${saslname(cred.username ?? "")},r=${this.clientNonce}`;
|
|
205
|
+
this.stage = "challenge";
|
|
206
|
+
return utf8Binary(`n,,${this.clientFirstBare}`);
|
|
207
|
+
}
|
|
208
|
+
async clientFinalMessage(cred) {
|
|
209
|
+
const attrs = parseAttributes(this.serverFirst);
|
|
210
|
+
const nonce = attrs?.get("r");
|
|
211
|
+
const saltText = attrs?.get("s");
|
|
212
|
+
const iterText = attrs?.get("i");
|
|
213
|
+
if (attrs?.has("e")) throw new ScramAuthError("server-error", this.name);
|
|
214
|
+
if (!attrs || attrs.has("m") || !nonce || !saltText || !iterText) {
|
|
215
|
+
throw new ScramAuthError("malformed-server-message", this.name);
|
|
216
|
+
}
|
|
217
|
+
if (!nonce.startsWith(this.clientNonce) || nonce.length <= this.clientNonce.length) {
|
|
218
|
+
throw new ScramAuthError("nonce-mismatch", this.name);
|
|
219
|
+
}
|
|
220
|
+
const salt = base64Decode(saltText);
|
|
221
|
+
if (!salt || salt.length === 0 || !/^[1-9][0-9]{0,9}$/.test(iterText)) {
|
|
222
|
+
throw new ScramAuthError("malformed-server-message", this.name);
|
|
223
|
+
}
|
|
224
|
+
const iterations = Number(iterText);
|
|
225
|
+
if (iterations < SCRAM_MIN_ITERATIONS) {
|
|
226
|
+
throw new ScramAuthError("weak-iteration-count", this.name);
|
|
227
|
+
}
|
|
228
|
+
if (iterations > SCRAM_MAX_ITERATIONS) {
|
|
229
|
+
throw new ScramAuthError("excessive-iteration-count", this.name);
|
|
230
|
+
}
|
|
231
|
+
const withoutProof = `c=biws,r=${nonce}`;
|
|
232
|
+
const authMessage = `${this.clientFirstBare},${this.serverFirst},${withoutProof}`;
|
|
233
|
+
const { clientProof, serverSignature } = await scramProofs(
|
|
234
|
+
this.hash,
|
|
235
|
+
cred.password ?? "",
|
|
236
|
+
salt,
|
|
237
|
+
iterations,
|
|
238
|
+
authMessage
|
|
239
|
+
);
|
|
240
|
+
this.expectedServerSignature = serverSignature;
|
|
241
|
+
this.stage = "final";
|
|
242
|
+
return utf8Binary(`${withoutProof},p=${base64Encode(clientProof)}`);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function utf8Binary(text) {
|
|
246
|
+
return bytesToBinary(utf8(text));
|
|
247
|
+
}
|
|
248
|
+
function utf8Decode(binary) {
|
|
249
|
+
return new TextDecoder().decode(binaryToBytes(binary));
|
|
250
|
+
}
|
|
251
|
+
function defaultNonce() {
|
|
252
|
+
const bytes = new Uint8Array(24);
|
|
253
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
254
|
+
return base64Encode(bytes);
|
|
255
|
+
}
|
|
256
|
+
var SCRAM_HASHES = ["SHA-256", "SHA-1"];
|
|
257
|
+
var SCRAM_NAMES = SCRAM_HASHES.map(
|
|
258
|
+
(h) => h === "SHA-1" ? "SCRAM-SHA-1" : "SCRAM-SHA-256"
|
|
259
|
+
);
|
|
260
|
+
var NS_STREAM = "http://etherx.jabber.org/streams";
|
|
261
|
+
function saslFeaturesRefusal(features) {
|
|
262
|
+
if (features.getChild("authentication", SASL2_NS)) return "sasl2-refused";
|
|
263
|
+
const sasl1 = features.getChild("mechanisms", SASL1_NS);
|
|
264
|
+
if (!sasl1) return void 0;
|
|
265
|
+
const offered = sasl1.getChildren("mechanism").map((m) => m.text().trim());
|
|
266
|
+
return SCRAM_NAMES.some((name) => offered.includes(name)) ? void 0 : "no-scram-mechanism";
|
|
267
|
+
}
|
|
268
|
+
var states = /* @__PURE__ */ new WeakMap();
|
|
269
|
+
function scramClient(options) {
|
|
270
|
+
const { username, password, ...rest } = options;
|
|
271
|
+
let current;
|
|
272
|
+
let rejectFailure = () => {
|
|
273
|
+
};
|
|
274
|
+
const state = {
|
|
275
|
+
failure: new Promise((_, reject) => {
|
|
276
|
+
rejectFailure = reject;
|
|
277
|
+
})
|
|
278
|
+
};
|
|
279
|
+
state.failure.catch(() => {
|
|
280
|
+
});
|
|
281
|
+
const userAgent = xml("user-agent", { id: globalThis.crypto.randomUUID() });
|
|
282
|
+
const cut = (entity, err) => {
|
|
283
|
+
state.cutting ??= (async () => {
|
|
284
|
+
entity.reconnect?.stop();
|
|
285
|
+
await entity.stop().catch(() => {
|
|
286
|
+
});
|
|
287
|
+
rejectFailure(err);
|
|
288
|
+
})();
|
|
289
|
+
return state.cutting;
|
|
290
|
+
};
|
|
291
|
+
let refused;
|
|
292
|
+
const xmpp = client({
|
|
293
|
+
...rest,
|
|
294
|
+
credentials: async (authenticate, mechanisms, _fast, entity) => {
|
|
295
|
+
const failClosed = async (err) => {
|
|
296
|
+
await cut(entity, err);
|
|
297
|
+
throw err;
|
|
298
|
+
};
|
|
299
|
+
if (refused) return failClosed(refused);
|
|
300
|
+
const mechanism = SCRAM_NAMES.find((name) => mechanisms.includes(name));
|
|
301
|
+
if (!mechanism) return failClosed(new ScramAuthError("no-scram-mechanism"));
|
|
302
|
+
let successData;
|
|
303
|
+
const onNonza = (el) => {
|
|
304
|
+
if (el.is("success", SASL1_NS)) successData = decodeSuccess(el.text());
|
|
305
|
+
};
|
|
306
|
+
entity.on("nonza", onNonza);
|
|
307
|
+
current = void 0;
|
|
308
|
+
try {
|
|
309
|
+
await authenticate({ username, password }, mechanism, userAgent);
|
|
310
|
+
const mech = current;
|
|
311
|
+
if (!mech) throw new ScramAuthError("out-of-order", mechanism);
|
|
312
|
+
if (!mech.verified) await mech.final(successData);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (err instanceof ScramAuthError) return failClosed(err);
|
|
315
|
+
throw err;
|
|
316
|
+
} finally {
|
|
317
|
+
entity.removeListener("nonza", onNonza);
|
|
318
|
+
current = void 0;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
const factory = xmpp.saslFactory;
|
|
323
|
+
const bound = SCRAM_HASHES.map((hash) => ({
|
|
324
|
+
name: hash === "SHA-1" ? "SCRAM-SHA-1" : "SCRAM-SHA-256",
|
|
325
|
+
mech: class extends ScramMechanism {
|
|
326
|
+
constructor() {
|
|
327
|
+
super(hash);
|
|
328
|
+
current = this;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}));
|
|
332
|
+
factory._mechs.splice(0, factory._mechs.length, ...bound);
|
|
333
|
+
xmpp.on("nonza", (el) => {
|
|
334
|
+
if (!el.is("features", NS_STREAM)) return;
|
|
335
|
+
const reason = saslFeaturesRefusal(el);
|
|
336
|
+
if (!reason) return;
|
|
337
|
+
refused = new ScramAuthError(reason);
|
|
338
|
+
void cut(xmpp, refused);
|
|
339
|
+
});
|
|
340
|
+
states.set(xmpp, state);
|
|
341
|
+
return xmpp;
|
|
342
|
+
}
|
|
343
|
+
function decodeSuccess(text) {
|
|
344
|
+
const trimmed = text.trim();
|
|
345
|
+
if (trimmed === "" || trimmed === "=") return void 0;
|
|
346
|
+
try {
|
|
347
|
+
return globalThis.atob(trimmed);
|
|
348
|
+
} catch {
|
|
349
|
+
return void 0;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function startScramClient(xmpp) {
|
|
353
|
+
const state = states.get(xmpp);
|
|
354
|
+
if (!state) return xmpp.start();
|
|
355
|
+
try {
|
|
356
|
+
return await Promise.race([xmpp.start(), state.failure]);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
if (state.cutting) {
|
|
359
|
+
await state.cutting;
|
|
360
|
+
return state.failure;
|
|
361
|
+
}
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// packages/xmpp/src/muc.ts
|
|
367
|
+
import xml2 from "@xmpp/xml";
|
|
8
368
|
|
|
9
369
|
// packages/xmpp/src/jid.ts
|
|
10
370
|
var COMPONENT_JID = `policy.${XMPP_DOMAIN}`;
|
|
@@ -113,7 +473,7 @@ var MucJoinError = class extends MucError {
|
|
|
113
473
|
nick;
|
|
114
474
|
};
|
|
115
475
|
function joinPresenceEl(room, nick) {
|
|
116
|
-
return
|
|
476
|
+
return xml2("presence", { to: occupantJid(room, nick) }, xml2("x", { xmlns: NS_MUC }));
|
|
117
477
|
}
|
|
118
478
|
async function joinRoom(entity, room, nick, timeoutMs = defaultTimeoutMs()) {
|
|
119
479
|
const self = occupantJid(room, nick);
|
|
@@ -266,24 +626,24 @@ var MucOriginTracker = class {
|
|
|
266
626
|
};
|
|
267
627
|
|
|
268
628
|
// packages/xmpp/src/mam.ts
|
|
269
|
-
import
|
|
629
|
+
import xml3 from "@xmpp/xml";
|
|
270
630
|
var NS_MAM = "urn:xmpp:mam:2";
|
|
271
631
|
var NS_FORWARD2 = "urn:xmpp:forward:0";
|
|
272
632
|
var NS_RSM = "http://jabber.org/protocol/rsm";
|
|
273
633
|
var queryCounter = 0;
|
|
274
634
|
function filterField(varName, value) {
|
|
275
|
-
return
|
|
635
|
+
return xml3("field", { var: varName }, xml3("value", {}, value));
|
|
276
636
|
}
|
|
277
637
|
function mamQueryEl(options) {
|
|
278
|
-
const fields = [
|
|
638
|
+
const fields = [xml3("field", { var: "FORM_TYPE", type: "hidden" }, xml3("value", {}, NS_MAM))];
|
|
279
639
|
if (options.withJid !== void 0) fields.push(filterField("with", options.withJid));
|
|
280
640
|
if (options.start !== void 0) fields.push(filterField("start", options.start));
|
|
281
641
|
if (options.end !== void 0) fields.push(filterField("end", options.end));
|
|
282
|
-
const children = [
|
|
642
|
+
const children = [xml3("x", { xmlns: NS_DATA_FORMS, type: "submit" }, ...fields)];
|
|
283
643
|
if (options.max !== void 0) {
|
|
284
|
-
children.push(
|
|
644
|
+
children.push(xml3("set", { xmlns: NS_RSM }, xml3("max", {}, String(options.max))));
|
|
285
645
|
}
|
|
286
|
-
return
|
|
646
|
+
return xml3("query", { xmlns: NS_MAM, queryid: options.queryId }, ...children);
|
|
287
647
|
}
|
|
288
648
|
async function queryMam(entity, options, timeoutMs = defaultTimeoutMs()) {
|
|
289
649
|
const queryId = options.queryId ?? `mam${++queryCounter}`;
|
|
@@ -326,7 +686,7 @@ function forwardedStanza(forwarded) {
|
|
|
326
686
|
|
|
327
687
|
// packages/xmpp/src/roster.ts
|
|
328
688
|
import { jid as parseJid } from "@xmpp/jid";
|
|
329
|
-
import
|
|
689
|
+
import xml4 from "@xmpp/xml";
|
|
330
690
|
function subscriptionRefusal(stanza) {
|
|
331
691
|
if (!stanza.is("presence") || stanza.getAttr("type") !== "subscribe") return null;
|
|
332
692
|
const from = stanza.getAttr("from");
|
|
@@ -337,7 +697,7 @@ function subscriptionRefusal(stanza) {
|
|
|
337
697
|
} catch {
|
|
338
698
|
return null;
|
|
339
699
|
}
|
|
340
|
-
return
|
|
700
|
+
return xml4("presence", { to: bare, type: "unsubscribed" });
|
|
341
701
|
}
|
|
342
702
|
function refuseSubscriptionRequests(entity) {
|
|
343
703
|
const onStanza = (stanza) => {
|
|
@@ -356,26 +716,26 @@ var UNKNOWN_PRESENCE = Object.freeze({
|
|
|
356
716
|
});
|
|
357
717
|
|
|
358
718
|
// packages/xmpp/src/pubsub.ts
|
|
359
|
-
import
|
|
719
|
+
import xml5 from "@xmpp/xml";
|
|
360
720
|
var NS_PUBSUB = "http://jabber.org/protocol/pubsub";
|
|
361
721
|
var NS_PUBSUB_PUBLISH_OPTIONS = "http://jabber.org/protocol/pubsub#publish-options";
|
|
362
722
|
function pepPublishEl(node, itemId, payload, accessModel = "open") {
|
|
363
|
-
return
|
|
723
|
+
return xml5(
|
|
364
724
|
"pubsub",
|
|
365
725
|
{ xmlns: NS_PUBSUB },
|
|
366
|
-
|
|
367
|
-
|
|
726
|
+
xml5("publish", { node }, xml5("item", { id: itemId }, payload)),
|
|
727
|
+
xml5(
|
|
368
728
|
"publish-options",
|
|
369
729
|
{},
|
|
370
|
-
|
|
730
|
+
xml5(
|
|
371
731
|
"x",
|
|
372
732
|
{ xmlns: NS_DATA_FORMS, type: "submit" },
|
|
373
|
-
|
|
733
|
+
xml5(
|
|
374
734
|
"field",
|
|
375
735
|
{ var: "FORM_TYPE", type: "hidden" },
|
|
376
|
-
|
|
736
|
+
xml5("value", {}, NS_PUBSUB_PUBLISH_OPTIONS)
|
|
377
737
|
),
|
|
378
|
-
|
|
738
|
+
xml5("field", { var: "pubsub#access_model" }, xml5("value", {}, accessModel))
|
|
379
739
|
)
|
|
380
740
|
)
|
|
381
741
|
);
|
|
@@ -383,7 +743,7 @@ function pepPublishEl(node, itemId, payload, accessModel = "open") {
|
|
|
383
743
|
function pubsubItemsEl(node, maxItems) {
|
|
384
744
|
const attrs = { node };
|
|
385
745
|
if (maxItems !== void 0) attrs.max_items = String(maxItems);
|
|
386
|
-
return
|
|
746
|
+
return xml5("pubsub", { xmlns: NS_PUBSUB }, xml5("items", attrs));
|
|
387
747
|
}
|
|
388
748
|
async function pepPublish(entity, to, node, itemId, payload, timeoutMs = defaultTimeoutMs()) {
|
|
389
749
|
await entity.iqCaller.set(pepPublishEl(node, itemId, payload), to, timeoutMs);
|
|
@@ -408,6 +768,9 @@ function isItemNotFound(err) {
|
|
|
408
768
|
}
|
|
409
769
|
|
|
410
770
|
export {
|
|
771
|
+
ScramAuthError,
|
|
772
|
+
scramClient,
|
|
773
|
+
startScramClient,
|
|
411
774
|
defaultTimeoutMs,
|
|
412
775
|
waitForStanza,
|
|
413
776
|
MucJoinError,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
BODY_MARKER,
|
|
4
4
|
GCM_IV_BYTES,
|
|
@@ -29,8 +29,8 @@ import {
|
|
|
29
29
|
signerJid,
|
|
30
30
|
signerRole,
|
|
31
31
|
targetJid
|
|
32
|
-
} from "./chunks/chunk-
|
|
33
|
-
import "./chunks/chunk-
|
|
32
|
+
} from "./chunks/chunk-F7J3K43U.mjs";
|
|
33
|
+
import "./chunks/chunk-FFLYSYV6.mjs";
|
|
34
34
|
|
|
35
35
|
// packages/omemo/src/policy-wire.ts
|
|
36
36
|
import xml from "@xmpp/xml";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
authenticatedMessageOrigin,
|
|
4
4
|
pepFetchItems,
|
|
5
5
|
pepPublish
|
|
6
|
-
} from "./chunks/chunk-
|
|
6
|
+
} from "./chunks/chunk-FUNUGIGH.mjs";
|
|
7
7
|
import {
|
|
8
8
|
BODY_MARKER,
|
|
9
9
|
GCM_IV_BYTES,
|
|
@@ -37,13 +37,13 @@ import {
|
|
|
37
37
|
signerJid,
|
|
38
38
|
signerRole,
|
|
39
39
|
utf8Encode
|
|
40
|
-
} from "./chunks/chunk-
|
|
40
|
+
} from "./chunks/chunk-F7J3K43U.mjs";
|
|
41
41
|
import {
|
|
42
42
|
ALLOWED_MEDIA_TYPES,
|
|
43
43
|
MAX_FILE_BYTES,
|
|
44
44
|
MIN_FILE_BYTES,
|
|
45
45
|
looksLikePlaintext
|
|
46
|
-
} from "./chunks/chunk-
|
|
46
|
+
} from "./chunks/chunk-FFLYSYV6.mjs";
|
|
47
47
|
|
|
48
48
|
// packages/omemo/src/libsignal.ts
|
|
49
49
|
import { jid as parseJid2 } from "@xmpp/jid";
|
|
@@ -585,8 +585,8 @@ var OmemoProvider = class {
|
|
|
585
585
|
/**
|
|
586
586
|
* Resolve OMEMO device targets for a member set via PEP — fail closed:
|
|
587
587
|
* missing devicelist or missing bundle throws OmemoError before any
|
|
588
|
-
* plaintext is touched. Bundle elements are kept raw:
|
|
589
|
-
* backend parses
|
|
588
|
+
* plaintext is touched. Bundle elements are kept raw: the session
|
|
589
|
+
* backend parses them and refuses any non-PQXDH (classic) bundle.
|
|
590
590
|
*/
|
|
591
591
|
async resolveDeviceTargets(entity, memberBareJids, timeoutMs = 1e4) {
|
|
592
592
|
const recipients = [.../* @__PURE__ */ new Set([...memberBareJids, this.bareJid])];
|
|
@@ -1175,6 +1175,7 @@ var NAME_INVISIBLE = new RegExp(
|
|
|
1175
1175
|
"g"
|
|
1176
1176
|
);
|
|
1177
1177
|
var NAME_RESERVED = /[<>:"|?*]/g;
|
|
1178
|
+
var NAME_DOT_LOOKALIKE = /[\uff0e\ufe52\u2024]/g;
|
|
1178
1179
|
var WINDOWS_DEVICE = /^(con|prn|aux|nul|conin\$|conout\$|clock\$|com[0-9\u00b9\u00b2\u00b3]|lpt[0-9\u00b9\u00b2\u00b3])\s*(\..*)?$/i;
|
|
1179
1180
|
function utf8Length(s) {
|
|
1180
1181
|
return new TextEncoder().encode(s).length;
|
|
@@ -1188,7 +1189,7 @@ function cutUtf8(s, max) {
|
|
|
1188
1189
|
return out;
|
|
1189
1190
|
}
|
|
1190
1191
|
function sanitizeFileName(raw) {
|
|
1191
|
-
let name = raw.normalize("NFC").replace(NAME_INVISIBLE, "");
|
|
1192
|
+
let name = raw.normalize("NFC").replace(NAME_INVISIBLE, "").replace(NAME_DOT_LOOKALIKE, ".");
|
|
1192
1193
|
name = name.split(/[/\\]/).pop() ?? "";
|
|
1193
1194
|
name = name.replace(NAME_RESERVED, "_").trim();
|
|
1194
1195
|
name = name.replace(/^[.\s]+/, "").replace(/[.\s]+$/, "");
|
|
@@ -1540,7 +1541,10 @@ function fileIdOfShare(share) {
|
|
|
1540
1541
|
}
|
|
1541
1542
|
async function downloadFile(share, opts) {
|
|
1542
1543
|
const { fetchUrl } = parseAesgcmUrl(share.url, opts.pin);
|
|
1543
|
-
const
|
|
1544
|
+
const expected = share.ciphertextSize;
|
|
1545
|
+
if (!Number.isSafeInteger(expected) || expected < FILE_TAG_BYTES)
|
|
1546
|
+
throw new FileCryptoError("invalid_descriptor", "file share: ciphertext size");
|
|
1547
|
+
const limit = Math.min(expected, MAX_FILE_CIPHERTEXT_BYTES);
|
|
1544
1548
|
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
1545
1549
|
const signal = AbortSignal.timeout(opts.timeoutMs ?? FILE_DOWNLOAD_TIMEOUT_MS);
|
|
1546
1550
|
const failed = (what) => new FileCryptoError("download_failed", `download ${what}`);
|
|
@@ -1593,6 +1597,7 @@ async function downloadFile(share, opts) {
|
|
|
1593
1597
|
if (e instanceof FileCryptoError) throw e;
|
|
1594
1598
|
throw failed(signal.aborted ? "timed out" : "interrupted");
|
|
1595
1599
|
}
|
|
1600
|
+
if (total !== expected) throw failed("truncated");
|
|
1596
1601
|
const out = new Uint8Array(total);
|
|
1597
1602
|
let at = 0;
|
|
1598
1603
|
for (const c of chunks) {
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
MucJoinError,
|
|
4
4
|
MucOriginTracker,
|
|
5
|
+
ScramAuthError,
|
|
5
6
|
authenticatedMessageOrigin,
|
|
6
7
|
defaultTimeoutMs,
|
|
7
8
|
forwardedStanza,
|
|
8
9
|
joinRoom,
|
|
9
10
|
queryMam,
|
|
10
11
|
refuseSubscriptionRequests,
|
|
12
|
+
scramClient,
|
|
13
|
+
startScramClient,
|
|
11
14
|
unwrapPolicyDelivery,
|
|
12
15
|
waitForStanza
|
|
13
|
-
} from "./chunks/chunk-
|
|
14
|
-
import "./chunks/chunk-
|
|
16
|
+
} from "./chunks/chunk-FUNUGIGH.mjs";
|
|
17
|
+
import "./chunks/chunk-FFLYSYV6.mjs";
|
|
15
18
|
|
|
16
19
|
// packages/xmpp/src/index.ts
|
|
17
20
|
import { default as default2 } from "@xmpp/xml";
|
|
@@ -73,6 +76,7 @@ async function requestUploadSlot(entity, service, req, timeoutMs = defaultTimeou
|
|
|
73
76
|
export {
|
|
74
77
|
MucJoinError,
|
|
75
78
|
MucOriginTracker,
|
|
79
|
+
ScramAuthError,
|
|
76
80
|
announceAvailable,
|
|
77
81
|
authenticatedMessageOrigin,
|
|
78
82
|
forwardedStanza,
|
|
@@ -80,6 +84,8 @@ export {
|
|
|
80
84
|
queryMam,
|
|
81
85
|
refuseSubscriptionRequests,
|
|
82
86
|
requestUploadSlot,
|
|
87
|
+
scramClient,
|
|
88
|
+
startScramClient,
|
|
83
89
|
unwrapPolicyDelivery,
|
|
84
90
|
uploadServiceJid,
|
|
85
91
|
default2 as xml
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wacando-claude",
|
|
3
3
|
"displayName": "Wacando",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.0",
|
|
5
5
|
"description": "Connect agents with Wacando onboarding and view live Claude Code conversations locally.",
|
|
6
6
|
"author": { "name": "Wacando" },
|
|
7
7
|
"keywords": ["agents", "conversation", "local", "observability"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wacando-cursor",
|
|
3
3
|
"displayName": "Wacando",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.0",
|
|
5
5
|
"description": "Connect agents with Wacando onboarding and view conversations in the built-in Cursor browser.",
|
|
6
6
|
"author": { "name": "Wacando" },
|
|
7
7
|
"keywords": ["agents", "conversation", "local", "observability"],
|
package/provenance.json
CHANGED
|
@@ -240,11 +240,11 @@
|
|
|
240
240
|
"source": {
|
|
241
241
|
"repository": "lbframe/wacando",
|
|
242
242
|
"branch": "main",
|
|
243
|
-
"commit": "
|
|
243
|
+
"commit": "40dd0c30b3653095f08994f9534fd5110e1d0925",
|
|
244
244
|
"trees": {
|
|
245
|
-
"packages/contracts/src": "
|
|
246
|
-
"packages/xmpp/src": "
|
|
247
|
-
"packages/omemo/src": "
|
|
245
|
+
"packages/contracts/src": "bf79bedbbb5bd0dc5cbf7d3a027a30f76a3f6904",
|
|
246
|
+
"packages/xmpp/src": "d8dbc0ea4b286c26bfb3e7b347c3210b27365c9b",
|
|
247
|
+
"packages/omemo/src": "850b707c9705211a6136486c78c38882699e8b39"
|
|
248
248
|
},
|
|
249
249
|
"packages": [
|
|
250
250
|
{
|
|
@@ -273,6 +273,7 @@
|
|
|
273
273
|
},
|
|
274
274
|
"externals": {
|
|
275
275
|
"@signalapp/libsignal-client": "0.102.2",
|
|
276
|
+
"@xmpp/client": "0.14.0",
|
|
276
277
|
"@xmpp/jid": "0.14.0",
|
|
277
278
|
"@xmpp/xml": "0.14.0",
|
|
278
279
|
"base64-js": "1.5.1",
|
|
@@ -325,6 +326,7 @@
|
|
|
325
326
|
"packages/xmpp/src/presence.ts",
|
|
326
327
|
"packages/xmpp/src/pubsub.ts",
|
|
327
328
|
"packages/xmpp/src/roster.ts",
|
|
329
|
+
"packages/xmpp/src/scram.ts",
|
|
328
330
|
"packages/xmpp/src/single-session.ts",
|
|
329
331
|
"packages/xmpp/src/stanza.ts",
|
|
330
332
|
"packages/xmpp/src/timeouts.ts",
|
|
@@ -332,34 +334,34 @@
|
|
|
332
334
|
],
|
|
333
335
|
"files": [
|
|
334
336
|
{
|
|
335
|
-
"path": "packages/wacando/chunks/chunk-
|
|
336
|
-
"bytes": 14356,
|
|
337
|
-
"sha256": "c9571af753d39674c6c17ee01542605bf9ca5b59e17e98e1672deee8fa30b096"
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
"path": "packages/wacando/chunks/chunk-PHGOH4AU.mjs",
|
|
337
|
+
"path": "packages/wacando/chunks/chunk-F7J3K43U.mjs",
|
|
341
338
|
"bytes": 13681,
|
|
342
|
-
"sha256": "
|
|
339
|
+
"sha256": "5b2797e10cb4c5ca5c976f2f670c5c41b18b403d28dc9622a07617e71dc33e80"
|
|
343
340
|
},
|
|
344
341
|
{
|
|
345
|
-
"path": "packages/wacando/chunks/chunk-
|
|
342
|
+
"path": "packages/wacando/chunks/chunk-FFLYSYV6.mjs",
|
|
346
343
|
"bytes": 2164,
|
|
347
|
-
"sha256": "
|
|
344
|
+
"sha256": "d84c6ca7a529acc6be0da790124bb73a35fd6c7703beb370fb4e8003eb2ca0e0"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"path": "packages/wacando/chunks/chunk-FUNUGIGH.mjs",
|
|
348
|
+
"bytes": 26532,
|
|
349
|
+
"sha256": "b237936a6254b8263768aef19c80c427183ed3f607c9af9f33595477bc0bddc9"
|
|
348
350
|
},
|
|
349
351
|
{
|
|
350
352
|
"path": "packages/wacando/omemo-wire.mjs",
|
|
351
353
|
"bytes": 5683,
|
|
352
|
-
"sha256": "
|
|
354
|
+
"sha256": "bf44f885b9e3e65da624f9251ec1b2148e8e647a6398c22750003cda3c961e84"
|
|
353
355
|
},
|
|
354
356
|
{
|
|
355
357
|
"path": "packages/wacando/omemo.mjs",
|
|
356
|
-
"bytes":
|
|
357
|
-
"sha256": "
|
|
358
|
+
"bytes": 109777,
|
|
359
|
+
"sha256": "334fe2228f4bdc5c66e9968c707094b6bc76f5a60a8f9fa59dad4a8cb1788c49"
|
|
358
360
|
},
|
|
359
361
|
{
|
|
360
362
|
"path": "packages/wacando/xmpp.mjs",
|
|
361
|
-
"bytes":
|
|
362
|
-
"sha256": "
|
|
363
|
+
"bytes": 2834,
|
|
364
|
+
"sha256": "1d5536b43cf9d38414c1ccd4c73334a07db60b57c6b584f6a61916010a3453e1"
|
|
363
365
|
}
|
|
364
366
|
]
|
|
365
367
|
}
|