@alfe.ai/openclaw-chat 0.0.21 → 0.0.23
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/dist/plugin2.cjs +133 -1
- package/dist/plugin2.js +133 -1
- package/openclaw.plugin.json +4 -0
- package/package.json +3 -3
package/dist/plugin2.cjs
CHANGED
|
@@ -402,6 +402,107 @@ function buildA2ATools(chatClient, log) {
|
|
|
402
402
|
];
|
|
403
403
|
}
|
|
404
404
|
//#endregion
|
|
405
|
+
//#region src/attachment-url.ts
|
|
406
|
+
/**
|
|
407
|
+
* Attachment URL validation for the openclaw-chat plugin.
|
|
408
|
+
*
|
|
409
|
+
* This package runs inside user-installed OpenClaw agents and has no
|
|
410
|
+
* dependency on the internal @alfe/api-core SSRF validator, so the
|
|
411
|
+
* plumbing is inlined here. The rules are deliberately conservative:
|
|
412
|
+
*
|
|
413
|
+
* - `https:` only.
|
|
414
|
+
* - Userinfo (`https://u:p@host/...`) is rejected.
|
|
415
|
+
* - IP-literal hosts (v4 or bracketed v6) are rejected outright — only
|
|
416
|
+
* DNS names for known provider CDNs are acceptable.
|
|
417
|
+
* - The host must match a curated allowlist of Alfe-issued / channel-
|
|
418
|
+
* provider CDNs. Anything else is dropped before we fetch it.
|
|
419
|
+
*
|
|
420
|
+
* Operators running an agent in a private network that needs to dereference
|
|
421
|
+
* additional hosts can extend the list via the
|
|
422
|
+
* `ALFE_ATTACHMENT_ALLOWED_HOSTS` env var (comma-separated, entries prefixed
|
|
423
|
+
* with `.` match suffixes).
|
|
424
|
+
*/
|
|
425
|
+
const DEFAULT_EXACT_HOSTS = [
|
|
426
|
+
"s3.amazonaws.com",
|
|
427
|
+
"api.twilio.com",
|
|
428
|
+
"graph.microsoft.com",
|
|
429
|
+
"mmg.whatsapp.net"
|
|
430
|
+
];
|
|
431
|
+
const DEFAULT_SUFFIX_HOSTS = [
|
|
432
|
+
".s3.amazonaws.com",
|
|
433
|
+
".amazonaws.com",
|
|
434
|
+
".twiliocdn.com",
|
|
435
|
+
".cdn.discordapp.com",
|
|
436
|
+
".discordapp.net",
|
|
437
|
+
".telegram.org",
|
|
438
|
+
".alfe.ai"
|
|
439
|
+
];
|
|
440
|
+
function parseExtraHosts(raw) {
|
|
441
|
+
const exact = /* @__PURE__ */ new Set();
|
|
442
|
+
const suffix = [];
|
|
443
|
+
if (!raw) return {
|
|
444
|
+
exact,
|
|
445
|
+
suffix
|
|
446
|
+
};
|
|
447
|
+
for (const part of raw.split(",")) {
|
|
448
|
+
const trimmed = part.trim().toLowerCase();
|
|
449
|
+
if (!trimmed) continue;
|
|
450
|
+
if (trimmed.startsWith(".")) suffix.push(trimmed);
|
|
451
|
+
else exact.add(trimmed);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
exact,
|
|
455
|
+
suffix
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function isIpLiteral(host) {
|
|
459
|
+
if (host.startsWith("[") && host.endsWith("]")) return true;
|
|
460
|
+
return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
461
|
+
}
|
|
462
|
+
function validateAttachmentUrl(input, opts = {}) {
|
|
463
|
+
if (!input || input.trim() === "") return {
|
|
464
|
+
ok: false,
|
|
465
|
+
reason: "empty"
|
|
466
|
+
};
|
|
467
|
+
let url;
|
|
468
|
+
try {
|
|
469
|
+
url = new URL(input);
|
|
470
|
+
} catch {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
reason: "invalid_url"
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (url.protocol !== "https:") return {
|
|
477
|
+
ok: false,
|
|
478
|
+
reason: "not_https"
|
|
479
|
+
};
|
|
480
|
+
if (url.username !== "" || url.password !== "") return {
|
|
481
|
+
ok: false,
|
|
482
|
+
reason: "userinfo_not_allowed"
|
|
483
|
+
};
|
|
484
|
+
const host = url.hostname.toLowerCase();
|
|
485
|
+
if (!host) return {
|
|
486
|
+
ok: false,
|
|
487
|
+
reason: "empty_host"
|
|
488
|
+
};
|
|
489
|
+
if (isIpLiteral(host)) return {
|
|
490
|
+
ok: false,
|
|
491
|
+
reason: "ip_literal"
|
|
492
|
+
};
|
|
493
|
+
if (host === "localhost" || host === "metadata" || host === "metadata.google.internal") return {
|
|
494
|
+
ok: false,
|
|
495
|
+
reason: "blocked_host"
|
|
496
|
+
};
|
|
497
|
+
const extra = parseExtraHosts(opts.extraHosts ?? process.env.ALFE_ATTACHMENT_ALLOWED_HOSTS);
|
|
498
|
+
if (new Set([...DEFAULT_EXACT_HOSTS.map((h) => h.toLowerCase()), ...extra.exact]).has(host)) return { ok: true };
|
|
499
|
+
if ([...DEFAULT_SUFFIX_HOSTS.map((h) => h.toLowerCase()), ...extra.suffix].some((rule) => host === rule.slice(1) || host.endsWith(rule))) return { ok: true };
|
|
500
|
+
return {
|
|
501
|
+
ok: false,
|
|
502
|
+
reason: "host_not_in_allowlist"
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
//#endregion
|
|
405
506
|
//#region src/plugin.ts
|
|
406
507
|
/**
|
|
407
508
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -452,6 +553,37 @@ let connectingPromise = null;
|
|
|
452
553
|
let metricsClient = null;
|
|
453
554
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
454
555
|
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
556
|
+
const MAX_REDIRECTS = 5;
|
|
557
|
+
/**
|
|
558
|
+
* Fetch `url` with `redirect: "manual"` and revalidate every hop against the
|
|
559
|
+
* attachment allowlist. This defeats presigned-URL substitution attacks
|
|
560
|
+
* where an attacker points us at a trusted host that 302s to metadata
|
|
561
|
+
* or an internal service.
|
|
562
|
+
*/
|
|
563
|
+
async function fetchAttachmentWithValidation(url, signal) {
|
|
564
|
+
let currentUrl = url;
|
|
565
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
|
|
566
|
+
const verdict = validateAttachmentUrl(currentUrl);
|
|
567
|
+
if (!verdict.ok) throw new Error(`attachment_url_rejected:${verdict.reason ?? "unknown"}`);
|
|
568
|
+
const res = await fetch(currentUrl, {
|
|
569
|
+
redirect: "manual",
|
|
570
|
+
signal
|
|
571
|
+
});
|
|
572
|
+
const status = res.status;
|
|
573
|
+
if (status >= 300 && status < 400) {
|
|
574
|
+
const location = res.headers.get("location");
|
|
575
|
+
if (!location) throw new Error("redirect_without_location");
|
|
576
|
+
try {
|
|
577
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
578
|
+
} catch {
|
|
579
|
+
throw new Error("invalid_redirect_target");
|
|
580
|
+
}
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
return res;
|
|
584
|
+
}
|
|
585
|
+
throw new Error("too_many_redirects");
|
|
586
|
+
}
|
|
455
587
|
async function downloadAttachments(attachments, log) {
|
|
456
588
|
const attachDir = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "attachments");
|
|
457
589
|
await (0, node_fs_promises.mkdir)(attachDir, { recursive: true });
|
|
@@ -464,7 +596,7 @@ async function downloadAttachments(attachments, log) {
|
|
|
464
596
|
controller.abort();
|
|
465
597
|
}, DOWNLOAD_TIMEOUT_MS);
|
|
466
598
|
try {
|
|
467
|
-
const res = await
|
|
599
|
+
const res = await fetchAttachmentWithValidation(att.url, controller.signal);
|
|
468
600
|
if (!res.ok) {
|
|
469
601
|
log.warn(`Failed to download attachment ${att.id}: ${String(res.status)}`);
|
|
470
602
|
continue;
|
package/dist/plugin2.js
CHANGED
|
@@ -405,6 +405,107 @@ function buildA2ATools(chatClient, log) {
|
|
|
405
405
|
];
|
|
406
406
|
}
|
|
407
407
|
//#endregion
|
|
408
|
+
//#region src/attachment-url.ts
|
|
409
|
+
/**
|
|
410
|
+
* Attachment URL validation for the openclaw-chat plugin.
|
|
411
|
+
*
|
|
412
|
+
* This package runs inside user-installed OpenClaw agents and has no
|
|
413
|
+
* dependency on the internal @alfe/api-core SSRF validator, so the
|
|
414
|
+
* plumbing is inlined here. The rules are deliberately conservative:
|
|
415
|
+
*
|
|
416
|
+
* - `https:` only.
|
|
417
|
+
* - Userinfo (`https://u:p@host/...`) is rejected.
|
|
418
|
+
* - IP-literal hosts (v4 or bracketed v6) are rejected outright — only
|
|
419
|
+
* DNS names for known provider CDNs are acceptable.
|
|
420
|
+
* - The host must match a curated allowlist of Alfe-issued / channel-
|
|
421
|
+
* provider CDNs. Anything else is dropped before we fetch it.
|
|
422
|
+
*
|
|
423
|
+
* Operators running an agent in a private network that needs to dereference
|
|
424
|
+
* additional hosts can extend the list via the
|
|
425
|
+
* `ALFE_ATTACHMENT_ALLOWED_HOSTS` env var (comma-separated, entries prefixed
|
|
426
|
+
* with `.` match suffixes).
|
|
427
|
+
*/
|
|
428
|
+
const DEFAULT_EXACT_HOSTS = [
|
|
429
|
+
"s3.amazonaws.com",
|
|
430
|
+
"api.twilio.com",
|
|
431
|
+
"graph.microsoft.com",
|
|
432
|
+
"mmg.whatsapp.net"
|
|
433
|
+
];
|
|
434
|
+
const DEFAULT_SUFFIX_HOSTS = [
|
|
435
|
+
".s3.amazonaws.com",
|
|
436
|
+
".amazonaws.com",
|
|
437
|
+
".twiliocdn.com",
|
|
438
|
+
".cdn.discordapp.com",
|
|
439
|
+
".discordapp.net",
|
|
440
|
+
".telegram.org",
|
|
441
|
+
".alfe.ai"
|
|
442
|
+
];
|
|
443
|
+
function parseExtraHosts(raw) {
|
|
444
|
+
const exact = /* @__PURE__ */ new Set();
|
|
445
|
+
const suffix = [];
|
|
446
|
+
if (!raw) return {
|
|
447
|
+
exact,
|
|
448
|
+
suffix
|
|
449
|
+
};
|
|
450
|
+
for (const part of raw.split(",")) {
|
|
451
|
+
const trimmed = part.trim().toLowerCase();
|
|
452
|
+
if (!trimmed) continue;
|
|
453
|
+
if (trimmed.startsWith(".")) suffix.push(trimmed);
|
|
454
|
+
else exact.add(trimmed);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
exact,
|
|
458
|
+
suffix
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function isIpLiteral(host) {
|
|
462
|
+
if (host.startsWith("[") && host.endsWith("]")) return true;
|
|
463
|
+
return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
464
|
+
}
|
|
465
|
+
function validateAttachmentUrl(input, opts = {}) {
|
|
466
|
+
if (!input || input.trim() === "") return {
|
|
467
|
+
ok: false,
|
|
468
|
+
reason: "empty"
|
|
469
|
+
};
|
|
470
|
+
let url;
|
|
471
|
+
try {
|
|
472
|
+
url = new URL(input);
|
|
473
|
+
} catch {
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
reason: "invalid_url"
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
if (url.protocol !== "https:") return {
|
|
480
|
+
ok: false,
|
|
481
|
+
reason: "not_https"
|
|
482
|
+
};
|
|
483
|
+
if (url.username !== "" || url.password !== "") return {
|
|
484
|
+
ok: false,
|
|
485
|
+
reason: "userinfo_not_allowed"
|
|
486
|
+
};
|
|
487
|
+
const host = url.hostname.toLowerCase();
|
|
488
|
+
if (!host) return {
|
|
489
|
+
ok: false,
|
|
490
|
+
reason: "empty_host"
|
|
491
|
+
};
|
|
492
|
+
if (isIpLiteral(host)) return {
|
|
493
|
+
ok: false,
|
|
494
|
+
reason: "ip_literal"
|
|
495
|
+
};
|
|
496
|
+
if (host === "localhost" || host === "metadata" || host === "metadata.google.internal") return {
|
|
497
|
+
ok: false,
|
|
498
|
+
reason: "blocked_host"
|
|
499
|
+
};
|
|
500
|
+
const extra = parseExtraHosts(opts.extraHosts ?? process.env.ALFE_ATTACHMENT_ALLOWED_HOSTS);
|
|
501
|
+
if (new Set([...DEFAULT_EXACT_HOSTS.map((h) => h.toLowerCase()), ...extra.exact]).has(host)) return { ok: true };
|
|
502
|
+
if ([...DEFAULT_SUFFIX_HOSTS.map((h) => h.toLowerCase()), ...extra.suffix].some((rule) => host === rule.slice(1) || host.endsWith(rule))) return { ok: true };
|
|
503
|
+
return {
|
|
504
|
+
ok: false,
|
|
505
|
+
reason: "host_not_in_allowlist"
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
408
509
|
//#region src/plugin.ts
|
|
409
510
|
/**
|
|
410
511
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -455,6 +556,37 @@ let connectingPromise = null;
|
|
|
455
556
|
let metricsClient = null;
|
|
456
557
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
457
558
|
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
559
|
+
const MAX_REDIRECTS = 5;
|
|
560
|
+
/**
|
|
561
|
+
* Fetch `url` with `redirect: "manual"` and revalidate every hop against the
|
|
562
|
+
* attachment allowlist. This defeats presigned-URL substitution attacks
|
|
563
|
+
* where an attacker points us at a trusted host that 302s to metadata
|
|
564
|
+
* or an internal service.
|
|
565
|
+
*/
|
|
566
|
+
async function fetchAttachmentWithValidation(url, signal) {
|
|
567
|
+
let currentUrl = url;
|
|
568
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
|
|
569
|
+
const verdict = validateAttachmentUrl(currentUrl);
|
|
570
|
+
if (!verdict.ok) throw new Error(`attachment_url_rejected:${verdict.reason ?? "unknown"}`);
|
|
571
|
+
const res = await fetch(currentUrl, {
|
|
572
|
+
redirect: "manual",
|
|
573
|
+
signal
|
|
574
|
+
});
|
|
575
|
+
const status = res.status;
|
|
576
|
+
if (status >= 300 && status < 400) {
|
|
577
|
+
const location = res.headers.get("location");
|
|
578
|
+
if (!location) throw new Error("redirect_without_location");
|
|
579
|
+
try {
|
|
580
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
581
|
+
} catch {
|
|
582
|
+
throw new Error("invalid_redirect_target");
|
|
583
|
+
}
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
return res;
|
|
587
|
+
}
|
|
588
|
+
throw new Error("too_many_redirects");
|
|
589
|
+
}
|
|
458
590
|
async function downloadAttachments(attachments, log) {
|
|
459
591
|
const attachDir = join(homedir(), ".alfe", "attachments");
|
|
460
592
|
await mkdir(attachDir, { recursive: true });
|
|
@@ -467,7 +599,7 @@ async function downloadAttachments(attachments, log) {
|
|
|
467
599
|
controller.abort();
|
|
468
600
|
}, DOWNLOAD_TIMEOUT_MS);
|
|
469
601
|
try {
|
|
470
|
-
const res = await
|
|
602
|
+
const res = await fetchAttachmentWithValidation(att.url, controller.signal);
|
|
471
603
|
if (!res.ok) {
|
|
472
604
|
log.warn(`Failed to download attachment ${att.id}: ${String(res.status)}`);
|
|
473
605
|
continue;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "@alfe.ai/openclaw-chat",
|
|
3
|
+
"name": "Chat Plugin",
|
|
4
|
+
"version": "0.0.21",
|
|
5
|
+
"description": "Chat conversation channel — web widget and mobile app share unified chat sessions",
|
|
6
|
+
"entry": "./dist/plugin.js",
|
|
3
7
|
"configSchema": {
|
|
4
8
|
"type": "object",
|
|
5
9
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-chat",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@alfe.ai/agent-api-client": "^0.0.7",
|
|
31
|
-
"@alfe.ai/chat": "^0.0.
|
|
32
|
-
"@alfe.ai/config": "^0.0.
|
|
31
|
+
"@alfe.ai/chat": "^0.0.8",
|
|
32
|
+
"@alfe.ai/config": "^0.0.8"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"openclaw": ">=2026.3.0"
|