@openclaw/imessage 2026.9.1-beta.1 → 2026.9.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.
Files changed (47) hide show
  1. package/dist/{accounts-DIpGOIiN.js → accounts-CCirBEIm.js} +2 -3
  2. package/dist/{actions.runtime-BhggncbG.js → actions.runtime-D5MAgeSs.js} +5 -5
  3. package/dist/api.js +5 -5
  4. package/dist/{approval-handler.runtime-BC-YsgAf.js → approval-handler.runtime-47rs6THN.js} +14 -9
  5. package/dist/{approval-reactions-Cvwmdlev.js → approval-reactions-sVpDno7d.js} +8 -16
  6. package/dist/{channel-CnKQ2MVB.js → channel-BxraMrmK.js} +21 -14
  7. package/dist/channel-config-api.js +1 -1
  8. package/dist/channel-plugin-api.js +1 -1
  9. package/dist/{channel.runtime-BQLCh7bE.js → channel.runtime-2jwkMPGR.js} +59 -14
  10. package/dist/{config-schema-DV0KP5nC.js → config-schema-DuGbA8Kp.js} +8 -7
  11. package/dist/contract-api.js +1 -1
  12. package/dist/doctor-contract-api.js +8 -281
  13. package/dist/{group-policy-BipELT2x.js → group-policy-Cp8SDG4G.js} +18 -12
  14. package/dist/media-contract-api.js +1 -1
  15. package/dist/{media-contract-BVXuQHG2.js → media-contract-pns3TSS5.js} +3 -2
  16. package/dist/{message-tool-api-BwIxJDoz.js → message-tool-api-CUoFESHj.js} +11 -6
  17. package/dist/message-tool-api.js +1 -1
  18. package/dist/{monitor-DRD_Dnp9.js → monitor-CESwxMQ4.js} +319 -48
  19. package/dist/{question-reactions-viP7ZcyG.js → question-reactions-9tBYB1_F.js} +4 -7
  20. package/dist/{reaction-context-BAYI7pz0.js → reaction-context-Br-vAJFp.js} +9 -3
  21. package/dist/{remote-file-CyTTuRAt.js → remote-file-Biz5PjXi.js} +5 -53
  22. package/dist/runtime-Cza4CY5T.js +8 -0
  23. package/dist/runtime-api.js +7 -7
  24. package/dist/{sanitize-outbound-g651fIhi.js → sanitize-outbound-Bvo4g1Mp.js} +470 -82
  25. package/dist/{send-DVQY6M5M.js → send-DC981dqK.js} +146 -23
  26. package/dist/state-contract-DvahEY4X.js +56 -0
  27. package/dist/state-migrations-D-fyHVcb.js +277 -0
  28. package/node_modules/typebox/build/format/email.mjs +1 -1
  29. package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
  30. package/node_modules/typebox/build/format/json_pointer.d.mts +2 -2
  31. package/node_modules/typebox/build/format/json_pointer.mjs +2 -2
  32. package/node_modules/typebox/build/format/json_pointer_uri_fragment.d.mts +2 -2
  33. package/node_modules/typebox/build/format/json_pointer_uri_fragment.mjs +2 -2
  34. package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
  35. package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
  36. package/node_modules/typebox/build/system/memory/assign.mjs +2 -1
  37. package/node_modules/typebox/build/system/memory/create.mjs +3 -4
  38. package/node_modules/typebox/build/system/memory/discard.mjs +2 -2
  39. package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
  40. package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
  41. package/node_modules/typebox/build/system/memory/update.mjs +3 -2
  42. package/node_modules/typebox/package.json +1 -1
  43. package/openclaw.plugin.json +2 -7
  44. package/package.json +5 -5
  45. package/dist/catchup-Dh-Rv4Ur.js +0 -294
  46. package/dist/monitor-reply-cache-BdeUQaHO.js +0 -353
  47. package/dist/persisted-echo-cache-DGxy-J4t.js +0 -143
