gewe-openclaw 2026.1.29

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/src/channel.ts ADDED
@@ -0,0 +1,465 @@
1
+ import {
2
+ applyAccountNameToChannelSection,
3
+ buildChannelConfigSchema,
4
+ DEFAULT_ACCOUNT_ID,
5
+ deleteAccountFromConfigSection,
6
+ formatPairingApproveHint,
7
+ missingTargetError,
8
+ normalizeAccountId,
9
+ PAIRING_APPROVED_MESSAGE,
10
+ setAccountEnabledInConfigSection,
11
+ type ChannelPlugin,
12
+ type OpenClawConfig,
13
+ type ChannelSetupInput,
14
+ } from "openclaw/plugin-sdk";
15
+
16
+ import { resolveGeweAccount, resolveDefaultGeweAccountId, listGeweAccountIds } from "./accounts.js";
17
+ import { GeweConfigSchema } from "./config-schema.js";
18
+ import { deliverGewePayload } from "./delivery.js";
19
+ import { monitorGeweProvider } from "./monitor.js";
20
+ import { looksLikeGeweTargetId, normalizeGeweMessagingTarget } from "./normalize.js";
21
+ import { resolveGeweGroupToolPolicy, resolveGeweRequireMention } from "./policy.js";
22
+ import { getGeweRuntime } from "./runtime.js";
23
+ import { sendTextGewe } from "./send.js";
24
+ import type { CoreConfig, ResolvedGeweAccount } from "./types.js";
25
+
26
+ const meta = {
27
+ id: "gewe",
28
+ label: "GeWe",
29
+ selectionLabel: "WeChat (GeWe)",
30
+ detailLabel: "WeChat (GeWe)",
31
+ docsPath: "/channels/gewe",
32
+ docsLabel: "gewe",
33
+ blurb: "WeChat channel via GeWe API and webhook callbacks.",
34
+ aliases: ["wechat", "wx", "gewe"],
35
+ order: 72,
36
+ quickstartAllowFrom: true,
37
+ };
38
+
39
+ type GeweSetupInput = ChannelSetupInput & {
40
+ token?: string;
41
+ tokenFile?: string;
42
+ appId?: string;
43
+ appIdFile?: string;
44
+ apiBaseUrl?: string;
45
+ };
46
+
47
+ export const gewePlugin: ChannelPlugin<ResolvedGeweAccount> = {
48
+ id: "gewe",
49
+ meta,
50
+ pairing: {
51
+ idLabel: "wechatUserId",
52
+ normalizeAllowEntry: (entry) => entry.replace(/^(gewe|wechat|wx):/i, ""),
53
+ notifyApproval: async ({ cfg, id }) => {
54
+ const account = resolveGeweAccount({ cfg: cfg as CoreConfig });
55
+ if (!account.token || !account.appId) {
56
+ throw new Error("GeWe token/appId not configured");
57
+ }
58
+ await sendTextGewe({
59
+ account,
60
+ toWxid: id,
61
+ content: PAIRING_APPROVED_MESSAGE,
62
+ });
63
+ },
64
+ },
65
+ capabilities: {
66
+ chatTypes: ["direct", "group"],
67
+ reactions: false,
68
+ threads: false,
69
+ media: true,
70
+ nativeCommands: false,
71
+ blockStreaming: true,
72
+ },
73
+ reload: { configPrefixes: ["channels.gewe"] },
74
+ configSchema: buildChannelConfigSchema(GeweConfigSchema),
75
+ config: {
76
+ listAccountIds: (cfg) => listGeweAccountIds(cfg as CoreConfig),
77
+ resolveAccount: (cfg, accountId) => resolveGeweAccount({ cfg: cfg as CoreConfig, accountId }),
78
+ defaultAccountId: (cfg) => resolveDefaultGeweAccountId(cfg as CoreConfig),
79
+ setAccountEnabled: ({ cfg, accountId, enabled }) =>
80
+ setAccountEnabledInConfigSection({
81
+ cfg,
82
+ sectionKey: "gewe",
83
+ accountId,
84
+ enabled,
85
+ allowTopLevel: true,
86
+ }),
87
+ deleteAccount: ({ cfg, accountId }) =>
88
+ deleteAccountFromConfigSection({
89
+ cfg,
90
+ sectionKey: "gewe",
91
+ accountId,
92
+ clearBaseFields: ["token", "tokenFile", "appId", "appIdFile", "name"],
93
+ }),
94
+ isConfigured: (account) => Boolean(account.token?.trim() && account.appId?.trim()),
95
+ describeAccount: (account) => ({
96
+ accountId: account.accountId,
97
+ name: account.name,
98
+ enabled: account.enabled,
99
+ configured: Boolean(account.token?.trim() && account.appId?.trim()),
100
+ tokenSource: account.tokenSource,
101
+ baseUrl: account.config.apiBaseUrl ? "[set]" : "[missing]",
102
+ }),
103
+ resolveAllowFrom: ({ cfg, accountId }) =>
104
+ (resolveGeweAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map(
105
+ (entry) => String(entry),
106
+ ),
107
+ formatAllowFrom: ({ allowFrom }) =>
108
+ allowFrom
109
+ .map((entry) => String(entry).trim())
110
+ .filter(Boolean)
111
+ .map((entry) => entry.replace(/^(gewe|wechat|wx):/i, "")),
112
+ },
113
+ security: {
114
+ resolveDmPolicy: ({ cfg, accountId, account }) => {
115
+ const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
116
+ const useAccountPath = Boolean(
117
+ cfg.channels?.gewe?.accounts?.[resolvedAccountId],
118
+ );
119
+ const basePath = useAccountPath
120
+ ? `channels.gewe.accounts.${resolvedAccountId}.`
121
+ : "channels.gewe.";
122
+ return {
123
+ policy: account.config.dmPolicy ?? "pairing",
124
+ allowFrom: account.config.allowFrom ?? [],
125
+ policyPath: `${basePath}dmPolicy`,
126
+ allowFromPath: basePath,
127
+ approveHint: formatPairingApproveHint("gewe"),
128
+ normalizeEntry: (raw) => raw.replace(/^(gewe|wechat|wx):/i, ""),
129
+ };
130
+ },
131
+ collectWarnings: ({ account, cfg }) => {
132
+ const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
133
+ const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
134
+ if (groupPolicy !== "open") return [];
135
+ const groupAllowlistConfigured =
136
+ account.config.groups && Object.keys(account.config.groups).length > 0;
137
+ if (groupAllowlistConfigured) {
138
+ return [
139
+ `- GeWe groups: groupPolicy="open" allows any member in allowed groups to trigger (mention-gated). Set channels.gewe.groupPolicy="allowlist" + channels.gewe.groupAllowFrom to restrict senders.`,
140
+ ];
141
+ }
142
+ return [
143
+ `- GeWe groups: groupPolicy="open" with no channels.gewe.groups allowlist; any group can add + ping (mention-gated). Set channels.gewe.groupPolicy="allowlist" + channels.gewe.groupAllowFrom or configure channels.gewe.groups.`,
144
+ ];
145
+ },
146
+ },
147
+ groups: {
148
+ resolveRequireMention: ({ cfg, groupId, accountId }) => {
149
+ const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
150
+ const groups = account.config.groups;
151
+ if (!groups || !groupId) return true;
152
+ const groupConfig = groups[groupId] ?? groups["*"];
153
+ return resolveGeweRequireMention({
154
+ groupConfig,
155
+ wildcardConfig: groups["*"],
156
+ });
157
+ },
158
+ resolveToolPolicy: resolveGeweGroupToolPolicy,
159
+ },
160
+ messaging: {
161
+ normalizeTarget: normalizeGeweMessagingTarget,
162
+ targetResolver: {
163
+ looksLikeId: looksLikeGeweTargetId,
164
+ hint: "<wxid|@chatroom>",
165
+ },
166
+ },
167
+ outbound: {
168
+ deliveryMode: "direct",
169
+ chunker: (text, limit) => {
170
+ const core = getGeweRuntime();
171
+ return core.channel.text.chunkMarkdownText(text, limit);
172
+ },
173
+ chunkerMode: "markdown",
174
+ resolveTarget: ({ to, allowFrom, mode }) => {
175
+ const trimmed = to?.trim() ?? "";
176
+ const allowListRaw = (allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
177
+ const allowList = allowListRaw
178
+ .filter((entry) => entry !== "*")
179
+ .map((entry) => normalizeGeweMessagingTarget(entry))
180
+ .filter((entry): entry is string => Boolean(entry));
181
+
182
+ if (trimmed) {
183
+ const normalized = normalizeGeweMessagingTarget(trimmed);
184
+ if (!normalized) {
185
+ if ((mode === "implicit" || mode === "heartbeat") && allowList.length > 0) {
186
+ return { ok: true, to: allowList[0] };
187
+ }
188
+ return {
189
+ ok: false,
190
+ error: missingTargetError("GeWe", "<wxid|@chatroom> or channels.gewe.allowFrom[0]"),
191
+ };
192
+ }
193
+ return { ok: true, to: normalized };
194
+ }
195
+
196
+ if (allowList.length > 0) {
197
+ return { ok: true, to: allowList[0] };
198
+ }
199
+ return {
200
+ ok: false,
201
+ error: missingTargetError("GeWe", "<wxid|@chatroom> or channels.gewe.allowFrom[0]"),
202
+ };
203
+ },
204
+ sendPayload: async ({ payload, cfg, to, accountId }) => {
205
+ const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
206
+ const result = await deliverGewePayload({
207
+ payload,
208
+ account,
209
+ cfg: cfg as OpenClawConfig,
210
+ toWxid: to,
211
+ });
212
+ return {
213
+ channel: "gewe",
214
+ messageId: result?.messageId ?? "ok",
215
+ timestamp: result?.timestamp,
216
+ meta: { newMessageId: result?.newMessageId },
217
+ };
218
+ },
219
+ sendText: async ({ cfg, to, text, accountId }) => {
220
+ const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
221
+ const result = await deliverGewePayload({
222
+ payload: { text },
223
+ account,
224
+ cfg: cfg as OpenClawConfig,
225
+ toWxid: to,
226
+ });
227
+ return {
228
+ channel: "gewe",
229
+ messageId: result?.messageId ?? "ok",
230
+ timestamp: result?.timestamp,
231
+ meta: { newMessageId: result?.newMessageId },
232
+ };
233
+ },
234
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
235
+ const account = resolveGeweAccount({ cfg: cfg as CoreConfig, accountId });
236
+ const result = await deliverGewePayload({
237
+ payload: { text, mediaUrl },
238
+ account,
239
+ cfg: cfg as OpenClawConfig,
240
+ toWxid: to,
241
+ });
242
+ return {
243
+ channel: "gewe",
244
+ messageId: result?.messageId ?? "ok",
245
+ timestamp: result?.timestamp,
246
+ meta: { newMessageId: result?.newMessageId },
247
+ };
248
+ },
249
+ },
250
+ status: {
251
+ defaultRuntime: {
252
+ accountId: DEFAULT_ACCOUNT_ID,
253
+ running: false,
254
+ lastStartAt: null,
255
+ lastStopAt: null,
256
+ lastError: null,
257
+ },
258
+ buildChannelSummary: ({ snapshot }) => ({
259
+ configured: snapshot.configured ?? false,
260
+ tokenSource: snapshot.tokenSource ?? "none",
261
+ running: snapshot.running ?? false,
262
+ mode: snapshot.mode ?? null,
263
+ lastStartAt: snapshot.lastStartAt ?? null,
264
+ lastStopAt: snapshot.lastStopAt ?? null,
265
+ lastError: snapshot.lastError ?? null,
266
+ lastInboundAt: snapshot.lastInboundAt ?? null,
267
+ lastOutboundAt: snapshot.lastOutboundAt ?? null,
268
+ }),
269
+ buildAccountSnapshot: ({ account, runtime }) => {
270
+ const configured = Boolean(account.token?.trim() && account.appId?.trim());
271
+ return {
272
+ accountId: account.accountId,
273
+ name: account.name,
274
+ enabled: account.enabled,
275
+ configured,
276
+ tokenSource: account.tokenSource,
277
+ baseUrl: account.config.apiBaseUrl ? "[set]" : "[missing]",
278
+ running: runtime?.running ?? false,
279
+ lastStartAt: runtime?.lastStartAt ?? null,
280
+ lastStopAt: runtime?.lastStopAt ?? null,
281
+ lastError: runtime?.lastError ?? null,
282
+ mode: "webhook",
283
+ lastInboundAt: runtime?.lastInboundAt ?? null,
284
+ lastOutboundAt: runtime?.lastOutboundAt ?? null,
285
+ dmPolicy: account.config.dmPolicy ?? "pairing",
286
+ };
287
+ },
288
+ },
289
+ gateway: {
290
+ startAccount: async (ctx) => {
291
+ const account = ctx.account;
292
+ if (!account.token || !account.appId) {
293
+ throw new Error(
294
+ `GeWe not configured for account "${account.accountId}" (missing token/appId)`,
295
+ );
296
+ }
297
+ ctx.log?.info(`[${account.accountId}] starting GeWe webhook server`);
298
+ const { stop } = await monitorGeweProvider({
299
+ accountId: account.accountId,
300
+ account,
301
+ config: ctx.cfg as CoreConfig,
302
+ runtime: ctx.runtime,
303
+ abortSignal: ctx.abortSignal,
304
+ statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
305
+ });
306
+ return { stop };
307
+ },
308
+ logoutAccount: async ({ accountId, cfg }) => {
309
+ const nextCfg = { ...cfg } as OpenClawConfig;
310
+ const nextSection = cfg.channels?.gewe ? { ...cfg.channels.gewe } : undefined;
311
+ let cleared = false;
312
+ let changed = false;
313
+
314
+ if (nextSection) {
315
+ if (accountId === DEFAULT_ACCOUNT_ID) {
316
+ if (nextSection.token) {
317
+ delete nextSection.token;
318
+ cleared = true;
319
+ changed = true;
320
+ }
321
+ if (nextSection.tokenFile) {
322
+ delete nextSection.tokenFile;
323
+ cleared = true;
324
+ changed = true;
325
+ }
326
+ if (nextSection.appId) {
327
+ delete nextSection.appId;
328
+ cleared = true;
329
+ changed = true;
330
+ }
331
+ if (nextSection.appIdFile) {
332
+ delete nextSection.appIdFile;
333
+ cleared = true;
334
+ changed = true;
335
+ }
336
+ }
337
+
338
+ const accounts =
339
+ nextSection.accounts && typeof nextSection.accounts === "object"
340
+ ? { ...nextSection.accounts }
341
+ : undefined;
342
+ if (accounts && accountId in accounts) {
343
+ const entry = accounts[accountId];
344
+ if (entry && typeof entry === "object") {
345
+ const nextEntry = { ...entry } as Record<string, unknown>;
346
+ if ("token" in nextEntry) {
347
+ if (nextEntry.token) cleared = true;
348
+ delete nextEntry.token;
349
+ changed = true;
350
+ }
351
+ if ("tokenFile" in nextEntry) {
352
+ if (nextEntry.tokenFile) cleared = true;
353
+ delete nextEntry.tokenFile;
354
+ changed = true;
355
+ }
356
+ if ("appId" in nextEntry) {
357
+ if (nextEntry.appId) cleared = true;
358
+ delete nextEntry.appId;
359
+ changed = true;
360
+ }
361
+ if ("appIdFile" in nextEntry) {
362
+ if (nextEntry.appIdFile) cleared = true;
363
+ delete nextEntry.appIdFile;
364
+ changed = true;
365
+ }
366
+ if (Object.keys(nextEntry).length === 0) {
367
+ delete accounts[accountId];
368
+ changed = true;
369
+ } else {
370
+ accounts[accountId] = nextEntry as typeof entry;
371
+ }
372
+ }
373
+ }
374
+
375
+ nextSection.accounts =
376
+ accounts && Object.keys(accounts).length > 0 ? accounts : undefined;
377
+ if (changed) {
378
+ nextCfg.channels = {
379
+ ...nextCfg.channels,
380
+ gewe: nextSection,
381
+ };
382
+ }
383
+ }
384
+
385
+ return { cleared, loggedOut: cleared, nextCfg };
386
+ },
387
+ },
388
+ setup: {
389
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
390
+ applyAccountName: ({ cfg, accountId, name }) =>
391
+ applyAccountNameToChannelSection({
392
+ cfg: cfg as OpenClawConfig,
393
+ channelKey: "gewe",
394
+ accountId,
395
+ name,
396
+ }),
397
+ validateInput: ({ accountId, input }) => {
398
+ const setupInput = input as GeweSetupInput;
399
+ if (setupInput.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
400
+ return "GEWE_TOKEN/GEWE_APP_ID can only be used for the default account.";
401
+ }
402
+ if (!setupInput.useEnv && !setupInput.token && !setupInput.tokenFile) {
403
+ return "GeWe requires --token or --token-file (or --use-env).";
404
+ }
405
+ if (!setupInput.useEnv && !setupInput.appId && !setupInput.appIdFile) {
406
+ return "GeWe requires --app-id or --app-id-file (or --use-env).";
407
+ }
408
+ return null;
409
+ },
410
+ applyAccountConfig: ({ cfg, accountId, input }) => {
411
+ const setupInput = input as GeweSetupInput;
412
+ const namedConfig = applyAccountNameToChannelSection({
413
+ cfg: cfg as OpenClawConfig,
414
+ channelKey: "gewe",
415
+ accountId,
416
+ name: setupInput.name,
417
+ });
418
+ const section = (namedConfig.channels?.gewe ?? {}) as Record<string, unknown>;
419
+ const useAccountPath = accountId !== DEFAULT_ACCOUNT_ID;
420
+ const base = useAccountPath
421
+ ? (section.accounts?.[accountId] as Record<string, unknown> | undefined) ?? {}
422
+ : section;
423
+ const nextEntry = {
424
+ ...base,
425
+ ...(setupInput.apiBaseUrl ? { apiBaseUrl: setupInput.apiBaseUrl } : {}),
426
+ ...(setupInput.useEnv
427
+ ? {}
428
+ : setupInput.token
429
+ ? { token: setupInput.token }
430
+ : setupInput.tokenFile
431
+ ? { tokenFile: setupInput.tokenFile }
432
+ : {}),
433
+ ...(setupInput.useEnv
434
+ ? {}
435
+ : setupInput.appId
436
+ ? { appId: setupInput.appId }
437
+ : setupInput.appIdFile
438
+ ? { appIdFile: setupInput.appIdFile }
439
+ : {}),
440
+ };
441
+ if (!useAccountPath) {
442
+ return {
443
+ ...namedConfig,
444
+ channels: {
445
+ ...namedConfig.channels,
446
+ gewe: nextEntry,
447
+ },
448
+ };
449
+ }
450
+ return {
451
+ ...namedConfig,
452
+ channels: {
453
+ ...namedConfig.channels,
454
+ gewe: {
455
+ ...section,
456
+ accounts: {
457
+ ...(section.accounts as Record<string, unknown> | undefined),
458
+ [accountId]: nextEntry,
459
+ },
460
+ },
461
+ },
462
+ };
463
+ },
464
+ },
465
+ };
@@ -0,0 +1,105 @@
1
+ import {
2
+ BlockStreamingCoalesceSchema,
3
+ DmConfigSchema,
4
+ DmPolicySchema,
5
+ GroupPolicySchema,
6
+ MarkdownConfigSchema,
7
+ ToolPolicySchema,
8
+ requireOpenAllowFrom,
9
+ } from "openclaw/plugin-sdk";
10
+ import { z } from "zod";
11
+
12
+ export const GeweGroupSchema = z
13
+ .object({
14
+ requireMention: z.boolean().optional(),
15
+ tools: ToolPolicySchema,
16
+ skills: z.array(z.string()).optional(),
17
+ enabled: z.boolean().optional(),
18
+ allowFrom: z.array(z.string()).optional(),
19
+ systemPrompt: z.string().optional(),
20
+ })
21
+ .strict();
22
+
23
+ export const GeweAccountSchemaBase = z
24
+ .object({
25
+ name: z.string().optional(),
26
+ enabled: z.boolean().optional(),
27
+ markdown: MarkdownConfigSchema,
28
+ apiBaseUrl: z.string().optional(),
29
+ token: z.string().optional(),
30
+ tokenFile: z.string().optional(),
31
+ appId: z.string().optional(),
32
+ appIdFile: z.string().optional(),
33
+ webhookPort: z.number().int().positive().optional(),
34
+ webhookHost: z.string().optional(),
35
+ webhookPath: z.string().optional(),
36
+ webhookSecret: z.string().optional(),
37
+ webhookPublicUrl: z.string().optional(),
38
+ mediaPort: z.number().int().positive().optional(),
39
+ mediaHost: z.string().optional(),
40
+ mediaPath: z.string().optional(),
41
+ mediaPublicUrl: z.string().optional(),
42
+ mediaMaxMb: z.number().positive().optional(),
43
+ voiceAutoConvert: z.boolean().optional(),
44
+ voiceFfmpegPath: z.string().optional(),
45
+ voiceSilkPath: z.string().optional(),
46
+ voiceSilkArgs: z.array(z.string()).optional(),
47
+ voiceSampleRate: z.number().int().positive().optional(),
48
+ voiceDecodePath: z.string().optional(),
49
+ voiceDecodeArgs: z.array(z.string()).optional(),
50
+ voiceDecodeSampleRate: z.number().int().positive().optional(),
51
+ voiceDecodeOutput: z.enum(["pcm", "wav"]).optional(),
52
+ videoFfmpegPath: z.string().optional(),
53
+ videoFfprobePath: z.string().optional(),
54
+ videoThumbUrl: z.string().optional(),
55
+ downloadMinDelayMs: z.number().int().min(0).optional(),
56
+ downloadMaxDelayMs: z.number().int().min(0).optional(),
57
+ dmPolicy: DmPolicySchema.optional().default("pairing"),
58
+ allowFrom: z.array(z.string()).optional(),
59
+ groupAllowFrom: z.array(z.string()).optional(),
60
+ groupPolicy: GroupPolicySchema.optional().default("allowlist"),
61
+ groups: z.record(z.string(), GeweGroupSchema.optional()).optional(),
62
+ historyLimit: z.number().int().min(0).optional(),
63
+ dmHistoryLimit: z.number().int().min(0).optional(),
64
+ dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
65
+ textChunkLimit: z.number().int().positive().optional(),
66
+ chunkMode: z.enum(["length", "newline"]).optional(),
67
+ blockStreaming: z.boolean().optional(),
68
+ blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
69
+ })
70
+ .strict()
71
+ .superRefine((value, ctx) => {
72
+ const min = value.downloadMinDelayMs;
73
+ const max = value.downloadMaxDelayMs;
74
+ if (typeof min === "number" && typeof max === "number" && min > max) {
75
+ ctx.addIssue({
76
+ code: z.ZodIssueCode.custom,
77
+ path: ["downloadMaxDelayMs"],
78
+ message: "downloadMaxDelayMs must be >= downloadMinDelayMs",
79
+ });
80
+ }
81
+ });
82
+
83
+ export const GeweAccountSchema = GeweAccountSchemaBase.superRefine((value, ctx) => {
84
+ requireOpenAllowFrom({
85
+ policy: value.dmPolicy,
86
+ allowFrom: value.allowFrom,
87
+ ctx,
88
+ path: ["allowFrom"],
89
+ message:
90
+ 'channels.gewe.dmPolicy="open" requires channels.gewe.allowFrom to include "*"',
91
+ });
92
+ });
93
+
94
+ export const GeweConfigSchema = GeweAccountSchemaBase.extend({
95
+ accounts: z.record(z.string(), GeweAccountSchema.optional()).optional(),
96
+ }).superRefine((value, ctx) => {
97
+ requireOpenAllowFrom({
98
+ policy: value.dmPolicy,
99
+ allowFrom: value.allowFrom,
100
+ ctx,
101
+ path: ["allowFrom"],
102
+ message:
103
+ 'channels.gewe.dmPolicy="open" requires channels.gewe.allowFrom to include "*"',
104
+ });
105
+ });