@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.100

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 (123) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/LICENSE +92 -0
  3. package/README.md +101 -2
  4. package/bin/hellolynk-client-setup.mjs +2748 -0
  5. package/bin/hellolynk-mcp-config-shape.mjs +23 -0
  6. package/hellolynk-client-pack-manifest.json +619 -0
  7. package/hellolynk-client-pack-pin.json +4 -0
  8. package/package.json +58 -4
  9. package/plugins/hellolynk-codex/.app.json +3 -0
  10. package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
  11. package/plugins/hellolynk-codex/.mcp.json +19 -0
  12. package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
  13. package/plugins/hellolynk-codex/package.json +11 -0
  14. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
  15. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
  16. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
  17. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
  18. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
  19. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
  20. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  21. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
  22. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
  23. package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  24. package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
  25. package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
  26. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
  27. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
  28. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
  29. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
  30. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
  31. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
  32. package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
  33. package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
  34. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  35. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  36. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
  37. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
  38. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
  39. package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
  40. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  41. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
  42. package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
  43. package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
  44. package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
  45. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
  46. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
  47. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
  48. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
  49. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
  50. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
  51. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
  52. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
  53. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
  54. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
  55. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
  56. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
  57. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  58. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
  59. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
  60. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
  61. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
  62. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
  63. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  64. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
  65. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
  66. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  67. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  68. package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
  69. package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
  70. package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
  71. package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
  72. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
  73. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
  74. package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
  75. package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
  76. package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
  77. package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
  78. package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
  79. package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
  80. package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
  81. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  82. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  83. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  84. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  85. package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
  86. package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
  87. package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
  88. package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
  89. package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
  90. package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
  91. package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
  92. package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
  93. package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
  94. package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
  95. package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
  96. package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
  97. package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
  98. package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
  99. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
  100. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
  101. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
  102. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
  103. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
  104. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
  105. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
  106. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
  107. package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
  108. package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
  109. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
  110. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
  111. package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
  112. package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
  113. package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
  114. package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
  115. package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
  116. package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
  117. package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
  118. package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
  119. package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
  120. package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
  121. package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
  122. package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
  123. package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
