@wacando/agent 2.1.0 → 2.3.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.
@@ -1,9 +1,9 @@
1
- // Embedded from lbframe/wacando@9f82881d4ee6cfd655727a08883877230917c955 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
1
+ // Embedded from lbframe/wacando@618922ecaf2213da18949a8b27d343c8767b64c5 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-GASYW3SG.mjs";
6
+ } from "./chunks/chunk-53E7SSTF.mjs";
7
7
  import {
8
8
  BODY_MARKER,
9
9
  GCM_IV_BYTES,
@@ -37,8 +37,13 @@ import {
37
37
  signerJid,
38
38
  signerRole,
39
39
  utf8Encode
40
- } from "./chunks/chunk-OI3RF7TS.mjs";
41
- import "./chunks/chunk-EIGFLZ2Y.mjs";
40
+ } from "./chunks/chunk-PHGOH4AU.mjs";
41
+ import {
42
+ ALLOWED_MEDIA_TYPES,
43
+ MAX_FILE_BYTES,
44
+ MIN_FILE_BYTES,
45
+ looksLikePlaintext
46
+ } from "./chunks/chunk-PZDQXGA6.mjs";
42
47
 
43
48
  // packages/omemo/src/libsignal.ts
44
49
  import { jid as parseJid2 } from "@xmpp/jid";
@@ -1076,6 +1081,527 @@ var OmemoTranscriptDecryptor = class {
1076
1081
  }
1077
1082
  };
1078
1083
 