@@ -1,17 +1,18 @@
1
- import { d as resolveIMessageChatDbLookupPath, l as resolveIMessageRemoteHost, n as hasExclusiveIMessageLocalDatabase, o as resolveIMessageAccount } from "./accounts-DIpGOIiN.js";
2
- import { l as parseIMessageTarget, n as formatIMessageChatTarget, s as normalizeIMessageHandle } from "./message-tool-api-BwIxJDoz.js";
3
- import { c as rememberIMessageReplyCache, h as chatContextFromIMessageTarget, p as getOptionalIMessageRuntime, v as resolveIMessageDirectChatService } from "./monitor-reply-cache-BdeUQaHO.js";
4
- import { a as IMessageRpcRequestError, n as sanitizeIMessageFinalOutboundText, o as createIMessageRpcClient, t as protectIMessageFencedRoleMarkers } from "./sanitize-outbound-g651fIhi.js";
5
- import { l as getIMessageApprovalApprovers, u as imessageApprovalAuth } from "./group-policy-BipELT2x.js";
6
- import { a as rememberPersistedIMessageEcho, r as forgetPersistedIMessageEchoKey } from "./persisted-echo-cache-DGxy-J4t.js";
7
- import { d as normalizeConversationKey, f as normalizeIMessageGuid, i as registerIMessageApprovalReactionTarget, l as enumerateApprovalTargetKeys, s as buildIMessageApprovalConversationKeyForInbound, u as enumerateConversationKeyForms } from "./approval-reactions-Cvwmdlev.js";
8
- import { i as runIMessageCliJsonCommand, r as resolveAuthorizedIMessageReplyReference, t as withIMessageRemoteFile } from "./remote-file-CyTTuRAt.js";
1
+ import { d as resolveIMessageChatDbLookupPath, l as resolveIMessageRemoteHost, n as hasExclusiveIMessageLocalDatabase, o as resolveIMessageAccount } from "./accounts-CCirBEIm.js";
2
+ import { l as parseIMessageTarget, n as formatIMessageChatTarget, s as normalizeIMessageHandle } from "./message-tool-api-CUoFESHj.js";
3
+ import { a as IMessageRpcRequestError, d as rememberIMessageReplyCache, g as chatContextFromIMessageTarget, n as sanitizeIMessageFinalOutboundText, o as createIMessageRpcClient, s as runIMessageCliJsonCommand, t as protectIMessageFencedRoleMarkers, y as resolveIMessageDirectChatService } from "./sanitize-outbound-Bvo4g1Mp.js";
4
+ import { l as getIMessageApprovalApprovers, u as imessageApprovalAuth } from "./group-policy-Cp8SDG4G.js";
5
+ import { n as getOptionalIMessageRuntime, t as getIMessageRuntime } from "./runtime-Cza4CY5T.js";
6
+ import { c as IMESSAGE_SENT_ECHOES_TTL_MS, d as resolveIMessageEchoMediaKey, p as resolveIMessageSentEchoEntryKey, s as IMESSAGE_SENT_ECHOES_NAMESPACE } from "./state-contract-DvahEY4X.js";
7
+ import { d as normalizeConversationKey, i as registerIMessageApprovalReactionTarget, l as enumerateApprovalTargetKeys, s as buildIMessageApprovalConversationKeyForInbound, u as enumerateConversationKeyForms } from "./approval-reactions-sVpDno7d.js";
8
+ import { n as normalizeIMessageGuid } from "./reaction-context-Br-vAJFp.js";
9
+ import { r as resolveAuthorizedIMessageReplyReference, t as withIMessageRemoteFile } from "./remote-file-Biz5PjXi.js";
9
10
  import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
10
11
  import { createLazyRuntimeSurface } from "openclaw/plugin-sdk/lazy-runtime";
11
12
  import { accessSync, constants } from "node:fs";
12
13
  import { basename } from "node:path";