@@ -0,0 +1,1365 @@
1
+ import crypto from "node:crypto";
2
+ import {
3
+ getMessageConversationBinding,
4
+ getOutgoingConversationIntent,
5
+ instanceConversationKey,
6
+ normalizeInstanceConversationBinding,
7
+ } from "./hellolynk-instance-conversation.mjs";
8
+ import {
9
+ HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
10
+ HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT,
11
+ HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION,
12
+ HELLOLYNK_WORKBENCH_MAX_QUERY_LENGTH,
13
+ } from "../workbench/contract.mjs";
14
+
15
+ export const HELLOLYNK_NEUTRAL_HOST_WORKBENCH_READ_MODEL_CONTRACT =
16
+ "hellolynk-neutral-host-workbench-read-model-v1";
17
+
18
+ const THREAD_PAGE_SIZE = 100;
19
+ const MESSAGE_PAGE_SIZE = 20;
20
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{1,198}$/;
21
+ const SAFE_REVISION = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,254}$/;
22
+ const HELLOLYNK_ID = /^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/;
23
+ const UNSAFE_TEXT = /[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]/u;
24
+ const FILTERS = new Set(["all", "unread", "archived"]);
25
+ const TRANSPORTS = new Set(["loopback-http", "mcp-app-rpc", "cursor-webview-rpc"]);
26
+ const HOST_STATES = new Set(["connecting", "online", "offline", "rebind-required", "error"]);
27
+ const RELATIONSHIP_METADATA_STATES = new Set(["loading", "fresh", "stale"]);
28
+ const CLIENT_TYPES = new Set(["codex", "cursor", "dsh", "openclaw", "trae-work", "workbuddy", "claude-code", "unknown"]);
29
+ const CLIENT_STATES = new Set(["connected", "disconnected", "stale"]);
30
+ const CONTACT_REQUEST_STATES = new Set([
31
+ "pending",
32
+ "accepted",
33
+ "rejected",
34
+ "cancelled",
35
+ "blocked",
36
+ "expired",
37
+ ]);
38
+ const MANUAL_RETRYABLE_OUTBOX_STATUSES = new Set([
39
+ "recipient_offline",
40
+ "failed_retryable",
41
+ "failed_permanent",
42
+ ]);
43
+ const MESSAGE_AUTHORIZATION_SCOPE = "relay.text.send";
44
+ const MESSAGE_INTENT_SOURCES = new Set([
45
+ "hellolynk_client_explicit_send",
46
+ "human_native_dialog",
47
+ "human_preauthorized_contact",
48
+ ]);
49
+ const DEFAULT_CAPABILITIES = Object.freeze([
50
+ "contacts.read",
51
+ "messages.read",
52
+ "outbox.read",
53
+ "search.local",
54
+ "security.read",
55
+ "threads.read",
56
+ ]);
57
+ const CAPABILITIES = new Set([
58
+ "contact.authorization.manage",
59
+ "contact.delete",
60
+ "contact.save",
61
+ "contact.rename",
62
+ "contact.instances.list",
63
+ "contact.instance.save",
64
+ "contact.instance.rename",
65
+ "contact.instance.delete",
66
+ "contact-request.clear-resolved",
67
+ "contact-request.recover-decision",
68
+ "contact-request.respond",
69
+ "contact-request.cancel",
70
+ "contact-request.send",
71
+ "contacts.read",
72
+ "messages.delete-read",
73
+ "messages.delete-sent",
74
+ "messages.mark-read",
75
+ "messages.mark-all-read",
76
+ "messages.auto-read.manage",
77
+ "messages.read",
78
+ "messages.request-send",
79
+ "outbox.read",
80
+ "outbox.request-retry",
81
+ "principal.search",
82
+ "relationship.end",
83
+ "relationships.read",
84
+ "block.remove",
85
+ "blocks.read",
86
+ "search.local",
87
+ "security.open-account",
88
+ "security.read",
89
+ "threads.read",
90
+ "threads.manage-local",
91
+ ]);
92
+
93
+ export class HelloLynkNeutralHostWorkbenchReadModelError extends Error {
94
+ constructor(code, options = {}) {
95
+ super(code, options.cause ? { cause: options.cause } : undefined);
96
+ this.name = "HelloLynkNeutralHostWorkbenchReadModelError";
97
+ this.code = code;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Build the single Host-owned read model used by every client transport.
103
+ * Messages use the current indexed store's bounded projections; this
104
+ * module has no write path and never retains a mutable store object.
105
+ */
106
+ export function createNeutralHostWorkbenchReadModel(options = {}) {
107
+ exactRecord(options, [
108
+ "subject",
109
+ "localDelegateLabels",
110
+ "hostGeneration",
111
+ "applicationChangeCursor",
112
+ "messageStore",
113
+ "contactStore",
114
+ "contactRequestStore",
115
+ "capabilities",
116
+ "hostStatus",
117
+ "relationshipMetadataState",
118
+ "connectedClients",
119
+ "changeEventTypes",
120
+ "snapshotRequired",
121
+ "now",
122
+ ], "neutral_host_workbench_options_invalid");
123
+
124
+ const subject = normalizeSubject(options.subject);
125
+ const localDelegateLabels = new Map();
126
+ for (const item of array(options.localDelegateLabels ?? [], "neutral_host_workbench_local_delegate_labels_invalid")) {
127
+ exactRecord(item, ["principalId", "delegateId", "delegateLabel"], "neutral_host_workbench_local_delegate_label_invalid");
128
+ const principalId = safeId(item.principalId, "neutral_host_workbench_local_delegate_label_invalid");
129
+ const delegateId = safeId(item.delegateId, "neutral_host_workbench_local_delegate_label_invalid");
130
+ const delegateLabel = optionalText(item.delegateLabel, 120, "neutral_host_workbench_local_delegate_label_invalid");
131
+ if (principalId !== subject.principalId || delegateLabel == null) continue;
132
+ if (localDelegateLabels.has(delegateId) && localDelegateLabels.get(delegateId) !== delegateLabel) {
133
+ fail("neutral_host_workbench_local_delegate_label_conflict");
134
+ }
135
+ localDelegateLabels.set(delegateId, delegateLabel);
136
+ }
137
+ const hostGeneration = safeId(options.hostGeneration, "neutral_host_workbench_generation_invalid");
138
+ const applicationChangeCursor = normalizeChangeCursor(options.applicationChangeCursor);
139
+ const indexedMessageStore = options.messageStore;
140
+ if (["readWorkbenchSummary", "readWorkbenchRevision", "queryConversationPage", "readLocalHistoryStats"]
141
+ .some((method) => typeof indexedMessageStore?.[method] !== "function")) {
142
+ fail("neutral_host_workbench_indexed_reader_incomplete");
143
+ }
144
+ const readers = Object.freeze({
145
+ contacts: contactReader(options.contactStore, subject.delegateId),
146
+ contactRequests: contactRequestReader(options.contactRequestStore),
147
+ outbox: outboxReader(indexedMessageStore, subject),
148
+ });
149
+ const capabilities = normalizeCapabilities(options.capabilities);
150
+ const relationshipMetadataFields = Object.hasOwn(options, "relationshipMetadataState") ? {
151
+ relationshipMetadataState: oneOf(options.relationshipMetadataState, RELATIONSHIP_METADATA_STATES,
152
+ "neutral_host_workbench_relationship_metadata_state_invalid"),
153
+ } : {};
154
+ const hostStatus = oneOf(
155
+ options.hostStatus ?? "online",
156
+ HOST_STATES,
157
+ "neutral_host_workbench_host_status_invalid",
158
+ );
159
+ const connectedClients = normalizeConnectedClients(options.connectedClients ?? []);
160
+ const changeEventTypes = normalizeEventTypes(options.changeEventTypes ?? []);
161
+ const snapshotRequired = options.snapshotRequired === true;
162
+ const now = typeof options.now === "function" ? options.now : () => new Date();
163
+ const generationBaseline = isoTimestamp(now(), "neutral_host_workbench_time_invalid");
164
+
165
+ async function readSnapshot(request = {}, source) {
166
+ const acceptedRequest = normalizeReadRequest(request);
167
+ const acceptedSource = normalizeSource(source);
168
+ return readIndexedSnapshot(acceptedRequest, acceptedSource);
169
+ }
170
+
171
+ async function readIndexedSnapshot(request, source) {
172
+ if (request.messageCursor != null && request.threadId == null) fail("neutral_host_workbench_message_cursor_thread_required");
173
+ if (request.historyBefore !== undefined && request.threadId == null) fail("neutral_host_workbench_thread_id_required");
174
+ const contactRequestRead = readers.contactRequests(Object.hasOwn(request, "contactRequestCursor")
175
+ ? { cursor: request.contactRequestCursor } : {});
176
+ const metadata = await Promise.all([
177
+ contactRequestRead.then(() => readers.contacts()),
178
+ contactRequestRead,
179
+ ]);
180
+ // Each indexed query holds a short store lock; a receiver may write between
181
+ // queries. Fence the complete local projection without holding that lock
182
+ // across a cloud lookup or a history scan. Retry reads only, reusing the
183
+ // relationship result rather than repeating its network/reconciliation work.
184
+ for (let attempt = 0; attempt < 3; attempt++) {
185
+ const revision = safeRevision(await indexedMessageStore.readWorkbenchRevision(),
186
+ "neutral_host_workbench_summary_invalid");
187
+ let snapshot, readError;
188
+ try { snapshot = await buildIndexedSnapshot(request, source, metadata); }
189
+ catch (error) { readError = error; }
190
+ const current = safeRevision(await indexedMessageStore.readWorkbenchRevision(),
191
+ "neutral_host_workbench_summary_invalid");
192
+ if (revision !== current) continue;
193
+ if (readError) throw readError;
194
+ return snapshot;
195
+ }
196
+ fail("neutral_host_workbench_snapshot_busy");
197
+ }
198
+
199
+ function validateRelationshipMetadata(value) {
200
+ const state = normalizeContactRequestState(value);
201
+ const requests = normalizeContactRequests(state.requests, subject);
202
+ const projection = {
203
+ contactRequests: requests.filter((item) => item.direction === "incoming"),
204
+ outgoingContactRequests: requests.filter((item) => item.direction === "outgoing"),
205
+ contactRequestNextCursor: state.nextCursor,
206
+ relationships: normalizeRelationships(state.relationships),
207
+ blocks: normalizeBlocks(state.blocks),
208
+ };
209
+ assertNoSensitiveKeys(projection);
210
+ return deepFreeze(projection);
211
+ }
212
+
213
+ async function buildIndexedSnapshot(request, source, metadata) {
214
+ const scope = { principalId: subject.principalId, delegateId: subject.delegateId, region: subject.region };
215
+ const [rawContacts, rawContactRequests] = metadata;
216
+ const [rawSummary, rawOutbox, searchThreadIds] = await Promise.all([
217
+ indexedMessageStore.readWorkbenchSummary(scope),
218
+ readers.outbox(),
219
+ request.query ? (typeof indexedMessageStore.searchWorkbenchThreads === "function"
220
+ // Filter and paginate the complete match set below. Truncating it here
221
+ // silently drops older-body matches in later conversations.
222
+ ? indexedMessageStore.searchWorkbenchThreads({ scope, query: request.rawQuery, limit: Number.MAX_SAFE_INTEGER })
223
+ : Promise.reject(new HelloLynkNeutralHostWorkbenchReadModelError("neutral_host_workbench_search_reader_invalid")))
224
+ : Promise.resolve(null),
225
+ ]);
226
+ exactRecord(rawSummary, ["threads", "revision", "latestTimestamp", "archivedThreadCount", "autoArchiveAfterHours", "hiddenThreadIds", "inboxReadBoundary"],
227
+ "neutral_host_workbench_summary_invalid");
228
+ if (Object.hasOwn(rawSummary, "inboxReadBoundary")
229
+ && (!Number.isSafeInteger(rawSummary.inboxReadBoundary) || rawSummary.inboxReadBoundary < 0)) fail("neutral_host_workbench_summary_invalid");
230
+ const summaryRows = array(rawSummary.threads, "neutral_host_workbench_summary_invalid");
231
+ const contacts = normalizeContacts(rawContacts, subject);
232
+ if (contacts.length > 500) fail("neutral_host_workbench_contacts_invalid");
233
+ const aiReadAuthorization = { contactRequests: true };
234
+ const contactByHelloLynkId = new Map(contacts.map((item) => [item.hellolynkId, item]));
235
+ const lastMessages = summaryRows.flatMap((row) => row?.lastMessage == null ? [] : [row.lastMessage]);
236
+ const incoming = normalizeMessages(lastMessages, subject, "messages", localDelegateLabels);
237
+ const outgoing = normalizeMessages(lastMessages, subject, "outbox");
238
+ const projected = buildThreads(deduplicateMessages([...incoming, ...outgoing]), contactByHelloLynkId, subject);
239
+ const threadById = new Map(projected.threads.map((thread) => [thread.threadId, thread]));
240
+ const summaryIds = new Set();
241
+ for (const row of summaryRows) {
242
+ exactRecord(row, ["threadId", "binding", "legacy", "peerPrincipalId", "peerHelloLynkId",
243
+ "lastMessage", "unreadCount", "updatedAt", "archived", "autoReadAfterAi", "recentSendFailureCount"],
244
+ "neutral_host_workbench_summary_invalid");
245
+ const threadId = safeId(row.threadId, "neutral_host_workbench_thread_id_invalid");
246
+ if (summaryIds.has(threadId)) fail("neutral_host_workbench_summary_invalid");
247
+ summaryIds.add(threadId);
248
+ let thread = threadById.get(threadId);
249
+ if (!thread && row.lastMessage == null && row.binding != null) {
250
+ const binding = normalizeInstanceConversationBinding(row.binding,
251
+ { region: subject.region, principalId: subject.principalId });
252
+ if (binding.localDelegateId !== subject.delegateId || instanceConversationKey(binding) !== threadId) {
253
+ fail("neutral_host_workbench_summary_scope_invalid");
254
+ }
255
+ const contact = contacts.find((item) => item.principalId === binding.peerPrincipalId) ?? null;
256
+ const instance = contact?.instances.find((item) => item.delegateId === binding.peerDelegateId) ?? null;
257
+ thread = {
258
+ threadId, conversationKind: "instance", localDelegateId: binding.localDelegateId,
259
+ peerDelegateId: binding.peerDelegateId, contactId: contact?.contactId ?? null,
260
+ peerPrincipalId: binding.peerPrincipalId, peerHelloLynkId: contact?.hellolynkId ?? null,
261
+ displayName: contact?.displayName ?? binding.peerPrincipalId,
262
+ delegateLabel: instance?.displayName ?? null, lastMessage: null,
263
+ unreadCount: 0, updatedAt: row.updatedAt,
264
+ };
265
+ threadById.set(threadId, thread);
266
+ }
267
+ if (!thread && row.lastMessage != null) fail("neutral_host_workbench_summary_scope_invalid");
268
+ if (!thread) continue;
269
+ if (!Number.isSafeInteger(row.unreadCount) || row.unreadCount < 0) fail("neutral_host_workbench_summary_invalid");
270
+ if (typeof row.archived !== "boolean") fail("neutral_host_workbench_summary_invalid");
271
+ thread.unreadCount = row.unreadCount;
272
+ thread.updatedAt = isoTimestamp(row.updatedAt, "neutral_host_workbench_summary_invalid");
273
+ thread.archived = row.archived === true;
274
+ if (Object.hasOwn(row, "recentSendFailureCount")) {
275
+ if (!Number.isSafeInteger(row.recentSendFailureCount) || row.recentSendFailureCount < 0
276
+ || row.recentSendFailureCount > 5) fail("neutral_host_workbench_summary_invalid");
277
+ thread.recentSendFailureCount = row.recentSendFailureCount;
278
+ }
279
+ if (Object.hasOwn(row, "autoReadAfterAi")) {
280
+ if (typeof row.autoReadAfterAi !== "boolean" || row.legacy === true) fail("neutral_host_workbench_summary_invalid");
281
+ thread.autoReadAfterAi = row.autoReadAfterAi;
282
+ }
283
+ }
284
+ const hiddenThreadIds = new Set(array(rawSummary.hiddenThreadIds ?? [], "neutral_host_workbench_summary_invalid")
285
+ .map((id) => safeId(id, "neutral_host_workbench_thread_id_invalid")));
286
+ const canonicalThreads = [...threadById.values()].filter((thread) => thread.conversationKind === "instance"
287
+ && thread.localDelegateId === subject.delegateId && !hiddenThreadIds.has(thread.threadId));
288
+ canonicalThreads.sort((left, right) => compareText(right.updatedAt, left.updatedAt)
289
+ || compareText(left.threadId, right.threadId));
290
+ const { contactRequests, outgoingContactRequests, contactRequestNextCursor, relationships, blocks }
291
+ = validateRelationshipMetadata(rawContactRequests);
292
+ const outbox = normalizeOutbox(rawOutbox, subject);
293
+ const security = { hostStatus, connectedClients };
294
+ // Keep the Host stream cursor captured when this read model was created.
295
+ // Together with hostGeneration it lets the UI recognize notifications
296
+ // already included in a snapshot; it never substitutes for revision checks.
297
+ const changeCursor = applicationChangeCursor;
298
+ const revision = `wbr_${digest(stableJson({
299
+ contractId: HELLOLYNK_NEUTRAL_HOST_WORKBENCH_READ_MODEL_CONTRACT,
300
+ hostGeneration, applicationChangeCursor, subject, capabilities,
301
+ storeRevision: safeRevision(String(rawSummary.revision), "neutral_host_workbench_summary_invalid"),
302
+ contacts, aiReadAuthorization, relationships, ...relationshipMetadataFields,
303
+ blocks, security, changeEventTypes, snapshotRequired,
304
+ })).slice(0, 48)}`;
305
+ const searchMatches = searchThreadIds == null ? null : new Set(array(searchThreadIds,
306
+ "neutral_host_workbench_search_invalid").map((id) => safeId(id, "neutral_host_workbench_thread_id_invalid")));
307
+ const matchingThreads = filterThreads(canonicalThreads, [], request.query, request.filter, searchMatches);
308
+ const selectedThread = request.threadId == null ? null
309
+ : canonicalThreads.find((item) => item.threadId === request.threadId);
310
+ if (request.threadId != null && !selectedThread) fail("neutral_host_workbench_thread_not_found");
311
+ const threadFingerprint = cursorFingerprint({ kind: "threads", hostGeneration, revision,
312
+ query: request.query, filter: request.filter });
313
+ const messageFingerprint = selectedThread ? cursorFingerprint({ kind: "indexed-messages-v1", hostGeneration, subject,
314
+ query: request.query, filter: request.filter, threadId: selectedThread.threadId }) : null;
315
+ const decodedMessageCursor = selectedThread ? decodeIndexedMessageCursor(request.messageCursor, messageFingerprint) : null;
316
+ const windowRead = ["from", "older-window", "newer-window"].includes(decodedMessageCursor?.direction);
317
+ let threadItems;
318
+ let threadNextCursor;
319
+ if (request.messageCursor != null && !windowRead) {
320
+ threadItems = [selectedThread];
321
+ threadNextCursor = null;
322
+ } else {
323
+ const offset = decodeCursor(request.threadCursor, "t", threadFingerprint, matchingThreads.length);
324
+ threadItems = matchingThreads.slice(offset, offset + THREAD_PAGE_SIZE);
325
+ const next = offset + threadItems.length;
326
+ threadNextCursor = next < matchingThreads.length ? encodeCursor("t", next, threadFingerprint) : null;
327
+ if (selectedThread && request.threadCursor == null
328
+ && !threadItems.some((item) => item.threadId === selectedThread.threadId)) threadItems = [...threadItems, selectedThread];
329
+ }
330
+ let messageItems = [];
331
+ let messageNextCursor = null;
332
+ let messagePreviousCursor = null;
333
+ let messageRefreshCursor = null;
334
+ let historySelection = null;
335
+ if (selectedThread) {
336
+ const fingerprint = messageFingerprint;
337
+ const decoded = decodedMessageCursor;
338
+ const messageLimit = windowRead ? 200 : MESSAGE_PAGE_SIZE;
339
+ const page = await indexedMessageStore.queryConversationPage({ scope, threadId: selectedThread.threadId,
340
+ anchor: decoded?.anchor ?? null, direction: decoded?.direction ?? "before", limit: messageLimit });
341
+ exactRecord(page, ["messages", "hasEarlier", "hasLater"], "neutral_host_workbench_message_page_invalid");
342
+ const rawPage = array(page.messages, "neutral_host_workbench_message_page_invalid");
343
+ if (rawPage.length > messageLimit || typeof page.hasEarlier !== "boolean" || typeof page.hasLater !== "boolean") {
344
+ fail("neutral_host_workbench_message_page_invalid");
345
+ }
346
+ const pageMessages = deduplicateMessages([
347
+ ...normalizeMessages(rawPage, subject, "messages", localDelegateLabels),
348
+ ...normalizeMessages(rawPage, subject, "outbox"),
349
+ ]);
350
+ const pageProjected = buildThreads(pageMessages, contactByHelloLynkId, subject).messages;
351
+ const pageKey = (item) => `${item.direction ?? "incoming"}\0${item.localMessageId ?? item.id}\0${item.clientMessageId ?? item.id}`;
352
+ const projectedById = new Map(pageProjected.map((item) => [pageKey(item), item]));
353
+ messageItems = rawPage.map((item) => projectedById.get(pageKey(item)));
354
+ if (messageItems.some((item) => !item || item.threadId !== selectedThread.threadId)) {
355
+ fail("neutral_host_workbench_message_page_scope_invalid");
356
+ }
357
+ if (messageItems.length) {
358
+ messageRefreshCursor = encodeIndexedMessageCursor("from", messageItems[0], fingerprint);
359
+ messageNextCursor = page.hasEarlier ? encodeIndexedMessageCursor("older-window", messageItems[0], fingerprint) : null;
360
+ messagePreviousCursor = page.hasLater
361
+ ? encodeIndexedMessageCursor("newer-window", messageItems.at(-1), fingerprint) : null;
362
+ }
363
+ if (Object.hasOwn(request, "historyBefore")) {
364
+ historySelection = await indexedMessageStore.readLocalHistoryStats({ scope,
365
+ threadId: selectedThread.threadId, before: request.historyBefore });
366
+ if (historySelection?.threadId !== selectedThread.threadId) fail("neutral_host_workbench_history_scope_invalid");
367
+ }
368
+ }
369
+ const latestTimestamp = rawSummary.latestTimestamp == null ? generationBaseline
370
+ : isoTimestamp(rawSummary.latestTimestamp, "neutral_host_workbench_summary_invalid");
371
+ if (!Number.isSafeInteger(rawSummary.archivedThreadCount) || rawSummary.archivedThreadCount < 0
372
+ || (rawSummary.autoArchiveAfterHours !== null && rawSummary.autoArchiveAfterHours !== 24)) {
373
+ fail("neutral_host_workbench_summary_invalid");
374
+ }
375
+ const generatedAt = deterministicGeneratedAt(generationBaseline,
376
+ [{ updatedAt: latestTimestamp }], contacts, relationships, blocks, outbox);
377
+ const snapshot = {
378
+ contractId: HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT,
379
+ schemaVersion: HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION,
380
+ compatibility: { minReaderVersion: HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION,
381
+ commandVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION },
382
+ hostGeneration, commandWindow: null, revision, changeCursor, generatedAt, source,
383
+ ...(Object.hasOwn(rawSummary, "inboxReadBoundary") ? { inboxReadBoundary: rawSummary.inboxReadBoundary } : {}),
384
+ subject, capabilities,
385
+ threads: { items: threadItems, nextCursor: threadNextCursor },
386
+ messages: { threadId: selectedThread?.threadId ?? null, items: messageItems,
387
+ nextCursor: messageNextCursor, previousCursor: messagePreviousCursor, refreshCursor: messageRefreshCursor },
388
+ outbox, contacts, aiReadAuthorization, contactRequests, outgoingContactRequests,
389
+ contactRequestNextCursor, relationships,
390
+ ...relationshipMetadataFields, blocks, security,
391
+ localHistory: { autoArchiveAfterHours: rawSummary.autoArchiveAfterHours,
392
+ archivedThreadCount: rawSummary.archivedThreadCount, selection: historySelection },
393
+ changes: { cursor: changeCursor, snapshotRequired, eventTypes: changeEventTypes },
394
+ };
395
+ assertNoSensitiveKeys(snapshot);
396
+ return deepFreeze(snapshot);
397
+ }
398
+
399
+ return Object.freeze({
400
+ contractId: HELLOLYNK_NEUTRAL_HOST_WORKBENCH_READ_MODEL_CONTRACT,
401
+ readSnapshot,
402
+ // Background display refreshes reuse the same projection validation without
403
+ // reading unrelated message, outbox or local-contact stores.
404
+ validateRelationshipMetadata,
405
+ });
406
+ }
407
+
408
+ function normalizeReadRequest(value) {
409
+ exactRecord(
410
+ value,
411
+ ["threadId", "threadCursor", "messageCursor", "contactRequestCursor", "query", "filter", "historyBefore"],
412
+ "neutral_host_workbench_read_request_invalid",
413
+ );
414
+ const rawQuery = value.query ?? "";
415
+ return Object.freeze({
416
+ threadId: value.threadId == null
417
+ ? null
418
+ : safeId(value.threadId, "neutral_host_workbench_thread_id_invalid"),
419
+ threadCursor: value.threadCursor == null
420
+ ? null
421
+ : safeRevision(value.threadCursor, "neutral_host_workbench_thread_cursor_invalid"),
422
+ messageCursor: value.messageCursor == null
423
+ ? null
424
+ : safeMessageCursor(value.messageCursor),
425
+ ...(Object.hasOwn(value, "historyBefore") ? { historyBefore: value.historyBefore == null ? null
426
+ : isoTimestamp(value.historyBefore, "neutral_host_workbench_history_before_invalid") } : {}),
427
+ ...(Object.hasOwn(value, "contactRequestCursor") ? {
428
+ contactRequestCursor: value.contactRequestCursor == null
429
+ ? null
430
+ : contactRequestCursor(
431
+ value.contactRequestCursor,
432
+ "neutral_host_workbench_contact_request_cursor_invalid",
433
+ ),
434
+ } : {}),
435
+ query: normalizeQuery(rawQuery),
436
+ rawQuery,
437
+ filter: oneOf(value.filter ?? "all", FILTERS, "neutral_host_workbench_filter_invalid"),
438
+ });
439
+ }
440
+
441
+ function normalizeSubject(value) {
442
+ exactRecord(value, [
443
+ "region",
444
+ "principalId",
445
+ "principalLabel",
446
+ "hellolynkId",
447
+ "delegateId",
448
+ "delegateLabel",
449
+ "hostInstallationId",
450
+ "authorizationVersion",
451
+ ], "neutral_host_workbench_subject_invalid");
452
+ const authorizationVersion = Number(value.authorizationVersion);
453
+ if (!Number.isSafeInteger(authorizationVersion) || authorizationVersion < 1) {
454
+ fail("neutral_host_workbench_authorization_version_invalid");
455
+ }
456
+ return deepFreeze({
457
+ region: safeId(value.region, "neutral_host_workbench_subject_invalid"),
458
+ principalId: safeId(value.principalId, "neutral_host_workbench_subject_invalid"),
459
+ principalLabel: optionalText(value.principalLabel, 120, "neutral_host_workbench_subject_invalid"),
460
+ hellolynkId: safeHelloLynkId(value.hellolynkId, "neutral_host_workbench_subject_invalid"),
461
+ delegateId: safeId(value.delegateId, "neutral_host_workbench_subject_invalid"),
462
+ delegateLabel: optionalText(value.delegateLabel, 120, "neutral_host_workbench_subject_invalid"),
463
+ hostInstallationId: safeId(value.hostInstallationId, "neutral_host_workbench_subject_invalid"),
464
+ authorizationVersion,
465
+ });
466
+ }
467
+
468
+ function normalizeSource(value) {
469
+ exactRecord(value, ["mode", "transport", "clientId", "clientLabel"], "neutral_host_workbench_source_invalid");
470
+ if (value.mode !== "neutral-host" || !TRANSPORTS.has(value.transport)) {
471
+ fail("neutral_host_workbench_source_invalid");
472
+ }
473
+ return deepFreeze({
474
+ mode: "neutral-host",
475
+ transport: value.transport,
476
+ clientId: safeId(value.clientId, "neutral_host_workbench_source_invalid"),
477
+ clientLabel: text(value.clientLabel, 80, "neutral_host_workbench_source_invalid"),
478
+ });
479
+ }
480
+
481
+ function outboxReader(store, subject) {
482
+ if (typeof store.queryOutbox !== "function") fail("neutral_host_workbench_outbox_reader_invalid");
483
+ return async () => array(await store.queryOutbox({
484
+ scope: { principalId: subject.principalId, delegateId: subject.delegateId, region: subject.region },
485
+ limit: 100, cursor: null,
486
+ }), "neutral_host_workbench_outbox_invalid");
487
+ }
488
+
489
+ function contactReader(store, delegateId) {
490
+ if (!store) fail("neutral_host_workbench_contact_reader_invalid");
491
+ if (typeof store.listForDelegate === "function") {
492
+ return async () => array(
493
+ await store.listForDelegate(delegateId),
494
+ "neutral_host_workbench_contacts_invalid",
495
+ );
496
+ }
497
+ if (typeof store.list === "function") {
498
+ return async () => array(await store.list(), "neutral_host_workbench_contacts_invalid");
499
+ }
500
+ fail("neutral_host_workbench_contact_reader_invalid");
501
+ }
502
+
503
+ function contactRequestReader(store) {
504
+ if (!store || typeof store.listForHuman !== "function") {
505
+ fail("neutral_host_workbench_contact_request_reader_invalid");
506
+ }
507
+ return async (input) => store.listForHuman(input);
508
+ }
509
+
510
+ function normalizeContactRequestState(value) {
511
+ if (Array.isArray(value)) {
512
+ return { requests: value, nextCursor: null, relationships: [], blocks: [] };
513
+ }
514
+ exactRecord(
515
+ value,
516
+ ["requests", "nextCursor", "relationships", "blocks"],
517
+ "neutral_host_workbench_contact_request_state_invalid",
518
+ );
519
+ return {
520
+ requests: array(value.requests, "neutral_host_workbench_contact_requests_invalid"),
521
+ nextCursor: value.nextCursor == null
522
+ ? null
523
+ : contactRequestCursor(
524
+ value.nextCursor,
525
+ "neutral_host_workbench_contact_request_cursor_invalid",
526
+ ),
527
+ relationships: array(
528
+ value.relationships,
529
+ "neutral_host_workbench_relationships_invalid",
530
+ ),
531
+ blocks: array(value.blocks, "neutral_host_workbench_blocks_invalid"),
532
+ };
533
+ }
534
+
535
+ function normalizeMessages(value, subject, origin, localDelegateLabels = null) {
536
+ return array(value, "neutral_host_workbench_messages_invalid")
537
+ .filter((item) => {
538
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
539
+ fail("neutral_host_workbench_message_invalid");
540
+ }
541
+ if (item.messageType === "local-deleted") return false;
542
+ if (origin === "messages") return (item.direction ?? "incoming") === "incoming";
543
+ return (item.direction ?? "outgoing") === "outgoing";
544
+ })
545
+ .filter((item) => origin !== "outbox"
546
+ || item.senderPrincipalId === subject.principalId)
547
+ .map((item) => {
548
+ const direction = origin === "messages" ? "incoming" : "outgoing";
549
+ let conversationBinding = null;
550
+ try {
551
+ const message = { ...item, direction };
552
+ const identityContext = { region: subject.region, principalId: subject.principalId };
553
+ conversationBinding = getMessageConversationBinding(message, identityContext)
554
+ || getOutgoingConversationIntent(message, identityContext);
555
+ } catch {
556
+ // Invalid or contradictory old evidence remains readable only in
557
+ // legacy history. The current Host instance cannot repair its identity.
558
+ }
559
+ const peerPrincipalId = direction === "incoming"
560
+ ? optionalSafeId(item.peerPrincipalId ?? item.senderPrincipalId ?? conversationBinding?.peerPrincipalId)
561
+ : optionalSafeId(item.peerPrincipalId ?? item.recipientPrincipalId ?? conversationBinding?.peerPrincipalId);
562
+ const peerHelloLynkId = direction === "incoming"
563
+ ? optionalHelloLynkId(item.peerHelloLynkId ?? item.senderHelloLynkId)
564
+ : optionalHelloLynkId(item.recipientHelloLynkId ?? item.peerHelloLynkId);
565
+ requirePeer(peerPrincipalId, peerHelloLynkId);
566
+ const senderPrincipalId = optionalSafeId(item.senderPrincipalId);
567
+ const senderDelegateId = optionalSafeId(item.senderDelegateId);
568
+ if (direction === "incoming" && senderPrincipalId != null && peerPrincipalId != null
569
+ && senderPrincipalId !== peerPrincipalId) {
570
+ fail("neutral_host_workbench_message_sender_invalid");
571
+ }
572
+ const provenance = normalizeMessageProvenance(item.authorizationScope, item.intentSource);
573
+ if (provenance.authorizationScope != null
574
+ && (senderPrincipalId == null || senderDelegateId == null)) {
575
+ fail("neutral_host_workbench_message_provenance_invalid");
576
+ }
577
+ const createdAt = isoTimestamp(
578
+ item.createdAt ?? item.receivedAt,
579
+ "neutral_host_workbench_message_time_invalid",
580
+ );
581
+ const updatedAt = isoTimestamp(
582
+ item.updatedAt ?? item.readAt ?? item.receivedAt ?? item.createdAt,
583
+ "neutral_host_workbench_message_time_invalid",
584
+ );
585
+ const unread = direction === "incoming"
586
+ ? (typeof item.unread === "boolean" ? item.unread : item.status === "received")
587
+ : false;
588
+ return {
589
+ localMessageId: safeId(
590
+ item.localMessageId ?? item.id,
591
+ "neutral_host_workbench_message_id_invalid",
592
+ ),
593
+ clientMessageId: safeId(
594
+ item.clientMessageId ?? item.id,
595
+ "neutral_host_workbench_message_id_invalid",
596
+ ),
597
+ direction,
598
+ conversationBinding,
599
+ peerPrincipalId,
600
+ peerHelloLynkId,
601
+ senderPrincipalId,
602
+ senderDelegateId,
603
+ authorizationScope: provenance.authorizationScope,
604
+ intentSource: provenance.intentSource,
605
+ senderPrincipalDisplayName: optionalText(
606
+ item.senderPrincipalDisplayName,
607
+ 160,
608
+ "neutral_host_workbench_message_label_invalid",
609
+ ),
610
+ senderDelegateDisplayName: optionalText(
611
+ item.senderDelegateDisplayName,
612
+ 120,
613
+ "neutral_host_workbench_message_label_invalid",
614
+ ),
615
+ senderDelegateFallbackLabel: direction === "incoming" && conversationBinding != null
616
+ && senderPrincipalId === subject.principalId
617
+ && conversationBinding.peerPrincipalId === subject.principalId
618
+ && senderDelegateId === conversationBinding.peerDelegateId
619
+ ? localDelegateLabels?.get(senderDelegateId) ?? null : null,
620
+ text: text(item.text ?? "", 64 * 1024, "neutral_host_workbench_message_text_invalid", true),
621
+ ...(direction === "incoming" && item.automaticDeliveryState === "expired" ? { automaticDeliveryState: "expired" } : {}),
622
+ status: errorCode(
623
+ item.status ?? (direction === "incoming" ? (unread ? "received" : "fetched") : "pending"),
624
+ "neutral_host_workbench_message_status_invalid",
625
+ ),
626
+ unread,
627
+ createdAt,
628
+ updatedAt,
629
+ };
630
+ })
631
+ // A verified binding belongs to one local AI. Sharing the principal's
632
+ // physical store must not project that AI's messages into another AI view.
633
+ // Unbound legacy rows stay read-only; current selection is not evidence
634
+ // from which to invent their original recipient.
635
+ .filter((message) => message.conversationBinding == null
636
+ || message.conversationBinding.localDelegateId === subject.delegateId);
637
+ }
638
+
639
+ function deduplicateMessages(messages) {
640
+ const byKey = new Map();
641
+ for (const message of messages) {
642
+ const key = `${message.direction}\0${message.localMessageId}\0${message.clientMessageId}`;
643
+ const previous = byKey.get(key);
644
+ if (previous && stableJson(previous) !== stableJson(message)) {
645
+ fail("neutral_host_workbench_message_conflict");
646
+ }
647
+ byKey.set(key, message);
648
+ }
649
+ return [...byKey.values()];
650
+ }
651
+
652
+ function normalizeContacts(value, subject) {
653
+ const contacts = array(value, "neutral_host_workbench_contacts_invalid").map((item) => {
654
+ record(item, "neutral_host_workbench_contact_invalid");
655
+ const principalId = optionalSafeId(item.principalId);
656
+ const hellolynkId = safeHelloLynkId(item.hellolynkId, "neutral_host_workbench_contact_hellolynk_id_invalid");
657
+ const instances = array(item.instances ?? [], "neutral_host_workbench_contact_instances_invalid").map((instance) => {
658
+ record(instance, "neutral_host_workbench_contact_instance_invalid");
659
+ if (principalId == null) fail("neutral_host_workbench_contact_instance_principal_required");
660
+ const delegateId = safeId(instance.delegateId, "neutral_host_workbench_contact_instance_invalid");
661
+ const region = subject.region === "international" ? "intl" : subject.region;
662
+ const conversationKey = instanceConversationKey(normalizeInstanceConversationBinding({
663
+ schemaVersion: 1, region, localPrincipalId: subject.principalId,
664
+ localDelegateId: subject.delegateId, peerPrincipalId: principalId, peerDelegateId: delegateId,
665
+ }, { region: subject.region, principalId: subject.principalId }));
666
+ return {
667
+ delegateId,
668
+ clientType: oneOf(instance.clientType, CLIENT_TYPES, "neutral_host_workbench_contact_instance_invalid"),
669
+ displayName: text(instance.displayName, 120, "neutral_host_workbench_contact_instance_invalid"),
670
+ createdAt: isoTimestamp(instance.createdAt, "neutral_host_workbench_contact_instance_invalid"),
671
+ updatedAt: isoTimestamp(instance.updatedAt, "neutral_host_workbench_contact_instance_invalid"),
672
+ autonomousSendAuthorized: item.autonomousSendAuthorized === true && instance.autonomousSendAuthorized === true,
673
+ continuousReadAuthorized: true,
674
+ conversationKey,
675
+ };
676
+ }).filter((instance) => principalId !== subject.principalId
677
+ || instance.delegateId !== subject.delegateId);
678
+ return {
679
+ contactId: safeId(item.contactId, "neutral_host_workbench_contact_id_invalid"),
680
+ principalId,
681
+ hellolynkId,
682
+ displayName: text(item.displayName, 160, "neutral_host_workbench_contact_label_invalid"),
683
+ autonomousSendAuthorized: item.autonomousSendAuthorized === true,
684
+ continuousReadAuthorized: true,
685
+ instances,
686
+ updatedAt: isoTimestamp(item.updatedAt, "neutral_host_workbench_contact_time_invalid"),
687
+ };
688
+ });
689
+ contacts.sort((left, right) => compareText(left.displayName, right.displayName)
690
+ || compareText(left.contactId, right.contactId));
691
+ assertUnique(contacts, (item) => item.contactId, "neutral_host_workbench_contact_conflict");
692
+ for (const contact of contacts) assertUnique(contact.instances, (item) => item.delegateId,
693
+ "neutral_host_workbench_contact_instance_conflict");
694
+ return contacts;
695
+ }
696
+
697
+ function normalizeContactRequests(value, subject) {
698
+ const requests = array(value, "neutral_host_workbench_contact_requests_invalid").map((item) => {
699
+ record(item, "neutral_host_workbench_contact_request_invalid");
700
+ if (item.humanOnly !== true || (item.modelVisibility != null && item.modelVisibility !== "none")) {
701
+ fail("neutral_host_workbench_contact_request_visibility_invalid");
702
+ }
703
+ const sender = item.sender && typeof item.sender === "object" ? item.sender : item;
704
+ const direction = oneOf(
705
+ item.direction ?? "incoming",
706
+ new Set(["incoming", "outgoing"]),
707
+ "neutral_host_workbench_contact_request_direction_invalid",
708
+ );
709
+ const other = item.otherPrincipal && typeof item.otherPrincipal === "object"
710
+ ? item.otherPrincipal
711
+ : sender;
712
+ const state = oneOf(
713
+ item.state ?? item.status,
714
+ CONTACT_REQUEST_STATES,
715
+ "neutral_host_workbench_contact_request_state_invalid",
716
+ );
717
+ const hasDecisionOutcome = Object.hasOwn(item, "decisionOutcome");
718
+ const hasDecisionAction = Object.hasOwn(item, "decisionAction");
719
+ if (hasDecisionOutcome !== hasDecisionAction) {
720
+ fail("neutral_host_workbench_contact_request_decision_invalid");
721
+ }
722
+ const decisionOutcome = hasDecisionOutcome
723
+ ? item.decisionOutcome
724
+ : null;
725
+ const decisionAction = hasDecisionAction ? item.decisionAction : null;
726
+ const actionReady = hasDecisionOutcome ? item.actionReady === true : false;
727
+ if ((hasDecisionOutcome && typeof item.actionReady !== "boolean")
728
+ || !new Set([null, "unknown", "recovery-required"]).has(decisionOutcome)
729
+ || (decisionAction != null && !new Set(["accept", "reject", "block"]).has(decisionAction))
730
+ || (actionReady && (state !== "pending"
731
+ || decisionOutcome != null
732
+ || decisionAction != null))
733
+ || (state !== "pending" && actionReady)
734
+ || ((decisionOutcome === "unknown") !== (decisionAction != null))
735
+ || (decisionOutcome != null && (state !== "pending" || actionReady))
736
+ ) {
737
+ fail("neutral_host_workbench_contact_request_decision_invalid");
738
+ }
739
+ return {
740
+ requestId: safeId(
741
+ item.serverContactRequestId ?? item.requestId ?? item.localRecordId,
742
+ "neutral_host_workbench_contact_request_id_invalid",
743
+ ),
744
+ senderHelloLynkId: safeHelloLynkId(
745
+ direction === "incoming"
746
+ ? (sender.hellolynkId ?? item.senderHelloLynkId ?? other.hellolynkId)
747
+ : subject.hellolynkId,
748
+ "neutral_host_workbench_contact_request_sender_invalid",
749
+ ),
750
+ senderLabel: optionalText(
751
+ direction === "incoming"
752
+ ? (sender.verifiedDisplayName ?? item.senderLabel
753
+ ?? item.senderPrincipalDisplayName ?? other.displayName)
754
+ : subject.principalLabel,
755
+ 160,
756
+ "neutral_host_workbench_contact_request_label_invalid",
757
+ ),
758
+ direction,
759
+ otherHelloLynkId: safeHelloLynkId(
760
+ other.hellolynkId ?? item.otherHelloLynkId ?? item.senderHelloLynkId,
761
+ "neutral_host_workbench_contact_request_sender_invalid",
762
+ ),
763
+ otherLabel: optionalText(
764
+ other.displayName ?? other.verifiedDisplayName ?? item.otherLabel
765
+ ?? item.senderLabel ?? item.senderPrincipalDisplayName,
766
+ 160,
767
+ "neutral_host_workbench_contact_request_label_invalid",
768
+ ),
769
+ state,
770
+ actionReady,
771
+ decisionOutcome,
772
+ decisionAction,
773
+ humanOnly: true,
774
+ ...(item.introText == null ? {} : {
775
+ introText: text(item.introText, 80, "neutral_host_workbench_contact_request_intro_invalid", true),
776
+ }),
777
+ receivedAt: isoTimestamp(
778
+ item.receivedAt ?? item.createdAt,
779
+ "neutral_host_workbench_contact_request_time_invalid",
780
+ ),
781
+ };
782
+ });
783
+ requests.sort((left, right) => compareText(right.receivedAt, left.receivedAt)
784
+ || compareText(left.requestId, right.requestId));
785
+ assertUnique(requests, (item) => item.requestId, "neutral_host_workbench_contact_request_conflict");
786
+ return requests;
787
+ }
788
+
789
+ function normalizeRelationships(value) {
790
+ const relationships = array(value, "neutral_host_workbench_relationships_invalid")
791
+ .map((item) => {
792
+ record(item, "neutral_host_workbench_relationship_invalid");
793
+ const other = record(
794
+ item.otherPrincipal,
795
+ "neutral_host_workbench_relationship_invalid",
796
+ );
797
+ const status = oneOf(
798
+ item.status,
799
+ new Set(["active", "ended"]),
800
+ "neutral_host_workbench_relationship_status_invalid",
801
+ );
802
+ const endedAt = item.endedAt == null
803
+ ? null
804
+ : isoTimestamp(item.endedAt, "neutral_host_workbench_relationship_time_invalid");
805
+ if ((status === "active") !== (endedAt == null)) {
806
+ fail("neutral_host_workbench_relationship_status_invalid");
807
+ }
808
+ return {
809
+ relationshipId: safeId(
810
+ item.relationshipId,
811
+ "neutral_host_workbench_relationship_id_invalid",
812
+ ),
813
+ otherHelloLynkId: safeHelloLynkId(
814
+ other.hellolynkId,
815
+ "neutral_host_workbench_relationship_peer_invalid",
816
+ ),
817
+ otherLabel: optionalText(
818
+ other.displayName,
819
+ 160,
820
+ "neutral_host_workbench_relationship_peer_invalid",
821
+ ),
822
+ status,
823
+ createdAt: isoTimestamp(
824
+ item.createdAt,
825
+ "neutral_host_workbench_relationship_time_invalid",
826
+ ),
827
+ updatedAt: isoTimestamp(
828
+ item.updatedAt,
829
+ "neutral_host_workbench_relationship_time_invalid",
830
+ ),
831
+ endedAt,
832
+ };
833
+ });
834
+ relationships.sort((left, right) => compareText(right.updatedAt, left.updatedAt)
835
+ || compareText(left.relationshipId, right.relationshipId));
836
+ assertUnique(
837
+ relationships,
838
+ (item) => item.relationshipId,
839
+ "neutral_host_workbench_relationship_conflict",
840
+ );
841
+ return relationships;
842
+ }
843
+
844
+ function normalizeBlocks(value) {
845
+ const blocks = array(value, "neutral_host_workbench_blocks_invalid").map((item) => {
846
+ record(item, "neutral_host_workbench_block_invalid");
847
+ const other = record(item.otherPrincipal, "neutral_host_workbench_block_invalid");
848
+ if (item.status !== "active") fail("neutral_host_workbench_block_status_invalid");
849
+ return {
850
+ blockId: safeId(item.blockId, "neutral_host_workbench_block_id_invalid"),
851
+ otherHelloLynkId: safeHelloLynkId(
852
+ other.hellolynkId,
853
+ "neutral_host_workbench_block_peer_invalid",
854
+ ),
855
+ otherLabel: optionalText(
856
+ other.displayName,
857
+ 160,
858
+ "neutral_host_workbench_block_peer_invalid",
859
+ ),
860
+ status: "active",
861
+ createdAt: isoTimestamp(item.createdAt, "neutral_host_workbench_block_time_invalid"),
862
+ };
863
+ });
864
+ blocks.sort((left, right) => compareText(right.createdAt, left.createdAt)
865
+ || compareText(left.blockId, right.blockId));
866
+ assertUnique(blocks, (item) => item.blockId, "neutral_host_workbench_block_conflict");
867
+ return blocks;
868
+ }
869
+
870
+ function normalizeOutbox(value, subject) {
871
+ const rawByMessageKey = new Map();
872
+ for (const item of array(value, "neutral_host_workbench_outbox_invalid")) {
873
+ if (!item || typeof item !== "object" || Array.isArray(item)
874
+ || (item.direction ?? "outgoing") !== "outgoing") continue;
875
+ if (item.senderPrincipalId !== subject.principalId) continue;
876
+ const localMessageId = safeId(
877
+ item.localMessageId ?? item.id,
878
+ "neutral_host_workbench_message_id_invalid",
879
+ );
880
+ const clientMessageId = safeId(
881
+ item.clientMessageId ?? item.id,
882
+ "neutral_host_workbench_message_id_invalid",
883
+ );
884
+ rawByMessageKey.set(`${localMessageId}\0${clientMessageId}`, item);
885
+ }
886
+ const outbox = normalizeMessages(value, subject, "outbox").map((message) => {
887
+ const raw = rawByMessageKey.get(`${message.localMessageId}\0${message.clientMessageId}`);
888
+ if (!raw) fail("neutral_host_workbench_outbox_invalid");
889
+ const status = message.status;
890
+ const unknown = status === "delivery_state_unknown" || status === "unknown";
891
+ const retryAllowed = !unknown
892
+ && message.conversationBinding != null
893
+ && message.conversationBinding.localDelegateId === subject.delegateId
894
+ && MANUAL_RETRYABLE_OUTBOX_STATUSES.has(status)
895
+ && raw.senderPrincipalId === subject.principalId
896
+ && raw.senderDelegateId === subject.delegateId
897
+ && raw.lastErrorCode !== "reply_target_deleted"
898
+ && raw.retryAllowed !== false;
899
+ return {
900
+ clientMessageId: message.clientMessageId,
901
+ threadId: messageThreadId(subject, message),
902
+ recipientHelloLynkId: message.peerHelloLynkId,
903
+ localDelegateId: message.conversationBinding?.localDelegateId ?? null,
904
+ recipientDelegateId: message.conversationBinding?.peerDelegateId ?? null,
905
+ text: message.text,
906
+ status,
907
+ retryAllowed,
908
+ errorCategory: raw.lastErrorCode === "reply_target_deleted" && raw.resendClientMessageId
909
+ ? "reply_target_deleted_resent" : redactErrorCategory(raw?.errorCategory ?? raw?.lastErrorCode, status),
910
+ createdAt: message.createdAt,
911
+ updatedAt: message.updatedAt,
912
+ };
913
+ });
914
+ outbox.sort((left, right) => compareText(right.updatedAt, left.updatedAt)
915
+ || compareText(left.clientMessageId, right.clientMessageId));
916
+ assertUnique(outbox, (item) => item.clientMessageId, "neutral_host_workbench_outbox_conflict");
917
+ return outbox;
918
+ }
919
+
920
+ function buildThreads(messages, contactByHelloLynkId, subject) {
921
+ const threadMap = new Map();
922
+ const fallbackLabels = new Map();
923
+ const projectedMessages = [];
924
+ for (const message of messages) {
925
+ const binding = message.conversationBinding;
926
+ const threadId = messageThreadId(subject, message);
927
+ if (message.senderDelegateFallbackLabel != null) {
928
+ fallbackLabels.set(threadId, message.senderDelegateFallbackLabel);
929
+ }
930
+ const candidateContact = message.peerHelloLynkId == null ? null : contactByHelloLynkId.get(message.peerHelloLynkId) ?? null;
931
+ const contact = binding && candidateContact?.principalId != null
932
+ && candidateContact.principalId !== binding.peerPrincipalId ? null : candidateContact;
933
+ const instance = binding == null ? null : contact?.instances.find((item) => item.delegateId === binding.peerDelegateId);
934
+ let thread = threadMap.get(threadId);
935
+ if (!thread) {
936
+ thread = {
937
+ threadId,
938
+ conversationKind: binding == null ? "legacy" : "instance",
939
+ localDelegateId: binding?.localDelegateId ?? null,
940
+ peerDelegateId: binding?.peerDelegateId ?? null,
941
+ contactId: contact?.contactId ?? null,
942
+ peerPrincipalId: message.peerPrincipalId,
943
+ peerHelloLynkId: message.peerHelloLynkId,
944
+ displayName: contact?.displayName
945
+ ?? (message.direction === "incoming" ? message.senderPrincipalDisplayName : null)
946
+ ?? message.peerHelloLynkId
947
+ ?? message.peerPrincipalId,
948
+ delegateLabel: instance?.displayName ?? (message.direction === "incoming" ? message.senderDelegateDisplayName : null),
949
+ lastMessage: null,
950
+ unreadCount: 0,
951
+ updatedAt: message.updatedAt,
952
+ };
953
+ threadMap.set(threadId, thread);
954
+ } else {
955
+ mergePeerIdentity(thread, message);
956
+ if (thread.contactId == null && contact) {
957
+ thread.contactId = contact.contactId;
958
+ thread.displayName = contact.displayName;
959
+ }
960
+ if (thread.delegateLabel == null && message.direction === "incoming" && message.senderDelegateDisplayName != null) {
961
+ thread.delegateLabel = message.senderDelegateDisplayName;
962
+ }
963
+ }
964
+ if (message.direction === "incoming" && message.unread) thread.unreadCount += 1;
965
+ const candidate = {
966
+ text: message.text,
967
+ direction: message.direction,
968
+ status: message.status,
969
+ createdAt: message.createdAt,
970
+ };
971
+ if (!thread.lastMessage
972
+ || compareText(candidate.createdAt, thread.lastMessage.createdAt) > 0
973
+ || (candidate.createdAt === thread.lastMessage.createdAt
974
+ && compareText(message.localMessageId, thread.lastMessageId) > 0)) {
975
+ thread.lastMessage = candidate;
976
+ thread.lastMessageId = message.localMessageId;
977
+ }
978
+ if (compareText(message.updatedAt, thread.updatedAt) > 0) thread.updatedAt = message.updatedAt;
979
+ projectedMessages.push({
980
+ localMessageId: message.localMessageId,
981
+ clientMessageId: message.clientMessageId,
982
+ threadId,
983
+ direction: message.direction,
984
+ senderPrincipalId: message.senderPrincipalId,
985
+ senderDelegateId: message.senderDelegateId,
986
+ intentSource: message.intentSource,
987
+ authorizationScope: message.authorizationScope,
988
+ text: message.text,
989
+ status: message.status,
990
+ ...(message.automaticDeliveryState === "expired" ? { automaticDeliveryState: "expired" } : {}),
991
+ unread: message.unread,
992
+ createdAt: message.createdAt,
993
+ updatedAt: message.updatedAt,
994
+ });
995
+ }
996
+ // Saved target instances are selectable before the first message. This
997
+ // explicit current selection never supplies missing identities to old rows.
998
+ for (const contact of contactByHelloLynkId.values()) {
999
+ if (contact.principalId == null) continue;
1000
+ const region = subject.region === "international" ? "intl" : subject.region;
1001
+ if (contact.hellolynkId.split(":")[1] !== region) continue;
1002
+ for (const instance of contact.instances) {
1003
+ const binding = normalizeInstanceConversationBinding({
1004
+ schemaVersion: 1,
1005
+ region,
1006
+ localPrincipalId: subject.principalId,
1007
+ localDelegateId: subject.delegateId,
1008
+ peerPrincipalId: contact.principalId,
1009
+ peerDelegateId: instance.delegateId,
1010
+ }, { region: subject.region, principalId: subject.principalId });
1011
+ const threadId = instanceConversationKey(binding);
1012
+ if (threadMap.has(threadId)) continue;
1013
+ threadMap.set(threadId, {
1014
+ threadId,
1015
+ conversationKind: "instance",
1016
+ localDelegateId: binding.localDelegateId,
1017
+ peerDelegateId: binding.peerDelegateId,
1018
+ contactId: contact.contactId,
1019
+ peerPrincipalId: contact.principalId,
1020
+ peerHelloLynkId: contact.hellolynkId,
1021
+ displayName: contact.displayName,
1022
+ delegateLabel: instance.displayName,
1023
+ lastMessage: null,
1024
+ unreadCount: 0,
1025
+ updatedAt: instance.updatedAt,
1026
+ });
1027
+ }
1028
+ }
1029
+ const threads = [...threadMap.values()].map(({ lastMessageId: _ignored, ...thread }) => ({
1030
+ ...thread,
1031
+ // Apply local display metadata only after inspecting all stored messages,
1032
+ // so an earlier missing label never overrides a later sender-provided name.
1033
+ delegateLabel: thread.delegateLabel ?? fallbackLabels.get(thread.threadId) ?? null,
1034
+ }));
1035
+ threads.sort((left, right) => compareText(right.updatedAt, left.updatedAt)
1036
+ || compareText(left.threadId, right.threadId));
1037
+ projectedMessages.sort((left, right) => compareText(left.createdAt, right.createdAt)
1038
+ || compareText(left.localMessageId, right.localMessageId));
1039
+ return { threads, messages: projectedMessages };
1040
+ }
1041
+
1042
+ function mergePeerIdentity(thread, message) {
1043
+ if (thread.peerPrincipalId != null && message.peerPrincipalId != null
1044
+ && thread.peerPrincipalId !== message.peerPrincipalId) {
1045
+ fail("neutral_host_workbench_thread_identity_conflict");
1046
+ }
1047
+ if (thread.peerHelloLynkId != null && message.peerHelloLynkId != null
1048
+ && thread.peerHelloLynkId !== message.peerHelloLynkId) {
1049
+ fail("neutral_host_workbench_thread_identity_conflict");
1050
+ }
1051
+ thread.peerPrincipalId ??= message.peerPrincipalId;
1052
+ thread.peerHelloLynkId ??= message.peerHelloLynkId;
1053
+ }
1054
+
1055
+ function stableThreadId(subject, peerPrincipalId, peerHelloLynkId) {
1056
+ requirePeer(peerPrincipalId, peerHelloLynkId);
1057
+ const peerKey = peerHelloLynkId == null ? `principal:${peerPrincipalId}` : `hellolynk:${peerHelloLynkId}`;
1058
+ const region = subject.region === "international" ? "intl" : subject.region;
1059
+ return `thread_${digest(`${region}\0${subject.principalId}\0${peerKey}`).slice(0, 40)}`;
1060
+ }
1061
+
1062
+ function messageThreadId(subject, message) {
1063
+ return message.conversationBinding == null
1064
+ ? stableThreadId(subject, message.peerPrincipalId, message.peerHelloLynkId)
1065
+ : instanceConversationKey(message.conversationBinding);
1066
+ }
1067
+
1068
+ function filterThreads(threads, messages, query, filter, indexedMatches = null) {
1069
+ const tokens = query.split(/\s+/u).filter(Boolean);
1070
+ const textByThread = new Map();
1071
+ for (const message of messages) {
1072
+ const values = textByThread.get(message.threadId) ?? [];
1073
+ values.push(message.text);
1074
+ textByThread.set(message.threadId, values);
1075
+ }
1076
+ return threads.filter((thread) => {
1077
+ if (filter === "archived" ? thread.archived !== true : thread.archived === true) return false;
1078
+ if (filter === "unread" && thread.unreadCount === 0) return false;
1079
+ if (tokens.length === 0) return true;
1080
+ if (indexedMatches?.has(thread.threadId)) return true;
1081
+ const haystack = normalizeSearchText([
1082
+ thread.displayName,
1083
+ thread.peerPrincipalId ?? "",
1084
+ thread.peerHelloLynkId ?? "",
1085
+ thread.delegateLabel ?? "",
1086
+ thread.lastMessage?.text ?? "",
1087
+ ...(textByThread.get(thread.threadId) ?? []),
1088
+ ].join("\n"));
1089
+ return tokens.every((token) => haystack.includes(token));
1090
+ });
1091
+ }
1092
+
1093
+ function encodeIndexedMessageCursor(direction, message, fingerprint) {
1094
+ const anchor = Buffer.from(JSON.stringify({ createdAt: message.createdAt,
1095
+ localMessageId: message.localMessageId }), "utf8").toString("base64url");
1096
+ return `wbc3.m.${direction}.${anchor}.${fingerprint}`;
1097
+ }
1098
+
1099
+ function decodeIndexedMessageCursor(value, fingerprint) {
1100
+ if (value == null) return null;
1101
+ const match = /^wbc3\.m\.(before|after|from|older-window|newer-window)\.([A-Za-z0-9_-]{8,512})\.([a-f0-9]{24})$/.exec(value);
1102
+ if (!match || match[3] !== fingerprint) fail("neutral_host_workbench_cursor_invalid");
1103
+ let anchor;
1104
+ try { anchor = JSON.parse(Buffer.from(match[2], "base64url").toString("utf8")); }
1105
+ catch { fail("neutral_host_workbench_cursor_invalid"); }
1106
+ exactRecord(anchor, ["createdAt", "localMessageId"], "neutral_host_workbench_cursor_invalid");
1107
+ return { direction: match[1], anchor: {
1108
+ createdAt: isoTimestamp(anchor.createdAt, "neutral_host_workbench_cursor_invalid"),
1109
+ localMessageId: safeId(anchor.localMessageId, "neutral_host_workbench_cursor_invalid"),
1110
+ } };
1111
+ }
1112
+
1113
+ function normalizeQuery(value) {
1114
+ if (typeof value !== "string" || value.length > HELLOLYNK_WORKBENCH_MAX_QUERY_LENGTH || UNSAFE_TEXT.test(value)) {
1115
+ fail("neutral_host_workbench_query_invalid");
1116
+ }
1117
+ return normalizeSearchText(value).replace(/\s+/gu, " ");
1118
+ }
1119
+
1120
+ function normalizeSearchText(value) {
1121
+ return String(value).normalize("NFKC").trim().toLocaleLowerCase("und");
1122
+ }
1123
+
1124
+ function encodeCursor(kind, offset, fingerprint) {
1125
+ return `wbc1.${kind}.${offset}.${fingerprint}`;
1126
+ }
1127
+
1128
+ function decodeCursor(value, kind, fingerprint, maximum) {
1129
+ if (value == null) return 0;
1130
+ const match = /^wbc1\.([tm])\.(0|[1-9][0-9]{0,8})\.([a-f0-9]{24})$/.exec(value);
1131
+ if (!match || match[1] !== kind || match[3] !== fingerprint) {
1132
+ fail("neutral_host_workbench_cursor_invalid");
1133
+ }
1134
+ const offset = Number(match[2]);
1135
+ if (!Number.isSafeInteger(offset) || offset <= 0 || offset >= maximum) {
1136
+ fail("neutral_host_workbench_cursor_invalid");
1137
+ }
1138
+ return offset;
1139
+ }
1140
+
1141
+ function cursorFingerprint(value) {
1142
+ return digest(stableJson(value)).slice(0, 24);
1143
+ }
1144
+
1145
+ function normalizeCapabilities(value) {
1146
+ const input = value == null ? DEFAULT_CAPABILITIES : array(value, "neutral_host_workbench_capabilities_invalid");
1147
+ const normalized = [...new Set(input.map((item) => {
1148
+ if (typeof item !== "string" || !CAPABILITIES.has(item)) {
1149
+ fail("neutral_host_workbench_capability_invalid");
1150
+ }
1151
+ return item;
1152
+ }))].sort();
1153
+ return deepFreeze(normalized);
1154
+ }
1155
+
1156
+ function normalizeConnectedClients(value) {
1157
+ const clients = array(value, "neutral_host_workbench_clients_invalid").map((item) => {
1158
+ exactRecord(
1159
+ item,
1160
+ ["clientId", "clientType", "state"],
1161
+ "neutral_host_workbench_client_invalid",
1162
+ );
1163
+ return {
1164
+ clientId: safeId(item.clientId, "neutral_host_workbench_client_invalid"),
1165
+ clientType: oneOf(item.clientType, CLIENT_TYPES, "neutral_host_workbench_client_invalid"),
1166
+ state: oneOf(item.state, CLIENT_STATES, "neutral_host_workbench_client_invalid"),
1167
+ };
1168
+ });
1169
+ clients.sort((left, right) => compareText(left.clientId, right.clientId));
1170
+ assertUnique(clients, (item) => item.clientId, "neutral_host_workbench_client_conflict");
1171
+ return clients;
1172
+ }
1173
+
1174
+ function normalizeEventTypes(value) {
1175
+ const result = [...new Set(array(value, "neutral_host_workbench_event_types_invalid")
1176
+ .map((item) => errorCode(item, "neutral_host_workbench_event_type_invalid")))].sort();
1177
+ if (result.length > 64) fail("neutral_host_workbench_event_types_invalid");
1178
+ return result;
1179
+ }
1180
+
1181
+ function normalizeChangeCursor(value) {
1182
+ const result = String(value ?? "0");
1183
+ if (!SAFE_REVISION.test(result)) fail("neutral_host_workbench_change_cursor_invalid");
1184
+ return result;
1185
+ }
1186
+
1187
+ function deterministicGeneratedAt(baseline, ...collections) {
1188
+ let latest = baseline;
1189
+ for (const collection of collections) {
1190
+ for (const item of collection) {
1191
+ const candidate = item.updatedAt ?? item.receivedAt ?? item.createdAt;
1192
+ if (candidate != null && compareText(candidate, latest) > 0) latest = candidate;
1193
+ }
1194
+ }
1195
+ return latest;
1196
+ }
1197
+
1198
+ function redactErrorCategory(value, status) {
1199
+ if (value === "reply_target_deleted") return "reply_target_deleted";
1200
+ if (["e2ee_envelope_expired", "e2ee_created_at_in_future"].includes(value)) return "message_time";
1201
+ if (value === "e2ee_peer_identity_pin_mismatch") return "device_identity_changed";
1202
+ if (value === "e2ee_recipient_external_contact_disabled") return "external_contact_disabled";
1203
+ const normalized = `${String(value || "")} ${status}`.trim().toLocaleLowerCase("und");
1204
+ if (!normalized) return null;
1205
+ if (/e2ee_request_timeout|e2ee_transport_error/u.test(normalized)) return "connectivity";
1206
+ if (/e2ee|encrypt|decrypt|key/u.test(normalized)) return "encryption";
1207
+ if (/auth|scope|permission|forbid/u.test(normalized)) return "authorization";
1208
+ if (/rate|thrott/u.test(normalized)) return "rate_limit";
1209
+ if (/offline|network|timeout|unavailable|connect/u.test(normalized)) return "connectivity";
1210
+ if (/input|invalid|payload|recipient|not_found/u.test(normalized)) return "validation";
1211
+ return "delivery";
1212
+ }
1213
+
1214
+ function requirePeer(principalId, hellolynkId) {
1215
+ if (principalId == null && hellolynkId == null) fail("neutral_host_workbench_peer_identity_missing");
1216
+ }
1217
+
1218
+ function optionalSafeId(value) {
1219
+ return value == null ? null : safeId(value, "neutral_host_workbench_id_invalid");
1220
+ }
1221
+
1222
+ function normalizeMessageProvenance(authorizationScope, intentSource) {
1223
+ if (authorizationScope == null && intentSource == null) {
1224
+ return { authorizationScope: null, intentSource: null };
1225
+ }
1226
+ if (
1227
+ authorizationScope !== MESSAGE_AUTHORIZATION_SCOPE
1228
+ || !MESSAGE_INTENT_SOURCES.has(intentSource)
1229
+ ) fail("neutral_host_workbench_message_provenance_invalid");
1230
+ return { authorizationScope, intentSource };
1231
+ }
1232
+
1233
+ function optionalHelloLynkId(value) {
1234
+ return value == null ? null : safeHelloLynkId(value, "neutral_host_workbench_hellolynk_id_invalid");
1235
+ }
1236
+
1237
+ function safeId(value, code) {
1238
+ if (typeof value !== "string" || !SAFE_ID.test(value)) fail(code);
1239
+ return value;
1240
+ }
1241
+
1242
+ function safeRevision(value, code) {
1243
+ if (typeof value !== "string" || !SAFE_REVISION.test(value)) fail(code);
1244
+ return value;
1245
+ }
1246
+
1247
+ function safeMessageCursor(value) {
1248
+ if (typeof value !== "string" || value.length > 800 || !/^[A-Za-z0-9._-]+$/.test(value)) {
1249
+ fail("neutral_host_workbench_message_cursor_invalid");
1250
+ }
1251
+ return value;
1252
+ }
1253
+
1254
+ function contactRequestCursor(value, code) {
1255
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{20,512}$/.test(value)) fail(code);
1256
+ return value;
1257
+ }
1258
+
1259
+ function safeHelloLynkId(value, code) {
1260
+ if (typeof value !== "string" || !HELLOLYNK_ID.test(value)) fail(code);
1261
+ return value;
1262
+ }
1263
+
1264
+ function text(value, maximum, code, emptyAllowed = false) {
1265
+ if (typeof value !== "string" || (!emptyAllowed && !value)
1266
+ || [...value].length > maximum || value.includes("\u0000")) {
1267
+ fail(code);
1268
+ }
1269
+ return value;
1270
+ }
1271
+
1272
+ function optionalText(value, maximum, code) {
1273
+ return value == null ? null : text(value, maximum, code);
1274
+ }
1275
+
1276
+ function errorCode(value, code) {
1277
+ const normalized = String(value || "");
1278
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,126}$/.test(normalized)) fail(code);
1279
+ return normalized;
1280
+ }
1281
+
1282
+ function isoTimestamp(value, code) {
1283
+ const time = value instanceof Date ? value.getTime() : Date.parse(String(value || ""));
1284
+ if (!Number.isFinite(time)) fail(code);
1285
+ return new Date(time).toISOString();
1286
+ }
1287
+
1288
+ function exactRecord(value, allowedKeys, code) {
1289
+ record(value, code);
1290
+ const keys = Object.keys(value).sort();
1291
+ const accepted = [...allowedKeys].sort();
1292
+ if (keys.some((key) => !accepted.includes(key))) fail(code);
1293
+ return value;
1294
+ }
1295
+
1296
+ function record(value, code) {
1297
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(code);
1298
+ return value;
1299
+ }
1300
+
1301
+ function array(value, code) {
1302
+ if (!Array.isArray(value)) fail(code);
1303
+ return value;
1304
+ }
1305
+
1306
+ function oneOf(value, allowed, code) {
1307
+ if (!allowed.has(value)) fail(code);
1308
+ return value;
1309
+ }
1310
+
1311
+ function assertUnique(items, keyOf, code) {
1312
+ const seen = new Set();
1313
+ for (const item of items) {
1314
+ const key = keyOf(item);
1315
+ if (seen.has(key)) fail(code);
1316
+ seen.add(key);
1317
+ }
1318
+ }
1319
+
1320
+ function assertNoSensitiveKeys(value, seen = new Set(), path = []) {
1321
+ if (!value || typeof value !== "object" || seen.has(value)) return;
1322
+ seen.add(value);
1323
+ for (const [key, nested] of Object.entries(value)) {
1324
+ const normalized = String(key).toLowerCase().replace(/[^a-z0-9]/g, "");
1325
+ const exactMessageAuthorizationScope = key === "authorizationScope"
1326
+ && path.length === 3
1327
+ && path[0] === "messages"
1328
+ && path[1] === "items"
1329
+ && /^(0|[1-9][0-9]*)$/.test(path[2])
1330
+ && (nested == null || nested === MESSAGE_AUTHORIZATION_SCOPE);
1331
+ if (!exactMessageAuthorizationScope && (
1332
+ (normalized.startsWith("authorization") && normalized !== "authorizationversion")
1333
+ || /(bearer|cookie|password|privatekey|secret|token|credential)/u.test(normalized)
1334
+ )) {
1335
+ fail("neutral_host_workbench_sensitive_field_forbidden");
1336
+ }
1337
+ assertNoSensitiveKeys(nested, seen, [...path, key]);
1338
+ }
1339
+ }
1340
+
1341
+ function stableJson(value) {
1342
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
1343
+ if (value && typeof value === "object") {
1344
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
1345
+ }
1346
+ return JSON.stringify(value);
1347
+ }
1348
+
1349
+ function digest(value) {
1350
+ return crypto.createHash("sha256").update(value, "utf8").digest("hex");
1351
+ }
1352
+
1353
+ function compareText(left, right) {
1354
+ return String(left).localeCompare(String(right), "und");
1355
+ }
1356
+
1357
+ function deepFreeze(value) {
1358
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
1359
+ for (const nested of Object.values(value)) deepFreeze(nested);
1360
+ return Object.freeze(value);
1361
+ }
1362
+
1363
+ function fail(code) {
1364
+ throw new HelloLynkNeutralHostWorkbenchReadModelError(code);
1365
+ }