@botcord/daemon 0.2.36 → 0.2.37
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/config.d.ts +29 -0
- package/dist/config.js +27 -0
- package/dist/daemon-config-map.d.ts +3 -0
- package/dist/daemon-config-map.js +30 -0
- package/dist/daemon.d.ts +15 -1
- package/dist/daemon.js +56 -11
- package/dist/gateway/channels/botcord.js +44 -0
- package/dist/gateway/channels/http-types.d.ts +19 -0
- package/dist/gateway/channels/http-types.js +1 -0
- package/dist/gateway/channels/index.d.ts +5 -0
- package/dist/gateway/channels/index.js +5 -0
- package/dist/gateway/channels/login-session.d.ts +83 -0
- package/dist/gateway/channels/login-session.js +99 -0
- package/dist/gateway/channels/secret-store.d.ts +21 -0
- package/dist/gateway/channels/secret-store.js +75 -0
- package/dist/gateway/channels/state-store.d.ts +60 -0
- package/dist/gateway/channels/state-store.js +173 -0
- package/dist/gateway/channels/telegram.d.ts +31 -0
- package/dist/gateway/channels/telegram.js +371 -0
- package/dist/gateway/channels/text-split.d.ts +13 -0
- package/dist/gateway/channels/text-split.js +33 -0
- package/dist/gateway/channels/url-guard.d.ts +18 -0
- package/dist/gateway/channels/url-guard.js +53 -0
- package/dist/gateway/channels/wechat-http.d.ts +18 -0
- package/dist/gateway/channels/wechat-http.js +28 -0
- package/dist/gateway/channels/wechat-login.d.ts +36 -0
- package/dist/gateway/channels/wechat-login.js +62 -0
- package/dist/gateway/channels/wechat.d.ts +40 -0
- package/dist/gateway/channels/wechat.js +472 -0
- package/dist/gateway/runtimes/openclaw-acp.js +211 -6
- package/dist/gateway/types.d.ts +10 -0
- package/dist/gateway-control.d.ts +53 -0
- package/dist/gateway-control.js +638 -0
- package/dist/provision.d.ts +7 -0
- package/dist/provision.js +255 -5
- package/package.json +1 -1
- package/src/__tests__/gateway-control.test.ts +499 -0
- package/src/__tests__/openclaw-acp.test.ts +63 -0
- package/src/__tests__/provision.test.ts +179 -0
- package/src/__tests__/secret-store.test.ts +70 -0
- package/src/__tests__/state-store.test.ts +119 -0
- package/src/__tests__/third-party-gateway.test.ts +126 -0
- package/src/__tests__/url-guard.test.ts +85 -0
- package/src/__tests__/wechat-channel.test.ts +1134 -0
- package/src/config.ts +71 -0
- package/src/daemon-config-map.ts +24 -0
- package/src/daemon.ts +70 -11
- package/src/gateway/__tests__/botcord-channel.test.ts +1 -1
- package/src/gateway/__tests__/telegram-channel.test.ts +555 -0
- package/src/gateway/channels/botcord.ts +39 -0
- package/src/gateway/channels/http-types.ts +22 -0
- package/src/gateway/channels/index.ts +22 -0
- package/src/gateway/channels/login-session.ts +135 -0
- package/src/gateway/channels/secret-store.ts +100 -0
- package/src/gateway/channels/state-store.ts +213 -0
- package/src/gateway/channels/telegram.ts +469 -0
- package/src/gateway/channels/text-split.ts +29 -0
- package/src/gateway/channels/url-guard.ts +55 -0
- package/src/gateway/channels/wechat-http.ts +35 -0
- package/src/gateway/channels/wechat-login.ts +90 -0
- package/src/gateway/channels/wechat.ts +572 -0
- package/src/gateway/runtimes/openclaw-acp.ts +211 -7
- package/src/gateway/types.ts +10 -0
- package/src/gateway-control.ts +709 -0
- package/src/provision.ts +336 -5
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W9: SSRF guard for user-supplied `baseUrl` values that flow into
|
|
3
|
+
* authenticated daemon-side fetches (Telegram getMe, WeChat iLink).
|
|
4
|
+
*
|
|
5
|
+
* Policy: scheme MUST be `https`; the hostname MUST match one of the
|
|
6
|
+
* explicitly-allowed well-known API hosts (case-insensitive exact match).
|
|
7
|
+
* Switching from blocklist to allowlist closes the GCP/AWS metadata hostname
|
|
8
|
+
* pivot vector — blocklists miss names like `metadata.google.internal`,
|
|
9
|
+
* `*.svc.cluster.local`, etc.
|
|
10
|
+
*
|
|
11
|
+
* The test host `botcord-test.local` is added only when
|
|
12
|
+
* NODE_ENV === "test" to keep unit tests working without relaxing production
|
|
13
|
+
* policy.
|
|
14
|
+
*/
|
|
15
|
+
export declare class UnsafeBaseUrlError extends Error {
|
|
16
|
+
constructor(reason: string);
|
|
17
|
+
}
|
|
18
|
+
export declare function assertSafeBaseUrl(value: string | undefined | null): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W9: SSRF guard for user-supplied `baseUrl` values that flow into
|
|
3
|
+
* authenticated daemon-side fetches (Telegram getMe, WeChat iLink).
|
|
4
|
+
*
|
|
5
|
+
* Policy: scheme MUST be `https`; the hostname MUST match one of the
|
|
6
|
+
* explicitly-allowed well-known API hosts (case-insensitive exact match).
|
|
7
|
+
* Switching from blocklist to allowlist closes the GCP/AWS metadata hostname
|
|
8
|
+
* pivot vector — blocklists miss names like `metadata.google.internal`,
|
|
9
|
+
* `*.svc.cluster.local`, etc.
|
|
10
|
+
*
|
|
11
|
+
* The test host `botcord-test.local` is added only when
|
|
12
|
+
* NODE_ENV === "test" to keep unit tests working without relaxing production
|
|
13
|
+
* policy.
|
|
14
|
+
*/
|
|
15
|
+
export class UnsafeBaseUrlError extends Error {
|
|
16
|
+
constructor(reason) {
|
|
17
|
+
super(`unsafe_base_url: ${reason}`);
|
|
18
|
+
this.name = "UnsafeBaseUrlError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function allowedHosts() {
|
|
22
|
+
const hosts = new Set(["api.telegram.org", "ilinkai.weixin.qq.com"]);
|
|
23
|
+
if (process.env.NODE_ENV === "test") {
|
|
24
|
+
hosts.add("botcord-test.local");
|
|
25
|
+
}
|
|
26
|
+
return hosts;
|
|
27
|
+
}
|
|
28
|
+
export function assertSafeBaseUrl(value) {
|
|
29
|
+
if (value === undefined || value === null || value === "") {
|
|
30
|
+
// Caller handles the "no baseUrl supplied → use default" path.
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== "string") {
|
|
34
|
+
throw new UnsafeBaseUrlError("not a string");
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = new URL(value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new UnsafeBaseUrlError("not a valid URL");
|
|
42
|
+
}
|
|
43
|
+
if (parsed.protocol !== "https:") {
|
|
44
|
+
throw new UnsafeBaseUrlError(`scheme "${parsed.protocol}" is not https`);
|
|
45
|
+
}
|
|
46
|
+
const host = parsed.hostname.toLowerCase();
|
|
47
|
+
if (!host) {
|
|
48
|
+
throw new UnsafeBaseUrlError("empty host");
|
|
49
|
+
}
|
|
50
|
+
if (!allowedHosts().has(host)) {
|
|
51
|
+
throw new UnsafeBaseUrlError(`host "${host}" is not in the allowlist`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP plumbing for the iLink WeChat adapter and its login helper.
|
|
3
|
+
*
|
|
4
|
+
* Centralises the four mandatory headers (`AuthorizationType`,
|
|
5
|
+
* `Authorization`, `X-WECHAT-UIN`, `Content-Type`) so the adapter and the
|
|
6
|
+
* login flow can't drift on header shape — the iLink server rejects requests
|
|
7
|
+
* that omit any of them.
|
|
8
|
+
*/
|
|
9
|
+
export type { FetchLike } from "./http-types.js";
|
|
10
|
+
/** `X-WECHAT-UIN: base64(str(random uint32))` — fresh per request, anti-replay. */
|
|
11
|
+
export declare function wechatUinHeader(): string;
|
|
12
|
+
/** Build the canonical iLink request headers. Token is optional for login calls. */
|
|
13
|
+
export declare function wechatHeaders(botToken?: string): Record<string, string>;
|
|
14
|
+
/** iLink `base_info` block required on every authenticated POST body. */
|
|
15
|
+
export declare const WECHAT_CHANNEL_VERSION = "1.0.2";
|
|
16
|
+
export declare const WECHAT_BASE_INFO: {
|
|
17
|
+
readonly channel_version: "1.0.2";
|
|
18
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP plumbing for the iLink WeChat adapter and its login helper.
|
|
3
|
+
*
|
|
4
|
+
* Centralises the four mandatory headers (`AuthorizationType`,
|
|
5
|
+
* `Authorization`, `X-WECHAT-UIN`, `Content-Type`) so the adapter and the
|
|
6
|
+
* login flow can't drift on header shape — the iLink server rejects requests
|
|
7
|
+
* that omit any of them.
|
|
8
|
+
*/
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
/** `X-WECHAT-UIN: base64(str(random uint32))` — fresh per request, anti-replay. */
|
|
11
|
+
export function wechatUinHeader() {
|
|
12
|
+
const n = randomBytes(4).readUInt32BE(0);
|
|
13
|
+
return Buffer.from(String(n), "utf8").toString("base64");
|
|
14
|
+
}
|
|
15
|
+
/** Build the canonical iLink request headers. Token is optional for login calls. */
|
|
16
|
+
export function wechatHeaders(botToken) {
|
|
17
|
+
const h = {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
AuthorizationType: "ilink_bot_token",
|
|
20
|
+
"X-WECHAT-UIN": wechatUinHeader(),
|
|
21
|
+
};
|
|
22
|
+
if (botToken)
|
|
23
|
+
h["Authorization"] = `Bearer ${botToken}`;
|
|
24
|
+
return h;
|
|
25
|
+
}
|
|
26
|
+
/** iLink `base_info` block required on every authenticated POST body. */
|
|
27
|
+
export const WECHAT_CHANNEL_VERSION = "1.0.2";
|
|
28
|
+
export const WECHAT_BASE_INFO = { channel_version: WECHAT_CHANNEL_VERSION };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iLink WeChat QR-code login helpers.
|
|
3
|
+
*
|
|
4
|
+
* Co-located with the channel adapter so the small set of unauthenticated
|
|
5
|
+
* iLink endpoints (`get_bot_qrcode`, `get_qrcode_status`) used during the
|
|
6
|
+
* scan-confirm flow stay alongside the authenticated calls in `wechat.ts`.
|
|
7
|
+
*
|
|
8
|
+
* This module deliberately exports ONLY the two HTTP calls. Login session
|
|
9
|
+
* persistence (mapping `loginId` → `{accountId, gatewayId, botToken, ...}`)
|
|
10
|
+
* is owned by the control-plane layer and is out of scope here.
|
|
11
|
+
*/
|
|
12
|
+
import { type FetchLike } from "./wechat-http.js";
|
|
13
|
+
export declare const DEFAULT_WECHAT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
14
|
+
export interface WechatQrcode {
|
|
15
|
+
qrcode: string;
|
|
16
|
+
qrcodeUrl?: string;
|
|
17
|
+
raw: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface WechatQrcodeStatus {
|
|
20
|
+
status: string;
|
|
21
|
+
botToken?: string;
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
raw: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface WechatLoginOptions {
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
fetchImpl?: FetchLike;
|
|
28
|
+
}
|
|
29
|
+
/** `GET /ilink/bot/get_bot_qrcode?bot_type=3` — fetch a fresh login QR. */
|
|
30
|
+
export declare function getBotQrcode(opts?: WechatLoginOptions): Promise<WechatQrcode>;
|
|
31
|
+
/**
|
|
32
|
+
* `GET /ilink/bot/get_qrcode_status?qrcode=...` — poll for scan/confirm.
|
|
33
|
+
* Caller is responsible for backoff and TTL; this just returns the parsed
|
|
34
|
+
* server response.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getQrcodeStatus(qrcode: string, opts?: WechatLoginOptions): Promise<WechatQrcodeStatus>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iLink WeChat QR-code login helpers.
|
|
3
|
+
*
|
|
4
|
+
* Co-located with the channel adapter so the small set of unauthenticated
|
|
5
|
+
* iLink endpoints (`get_bot_qrcode`, `get_qrcode_status`) used during the
|
|
6
|
+
* scan-confirm flow stay alongside the authenticated calls in `wechat.ts`.
|
|
7
|
+
*
|
|
8
|
+
* This module deliberately exports ONLY the two HTTP calls. Login session
|
|
9
|
+
* persistence (mapping `loginId` → `{accountId, gatewayId, botToken, ...}`)
|
|
10
|
+
* is owned by the control-plane layer and is out of scope here.
|
|
11
|
+
*/
|
|
12
|
+
import { wechatHeaders } from "./wechat-http.js";
|
|
13
|
+
import { assertSafeBaseUrl } from "./url-guard.js";
|
|
14
|
+
export const DEFAULT_WECHAT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
15
|
+
/** `GET /ilink/bot/get_bot_qrcode?bot_type=3` — fetch a fresh login QR. */
|
|
16
|
+
export async function getBotQrcode(opts = {}) {
|
|
17
|
+
// W1: defense-in-depth SSRF guard at the fetch boundary.
|
|
18
|
+
assertSafeBaseUrl(opts.baseUrl);
|
|
19
|
+
const base = (opts.baseUrl ?? DEFAULT_WECHAT_BASE_URL).replace(/\/+$/, "");
|
|
20
|
+
const fetcher = opts.fetchImpl ?? globalThis.fetch;
|
|
21
|
+
const res = await fetcher(`${base}/ilink/bot/get_bot_qrcode?bot_type=3`, {
|
|
22
|
+
method: "GET",
|
|
23
|
+
headers: wechatHeaders(),
|
|
24
|
+
});
|
|
25
|
+
const data = (await safeJson(res)) ?? {};
|
|
26
|
+
const qrcode = typeof data.qrcode === "string" ? data.qrcode : "";
|
|
27
|
+
if (!qrcode) {
|
|
28
|
+
throw new Error(`wechat get_bot_qrcode: missing qrcode in response`);
|
|
29
|
+
}
|
|
30
|
+
const qrcodeUrl = (typeof data.qrcode_url === "string" && data.qrcode_url) ||
|
|
31
|
+
(typeof data.qrcode_img_content === "string" && data.qrcode_img_content) ||
|
|
32
|
+
undefined;
|
|
33
|
+
return { qrcode, qrcodeUrl, raw: data };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `GET /ilink/bot/get_qrcode_status?qrcode=...` — poll for scan/confirm.
|
|
37
|
+
* Caller is responsible for backoff and TTL; this just returns the parsed
|
|
38
|
+
* server response.
|
|
39
|
+
*/
|
|
40
|
+
export async function getQrcodeStatus(qrcode, opts = {}) {
|
|
41
|
+
// W1: defense-in-depth SSRF guard at the fetch boundary.
|
|
42
|
+
assertSafeBaseUrl(opts.baseUrl);
|
|
43
|
+
const base = (opts.baseUrl ?? DEFAULT_WECHAT_BASE_URL).replace(/\/+$/, "");
|
|
44
|
+
const fetcher = opts.fetchImpl ?? globalThis.fetch;
|
|
45
|
+
const res = await fetcher(`${base}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`, { method: "GET", headers: wechatHeaders() });
|
|
46
|
+
const data = (await safeJson(res)) ?? {};
|
|
47
|
+
const status = typeof data.status === "string" ? data.status : "unknown";
|
|
48
|
+
const botToken = typeof data.bot_token === "string" ? data.bot_token : undefined;
|
|
49
|
+
const baseUrl = typeof data.baseurl === "string" ? data.baseurl : undefined;
|
|
50
|
+
return { status, botToken, baseUrl, raw: data };
|
|
51
|
+
}
|
|
52
|
+
async function safeJson(res) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = await res.text();
|
|
55
|
+
if (!raw)
|
|
56
|
+
return null;
|
|
57
|
+
return JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChannelAdapter } from "../types.js";
|
|
2
|
+
import { type FetchLike } from "./wechat-http.js";
|
|
3
|
+
/** Options accepted by {@link createWechatChannel}. */
|
|
4
|
+
export interface WechatChannelOptions {
|
|
5
|
+
id: string;
|
|
6
|
+
accountId: string;
|
|
7
|
+
/** iLink bot token. When omitted, the adapter loads it from the secret-store on start. */
|
|
8
|
+
botToken?: string;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
/** Empty / missing list = default-deny (per security doc §"入站白名单"). */
|
|
11
|
+
allowedSenderIds?: string[];
|
|
12
|
+
splitAt?: number;
|
|
13
|
+
secretFile?: string;
|
|
14
|
+
stateFile?: string;
|
|
15
|
+
/** Test hook: override `globalThis.fetch`. */
|
|
16
|
+
fetchImpl?: FetchLike;
|
|
17
|
+
/** Test hook: synchronous state writes (`debounceMs: 0`). */
|
|
18
|
+
stateDebounceMs?: number;
|
|
19
|
+
/** Test hook: override Date.now() for trace cache TTL assertions. */
|
|
20
|
+
now?: () => number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* WeChat (iLink Bot API) channel adapter.
|
|
24
|
+
*
|
|
25
|
+
* - long-polls `POST /ilink/bot/getupdates` (cursor = `get_updates_buf`,
|
|
26
|
+
* persisted via state-store)
|
|
27
|
+
* - normalizes `message_type === 1` text into a `GatewayInboundEnvelope`
|
|
28
|
+
* with `conversation.id = "wechat:user:${fromUserId}"` and trace id
|
|
29
|
+
* `wechat:${fromUserId}:${receivedAt}` (or `client_id` when present)
|
|
30
|
+
* - per-trace cache binds `traceId → context_token`; `send()` looks up by
|
|
31
|
+
* `GatewayOutboundMessage.traceId` and rejects if missing/expired
|
|
32
|
+
* (no conversation-level fallback — see doc §"WeChat channel adapter")
|
|
33
|
+
* - `send()` splits long replies at `splitAt` (default 1800), preferring
|
|
34
|
+
* newline boundaries; `typing()` caches the per-user `typing_ticket`
|
|
35
|
+
* fetched via `getconfig`.
|
|
36
|
+
*
|
|
37
|
+
* Allowlist is default-deny: an empty (or missing) `allowedSenderIds` rejects
|
|
38
|
+
* every inbound message.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createWechatChannel(opts: WechatChannelOptions): ChannelAdapter;
|