13
14
  import { asOptionalRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
14
- import { sleep } from "openclaw/plugin-sdk/runtime-env";
15
+ import { logVerbose, sleep } from "openclaw/plugin-sdk/runtime-env";
15
16
  import { extractOriginalFilename, kindFromMime, resolveOutboundAttachmentFromUrl } from "openclaw/plugin-sdk/media-runtime";
16
17
  import { asDateTimestampMs } from "openclaw/plugin-sdk/number-runtime";
17
18
  import { PlatformMessageNotDispatchedError, isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
@@ -94,14 +95,14 @@ const iMessageApprovalControlBindings = {
94
95
  const TARGET_NAMESPACE = "imessage.approval-polls";
95
96
  const TOMBSTONE_NAMESPACE = "imessage.approval-poll-tombstones";
96
97
  const MAX_ENTRIES = 1e3;
97
- const DEFAULT_TARGET_TTL_MS = 1440 * 60 * 1e3;
98
+ const DEFAULT_TARGET_TTL_MS = 864e5;
98
99
  /**
99
100
  * Messages has no close-poll API, so a resolved approval's balloon stays
100
101
  * tappable forever. Tombstones outlive the binding so late taps are swallowed
101
102
  * instead of reaching the agent as "Poll vote: ..." prose. Persisted, because a
102
103
  * gateway restart must not turn old polls back into chat noise.
103
104
  */
104
- const TOMBSTONE_TTL_MS = 720 * 60 * 60 * 1e3;
105
+ const TOMBSTONE_TTL_MS = 2592e6;
105
106
  const APPROVAL_DECISIONS = /* @__PURE__ */ new Set([
106
107
  "allow-once",
107
108
  "allow-always",
@@ -453,6 +454,127 @@ const iMessageApprovalPollTargets = {
453
454
  clearForTest: clearIMessageApprovalPollTargetsForTest
454
455
  };
455
456
  //#endregion
457
+ //#region extensions/imessage/src/monitor/echo-text-corruption.ts
458
+ function isLeadingEchoTextCorruptionMarker(code) {
459
+ return code === 0 || code === 65279 || code === 65533 || code === 65534 || code === 65535;
460
+ }
461
+ function stripLeadingEchoTextCorruptionMarkers(text) {
462
+ let offset = 0;
463
+ while (offset < text.length && isLeadingEchoTextCorruptionMarker(text.charCodeAt(offset))) offset += 1;
464
+ return offset === 0 ? text : text.slice(offset);
465
+ }
466
+ //#endregion
467
+ //#region extensions/imessage/src/monitor/persisted-echo-cache.ts
468
+ function normalizeText(text) {
469
+ if (!text) return;
470
+ return stripLeadingEchoTextCorruptionMarkers(text.replace(/\r\n?/g, "\n").trim()).trim() || void 0;
471
+ }
472
+ function normalizeMessageId(messageId) {
473
+ const normalized = messageId?.trim();
474
+ if (!normalized || normalized === "ok" || normalized === "unknown") return;
475
+ return normalized;
476
+ }
477
+ let mirror = null;
478
+ let persistenceFailureLogged = false;
479
+ function reportFailure(scope, err) {
480
+ if (persistenceFailureLogged) return;
481
+ persistenceFailureLogged = true;
482
+ logVerbose(`imessage echo-cache: ${scope} disabled after first failure: ${String(err)}`);
483
+ }
484
+ function normalizeMedia(media) {
485
+ if (!resolveIMessageEchoMediaKey(media)) return;
486
+ const contentType = media?.contentType?.trim().toLowerCase() || void 0;
487
+ const kind = media?.kind ?? void 0;
488
+ return {
489
+ ...kind ? { kind } : {},
490
+ ...contentType ? { contentType } : {}
491
+ };
492
+ }
493
+ function openPersistedEchoStore() {
494
+ return getIMessageRuntime().state.openSyncKeyedStore({
495
+ namespace: IMESSAGE_SENT_ECHOES_NAMESPACE,
496
+ maxEntries: 256
497
+ });
498
+ }
499
+ function remainingTtlMs(timestamp) {
500
+ const remaining = IMESSAGE_SENT_ECHOES_TTL_MS - Math.max(0, Date.now() - timestamp);
501
+ return remaining > 0 ? remaining : void 0;
502
+ }
503
+ function resolveEntryTtlMs(entry, ttlMs) {
504
+ if (typeof ttlMs === "number" && Number.isFinite(ttlMs) && ttlMs > 0) return ttlMs;
505
+ return remainingTtlMs(entry.timestamp);
506
+ }
507
+ function isLiveEntry(entry, now = Date.now()) {
508
+ const cutoff = now - IMESSAGE_SENT_ECHOES_TTL_MS;
509
+ return entry.timestamp >= cutoff && (entry.expiresAt == null || entry.expiresAt > now);
510
+ }
511
+ function loadMirrorFromStore() {
512
+ try {
513
+ mirror = openPersistedEchoStore().entries().map(({ value }) => value).filter((entry) => isLiveEntry(entry)).toSorted((a, b) => a.timestamp - b.timestamp).slice(-256);
514
+ } catch (err) {
515
+ reportFailure("read", err);
516
+ mirror = [];
517
+ }
518
+ }
519
+ function readRecentEntries() {
520
+ loadMirrorFromStore();
521
+ return mirror ?? [];
522
+ }
523
+ function persistEntry(entry, ttlMs) {
524
+ const effectiveTtlMs = resolveEntryTtlMs(entry, ttlMs);
525
+ if (!effectiveTtlMs) return;
526
+ const key = resolveIMessageSentEchoEntryKey(entry);
527
+ try {
528
+ openPersistedEchoStore().register(key, entry, { ttlMs: effectiveTtlMs });
529
+ } catch (err) {
530
+ reportFailure("write", err);
531
+ return;
532
+ }
533
+ return key;
534
+ }
535
+ function rememberPersistedIMessageEcho(params) {
536
+ const text = normalizeText(params.text);
537
+ const media = normalizeMedia(params.media);
538
+ const messageId = normalizeMessageId(params.messageId);
539
+ const entry = {
540
+ scope: params.scope,
541
+ timestamp: Date.now(),
542
+ ...text ? { text } : {},
543
+ ...media ? { media } : {},
544
+ ...messageId ? { messageId } : {},
545
+ ...params.pending ? { pending: true } : {}
546
+ };
547
+ if (typeof params.ttlMs === "number" && Number.isFinite(params.ttlMs) && params.ttlMs > 0) entry.expiresAt = entry.timestamp + params.ttlMs;
548
+ if (!entry.text && !entry.media && !entry.messageId) return;
549
+ loadMirrorFromStore();
550
+ const key = persistEntry(entry, params.ttlMs);
551
+ mirror = [...mirror ?? [], entry].filter((candidate) => isLiveEntry(candidate)).slice(-256);
552
+ return key;
553
+ }
554
+ function forgetPersistedIMessageEchoKey(key) {
555
+ if (!key) return;
556
+ try {
557
+ openPersistedEchoStore().delete(key);
558
+ } catch (err) {
559
+ reportFailure("delete", err);
560
+ }
561
+ mirror = (mirror ?? []).filter((entry) => resolveIMessageSentEchoEntryKey(entry) !== key);
562
+ }
563
+ function hasPersistedIMessageEcho(params) {
564
+ const text = normalizeText(params.text);
565
+ const mediaKey = resolveIMessageEchoMediaKey(params.media);
566
+ const messageId = normalizeMessageId(params.messageId);
567
+ if (!text && !mediaKey && !messageId) return false;
568
+ for (const entry of readRecentEntries()) {
569
+ if (entry.scope !== params.scope) continue;
570
+ if (messageId && entry.messageId === messageId) return true;
571
+ const hasConflictingMessageIds = Boolean(messageId && entry.messageId && messageId !== entry.messageId);
572
+ if (text && (!hasConflictingMessageIds || params.skipIdShortCircuit) && entry.text === text && (!entry.pending || params.includePendingText)) return true;
573
+ if (mediaKey && !hasConflictingMessageIds && resolveIMessageEchoMediaKey(entry.media) === mediaKey && (!entry.pending || params.includePendingText)) return true;
574
+ }
575
+ return false;
576
+ }
577
+ //#endregion
456
578
  //#region extensions/imessage/src/send.ts
457
579
  const MIN_PENDING_PERSISTED_ECHO_TTL_MS = 6e4;
458
580
  const PENDING_PERSISTED_ECHO_GRACE_MS = 5e3;
@@ -722,16 +844,17 @@ async function trySendAttachmentForTarget(params) {
722
844
  if (params.target.kind === "handle" && !params.audioAsVoice && params.sendTransport !== "bridge" && (params.service === "sms" || params.service === "imessage")) return null;
723
845
  if (params.remoteHost && params.sendTransport === "applescript") return null;
724
846
  let attachmentChatTarget = null;
725
- if (params.remoteHost) if (params.target.kind === "chat_guid") attachmentChatTarget = params.target.chatGuid;
726
- else if (params.target.kind === "chat_identifier") attachmentChatTarget = params.target.chatIdentifier;
727
- else if (params.target.kind === "handle") {
728
- const normalizedHandle = normalizeIMessageHandle(params.target.to);
729
- if (normalizedHandle) {
730
- const service = params.target.service !== "auto" ? params.target.service : params.service;
731
- attachmentChatTarget = `${service === "sms" ? "SMS" : service === "imessage" ? "iMessage" : "any"};-;${normalizedHandle}`;
732
- }
733
- } else attachmentChatTarget = formatIMessageChatTarget(params.target.chatId);
734
- else try {
847
+ if (params.remoteHost) {
848
+ if (params.target.kind === "chat_guid") attachmentChatTarget = params.target.chatGuid;
849
+ else if (params.target.kind === "chat_identifier") attachmentChatTarget = params.target.chatIdentifier;
850
+ else if (params.target.kind === "handle") {
851
+ const normalizedHandle = normalizeIMessageHandle(params.target.to);
852
+ if (normalizedHandle) {
853
+ const service = params.target.service !== "auto" ? params.target.service : params.service;
854
+ attachmentChatTarget = `${service === "sms" ? "SMS" : service === "imessage" ? "iMessage" : "any"};-;${normalizedHandle}`;
855
+ }
856
+ } else attachmentChatTarget = formatIMessageChatTarget(params.target.chatId);
857
+ } else try {
735
858
  attachmentChatTarget = await resolveAttachmentChatTarget({
736
859
  target: params.target,
737
860
  service: params.service,
@@ -881,7 +1004,7 @@ async function sendMessageIMessage(to, text, opts) {
881
1004
  const timeoutMs = opts.timeoutMs ?? Math.max(account.config.probeTimeoutMs ?? 0, 18e4);
882
1005
  const pendingEchoTtlMs = resolvePendingPersistedEchoTtlMs(timeoutMs);
883
1006
  const region = opts.region?.trim() || account.config.region?.trim() || "US";
884
- const maxBytes = typeof opts.maxBytes === "number" ? opts.maxBytes : typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb * 1024 * 1024 : 16 * 1024 * 1024;
1007
+ const maxBytes = typeof opts.maxBytes === "number" ? opts.maxBytes : typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb * 1024 * 1024 : 16777216;
885
1008
  let message = opts.approvalPrompt ? addApprovalReactionHintToText({
886
1009
  text,
887
1010
  allowedDecisions: opts.approvalPrompt.allowedDecisions
@@ -1173,4 +1296,4 @@ async function sendMessageIMessage(to, text, opts) {
1173
1296
  }
1174
1297
  }
1175
1298
  //#endregion
1176
- export { maybeResolveIMessageApprovalPollVote as a, mapSentPollOptionsToDecisions as i, buildApprovalPollOptions as n, iMessageApprovalControlBindings as o, iMessageApprovalPollTargets as r, sendMessageIMessage as t };
1299
+ export { iMessageApprovalPollTargets as a, iMessageApprovalControlBindings as c, buildApprovalPollOptions as i, hasPersistedIMessageEcho as n, mapSentPollOptionsToDecisions as o, stripLeadingEchoTextCorruptionMarkers as r, maybeResolveIMessageApprovalPollVote as s, sendMessageIMessage as t };
@@ -0,0 +1,56 @@
1
+ import { createHash } from "node:crypto";
2
+ //#region extensions/imessage/src/state-contract.ts
3
+ const IMESSAGE_REPLY_CACHE_NAMESPACE = "imessage.reply-cache";
4
+ const IMESSAGE_REPLY_CACHE_MAX_ENTRIES = 2e3;
5
+ const IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE = "imessage.reply-cache-counter";
6
+ const IMESSAGE_REPLY_CACHE_COUNTER_KEY = "short-id-counter";
7
+ const IMESSAGE_REPLY_CACHE_TTL_MS = 216e5;
8
+ function resolveIMessageReplyCacheEntryKey(messageId) {
9
+ return createHash("sha256").update(messageId, "utf8").digest("hex").slice(0, 32);
10
+ }
11
+ const MAX_FAILURE_RETRY_MAP_SIZE = 512;
12
+ const MAX_FAILURE_RETRY_MAP_JSON_BYTES = 48e3;
13
+ const textEncoder = new TextEncoder();
14
+ const IMESSAGE_CATCHUP_CURSOR_NAMESPACE = "imessage.catchup-cursors";
15
+ function resolveIMessageCatchupCursorKey(accountId) {
16
+ return createHash("sha256").update(accountId, "utf8").digest("hex").slice(0, 32);
17
+ }
18
+ /**
19
+ * Bound the retry map so a pathological storm of unique failing GUIDs
20
+ * cannot grow the cursor value without limit. Keeps the `maxSize` entries
21
+ * with the highest counts (closest to give-up) when over the bound.
22
+ */
23
+ function capFailureRetriesMap(map, maxSize = MAX_FAILURE_RETRY_MAP_SIZE, maxBytes = MAX_FAILURE_RETRY_MAP_JSON_BYTES) {
24
+ const entries = Object.entries(map);
25
+ if (entries.length <= maxSize && textEncoder.encode(JSON.stringify(map)).byteLength <= maxBytes) return map;
26
+ entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
27
+ const capped = {};
28
+ for (const [guid, count] of entries.slice(0, maxSize)) {
29
+ capped[guid] = count;
30
+ if (textEncoder.encode(JSON.stringify(capped)).byteLength > maxBytes) {
31
+ delete capped[guid];
32
+ break;
33
+ }
34
+ }
35
+ return capped;
36
+ }
37
+ const IMESSAGE_SENT_ECHOES_TTL_MS = 432e5;
38
+ const IMESSAGE_SENT_ECHOES_NAMESPACE = "imessage.sent-echoes";
39
+ function resolveIMessageSentEchoEntryKey(entry) {
40
+ return createHash("sha256").update(JSON.stringify([
41
+ entry.scope,
42
+ entry.text ?? "",
43
+ resolveIMessageEchoMediaKey(entry.media) ?? "",
44
+ entry.messageId ?? "",
45
+ entry.timestamp
46
+ ])).digest("hex").slice(0, 32);
47
+ }
48
+ function resolveIMessageEchoMediaKey(media) {
49
+ const contentType = media?.contentType?.trim().toLowerCase() || void 0;
50
+ const kind = media?.kind && media.kind !== "unknown" ? media.kind : void 0;
51
+ if (kind) return `kind:${kind}`;
52
+ const normalizedContentType = contentType?.split(";", 1)[0]?.trim();
53
+ return normalizedContentType ? `mime:${normalizedContentType}` : void 0;
54
+ }
55
+ //#endregion
56
+ export { IMESSAGE_REPLY_CACHE_NAMESPACE as a, IMESSAGE_SENT_ECHOES_TTL_MS as c, resolveIMessageEchoMediaKey as d, resolveIMessageReplyCacheEntryKey as f, IMESSAGE_REPLY_CACHE_MAX_ENTRIES as i, capFailureRetriesMap as l, IMESSAGE_REPLY_CACHE_COUNTER_KEY as n, IMESSAGE_REPLY_CACHE_TTL_MS as o, resolveIMessageSentEchoEntryKey as p, IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE as r, IMESSAGE_SENT_ECHOES_NAMESPACE as s, IMESSAGE_CATCHUP_CURSOR_NAMESPACE as t, resolveIMessageCatchupCursorKey as u };
@@ -0,0 +1,277 @@
1
+ import { a as resolveDefaultIMessageAccountId, i as listIMessageAccountIds, o as resolveIMessageAccount } from "./accounts-CCirBEIm.js";
2
+ import { a as IMESSAGE_REPLY_CACHE_NAMESPACE, c as IMESSAGE_SENT_ECHOES_TTL_MS, f as resolveIMessageReplyCacheEntryKey, i as IMESSAGE_REPLY_CACHE_MAX_ENTRIES, l as capFailureRetriesMap, n as IMESSAGE_REPLY_CACHE_COUNTER_KEY, o as IMESSAGE_REPLY_CACHE_TTL_MS, p as resolveIMessageSentEchoEntryKey, r as IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE, s as IMESSAGE_SENT_ECHOES_NAMESPACE, t as IMESSAGE_CATCHUP_CURSOR_NAMESPACE, u as resolveIMessageCatchupCursorKey } from "./state-contract-DvahEY4X.js";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { asFiniteNumber, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
6
+ import { createHash } from "node:crypto";
7
+ import { fileExists } from "openclaw/plugin-sdk/file-access-runtime";
8
+ import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
9
+ //#region extensions/imessage/src/state-migrations.ts
10
+ function resolveMigrationStateDir(params) {
11
+ return params.stateDir ?? resolveStateDir(params.env);
12
+ }
13
+ function remainingTtlMs(timestamp, ttlMs) {
14
+ const remaining = ttlMs - Math.max(0, Date.now() - timestamp);
15
+ return remaining > 0 ? remaining : void 0;
16
+ }
17
+ function readJsonl(pathValue) {
18
+ try {
19
+ return fs.readFileSync(pathValue, "utf8").split(/\n+/).flatMap((line) => {
20
+ if (!line) return [];
21
+ try {
22
+ return [JSON.parse(line)];
23
+ } catch {
24
+ return [];
25
+ }
26
+ });
27
+ } catch (err) {
28
+ throw new Error(`Failed reading ${pathValue}: ${String(err)}`, { cause: err });
29
+ }
30
+ }
31
+ function parseReplyCacheEntry(raw) {
32
+ if (!raw || typeof raw !== "object") return null;
33
+ const parsed = raw;
34
+ if (typeof parsed.accountId !== "string" || typeof parsed.messageId !== "string" || typeof parsed.shortId !== "string" || typeof parsed.timestamp !== "number") return null;
35
+ return {
36
+ accountId: parsed.accountId,
37
+ messageId: parsed.messageId,
38
+ shortId: parsed.shortId,
39
+ timestamp: parsed.timestamp,
40
+ ...typeof parsed.chatGuid === "string" ? { chatGuid: parsed.chatGuid } : {},
41
+ ...typeof parsed.chatIdentifier === "string" ? { chatIdentifier: parsed.chatIdentifier } : {},
42
+ ...typeof parsed.chatId === "number" ? { chatId: parsed.chatId } : {},
43
+ ...typeof parsed.isFromMe === "boolean" ? { isFromMe: parsed.isFromMe } : {}
44
+ };
45
+ }
46
+ function readReplyCacheMaxShortId(sourcePath) {
47
+ let max = 0;
48
+ for (const raw of readJsonl(sourcePath)) {
49
+ if (!raw || typeof raw !== "object") continue;
50
+ const shortId = raw.shortId;
51
+ if (typeof shortId !== "string") continue;
52
+ const numeric = Number.parseInt(shortId, 10);
53
+ if (Number.isFinite(numeric) && numeric > max) max = numeric;
54
+ }
55
+ return max;
56
+ }
57
+ function readReplyCounterValue(value) {
58
+ if (!value || typeof value !== "object") return null;
59
+ const counter = value.counter;
60
+ return asFiniteNumber(counter) ?? null;
61
+ }
62
+ function shouldReplaceReplyCounter(existingValue, incomingValue) {
63
+ const incomingCounter = readReplyCounterValue(incomingValue);
64
+ if (incomingCounter === null) return false;
65
+ const existingCounter = readReplyCounterValue(existingValue);
66
+ return existingCounter === null || incomingCounter > existingCounter;
67
+ }
68
+ function parseSentEchoEntry(raw) {
69
+ if (!raw || typeof raw !== "object") return null;
70
+ const parsed = raw;
71
+ if (typeof parsed.scope !== "string" || typeof parsed.timestamp !== "number") return null;
72
+ return {
73
+ scope: parsed.scope,
74
+ timestamp: parsed.timestamp,
75
+ ...typeof parsed.text === "string" ? { text: parsed.text } : {},
76
+ ...typeof parsed.messageId === "string" ? { messageId: parsed.messageId } : {}
77
+ };
78
+ }
79
+ function listReplyCacheEntries(sourcePath) {
80
+ const entriesByKey = /* @__PURE__ */ new Map();
81
+ for (const entry of readJsonl(sourcePath).map(parseReplyCacheEntry)) {
82
+ if (!entry) continue;
83
+ const ttlMs = remainingTtlMs(entry.timestamp, IMESSAGE_REPLY_CACHE_TTL_MS);
84
+ if (!ttlMs) continue;
85
+ const key = resolveIMessageReplyCacheEntryKey(entry.messageId);
86
+ entriesByKey.delete(key);
87
+ entriesByKey.set(key, {
88
+ value: entry,
89
+ ttlMs
90
+ });
91
+ }
92
+ return [...entriesByKey.entries()].slice(-IMESSAGE_REPLY_CACHE_MAX_ENTRIES).map(([key, entry]) => ({
93
+ key,
94
+ value: entry.value,
95
+ ttlMs: entry.ttlMs
96
+ }));
97
+ }
98
+ function listSentEchoEntries(sourcePath) {
99
+ return readJsonl(sourcePath).map(parseSentEchoEntry).filter((entry) => Boolean(entry)).slice(-256).flatMap((entry) => {
100
+ const ttlMs = remainingTtlMs(entry.timestamp, IMESSAGE_SENT_ECHOES_TTL_MS);
101
+ if (!ttlMs) return [];
102
+ return [{
103
+ key: resolveIMessageSentEchoEntryKey(entry),
104
+ value: entry,
105
+ ttlMs
106
+ }];
107
+ });
108
+ }
109
+ function resolveLegacyCatchupCursorPath(stateDir, accountId) {
110
+ const safePrefix = accountId.replace(/[^a-zA-Z0-9_-]/g, "_") || "account";
111
+ const hash = createHash("sha256").update(accountId, "utf8").digest("hex").slice(0, 12);
112
+ return path.join(stateDir, "imessage", "catchup", `${safePrefix}__${hash}.json`);
113
+ }
114
+ function listLegacyCatchupCursorPaths(stateDir) {
115
+ const dir = path.join(stateDir, "imessage", "catchup");
116
+ try {
117
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path.join(dir, entry.name));
118
+ } catch {
119
+ return [];
120
+ }
121
+ }
122
+ function normalizeCatchupCursor(raw) {
123
+ if (!raw || typeof raw !== "object") return null;
124
+ const value = raw;
125
+ if (typeof value.lastSeenMs !== "number" || !Number.isFinite(value.lastSeenMs) || typeof value.lastSeenRowid !== "number" || !Number.isFinite(value.lastSeenRowid)) return null;
126
+ const failureRetries = sanitizeCatchupFailureRetries(value.failureRetries);
127
+ const hasRetries = Object.keys(failureRetries).length > 0;
128
+ return {
129
+ lastSeenMs: value.lastSeenMs,
130
+ lastSeenRowid: value.lastSeenRowid,
131
+ updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : 0,
132
+ ...hasRetries ? { failureRetries } : {}
133
+ };
134
+ }
135
+ function readCatchupCursor(sourcePath) {
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
139
+ } catch (err) {
140
+ throw new Error(`Failed reading ${sourcePath}: ${String(err)}`, { cause: err });
141
+ }
142
+ const cursor = normalizeCatchupCursor(parsed);
143
+ if (!cursor) throw new Error(`Invalid iMessage catchup cursor: ${sourcePath}`);
144
+ return cursor;
145
+ }
146
+ function sanitizeCatchupFailureRetries(raw) {
147
+ if (!raw || typeof raw !== "object") return {};
148
+ const out = {};
149
+ for (const [guid, count] of Object.entries(raw)) if (typeof count === "number" && Number.isFinite(count) && count > 0) out[guid] = Math.floor(count);
150
+ return capFailureRetriesMap(out);
151
+ }
152
+ function shouldReplaceCatchupCursor(existingValue, incomingValue) {
153
+ const incoming = normalizeCatchupCursor(incomingValue);
154
+ if (!incoming) return false;
155
+ const existing = normalizeCatchupCursor(existingValue);
156
+ return !existing || incoming.lastSeenRowid > existing.lastSeenRowid || incoming.lastSeenRowid === existing.lastSeenRowid && incoming.lastSeenMs > existing.lastSeenMs;
157
+ }
158
+ function detectReplyCacheMigration(params) {
159
+ const stateDir = resolveMigrationStateDir(params);
160
+ const sourcePath = path.join(stateDir, "imessage", "reply-cache.jsonl");
161
+ if (!fileExists(sourcePath)) return [];
162
+ const plans = [];
163
+ plans.push({
164
+ kind: "plugin-state-import",
165
+ label: "iMessage reply short-id counter",
166
+ sourcePath,
167
+ targetPath: `plugin state:${IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE}`,
168
+ pluginId: "imessage",
169
+ namespace: IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE,
170
+ maxEntries: 1,
171
+ scopeKey: "",
172
+ stateDir,
173
+ preview: `- iMessage reply short-id counter: ${sourcePath} → plugin state (${IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE})`,
174
+ readEntries: () => {
175
+ const maxShortId = readReplyCacheMaxShortId(sourcePath);
176
+ return maxShortId > 0 ? [{
177
+ key: IMESSAGE_REPLY_CACHE_COUNTER_KEY,
178
+ value: { counter: maxShortId }
179
+ }] : [];
180
+ },
181
+ shouldReplaceExistingEntry: ({ existingValue, incomingValue }) => shouldReplaceReplyCounter(existingValue, incomingValue)
182
+ });
183
+ plans.push({
184
+ kind: "plugin-state-import",
185
+ label: "iMessage reply short-id cache",
186
+ sourcePath,
187
+ targetPath: `plugin state:${IMESSAGE_REPLY_CACHE_NAMESPACE}`,
188
+ pluginId: "imessage",
189
+ namespace: IMESSAGE_REPLY_CACHE_NAMESPACE,
190
+ maxEntries: IMESSAGE_REPLY_CACHE_MAX_ENTRIES,
191
+ scopeKey: "",
192
+ stateDir,
193
+ cleanupSource: "rename",
194
+ cleanupWhenEmpty: true,
195
+ preview: `- iMessage reply short-id cache: ${sourcePath} → plugin state (${IMESSAGE_REPLY_CACHE_NAMESPACE})`,
196
+ readEntries: () => listReplyCacheEntries(sourcePath)
197
+ });
198
+ return plans;
199
+ }
200
+ function detectSentEchoMigration(params) {
201
+ const stateDir = resolveMigrationStateDir(params);
202
+ const sourcePath = path.join(stateDir, "imessage", "sent-echoes.jsonl");
203
+ if (!fileExists(sourcePath)) return [];
204
+ return [{
205
+ kind: "plugin-state-import",
206
+ label: "iMessage sent-echo dedupe cache",
207
+ sourcePath,
208
+ targetPath: `plugin state:${IMESSAGE_SENT_ECHOES_NAMESPACE}`,
209
+ pluginId: "imessage",
210
+ namespace: IMESSAGE_SENT_ECHOES_NAMESPACE,
211
+ maxEntries: 256,
212
+ scopeKey: "",
213
+ stateDir,
214
+ cleanupSource: "rename",
215
+ cleanupWhenEmpty: true,
216
+ preview: `- iMessage sent-echo dedupe cache: ${sourcePath} → plugin state (${IMESSAGE_SENT_ECHOES_NAMESPACE})`,
217
+ readEntries: () => listSentEchoEntries(sourcePath)
218
+ }];
219
+ }
220
+ function detectCatchupCursorMigrations(params) {
221
+ const stateDir = resolveMigrationStateDir(params);
222
+ const accountIds = uniqueStrings([resolveDefaultIMessageAccountId(params.cfg), ...listIMessageAccountIds(params.cfg)].map((accountId) => resolveIMessageAccount({
223
+ cfg: params.cfg,
224
+ accountId
225
+ }).accountId));
226
+ const configuredPaths = new Set(accountIds.map((accountId) => resolveLegacyCatchupCursorPath(stateDir, accountId)));
227
+ const configuredPlans = accountIds.flatMap((accountId) => {
228
+ const sourcePath = resolveLegacyCatchupCursorPath(stateDir, accountId);
229
+ if (!fileExists(sourcePath)) return [];
230
+ return {
231
+ kind: "plugin-state-import",
232
+ label: "iMessage catchup cursor",
233
+ sourcePath,
234
+ targetPath: `plugin state:${IMESSAGE_CATCHUP_CURSOR_NAMESPACE}`,
235
+ pluginId: "imessage",
236
+ namespace: IMESSAGE_CATCHUP_CURSOR_NAMESPACE,
237
+ maxEntries: 256,
238
+ scopeKey: "",
239
+ stateDir,
240
+ cleanupSource: "rename",
241
+ preview: `- iMessage catchup cursor: ${sourcePath} → plugin state (${IMESSAGE_CATCHUP_CURSOR_NAMESPACE})`,
242
+ readEntries: () => {
243
+ const cursor = readCatchupCursor(sourcePath);
244
+ return [{
245
+ key: resolveIMessageCatchupCursorKey(accountId),
246
+ value: cursor
247
+ }];
248
+ },
249
+ shouldReplaceExistingEntry: (replaceParams) => shouldReplaceCatchupCursor(replaceParams.existingValue, replaceParams.incomingValue)
250
+ };
251
+ });
252
+ const orphanPlans = listLegacyCatchupCursorPaths(stateDir).filter((sourcePath) => !configuredPaths.has(sourcePath)).map((sourcePath) => ({
253
+ kind: "plugin-state-import",
254
+ label: "iMessage orphan catchup cursor",
255
+ sourcePath,
256
+ targetPath: `plugin state:${IMESSAGE_CATCHUP_CURSOR_NAMESPACE}`,
257
+ pluginId: "imessage",
258
+ namespace: IMESSAGE_CATCHUP_CURSOR_NAMESPACE,
259
+ maxEntries: 256,
260
+ scopeKey: "",
261
+ stateDir,
262
+ cleanupSource: "rename",
263
+ cleanupWhenEmpty: true,
264
+ preview: `- iMessage orphan catchup cursor: ${sourcePath} → archived legacy state`,
265
+ readEntries: () => []
266
+ }));
267
+ return [...configuredPlans, ...orphanPlans];
268
+ }
269
+ async function detectIMessageLegacyStateMigrations(params) {
270
+ return [
271
+ ...detectCatchupCursorMigrations(params),
272
+ ...detectReplyCacheMigration(params),
273
+ ...detectSentEchoMigration(params)
274
+ ];
275
+ }
276
+ //#endregion
277
+ export { detectIMessageLegacyStateMigrations };
@@ -1,4 +1,4 @@
1
- const Email = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[^"\\]|\\.)*")@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*|\[(?:IPv6:[a-f0-9:]+|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})\])$/i;
1
+ const Email = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[^"\\]|\\[\x20-\x7e])*")@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*|\[(?:IPv6:[a-f0-9:]+|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})\])$/i;
2
2
  /**
3
3
  * Returns true if the value is an Email
4
4
  * @specification https://datatracker.ietf.org/doc/html/rfc5322
@@ -1,6 +1,9 @@
1
1
  import * as FormatBidi from './format/bidi.mjs';
2
2
  import * as LabelUnicode from './label/unicode.mjs';
3
3
  import * as LabelPuny from './label/puny.mjs';
4
+ // ------------------------------------------------------------------
5
+ // IsLabel
6
+ // ------------------------------------------------------------------
4
7
  function IsValidLabelLength(value) {
5
8
  return value.length > 0 && value.length <= 63;
6
9
  }
@@ -8,9 +11,24 @@ function IsLabel(value) {
8
11
  return IsValidLabelLength(value) && (LabelPuny.IsPunyLabel(value) ||
9
12
  LabelUnicode.IsUnicodeLabel(value));
10
13
  }
14
+ // ------------------------------------------------------------------
15
+ // NormalizeHostname
16
+ //
17
+ // Normalizes a hostname for label validation. Normalizes for NFC
18
+ // such that equivalent codepoint sequences can be uniformly
19
+ // compared. It also removes codepoints that IDNA mapping ignores,
20
+ // and converts full stop variants to ASCII '.' so the hostname can
21
+ // be split into labels.
22
+ // ------------------------------------------------------------------
11
23
  function NormalizeHostname(value) {
12
- return value.normalize('NFC').replace(/[\u002E\u3002\uFF0E\uFF61]/g, '.');
24
+ return value
25
+ .normalize('NFC')
26
+ .replace(/[\u00ad\u034f\u180b-\u180d\u200b\ufe00-\ufe0f\u{e0100}-\u{e01ef}]/gu, '') // RE_IGNORED_MAPPING
27
+ .replace(/[\u002E\u3002\uFF0E\uFF61]/g, '.'); // RE_FULL_STOP_MAPPING
13
28
  }
29
+ // ------------------------------------------------------------------
30
+ // IsIdnHostname
31
+ // ------------------------------------------------------------------
14
32
  export function IsIdnHostname(value) {
15
33
  if (value.length === 0 || value.includes(' '))
16
34
  return false;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Returns true if the value is a json pointer
3
- * @specification
4
- * @source ajv-formats
3
+ * @specification https://datatracker.ietf.org/doc/html/rfc6901
4
+ * @source https://github.com/ajv-validator/ajv-formats
5
5
  */
6
6
  export declare function IsJsonPointer(value: string): boolean;
@@ -1,8 +1,8 @@
1
1
  const JsonPointer = /^(?:\/(?:[^~/]|~0|~1)*)*$/;
2
2
  /**
3
3
  * Returns true if the value is a json pointer
4
- * @specification
5
- * @source ajv-formats
4
+ * @specification https://datatracker.ietf.org/doc/html/rfc6901
5
+ * @source https://github.com/ajv-validator/ajv-formats
6
6
  */
7
7
  export function IsJsonPointer(value) {
8
8
  return JsonPointer.test(value);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Returns true if the value is a json pointer uri fragment
3
- * @specification
4
- * @source ajv-formats
3
+ * @specification https://datatracker.ietf.org/doc/html/rfc6901
4
+ * @source https://github.com/ajv-validator/ajv-formats
5
5
  */
6
6
  export declare function IsJsonPointerUriFragment(value: string): boolean;
@@ -1,8 +1,8 @@
1
1
  const JsonPointerUriFragment = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i;
2
2
  /**
3
3
  * Returns true if the value is a json pointer uri fragment
4
- * @specification
5
- * @source ajv-formats
4
+ * @specification https://datatracker.ietf.org/doc/html/rfc6901
5
+ * @source https://github.com/ajv-validator/ajv-formats
6
6
  */
7
7
  export function IsJsonPointerUriFragment(value) {
8
8
  return JsonPointerUriFragment.test(value);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Returns true if the value is a relative json pointer
3
- * @specification
4
- * @source ajv-formats
3
+ * @specification https://datatracker.ietf.org/doc/html/rfc6901
4
+ * @source https://github.com/ajv-validator/ajv-formats
5
5
  */
6
6
  export declare function IsRelativeJsonPointer(value: string): boolean;
@@ -1,8 +1,8 @@
1
1
  const RelativeJsonPointer = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/;
2
2
  /**
3
3
  * Returns true if the value is a relative json pointer
4
- * @specification
5
- * @source ajv-formats
4
+ * @specification https://datatracker.ietf.org/doc/html/rfc6901
5
+ * @source https://github.com/ajv-validator/ajv-formats
6
6
  */
7
7
  export function IsRelativeJsonPointer(value) {
8
8
  return RelativeJsonPointer.test(value);