@openclaw/zalo 2026.7.2-beta.3 → 2026.7.2-beta.4

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,4 +1,4 @@
1
- import { t as sendMessageZalo } from "./send-9sVVxyap.js";
1
+ import { t as sendMessageZalo } from "./send-WPSy5EgF.js";
2
2
  //#region extensions/zalo/src/actions.runtime.ts
3
3
  const zaloActionsRuntime = { sendMessageZalo };
4
4
  //#endregion
package/dist/api.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-fwCh0QUi.js";
2
- import { t as zaloPlugin } from "./channel-DikWJ2TC.js";
1
+ import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-BBA-V-j0.js";
2
+ import { t as zaloPlugin } from "./channel-CvZItLo-.js";
3
3
  import { n as resolveZaloRuntimeGroupPolicy } from "./group-access-8qHRzDHx.js";
4
4
  import { zaloSetupWizard } from "./setup-api.js";
5
5
  export { createZaloSetupWizardProxy, resolveZaloRuntimeGroupPolicy, zaloDmPolicy, zaloPlugin, zaloSetupAdapter, zaloSetupWizard };
@@ -1,4 +1,4 @@
1
- import { c as listZaloAccountIds, f as buildSecretInputSchema, l as resolveDefaultZaloAccountId, r as zaloSetupAdapter, s as listEnabledZaloAccounts, t as createZaloSetupWizardProxy, u as resolveZaloAccount } from "./setup-core-fwCh0QUi.js";
1
+ import { c as listEnabledZaloAccounts, d as resolveZaloAccount, i as zaloSetupContract, l as listZaloAccountIds, p as buildSecretInputSchema, r as zaloSetupAdapter, t as createZaloSetupWizardProxy, u as resolveDefaultZaloAccountId } from "./setup-core-BBA-V-j0.js";
2
2
  import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-Bxvjh0Hq.js";