1084
+ // packages/omemo/src/file-core.ts
1085
+ var FILE_KEY_BYTES = 32;
1086
+ var FILE_IV_BYTES = 12;
1087
+ var FILE_TAG_BYTES = MIN_FILE_BYTES;
1088
+ var MAX_FILE_CIPHERTEXT_BYTES = MAX_FILE_BYTES;
1089
+ var MAX_FILE_PLAINTEXT_BYTES = MAX_FILE_CIPHERTEXT_BYTES - FILE_TAG_BYTES;
1090
+ var PLAINTEXT_GUARD_ATTEMPTS = 4;
1091
+ var FileCryptoError = class extends OmemoError {
1092
+ constructor(reason, message) {
1093
+ super(message);
1094
+ this.reason = reason;
1095
+ this.name = "FileCryptoError";
1096
+ }
1097
+ reason;
1098
+ };
1099
+ function subtle() {
1100
+ const s = globalThis.crypto?.subtle;
1101
+ if (!s) throw new FileCryptoError("unavailable", "WebCrypto is not available");
1102
+ return s;
1103
+ }
1104
+ function own(bytes) {
1105
+ return new Uint8Array(bytes);
1106
+ }
1107
+ var HEX = /^[0-9a-f]*$/;
1108
+ function toHex(bytes) {
1109
+ let out = "";
1110
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
1111
+ return out;
1112
+ }
1113
+ function fromHex(hex) {
1114
+ const h = hex.toLowerCase();
1115
+ if (h.length % 2 !== 0 || !HEX.test(h))
1116
+ throw new FileCryptoError("invalid_descriptor", "invalid hex");
1117
+ const out = new Uint8Array(h.length / 2);
1118
+ for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(h.slice(i * 2, i * 2 + 2), 16);
1119
+ return out;
1120
+ }
1121
+ async function fileSha256Hex(bytes) {
1122
+ return toHex(new Uint8Array(await subtle().digest("SHA-256", own(bytes))));
1123
+ }
1124
+ function checkKeyIv(key2, iv) {
1125
+ if (key2.length !== FILE_KEY_BYTES)
1126
+ throw new FileCryptoError("invalid_descriptor", "file key must be 32 bytes");
1127
+ if (iv.length !== FILE_IV_BYTES)
1128
+ throw new FileCryptoError("invalid_descriptor", "file IV must be 12 bytes");
1129
+ }
1130
+ function importKey(key2, usage) {
1131
+ return subtle().importKey("raw", own(key2), { name: "AES-GCM" }, false, [usage]);
1132
+ }
1133
+ async function aesGcmSeal(key2, iv, plaintext) {
1134
+ checkKeyIv(key2, iv);
1135
+ const out = await subtle().encrypt(
1136
+ { name: "AES-GCM", iv: own(iv), tagLength: FILE_TAG_BYTES * 8 },
1137
+ await importKey(key2, "encrypt"),
1138
+ own(plaintext)
1139
+ );
1140
+ return new Uint8Array(out);
1141
+ }
1142
+ async function aesGcmOpen(key2, iv, sealed) {
1143
+ checkKeyIv(key2, iv);
1144
+ if (sealed.length < FILE_TAG_BYTES)
1145
+ throw new FileCryptoError("authentication_failed", "ciphertext shorter than its tag");
1146
+ let out;
1147
+ try {
1148
+ out = await subtle().decrypt(
1149
+ { name: "AES-GCM", iv: own(iv), tagLength: FILE_TAG_BYTES * 8 },
1150
+ await importKey(key2, "decrypt"),
1151
+ own(sealed)
1152
+ );
1153
+ } catch {
1154
+ throw new FileCryptoError("authentication_failed", "file authentication failed");
1155
+ }
1156
+ return new Uint8Array(out);
1157
+ }
1158
+ var DEFAULT_SEAL_DEPS = { random: randomBytes, guard: looksLikePlaintext };
1159
+ async function sealFresh(plaintext, deps = DEFAULT_SEAL_DEPS) {
1160
+ for (let attempt = 0; attempt < PLAINTEXT_GUARD_ATTEMPTS; attempt++) {
1161
+ const key2 = deps.random(FILE_KEY_BYTES);
1162
+ const iv = deps.random(FILE_IV_BYTES);
1163
+ const ciphertext = await aesGcmSeal(key2, iv, plaintext);
1164
+ if (!deps.guard(ciphertext)) return { ciphertext, key: key2, iv };
1165
+ }
1166
+ throw new FileCryptoError("unavailable", "could not produce a depositable ciphertext");
1167
+ }
1168
+
1169
+ // packages/omemo/src/file-crypto.ts
1170
+ var FILE_MEDIA_TYPES = ALLOWED_MEDIA_TYPES;
1171
+ var MAX_FILE_NAME_BYTES = 200;
1172
+ var FILE_DOWNLOAD_TIMEOUT_MS = 6e4;
1173
+ var NAME_INVISIBLE = new RegExp(
1174
+ "[\\u0000-\\u001f\\u007f-\\u009f\\u00ad\\u061c\\u115f\\u1160\\u180e\\u200b-\\u200f\\u2028-\\u202e\\u2060-\\u206f\\u3164\\ufe00-\\ufe0f\\ufeff\\uffa0]",
1175
+ "g"
1176
+ );
1177
+ var NAME_RESERVED = /[<>:"|?*]/g;
1178
+ 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
+ function utf8Length(s) {
1180
+ return new TextEncoder().encode(s).length;
1181
+ }
1182
+ function cutUtf8(s, max) {
1183
+ let out = "";
1184
+ for (const ch of s) {
1185
+ if (utf8Length(out + ch) > max) break;
1186
+ out += ch;
1187
+ }
1188
+ return out;
1189
+ }
1190
+ function sanitizeFileName(raw) {
1191
+ let name = raw.normalize("NFC").replace(NAME_INVISIBLE, "");
1192
+ name = name.split(/[/\\]/).pop() ?? "";
1193
+ name = name.replace(NAME_RESERVED, "_").trim();
1194
+ name = name.replace(/^[.\s]+/, "").replace(/[.\s]+$/, "");
1195
+ if (WINDOWS_DEVICE.test(name)) name = `_${name}`;
1196
+ if (utf8Length(name) > MAX_FILE_NAME_BYTES) {
1197
+ const ext = fileExtension(name);
1198
+ const keep = ext && utf8Length(ext) < 32 ? ext : "";
1199
+ name = cutUtf8(
1200
+ name.slice(0, name.length - keep.length),
1201
+ MAX_FILE_NAME_BYTES - utf8Length(keep)
1202
+ );
1203
+ name = name.replace(/[.\s]+$/, "") + keep;
1204
+ }
1205
+ return name === "" ? "file" : name;
1206
+ }
1207
+ function fileExtension(name) {
1208
+ const lower = name.toLowerCase();
1209
+ if (lower.endsWith(".tar.gz")) return ".tar.gz";
1210
+ const dot = lower.lastIndexOf(".");
1211
+ return dot > 0 ? lower.slice(dot) : "";
1212
+ }
1213
+ var EXTENSION_TYPES = {
1214
+ "": "text/plain",
1215
+ ".txt": "text/plain",
1216
+ ".text": "text/plain",
1217
+ ".log": "text/plain",
1218
+ ".csv": "text/plain",
1219
+ ".tsv": "text/plain",
1220
+ ".json": "text/plain",
1221
+ ".yaml": "text/plain",
1222
+ ".yml": "text/plain",
1223
+ ".md": "text/markdown",
1224
+ ".markdown": "text/markdown",
1225
+ ".pdf": "application/pdf",
1226
+ ".zip": "application/zip",
1227
+ ".tar.gz": "application/gzip",
1228
+ ".tgz": "application/gzip"
1229
+ };
1230
+ function startsWith(bytes, sig) {
1231
+ return bytes.length >= sig.length && sig.every((b, i) => bytes[i] === b);
1232
+ }
1233
+ function isGzipContent(bytes) {
1234
+ if (bytes.length < 10 || !startsWith(bytes, [31, 139, 8])) return false;
1235
+ const flg = bytes[3];
1236
+ const os = bytes[9];
1237
+ return (flg & 224) === 0 && (os <= 13 || os === 255);
1238
+ }
1239
+ function isText(bytes) {
1240
+ let text;
1241
+ try {
1242
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1243
+ } catch {
1244
+ return false;
1245
+ }
1246
+ return !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(text);
1247
+ }
1248
+ function sniffContent(bytes) {
1249
+ if (startsWith(bytes, [37, 80, 68, 70, 45])) return "application/pdf";
1250
+ if (startsWith(bytes, [80, 75, 3, 4]) || startsWith(bytes, [80, 75, 5, 6]))
1251
+ return "application/zip";
1252
+ if (isGzipContent(bytes)) return "application/gzip";
1253
+ return isText(bytes) ? "text" : null;
1254
+ }
1255
+ function detectFileType(name, bytes) {
1256
+ const expected = EXTENSION_TYPES[fileExtension(name)];
1257
+ if (!expected) throw new FileCryptoError("type_not_allowed", "file extension not allowed");
1258
+ const sniffed = sniffContent(bytes);
1259
+ if (sniffed === null) throw new FileCryptoError("type_not_allowed", "file content not allowed");
1260
+ const actual = sniffed === "text" && (expected === "text/plain" || expected === "text/markdown") ? expected : sniffed;
1261
+ if (actual !== expected)
1262
+ throw new FileCryptoError("type_mismatch", "file content does not match its extension");
1263
+ return expected;
1264
+ }
1265
+ async function encryptFile(input) {
1266
+ if (input.bytes.length > MAX_FILE_PLAINTEXT_BYTES)
1267
+ throw new FileCryptoError(
1268
+ "too_large",
1269
+ `files are limited to ${MAX_FILE_PLAINTEXT_BYTES} bytes`
1270
+ );
1271
+ const name = sanitizeFileName(input.name);
1272
+ const mediaType = detectFileType(name, input.bytes);
1273
+ if (input.mediaType !== void 0 && input.mediaType !== mediaType)
1274
+ throw new FileCryptoError("type_mismatch", "declared type does not match the file");
1275
+ const plaintextSha256 = await fileSha256Hex(input.bytes);
1276
+ const { ciphertext, key: key2, iv } = await sealFresh(input.bytes);
1277
+ return {
1278
+ ciphertext,
1279
+ approval: {
1280
+ ciphertextSha256: await fileSha256Hex(ciphertext),
1281
+ sizeBytes: ciphertext.length,
1282
+ mediaType
1283
+ },
1284
+ name,
1285
+ mediaType,
1286
+ plaintextSize: input.bytes.length,
1287
+ plaintextSha256,
1288
+ key: key2,
1289
+ iv
1290
+ };
1291
+ }
1292
+ async function decryptFile(ciphertext, share, opts = {}) {
1293
+ const { key: key2, iv } = aesgcmSecrets(share.url);
1294
+ return openChecked(ciphertext, share, key2, iv, opts);
1295
+ }
1296
+ async function openChecked(ciphertext, facts, key2, iv, opts) {
1297
+ if (ciphertext.length > MAX_FILE_CIPHERTEXT_BYTES)
1298
+ throw new FileCryptoError("too_large", "ciphertext over the file limit");
1299
+ if (opts.approvedCiphertextSha256 !== void 0 && opts.approvedCiphertextSha256 !== facts.ciphertextSha256)
1300
+ throw new FileCryptoError("integrity", "file share differs from the approved file");
1301
+ if (ciphertext.length !== facts.ciphertextSize || await fileSha256Hex(ciphertext) !== facts.ciphertextSha256)
1302
+ throw new FileCryptoError("integrity", "ciphertext does not match the file share");
1303
+ const bytes = await aesGcmOpen(key2, iv, ciphertext);
1304
+ if (bytes.length !== facts.size || await fileSha256Hex(bytes) !== facts.sha256)
1305
+ throw new FileCryptoError("integrity", "plaintext does not match the file share");
1306
+ const name = sanitizeFileName(facts.name);
1307
+ const mediaType = detectFileType(name, bytes);
1308
+ if (mediaType !== facts.mediaType)
1309
+ throw new FileCryptoError("type_mismatch", "file content does not match its declared type");
1310
+ return { bytes, name, mediaType };
1311
+ }
1312
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1313
+ var IPV4_LITERAL = /^\d+(\.\d+){0,3}$/;
1314
+ var 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}$/;
1315
+ function pinnedOrigin(pin) {
1316
+ const bad = (what) => new FileCryptoError("invalid_descriptor", `upload origin: ${what}`);
1317
+ if (!pin || typeof pin.origin !== "string") throw bad("missing");
1318
+ let url;
1319
+ try {
1320
+ url = new URL(pin.origin);
1321
+ } catch {
1322
+ throw bad("invalid");
1323
+ }
1324
+ if (url.origin !== pin.origin.replace(/\/$/, "")) throw bad("must be a bare origin");
1325
+ const host = url.hostname.toLowerCase();
1326
+ const loopback = LOOPBACK_HOSTS.has(host) || host.endsWith(".localhost");
1327
+ if (loopback) {
1328
+ if (!pin.allowLoopbackHttp) throw bad("loopback only in explicit test mode");
1329
+ } else {
1330
+ if (url.protocol !== "https:") throw bad("must be https");
1331
+ if (IPV4_LITERAL.test(host) || host.startsWith("[")) throw bad("no IP literal");
1332
+ }
1333
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw bad("must be http(s)");
1334
+ return url;
1335
+ }
1336
+ function aesgcmSecrets(raw) {
1337
+ if (typeof raw !== "string" || !raw.startsWith("aesgcm://"))
1338
+ throw new FileCryptoError("invalid_descriptor", "not an aesgcm URL");
1339
+ const hash = raw.indexOf("#");
1340
+ const fragment = hash < 0 ? "" : raw.slice(hash + 1).toLowerCase();
1341
+ if (!/^[0-9a-f]{88}$/.test(fragment))
1342
+ throw new FileCryptoError(
1343
+ "invalid_descriptor",
1344
+ "aesgcm URL needs a 12-byte IV and a 32-byte key"
1345
+ );
1346
+ return {
1347
+ iv: fromHex(fragment.slice(0, FILE_IV_BYTES * 2)),
1348
+ key: fromHex(fragment.slice(FILE_IV_BYTES * 2))
1349
+ };
1350
+ }
1351
+ function aesgcmUrl(getUrl, key2, iv) {
1352
+ checkKeyIv(key2, iv);
1353
+ let url;
1354
+ try {
1355
+ url = new URL(getUrl);
1356
+ } catch {
1357
+ throw new FileCryptoError("invalid_descriptor", "invalid download URL");
1358
+ }
1359
+ if (url.protocol !== "https:" && url.protocol !== "http:")
1360
+ throw new FileCryptoError("invalid_descriptor", "download URL must be http(s)");
1361
+ if (url.hash !== "" || url.search !== "" || url.username !== "" || url.password !== "")
1362
+ throw new FileCryptoError(
1363
+ "invalid_descriptor",
1364
+ "download URL must not carry a query, fragment or credentials"
1365
+ );
1366
+ if (!UPLOAD_PATH.test(url.pathname))
1367
+ throw new FileCryptoError("invalid_descriptor", "not a file download URL");
1368
+ return `aesgcm://${url.host}${url.pathname}#${toHex(iv)}${toHex(key2)}`;
1369
+ }
1370
+ function parseAesgcmUrl(raw, pin) {
1371
+ const origin = pinnedOrigin(pin);
1372
+ const { key: key2, iv } = aesgcmSecrets(raw);
1373
+ const rest = raw.slice("aesgcm://".length, raw.indexOf("#"));
1374
+ const slash = rest.indexOf("/");
1375
+ const host = slash < 0 ? rest : rest.slice(0, slash);
1376
+ const path = slash < 0 ? "" : rest.slice(slash);
1377
+ if (host.toLowerCase() !== origin.host.toLowerCase())
1378
+ throw new FileCryptoError("invalid_descriptor", "file is not on the expected upload origin");
1379
+ if (!UPLOAD_PATH.test(path))
1380
+ throw new FileCryptoError("invalid_descriptor", "not a file download path");
1381
+ return { fetchUrl: `${origin.origin}${path}`, key: key2, iv };
1382
+ }
1383
+ var FILE_SHARE_VERSION = 1;
1384
+ var SHARE_KEYS = [
1385
+ "wacandoFile",
1386
+ "url",
1387
+ "name",
1388
+ "mediaType",
1389
+ "size",
1390
+ "sha256",
1391
+ "ciphertextSize",
1392
+ "ciphertextSha256"
1393
+ ];
1394
+ var SHA256 = /^[0-9a-f]{64}$/;
1395
+ function fileShareFor(file, getUrl) {
1396
+ return {
1397
+ url: aesgcmUrl(getUrl, file.key, file.iv),
1398
+ name: file.name,
1399
+ mediaType: file.mediaType,
1400
+ size: file.plaintextSize,
1401
+ sha256: file.plaintextSha256,
1402
+ ciphertextSize: file.approval.sizeBytes,
1403
+ ciphertextSha256: file.approval.ciphertextSha256
1404
+ };
1405
+ }
1406
+ function checkShape(v) {
1407
+ const bad = (what) => new FileCryptoError("invalid_descriptor", `file share: ${what}`);
1408
+ if (typeof v !== "object" || v === null || Array.isArray(v)) throw bad("not an object");
1409
+ const o = v;
1410
+ const keys = Object.keys(o);
1411
+ if (keys.length !== SHARE_KEYS.length || !SHARE_KEYS.every((k) => keys.includes(k)))
1412
+ throw bad("unexpected fields");
1413
+ if (o.wacandoFile !== FILE_SHARE_VERSION) throw bad("unsupported version");
1414
+ if (typeof o.url !== "string") throw bad("url");
1415
+ aesgcmSecrets(o.url);
1416
+ if (typeof o.name !== "string" || o.name.length === 0 || utf8Length(o.name) > 1024)
1417
+ throw bad("name");
1418
+ if (!ALLOWED_MEDIA_TYPES.includes(o.mediaType)) throw bad("media type");
1419
+ const int = (x, min, max) => typeof x === "number" && Number.isSafeInteger(x) && x >= min && x <= max;
1420
+ if (!int(o.size, 0, MAX_FILE_PLAINTEXT_BYTES)) throw bad("size");
1421
+ if (!int(o.ciphertextSize, FILE_TAG_BYTES, MAX_FILE_CIPHERTEXT_BYTES))
1422
+ throw bad("ciphertext size");
1423
+ if (o.ciphertextSize !== o.size + FILE_TAG_BYTES) throw bad("sizes disagree");
1424
+ if (typeof o.sha256 !== "string" || !SHA256.test(o.sha256)) throw bad("sha256");
1425
+ if (typeof o.ciphertextSha256 !== "string" || !SHA256.test(o.ciphertextSha256))
1426
+ throw bad("ciphertext sha256");
1427
+ return {
1428
+ url: o.url,
1429
+ name: o.name,
1430
+ mediaType: o.mediaType,
1431
+ size: o.size,
1432
+ sha256: o.sha256,
1433
+ ciphertextSize: o.ciphertextSize,
1434
+ ciphertextSha256: o.ciphertextSha256
1435
+ };
1436
+ }
1437
+ function formatFileShare(share) {
1438
+ const checked = checkShape({ wacandoFile: FILE_SHARE_VERSION, ...share });
1439
+ return JSON.stringify({ wacandoFile: FILE_SHARE_VERSION, ...checked });
1440
+ }
1441
+ function isFileShareBody(body) {
1442
+ return body.startsWith('{"wacandoFile":');
1443
+ }
1444
+ function parseFileShare(body, pin) {
1445
+ let v;
1446
+ try {
1447
+ v = JSON.parse(body);
1448
+ } catch {
1449
+ throw new FileCryptoError("invalid_descriptor", "file share is not JSON");
1450
+ }
1451
+ const share = checkShape(v);
1452
+ parseAesgcmUrl(share.url, pin);
1453
+ return share;
1454
+ }
1455
+ var FILE_REVIEW_VERSION = 1;
1456
+ var REVIEW_KEYS = [
1457
+ "wacandoFileReview",
1458
+ "fileId",
1459
+ "name",
1460
+ "mediaType",
1461
+ "size",
1462
+ "sha256",
1463
+ "ciphertextSize",
1464
+ "ciphertextSha256",
1465
+ "secret"
1466
+ ];
1467
+ var FILE_ID = /^fil_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
1468
+ function fileReviewFor(file, fileId) {
1469
+ checkKeyIv(file.key, file.iv);
1470
+ return checkReview({
1471
+ wacandoFileReview: FILE_REVIEW_VERSION,
1472
+ fileId,
1473
+ name: file.name,
1474
+ mediaType: file.mediaType,
1475
+ size: file.plaintextSize,
1476
+ sha256: file.plaintextSha256,
1477
+ ciphertextSize: file.approval.sizeBytes,
1478
+ ciphertextSha256: file.approval.ciphertextSha256,
1479
+ secret: `${toHex(file.iv)}${toHex(file.key)}`
1480
+ });
1481
+ }
1482
+ function checkReview(v) {
1483
+ const bad = (what) => new FileCryptoError("invalid_descriptor", `file review: ${what}`);
1484
+ if (typeof v !== "object" || v === null || Array.isArray(v)) throw bad("not an object");
1485
+ const o = v;
1486
+ const keys = Object.keys(o);
1487
+ if (keys.length !== REVIEW_KEYS.length || !REVIEW_KEYS.every((k) => keys.includes(k)))
1488
+ throw bad("unexpected fields");
1489
+ if (o.wacandoFileReview !== FILE_REVIEW_VERSION) throw bad("unsupported version");
1490
+ if (typeof o.fileId !== "string" || !FILE_ID.test(o.fileId)) throw bad("file id");
1491
+ if (typeof o.secret !== "string" || !/^[0-9a-f]{88}$/.test(o.secret)) throw bad("secret");
1492
+ const share = checkShape({
1493
+ wacandoFile: FILE_SHARE_VERSION,
1494
+ url: `aesgcm://review.invalid/#${o.secret}`,
1495
+ name: o.name,
1496
+ mediaType: o.mediaType,
1497
+ size: o.size,
1498
+ sha256: o.sha256,
1499
+ ciphertextSize: o.ciphertextSize,
1500
+ ciphertextSha256: o.ciphertextSha256
1501
+ });
1502
+ return {
1503
+ fileId: o.fileId,
1504
+ name: share.name,
1505
+ mediaType: share.mediaType,
1506
+ size: share.size,
1507
+ sha256: share.sha256,
1508
+ ciphertextSize: share.ciphertextSize,
1509
+ ciphertextSha256: share.ciphertextSha256,
1510
+ secret: o.secret
1511
+ };
1512
+ }
1513
+ function formatFileReview(review) {
1514
+ const checked = checkReview({ wacandoFileReview: FILE_REVIEW_VERSION, ...review });
1515
+ return JSON.stringify({ wacandoFileReview: FILE_REVIEW_VERSION, ...checked });
1516
+ }
1517
+ function isFileReviewBody(body) {
1518
+ return body.startsWith('{"wacandoFileReview":');
1519
+ }
1520
+ function parseFileReview(body) {
1521
+ let v;
1522
+ try {
1523
+ v = JSON.parse(body);
1524
+ } catch {
1525
+ throw new FileCryptoError("invalid_descriptor", "file review is not JSON");
1526
+ }
1527
+ return checkReview(v);
1528
+ }
1529
+ async function decryptFileReview(ciphertext, review, opts = {}) {
1530
+ const checked = checkReview({ wacandoFileReview: FILE_REVIEW_VERSION, ...review });
1531
+ const iv = fromHex(checked.secret.slice(0, FILE_IV_BYTES * 2));
1532
+ const key2 = fromHex(checked.secret.slice(FILE_IV_BYTES * 2));
1533
+ return openChecked(ciphertext, checked, key2, iv, opts);
1534
+ }
1535
+ function fileIdOfShare(share) {
1536
+ const match = /^aesgcm:\/\/[^/#]+\/v1\/uploads\/(fil_[0-9a-f-]{36})\/[A-Za-z0-9_-]{43}#/.exec(
1537
+ share.url
1538
+ );
1539
+ return match?.[1] && FILE_ID.test(match[1]) ? match[1] : null;
1540
+ }
1541
+ async function downloadFile(share, opts) {
1542
+ const { fetchUrl } = parseAesgcmUrl(share.url, opts.pin);
1543
+ const limit = share.ciphertextSize;
1544
+ const doFetch = opts.fetch ?? globalThis.fetch;
1545
+ const signal = AbortSignal.timeout(opts.timeoutMs ?? FILE_DOWNLOAD_TIMEOUT_MS);
1546
+ const failed = (what) => new FileCryptoError("download_failed", `download ${what}`);
1547
+ let res;
1548
+ try {
1549
+ res = await doFetch(fetchUrl, {
1550
+ method: "GET",
1551
+ headers: opts.bearer ? { authorization: `Bearer ${opts.bearer}` } : {},
1552
+ redirect: "error",
1553
+ credentials: "omit",
1554
+ referrerPolicy: "no-referrer",
1555
+ signal
1556
+ });
1557
+ } catch {
1558
+ throw failed(signal.aborted ? "timed out" : "failed (network or redirect)");
1559
+ }
1560
+ if (res.redirected || res.type === "opaqueredirect") {
1561
+ await res.body?.cancel().catch(() => {
1562
+ });
1563
+ throw failed("redirected");
1564
+ }
1565
+ if (res.status !== 200) {
1566
+ await res.body?.cancel().catch(() => {
1567
+ });
1568
+ throw failed(`refused with HTTP ${res.status}`);
1569
+ }
1570
+ const declared = res.headers.get("content-length");
1571
+ if (declared !== null && !(Number(declared) <= limit)) {
1572
+ await res.body?.cancel().catch(() => {
1573
+ });
1574
+ throw new FileCryptoError("too_large", "download larger than the announced file");
1575
+ }
1576
+ if (!res.body) throw failed("has no body");
1577
+ const reader = res.body.getReader();
1578
+ const chunks = [];
1579
+ let total = 0;
1580
+ try {
1581
+ for (; ; ) {
1582
+ const { done, value } = await reader.read();
1583
+ if (done) break;
1584
+ total += value.length;
1585
+ if (total > limit) {
1586
+ await reader.cancel().catch(() => {
1587
+ });
1588
+ throw new FileCryptoError("too_large", "download larger than the announced file");
1589
+ }
1590
+ chunks.push(value);
1591
+ }
1592
+ } catch (e) {
1593
+ if (e instanceof FileCryptoError) throw e;
1594
+ throw failed(signal.aborted ? "timed out" : "interrupted");
1595
+ }
1596
+ const out = new Uint8Array(total);
1597
+ let at = 0;
1598
+ for (const c of chunks) {
1599
+ out.set(c, at);
1600
+ at += c.length;
1601
+ }
1602
+ return out;
1603
+ }
1604
+
1079
1605
  // packages/omemo/src/recovery.ts
