@openclaw/zalo 2026.2.25 → 2026.3.2
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/CHANGELOG.md +18 -0
- package/index.ts +0 -2
- package/package.json +1 -1
- package/src/accounts.ts +13 -3
- package/src/channel.sendpayload.test.ts +102 -0
- package/src/channel.ts +36 -1
- package/src/config-schema.test.ts +30 -0
- package/src/config-schema.ts +3 -2
- package/src/monitor.ts +98 -78
- package/src/monitor.webhook.test.ts +183 -5
- package/src/monitor.webhook.ts +80 -71
- package/src/onboarding.status.test.ts +24 -0
- package/src/onboarding.ts +89 -64
- package/src/secret-input.ts +19 -0
- package/src/token.test.ts +58 -0
- package/src/token.ts +50 -9
- package/src/types.ts +4 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasConfiguredSecretInput,
|
|
3
|
+
normalizeResolvedSecretInputString,
|
|
4
|
+
normalizeSecretInputString,
|
|
5
|
+
} from "openclaw/plugin-sdk";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString };
|
|
9
|
+
|
|
10
|
+
export function buildSecretInputSchema() {
|
|
11
|
+
return z.union([
|
|
12
|
+
z.string(),
|
|
13
|
+
z.object({
|
|
14
|
+
source: z.enum(["env", "file", "exec"]),
|
|
15
|
+
provider: z.string().min(1),
|
|
16
|
+
id: z.string().min(1),
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveZaloToken } from "./token.js";
|
|
3
|
+
import type { ZaloConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
describe("resolveZaloToken", () => {
|
|
6
|
+
it("falls back to top-level token for non-default accounts without overrides", () => {
|
|
7
|
+
const cfg = {
|
|
8
|
+
botToken: "top-level-token",
|
|
9
|
+
accounts: {
|
|
10
|
+
work: {},
|
|
11
|
+
},
|
|
12
|
+
} as ZaloConfig;
|
|
13
|
+
const res = resolveZaloToken(cfg, "work");
|
|
14
|
+
expect(res.token).toBe("top-level-token");
|
|
15
|
+
expect(res.source).toBe("config");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("uses accounts.default botToken for default account when configured", () => {
|
|
19
|
+
const cfg = {
|
|
20
|
+
botToken: "top-level-token",
|
|
21
|
+
accounts: {
|
|
22
|
+
default: {
|
|
23
|
+
botToken: "default-account-token",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
} as ZaloConfig;
|
|
27
|
+
const res = resolveZaloToken(cfg, "default");
|
|
28
|
+
expect(res.token).toBe("default-account-token");
|
|
29
|
+
expect(res.source).toBe("config");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("does not inherit top-level token when account token is explicitly blank", () => {
|
|
33
|
+
const cfg = {
|
|
34
|
+
botToken: "top-level-token",
|
|
35
|
+
accounts: {
|
|
36
|
+
work: {
|
|
37
|
+
botToken: "",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
} as ZaloConfig;
|
|
41
|
+
const res = resolveZaloToken(cfg, "work");
|
|
42
|
+
expect(res.token).toBe("");
|
|
43
|
+
expect(res.source).toBe("none");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("resolves account token when account key casing differs from normalized id", () => {
|
|
47
|
+
const cfg = {
|
|
48
|
+
accounts: {
|
|
49
|
+
Work: {
|
|
50
|
+
botToken: "work-token",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
} as ZaloConfig;
|
|
54
|
+
const res = resolveZaloToken(cfg, "work");
|
|
55
|
+
expect(res.token).toBe("work-token");
|
|
56
|
+
expect(res.source).toBe("config");
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/token.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import type { BaseTokenResolution } from "openclaw/plugin-sdk";
|
|
3
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
4
|
+
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
|
|
3
5
|
import type { ZaloConfig } from "./types.js";
|
|
4
6
|
|
|
5
7
|
export type ZaloTokenResolution = BaseTokenResolution & {
|
|
@@ -9,17 +11,36 @@ export type ZaloTokenResolution = BaseTokenResolution & {
|
|
|
9
11
|
export function resolveZaloToken(
|
|
10
12
|
config: ZaloConfig | undefined,
|
|
11
13
|
accountId?: string | null,
|
|
14
|
+
options?: { allowUnresolvedSecretRef?: boolean },
|
|
12
15
|
): ZaloTokenResolution {
|
|
13
16
|
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
14
17
|
const isDefaultAccount = resolvedAccountId === DEFAULT_ACCOUNT_ID;
|
|
15
18
|
const baseConfig = config;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const resolveAccountConfig = (id: string): ZaloConfig | undefined => {
|
|
20
|
+
const accounts = baseConfig?.accounts;
|
|
21
|
+
if (!accounts || typeof accounts !== "object") {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const direct = accounts[id] as ZaloConfig | undefined;
|
|
25
|
+
if (direct) {
|
|
26
|
+
return direct;
|
|
27
|
+
}
|
|
28
|
+
const normalized = normalizeAccountId(id);
|
|
29
|
+
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
|
30
|
+
return matchKey ? ((accounts as Record<string, ZaloConfig>)[matchKey] ?? undefined) : undefined;
|
|
31
|
+
};
|
|
32
|
+
const accountConfig = resolveAccountConfig(resolvedAccountId);
|
|
33
|
+
const accountHasBotToken = Boolean(
|
|
34
|
+
accountConfig && Object.prototype.hasOwnProperty.call(accountConfig, "botToken"),
|
|
35
|
+
);
|
|
20
36
|
|
|
21
|
-
if (accountConfig) {
|
|
22
|
-
const token =
|
|
37
|
+
if (accountConfig && accountHasBotToken) {
|
|
38
|
+
const token = options?.allowUnresolvedSecretRef
|
|
39
|
+
? normalizeSecretInputString(accountConfig.botToken)
|
|
40
|
+
: normalizeResolvedSecretInputString({
|
|
41
|
+
value: accountConfig.botToken,
|
|
42
|
+
path: `channels.zalo.accounts.${resolvedAccountId}.botToken`,
|
|
43
|
+
});
|
|
23
44
|
if (token) {
|
|
24
45
|
return { token, source: "config" };
|
|
25
46
|
}
|
|
@@ -36,8 +57,25 @@ export function resolveZaloToken(
|
|
|
36
57
|
}
|
|
37
58
|
}
|
|
38
59
|
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
const accountTokenFile = accountConfig?.tokenFile?.trim();
|
|
61
|
+
if (!accountHasBotToken && accountTokenFile) {
|
|
62
|
+
try {
|
|
63
|
+
const fileToken = readFileSync(accountTokenFile, "utf8").trim();
|
|
64
|
+
if (fileToken) {
|
|
65
|
+
return { token: fileToken, source: "configFile" };
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore read failures
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!accountHasBotToken) {
|
|
73
|
+
const token = options?.allowUnresolvedSecretRef
|
|
74
|
+
? normalizeSecretInputString(baseConfig?.botToken)
|
|
75
|
+
: normalizeResolvedSecretInputString({
|
|
76
|
+
value: baseConfig?.botToken,
|
|
77
|
+
path: "channels.zalo.botToken",
|
|
78
|
+
});
|
|
41
79
|
if (token) {
|
|
42
80
|
return { token, source: "config" };
|
|
43
81
|
}
|
|
@@ -52,6 +90,9 @@ export function resolveZaloToken(
|
|
|
52
90
|
// ignore read failures
|
|
53
91
|
}
|
|
54
92
|
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isDefaultAccount) {
|
|
55
96
|
const envToken = process.env.ZALO_BOT_TOKEN?.trim();
|
|
56
97
|
if (envToken) {
|
|
57
98
|
return { token: envToken, source: "env" };
|
package/src/types.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import type { SecretInput } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
1
3
|
export type ZaloAccountConfig = {
|
|
2
4
|
/** Optional display name for this account (used in CLI/UI lists). */
|
|
3
5
|
name?: string;
|
|
4
6
|
/** If false, do not start this Zalo account. Default: true. */
|
|
5
7
|
enabled?: boolean;
|
|
6
8
|
/** Bot token from Zalo Bot Creator. */
|
|
7
|
-
botToken?:
|
|
9
|
+
botToken?: SecretInput;
|
|
8
10
|
/** Path to file containing the bot token. */
|
|
9
11
|
tokenFile?: string;
|
|
10
12
|
/** Webhook URL for receiving updates (HTTPS required). */
|
|
11
13
|
webhookUrl?: string;
|
|
12
14
|
/** Webhook secret token (8-256 chars) for request verification. */
|
|
13
|
-
webhookSecret?:
|
|
15
|
+
webhookSecret?: SecretInput;
|
|
14
16
|
/** Webhook path for the gateway HTTP server (defaults to webhook URL path). */
|
|
15
17
|
webhookPath?: string;
|
|
16
18
|
/** Direct message access policy (default: pairing). */
|