3
3
  import { describeWebhookAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
4
4
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
@@ -14,7 +14,7 @@ import { createChannelDirectoryAdapter, listResolvedDirectoryUserEntriesFromAllo
14
14
  import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
15
15
  import { sendPayloadWithChunkedTextAndMedia } from "openclaw/plugin-sdk/reply-payload";
16
16
  import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, standardDmPolicyOpenIssue } from "openclaw/plugin-sdk/status-helpers";
17
- import { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
17
+ import { chunkTextForOutbound, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
18
18
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
19
19
  import { buildChannelOutboundSessionRoute, stripChannelTargetPrefix, stripTargetKindPrefix } from "openclaw/plugin-sdk/core";
20
20
  import { jsonResult, readStringParam } from "openclaw/plugin-sdk/channel-actions";
@@ -24,7 +24,7 @@ import { AllowFromListSchema, DmPolicySchema, GroupPolicySchema, MarkdownConfigS
24
24
  import { z } from "zod";
25
25
  import { coerceStatusIssueAccountId, readStatusIssueFields } from "openclaw/plugin-sdk/extension-shared";
26
26
  //#region extensions/zalo/src/actions.ts
27
- const loadZaloActionsRuntime = createLazyRuntimeNamedExport(() => import("./actions.runtime-OmfJFYZn.js"), "zaloActionsRuntime");
27
+ const loadZaloActionsRuntime = createLazyRuntimeNamedExport(() => import("./actions.runtime-DwjVG1H3.js"), "zaloActionsRuntime");
28
28
  const providerId = "zalo";
29
29
  const ZALO_ACTIONS = /* @__PURE__ */ new Set(["send"]);
30
30
  function listEnabledAccounts(cfg, accountId) {
@@ -176,8 +176,8 @@ function looksLikeZaloChatId(raw, normalized) {
176
176
  const target = normalizeZaloMessagingTarget(normalized ?? raw);
177
177
  return Boolean(target);
178
178
  }
179
- const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-DrCGqgEo.js"));
180
- const zaloSetupWizard = createZaloSetupWizardProxy(async () => (await import("./setup-surface-8eRimod9.js")).zaloSetupWizard);
179
+ const loadZaloChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime-U7gDleS5.js"));
180
+ const zaloSetupWizard = createZaloSetupWizardProxy(async () => (await import("./setup-surface-dFkadq_c.js")).zaloSetupWizard);
181
181
  const zaloTextChunkLimit = 2e3;
182
182
  async function sendZaloDelivery(ctx) {
183
183
  const result = await (await loadZaloChannelRuntime()).sendZaloText({
@@ -258,6 +258,7 @@ const zaloPlugin = createChatChannelPlugin({
258
258
  id: "zalo",
259
259
  meta,
260
260
  setup: zaloSetupAdapter,
261
+ setupContract: zaloSetupContract,
261
262
  setupWizard: zaloSetupWizard,
262
263
  capabilities: {
263
264
  chatTypes: ["direct", "group"],
@@ -347,6 +348,7 @@ const zaloPlugin = createChatChannelPlugin({
347
348
  chunker: chunkTextForOutbound,
348
349
  chunkerMode: "text",
349
350
  textChunkLimit: zaloTextChunkLimit,
351
+ sanitizeText: ({ text }) => sanitizeAssistantVisibleText(text),
350
352
  sendPayload: async (ctx) => await sendPayloadWithChunkedTextAndMedia({
351
353
  ctx,
352
354
  textChunkLimit: zaloTextChunkLimit,
@@ -1,2 +1,2 @@
1
- import { t as zaloPlugin } from "./channel-DikWJ2TC.js";
1
+ import { t as zaloPlugin } from "./channel-CvZItLo-.js";
2
2
  export { zaloPlugin };
@@ -1,6 +1,6 @@
1
- import { p as normalizeSecretInputString } from "./setup-core-fwCh0QUi.js";
1
+ import { m as normalizeSecretInputString } from "./setup-core-BBA-V-j0.js";
2
2
  import { i as PAIRING_APPROVED_MESSAGE } from "./runtime-api-B53PGWBW.js";
3
- import { a as getMe, n as resolveZaloProxyFetch, r as ZaloApiError, t as sendMessageZalo } from "./send-9sVVxyap.js";
3
+ import { a as getMe, n as resolveZaloProxyFetch, r as ZaloApiError, t as sendMessageZalo } from "./send-WPSy5EgF.js";
4
4
  import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-outbound";
5
5
  //#region extensions/zalo/src/probe.ts
6
6
  async function probeZalo(token, timeoutMs = 5e3, fetcher) {
@@ -52,7 +52,7 @@ async function probeZalo(token, timeoutMs = 5e3, fetcher) {
52
52
  //#endregion
53
53
  //#region extensions/zalo/src/channel.runtime.ts
54
54
  async function notifyZaloPairingApproval(params) {
55
- const { resolveZaloAccount } = await import("./setup-core-fwCh0QUi.js").then((n) => n.o);
55
+ const { resolveZaloAccount } = await import("./setup-core-BBA-V-j0.js").then((n) => n.s);
56
56
  const account = resolveZaloAccount({ cfg: params.cfg });
57
57
  if (!account.token) throw new Error("Zalo token not configured");
58
58
  await sendMessageZalo(params.id, PAIRING_APPROVED_MESSAGE, { token: account.token });
@@ -86,7 +86,7 @@ async function startZaloGatewayAccount(ctx) {
86
86
  setStatus: ctx.setStatus
87
87
  });
88
88
  ctx.log?.info(`[${account.accountId}] starting provider${zaloBotLabel} mode=${mode}`);
89
- const { monitorZaloProvider } = await import("./monitor-oEpn3pC7.js");
89
+ const { monitorZaloProvider } = await import("./monitor-DfHhlqTv.js");
90
90
  return monitorZaloProvider({
91
91
  token,
92
92
  account,
@@ -1,7 +1,7 @@
1
1
  import { n as resolveZaloRuntimeGroupPolicy, t as normalizeZaloAllowEntry } from "./group-access-8qHRzDHx.js";
2
2
  import { t as getZaloRuntime } from "./runtime-api-B53PGWBW.js";
3
- import { c as sendChatAction, d as setWebhook, i as deleteWebhook, l as sendMessage, n as resolveZaloProxyFetch, o as getUpdates, r as ZaloApiError, s as getWebhookInfo, u as sendPhoto } from "./send-9sVVxyap.js";
4
- import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
3
+ import { c as sendChatAction, d as setWebhook, f as ZALO_OUTBOUND_MEDIA_TTL_MS, i as deleteWebhook, l as sendMessage, n as resolveZaloProxyFetch, o as getUpdates, r as ZaloApiError, s as getWebhookInfo, u as sendPhoto } from "./send-WPSy5EgF.js";
4
+ import { createLazyRuntimeModule, createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime";
5
5
  import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
6
6
  import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
7
  import { resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
@@ -30,7 +30,6 @@ function resolveZaloDurableReplyOptions(params) {
30
30
  }
31
31
  //#endregion
32
32
  //#region extensions/zalo/src/outbound-media.ts
33
- const ZALO_OUTBOUND_MEDIA_TTL_MS = 2 * 6e4;
34
33
  const ZALO_OUTBOUND_MEDIA_SEGMENT = "media";
35
34
  const ZALO_OUTBOUND_MEDIA_PREFIX = `/${ZALO_OUTBOUND_MEDIA_SEGMENT}/`;
36
35
  const ZALO_OUTBOUND_MEDIA_ID_RE = /^[a-f0-9]{24}$/;
@@ -74,7 +73,7 @@ function resolveHostedZaloMediaRoutePath(params) {
74
73
  }
75
74
  async function prepareHostedZaloMediaUrl(params) {
76
75
  const now = asDateTimestampMs(Date.now());
77
- if ((now === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(ZALO_OUTBOUND_MEDIA_TTL_MS, { nowMs: now })) === void 0) throw new Error("Zalo outbound media expiry could not be resolved");
76
+ if ((now === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(12e4, { nowMs: now })) === void 0) throw new Error("Zalo outbound media expiry could not be resolved");
78
77
  const routePath = resolveHostedZaloMediaRoutePath({
79
78
  webhookUrl: params.webhookUrl,
80
79
  webhookPath: params.webhookPath
@@ -164,7 +163,12 @@ function resolveZaloTimestampMs(date) {
164
163
  if (!date) return;
165
164
  return date >= UNIX_MILLISECONDS_THRESHOLD ? date : date * 1e3;
166
165
  }
167
- const loadZaloWebhookModule = createLazyRuntimeModule(() => import("./monitor.webhook-CiT0mp4W.js"));
166
+ const loadZaloWebhookRuntime = createLazyRuntimeNamedExport(() => import("./monitor.webhook-BCTmWQY8.js"), "zaloWebhookRuntime");
167
+ const loadZaloWebhookIngressRuntime = createLazyRuntimeNamedExport(() => import("./webhook-spool-Ozf0tcbs.js"), "zaloWebhookIngressRuntime");
168
+ const loadZaloWebhookModule = createLazyRuntimeModule(async () => ({
169
+ ...await loadZaloWebhookRuntime(),
170
+ ...await loadZaloWebhookIngressRuntime()
171
+ }));
168
172
  function releaseSharedHostedMediaRouteRef(routePath) {
169
173
  const current = hostedMediaRouteRefs.get(routePath);
170
174
  if (!current) return;
@@ -225,22 +229,7 @@ function logVerbose(core, runtime, message) {
225
229
  }
226
230
  async function handleZaloWebhookRequest(req, res) {
227
231
  const { handleZaloWebhookRequest: handleZaloWebhookRequestInternal } = await loadZaloWebhookModule();
228
- return await handleZaloWebhookRequestInternal(req, res, async ({ update, target }) => {
229
- await processUpdate({
230
- update,
231
- token: target.token,
232
- account: target.account,
233
- config: target.config,
234
- runtime: target.runtime,
235
- core: target.core,
236
- mediaMaxMb: target.mediaMaxMb,
237
- canHostMedia: target.canHostMedia,
238
- webhookUrl: target.webhookUrl,
239
- webhookPath: target.webhookPath,
240
- statusSink: target.statusSink,
241
- fetcher: target.fetcher
242
- });
243
- });
232
+ return await handleZaloWebhookRequestInternal(req, res);
244
233
  }
245
234
  function startPollingLoop(params) {
246
235
  const { token, account, config, runtime, core, mediaMaxMb, canHostMedia, webhookUrl, webhookPath, abortSignal, isStopped, statusSink, fetcher } = params;
@@ -297,7 +286,8 @@ async function processUpdate(params) {
297
286
  webhookUrl: params.webhookUrl,
298
287
  webhookPath: params.webhookPath,
299
288
  statusSink,
300
- fetcher
289
+ fetcher,
290
+ turnAdoptionLifecycle: params.turnAdoptionLifecycle
301
291
  };
302
292
  if (!message) return;
303
293
  switch (event_name) {
@@ -329,6 +319,7 @@ async function handleTextMessage(params) {
329
319
  await processMessageWithPipeline({
330
320
  ...params,
331
321
  text,
322
+ mediaKind: void 0,
332
323
  mediaPath: void 0,
333
324
  mediaType: void 0
334
325
  });
@@ -339,7 +330,8 @@ async function handleImageMessage(params) {
339
330
  const authorization = await authorizeZaloMessage({
340
331
  ...params,
341
332
  text: caption,
342
- mediaPath: "__pending_media__",
333
+ mediaKind: "image",
334
+ mediaPath: void 0,
343
335
  mediaType: void 0
344
336
  });
345
337
  if (!authorization) return;
@@ -359,21 +351,21 @@ async function handleImageMessage(params) {
359
351
  runtime.error?.(`[${account.accountId}] Failed to download Zalo image: ${String(err)}`);
360
352
  }
361
353
  const agentBody = mediaPath ? authorization.rawBody : formatInboundMediaUnavailableText({
362
- body: caption,
363
- mediaPlaceholder: "<media:image>",
354
+ body: authorization.rawBody,
364
355
  notice: "[zalo image attachment unavailable]"
365
356
  });
366
357
  await processMessageWithPipeline({
367
358
  ...params,
368
359
  authorization,
369
360
  agentBody,
361
+ mediaKind: "image",
370
362
  text: caption,
371
363
  mediaPath,
372
364
  mediaType
373
365
  });
374
366
  }
375
367
  async function authorizeZaloMessage(params) {
376
- const { message, account, config, runtime, core, text, mediaPath, token, statusSink, fetcher } = params;
368
+ const { message, account, config, runtime, core, text, token, statusSink, fetcher } = params;
377
369
  const pairing = createChannelPairingController({
378
370
  core,
379
371
  channel: "zalo",
@@ -386,7 +378,7 @@ async function authorizeZaloMessage(params) {
386
378
  const senderName = from.display_name ?? from.name;
387
379
  const dmPolicy = account.config.dmPolicy ?? "pairing";
388
380
  const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
389
- const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
381
+ const rawBody = text?.trim() ?? "";
390
382
  const { groupPolicy, providerMissingFallbackApplied } = resolveZaloRuntimeGroupPolicy({
391
383
  providerConfigPresent: config.channels?.zalo !== void 0,
392
384
  groupPolicy: account.config.groupPolicy,
@@ -468,7 +460,7 @@ async function authorizeZaloMessage(params) {
468
460
  };
469
461
  }
470
462
  async function processMessageWithPipeline(params) {
471
- const { message, token, account, config, runtime, core, mediaPath, mediaType, statusSink, fetcher, agentBody: agentBodyOverride, authorization: authorizationOverride } = params;
463
+ const { message, token, account, config, runtime, core, mediaPath, mediaKind, mediaType, statusSink, fetcher, agentBody: agentBodyOverride, authorization: authorizationOverride } = params;
472
464
  const { message_id, date } = message;
473
465
  const authorization = authorizationOverride ?? await authorizeZaloMessage({
474
466
  ...params,
@@ -527,10 +519,11 @@ async function processMessageWithPipeline(params) {
527
519
  rawBody,
528
520
  commandBody: rawBody
529
521
  },
530
- media: mediaPath || mediaType ? [{
522
+ media: mediaKind || mediaPath || mediaType ? [{
531
523
  path: mediaPath,
532
524
  url: mediaPath,
533
- contentType: mediaType
525
+ contentType: mediaType,
526
+ kind: mediaKind ?? void 0
534
527
  }] : void 0,
535
528
  extra: {
536
529
  CommandAuthorized: commandAuthorized,
@@ -608,7 +601,8 @@ async function processMessageWithPipeline(params) {
608
601
  } },
609
602
  record: { onRecordError: (err) => {
610
603
  runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
611
- } }
604
+ } },
605
+ ...params.turnAdoptionLifecycle ? { turnAdoptionLifecycle: params.turnAdoptionLifecycle } : {}
612
606
  });
613
607
  }
614
608
  async function deliverZaloReply(params) {
@@ -670,6 +664,7 @@ async function monitorZaloProvider(options) {
670
664
  }) : void 0;
671
665
  let stopped = false;
672
666
  const stopHandlers = [];
667
+ const asyncStopHandlers = [];
673
668
  let cleanupWebhook;
674
669
  const stop = () => {
675
670
  if (stopped) return;
@@ -691,46 +686,44 @@ async function monitorZaloProvider(options) {
691
686
  stopHandlers.push(unregisterHostedMediaRoute);
692
687
  }
693
688
  if (useWebhook) {
694
- const { registerZaloWebhookTarget } = await loadZaloWebhookModule();
689
+ const { createZaloWebhookIngress, registerZaloWebhookTarget } = await loadZaloWebhookModule();
695
690
  if (!effectiveWebhookUrl || !webhookSecret) throw new Error("Zalo webhookUrl and webhookSecret are required for webhook mode");
696
691
  if (!effectiveWebhookUrl.startsWith("https://")) throw new Error("Zalo webhook URL must use HTTPS");
697
692
  if (webhookSecret.length < 8 || webhookSecret.length > 256) throw new Error("Zalo webhook secret must be 8-256 characters");
698
693
  const path = effectiveWebhookPath;
699
694
  if (!path) throw new Error("Zalo webhookPath could not be derived");
700
695
  runtime.log?.(`[${account.accountId}] Zalo configuring webhook path=${path} target=${describeWebhookTarget(effectiveWebhookUrl)}`);
701
- await setWebhook(token, {
702
- url: effectiveWebhookUrl,
703
- secret_token: webhookSecret
704
- }, fetcher);
705
- let webhookCleanupPromise;
706
- cleanupWebhook = async () => {
707
- if (!webhookCleanupPromise) webhookCleanupPromise = (async () => {
708
- runtime.log?.(`[${account.accountId}] Zalo stopping; deleting webhook`);
709
- try {
710
- await deleteWebhook(token, fetcher, WEBHOOK_CLEANUP_TIMEOUT_MS);
711
- runtime.log?.(`[${account.accountId}] Zalo webhook deleted`);
712
- } catch (err) {
713
- const detail = err instanceof Error && err.name === "AbortError" ? `timed out after ${String(WEBHOOK_CLEANUP_TIMEOUT_MS)}ms` : formatZaloError(err);
714
- runtime.error?.(`[${account.accountId}] Zalo webhook delete failed: ${detail}`);
715
- }
716
- })();
717
- await webhookCleanupPromise;
718
- };
719
- runtime.log?.(`[${account.accountId}] Zalo webhook registered path=${path}`);
696
+ const ingress = createZaloWebhookIngress({
697
+ accountId: account.accountId,
698
+ runtime,
699
+ deliver: async (update, turnAdoptionLifecycle) => {
700
+ statusSink?.({ lastInboundAt: Date.now() });
701
+ await processUpdate({
702
+ update,
703
+ token,
704
+ account,
705
+ config,
706
+ runtime,
707
+ core,
708
+ mediaMaxMb: effectiveMediaMaxMb,
709
+ canHostMedia,
710
+ webhookUrl: effectiveWebhookUrl,
711
+ webhookPath: path,
712
+ statusSink,
713
+ fetcher,
714
+ turnAdoptionLifecycle
715
+ });
716
+ }
717
+ });
718
+ ingress.start();
719
+ asyncStopHandlers.push(ingress.stop);
720
720
  const unregister = registerZaloWebhookTarget({
721
- token,
722
721
  account,
723
722
  config,
724
723
  runtime,
725
- core,
726
724
  path,
727
- webhookUrl: effectiveWebhookUrl,
728
- webhookPath: path,
729
725
  secret: webhookSecret,
730
- statusSink: (patch) => statusSink?.(patch),
731
- mediaMaxMb: effectiveMediaMaxMb,
732
- canHostMedia,
733
- fetcher
726
+ acceptWebhook: ingress.accept
734
727
  }, { route: {
735
728
  auth: "plugin",
736
729
  match: "exact",
@@ -747,6 +740,25 @@ async function monitorZaloProvider(options) {
747
740
  }
748
741
  } });
749
742
  stopHandlers.push(unregister);
743
+ await setWebhook(token, {
744
+ url: effectiveWebhookUrl,
745
+ secret_token: webhookSecret
746
+ }, fetcher);
747
+ let webhookCleanupPromise;
748
+ cleanupWebhook = async () => {
749
+ if (!webhookCleanupPromise) webhookCleanupPromise = (async () => {
750
+ runtime.log?.(`[${account.accountId}] Zalo stopping; deleting webhook`);
751
+ try {
752
+ await deleteWebhook(token, fetcher, WEBHOOK_CLEANUP_TIMEOUT_MS);
753
+ runtime.log?.(`[${account.accountId}] Zalo webhook deleted`);
754
+ } catch (err) {
755
+ const detail = err instanceof Error && err.name === "AbortError" ? `timed out after ${String(WEBHOOK_CLEANUP_TIMEOUT_MS)}ms` : formatZaloError(err);
756
+ runtime.error?.(`[${account.accountId}] Zalo webhook delete failed: ${detail}`);
757
+ }
758
+ })();
759
+ await webhookCleanupPromise;
760
+ };
761
+ runtime.log?.(`[${account.accountId}] Zalo webhook registered path=${path}`);
750
762
  await waitForAbortSignal(abortSignal);
751
763
  return;
752
764
  }
@@ -790,6 +802,7 @@ async function monitorZaloProvider(options) {
790
802
  abortSignal.removeEventListener("abort", stopOnAbort);
791
803
  await cleanupWebhook?.();
792
804
  stop();
805
+ for (const stopAsync of asyncStopHandlers) await stopAsync();
793
806
  runtime.log?.(`[${account.accountId}] Zalo provider stopped mode=${mode}`);
794
807
  }
795
808
  }
@@ -1,9 +1,8 @@
1
- import { B as registerWebhookTargetWithPluginRoute, G as resolveWebhookTargetWithAuthOrRejectSync, I as readJsonWebhookBodyOrReject, V as resolveClientIp, Z as withResolvedWebhookRequestPipeline, a as WEBHOOK_ANOMALY_COUNTER_DEFAULTS, b as createFixedWindowRateLimiter, l as applyBasicWebhookRequestGuards, o as WEBHOOK_RATE_LIMIT_DEFAULTS, x as createWebhookAnomalyTracker, z as registerWebhookTarget } from "./runtime-api-B53PGWBW.js";
1
+ import { B as registerWebhookTargetWithPluginRoute, G as resolveWebhookTargetWithAuthOrRejectSync, V as resolveClientIp, Z as withResolvedWebhookRequestPipeline, a as WEBHOOK_ANOMALY_COUNTER_DEFAULTS, b as createFixedWindowRateLimiter, l as applyBasicWebhookRequestGuards, o as WEBHOOK_RATE_LIMIT_DEFAULTS, x as createWebhookAnomalyTracker, z as registerWebhookTarget } from "./runtime-api-B53PGWBW.js";
2
+ import { ZaloWebhookPayloadError } from "./webhook-spool-Ozf0tcbs.js";
2
3
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
3
- import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
4
- import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
4
+ import { readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
5
5
  //#region extensions/zalo/src/monitor.webhook.ts
6
- const ZALO_WEBHOOK_REPLAY_WINDOW_MS = 5 * 6e4;
7
6
  const webhookTargets = /* @__PURE__ */ new Map();
8
7
  const webhookRateLimiter = createFixedWindowRateLimiter({
9
8
  windowMs: WEBHOOK_RATE_LIMIT_DEFAULTS.windowMs,
@@ -17,7 +16,6 @@ const webhookAnomalyTracker = createWebhookAnomalyTracker({
17
16
  });
18
17
  function clearZaloWebhookSecurityStateForTest() {
19
18
  webhookRateLimiter.clear();
20
- recentWebhookEvents.clearMemory();
21
19
  webhookAnomalyTracker.clear();
22
20
  }
23
21
  function getZaloWebhookRateLimitStateSizeForTest() {
@@ -26,46 +24,6 @@ function getZaloWebhookRateLimitStateSizeForTest() {
26
24
  function getZaloWebhookStatusCounterSizeForTest() {
27
25
  return webhookAnomalyTracker.size();
28
26
  }
29
- function buildReplayEventCacheKey(target, update) {
30
- const messageId = update.message?.message_id;
31
- if (!messageId) return null;
32
- const chatId = update.message?.chat?.id ?? "";
33
- const senderId = update.message?.from?.id ?? "";
34
- return JSON.stringify([
35
- target.path,
36
- target.account.accountId,
37
- update.event_name,
38
- chatId,
39
- senderId,
40
- messageId
41
- ]);
42
- }
43
- const recentWebhookEvents = createChannelReplayGuard({
44
- dedupe: {
45
- ttlMs: ZALO_WEBHOOK_REPLAY_WINDOW_MS,
46
- memoryMaxSize: 5e3
47
- },
48
- buildReplayKey: ({ target, update }) => buildReplayEventCacheKey(target, update)
49
- });
50
- var ZaloRetryableWebhookError = class extends Error {
51
- constructor(message, options) {
52
- super(message, options);
53
- this.name = "ZaloRetryableWebhookError";
54
- }
55
- };
56
- async function processZaloReplayGuardedUpdate(params) {
57
- const event = {
58
- target: params.target,
59
- update: params.update
60
- };
61
- return (await recentWebhookEvents.processGuarded(event, async () => {
62
- params.target.statusSink?.({ lastInboundAt: Date.now() });
63
- await params.processUpdate(event);
64
- }, {
65
- dedupe: { now: params.nowMs },
66
- onError: (error) => error instanceof ZaloRetryableWebhookError ? "release" : "commit"
67
- })).kind === "processed" ? "processed" : "duplicate";
68
- }
69
27
  function recordWebhookStatus(runtime, path, statusCode) {
70
28
  webhookAnomalyTracker.record({
71
29
  key: `${path}:${statusCode}`,
@@ -86,7 +44,7 @@ function registerZaloWebhookTarget(target, opts) {
86
44
  }).unregister;
87
45
  return registerWebhookTarget(webhookTargets, target, opts).unregister;
88
46
  }
89
- async function handleZaloWebhookRequest(req, res, processUpdate) {
47
+ async function handleZaloWebhookRequest(req, res) {
90
48
  return await withResolvedWebhookRequestPipeline({
91
49
  req,
92
50
  res,
@@ -102,13 +60,12 @@ async function handleZaloWebhookRequest(req, res, processUpdate) {
102
60
  trustedProxies,
103
61
  allowRealIpFallback
104
62
  }) ?? req.socket.remoteAddress ?? "unknown"}`;
105
- const nowMs = Date.now();
106
63
  if (!applyBasicWebhookRequestGuards({
107
64
  req,
108
65
  res,
109
66
  rateLimiter: webhookRateLimiter,
110
67
  rateLimitKey,
111
- nowMs
68
+ nowMs: Date.now()
112
69
  })) {
113
70
  recordWebhookStatus(targets[0]?.runtime, path, res.statusCode);
114
71
  return true;
@@ -131,40 +88,38 @@ async function handleZaloWebhookRequest(req, res, processUpdate) {
131
88
  recordWebhookStatus(target.runtime, path, res.statusCode);
132
89
  return true;
133
90
  }
134
- const body = await readJsonWebhookBodyOrReject({
91
+ const body = await readWebhookBodyOrReject({
135
92
  req,
136
93
  res,
137
94
  maxBytes: 1024 * 1024,
138
95
  timeoutMs: 3e4,
139
- emptyObjectOnEmpty: false,
140
- invalidJsonMessage: "Bad Request"
96
+ invalidBodyMessage: "Bad Request"
141
97
  });
142
98
  if (!body.ok) {
143
99
  recordWebhookStatus(target.runtime, path, res.statusCode);
144
100
  return true;
145
101
  }
146
- const raw = body.value;
147
- const record = raw && typeof raw === "object" ? raw : null;
148
- const update = record && record.ok === true && record.result ? record.result : record ?? void 0;
149
- if (!update?.event_name) {
150
- res.statusCode = 400;
151
- res.end("Bad Request");
102
+ try {
103
+ await target.acceptWebhook(body.value);
104
+ } catch (error) {
105
+ res.statusCode = error instanceof ZaloWebhookPayloadError ? 400 : 500;
106
+ res.end(res.statusCode === 400 ? "Bad Request" : "Internal Server Error");
152
107
  recordWebhookStatus(target.runtime, path, res.statusCode);
108
+ target.runtime.error?.(`[${target.account.accountId}] Zalo webhook admission failed: ${String(error)}`);
153
109
  return true;
154
110
  }
155
- runDetachedWebhookWork(() => processZaloReplayGuardedUpdate({
156
- target,
157
- update,
158
- processUpdate,
159
- nowMs
160
- })).catch((err) => {
161
- target.runtime.error?.(`[${target.account.accountId}] Zalo webhook failed: ${String(err)}`);
162
- });
163
111
  res.statusCode = 200;
164
112
  res.end("ok");
165
113
  return true;
166
114
  }
167
115
  });
168
116
  }
117
+ const zaloWebhookRuntime = {
118
+ clearZaloWebhookSecurityStateForTest,
119
+ getZaloWebhookRateLimitStateSizeForTest,
120
+ getZaloWebhookStatusCounterSizeForTest,
121
+ handleZaloWebhookRequest,
122
+ registerZaloWebhookTarget
123
+ };
169
124
  //#endregion
170
- export { ZaloRetryableWebhookError, clearZaloWebhookSecurityStateForTest, getZaloWebhookRateLimitStateSizeForTest, getZaloWebhookStatusCounterSizeForTest, handleZaloWebhookRequest, registerZaloWebhookTarget };
125
+ export { zaloWebhookRuntime };
@@ -1,4 +1,4 @@
1
- import { d as resolveZaloToken, u as resolveZaloAccount } from "./setup-core-fwCh0QUi.js";
1
+ import { d as resolveZaloAccount, f as resolveZaloToken } from "./setup-core-BBA-V-j0.js";
2
2
  import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
3
3
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
4
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -6,6 +6,11 @@ import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
6
6
  import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
7
7
  import { resolvePinnedHostnameWithPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
8
8
  import { makeProxyFetch } from "openclaw/plugin-sdk/fetch-runtime";
9
+ //#region extensions/zalo/src/timeouts.ts
10
+ const ZALO_DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
11
+ const ZALO_OUTBOUND_MEDIA_TTL_MS = 2 * 6e4;
12
+ const ZALO_SEND_PHOTO_REQUEST_TIMEOUT_MS = 15e4;
13
+ //#endregion
9
14
  //#region extensions/zalo/src/api.ts
10
15
  /**
11
16
  * Zalo Bot API client
@@ -45,8 +50,8 @@ function resolveZaloApiUrl(apiUrl) {
45
50
  async function callZaloApi(method, token, body, options) {
46
51
  const url = `${resolveZaloApiUrl(options?.apiUrl)}/bot${token}/${method}`;
47
52
  const controller = new AbortController();
48
- const requestTimeoutMs = options?.timeoutMs === void 0 ? void 0 : resolveTimerTimeoutMs(options.timeoutMs, 1);
49
- const timeoutId = requestTimeoutMs === void 0 ? void 0 : setTimeout(() => controller.abort(), requestTimeoutMs);
53
+ const requestTimeoutMs = resolveTimerTimeoutMs(options?.timeoutMs, ZALO_DEFAULT_REQUEST_TIMEOUT_MS);
54
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
50
55
  const fetcher = options?.fetch ?? fetch;
51
56
  try {
52
57
  const data = await readProviderJsonResponse(await fetcher(url, {
@@ -58,7 +63,7 @@ async function callZaloApi(method, token, body, options) {
58
63
  if (!data.ok) throw new ZaloApiError(data.description ?? `Zalo API error: ${method}`, data.error_code, data.description);
59
64
  return data;
60
65
  } finally {
61
- if (timeoutId) clearTimeout(timeoutId);
66
+ clearTimeout(timeoutId);
62
67
  }
63
68
  }
64
69
  /**
@@ -92,7 +97,10 @@ async function sendPhoto(token, params, fetcher) {
92
97
  return callZaloApi("sendPhoto", token, {
93
98
  ...params,
94
99
  photo: parsedPhotoUrl.href
95
- }, { fetch: fetcher });
100
+ }, {
101
+ timeoutMs: ZALO_SEND_PHOTO_REQUEST_TIMEOUT_MS,
102
+ fetch: fetcher
103
+ });
96
104
  }
97
105
  /**
98
106
  * Send a temporary chat action such as typing.
@@ -285,4 +293,4 @@ async function sendPhotoZalo(chatId, photoUrl, options = {}) {
285
293
  }, context.fetcher))());
286
294
  }
287
295
  //#endregion
288
- export { getMe as a, sendChatAction as c, setWebhook as d, deleteWebhook as i, sendMessage as l, resolveZaloProxyFetch as n, getUpdates as o, ZaloApiError as r, getWebhookInfo as s, sendMessageZalo as t, sendPhoto as u };
296
+ export { getMe as a, sendChatAction as c, setWebhook as d, ZALO_OUTBOUND_MEDIA_TTL_MS as f, deleteWebhook as i, sendMessage as l, resolveZaloProxyFetch as n, getUpdates as o, ZaloApiError as r, getWebhookInfo as s, sendMessageZalo as t, sendPhoto as u };
package/dist/setup-api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-fwCh0QUi.js";
1
+ import { n as zaloDmPolicy, r as zaloSetupAdapter, t as createZaloSetupWizardProxy } from "./setup-core-BBA-V-j0.js";
2
2
  import { n as resolveZaloRuntimeGroupPolicy } from "./group-access-8qHRzDHx.js";
3
3
  import { loadBundledEntryExportSync } from "openclaw/plugin-sdk/channel-entry-contract";
4
4
  //#region extensions/zalo/setup-api.ts
@@ -4,6 +4,7 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runti
4
4
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
5
5
  import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
6
6
  import { buildSecretInputSchema, normalizeResolvedSecretInputString, normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
7
+ import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
7
8
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, addWildcardAllowFrom, createDelegatedSetupWizardProxy, createPatchedAccountSetupAdapter, createSetupInputPresenceValidator, createSetupTranslator, formatDocsLink, mergeAllowFromEntries, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/setup";
8
9
  //#region \0rolldown/runtime.js
9
10
  var __defProp = Object.defineProperty;
@@ -184,16 +185,47 @@ async function promptZaloAllowFrom(params) {
184
185
  //#region extensions/zalo/src/setup-core.ts
185
186
  const t = createSetupTranslator();
186
187
  const channel = "zalo";
187
- const zaloSetupAdapter = createPatchedAccountSetupAdapter({
188
- channelKey: channel,
189
- validateInput: createSetupInputPresenceValidator({
190
- defaultAccountOnlyEnvError: "ZALO_BOT_TOKEN can only be used for the default account.",
191
- whenNotUseEnv: [{
192
- someOf: ["token", "tokenFile"],
193
- message: "Zalo requires token or --token-file (or --use-env)."
194
- }]
188
+ const zaloSetupAdapter = {
189
+ ...createPatchedAccountSetupAdapter({
190
+ channelKey: channel,
191
+ validateInput: createSetupInputPresenceValidator({
192
+ defaultAccountOnlyEnvError: "ZALO_BOT_TOKEN can only be used for the default account.",
193
+ whenNotUseEnv: [{
194
+ someOf: ["token", "tokenFile"],
195
+ message: "Zalo requires token or --token-file (or --use-env)."
196
+ }]
197
+ }),
198
+ buildPatch: (input) => input.useEnv ? {} : input.tokenFile ? { tokenFile: input.tokenFile } : input.token ? { botToken: input.token } : {}
195
199
  }),
196
- buildPatch: (input) => input.useEnv ? {} : input.tokenFile ? { tokenFile: input.tokenFile } : input.token ? { botToken: input.token } : {}
200
+ singleAccountKeysToMove: ["webhookSecret", "tokenFile"]
201
+ };
202
+ const zaloSetupContract = defineChannelSetupContract({
203
+ fields: {
204
+ token: {
205
+ kind: "string",
206
+ sensitive: true,
207
+ cli: {
208
+ flags: "--token <token>",
209
+ description: "Zalo bot token"
210
+ }
211
+ },
212
+ tokenFile: {
213
+ kind: "string",
214
+ sensitive: true,
215
+ cli: {
216
+ flags: "--token-file <path>",
217
+ description: "Zalo bot token file"
218
+ }
219
+ },
220
+ useEnv: {
221
+ kind: "boolean",
222
+ cli: {
223
+ flags: "--use-env",
224
+ description: "Use ZALO_BOT_TOKEN"
225
+ }
226
+ }
227
+ },
228
+ legacyAdapter: zaloSetupAdapter
197
229
  });
198
230
  const zaloDmPolicy = {
199
231
  label: "Zalo",
@@ -284,4 +316,4 @@ function createZaloSetupWizardProxy(loadWizard) {
284
316
  });
285
317
  }
286
318
  //#endregion
287
- export { promptZaloAllowFrom as a, listZaloAccountIds as c, resolveZaloToken as d, buildSecretInputSchema as f, noteZaloTokenHelp as i, resolveDefaultZaloAccountId as l, zaloDmPolicy as n, accounts_exports as o, normalizeSecretInputString as p, zaloSetupAdapter as r, listEnabledZaloAccounts as s, createZaloSetupWizardProxy as t, resolveZaloAccount as u };
319
+ export { noteZaloTokenHelp as a, listEnabledZaloAccounts as c, resolveZaloAccount as d, resolveZaloToken as f, zaloSetupContract as i, listZaloAccountIds as l, normalizeSecretInputString as m, zaloDmPolicy as n, promptZaloAllowFrom as o, buildSecretInputSchema as p, zaloSetupAdapter as r, accounts_exports as s, createZaloSetupWizardProxy as t, resolveDefaultZaloAccountId as u };
@@ -1,4 +1,4 @@
1
- import { a as promptZaloAllowFrom, i as noteZaloTokenHelp, n as zaloDmPolicy, u as resolveZaloAccount } from "./setup-core-fwCh0QUi.js";
1
+ import { a as noteZaloTokenHelp, d as resolveZaloAccount, n as zaloDmPolicy, o as promptZaloAllowFrom } from "./setup-core-BBA-V-j0.js";
2
2
  import { DEFAULT_ACCOUNT_ID, buildSingleChannelSecretPromptState, createSetupTranslator, createStandardChannelSetupStatus, hasConfiguredSecretInput, promptSingleChannelSecretInput, runSingleChannelSecretStep } from "openclaw/plugin-sdk/setup";
3
3
  //#region extensions/zalo/src/setup-surface.ts
4
4
  const t = createSetupTranslator();
@@ -0,0 +1,174 @@
1
+ import { t as getZaloRuntime } from "./runtime-api-B53PGWBW.js";
2
+ import { r as ZaloApiError } from "./send-WPSy5EgF.js";
3
+ import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
4
+ import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
5
+ //#region extensions/zalo/src/webhook-spool.ts
6
+ const ZALO_WEBHOOK_SPOOL_VERSION = 1;
7
+ const ZALO_WEBHOOK_DRAIN_INTERVAL_MS = 500;
8
+ const ZALO_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
9
+ const ZALO_WEBHOOK_PRUNE_INTERVAL_MS = 3600 * 1e3;
10
+ const ZALO_WEBHOOK_COMPLETED_TTL_MS = 720 * 60 * 6e4;
11
+ const ZALO_WEBHOOK_COMPLETED_MAX_ENTRIES = 2e4;
12
+ const ZALO_WEBHOOK_FAILED_TTL_MS = 720 * 60 * 6e4;
13
+ const ZALO_WEBHOOK_FAILED_MAX_ENTRIES = 5e3;
14
+ var ZaloWebhookPayloadError = class extends Error {
15
+ constructor(message, options) {
16
+ super(message, options);
17
+ this.name = "ZaloWebhookPayloadError";
18
+ }
19
+ };
20
+ function isRecord(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ function nonEmptyString(value) {
24
+ return typeof value === "string" && value.trim() ? value.trim() : null;
25
+ }
26
+ function parseRawRecord(rawEvent) {
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(rawEvent);
30
+ } catch (error) {
31
+ throw new ZaloWebhookPayloadError("Zalo webhook body contains invalid JSON.", { cause: error });
32
+ }
33
+ if (!isRecord(parsed)) throw new ZaloWebhookPayloadError("Zalo webhook body must be a JSON object.");
34
+ return parsed;
35
+ }
36
+ function resolveUpdateRecord(envelope) {
37
+ if (envelope.ok === true && isRecord(envelope.result)) return envelope.result;
38
+ return envelope;
39
+ }
40
+ function inspectZaloWebhookEvent(rawEvent) {
41
+ const update = resolveUpdateRecord(parseRawRecord(rawEvent));
42
+ const message = isRecord(update.message) ? update.message : null;
43
+ const eventId = nonEmptyString(message?.message_id);
44
+ if (!eventId) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.message_id.");
45
+ const chatId = nonEmptyString((isRecord(message?.chat) ? message.chat : null)?.id);
46
+ if (!chatId) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.chat.id.");
47
+ return {
48
+ eventId,
49
+ laneKey: `chat:${chatId}`,
50
+ update
51
+ };
52
+ }
53
+ function parseClaimedUpdate(payload, claimedId) {
54
+ if (payload.version !== ZALO_WEBHOOK_SPOOL_VERSION || typeof payload.rawEvent !== "string") throw new ZaloWebhookPayloadError("Zalo webhook spool payload is invalid.");
55
+ const facts = inspectZaloWebhookEvent(payload.rawEvent);
56
+ if (facts.eventId !== claimedId) throw new ZaloWebhookPayloadError("Zalo webhook message id changed after durable admission.");
57
+ const eventName = nonEmptyString(facts.update.event_name);
58
+ if (eventName !== "message.text.received" && eventName !== "message.image.received" && eventName !== "message.sticker.received" && eventName !== "message.unsupported.received") throw new ZaloWebhookPayloadError("Zalo webhook event_name is unsupported.");
59
+ const message = facts.update.message;
60
+ const from = isRecord(message.from) ? message.from : null;
61
+ const chat = isRecord(message.chat) ? message.chat : null;
62
+ if (!nonEmptyString(from?.id)) throw new ZaloWebhookPayloadError("Zalo webhook message is missing message.from.id.");
63
+ if (chat?.chat_type !== "PRIVATE" && chat?.chat_type !== "GROUP") throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid chat type.");
64
+ if (typeof message.date !== "number" || !Number.isFinite(message.date)) throw new ZaloWebhookPayloadError("Zalo webhook message has an invalid date.");
65
+ if (eventName === "message.text.received" && typeof message.text !== "string") throw new ZaloWebhookPayloadError("Zalo text event is missing message.text.");
66
+ return facts.update;
67
+ }
68
+ function errorText(error) {
69
+ return error instanceof Error ? error.message : String(error);
70
+ }
71
+ function isZaloAuthenticationFailure(error) {
72
+ let current = error;
73
+ const seen = /* @__PURE__ */ new Set();
74
+ while (current && typeof current === "object" && !seen.has(current)) {
75
+ seen.add(current);
76
+ const candidate = current;
77
+ if (current instanceof ZaloApiError && (current.errorCode === 401 || current.errorCode === 403) || candidate.status === 401 || candidate.status === 403 || candidate.statusCode === 401 || candidate.statusCode === 403) return true;
78
+ current = candidate.cause;
79
+ }
80
+ return false;
81
+ }
82
+ function createZaloWebhookIngress(options) {
83
+ const queue = options.queue ?? getZaloRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
84
+ const deferredClaims = /* @__PURE__ */ new Map();
85
+ const monitor = createChannelIngressMonitor({
86
+ queue,
87
+ inspect: (rawEvent) => inspectZaloWebhookEvent(rawEvent),
88
+ payload: {
89
+ storage: "raw-event",
90
+ version: ZALO_WEBHOOK_SPOOL_VERSION,
91
+ serialize: (rawEvent) => rawEvent,
92
+ deserialize: (rawEvent) => rawEvent,
93
+ createClaimError: (kind) => new ZaloWebhookPayloadError(kind === "invalid-version" ? "Zalo webhook spool payload is invalid." : "Zalo webhook identity changed after durable admission.")
94
+ },
95
+ deliver: async (_rawEvent, lifecycle, claim) => {
96
+ const update = parseClaimedUpdate(claim.payload, claim.id);
97
+ const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
98
+ let resolveDeferredClaim;
99
+ const deferredClaim = new Promise((resolve) => {
100
+ resolveDeferredClaim = resolve;
101
+ });
102
+ let deferredClaimSettled = false;
103
+ const settleDeferredClaim = () => {
104
+ if (deferredClaimSettled) return;
105
+ deferredClaimSettled = true;
106
+ if (deferredClaims.get(claim.id) === deferredClaim) deferredClaims.delete(claim.id);
107
+ resolveDeferredClaim();
108
+ };
109
+ await options.deliver(update, {
110
+ ...boundLifecycle,
111
+ onAdopted: async () => {
112
+ try {
113
+ await boundLifecycle.onAdopted();
114
+ } finally {
115
+ settleDeferredClaim();
116
+ }
117
+ },
118
+ onDeferred: () => {
119
+ if (!deferredClaimSettled) deferredClaims.set(claim.id, deferredClaim);
120
+ boundLifecycle.onDeferred();
121
+ },
122
+ onAbandoned: () => {
123
+ Promise.resolve(boundLifecycle.onAbandoned()).finally(settleDeferredClaim);
124
+ }
125
+ });
126
+ return deferredClaims.has(claim.id) ? { kind: "deferred" } : { kind: "completed" };
127
+ },
128
+ pollIntervalMs: ZALO_WEBHOOK_DRAIN_INTERVAL_MS,
129
+ retention: {
130
+ pruneIntervalMs: ZALO_WEBHOOK_PRUNE_INTERVAL_MS,
131
+ completedTtlMs: ZALO_WEBHOOK_COMPLETED_TTL_MS,
132
+ completedMaxEntries: ZALO_WEBHOOK_COMPLETED_MAX_ENTRIES,
133
+ failedTtlMs: ZALO_WEBHOOK_FAILED_TTL_MS,
134
+ failedMaxEntries: ZALO_WEBHOOK_FAILED_MAX_ENTRIES
135
+ },
136
+ waitForDeliveryIdleBeforeRepump: false,
137
+ runPumpTask: runDetachedWebhookWork,
138
+ drain: {
139
+ adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
140
+ startLimit: ZALO_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
141
+ retryPolicy: {
142
+ maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
143
+ deadLetterMinAgeMs: 0
144
+ },
145
+ resolveNonRetryableFailure: (error) => {
146
+ if (error instanceof ZaloWebhookPayloadError) return {
147
+ reason: "invalid-event",
148
+ message: error.message
149
+ };
150
+ if (isZaloAuthenticationFailure(error)) return {
151
+ reason: "authentication-failed",
152
+ message: errorText(error)
153
+ };
154
+ return null;
155
+ },
156
+ onLog: (message) => options.runtime.error?.(`zalo ingress: ${message}`)
157
+ },
158
+ createStoppedError: () => /* @__PURE__ */ new Error("Zalo ingress stopped."),
159
+ onError: (error) => options.runtime.error?.(`zalo ingress drain failed: ${errorText(error)}`)
160
+ });
161
+ return {
162
+ accept: async (rawEvent) => {
163
+ await monitor.admit(rawEvent);
164
+ },
165
+ start: monitor.start,
166
+ stop: async () => {
167
+ await monitor.stop();
168
+ await Promise.allSettled(deferredClaims.values());
169
+ }
170
+ };
171
+ }
172
+ const zaloWebhookIngressRuntime = { createZaloWebhookIngress };
173
+ //#endregion
174
+ export { ZaloWebhookPayloadError, zaloWebhookIngressRuntime };
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@openclaw/zalo",
3
- "version": "2026.7.2-beta.3",
3
+ "version": "2026.7.2-beta.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/zalo",
9
- "version": "2026.7.2-beta.3",
9
+ "version": "2026.7.2-beta.4",
10
10
  "dependencies": {
11
11
  "zod": "4.4.3"
12
12
  },
13
13
  "peerDependencies": {
14
- "openclaw": ">=2026.7.2-beta.3"
14
+ "openclaw": ">=2026.7.2-beta.4"
15
15
  },
16
16
  "peerDependenciesMeta": {
17
17
  "openclaw": {
@@ -9,12 +9,6 @@
9
9
  "channels": [
10
10
  "zalo"
11
11
  ],
12
- "channelEnvVars": {
13
- "zalo": [
14
- "ZALO_BOT_TOKEN",
15
- "ZALO_WEBHOOK_SECRET"
16
- ]
17
- },
18
12
  "configSchema": {
19
13
  "type": "object",
20
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/zalo",
3
- "version": "2026.7.2-beta.3",
3
+ "version": "2026.7.2-beta.4",
4
4
  "description": "OpenClaw Zalo channel plugin for bot and webhook chats.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "type": "module",
10
10
  "peerDependencies": {
11
- "openclaw": ">=2026.7.2-beta.3"
11
+ "openclaw": ">=2026.7.2-beta.4"
12
12
  },
13
13
  "peerDependenciesMeta": {
14
14
  "openclaw": {
@@ -20,8 +20,19 @@
20
20
  "./index.ts"
21
21
  ],
22
22
  "setupEntry": "./dist/setup-entry.js",
23
+ "setupFeatures": {
24
+ "configPromotion": true
25
+ },
23
26
  "channel": {
24
27
  "id": "zalo",
28
+ "configuredState": {
29
+ "env": {
30
+ "anyOf": [
31
+ "ZALO_BOT_TOKEN",
32
+ "ZALO_WEBHOOK_SECRET"
33
+ ]
34
+ }
35
+ },
25
36
  "label": "Zalo",
26
37
  "selectionLabel": "Zalo (Bot API)",
27
38
  "docsPath": "/channels/zalo",
@@ -31,7 +42,37 @@
31
42
  "zl"
32
43
  ],
33
44
  "order": 80,
34
- "quickstartAllowFrom": true
45
+ "quickstartAllowFrom": true,
46
+ "setup": {
47
+ "fields": [
48
+ {
49
+ "key": "token",
50
+ "kind": "string",
51
+ "sensitive": true,
52
+ "cli": {
53
+ "flags": "--token <token>",
54
+ "description": "Zalo bot token"
55
+ }
56
+ },
57
+ {
58
+ "key": "tokenFile",
59
+ "kind": "string",
60
+ "sensitive": true,
61
+ "cli": {
62
+ "flags": "--token-file <path>",
63
+ "description": "Zalo bot token file"
64
+ }
65
+ },
66
+ {
67
+ "key": "useEnv",
68
+ "kind": "boolean",
69
+ "cli": {
70
+ "flags": "--use-env",
71
+ "description": "Use ZALO_BOT_TOKEN"
72
+ }
73
+ }
74
+ ]
75
+ }
35
76
  },
36
77
  "install": {
37
78
  "npmSpec": "@openclaw/zalo",
@@ -39,10 +80,10 @@
39
80
  "minHostVersion": ">=2026.4.10"
40
81
  },
41
82
  "compat": {
42
- "pluginApi": ">=2026.7.2-beta.3"
83
+ "pluginApi": ">=2026.7.2-beta.4"
43
84
  },
44
85
  "build": {
45
- "openclawVersion": "2026.7.2-beta.3"
86
+ "openclawVersion": "2026.7.2-beta.4"
46
87
  },
47
88
  "release": {
48
89
  "publishToClawHub": true,