1080
1606
  import {
1081
1607
  CiphertextMessageType as CiphertextMessageType2,
@@ -1983,14 +2509,14 @@ var SessionRecovery = class _SessionRecovery {
1983
2509
  if (this.inflight.has(pair)) throw new Refused("resource-constraint", "busy");
1984
2510
  if (!findPendingSend(this.ledger, pair, r.roomJid, r.stanzaId, r.keyDigest, r.consumedPreKeyId))
1985
2511
  throw new Refused("not-acceptable", "stale");
1986
- const own = activeAttempt(this.ledger, pair);
1987
- if (own) {
2512
+ const own2 = activeAttempt(this.ledger, pair);
2513
+ if (own2) {
1988
2514
  const weInitiate = { jid: this.o.provider.bareJid, device: this.state.deviceId };
1989
2515
  const theyInitiate = { jid: r.bJid, device: r.bDevice };
1990
- if (own.role === "B" && own.state === "offered" && compareInitiators(weInitiate, theyInitiate) < 0) {
1991
- own.state = "failed";
1992
- own.reason = "stale";
1993
- delete own.cached;
2516
+ if (own2.role === "B" && own2.state === "offered" && compareInitiators(weInitiate, theyInitiate) < 0) {
2517
+ own2.state = "failed";
2518
+ own2.reason = "stale";
2519
+ delete own2.cached;
1994
2520
  this.persist();
1995
2521
  } else throw new Refused("not-acceptable", "fenced");
1996
2522
  }
@@ -2462,7 +2988,18 @@ function loadNativeOmemoProvider(jid, options = {}) {
2462
2988
  export {
2463
2989
  BODY_MARKER,
2464
2990
  DeviceState,
2991
+ FILE_DOWNLOAD_TIMEOUT_MS,
2992
+ FILE_IV_BYTES,
2993
+ FILE_KEY_BYTES,
2994
+ FILE_MEDIA_TYPES,
2995
+ FILE_REVIEW_VERSION,
2996
+ FILE_SHARE_VERSION,
2997
+ FILE_TAG_BYTES,
2998
+ FileCryptoError,
2465
2999
  LibsignalNodeBackend,
3000
+ MAX_FILE_CIPHERTEXT_BYTES,
3001
+ MAX_FILE_NAME_BYTES,
3002
+ MAX_FILE_PLAINTEXT_BYTES,
2466
3003
  NS_DEVICELIST,
2467
3004
  NS_EME,
2468
3005
  NS_HINTS,
@@ -2478,11 +3015,31 @@ export {
2478
3015
  RECOVERY_LIMITS,
2479
3016
  SessionRecovery,
2480
3017
  UnsupportedSignalFormatError,
3018
+ aesgcmUrl,
2481
3019
  bundleNode,
3020
+ decryptFile,
3021
+ decryptFileReview,
2482
3022
  defaultStateBaseDir,
3023
+ detectFileType,
2483
3024
  deviceListEl,
3025
+ downloadFile,
3026
+ encryptFile,
3027
+ fileExtension,
3028
+ fileIdOfShare,
3029
+ fileReviewFor,
3030
+ fileSha256Hex,
3031
+ fileShareFor,
3032
+ formatFileReview,
3033
+ formatFileShare,
3034
+ isFileReviewBody,
3035
+ isFileShareBody,
2484
3036
  jidStateDirName,
2485
3037
  loadNativeOmemoProvider,
3038
+ parseAesgcmUrl,
2486
3039
  parseDeviceList,
2487
- parseEncrypted
3040
+ parseEncrypted,
3041
+ parseFileReview,
3042
+ parseFileShare,
3043
+ sanitizeFileName,
3044
+ sniffContent
2488
3045
  };
@@ -1,16 +1,17 @@
1
- // Embedded from lbframe/wacando@9f82881d4ee6cfd655727a08883877230917c955 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
1
+ // Embedded from lbframe/wacando@618922ecaf2213da18949a8b27d343c8767b64c5 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
2
2
  import {
3
3
  MucJoinError,
4
4
  MucOriginTracker,
5
5
  authenticatedMessageOrigin,
6
+ defaultTimeoutMs,
6
7
  forwardedStanza,
7
8
  joinRoom,
8
9
  queryMam,
9
10
  refuseSubscriptionRequests,
10
11
  unwrapPolicyDelivery,
11
12
  waitForStanza
12
- } from "./chunks/chunk-GASYW3SG.mjs";
13
- import "./chunks/chunk-EIGFLZ2Y.mjs";
13
+ } from "./chunks/chunk-53E7SSTF.mjs";
14
+ import "./chunks/chunk-PZDQXGA6.mjs";
14
15
 
15
16
  // packages/xmpp/src/index.ts
16
17
  import { default as default2 } from "@xmpp/xml";
@@ -34,6 +35,41 @@ async function announceAvailable(entity, timeoutMs) {
34
35
  }
35
36
  await echoed;
36
37
  }
38
+
39
+ // packages/xmpp/src/upload.ts
40
+ import xml2 from "@xmpp/xml";
41
+ var NS_HTTP_UPLOAD = "urn:xmpp:http:upload:0";
42
+ function uploadServiceJid(domain) {
43
+ return `upload.${domain}`;
44
+ }
45
+ function uploadSlotRequest(req) {
46
+ return xml2("request", {
47
+ xmlns: NS_HTTP_UPLOAD,
48
+ filename: req.filename,
49
+ size: String(req.size),
50
+ "content-type": req.contentType ?? "application/octet-stream"
51
+ });
52
+ }
53
+ var ALLOWED_PUT_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "expires"]);
54
+ function parseUploadSlot(slot) {
55
+ if (!slot || slot.name !== "slot" || slot.attrs.xmlns !== NS_HTTP_UPLOAD)
56
+ throw new Error("upload service returned no slot");
57
+ const put = slot.getChild("put")?.attrs.url;
58
+ const get = slot.getChild("get")?.attrs.url;
59
+ if (typeof put !== "string" || typeof get !== "string")
60
+ throw new Error("upload slot is missing its put/get URL");
61
+ const putHeaders = {};
62
+ for (const header of slot.getChild("put")?.getChildren("header") ?? []) {
63
+ const name = String(header.attrs.name ?? "").toLowerCase();
64
+ if (ALLOWED_PUT_HEADERS.has(name)) putHeaders[name] = header.text().replace(/[\r\n]/g, "");
65
+ }
66
+ return { put, get, putHeaders };
67
+ }
68
+ async function requestUploadSlot(entity, service, req, timeoutMs = defaultTimeoutMs()) {
69
+ const iq = xml2("iq", { type: "get", to: service }, uploadSlotRequest(req));
70
+ const result = await entity.iqCaller.request(iq, timeoutMs);
71
+ return parseUploadSlot(result.getChild("slot", NS_HTTP_UPLOAD));
72
+ }
37
73
  export {
38
74
  MucJoinError,
39
75
  MucOriginTracker,
@@ -43,6 +79,8 @@ export {
43
79
  joinRoom,
44
80
  queryMam,
45
81
  refuseSubscriptionRequests,
82
+ requestUploadSlot,
46
83
  unwrapPolicyDelivery,
84
+ uploadServiceJid,
47
85
  default2 as xml
48
86
  };