@wacando/agent 2.4.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.
- package/STATUS.md +32 -0
- package/bin/wacando-agent.mjs +3 -3
- package/docs/AGENT_TOOLS.md +33 -11
- package/docs/WACANDO_PACKAGES.md +11 -1
- package/package.json +2 -1
- package/packages/onboarding/version.mjs +1 -1
- package/packages/runtime/conversations.mjs +9 -4
- package/packages/runtime/files.mjs +154 -38
- package/packages/runtime/runtime.mjs +8 -5
- package/packages/runtime/session.mjs +6 -27
- package/packages/wacando/chunks/{chunk-FFLYSYV6.mjs → chunk-4Z6TL6JY.mjs} +1 -1
- package/packages/wacando/chunks/{chunk-FUNUGIGH.mjs → chunk-5Q4655F4.mjs} +27 -2
- package/packages/wacando/chunks/{chunk-F7J3K43U.mjs → chunk-L6IDYNL5.mjs} +2 -2
- package/packages/wacando/omemo-wire.mjs +3 -3
- package/packages/wacando/omemo.mjs +4 -4
- package/packages/wacando/xmpp.mjs +3 -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 +16 -14
package/STATUS.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Handoff — October 15, 2026
|
|
2
2
|
|
|
3
|
+
## Runtime 2.4.1 — October 17, 2026
|
|
4
|
+
|
|
5
|
+
File runtime follow-ups (lbframe/wacando-agent#24, review of #23):
|
|
6
|
+
- A ciphertext left without its record (crash between the two writes) is
|
|
7
|
+
removed by the cleanup once it is more than an hour old.
|
|
8
|
+
- The quarantine keeps at most 20 descriptors per conversation (100 in all),
|
|
9
|
+
and a full quarantine is cleaned up before a share is refused, so a peer's
|
|
10
|
+
forged descriptors cannot block real shares.
|
|
11
|
+
- A file descriptor is also detected behind a zero-width space, BOM or word
|
|
12
|
+
joiner, with fullwidth braces, and nested, inside an array or inside prose
|
|
13
|
+
together with its key; text that only names the format (also a nested
|
|
14
|
+
`{"wacandoFile": false}`) stays text. Detection is linear in the body size.
|
|
15
|
+
- `fileStatus` of an incoming file runs one step at a time with
|
|
16
|
+
`receiveFile`, so it never drops the record of a file being received.
|
|
17
|
+
- The cleanup no longer runs when a read-only tool (`wacando_read`,
|
|
18
|
+
`wacando_conversations`, ...) connects: it runs before the first file
|
|
19
|
+
operation of a connection, and at every connection only for
|
|
20
|
+
`wacando-agent daemon` (SDK `files.sweepOnConnect`). A record that changed
|
|
21
|
+
while the cleanup consulted the server is kept (closes the STOP race that
|
|
22
|
+
could unlist a received file), and two processes of one identity can clean
|
|
23
|
+
up at the same time.
|
|
24
|
+
See [agent tools](docs/AGENT_TOOLS.md#files-10); evidence
|
|
25
|
+
`docs/evidence/issue-24/`.
|
|
26
|
+
|
|
27
|
+
Embedded Wacando code rebuilt from lbframe/wacando main `87b51a9`, which
|
|
28
|
+
brings lbframe/wacando#200: the embedded `scramClient` now guards its own
|
|
29
|
+
`start()` (`guardStart`), so the runtime's local copy of that guard (added in
|
|
30
|
+
2.4.0) is removed; an error while the stream opens is still an ordinary,
|
|
31
|
+
retried connection failure. `@xmpp/events` 0.14.0 becomes an exact dependency
|
|
32
|
+
([embedded Wacando code](docs/WACANDO_PACKAGES.md)); evidence
|
|
33
|
+
`docs/evidence/release-2.4.1/`.
|
|
34
|
+
|
|
3
35
|
## Runtime 2.4.0 — October 17, 2026
|
|
4
36
|
|
|
5
37
|
Verified XMPP server (lbframe/wacando-agent#26): every XMPP connection of
|
package/bin/wacando-agent.mjs
CHANGED
|
@@ -70,10 +70,10 @@ export function parseFlags(flags, allowed) {
|
|
|
70
70
|
class UsageError extends Error {}
|
|
71
71
|
const print = value => console.log(JSON.stringify(value, null, 2));
|
|
72
72
|
|
|
73
|
-
async function runtime(options, report) {
|
|
73
|
+
async function runtime(options, report, extra = {}) {
|
|
74
74
|
const { AgentRuntime } = await import('../packages/runtime/runtime.mjs');
|
|
75
75
|
const { CredentialStore } = await import('../packages/onboarding/storage.mjs');
|
|
76
|
-
return new AgentRuntime({ host: options.host ?? 'cli', origin: options.origin, store: new CredentialStore(), ...(report ? { report } : {}) });
|
|
76
|
+
return new AgentRuntime({ host: options.host ?? 'cli', origin: options.origin, store: new CredentialStore(), ...(report ? { report } : {}), ...extra });
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
commands.set('status', async flags => {
|
|
@@ -198,7 +198,7 @@ commands.set('daemon', async flags => {
|
|
|
198
198
|
// Diagnostics carry only local enums and public ids (never content).
|
|
199
199
|
process.stderr.write(`${JSON.stringify({ component: 'wacando-connection', ...event })}\n`);
|
|
200
200
|
if (event.state === 'replaced' || event.state === 'refused') terminal(event.state);
|
|
201
|
-
});
|
|
201
|
+
}, { files: { sweepOnConnect: true } }); // the daemon cleans up ended file transfers at every connection (#24)
|
|
202
202
|
try {
|
|
203
203
|
try { print(await agent.connect(options.agent)); }
|
|
204
204
|
catch (error) {
|
package/docs/AGENT_TOOLS.md
CHANGED
|
@@ -188,16 +188,27 @@ Code: `packages/runtime/files.mjs`.
|
|
|
188
188
|
The URL in a message never chooses where the runtime connects nor where the
|
|
189
189
|
agent credential goes; the fetch URL is rebuilt from the pin. https only,
|
|
190
190
|
except loopback http for local stacks.
|
|
191
|
-
- **Receive**: a decrypted body that is a file descriptor is never
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
- **Receive**: a decrypted body that is or carries a file descriptor is never
|
|
192
|
+
shown as text (`fileBodyKind`, lbframe/wacando-agent#21, #24): a JSON object
|
|
193
|
+
with a `wacandoFile` key at its top level, whatever its whitespace, key order,
|
|
194
|
+
escaping or values; a body that opens like one but is not valid JSON; the
|
|
195
|
+
same behind invisible format characters (zero-width space, BOM, word joiner)
|
|
196
|
+
or written with fullwidth braces; and a descriptor anywhere else (nested in
|
|
197
|
+
JSON, inside an array, in prose, a code block or a JSON string) when its key
|
|
198
|
+
is there too (an `aesgcm://…#<88 hex>` URL, or a review `secret`). Text
|
|
199
|
+
that only names the format without a key (e.g. a nested
|
|
200
|
+
`{"wacandoFile": false}` setting) stays text. Detection is linear in the
|
|
201
|
+
body size. A review descriptor (`wacandoFileReview`) in the room is
|
|
195
202
|
`file: { state: 'refused', reason: 'review_descriptor' }`. It is parsed strictly against
|
|
196
203
|
the pin: valid -> `file: { fileId, name, mediaType, size, state:
|
|
197
204
|
'quarantined' }` in the transcript entry (and `wacando_wait`), otherwise
|
|
198
205
|
`file: { state: 'refused', reason: 'invalid_descriptor' }`, nothing stored or
|
|
199
|
-
fetched. At most 100 descriptors wait in quarantine on one runtime
|
|
200
|
-
|
|
206
|
+
fetched. At most 100 descriptors wait in quarantine on one runtime, and at
|
|
207
|
+
most 20 of one conversation (one peer cannot fill the quarantine for the
|
|
208
|
+
others). When either limit is reached the runtime first cleans up ended
|
|
209
|
+
transfers (a peer's forged descriptors, which the server never lists, go
|
|
210
|
+
then); if it is still full, the new share is `refused`
|
|
211
|
+
(`too_many_quarantined`) and never stored. `wacando_receive_file` checks that the server lists the file for
|
|
201
212
|
this agent with the same digest, size and type, waits (bounded) while it is
|
|
202
213
|
quarantined (`uploaded`), and only once the recipient owner released it
|
|
203
214
|
downloads (redirects refused, size capped), verifies and decrypts
|
|
@@ -214,17 +225,28 @@ Code: `packages/runtime/files.mjs`.
|
|
|
214
225
|
in a conversation that is not `active` (checked locally before each step and
|
|
215
226
|
by the server). Past `expiresAt` (30 days) the transfer is `expired`.
|
|
216
227
|
Owners keep observing exchanged files in the app until then.
|
|
217
|
-
- **Local cleanup** (lbframe/wacando-agent#21, `FileTransfers.sweep`):
|
|
218
|
-
connection
|
|
219
|
-
`wacando_files
|
|
220
|
-
|
|
228
|
+
- **Local cleanup** (lbframe/wacando-agent#21, #24, `FileTransfers.sweep`):
|
|
229
|
+
once per connection, before the first file operation (`wacando_send_file`,
|
|
230
|
+
`wacando_file_status`, `wacando_receive_file`, `wacando_files` and their CLI
|
|
231
|
+
and SDK forms; never from the read-only `wacando_contacts`,
|
|
232
|
+
`wacando_conversations`, `wacando_read`, `wacando_wait`); at every
|
|
233
|
+
connection and reconnection for `wacando-agent daemon` (SDK:
|
|
234
|
+
`files.sweepOnConnect: true`); before every `wacando_files`/`files` listing;
|
|
235
|
+
and before refusing a send or a received share for a full queue or
|
|
236
|
+
quarantine, the runtime deletes the **local ciphertext, keys and quarantined
|
|
221
237
|
descriptors** of every transfer that can never continue: conversation no
|
|
222
238
|
longer `active` (STOP) or unknown to the server, file unknown, different,
|
|
223
239
|
`rejected` or `expired` on the server, or past its expiry by the local clock
|
|
224
240
|
(the server's `expiresAt`, else 30 days after the local request or receipt;
|
|
225
241
|
this one also works offline). A server that cannot be reached removes
|
|
226
242
|
nothing else. Sent records (public metadata, no key) are kept until their
|
|
227
|
-
expiry. Leftovers of an interrupted write older than an hour are removed
|
|
243
|
+
expiry. Leftovers of an interrupted write older than an hour are removed,
|
|
244
|
+
including a ciphertext whose record was never written (a crash between the
|
|
245
|
+
two writes: without its record it has no key). A record that changed while
|
|
246
|
+
the sweep consulted the server is kept (a file received meanwhile stays
|
|
247
|
+
listed, also if STOP follows), and no transfer step of the same process runs
|
|
248
|
+
during a removal. Two processes of one identity sweeping at once (a daemon
|
|
249
|
+
and an MCP server) only ever remove ended transfers.
|
|
228
250
|
The listing reports `cleaned: { outgoing, incoming }` counts when it removed
|
|
229
251
|
something. Nothing is logged.
|
|
230
252
|
- **Received files are kept**: a file written to the quarantine directory is
|
package/docs/WACANDO_PACKAGES.md
CHANGED
|
@@ -42,7 +42,17 @@ 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.4.
|
|
45
|
+
Current source (2.4.1): lbframe/wacando main
|
|
46
|
+
`87b51a96aff0a13ca5ad6650c9c841bee72e1612` (#200 merged at `e669157`). It brings lbframe/wacando#200:
|
|
47
|
+
the embedded `scramClient` applies `guardStart`, so an error while the XMPP
|
|
48
|
+
stream opens rejects `start()` without leaving an unhandled rejection behind
|
|
49
|
+
(the runtime's own copy of that guard is removed); `@xmpp/events` is now a
|
|
50
|
+
recorded external of the embedded code and an exact dependency. The entry
|
|
51
|
+
list is unchanged: `guardStart` is applied inside `scramClient`, and the
|
|
52
|
+
runtime does not use `startOrStop` (`connectSession` already stops the
|
|
53
|
+
reconnect loop and the client on any failure, presence echo included).
|
|
54
|
+
|
|
55
|
+
2.4.0 embedded lbframe/wacando main
|
|
46
56
|
`40dd0c30b3653095f08994f9534fd5110e1d0925`. It brings lbframe/wacando#186:
|
|
47
57
|
`#wacando/xmpp` now also exports `scramClient`, `startScramClient` and
|
|
48
58
|
`ScramAuthError` (SCRAM client verifying the ServerSignature `v=`, SASL2
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wacando/agent",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
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",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@signalapp/libsignal-client": "0.102.2",
|
|
55
55
|
"@xmpp/client": "0.14.0",
|
|
56
|
+
"@xmpp/events": "0.14.0",
|
|
56
57
|
"@xmpp/jid": "0.14.0",
|
|
57
58
|
"@xmpp/xml": "0.14.0",
|
|
58
59
|
"base64-js": "1.5.1",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Must match all distributed plugin manifests; checked by the test suite.
|
|
2
|
-
export const pluginVersion = '2.4.
|
|
2
|
+
export const pluginVersion = '2.4.1';
|
|
@@ -104,7 +104,7 @@ export function publicConversation(view, extra = {}) {
|
|
|
104
104
|
|
|
105
105
|
/** Messaging for ONE connected identity. Created by AgentRuntime.conversations(). */
|
|
106
106
|
export class ConversationClient {
|
|
107
|
-
constructor({ runtime, agent, session, files }) {
|
|
107
|
+
constructor({ runtime, agent, session, files, sweepOnConnect = false }) {
|
|
108
108
|
this.runtime = runtime;
|
|
109
109
|
this.agent = agent;
|
|
110
110
|
this.session = session;
|
|
@@ -131,8 +131,11 @@ export class ConversationClient {
|
|
|
131
131
|
this.lifecyclesLoaded = this.loadLifecycles();
|
|
132
132
|
// lbframe/wacando-agent#10: encrypted files under owner approval.
|
|
133
133
|
this.files = files?.(this) ?? null;
|
|
134
|
-
// lbframe/wacando-agent#21: clean up ended transfers
|
|
135
|
-
|
|
134
|
+
// lbframe/wacando-agent#21, #24: clean up ended transfers once per
|
|
135
|
+
// connection, at the connection only for a daemon (sweepOnConnect), else
|
|
136
|
+
// before the first file operation: a read-only tool never deletes anything.
|
|
137
|
+
this.sweepOnConnect = sweepOnConnect;
|
|
138
|
+
this.filesSwept = this.sweepOnConnect ? this.sweepFiles() : null;
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
/** Content-free counts of the local file records removed, or null (never throws). */
|
|
@@ -191,7 +194,7 @@ export class ConversationClient {
|
|
|
191
194
|
this.session = session;
|
|
192
195
|
this.recovery = this.attachRecovery(session);
|
|
193
196
|
this.lifecyclesLoaded = this.loadLifecycles();
|
|
194
|
-
this.filesSwept = this.sweepFiles();
|
|
197
|
+
this.filesSwept = this.sweepOnConnect ? this.sweepFiles() : null;
|
|
195
198
|
this.tracker = new MucOriginTracker(session.client, { trustedRelayJid: this.policyJid });
|
|
196
199
|
this.rooms.clear();
|
|
197
200
|
for (const resolve of this.submissions.values()) resolve({ state: 'refused', reason: 'connection-lost' });
|
|
@@ -343,6 +346,8 @@ export class ConversationClient {
|
|
|
343
346
|
|
|
344
347
|
fileTransfers() {
|
|
345
348
|
if (!this.files) throw new SafeError('File transfer is not available on this runtime.');
|
|
349
|
+
// First file operation of this connection (none is read-only): clean up.
|
|
350
|
+
this.filesSwept ??= this.sweepFiles();
|
|
346
351
|
return this.files;
|
|
347
352
|
}
|
|
348
353
|
/** Encrypt a local file, ask the owner's approval, then deposit and share it (bounded wait). */
|
|
@@ -29,8 +29,10 @@ import { protectedDirectory, SafeError, storageError } from '../onboarding/stora
|
|
|
29
29
|
// directory (0700/0600). The runtime never opens, extracts or executes it.
|
|
30
30
|
// Nothing here logs; errors carry no name, digest, URL token or key.
|
|
31
31
|
//
|
|
32
|
-
// Local retention (lbframe/wacando-agent#21): sweep() runs
|
|
33
|
-
//
|
|
32
|
+
// Local retention (lbframe/wacando-agent#21, #24): sweep() runs before the
|
|
33
|
+
// first file operation of each connection (at every connection for the
|
|
34
|
+
// daemon, never from a read-only tool), before every listing and before a
|
|
35
|
+
// full queue or quarantine refuses; it deletes the local ciphertext, keys and
|
|
34
36
|
// quarantined descriptors of transfers that can never continue (STOP or any
|
|
35
37
|
// other closed conversation, rejected, expired or unknown on the server, or
|
|
36
38
|
// past their 30-day expiry by the local clock). A RECEIVED file, written to
|
|
@@ -40,11 +42,15 @@ import { protectedDirectory, SafeError, storageError } from '../onboarding/stora
|
|
|
40
42
|
export const FILE_POLL_MS = 2000;
|
|
41
43
|
export const MAX_PENDING_OUTGOING = 10;
|
|
42
44
|
export const MAX_QUARANTINED_INCOMING = 100;
|
|
45
|
+
// One conversation (one peer) can fill only part of the quarantine (lbframe/wacando-agent#24).
|
|
46
|
+
export const MAX_QUARANTINED_PER_CONVERSATION = 20;
|
|
43
47
|
export const FILE_RETENTION_MS = 30 * 24 * 3600 * 1000;
|
|
44
|
-
// Leftovers of an interrupted write (staged ciphertext, .part
|
|
48
|
+
// Leftovers of an interrupted write (staged ciphertext, .part, a ciphertext
|
|
49
|
+
// without its record) older than this are removed by the sweep.
|
|
45
50
|
const STALE_LEFTOVER_MS = 3600 * 1000;
|
|
46
51
|
const STAGED = /^staged-[0-9a-f-]{36}\.bin$/;
|
|
47
52
|
const PART = /^\.[0-9a-f-]{36}\.part$/;
|
|
53
|
+
const COMMITTED = /^(fil_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.bin$/;
|
|
48
54
|
const FILE_ID = /^fil_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
49
55
|
const UPLOAD_PATH = /^\/v1\/uploads\/(fil_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/[A-Za-z0-9_-]{43}$/;
|
|
50
56
|
const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
@@ -66,6 +72,7 @@ export const fileErrors = {
|
|
|
66
72
|
download: 'The encrypted file could not be downloaded from the configured Wacando upload origin. Retry later.',
|
|
67
73
|
plainText: 'A message body may not be a file share; use wacando_send_file.',
|
|
68
74
|
tooManyIncoming: `Too many received files are waiting in quarantine on this runtime (at most ${MAX_QUARANTINED_INCOMING}). This file share was refused and never stored; ask your owner to authorize or the sender to resend later.`,
|
|
75
|
+
tooManyInConversation: `Too many received files of this conversation are waiting in quarantine (at most ${MAX_QUARANTINED_PER_CONVERSATION} per conversation). This file share was refused and never stored; ask your owner to authorize or the sender to resend later.`,
|
|
69
76
|
orphan: 'The file could not be kept on this device after the request: nothing will be deposited. Your owner sees a pending request without a review copy and can refuse it; it expires after 30 days.',
|
|
70
77
|
noOwnerDevice: 'Your owner has no active Wacando app device to review this file: ask your owner to open the Wacando desktop app (signed in) to approve it, then call wacando_file_status. The file stays pending; nothing was deposited.',
|
|
71
78
|
reviewRetry: 'The review copy for your owner could not be deposited yet; call wacando_file_status to retry. The file stays pending.',
|
|
@@ -161,23 +168,67 @@ export function ownOwnerId(view, agentId) {
|
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
/**
|
|
164
|
-
* Pure: whether a message body is a Wacando file descriptor
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
171
|
+
* Pure: whether a message body is, or carries, a Wacando file descriptor:
|
|
172
|
+
* 'share' (file share, `wacandoFile`), 'review' (owner review descriptor,
|
|
173
|
+
* `wacandoFileReview`, never meant for a room) or null. Such a body is never
|
|
174
|
+
* shown nor sent as text: it carries a key (lbframe/wacando-agent#21, #24).
|
|
175
|
+
* Detected whatever the whitespace, key order or JSON escaping, behind
|
|
176
|
+
* invisible format characters (ZWSP, BOM, word joiner...) or compatibility
|
|
177
|
+
* forms (fullwidth braces):
|
|
178
|
+
* - a JSON object with such a key at its top level (strict: whatever its
|
|
179
|
+
* values), or a body that opens like one but is not valid JSON;
|
|
180
|
+
* - a descriptor anywhere else (nested in JSON, in an array, in prose, a code
|
|
181
|
+
* block or a JSON string), only when its key material is there too: an
|
|
182
|
+
* `aesgcm://...#<88 hex>` URL for a share, a `"secret"` of 88 hex for a
|
|
183
|
+
* review. Text that merely names the format (e.g. a nested
|
|
184
|
+
* `{"wacandoFile":false}` setting) carries no key and stays text.
|
|
185
|
+
* Linear in the body size (a hostile body cannot stall the event loop).
|
|
168
186
|
*/
|
|
169
187
|
export function fileBodyKind(text) {
|
|
170
188
|
if (typeof text !== 'string') return null;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
189
|
+
// Detection only: the body itself is never rewritten.
|
|
190
|
+
const plain = text.normalize('NFKC').replace(/\p{Cf}/gu, '').trim();
|
|
191
|
+
if (plain.startsWith('{') || plain.startsWith('[')) {
|
|
192
|
+
let value;
|
|
193
|
+
let parsed = true;
|
|
194
|
+
try { value = JSON.parse(plain); } catch { parsed = false; }
|
|
195
|
+
if (parsed) {
|
|
196
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
197
|
+
if (Object.hasOwn(value, 'wacandoFileReview')) return 'review';
|
|
198
|
+
if (Object.hasOwn(value, 'wacandoFile')) return 'share';
|
|
199
|
+
}
|
|
200
|
+
} else if (/^[[\s]*\{\s*"wacandoFileReview"\s*:/.test(plain)) return 'review';
|
|
201
|
+
else if (/^[[\s]*\{\s*"wacandoFile"\s*:/.test(plain)) return 'share';
|
|
202
|
+
}
|
|
203
|
+
let unescaped = plain;
|
|
204
|
+
for (let i = 0; i < 3; i++) {
|
|
205
|
+
const next = unescaped.replace(/\\u([0-9a-fA-F]{4})/g, (_, code) => String.fromCharCode(Number.parseInt(code, 16))).replace(/\\(["/\\])/g, '$1');
|
|
206
|
+
if (next === unescaped) break;
|
|
207
|
+
unescaped = next;
|
|
208
|
+
}
|
|
209
|
+
if (/"wacandoFileReview"\s*:/.test(unescaped) && /"secret"\s*:\s*"[0-9a-fA-F]{88}"/.test(unescaped)) return 'review';
|
|
210
|
+
if (/"wacandoFile"\s*:/.test(unescaped) && hasAesgcmKey(unescaped)) return 'share';
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const URL_END = /[\s"#]/;
|
|
215
|
+
const KEY_HEX = /^[0-9a-fA-F]{88}/;
|
|
216
|
+
/**
|
|
217
|
+
* Pure: whether the text holds an `aesgcm://<no space, quote or #>#<88 hex>`
|
|
218
|
+
* URL, in one linear pass (the equivalent regular expression is quadratic on
|
|
219
|
+
* `aesgcm://` repeated without a `#`, #24 review LOW-2): every later
|
|
220
|
+
* `aesgcm://` inside the same URL body ends at the same character.
|
|
221
|
+
*/
|
|
222
|
+
function hasAesgcmKey(text) {
|
|
223
|
+
let from = 0;
|
|
224
|
+
for (;;) {
|
|
225
|
+
const start = text.indexOf('aesgcm://', from);
|
|
226
|
+
if (start < 0) return false;
|
|
227
|
+
let end = start + 'aesgcm://'.length;
|
|
228
|
+
while (end < text.length && !URL_END.test(text[end])) end++;
|
|
229
|
+
if (text[end] === '#' && KEY_HEX.test(text.slice(end + 1, end + 89))) return true;
|
|
230
|
+
from = end;
|
|
231
|
+
}
|
|
181
232
|
}
|
|
182
233
|
|
|
183
234
|
const BARE_JID = /^[^\s/@]+@[^\s/@]+$/;
|
|
@@ -269,15 +320,24 @@ export class FileVault {
|
|
|
269
320
|
try { await rename(join(dir, staged), join(dir, `${FileVault.id(fileId)}.bin`)); } catch { throw storageError(); }
|
|
270
321
|
}
|
|
271
322
|
async dropStaged(staged) { if (STAGED.test(staged ?? '')) await unlink(join(await this.dir('outgoing'), staged)).catch(() => {}); }
|
|
272
|
-
/**
|
|
323
|
+
/**
|
|
324
|
+
* Remove interrupted-write leftovers older than `before` (ms): staged
|
|
325
|
+
* ciphertext, .part files, and a committed ciphertext whose record was never
|
|
326
|
+
* written (a crash between the rename and the record, lbframe/wacando-agent#24;
|
|
327
|
+
* without its record it has no key and can never be used). Returns the count.
|
|
328
|
+
*/
|
|
273
329
|
async dropLeftovers(before) {
|
|
274
330
|
let count = 0;
|
|
275
331
|
for (const kind of ['outgoing', 'incoming']) {
|
|
276
332
|
const dir = await this.dir(kind);
|
|
277
333
|
for (const name of await readdir(dir)) {
|
|
278
|
-
|
|
334
|
+
const orphan = kind === 'outgoing' ? COMMITTED.exec(name)?.[1] : null;
|
|
335
|
+
if (!STAGED.test(name) && !PART.test(name) && !orphan) continue;
|
|
279
336
|
const info = await stat(join(dir, name)).catch(() => null);
|
|
280
|
-
if (info?.isFile()
|
|
337
|
+
if (!info?.isFile() || info.mtimeMs >= before) continue;
|
|
338
|
+
// Its record may have been written since the directory was read.
|
|
339
|
+
if (orphan && await lstat(join(dir, `${orphan}.json`)).then(() => true, error => error.code !== 'ENOENT')) continue;
|
|
340
|
+
if (await unlink(join(dir, name)).then(() => true, () => false)) count++;
|
|
281
341
|
}
|
|
282
342
|
}
|
|
283
343
|
return count;
|
|
@@ -326,6 +386,7 @@ export class FileTransfers {
|
|
|
326
386
|
this.pollMs = pollMs;
|
|
327
387
|
this.fetch = fetch;
|
|
328
388
|
this.busy = new Map(); // fileId -> in-flight advance (one at a time per file)
|
|
389
|
+
this.claims = new WeakSet(); // busy entries that are a sweep removal
|
|
329
390
|
}
|
|
330
391
|
|
|
331
392
|
get pin() { return this.pinSetting ?? (this.pinSetting = uploadPin(this.c.agent.origin)); }
|
|
@@ -489,15 +550,24 @@ export class FileTransfers {
|
|
|
489
550
|
async status(fileId, { timeoutMs = 0 } = {}) {
|
|
490
551
|
FileVault.id(fileId);
|
|
491
552
|
if (await this.vault.read('outgoing', fileId)) return this.once(fileId, () => this.advance(fileId, timeoutMs));
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return this.
|
|
553
|
+
// Incoming: one step at a time per file, like receive (a record written by
|
|
554
|
+
// a receive in flight is never dropped by this step, #24 review LOW-1).
|
|
555
|
+
return this.once(fileId, async () => {
|
|
556
|
+
const incoming = await this.vault.read('incoming', fileId);
|
|
557
|
+
if (!incoming) throw new SafeError(fileErrors.unknown);
|
|
558
|
+
return this.incomingState(incoming, { timeoutMs, fetch: false });
|
|
559
|
+
});
|
|
495
560
|
}
|
|
496
561
|
|
|
562
|
+
/**
|
|
563
|
+
* One transfer step at a time per file in this process: a caller joins the
|
|
564
|
+
* step in flight; a removal by the sweep (a claim) is waited for first.
|
|
565
|
+
*/
|
|
497
566
|
once(fileId, run) {
|
|
498
567
|
const running = this.busy.get(fileId);
|
|
499
|
-
if (running) return running;
|
|
500
|
-
const task =
|
|
568
|
+
if (running && !this.claims.has(running)) return running;
|
|
569
|
+
const task = (running ? running.then(() => run(), () => run()) : run())
|
|
570
|
+
.finally(() => { if (this.busy.get(fileId) === task) this.busy.delete(fileId); });
|
|
501
571
|
this.busy.set(fileId, task);
|
|
502
572
|
return task;
|
|
503
573
|
}
|
|
@@ -617,19 +687,36 @@ export class FileTransfers {
|
|
|
617
687
|
}
|
|
618
688
|
return { ...publicIncoming(known), state: known.path ? 'received' : 'quarantined' };
|
|
619
689
|
}
|
|
620
|
-
|
|
621
|
-
|
|
690
|
+
// Bounded quarantine, and one conversation (one peer) fills at most part of
|
|
691
|
+
// it. Ended transfers do not count: clean them up before refusing (#24).
|
|
692
|
+
let full = await this.quarantineFull(conversationId);
|
|
693
|
+
if (full) {
|
|
694
|
+
await this.sweep().catch(() => null);
|
|
695
|
+
full = await this.quarantineFull(conversationId);
|
|
622
696
|
}
|
|
697
|
+
if (full) return { state: 'refused', reason: 'too_many_quarantined', note: full };
|
|
623
698
|
const record = { fileId, conversationId, messageId, share, ciphertextSha256: share.ciphertextSha256, receivedAt: new Date(this.now()).toISOString() };
|
|
624
699
|
await this.vault.write('incoming', fileId, record);
|
|
625
700
|
return { ...publicIncoming(record), state: 'quarantined', note: `In quarantine until your owner authorizes it; then call wacando_receive_file. ${UNTRUSTED}` };
|
|
626
701
|
}
|
|
627
702
|
|
|
703
|
+
/** The refusal note when the quarantine (or this conversation's share of it) is full, else null. */
|
|
704
|
+
async quarantineFull(conversationId) {
|
|
705
|
+
const waiting = (await this.vault.list('incoming')).filter(r => !r.path);
|
|
706
|
+
if (waiting.length >= MAX_QUARANTINED_INCOMING) return fileErrors.tooManyIncoming;
|
|
707
|
+
if (waiting.filter(r => r.conversationId === conversationId).length >= MAX_QUARANTINED_PER_CONVERSATION) return fileErrors.tooManyInConversation;
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
|
|
628
711
|
/** Fetch a released incoming file into the quarantine directory (bounded wait for the release). */
|
|
629
712
|
async receive(fileId, { timeoutMs = 0 } = {}) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
return this.once(fileId, () =>
|
|
713
|
+
FileVault.id(fileId);
|
|
714
|
+
// Read inside the step: a removal by the sweep is never overtaken.
|
|
715
|
+
return this.once(fileId, async () => {
|
|
716
|
+
const record = await this.vault.read('incoming', fileId);
|
|
717
|
+
if (!record) throw new SafeError(fileErrors.unknown);
|
|
718
|
+
return this.incomingState(record, { timeoutMs, fetch: true });
|
|
719
|
+
});
|
|
633
720
|
}
|
|
634
721
|
|
|
635
722
|
async incomingState(record, { timeoutMs, fetch }) {
|
|
@@ -640,17 +727,17 @@ export class FileTransfers {
|
|
|
640
727
|
const view = await this.c.conversation(record.conversationId);
|
|
641
728
|
if (view.state !== 'active') {
|
|
642
729
|
// STOP is terminal: this agent can never fetch it; drop the key.
|
|
643
|
-
await this.
|
|
730
|
+
await this.dropIncoming(record);
|
|
644
731
|
return { file: summary, state: 'refused', reason: 'conversation_closed', note: fileErrors.notActive(view.state) };
|
|
645
732
|
}
|
|
646
733
|
const dto = (await this.serverFiles(record.conversationId)).find(f => f.id === record.fileId);
|
|
647
734
|
if (!dto || dto.recipientAgentId !== this.c.agent.agentId || dto.ciphertextSha256 !== record.share.ciphertextSha256 ||
|
|
648
735
|
dto.sizeBytes !== record.share.ciphertextSize || dto.mediaType !== record.share.mediaType) {
|
|
649
|
-
await this.
|
|
736
|
+
await this.dropIncoming(record);
|
|
650
737
|
return { file: summary, state: 'refused', reason: 'not_approved_file', note: 'The server does not list this file as approved for this agent. It was refused and never fetched.' };
|
|
651
738
|
}
|
|
652
739
|
if (this.expired(dto)) {
|
|
653
|
-
await this.
|
|
740
|
+
await this.dropIncoming(record);
|
|
654
741
|
return { file: summary, state: 'expired', note: fileApiMessages.FILE_EXPIRED };
|
|
655
742
|
}
|
|
656
743
|
if (dto.status === 'released' && fetch) return this.fetchReleased(record, dto, summary);
|
|
@@ -663,6 +750,12 @@ export class FileTransfers {
|
|
|
663
750
|
}
|
|
664
751
|
}
|
|
665
752
|
|
|
753
|
+
/** Drop a quarantined descriptor (its key), never the record of a file received meanwhile. */
|
|
754
|
+
async dropIncoming(record) {
|
|
755
|
+
if ((await this.vault.read('incoming', record.fileId))?.path) return;
|
|
756
|
+
await this.vault.remove('incoming', record.fileId);
|
|
757
|
+
}
|
|
758
|
+
|
|
666
759
|
async fetchReleased(record, dto, summary) {
|
|
667
760
|
let file;
|
|
668
761
|
try {
|
|
@@ -690,16 +783,22 @@ export class FileTransfers {
|
|
|
690
783
|
|
|
691
784
|
/**
|
|
692
785
|
* Delete the local secrets of every transfer that can never continue
|
|
693
|
-
* (lbframe/wacando-agent#21),
|
|
786
|
+
* (lbframe/wacando-agent#21, #24), before the first file operation of a
|
|
787
|
+
* connection (at every connection for the daemon), before each listing and
|
|
788
|
+
* before a full queue or quarantine refuses:
|
|
694
789
|
* - pending/deposited sends (ciphertext + key) and quarantined descriptors
|
|
695
790
|
* (key): conversation no longer active (STOP is terminal) or unknown to
|
|
696
791
|
* the server, file unknown, different, rejected or expired on the server,
|
|
697
792
|
* or past its 30-day expiry by the local clock (even offline);
|
|
698
793
|
* - sent records (public metadata only, no key): past their expiry;
|
|
699
|
-
* - interrupted-write leftovers older than an hour
|
|
794
|
+
* - interrupted-write leftovers older than an hour, including a ciphertext
|
|
795
|
+
* whose record was never written.
|
|
700
796
|
* A received file (written to the quarantine directory) and its record are
|
|
701
797
|
* never touched. A server that cannot be reached removes nothing it cannot
|
|
702
|
-
* decide locally. Transfers in progress in this process are skipped
|
|
798
|
+
* decide locally. Transfers in progress in this process are skipped; a
|
|
799
|
+
* record that changed since it was listed (e.g. a file received meanwhile)
|
|
800
|
+
* is kept. Another process of the same identity may sweep at the same time:
|
|
801
|
+
* every step tolerates a file the other one already removed.
|
|
703
802
|
* Returns content-free counts.
|
|
704
803
|
*/
|
|
705
804
|
sweep() {
|
|
@@ -721,14 +820,31 @@ export class FileTransfers {
|
|
|
721
820
|
if (this.busy.has(record.fileId)) continue;
|
|
722
821
|
if (!(await this.ended(direction, record, now, server))) continue;
|
|
723
822
|
if (this.busy.has(record.fileId)) continue;
|
|
724
|
-
await this.
|
|
725
|
-
counts[direction]++;
|
|
823
|
+
if (await this.claim(direction, record)) counts[direction]++;
|
|
726
824
|
}
|
|
727
825
|
}
|
|
728
826
|
await this.vault.dropLeftovers(now - STALE_LEFTOVER_MS).catch(() => 0);
|
|
729
827
|
return counts;
|
|
730
828
|
}
|
|
731
829
|
|
|
830
|
+
/**
|
|
831
|
+
* Remove an ended record unless it changed since it was listed. Set as the
|
|
832
|
+
* file's busy step in the same tick as the caller's last busy check, so no
|
|
833
|
+
* step of this process (receive, status) interleaves with the removal.
|
|
834
|
+
*/
|
|
835
|
+
claim(direction, record) {
|
|
836
|
+
const removal = (async () => {
|
|
837
|
+
const fresh = await this.vault.read(direction, record.fileId);
|
|
838
|
+
if (!fresh || JSON.stringify(fresh) !== JSON.stringify(record)) return false;
|
|
839
|
+
await this.vault.remove(direction, record.fileId);
|
|
840
|
+
return true;
|
|
841
|
+
})();
|
|
842
|
+
const held = removal.finally(() => { if (this.busy.get(record.fileId) === held) this.busy.delete(record.fileId); });
|
|
843
|
+
this.claims.add(held);
|
|
844
|
+
this.busy.set(record.fileId, held);
|
|
845
|
+
return held;
|
|
846
|
+
}
|
|
847
|
+
|
|
732
848
|
/** Conversation state and files as the server lists them for this agent; 'gone' when unknown; null when unreachable. */
|
|
733
849
|
async serverState(conversationId) {
|
|
734
850
|
let state;
|
|
@@ -30,9 +30,12 @@ export function publicIdentity(agent, state) {
|
|
|
30
30
|
*/
|
|
31
31
|
export class AgentRuntime {
|
|
32
32
|
/**
|
|
33
|
-
* `files`: `{ uploadOrigin, quarantineDir }` (defaults:
|
|
34
|
-
* or the API origin; WACANDO_QUARANTINE_DIR or a
|
|
35
|
-
* protected runtime state
|
|
33
|
+
* `files`: `{ uploadOrigin, quarantineDir, sweepOnConnect }` (defaults:
|
|
34
|
+
* WACANDO_UPLOAD_ORIGIN or the API origin; WACANDO_QUARANTINE_DIR or a
|
|
35
|
+
* directory inside the protected runtime state; `sweepOnConnect: true`
|
|
36
|
+
* cleans up ended transfers at every connection, as the daemon does,
|
|
37
|
+
* otherwise before the first file operation of a connection) plus test
|
|
38
|
+
* hooks (`now`, `sleep`, `pollMs`, `fetch`).
|
|
36
39
|
*/
|
|
37
40
|
constructor({ host = 'cli', origin, store = new CredentialStore(), report, request, connect, onStanza, files = {} } = {}) {
|
|
38
41
|
if (!/^[a-z0-9][a-z0-9_-]{0,31}$/.test(host)) throw new SafeError('Invalid runtime host name.');
|
|
@@ -147,7 +150,7 @@ export class AgentRuntime {
|
|
|
147
150
|
if (client && client.session !== session) this.rebind(agent.agentId, session);
|
|
148
151
|
if (!client) {
|
|
149
152
|
const files = await this.fileTransfersFor(agent);
|
|
150
|
-
client = new ConversationClient({ runtime: this, agent, session, files });
|
|
153
|
+
client = new ConversationClient({ runtime: this, agent, session, files, sweepOnConnect: this.fileOptions.sweepOnConnect === true });
|
|
151
154
|
this.clients.set(agent.agentId, client);
|
|
152
155
|
for (const stanza of this.buffered.get(agent.agentId) ?? []) client.handle(stanza);
|
|
153
156
|
this.buffered.delete(agent.agentId);
|
|
@@ -220,7 +223,7 @@ export class AgentRuntime {
|
|
|
220
223
|
|
|
221
224
|
/** File transfer state of one identity: private directories, pinned upload origin. */
|
|
222
225
|
async fileTransfersFor(agent) {
|
|
223
|
-
const { uploadOrigin, quarantineDir = process.env.WACANDO_QUARANTINE_DIR, ...hooks } = this.fileOptions;
|
|
226
|
+
const { uploadOrigin, quarantineDir = process.env.WACANDO_QUARANTINE_DIR, sweepOnConnect: _sweep, ...hooks } = this.fileOptions;
|
|
224
227
|
const pin = uploadPin(agent.origin, uploadOrigin ?? process.env.WACANDO_UPLOAD_ORIGIN);
|
|
225
228
|
const base = join(await this.store.directory(agent.origin, this.host), `files-${digest(agent.agentId)}`);
|
|
226
229
|
const vault = new FileVault(base, quarantineDir ? join(quarantineDir, digest(`${agent.origin}\n${agent.agentId}`).slice(0, 32)) : undefined);
|
|
@@ -3,32 +3,6 @@ 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
|
-
|
|
32
6
|
const unavailable = () => new SafeError('Secure messaging is unavailable. Retry the saved identity.');
|
|
33
7
|
|
|
34
8
|
/** SafeError for a ScramAuthError: the fixed message plus its local reason enum (no secret). */
|
|
@@ -47,13 +21,18 @@ const serverUnverified = error => Object.assign(new SafeError(XMPP_SERVER_UNVERI
|
|
|
47
21
|
* SASL1 only, and authentication succeeds only once the server's
|
|
48
22
|
* ServerSignature (`v=`) was verified. A refusal closes the connection,
|
|
49
23
|
* stops @xmpp/client's reconnect and rejects with `code: 'SCRAM_REFUSED'`.
|
|
24
|
+
* Any failure (start, presence echo or deadline) stops the reconnect loop and
|
|
25
|
+
* the client before rejecting: the same guarantee as the embedded
|
|
26
|
+
* `startOrStop`, extended to the presence echo, so that helper is not used.
|
|
50
27
|
*/
|
|
51
28
|
export async function connectSession({ jid, password, service, stateBaseDir, onStanza, onReplaced, timeoutMs = 15000 }) {
|
|
52
29
|
const provider = await loadDevice(jid, stateBaseDir);
|
|
53
30
|
const at = jid.indexOf('@');
|
|
54
31
|
const domain = jid.slice(at + 1);
|
|
32
|
+
// scramClient applies the embedded guardStart (lbframe/wacando#200): an
|
|
33
|
+
// 'error' while the stream opens rejects start() (an ordinary, retried
|
|
34
|
+
// failure here), never leaves an unhandled rejection behind.
|
|
55
35
|
const xmpp = scramClient({ service, domain, username: jid.slice(0, at), password });
|
|
56
|
-
observeOnlinePromise(xmpp);
|
|
57
36
|
// One server-enforced session per agent (lbframe/wacando #15): a newer login
|
|
58
37
|
// ends this one with <conflict/>. Remember it so nothing reconnects in a loop.
|
|
59
38
|
let replaced = false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 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,11 +1,35 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
XMPP_DOMAIN
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4Z6TL6JY.mjs";
|
|
5
5
|
|
|
6
6
|
// packages/xmpp/src/scram.ts
|
|
7
7
|
import { client } from "@xmpp/client";
|
|
8
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
|
|
9
33
|
var SASL1_NS = "urn:ietf:params:xml:ns:xmpp-sasl";
|
|
10
34
|
var SASL2_NS = "urn:xmpp:sasl:2";
|
|
11
35
|
var SCRAM_MIN_ITERATIONS = 4096;
|
|
@@ -337,6 +361,7 @@ function scramClient(options) {
|
|
|
337
361
|
refused = new ScramAuthError(reason);
|
|
338
362
|
void cut(xmpp, refused);
|
|
339
363
|
});
|
|
364
|
+
guardStart(xmpp);
|
|
340
365
|
states.set(xmpp, state);
|
|
341
366
|
return xmpp;
|
|
342
367
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
DomainError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4Z6TL6JY.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@87b51a96aff0a13ca5ad6650c9c841bee72e1612 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-L6IDYNL5.mjs";
|
|
33
|
+
import "./chunks/chunk-4Z6TL6JY.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@87b51a96aff0a13ca5ad6650c9c841bee72e1612 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-5Q4655F4.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-L6IDYNL5.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-4Z6TL6JY.mjs";
|
|
47
47
|
|
|
48
48
|
// packages/omemo/src/libsignal.ts
|
|
49
49
|
import { jid as parseJid2 } from "@xmpp/jid";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Embedded from lbframe/wacando@
|
|
1
|
+
// Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
2
|
import {
|
|
3
3
|
MucJoinError,
|
|
4
4
|
MucOriginTracker,
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
startScramClient,
|
|
14
14
|
unwrapPolicyDelivery,
|
|
15
15
|
waitForStanza
|
|
16
|
-
} from "./chunks/chunk-
|
|
17
|
-
import "./chunks/chunk-
|
|
16
|
+
} from "./chunks/chunk-5Q4655F4.mjs";
|
|
17
|
+
import "./chunks/chunk-4Z6TL6JY.mjs";
|
|
18
18
|
|
|
19
19
|
// packages/xmpp/src/index.ts
|
|
20
20
|
import { default as default2 } from "@xmpp/xml";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wacando-claude",
|
|
3
3
|
"displayName": "Wacando",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.1",
|
|
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.
|
|
4
|
+
"version": "2.4.1",
|
|
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,10 +240,10 @@
|
|
|
240
240
|
"source": {
|
|
241
241
|
"repository": "lbframe/wacando",
|
|
242
242
|
"branch": "main",
|
|
243
|
-
"commit": "
|
|
243
|
+
"commit": "87b51a96aff0a13ca5ad6650c9c841bee72e1612",
|
|
244
244
|
"trees": {
|
|
245
245
|
"packages/contracts/src": "bf79bedbbb5bd0dc5cbf7d3a027a30f76a3f6904",
|
|
246
|
-
"packages/xmpp/src": "
|
|
246
|
+
"packages/xmpp/src": "06bc1e27b585ce669cf12c10b01b7771d44b91f4",
|
|
247
247
|
"packages/omemo/src": "850b707c9705211a6136486c78c38882699e8b39"
|
|
248
248
|
},
|
|
249
249
|
"packages": [
|
|
@@ -274,6 +274,7 @@
|
|
|
274
274
|
"externals": {
|
|
275
275
|
"@signalapp/libsignal-client": "0.102.2",
|
|
276
276
|
"@xmpp/client": "0.14.0",
|
|
277
|
+
"@xmpp/events": "0.14.0",
|
|
277
278
|
"@xmpp/jid": "0.14.0",
|
|
278
279
|
"@xmpp/xml": "0.14.0",
|
|
279
280
|
"base64-js": "1.5.1",
|
|
@@ -329,39 +330,40 @@
|
|
|
329
330
|
"packages/xmpp/src/scram.ts",
|
|
330
331
|
"packages/xmpp/src/single-session.ts",
|
|
331
332
|
"packages/xmpp/src/stanza.ts",
|
|
333
|
+
"packages/xmpp/src/start.ts",
|
|
332
334
|
"packages/xmpp/src/timeouts.ts",
|
|
333
335
|
"packages/xmpp/src/upload.ts"
|
|
334
336
|
],
|
|
335
337
|
"files": [
|
|
336
338
|
{
|
|
337
|
-
"path": "packages/wacando/chunks/chunk-
|
|
338
|
-
"bytes":
|
|
339
|
-
"sha256": "
|
|
339
|
+
"path": "packages/wacando/chunks/chunk-4Z6TL6JY.mjs",
|
|
340
|
+
"bytes": 2164,
|
|
341
|
+
"sha256": "510beba0db3b2bf41032a4bd2abd003662504f03721f69cdca8ca62da7e2491c"
|
|
340
342
|
},
|
|
341
343
|
{
|
|
342
|
-
"path": "packages/wacando/chunks/chunk-
|
|
343
|
-
"bytes":
|
|
344
|
-
"sha256": "
|
|
344
|
+
"path": "packages/wacando/chunks/chunk-5Q4655F4.mjs",
|
|
345
|
+
"bytes": 27209,
|
|
346
|
+
"sha256": "05463ed7058575f8d167c157ac88c8ce44d77f1296235afd6bc96ef72e82501b"
|
|
345
347
|
},
|
|
346
348
|
{
|
|
347
|
-
"path": "packages/wacando/chunks/chunk-
|
|
348
|
-
"bytes":
|
|
349
|
-
"sha256": "
|
|
349
|
+
"path": "packages/wacando/chunks/chunk-L6IDYNL5.mjs",
|
|
350
|
+
"bytes": 13681,
|
|
351
|
+
"sha256": "cda8c73a2d729ba969abea7bd5a460f142c4df3b30dc84cfc4ee4092d2c448a4"
|
|
350
352
|
},
|
|
351
353
|
{
|
|
352
354
|
"path": "packages/wacando/omemo-wire.mjs",
|
|
353
355
|
"bytes": 5683,
|
|
354
|
-
"sha256": "
|
|
356
|
+
"sha256": "3cfcbc94e80e57856db5a9f9ded5f441a333cf1849481273f6f23d62ed4a4414"
|
|
355
357
|
},
|
|
356
358
|
{
|
|
357
359
|
"path": "packages/wacando/omemo.mjs",
|
|
358
360
|
"bytes": 109777,
|
|
359
|
-
"sha256": "
|
|
361
|
+
"sha256": "b06b6771e7f1796150611ee2da60887f682ccba26262107e1a869732e57fc5f6"
|
|
360
362
|
},
|
|
361
363
|
{
|
|
362
364
|
"path": "packages/wacando/xmpp.mjs",
|
|
363
365
|
"bytes": 2834,
|
|
364
|
-
"sha256": "
|
|
366
|
+
"sha256": "e95455a1bc5d8d5c8773129c45fdd2ab19f2282b16acf446f981ccd2051c220c"
|
|
365
367
|
}
|
|
366
368
|
]
|
|
367
369
|
}
|