openclaw-threema 0.5.0 → 0.5.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/index.ts CHANGED
@@ -1109,6 +1109,28 @@ function getMimeFromPath(filePath: string): string {
1109
1109
  // Channel Plugin Definition
1110
1110
  // ============================================================================
1111
1111
 
1112
+ // Shared status updater — allows the webhook handler (registered outside the
1113
+ // channel adapter) to update the channel's health-monitor status on inbound events.
1114
+ // startAccount populates this; the webhook handler calls updateActivity().
1115
+ const channelStatus = {
1116
+ _getStatus: null as (() => ChannelAccountSnapshot) | null,
1117
+ _setStatus: null as ((s: ChannelAccountSnapshot) => void) | null,
1118
+ bind(getStatus: () => ChannelAccountSnapshot, setStatus: (s: ChannelAccountSnapshot) => void) {
1119
+ this._getStatus = getStatus;
1120
+ this._setStatus = setStatus;
1121
+ },
1122
+ updateActivity() {
1123
+ if (this._getStatus && this._setStatus) {
1124
+ const now = Date.now();
1125
+ this._setStatus({
1126
+ ...this._getStatus(),
1127
+ lastEventAt: now,
1128
+ lastInboundAt: now,
1129
+ });
1130
+ }
1131
+ },
1132
+ };
1133
+
1112
1134
  const threemaChannel = {
1113
1135
  id: "threema" as const,
1114
1136
 
@@ -1549,18 +1571,34 @@ const threemaChannel = {
1549
1571
  log?.info?.(`E2E public key: ${client.ownPublicKey}`);
1550
1572
  }
1551
1573
 
1574
+ // Bind shared status so the webhook handler can report activity
1575
+ channelStatus.bind(getStatus, setStatus);
1576
+
1552
1577
  setStatus({
1553
1578
  ...getStatus(),
1554
1579
  running: true,
1555
1580
  connected: true,
1556
1581
  lastConnectedAt: Date.now(),
1582
+ lastEventAt: Date.now(),
1557
1583
  });
1558
1584
 
1585
+ // Periodic health heartbeat — update lastEventAt every 15 min so the
1586
+ // health-monitor doesn't think we're stuck (webhook channels are passive).
1587
+ const heartbeatInterval = setInterval(() => {
1588
+ setStatus({
1589
+ ...getStatus(),
1590
+ lastEventAt: Date.now(),
1591
+ });
1592
+ }, 15 * 60 * 1000);
1593
+
1559
1594
  // Keep the promise alive until abortSignal fires — resolving immediately
1560
1595
  // causes the gateway to treat the channel as "exited" and restart it.
1561
1596
  await new Promise<void>((resolve) => {
1562
1597
  if (abortSignal.aborted) return resolve();
1563
- abortSignal.addEventListener("abort", () => resolve(), { once: true });
1598
+ abortSignal.addEventListener("abort", () => {
1599
+ clearInterval(heartbeatInterval);
1600
+ resolve();
1601
+ }, { once: true });
1564
1602
  });
1565
1603
 
1566
1604
  log?.info?.("Threema Gateway stopped via abort signal");
@@ -1584,7 +1622,7 @@ const threemaChannel = {
1584
1622
 
1585
1623
  export const id = "threema";
1586
1624
  export const name = "Threema Gateway";
1587
- export const version = "0.5.0";
1625
+ export const version = "0.5.2";
1588
1626
  export const description =
1589
1627
  "Threema messaging channel via Threema Gateway API (E2E encrypted, with media support)";
1590
1628
 
@@ -1594,6 +1632,12 @@ export default function register(api: any) {
1594
1632
  const threemaCfg = getThreemaConfig(config);
1595
1633
  const runtime = api.runtime;
1596
1634
 
1635
+ // Store runtime reference for wakeAgent() to use requestHeartbeatNow
1636
+ _runtimeApi = runtime;
1637
+
1638
+ // Debug: check if requestHeartbeatNow is available
1639
+ api.logger?.info?.(`Threema wake API: runtime.system.requestHeartbeatNow=${typeof runtime?.system?.requestHeartbeatNow}`);
1640
+
1597
1641
  // Register the channel plugin
1598
1642
  api.registerChannel({ plugin: threemaChannel });
1599
1643
  api.logger?.info?.("Threema channel plugin registered");
@@ -1606,17 +1650,16 @@ export default function register(api: any) {
1606
1650
  const dmPolicy = threemaCfg.dmPolicy ?? "allowlist";
1607
1651
  const allowFrom = threemaCfg.allowFrom ?? [];
1608
1652
 
1609
- // Use registerHttpHandler (not registerHttpRoute) so the webhook is NOT
1610
- // behind gateway auth Threema's API needs to POST without our token.
1611
- // The webhook has its own MAC-based authentication.
1612
- api.registerHttpHandler?.(async (req: any, res: any): Promise<boolean> => {
1613
- const url = new URL(req.url ?? "/", "http://localhost");
1614
- if (url.pathname !== webhookPath) return false;
1615
-
1653
+ // Webhook handler Threema's API POSTs without our gateway token,
1654
+ // so we need an unauthenticated route. The webhook has its own MAC-based auth.
1655
+ //
1656
+ // Forward-compatible: use registerHttpRoute with auth:"none" (2026.3.2+),
1657
+ // fall back to registerHttpHandler for 2026.3.1.
1658
+ const webhookHandler = async (req: any, res: any): Promise<void> => {
1616
1659
  if (req.method !== "POST") {
1617
1660
  res.writeHead(405, { "Content-Type": "text/plain" });
1618
1661
  res.end("Method Not Allowed");
1619
- return true;
1662
+ return;
1620
1663
  }
1621
1664
 
1622
1665
  try {
@@ -1669,11 +1712,13 @@ export default function register(api: any) {
1669
1712
  return;
1670
1713
  }
1671
1714
 
1672
- // 4. Validate date (not older than 10 minutes)
1715
+ // 4. Validate date (not older than 60 minutes)
1716
+ // Threema retries delivery on webhook errors, so messages can arrive late.
1717
+ // 60 min is generous enough for retries while still rejecting stale replays.
1673
1718
  if (date) {
1674
1719
  const msgTimestamp = parseInt(date, 10) * 1000; // Convert to ms
1675
1720
  const now = Date.now();
1676
- const maxAge = 10 * 60 * 1000; // 10 minutes in ms
1721
+ const maxAge = 60 * 60 * 1000; // 60 minutes in ms
1677
1722
  if (now - msgTimestamp > maxAge) {
1678
1723
  api.logger?.warn?.(`Threema webhook: message too old (date=${date})`);
1679
1724
  res.writeHead(400, { "Content-Type": "text/plain" });
@@ -1719,6 +1764,9 @@ export default function register(api: any) {
1719
1764
 
1720
1765
  // === PROCESSING PHASE ===
1721
1766
 
1767
+ // Report activity to health-monitor (prevents "stuck" restarts)
1768
+ channelStatus.updateActivity();
1769
+
1722
1770
  // Decrypt the message
1723
1771
  const senderPubKey = await client.getPublicKey(from);
1724
1772
  const decrypted = client.decryptMessage(
@@ -1834,9 +1882,37 @@ export default function register(api: any) {
1834
1882
  res.end("Internal Server Error");
1835
1883
  }
1836
1884
  }
1885
+ return;
1886
+ };
1887
+
1888
+ // Register the webhook — Threema's callback server POSTs without our
1889
+ // gateway auth token, so we need an unauthenticated route.
1890
+ //
1891
+ // Strategy: prefer registerHttpHandler (available in 2026.3.1, removed in 2026.3.2)
1892
+ // because it bypasses gateway auth. Fall back to registerHttpRoute with auth:"none"
1893
+ // (supported from 2026.3.2+). The order matters: registerHttpRoute in 2026.3.1
1894
+ // silently ignores auth:"none" and applies gateway auth, breaking external webhooks.
1895
+ if (api.registerHttpHandler) {
1896
+ // 2026.3.1: registerHttpHandler with manual path matching (no gateway auth)
1897
+ api.registerHttpHandler(async (req: any, res: any): Promise<boolean> => {
1898
+ const url = new URL(req.url ?? "/", "http://localhost");
1899
+ if (url.pathname !== webhookPath) return false;
1900
+ await webhookHandler(req, res);
1837
1901
  return true;
1838
- });
1839
- api.logger?.info?.(`Threema webhook registered at ${webhookPath}`);
1902
+ });
1903
+ api.logger?.info?.(`Threema webhook registered via registerHttpHandler at ${webhookPath}`);
1904
+ } else if (api.registerHttpRoute) {
1905
+ // 2026.3.2+: registerHttpHandler removed, use registerHttpRoute with auth:"plugin"
1906
+ // auth:"plugin" = no gateway auth, plugin handles its own auth (MAC verification)
1907
+ api.registerHttpRoute({
1908
+ path: webhookPath,
1909
+ auth: "plugin",
1910
+ handler: webhookHandler,
1911
+ });
1912
+ api.logger?.info?.(`Threema webhook registered via registerHttpRoute (auth:plugin) at ${webhookPath}`);
1913
+ } else {
1914
+ api.logger?.error?.("No HTTP registration method available — Threema webhook NOT registered!");
1915
+ }
1840
1916
  }
1841
1917
 
1842
1918
  // Register CLI commands
@@ -1962,10 +2038,21 @@ export default function register(api: any) {
1962
2038
  /**
1963
2039
  * Wake the agent to process new messages
1964
2040
  */
2041
+ // Shared reference to runtime API — set during register(), used by wakeAgent()
2042
+ let _runtimeApi: any = null;
2043
+
1965
2044
  function wakeAgent(config: any) {
2045
+ // Prefer the new requestHeartbeatNow API (2026.3.2+) — direct, no HTTP roundtrip
2046
+ if (_runtimeApi?.system?.requestHeartbeatNow) {
2047
+ try {
2048
+ _runtimeApi.system.requestHeartbeatNow({ sessionKey: "agent:main:main" });
2049
+ return;
2050
+ } catch {}
2051
+ }
2052
+
2053
+ // Fallback: HTTP wake call for older versions
1966
2054
  const gatewayPort = config?.gateway?.port || 18789;
1967
2055
 
1968
- // Try ENV first, fallback to config file (security: ENV is preferred)
1969
2056
  let hooksToken: string | undefined = process.env.OPENCLAW_HOOKS_TOKEN;
1970
2057
 
1971
2058
  if (!hooksToken) {
@@ -2,7 +2,7 @@
2
2
  "id": "threema",
3
3
  "name": "Threema Gateway",
4
4
  "description": "Threema messaging channel via Threema Gateway API (E2E encrypted)",
5
- "version": "0.5.0",
5
+ "version": "0.5.1",
6
6
  "channels": [
7
7
  "threema"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-threema",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Threema Gateway channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.ts",