@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,1314 @@
1
+ export const HELLOLYNK_WORKBENCH_LEGACY_SNAPSHOT_CONTRACT = "hellolynk-workbench-snapshot-v1";
2
+ export const HELLOLYNK_WORKBENCH_SNAPSHOT_V2_CONTRACT = "hellolynk-workbench-snapshot-v2";
3
+ export const HELLOLYNK_WORKBENCH_SNAPSHOT_V3_CONTRACT = "hellolynk-workbench-snapshot-v3";
4
+ export const HELLOLYNK_WORKBENCH_SNAPSHOT_V4_CONTRACT = "hellolynk-workbench-snapshot-v4";
5
+ export const HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT = "hellolynk-workbench-snapshot-v12";
6
+ export const HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION = 12;
7
+ export const HELLOLYNK_WORKBENCH_COMMAND_CONTRACT = "hellolynk-workbench-command-v6";
8
+ export const HELLOLYNK_WORKBENCH_RESULT_CONTRACT = "hellolynk-workbench-command-result-v6";
9
+ export const HELLOLYNK_WORKBENCH_READ_REQUEST_CONTRACT = "hellolynk-workbench-read-request-v1";
10
+ export const HELLOLYNK_WORKBENCH_SCHEMA_VERSION = 6;
11
+ export const HELLOLYNK_WORKBENCH_MAX_QUERY_LENGTH = 512;
12
+
13
+ export const HELLOLYNK_WORKBENCH_TRANSPORTS = Object.freeze([
14
+ "loopback-http",
15
+ "mcp-app-rpc",
16
+ "cursor-webview-rpc",
17
+ "synthetic-memory",
18
+ ]);
19
+
20
+ export const HELLOLYNK_WORKBENCH_CAPABILITIES = Object.freeze([
21
+ "block.remove",
22
+ "blocks.read",
23
+ "contact.authorization.manage",
24
+ "contact.delete",
25
+ "contact.instance.delete",
26
+ "contact.instance.rename",
27
+ "contact.instance.save",
28
+ "contact.instances.list",
29
+ "contact.save",
30
+ "contact.rename",
31
+ "contact-request.cancel",
32
+ "contact-request.clear-resolved",
33
+ "contact-request.recover-decision",
34
+ "contact-request.respond",
35
+ "contact-request.send",
36
+ "contacts.read",
37
+ "messages.delete-read",
38
+ "messages.delete-sent",
39
+ "messages.mark-read",
40
+ "messages.mark-all-read",
41
+ "messages.auto-read.manage",
42
+ "messages.read",
43
+ "messages.request-send",
44
+ "outbox.read",
45
+ "outbox.request-retry",
46
+ "principal.search",
47
+ "relationship.end",
48
+ "relationships.read",
49
+ "search.local",
50
+ "security.open-account",
51
+ "security.read",
52
+ "threads.manage-local",
53
+ "threads.read",
54
+ ]);
55
+
56
+ export const HELLOLYNK_WORKBENCH_COMMAND_TYPES = Object.freeze({
57
+ requestSend: "message.request-send",
58
+ getSendStatus: "message.send-status",
59
+ markRead: "message.mark-read",
60
+ markAllRead: "messages.mark-all-read",
61
+ setAutoRead: "thread.set-auto-read",
62
+ deleteRead: "message.delete-read",
63
+ deleteSent: "message.delete-sent",
64
+ hideThread: "thread.hide",
65
+ restoreThread: "thread.restore",
66
+ clearThread: "thread.clear-local",
67
+ deleteThread: "thread.delete-local",
68
+ deleteArchivedThreads: "thread.delete-archived-local",
69
+ configureHistory: "history.configure",
70
+ requestRetry: "outbox.request-retry",
71
+ resendDeletedReply: "outbox.resend-deleted-reply",
72
+ saveContact: "contact.save",
73
+ renameContact: "contact.rename",
74
+ deleteContact: "contact.delete",
75
+ listContactInstances: "contact.instances.list",
76
+ saveContactInstance: "contact.instance.save",
77
+ renameContactInstance: "contact.instance.rename",
78
+ deleteContactInstance: "contact.instance.delete",
79
+ clearResolvedContactRequests: "contact-request.clear-resolved",
80
+ recoverContactRequestDecision: "contact-request.recover-decision",
81
+ respondContactRequest: "contact-request.respond",
82
+ searchPrincipal: "principal.search",
83
+ sendContactRequest: "contact-request.send",
84
+ cancelContactRequest: "contact-request.cancel",
85
+ setContactAuthorization: "contact.authorization.set",
86
+ endRelationship: "relationship.end",
87
+ removeBlock: "block.remove",
88
+ openAccount: "security.open-account",
89
+ });
90
+
91
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{1,198}$/;
92
+ const SAFE_REVISION = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,254}$/;
93
+ const COMMAND_WINDOW = /^wb5\.[a-f0-9]{32}\.([1-9][0-9]{0,15})$/;
94
+ const CAPABILITY_SET = new Set(HELLOLYNK_WORKBENCH_CAPABILITIES);
95
+ const TRANSPORT_SET = new Set(HELLOLYNK_WORKBENCH_TRANSPORTS);
96
+ const COMMAND_SET = new Set(Object.values(HELLOLYNK_WORKBENCH_COMMAND_TYPES));
97
+ const MESSAGE_DIRECTIONS = new Set(["incoming", "outgoing"]);
98
+ const HOST_STATES = new Set(["connecting", "online", "offline", "rebind-required", "error"]);
99
+ const CLIENT_STATES = new Set(["connected", "disconnected", "stale"]);
100
+ const READ_FILTERS = new Set(["all", "unread", "archived"]);
101
+ const MESSAGE_AUTHORIZATION_SCOPE = "relay.text.send";
102
+ const MESSAGE_INTENT_SOURCES = new Set([
103
+ "hellolynk_client_explicit_send",
104
+ "human_native_dialog",
105
+ "human_preauthorized_contact",
106
+ ]);
107
+ const MAX_SNAPSHOT_MESSAGE_ITEMS = 50_000;
108
+ const MAX_LEGACY_SNAPSHOT_MESSAGE_ITEMS = 2_000;
109
+ const SNAPSHOT_VERSION_BY_CONTRACT = new Map([
110
+ [HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT, HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION],
111
+ ]);
112
+
113
+ export class HelloLynkWorkbenchContractError extends Error {
114
+ constructor(code) {
115
+ super(code);
116
+ this.name = "HelloLynkWorkbenchContractError";
117
+ this.code = code;
118
+ }
119
+ }
120
+
121
+ export function validateWorkbenchSnapshot(value) {
122
+ const input = record(value, "workbench_snapshot_invalid");
123
+ assertNoSensitiveKeys(input);
124
+ const snapshotSchemaVersion = SNAPSHOT_VERSION_BY_CONTRACT.get(input.contractId);
125
+ if (snapshotSchemaVersion == null) fail("workbench_snapshot_contract_unsupported");
126
+ const baseFields = [
127
+ "contractId", "schemaVersion", "compatibility", "hostGeneration", "commandWindow", "revision", "changeCursor",
128
+ "generatedAt", "source", "subject", "capabilities", "threads", "messages", "outbox",
129
+ "contacts", "contactRequests", "aiReadAuthorization", "security", "changes",
130
+ ];
131
+ const requiredFields = snapshotSchemaVersion >= 4
132
+ ? [...baseFields, "contactRequestNextCursor", "outgoingContactRequests", "relationships", "blocks"]
133
+ : baseFields;
134
+ exactOptionalKeys(input, [...requiredFields, "relationshipMetadataState", "localHistory", "inboxReadBoundary"], requiredFields,
135
+ "workbench_snapshot_fields_invalid");
136
+ if (input.schemaVersion !== snapshotSchemaVersion) fail("workbench_snapshot_version_unsupported");
137
+
138
+ const compatibility = record(input.compatibility, "workbench_snapshot_compatibility_invalid");
139
+ const source = record(input.source, "workbench_snapshot_source_invalid");
140
+ exactKeys(compatibility, ["minReaderVersion", "commandVersion"], "workbench_snapshot_compatibility_invalid");
141
+ exactKeys(source, ["mode", "transport", "clientId", "clientLabel"], "workbench_snapshot_source_invalid");
142
+ const mode = oneOf(source.mode, ["neutral-host", "synthetic-preview"], "workbench_snapshot_source_invalid");
143
+ const transport = oneOf(source.transport, HELLOLYNK_WORKBENCH_TRANSPORTS, "workbench_snapshot_transport_invalid");
144
+ if ((mode === "synthetic-preview") !== (transport === "synthetic-memory")) {
145
+ fail("workbench_snapshot_source_transport_mismatch");
146
+ }
147
+ const inputMinReaderVersion = positiveInteger(
148
+ compatibility.minReaderVersion,
149
+ "workbench_snapshot_compatibility_invalid",
150
+ );
151
+ const inputCommandVersion = positiveInteger(
152
+ compatibility.commandVersion,
153
+ "workbench_snapshot_compatibility_invalid",
154
+ );
155
+ if (inputMinReaderVersion !== snapshotSchemaVersion
156
+ || inputCommandVersion !== HELLOLYNK_WORKBENCH_SCHEMA_VERSION) {
157
+ fail("workbench_snapshot_compatibility_unsupported");
158
+ }
159
+
160
+ const snapshot = {
161
+ contractId: HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT,
162
+ schemaVersion: HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION,
163
+ compatibility: {
164
+ minReaderVersion: HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION,
165
+ commandVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
166
+ },
167
+ hostGeneration: safeId(input.hostGeneration, "workbench_snapshot_host_generation_invalid"),
168
+ commandWindow: input.commandWindow === null ? null : validateWorkbenchCommandWindow(input.commandWindow),
169
+ revision: safeRevision(input.revision),
170
+ changeCursor: safeRevision(input.changeCursor),
171
+ generatedAt: timestamp(input.generatedAt, "workbench_snapshot_generated_at_invalid"),
172
+ ...(Object.hasOwn(input, "inboxReadBoundary") ? { inboxReadBoundary:
173
+ nonNegativeInteger(input.inboxReadBoundary, "workbench_snapshot_inbox_boundary_invalid") } : {}),
174
+ source: {
175
+ mode,
176
+ transport,
177
+ clientId: safeId(source.clientId, "workbench_snapshot_client_id_invalid"),
178
+ clientLabel: text(source.clientLabel, 80, "workbench_snapshot_client_label_invalid"),
179
+ },
180
+ subject: validateSubject(input.subject),
181
+ capabilities: validateCapabilities(input.capabilities),
182
+ threads: validateThreadPage(input.threads),
183
+ messages: validateMessagePage(input.messages, snapshotSchemaVersion),
184
+ ...(Object.hasOwn(input, "localHistory") ? { localHistory: validateLocalHistory(input.localHistory) } : {}),
185
+ outbox: validateOutbox(input.outbox),
186
+ contacts: list(input.contacts, 1_000, "workbench_snapshot_contacts_invalid").map(validateContact),
187
+ aiReadAuthorization: validateAiReadAuthorization(input.aiReadAuthorization),
188
+ contactRequests: list(input.contactRequests, 1_000, "workbench_snapshot_contact_requests_invalid")
189
+ .map((item) => validateContactRequest(item, snapshotSchemaVersion)),
190
+ outgoingContactRequests: snapshotSchemaVersion >= 4
191
+ ? list(
192
+ input.outgoingContactRequests,
193
+ 1_000,
194
+ "workbench_snapshot_contact_requests_invalid",
195
+ ).map((item) => validateContactRequest(item, snapshotSchemaVersion))
196
+ : [],
197
+ contactRequestNextCursor: snapshotSchemaVersion >= 4 && input.contactRequestNextCursor != null
198
+ ? contactRequestCursor(input.contactRequestNextCursor)
199
+ : null,
200
+ relationships: snapshotSchemaVersion >= 4
201
+ ? list(input.relationships, 1_000, "workbench_snapshot_relationships_invalid")
202
+ .map(validateRelationship)
203
+ : [],
204
+ ...(Object.hasOwn(input, "relationshipMetadataState") ? {
205
+ relationshipMetadataState: oneOf(input.relationshipMetadataState, ["loading", "fresh", "stale"],
206
+ "workbench_snapshot_relationship_metadata_state_invalid"),
207
+ } : {}),
208
+ blocks: snapshotSchemaVersion >= 4
209
+ ? list(input.blocks, 1_000, "workbench_snapshot_blocks_invalid").map(validateBlock)
210
+ : [],
211
+ security: validateSecurity(input.security),
212
+ changes: validateChanges(input.changes),
213
+ };
214
+
215
+ if (snapshot.contactRequests.some((item) => item.direction !== "incoming")
216
+ || snapshot.outgoingContactRequests.some((item) => item.direction !== "outgoing")) {
217
+ fail("workbench_snapshot_contact_request_direction_invalid");
218
+ }
219
+
220
+ if (snapshot.messages.threadId != null
221
+ && !snapshot.threads.items.some((item) => item.threadId === snapshot.messages.threadId)) {
222
+ fail("workbench_snapshot_selected_thread_missing");
223
+ }
224
+ if (snapshot.localHistory?.selection != null
225
+ && snapshot.localHistory.selection.threadId !== snapshot.messages.threadId) {
226
+ fail("workbench_history_selection_thread_mismatch");
227
+ }
228
+ if (snapshot.changes.cursor !== snapshot.changeCursor) fail("workbench_snapshot_cursor_mismatch");
229
+ return deepFreeze(snapshot);
230
+ }
231
+
232
+ export function validateWorkbenchReadRequest(value = {}) {
233
+ const input = record(value, "workbench_read_request_invalid");
234
+ exactOptionalKeys(
235
+ input,
236
+ ["threadId", "threadCursor", "messageCursor", "contactRequestCursor", "query", "filter", "historyBefore"],
237
+ [],
238
+ "workbench_read_request_fields_invalid",
239
+ );
240
+ if (Object.hasOwn(input, "historyBefore") && input.threadId == null) fail("workbench_read_history_thread_required");
241
+ return deepFreeze({
242
+ threadId: input.threadId == null ? null : safeId(input.threadId, "workbench_read_thread_id_invalid"),
243
+ ...(Object.hasOwn(input, "historyBefore") ? {
244
+ historyBefore: input.historyBefore == null ? null : timestamp(input.historyBefore, "workbench_read_history_before_invalid"),
245
+ } : {}),
246
+ threadCursor: input.threadCursor == null ? null : safeRevision(input.threadCursor),
247
+ messageCursor: input.messageCursor == null ? null : messagePageCursor(input.messageCursor),
248
+ ...(Object.hasOwn(input, "contactRequestCursor") ? {
249
+ contactRequestCursor: input.contactRequestCursor == null
250
+ ? null
251
+ : contactRequestCursor(input.contactRequestCursor),
252
+ } : {}),
253
+ query: input.query == null ? "" : searchText(input.query),
254
+ filter: input.filter == null ? "all" : oneOf(input.filter, [...READ_FILTERS], "workbench_read_filter_invalid"),
255
+ });
256
+ }
257
+
258
+ export function createWorkbenchCommand(type, payload, options = {}) {
259
+ const commandType = oneOf(type, [...COMMAND_SET], "workbench_command_type_invalid");
260
+ const command = {
261
+ contractId: HELLOLYNK_WORKBENCH_COMMAND_CONTRACT,
262
+ schemaVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
263
+ command: commandType,
264
+ requestId: safeId(options.requestId, "workbench_command_request_id_invalid"),
265
+ idempotencyKey: workbenchIdempotencyKey(options.idempotencyKey),
266
+ expectedRevision: options.expectedRevision == null ? null : safeRevision(options.expectedRevision),
267
+ payload: validateCommandPayload(commandType, payload),
268
+ };
269
+ return deepFreeze(command);
270
+ }
271
+
272
+ // A new key is created only for a new human intent. Transports must forward the
273
+ // complete key unchanged, including after a refresh, timeout or Host restart.
274
+ export function createWorkbenchIdempotencyKey(commandWindow, nonce) {
275
+ return workbenchIdempotencyKey(`${validateWorkbenchCommandWindow(commandWindow)}.${safeId(nonce, "workbench_command_nonce_invalid")}`);
276
+ }
277
+
278
+ export function validateWorkbenchCommandWindow(value) {
279
+ const match = typeof value === "string" ? COMMAND_WINDOW.exec(value) : null;
280
+ if (!match || !Number.isSafeInteger(Number(match[1]))) fail("workbench_command_window_invalid");
281
+ return value;
282
+ }
283
+
284
+ export function workbenchCommandWindowFromKey(value) {
285
+ const key = safeId(value, "workbench_command_idempotency_key_invalid");
286
+ const parts = key.split(".");
287
+ validateWorkbenchCommandWindow(parts.slice(0, 3).join("."));
288
+ safeId(parts.slice(3).join("."), "workbench_command_nonce_invalid");
289
+ return parts.slice(0, 3).join(".");
290
+ }
291
+
292
+ function workbenchIdempotencyKey(value) {
293
+ workbenchCommandWindowFromKey(value);
294
+ return value;
295
+ }
296
+
297
+ export function validateWorkbenchCommand(value) {
298
+ const input = record(value, "workbench_command_invalid");
299
+ assertNoSensitiveKeys(input);
300
+ exactKeys(input, [
301
+ "contractId",
302
+ "schemaVersion",
303
+ "command",
304
+ "requestId",
305
+ "idempotencyKey",
306
+ "expectedRevision",
307
+ "payload",
308
+ ], "workbench_command_fields_invalid");
309
+ if (input.contractId !== HELLOLYNK_WORKBENCH_COMMAND_CONTRACT) fail("workbench_command_contract_unsupported");
310
+ if (input.schemaVersion !== HELLOLYNK_WORKBENCH_SCHEMA_VERSION) fail("workbench_command_version_unsupported");
311
+ return createWorkbenchCommand(input.command, input.payload, {
312
+ requestId: input.requestId,
313
+ idempotencyKey: input.idempotencyKey,
314
+ expectedRevision: input.expectedRevision,
315
+ });
316
+ }
317
+
318
+ export function validateWorkbenchCommandResult(value, expectedCommand = null) {
319
+ const input = record(value, "workbench_command_result_invalid");
320
+ assertNoSensitiveKeys(input);
321
+ if (input.contractId !== HELLOLYNK_WORKBENCH_RESULT_CONTRACT) fail("workbench_result_contract_unsupported");
322
+ if (input.schemaVersion !== HELLOLYNK_WORKBENCH_SCHEMA_VERSION) fail("workbench_result_version_unsupported");
323
+ if (typeof input.ok !== "boolean") fail("workbench_command_result_invalid");
324
+ exactKeys(
325
+ input,
326
+ input.ok
327
+ ? ["contractId", "schemaVersion", "ok", "requestId", "revision", "changeCursor", "result"]
328
+ : ["contractId", "schemaVersion", "ok", "requestId", "revision", "changeCursor", "error"],
329
+ "workbench_command_result_fields_invalid",
330
+ );
331
+
332
+ const result = {
333
+ contractId: HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
334
+ schemaVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
335
+ ok: input.ok,
336
+ requestId: safeId(input.requestId, "workbench_result_request_id_invalid"),
337
+ revision: input.revision == null ? null : safeRevision(input.revision),
338
+ changeCursor: input.changeCursor == null ? null : safeRevision(input.changeCursor),
339
+ };
340
+ if (input.ok) {
341
+ result.result = validateCommandSuccessResult(input.result, expectedCommand);
342
+ } else {
343
+ const error = record(input.error, "workbench_command_error_invalid");
344
+ exactKeys(error, ["code", "retryable", "outcome", "retryAfterMs", "currentRevision"], "workbench_command_error_invalid");
345
+ result.error = {
346
+ code: errorCode(error.code),
347
+ retryable: boolean(error.retryable, "workbench_command_error_invalid"),
348
+ outcome: oneOf(error.outcome, ["known-failed", "unknown"], "workbench_command_error_invalid"),
349
+ retryAfterMs: error.retryAfterMs == null
350
+ ? null
351
+ : nonNegativeInteger(error.retryAfterMs, "workbench_command_error_invalid"),
352
+ currentRevision: error.currentRevision == null ? null : safeRevision(error.currentRevision),
353
+ };
354
+ if (result.error.outcome === "unknown" && result.error.retryable) {
355
+ fail("workbench_unknown_outcome_cannot_auto_retry");
356
+ }
357
+ }
358
+ if (expectedCommand != null) {
359
+ const command = oneOf(expectedCommand, [...COMMAND_SET], "workbench_command_type_invalid");
360
+ if (result.ok) {
361
+ const expectedKind = command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.markAllRead ? "messages-marked-read"
362
+ : command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.setAutoRead ? "conversation-auto-read"
363
+ : command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.resendDeletedReply ? "deleted-reply-resend"
364
+ : command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestSend
365
+ || command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus
366
+ ? "message-send"
367
+ : command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.listContactInstances
368
+ ? "contact-instances"
369
+ : command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal
370
+ ? "principal-search"
371
+ : command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.sendContactRequest
372
+ ? "contact-request-send"
373
+ : command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearResolvedContactRequests
374
+ ? "contact-request-clear-resolved"
375
+ : command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteSent
376
+ || command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearThread
377
+ || command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteThread
378
+ || command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteArchivedThreads
379
+ ? "local-history"
380
+ : null;
381
+ const actualKind = result.result.kind ?? null;
382
+ const legacyEmptySend = command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestSend
383
+ && Object.keys(result.result).length === 0;
384
+ if (!legacyEmptySend && actualKind !== expectedKind) fail("workbench_command_result_type_mismatch");
385
+ }
386
+ }
387
+ return deepFreeze(result);
388
+ }
389
+
390
+ export function hasWorkbenchCapability(snapshot, capability) {
391
+ const validated = validateWorkbenchSnapshot(snapshot);
392
+ return validated.capabilities.includes(String(capability));
393
+ }
394
+
395
+ function validateSubject(value) {
396
+ const input = record(value, "workbench_snapshot_subject_invalid");
397
+ exactKeys(input, ["region", "principalId", "principalLabel", "hellolynkId", "delegateId", "delegateLabel", "hostInstallationId", "authorizationVersion"], "workbench_snapshot_subject_invalid");
398
+ return {
399
+ region: safeId(input.region, "workbench_snapshot_region_invalid"),
400
+ principalId: safeId(input.principalId, "workbench_snapshot_principal_id_invalid"),
401
+ principalLabel: optionalText(input.principalLabel, 120, "workbench_snapshot_principal_label_invalid"),
402
+ hellolynkId: text(input.hellolynkId, 200, "workbench_snapshot_hellolynk_id_invalid"),
403
+ delegateId: safeId(input.delegateId, "workbench_snapshot_delegate_id_invalid"),
404
+ delegateLabel: optionalText(input.delegateLabel, 120, "workbench_snapshot_delegate_label_invalid"),
405
+ hostInstallationId: safeId(input.hostInstallationId, "workbench_snapshot_host_installation_id_invalid"),
406
+ authorizationVersion: positiveInteger(input.authorizationVersion, "workbench_snapshot_authorization_version_invalid"),
407
+ };
408
+ }
409
+
410
+ function validateCapabilities(value) {
411
+ const capabilities = list(value, HELLOLYNK_WORKBENCH_CAPABILITIES.length, "workbench_snapshot_capabilities_invalid")
412
+ .map((item) => {
413
+ if (typeof item !== "string") fail("workbench_snapshot_capability_invalid");
414
+ return item;
415
+ });
416
+ if (capabilities.some((item) => !CAPABILITY_SET.has(item))) fail("workbench_snapshot_capability_unknown");
417
+ const canonical = [...new Set(capabilities)].sort();
418
+ if (canonical.length !== capabilities.length || canonical.some((item, index) => item !== capabilities[index])) {
419
+ fail("workbench_snapshot_capabilities_noncanonical");
420
+ }
421
+ return canonical;
422
+ }
423
+
424
+ function validateThreadPage(value) {
425
+ const input = record(value, "workbench_snapshot_threads_invalid");
426
+ exactKeys(input, ["items", "nextCursor"], "workbench_snapshot_threads_invalid");
427
+ return {
428
+ items: list(input.items, 1_000, "workbench_snapshot_threads_invalid").map(validateThread),
429
+ nextCursor: input.nextCursor == null ? null : safeRevision(input.nextCursor),
430
+ };
431
+ }
432
+
433
+ function validateThread(value) {
434
+ const input = record(value, "workbench_snapshot_thread_invalid");
435
+ const fields = ["threadId", "conversationKind", "localDelegateId", "peerDelegateId", "contactId", "peerPrincipalId", "peerHelloLynkId", "displayName", "delegateLabel", "lastMessage", "unreadCount", "updatedAt"];
436
+ exactOptionalKeys(input, [...fields, "archived", "autoReadAfterAi", "recentSendFailureCount"], fields, "workbench_snapshot_thread_invalid");
437
+ const lastMessage = input.lastMessage == null ? null : record(input.lastMessage, "workbench_snapshot_thread_invalid");
438
+ if (lastMessage) exactKeys(lastMessage, ["text", "direction", "status", "createdAt"], "workbench_snapshot_thread_invalid");
439
+ const thread = {
440
+ threadId: safeId(input.threadId, "workbench_snapshot_thread_id_invalid"),
441
+ conversationKind: oneOf(input.conversationKind, ["instance", "legacy"], "workbench_snapshot_conversation_kind_invalid"),
442
+ localDelegateId: input.localDelegateId == null ? null : safeId(input.localDelegateId, "workbench_snapshot_delegate_id_invalid"),
443
+ peerDelegateId: input.peerDelegateId == null ? null : safeId(input.peerDelegateId, "workbench_snapshot_delegate_id_invalid"),
444
+ contactId: input.contactId == null ? null : safeId(input.contactId, "workbench_snapshot_contact_id_invalid"),
445
+ peerPrincipalId: input.peerPrincipalId == null
446
+ ? null
447
+ : safeId(input.peerPrincipalId, "workbench_snapshot_peer_id_invalid"),
448
+ peerHelloLynkId: input.peerHelloLynkId == null
449
+ ? null
450
+ : text(input.peerHelloLynkId, 200, "workbench_snapshot_peer_hellolynk_id_invalid"),
451
+ displayName: text(input.displayName, 160, "workbench_snapshot_display_name_invalid"),
452
+ delegateLabel: optionalText(input.delegateLabel, 120, "workbench_snapshot_delegate_label_invalid"),
453
+ lastMessage: lastMessage == null ? null : {
454
+ text: text(lastMessage.text, 20_480, "workbench_snapshot_message_text_invalid", true),
455
+ direction: oneOf(lastMessage.direction, [...MESSAGE_DIRECTIONS], "workbench_snapshot_message_direction_invalid"),
456
+ status: errorCode(lastMessage.status),
457
+ createdAt: timestamp(lastMessage.createdAt, "workbench_snapshot_message_time_invalid"),
458
+ },
459
+ unreadCount: nonNegativeInteger(input.unreadCount, "workbench_snapshot_unread_count_invalid"),
460
+ ...(Object.hasOwn(input, "recentSendFailureCount") ? { recentSendFailureCount:
461
+ nonNegativeInteger(input.recentSendFailureCount, "workbench_snapshot_recent_failure_count_invalid") } : {}),
462
+ ...(Object.hasOwn(input, "archived") ? { archived: boolean(input.archived, "workbench_snapshot_archived_invalid") } : {}),
463
+ ...(Object.hasOwn(input, "autoReadAfterAi") ? { autoReadAfterAi: boolean(input.autoReadAfterAi, "workbench_snapshot_auto_read_invalid") } : {}),
464
+ updatedAt: timestamp(input.updatedAt, "workbench_snapshot_thread_time_invalid"),
465
+ };
466
+ if (thread.peerPrincipalId == null && thread.peerHelloLynkId == null) {
467
+ fail("workbench_snapshot_peer_identity_missing");
468
+ }
469
+ if (thread.conversationKind === "instance"
470
+ ? !thread.localDelegateId || !thread.peerDelegateId || !thread.peerPrincipalId
471
+ : thread.localDelegateId != null || thread.peerDelegateId != null) {
472
+ fail("workbench_snapshot_conversation_binding_invalid");
473
+ }
474
+ if (thread.autoReadAfterAi === true && thread.conversationKind !== "instance") fail("workbench_snapshot_auto_read_invalid");
475
+ if (thread.recentSendFailureCount > 5) fail("workbench_snapshot_recent_failure_count_invalid");
476
+ return thread;
477
+ }
478
+
479
+ function validateMessagePage(value, snapshotSchemaVersion) {
480
+ const input = record(value, "workbench_snapshot_messages_invalid");
481
+ exactOptionalKeys(input, ["threadId", "items", "nextCursor", "previousCursor", "refreshCursor"],
482
+ ["threadId", "items", "nextCursor"], "workbench_snapshot_messages_invalid");
483
+ const threadId = input.threadId == null ? null : safeId(input.threadId, "workbench_snapshot_thread_id_invalid");
484
+ const maximumItems = snapshotSchemaVersion >= 3
485
+ ? MAX_SNAPSHOT_MESSAGE_ITEMS
486
+ : MAX_LEGACY_SNAPSHOT_MESSAGE_ITEMS;
487
+ const items = list(input.items, maximumItems, "workbench_snapshot_messages_invalid")
488
+ .map((item) => validateMessage(item, snapshotSchemaVersion));
489
+ if (threadId == null && items.length) fail("workbench_snapshot_message_thread_missing");
490
+ if (threadId != null && items.some((item) => item.threadId !== threadId)) fail("workbench_snapshot_message_thread_mismatch");
491
+ return { threadId, items, nextCursor: input.nextCursor == null ? null : messagePageCursor(input.nextCursor),
492
+ ...(Object.hasOwn(input, "previousCursor") ? {
493
+ previousCursor: input.previousCursor == null ? null : messagePageCursor(input.previousCursor),
494
+ } : {}),
495
+ ...(Object.hasOwn(input, "refreshCursor") ? {
496
+ refreshCursor: input.refreshCursor == null ? null : messagePageCursor(input.refreshCursor),
497
+ } : {}) };
498
+ }
499
+
500
+ function validateLocalHistory(value) {
501
+ const input = record(value, "workbench_history_invalid");
502
+ exactKeys(input, ["autoArchiveAfterHours", "archivedThreadCount", "selection"], "workbench_history_invalid");
503
+ if (input.autoArchiveAfterHours !== null && input.autoArchiveAfterHours !== 24) fail("workbench_history_setting_invalid");
504
+ let selection = null;
505
+ if (input.selection != null) {
506
+ const fields = ["totalCount", "totalBytes", "deletableCount", "deletableBytes", "retainedUnreadCount", "retainedOutgoingCount", "retainedTaskCount"];
507
+ const source = record(input.selection, "workbench_history_selection_invalid");
508
+ exactKeys(source, ["threadId", "before", ...fields], "workbench_history_selection_invalid");
509
+ selection = {
510
+ threadId: safeId(source.threadId, "workbench_history_thread_invalid"),
511
+ before: source.before == null ? null : timestamp(source.before, "workbench_history_before_invalid"),
512
+ ...Object.fromEntries(fields.map((field) => [field, nonNegativeInteger(source[field], "workbench_history_count_invalid")])),
513
+ };
514
+ if (selection.deletableCount > selection.totalCount || selection.deletableBytes > selection.totalBytes) fail("workbench_history_count_invalid");
515
+ }
516
+ return { autoArchiveAfterHours: input.autoArchiveAfterHours,
517
+ archivedThreadCount: nonNegativeInteger(input.archivedThreadCount, "workbench_history_count_invalid"), selection };
518
+ }
519
+
520
+ function messagePageCursor(value) {
521
+ if (typeof value !== "string" || !/^[A-Za-z0-9._-]{1,800}$/.test(value)) fail("workbench_message_cursor_invalid");
522
+ return value;
523
+ }
524
+
525
+ function validateMessage(value, snapshotSchemaVersion) {
526
+ const input = record(value, "workbench_snapshot_message_invalid");
527
+ const legacyKeys = [
528
+ "localMessageId", "clientMessageId", "threadId", "direction", "senderPrincipalId",
529
+ "senderDelegateId", "text", "status", "unread", "createdAt", "updatedAt",
530
+ ];
531
+ if (Object.hasOwn(input, "automaticDeliveryState")) {
532
+ if (input.direction !== "incoming" || input.automaticDeliveryState !== "expired") fail("workbench_snapshot_message_invalid");
533
+ legacyKeys.push("automaticDeliveryState");
534
+ }
535
+ exactKeys(
536
+ input,
537
+ snapshotSchemaVersion >= 3
538
+ ? [...legacyKeys, "intentSource", "authorizationScope"]
539
+ : legacyKeys,
540
+ "workbench_snapshot_message_invalid",
541
+ );
542
+ const authorizationScope = snapshotSchemaVersion >= 3 ? input.authorizationScope : null;
543
+ const intentSource = snapshotSchemaVersion >= 3 ? input.intentSource : null;
544
+ if (
545
+ (authorizationScope == null) !== (intentSource == null)
546
+ || (authorizationScope != null && authorizationScope !== MESSAGE_AUTHORIZATION_SCOPE)
547
+ || (intentSource != null && !MESSAGE_INTENT_SOURCES.has(intentSource))
548
+ ) fail("workbench_snapshot_message_provenance_invalid");
549
+ const senderPrincipalId = snapshotSchemaVersion >= 3 && input.senderPrincipalId == null
550
+ ? null
551
+ : safeId(input.senderPrincipalId, "workbench_snapshot_sender_id_invalid");
552
+ const senderDelegateId = input.senderDelegateId == null
553
+ ? null
554
+ : safeId(input.senderDelegateId, "workbench_snapshot_sender_delegate_id_invalid");
555
+ if (authorizationScope != null && (senderPrincipalId == null || senderDelegateId == null)) {
556
+ fail("workbench_snapshot_message_provenance_invalid");
557
+ }
558
+ return {
559
+ localMessageId: safeId(input.localMessageId, "workbench_snapshot_message_id_invalid"),
560
+ clientMessageId: safeId(input.clientMessageId, "workbench_snapshot_client_message_id_invalid"),
561
+ threadId: safeId(input.threadId, "workbench_snapshot_thread_id_invalid"),
562
+ direction: oneOf(input.direction, [...MESSAGE_DIRECTIONS], "workbench_snapshot_message_direction_invalid"),
563
+ senderPrincipalId,
564
+ senderDelegateId,
565
+ intentSource,
566
+ authorizationScope,
567
+ text: text(input.text, 64 * 1024, "workbench_snapshot_message_text_invalid", true),
568
+ ...(input.automaticDeliveryState === "expired" ? { automaticDeliveryState: "expired" } : {}),
569
+ status: errorCode(input.status),
570
+ unread: boolean(input.unread, "workbench_snapshot_message_unread_invalid"),
571
+ createdAt: timestamp(input.createdAt, "workbench_snapshot_message_time_invalid"),
572
+ updatedAt: timestamp(input.updatedAt, "workbench_snapshot_message_time_invalid"),
573
+ };
574
+ }
575
+
576
+ function validateOutbox(value) {
577
+ return list(value, 2_000, "workbench_snapshot_outbox_invalid").map((item) => {
578
+ const input = record(item, "workbench_snapshot_outbox_item_invalid");
579
+ exactKeys(input, ["clientMessageId", "threadId", "localDelegateId", "recipientDelegateId", "recipientHelloLynkId", "text", "status", "errorCategory", "retryAllowed", "createdAt", "updatedAt"], "workbench_snapshot_outbox_item_invalid");
580
+ return {
581
+ clientMessageId: safeId(input.clientMessageId, "workbench_snapshot_client_message_id_invalid"),
582
+ threadId: input.threadId == null ? null : safeId(input.threadId, "workbench_snapshot_thread_id_invalid"),
583
+ localDelegateId: input.localDelegateId == null ? null : safeId(input.localDelegateId, "workbench_snapshot_delegate_id_invalid"),
584
+ recipientDelegateId: input.recipientDelegateId == null ? null : safeId(input.recipientDelegateId, "workbench_snapshot_delegate_id_invalid"),
585
+ recipientHelloLynkId: text(input.recipientHelloLynkId, 200, "workbench_snapshot_peer_hellolynk_id_invalid"),
586
+ text: text(input.text, 20_480, "workbench_snapshot_message_text_invalid", true),
587
+ status: errorCode(input.status),
588
+ errorCategory: input.errorCategory == null ? null : errorCode(input.errorCategory),
589
+ retryAllowed: boolean(input.retryAllowed, "workbench_snapshot_outbox_retry_invalid"),
590
+ createdAt: timestamp(input.createdAt, "workbench_snapshot_message_time_invalid"),
591
+ updatedAt: timestamp(input.updatedAt, "workbench_snapshot_message_time_invalid"),
592
+ };
593
+ });
594
+ }
595
+
596
+ function validateAiReadAuthorization(value) {
597
+ const input = record(value, "workbench_snapshot_ai_read_authorization_invalid");
598
+ exactKeys(input, ["contactRequests"], "workbench_snapshot_ai_read_authorization_invalid");
599
+ return {
600
+ contactRequests: boolean(input.contactRequests, "workbench_snapshot_ai_read_authorization_invalid"),
601
+ };
602
+ }
603
+
604
+ function validateContact(value) {
605
+ const input = record(value, "workbench_snapshot_contact_invalid");
606
+ exactOptionalKeys(input, ["contactId", "principalId", "hellolynkId", "displayName", "autonomousSendAuthorized", "continuousReadAuthorized", "updatedAt", "instances"], ["contactId", "hellolynkId", "displayName", "autonomousSendAuthorized", "continuousReadAuthorized", "updatedAt", "instances"], "workbench_snapshot_contact_invalid");
607
+ return {
608
+ contactId: safeId(input.contactId, "workbench_snapshot_contact_id_invalid"),
609
+ principalId: input.principalId == null
610
+ ? null
611
+ : safeId(input.principalId, "workbench_snapshot_peer_id_invalid"),
612
+ hellolynkId: text(input.hellolynkId, 200, "workbench_snapshot_peer_hellolynk_id_invalid"),
613
+ displayName: text(input.displayName, 160, "workbench_snapshot_display_name_invalid"),
614
+ autonomousSendAuthorized: boolean(input.autonomousSendAuthorized, "workbench_snapshot_contact_authorization_invalid"),
615
+ continuousReadAuthorized: boolean(input.continuousReadAuthorized, "workbench_snapshot_ai_read_authorization_invalid"),
616
+ instances: validateContactInstances(input.instances),
617
+ updatedAt: timestamp(input.updatedAt, "workbench_snapshot_contact_time_invalid"),
618
+ };
619
+ }
620
+
621
+ function validateContactInstances(value) {
622
+ const instances = list(value, 1_000, "workbench_snapshot_contact_instances_invalid").map((value) => {
623
+ const item = record(value, "workbench_snapshot_contact_instance_invalid");
624
+ exactKeys(item, ["delegateId", "clientType", "displayName", "createdAt", "updatedAt", "autonomousSendAuthorized", "continuousReadAuthorized", "conversationKey"], "workbench_snapshot_contact_instance_invalid");
625
+ return {
626
+ delegateId: safeId(item.delegateId, "workbench_snapshot_delegate_id_invalid"),
627
+ clientType: text(item.clientType, 80, "workbench_snapshot_client_type_invalid"),
628
+ displayName: text(item.displayName, 160, "workbench_snapshot_display_name_invalid"),
629
+ createdAt: timestamp(item.createdAt, "workbench_snapshot_contact_time_invalid"),
630
+ updatedAt: timestamp(item.updatedAt, "workbench_snapshot_contact_time_invalid"),
631
+ autonomousSendAuthorized: boolean(item.autonomousSendAuthorized, "workbench_snapshot_contact_authorization_invalid"),
632
+ continuousReadAuthorized: boolean(item.continuousReadAuthorized, "workbench_snapshot_ai_read_authorization_invalid"),
633
+ conversationKey: safeId(item.conversationKey, "workbench_snapshot_conversation_key_invalid"),
634
+ };
635
+ });
636
+ if (new Set(instances.map((item) => item.delegateId)).size !== instances.length) fail("workbench_snapshot_contact_instances_duplicate");
637
+ return instances;
638
+ }
639
+
640
+ function validateContactRequest(value, snapshotSchemaVersion) {
641
+ const input = record(value, "workbench_snapshot_contact_request_invalid");
642
+ const legacyKeys = [
643
+ "requestId", "senderHelloLynkId", "senderLabel", "state", "humanOnly", "receivedAt",
644
+ ];
645
+ const decisionKeys = ["actionReady", "decisionOutcome", "decisionAction"];
646
+ const lifecycleKeys = ["direction", "otherHelloLynkId", "otherLabel"];
647
+ const hasDecisionFields = snapshotSchemaVersion >= 2;
648
+ const hasLifecycleFields = snapshotSchemaVersion >= 4;
649
+ exactOptionalKeys(
650
+ input,
651
+ [
652
+ ...legacyKeys,
653
+ ...(hasDecisionFields ? decisionKeys : []),
654
+ ...(hasLifecycleFields ? lifecycleKeys : []),
655
+ "introText",
656
+ ],
657
+ [
658
+ ...legacyKeys,
659
+ ...(hasDecisionFields ? decisionKeys : []),
660
+ ...(hasLifecycleFields ? lifecycleKeys : []),
661
+ ],
662
+ "workbench_snapshot_contact_request_invalid",
663
+ );
664
+ const state = oneOf(input.state, [
665
+ "pending", "accepted", "rejected", "cancelled", "blocked", "expired",
666
+ ], "workbench_snapshot_contact_request_state_invalid");
667
+ const decisionOutcome = hasDecisionFields
668
+ ? (input.decisionOutcome === null
669
+ ? null
670
+ : oneOf(
671
+ input.decisionOutcome,
672
+ ["unknown", "recovery-required"],
673
+ "workbench_snapshot_contact_request_decision_invalid",
674
+ ))
675
+ : null;
676
+ const decisionAction = hasDecisionFields
677
+ ? (input.decisionAction === null
678
+ ? null
679
+ : oneOf(
680
+ input.decisionAction,
681
+ ["accept", "reject", "block"],
682
+ "workbench_snapshot_contact_request_decision_invalid",
683
+ ))
684
+ : null;
685
+ const actionReady = hasDecisionFields
686
+ ? boolean(
687
+ input.actionReady,
688
+ "workbench_snapshot_contact_request_decision_invalid",
689
+ )
690
+ : false;
691
+ if ((actionReady && (state !== "pending"
692
+ || decisionOutcome != null
693
+ || decisionAction != null))
694
+ || (state !== "pending" && actionReady)
695
+ || ((decisionOutcome === "unknown") !== (decisionAction != null))
696
+ || (decisionOutcome != null && (state !== "pending" || actionReady))
697
+ ) {
698
+ fail("workbench_snapshot_contact_request_decision_invalid");
699
+ }
700
+ return {
701
+ requestId: safeId(input.requestId, "workbench_snapshot_contact_request_id_invalid"),
702
+ direction: hasLifecycleFields
703
+ ? oneOf(
704
+ input.direction,
705
+ ["incoming", "outgoing"],
706
+ "workbench_snapshot_contact_request_direction_invalid",
707
+ )
708
+ : "incoming",
709
+ otherHelloLynkId: hasLifecycleFields
710
+ ? text(input.otherHelloLynkId, 200, "workbench_snapshot_peer_hellolynk_id_invalid")
711
+ : text(input.senderHelloLynkId, 200, "workbench_snapshot_peer_hellolynk_id_invalid"),
712
+ otherLabel: hasLifecycleFields
713
+ ? optionalText(input.otherLabel, 160, "workbench_snapshot_display_name_invalid")
714
+ : optionalText(input.senderLabel, 160, "workbench_snapshot_display_name_invalid"),
715
+ senderHelloLynkId: text(input.senderHelloLynkId, 200, "workbench_snapshot_peer_hellolynk_id_invalid"),
716
+ senderLabel: optionalText(input.senderLabel, 160, "workbench_snapshot_display_name_invalid"),
717
+ state,
718
+ actionReady,
719
+ decisionOutcome,
720
+ decisionAction,
721
+ humanOnly: boolean(input.humanOnly, "workbench_snapshot_contact_request_visibility_invalid"),
722
+ ...(Object.hasOwn(input, "introText") ? {
723
+ introText: text(input.introText, 80, "workbench_snapshot_contact_request_intro_invalid", true),
724
+ } : {}),
725
+ receivedAt: timestamp(input.receivedAt, "workbench_snapshot_contact_request_time_invalid"),
726
+ };
727
+
728
+ }
729
+
730
+ function validateRelationship(value) {
731
+ const input = record(value, "workbench_snapshot_relationship_invalid");
732
+ exactKeys(input, [
733
+ "relationshipId", "otherHelloLynkId", "otherLabel", "status", "createdAt", "updatedAt", "endedAt",
734
+ ], "workbench_snapshot_relationship_invalid");
735
+ const status = oneOf(
736
+ input.status,
737
+ ["active", "ended"],
738
+ "workbench_snapshot_relationship_status_invalid",
739
+ );
740
+ const endedAt = input.endedAt == null
741
+ ? null
742
+ : timestamp(input.endedAt, "workbench_snapshot_relationship_time_invalid");
743
+ if ((status === "active") !== (endedAt == null)) {
744
+ fail("workbench_snapshot_relationship_status_invalid");
745
+ }
746
+ return {
747
+ relationshipId: safeId(input.relationshipId, "workbench_snapshot_relationship_id_invalid"),
748
+ otherHelloLynkId: text(input.otherHelloLynkId, 200, "workbench_snapshot_peer_hellolynk_id_invalid"),
749
+ otherLabel: optionalText(input.otherLabel, 160, "workbench_snapshot_display_name_invalid"),
750
+ status,
751
+ createdAt: timestamp(input.createdAt, "workbench_snapshot_relationship_time_invalid"),
752
+ updatedAt: timestamp(input.updatedAt, "workbench_snapshot_relationship_time_invalid"),
753
+ endedAt,
754
+ };
755
+ }
756
+
757
+ function validateBlock(value) {
758
+ const input = record(value, "workbench_snapshot_block_invalid");
759
+ exactKeys(input, [
760
+ "blockId", "otherHelloLynkId", "otherLabel", "status", "createdAt",
761
+ ], "workbench_snapshot_block_invalid");
762
+ return {
763
+ blockId: safeId(input.blockId, "workbench_snapshot_block_id_invalid"),
764
+ otherHelloLynkId: text(input.otherHelloLynkId, 200, "workbench_snapshot_peer_hellolynk_id_invalid"),
765
+ otherLabel: optionalText(input.otherLabel, 160, "workbench_snapshot_display_name_invalid"),
766
+ status: oneOf(input.status, ["active"], "workbench_snapshot_block_status_invalid"),
767
+ createdAt: timestamp(input.createdAt, "workbench_snapshot_block_time_invalid"),
768
+ };
769
+ }
770
+
771
+ function validateSecurity(value) {
772
+ const input = record(value, "workbench_snapshot_security_invalid");
773
+ exactKeys(input, ["hostStatus", "connectedClients"], "workbench_snapshot_security_invalid");
774
+ return {
775
+ hostStatus: oneOf(input.hostStatus, [...HOST_STATES], "workbench_snapshot_host_status_invalid"),
776
+ connectedClients: list(input.connectedClients, 32, "workbench_snapshot_clients_invalid").map((item) => {
777
+ const client = record(item, "workbench_snapshot_client_invalid");
778
+ exactKeys(client, ["clientId", "clientType", "state"], "workbench_snapshot_client_invalid");
779
+ return {
780
+ clientId: safeId(client.clientId, "workbench_snapshot_client_id_invalid"),
781
+ clientType: oneOf(client.clientType, ["codex", "cursor", "dsh", "openclaw", "trae-work", "workbuddy", "claude-code", "unknown"], "workbench_snapshot_client_type_invalid"),
782
+ state: oneOf(client.state, [...CLIENT_STATES], "workbench_snapshot_client_state_invalid"),
783
+ };
784
+ }),
785
+ };
786
+ }
787
+
788
+ function validateChanges(value) {
789
+ const input = record(value, "workbench_snapshot_changes_invalid");
790
+ exactKeys(input, ["cursor", "snapshotRequired", "eventTypes"], "workbench_snapshot_changes_invalid");
791
+ return {
792
+ cursor: safeRevision(input.cursor),
793
+ snapshotRequired: boolean(input.snapshotRequired, "workbench_snapshot_changes_invalid"),
794
+ eventTypes: list(input.eventTypes, 64, "workbench_snapshot_changes_invalid").map(errorCode),
795
+ };
796
+ }
797
+
798
+ function validateCommandPayload(type, value) {
799
+ const input = record(value, "workbench_command_payload_invalid");
800
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal) {
801
+ exactKeys(input, ["mode", "query"], "workbench_command_payload_fields_invalid");
802
+ const mode = oneOf(input.mode, ["hellolynk_id", "nickname"], "workbench_command_principal_search_mode_invalid");
803
+ const query = text(input.query, 200, "workbench_command_principal_search_query_invalid");
804
+ if (mode === "hellolynk_id" && !/^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/.test(query)) {
805
+ fail("workbench_command_principal_search_query_invalid");
806
+ }
807
+ return { mode, query };
808
+ }
809
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.sendContactRequest) {
810
+ exactKeys(input, ["targetRef", "introText"], "workbench_command_payload_fields_invalid");
811
+ return {
812
+ targetRef: safeId(input.targetRef, "workbench_command_contact_target_invalid"),
813
+ introText: validateWorkbenchContactRequestIntro(input.introText),
814
+ };
815
+ }
816
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestSend) {
817
+ exactKeys(input, ["clientMessageId", "recipientHelloLynkId", "recipientDelegateId", "text"], "workbench_command_payload_fields_invalid");
818
+ const message = text(input.text, 20_480, "workbench_command_text_invalid");
819
+ if (new TextEncoder().encode(message).byteLength > 20_480) fail("workbench_command_text_invalid");
820
+ return {
821
+ clientMessageId: safeId(input.clientMessageId, "workbench_command_client_message_id_invalid"),
822
+ recipientHelloLynkId: text(input.recipientHelloLynkId, 200, "workbench_command_recipient_invalid"),
823
+ recipientDelegateId: safeId(input.recipientDelegateId, "workbench_command_recipient_delegate_invalid"),
824
+ text: message,
825
+ };
826
+ }
827
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus) {
828
+ exactKeys(input, ["actionId"], "workbench_command_payload_fields_invalid");
829
+ return { actionId: safeId(input.actionId, "workbench_command_action_id_invalid") };
830
+ }
831
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.markRead) {
832
+ exactKeys(input, ["localMessageId"], "workbench_command_payload_fields_invalid");
833
+ return { localMessageId: safeId(input.localMessageId, "workbench_command_message_id_invalid") };
834
+ }
835
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.markAllRead) {
836
+ exactKeys(input, ["threadId", "beforeReceiptSeq"], "workbench_command_payload_fields_invalid");
837
+ return { threadId: input.threadId === null ? null : safeId(input.threadId, "workbench_command_thread_id_invalid"),
838
+ beforeReceiptSeq: nonNegativeInteger(input.beforeReceiptSeq, "workbench_command_inbox_boundary_invalid") };
839
+ }
840
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.setAutoRead) {
841
+ exactKeys(input, ["threadId", "enabled"], "workbench_command_payload_fields_invalid");
842
+ return { threadId: safeId(input.threadId, "workbench_command_thread_id_invalid"),
843
+ enabled: boolean(input.enabled, "workbench_command_auto_read_invalid") };
844
+ }
845
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteRead
846
+ || type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteSent) {
847
+ exactKeys(input, ["localMessageIds"], "workbench_command_payload_fields_invalid");
848
+ if (!Array.isArray(input.localMessageIds) || input.localMessageIds.length === 0) {
849
+ fail("workbench_command_message_ids_invalid");
850
+ }
851
+ return {
852
+ localMessageIds: list(input.localMessageIds, 500, "workbench_command_message_ids_invalid")
853
+ .map((item) => safeId(item, "workbench_command_message_id_invalid")),
854
+ };
855
+ }
856
+ if ([HELLOLYNK_WORKBENCH_COMMAND_TYPES.hideThread,
857
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.restoreThread,
858
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearThread, HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteThread].includes(type)) {
859
+ exactOptionalKeys(input, type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearThread ? ["threadId", "before"] : ["threadId"],
860
+ ["threadId"], "workbench_command_payload_fields_invalid");
861
+ return { threadId: safeId(input.threadId, "workbench_command_thread_id_invalid"),
862
+ ...(Object.hasOwn(input, "before") ? {
863
+ before: input.before == null ? null : timestamp(input.before, "workbench_command_history_before_invalid"),
864
+ } : {}) };
865
+ }
866
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteArchivedThreads) {
867
+ exactKeys(input, [], "workbench_command_payload_fields_invalid"); return {};
868
+ }
869
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.configureHistory) {
870
+ exactKeys(input, ["autoArchiveAfterHours"], "workbench_command_payload_fields_invalid");
871
+ if (input.autoArchiveAfterHours !== null && input.autoArchiveAfterHours !== 24) fail("workbench_history_setting_invalid");
872
+ return { autoArchiveAfterHours: input.autoArchiveAfterHours };
873
+ }
874
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestRetry || type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.resendDeletedReply) {
875
+ exactKeys(input, ["clientMessageId"], "workbench_command_payload_fields_invalid");
876
+ return { clientMessageId: safeId(input.clientMessageId, "workbench_command_client_message_id_invalid") };
877
+ }
878
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.saveContact) {
879
+ exactOptionalKeys(
880
+ input,
881
+ ["contactId", "hellolynkId", "displayName"],
882
+ ["hellolynkId", "displayName"],
883
+ "workbench_command_payload_fields_invalid",
884
+ );
885
+ const contactId = input.contactId == null ? null : safeId(input.contactId, "workbench_command_contact_id_invalid");
886
+ return {
887
+ contactId,
888
+ hellolynkId: text(input.hellolynkId, 200, "workbench_command_recipient_invalid"),
889
+ displayName: text(input.displayName, 160, "workbench_command_contact_name_invalid"),
890
+ };
891
+ }
892
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContact) {
893
+ exactKeys(input, ["contactId", "displayName", "expectedDisplayName", "expectedHelloLynkId"], "workbench_command_payload_fields_invalid");
894
+ return {
895
+ contactId: safeId(input.contactId, "workbench_command_contact_id_invalid"),
896
+ displayName: text(input.displayName, 160, "workbench_command_contact_name_invalid"),
897
+ expectedDisplayName: text(input.expectedDisplayName, 160, "workbench_command_contact_name_invalid"),
898
+ expectedHelloLynkId: text(input.expectedHelloLynkId, 200, "workbench_command_recipient_invalid"),
899
+ };
900
+ }
901
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.listContactInstances
902
+ || type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteContact) {
903
+ exactKeys(input, ["contactId"], "workbench_command_payload_fields_invalid");
904
+ return { contactId: safeId(input.contactId, "workbench_command_contact_id_invalid") };
905
+ }
906
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.saveContactInstance) {
907
+ exactOptionalKeys(input, ["contactId", "recipientDelegateId", "displayName"], ["contactId", "recipientDelegateId"], "workbench_command_payload_fields_invalid");
908
+ const accepted = {
909
+ contactId: safeId(input.contactId, "workbench_command_contact_id_invalid"),
910
+ recipientDelegateId: safeId(input.recipientDelegateId, "workbench_command_recipient_delegate_invalid"),
911
+ };
912
+ if (input.displayName !== "" && Object.hasOwn(input, "displayName")) {
913
+ accepted.displayName = text(input.displayName, 80, "workbench_command_contact_name_invalid");
914
+ }
915
+ return accepted;
916
+ }
917
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContactInstance) {
918
+ exactKeys(input, ["contactId", "recipientDelegateId", "displayName", "expectedDisplayName", "expectedHelloLynkId", "expectedPrincipalId"], "workbench_command_payload_fields_invalid");
919
+ return {
920
+ contactId: safeId(input.contactId, "workbench_command_contact_id_invalid"),
921
+ recipientDelegateId: safeId(input.recipientDelegateId, "workbench_command_recipient_delegate_invalid"),
922
+ displayName: text(input.displayName, 80, "workbench_command_contact_name_invalid"),
923
+ expectedDisplayName: text(input.expectedDisplayName, 80, "workbench_command_contact_name_invalid"),
924
+ expectedHelloLynkId: text(input.expectedHelloLynkId, 200, "workbench_command_recipient_invalid"),
925
+ expectedPrincipalId: safeId(input.expectedPrincipalId, "workbench_command_principal_invalid"),
926
+ };
927
+ }
928
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteContactInstance) {
929
+ exactKeys(input, ["contactId", "recipientDelegateId"], "workbench_command_payload_fields_invalid");
930
+ return {
931
+ contactId: safeId(input.contactId, "workbench_command_contact_id_invalid"),
932
+ recipientDelegateId: safeId(input.recipientDelegateId, "workbench_command_recipient_delegate_invalid"),
933
+ };
934
+ }
935
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearResolvedContactRequests) {
936
+ exactKeys(input, [], "workbench_command_payload_fields_invalid");
937
+ return {};
938
+ }
939
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.recoverContactRequestDecision) {
940
+ exactKeys(input, ["requestId"], "workbench_command_payload_fields_invalid");
941
+ return {
942
+ requestId: safeId(input.requestId, "workbench_command_contact_request_id_invalid"),
943
+ };
944
+ }
945
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.respondContactRequest) {
946
+ exactKeys(input, ["requestId", "decision"], "workbench_command_payload_fields_invalid");
947
+ return {
948
+ requestId: safeId(input.requestId, "workbench_command_contact_request_id_invalid"),
949
+ decision: oneOf(input.decision, ["accept", "reject", "block"], "workbench_command_contact_request_decision_invalid"),
950
+ };
951
+ }
952
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.cancelContactRequest) {
953
+ exactKeys(input, ["requestId"], "workbench_command_payload_fields_invalid");
954
+ return {
955
+ requestId: safeId(input.requestId, "workbench_command_contact_request_id_invalid"),
956
+ };
957
+ }
958
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.setContactAuthorization) {
959
+ exactKeys(input, ["contactId", "recipientDelegateId", "enabled"], "workbench_command_payload_fields_invalid");
960
+ if (typeof input.enabled !== "boolean") fail("workbench_command_authorization_enabled_invalid");
961
+ return {
962
+ contactId: safeId(input.contactId, "workbench_command_contact_id_invalid"),
963
+ recipientDelegateId: safeId(input.recipientDelegateId, "workbench_command_recipient_delegate_invalid"),
964
+ enabled: input.enabled,
965
+ };
966
+ }
967
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.endRelationship) {
968
+ exactKeys(input, ["relationshipId"], "workbench_command_payload_fields_invalid");
969
+ return {
970
+ relationshipId: safeId(input.relationshipId, "workbench_command_relationship_id_invalid"),
971
+ };
972
+ }
973
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.removeBlock) {
974
+ exactKeys(input, ["blockId"], "workbench_command_payload_fields_invalid");
975
+ return { blockId: safeId(input.blockId, "workbench_command_block_id_invalid") };
976
+ }
977
+ if (type === HELLOLYNK_WORKBENCH_COMMAND_TYPES.openAccount) {
978
+ exactKeys(input, ["target"], "workbench_command_payload_fields_invalid");
979
+ return { target: oneOf(input.target, ["account", "devices", "keys"], "workbench_command_account_target_invalid") };
980
+ }
981
+ fail("workbench_command_type_invalid");
982
+ }
983
+
984
+ function validateCommandSuccessResult(value, expectedCommand) {
985
+ const input = record(value, "workbench_command_result_invalid");
986
+ if (Object.keys(input).length === 0) {
987
+ // Older Hosts returned an empty success body after admitting request-send.
988
+ // Keep parsing that result so upgraded clients can treat it as unknown,
989
+ // while the new status command always requires an authoritative state.
990
+ if (expectedCommand === HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus) {
991
+ fail("workbench_command_result_type_mismatch");
992
+ }
993
+ return {};
994
+ }
995
+ if (expectedCommand == null) fail("workbench_command_result_type_required");
996
+ if (input.kind === "messages-marked-read") {
997
+ exactKeys(input, ["kind", "markedCount", "threadId", "beforeReceiptSeq"], "workbench_command_result_fields_invalid");
998
+ return { kind: input.kind,
999
+ markedCount: nonNegativeInteger(input.markedCount, "workbench_command_read_count_invalid"),
1000
+ threadId: input.threadId === null ? null : safeId(input.threadId, "workbench_command_thread_id_invalid"),
1001
+ beforeReceiptSeq: nonNegativeInteger(input.beforeReceiptSeq, "workbench_command_inbox_boundary_invalid") };
1002
+ }
1003
+ if (input.kind === "conversation-auto-read") {
1004
+ exactKeys(input, ["kind", "threadId", "enabled"], "workbench_command_result_fields_invalid");
1005
+ return { kind: input.kind, threadId: safeId(input.threadId, "workbench_command_thread_id_invalid"),
1006
+ enabled: boolean(input.enabled, "workbench_command_auto_read_invalid") };
1007
+ }
1008
+ if (input.kind === "deleted-reply-resend") {
1009
+ exactKeys(input, ["kind", "originalClientMessageId", "clientMessageId", "status"], "workbench_command_result_invalid");
1010
+ return { kind: input.kind, originalClientMessageId: safeId(input.originalClientMessageId, "workbench_command_result_invalid"),
1011
+ clientMessageId: safeId(input.clientMessageId, "workbench_command_result_invalid"),
1012
+ status: oneOf(input.status, ["sent", "not-delivered", "unknown"], "workbench_command_result_invalid") };
1013
+ }
1014
+ if (input.kind === "local-history") {
1015
+ exactOptionalKeys(input, ["kind", "deletedCount", "retainedUnreadCount", "retainedOutgoingCount", "retainedTaskCount", "deletedBytes"],
1016
+ ["kind", "deletedCount", "retainedUnreadCount", "retainedOutgoingCount"],
1017
+ "workbench_command_result_fields_invalid");
1018
+ return {
1019
+ kind: "local-history",
1020
+ deletedCount: nonNegativeInteger(input.deletedCount, "workbench_command_local_history_count_invalid"),
1021
+ retainedUnreadCount: nonNegativeInteger(input.retainedUnreadCount, "workbench_command_local_history_count_invalid"),
1022
+ retainedOutgoingCount: nonNegativeInteger(input.retainedOutgoingCount, "workbench_command_local_history_count_invalid"),
1023
+ ...Object.fromEntries(["retainedTaskCount", "deletedBytes"].filter((field) => Object.hasOwn(input, field))
1024
+ .map((field) => [field, nonNegativeInteger(input[field], "workbench_command_local_history_count_invalid")])),
1025
+ };
1026
+ }
1027
+ if (input.kind === "message-send") {
1028
+ exactKeys(input, ["kind", "actionId", "state", "code", "createdAtMs", "expiresAtMs"], "workbench_command_result_fields_invalid");
1029
+ const state = oneOf(input.state, [
1030
+ "pending", "consuming", "approved", "cancelled", "expired", "failed", "invalidated",
1031
+ ], "workbench_command_send_state_invalid");
1032
+ const code = input.code == null ? null : errorCode(input.code);
1033
+ if ((["pending", "consuming", "approved"].includes(state) && code !== null)
1034
+ || (["cancelled", "expired", "failed", "invalidated"].includes(state)
1035
+ && (code === null || !/^human_confirmation_[a-z0-9_]{1,100}$/.test(code)))) {
1036
+ fail("workbench_command_send_state_invalid");
1037
+ }
1038
+ const createdAtMs = nonNegativeInteger(input.createdAtMs, "workbench_command_send_time_invalid");
1039
+ const expiresAtMs = nonNegativeInteger(input.expiresAtMs, "workbench_command_send_time_invalid");
1040
+ if (expiresAtMs <= createdAtMs) fail("workbench_command_send_time_invalid");
1041
+ return {
1042
+ kind: "message-send",
1043
+ actionId: safeId(input.actionId, "workbench_command_action_id_invalid"),
1044
+ state,
1045
+ code,
1046
+ createdAtMs,
1047
+ expiresAtMs,
1048
+ };
1049
+ }
1050
+ if (input.kind === "contact-instances") {
1051
+ exactKeys(input, ["kind", "contactId", "principalId", "hellolynkId", "instances"], "workbench_command_result_fields_invalid");
1052
+ const instances = list(input.instances, 1_000, "workbench_command_contact_instances_invalid").map((value) => {
1053
+ const item = record(value, "workbench_command_contact_instance_invalid");
1054
+ exactOptionalKeys(item, ["delegateId", "displayName", "clientType", "status", "createdAt"],
1055
+ ["delegateId", "displayName", "clientType", "status"], "workbench_command_contact_instance_invalid");
1056
+ return {
1057
+ delegateId: safeId(item.delegateId, "workbench_command_recipient_delegate_invalid"),
1058
+ displayName: text(item.displayName, 160, "workbench_command_contact_name_invalid"),
1059
+ clientType: text(item.clientType, 80, "workbench_command_client_type_invalid"),
1060
+ status: oneOf(item.status, ["active"], "workbench_command_contact_instance_status_invalid"),
1061
+ ...(item.createdAt === undefined ? {} : { createdAt: timestamp(item.createdAt, "workbench_command_contact_instance_invalid") }),
1062
+ };
1063
+ });
1064
+ if (new Set(instances.map((item) => item.delegateId)).size !== instances.length) fail("workbench_command_contact_instances_duplicate");
1065
+ return {
1066
+ kind: "contact-instances",
1067
+ contactId: safeId(input.contactId, "workbench_command_contact_id_invalid"),
1068
+ principalId: safeId(input.principalId, "workbench_snapshot_principal_id_invalid"),
1069
+ hellolynkId: text(input.hellolynkId, 200, "workbench_command_recipient_invalid"),
1070
+ instances,
1071
+ };
1072
+ }
1073
+ if (input.kind === "principal-search") {
1074
+ exactKeys(input, ["kind", "mode", "results", "quota", "exactMatchOnly", "modelCalls"], "workbench_command_result_fields_invalid");
1075
+ const quota = record(input.quota, "workbench_command_search_quota_invalid");
1076
+ exactKeys(quota, ["limit", "remaining", "resetsAt"], "workbench_command_search_quota_invalid");
1077
+ if (input.exactMatchOnly !== true || input.modelCalls !== 0) fail("workbench_command_result_invalid");
1078
+ const acceptedQuota = {
1079
+ limit: nonNegativeInteger(quota.limit, "workbench_command_search_quota_invalid"),
1080
+ remaining: nonNegativeInteger(quota.remaining, "workbench_command_search_quota_invalid"),
1081
+ resetsAt: timestamp(quota.resetsAt, "workbench_command_search_quota_invalid"),
1082
+ };
1083
+ if (acceptedQuota.limit !== 30 || acceptedQuota.remaining > acceptedQuota.limit) {
1084
+ fail("workbench_command_search_quota_invalid");
1085
+ }
1086
+ const mode = oneOf(input.mode, ["hellolynk_id", "nickname"], "workbench_command_principal_search_mode_invalid");
1087
+ const results = list(input.results, 32, "workbench_command_search_results_invalid").map((value) => {
1088
+ const item = record(value, "workbench_command_search_result_invalid");
1089
+ exactKeys(item, ["targetRef", "displayName", "hellolynkId", "expiresAt"], "workbench_command_search_result_invalid");
1090
+ return {
1091
+ targetRef: safeId(item.targetRef, "workbench_command_contact_target_invalid"),
1092
+ displayName: optionalText(item.displayName, 160, "workbench_command_search_result_invalid"),
1093
+ hellolynkId: item.hellolynkId == null ? null : searchResultHelloLynkId(item.hellolynkId),
1094
+ expiresAt: timestamp(item.expiresAt, "workbench_command_search_result_invalid"),
1095
+ };
1096
+ });
1097
+ if (mode === "nickname" && results.some((item) => item.hellolynkId != null)) {
1098
+ fail("workbench_command_search_result_privacy_invalid");
1099
+ }
1100
+ return {
1101
+ kind: "principal-search",
1102
+ mode,
1103
+ results,
1104
+ quota: acceptedQuota,
1105
+ exactMatchOnly: true,
1106
+ modelCalls: 0,
1107
+ };
1108
+ }
1109
+ if (input.kind === "contact-request-send") {
1110
+ exactKeys(input, ["kind", "status", "quota", "serverStoredIntro", "serverStoredCiphertext", "modelCalls"], "workbench_command_result_fields_invalid");
1111
+ if (input.serverStoredIntro !== false
1112
+ || (input.serverStoredCiphertext !== null && typeof input.serverStoredCiphertext !== "boolean") || input.modelCalls !== 0) {
1113
+ fail("workbench_command_result_invalid");
1114
+ }
1115
+ let quota = null;
1116
+ if (input.quota != null) {
1117
+ const rawQuota = record(input.quota, "workbench_command_contact_request_quota_invalid");
1118
+ exactKeys(rawQuota, ["limit", "used", "remaining", "nextResetAt"], "workbench_command_contact_request_quota_invalid");
1119
+ quota = {
1120
+ limit: nonNegativeInteger(rawQuota.limit, "workbench_command_contact_request_quota_invalid"),
1121
+ used: nonNegativeInteger(rawQuota.used, "workbench_command_contact_request_quota_invalid"),
1122
+ remaining: nonNegativeInteger(rawQuota.remaining, "workbench_command_contact_request_quota_invalid"),
1123
+ nextResetAt: timestamp(rawQuota.nextResetAt, "workbench_command_contact_request_quota_invalid"),
1124
+ };
1125
+ if (quota.limit !== 3 || quota.used > quota.limit || quota.remaining > quota.limit
1126
+ || quota.used + quota.remaining !== quota.limit) {
1127
+ fail("workbench_command_contact_request_quota_invalid");
1128
+ }
1129
+ }
1130
+ return {
1131
+ kind: "contact-request-send",
1132
+ status: oneOf(input.status, ["submitted", "delivery_unknown"], "workbench_command_contact_request_status_invalid"),
1133
+ quota,
1134
+ serverStoredIntro: false,
1135
+ serverStoredCiphertext: input.serverStoredCiphertext,
1136
+ modelCalls: 0,
1137
+ };
1138
+ }
1139
+ if (input.kind === "contact-request-clear-resolved") {
1140
+ exactKeys(input, ["kind", "deletedCount", "inboundResume", "localOnly", "modelCalls"], "workbench_command_result_fields_invalid");
1141
+ if (input.localOnly !== true || input.modelCalls !== 0) fail("workbench_command_result_invalid");
1142
+ return {
1143
+ kind: "contact-request-clear-resolved",
1144
+ deletedCount: nonNegativeInteger(input.deletedCount, "workbench_command_clear_resolved_count_invalid"),
1145
+ inboundResume: oneOf(input.inboundResume, ["not_applicable", "resume_scheduled", "resume_attempted", "retry_required"], "workbench_command_clear_resolved_resume_invalid"),
1146
+ localOnly: true,
1147
+ modelCalls: 0,
1148
+ };
1149
+ }
1150
+ fail("workbench_command_result_fields_invalid");
1151
+ }
1152
+
1153
+ export function validateWorkbenchContactRequestIntro(value) {
1154
+ if (typeof value !== "string") {
1155
+ fail("contact_request_intro_invalid");
1156
+ }
1157
+ if (value.length > 80 || /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u.test(value)) {
1158
+ fail(value.length > 80 ? "contact_request_intro_too_long" : "contact_request_intro_forbidden_character");
1159
+ }
1160
+ if (/^\p{Zs}*$/u.test(value)) return "";
1161
+ let weight = 0;
1162
+ for (const codePoint of value) {
1163
+ const scalar = codePoint.codePointAt(0);
1164
+ weight += scalar >= 0x20 && scalar <= 0x7e ? 1 : 2;
1165
+ if (weight > 40) fail("contact_request_intro_too_long");
1166
+ }
1167
+ const candidate = value.normalize("NFKC").replace(/[\u3002\uFF0E\uFF61]/gu, ".");
1168
+ if (/\b(?:[A-Za-z][A-Za-z0-9+.-]{0,31}:[/\\]|(?:https?|ftps?|wss?|file|mailto|tel|sms|data|javascript|vbscript|blob|about|intent|magnet|urn|hellolynk):\S)/iu.test(candidate)
1169
+ || /[\p{L}\p{N}\p{M}.!#$%&'*+/=?^_`{|}~-]+@[\p{L}\p{N}\p{M}](?:[\p{L}\p{N}\p{M}-]{0,61}[\p{L}\p{N}\p{M}])?(?:\.[\p{L}\p{N}\p{M}](?:[\p{L}\p{N}\p{M}-]{0,61}[\p{L}\p{N}\p{M}])?)*/iu.test(candidate)
1170
+ || /(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{2,59})(?=$|[^a-z0-9-])/iu.test(candidate)
1171
+ || /(?:[\p{L}\p{N}](?:[\p{L}\p{N}\p{M}-]{0,61}[\p{L}\p{N}\p{M}])?\.)+(?=[\p{L}\p{N}\p{M}-]{2,63}(?=$|[^\p{L}\p{N}\p{M}-]))\p{L}(?:[\p{L}\p{N}\p{M}-]{0,61}[\p{L}\p{N}\p{M}])?(?=$|[^\p{L}\p{N}\p{M}-])/iu.test(candidate)
1172
+ || /\bwww\s*\./iu.test(candidate)
1173
+ || /(?:^|[^0-9])(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?=$|[^0-9])/u.test(candidate)) {
1174
+ fail("contact_request_intro_link_forbidden");
1175
+ }
1176
+ return value;
1177
+ }
1178
+
1179
+ function searchResultHelloLynkId(value) {
1180
+ const accepted = text(value, 200, "workbench_command_search_result_invalid");
1181
+ if (!/^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$/.test(accepted)) {
1182
+ fail("workbench_command_search_result_invalid");
1183
+ }
1184
+ return accepted;
1185
+ }
1186
+
1187
+ function record(value, code) {
1188
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(code);
1189
+ return value;
1190
+ }
1191
+
1192
+ function list(value, maximum, code) {
1193
+ if (!Array.isArray(value) || value.length > maximum) fail(code);
1194
+ return value;
1195
+ }
1196
+
1197
+ function exactKeys(value, keys, code) {
1198
+ const actual = Object.keys(value);
1199
+ if (actual.length !== keys.length || actual.some((key) => !keys.includes(key))) fail(code);
1200
+ }
1201
+
1202
+ function exactOptionalKeys(value, allowed, required, code) {
1203
+ const actual = Object.keys(value);
1204
+ if (actual.some((key) => !allowed.includes(key)) || required.some((key) => !Object.hasOwn(value, key))) fail(code);
1205
+ }
1206
+
1207
+ function assertNoSensitiveKeys(value, seen = new WeakSet(), depth = 0, path = []) {
1208
+ if (!value || typeof value !== "object") return;
1209
+ if (depth > 32 || seen.has(value)) fail("workbench_sensitive_field_invalid");
1210
+ seen.add(value);
1211
+ const forbiddenExact = new Set([
1212
+ "apikey",
1213
+ "authorization",
1214
+ "bearer",
1215
+ "bindingkey",
1216
+ "capabilitysecret",
1217
+ "cookie",
1218
+ "password",
1219
+ "privatekey",
1220
+ "secret",
1221
+ "token",
1222
+ ]);
1223
+ for (const [key, nested] of Object.entries(value)) {
1224
+ const normalized = String(key).toLowerCase().replace(/[^a-z0-9]/g, "");
1225
+ const exactMessageAuthorizationScope = key === "authorizationScope"
1226
+ && path.length === 3
1227
+ && path[0] === "messages"
1228
+ && path[1] === "items"
1229
+ && /^(0|[1-9][0-9]*)$/.test(path[2])
1230
+ && (nested == null || nested === MESSAGE_AUTHORIZATION_SCOPE);
1231
+ if (!exactMessageAuthorizationScope && (forbiddenExact.has(normalized)
1232
+ || (normalized.startsWith("authorization") && normalized !== "authorizationversion")
1233
+ || /(token|cookie|password|privatekey|bindingkey|capabilitysecret|apikey|bearer|credential)/.test(normalized))) {
1234
+ fail("workbench_sensitive_field_invalid");
1235
+ }
1236
+ assertNoSensitiveKeys(nested, seen, depth + 1, [...path, key]);
1237
+ }
1238
+ }
1239
+
1240
+ function text(value, maximum, code, allowEmpty = false) {
1241
+ if (typeof value !== "string" || (!allowEmpty && !value.trim()) || value.length > maximum || value.includes("\u0000")) fail(code);
1242
+ return value;
1243
+ }
1244
+
1245
+ function optionalText(value, maximum, code) {
1246
+ if (value == null || value === "") return null;
1247
+ return text(value, maximum, code);
1248
+ }
1249
+
1250
+ function searchText(value) {
1251
+ if (typeof value !== "string" || value.length > HELLOLYNK_WORKBENCH_MAX_QUERY_LENGTH || value.includes("\u0000")) {
1252
+ fail("workbench_read_query_invalid");
1253
+ }
1254
+ // Keep Unicode unexpanded across transport validation. NFKC/lowercase can
1255
+ // expand it; downstream guards must not count that as extra user input.
1256
+ return value.trim().replace(/\s+/gu, " ");
1257
+ }
1258
+
1259
+ function safeId(value, code) {
1260
+ if (typeof value !== "string" || !SAFE_ID.test(value)) fail(code);
1261
+ return value;
1262
+ }
1263
+
1264
+ function safeRevision(value) {
1265
+ if (typeof value !== "string" || !SAFE_REVISION.test(value)) fail("workbench_revision_invalid");
1266
+ return value;
1267
+ }
1268
+
1269
+ function contactRequestCursor(value) {
1270
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{20,512}$/.test(value)) {
1271
+ fail("workbench_contact_request_cursor_invalid");
1272
+ }
1273
+ return value;
1274
+ }
1275
+
1276
+ function errorCode(value) {
1277
+ if (typeof value !== "string" || !/^[a-z][a-z0-9_.:-]{1,126}$/.test(value)) fail("workbench_error_code_invalid");
1278
+ return value;
1279
+ }
1280
+
1281
+ function timestamp(value, code) {
1282
+ if (typeof value !== "string" || Number.isNaN(Date.parse(value))) fail(code);
1283
+ return value;
1284
+ }
1285
+
1286
+ function nonNegativeInteger(value, code) {
1287
+ if (!Number.isSafeInteger(value) || value < 0) fail(code);
1288
+ return value;
1289
+ }
1290
+
1291
+ function positiveInteger(value, code) {
1292
+ if (!Number.isSafeInteger(value) || value < 1) fail(code);
1293
+ return value;
1294
+ }
1295
+
1296
+ function boolean(value, code) {
1297
+ if (typeof value !== "boolean") fail(code);
1298
+ return value;
1299
+ }
1300
+
1301
+ function oneOf(value, allowed, code) {
1302
+ if (!allowed.includes(value)) fail(code);
1303
+ return value;
1304
+ }
1305
+
1306
+ function deepFreeze(value) {
1307
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
1308
+ for (const nested of Object.values(value)) deepFreeze(nested);
1309
+ return Object.freeze(value);
1310
+ }
1311
+
1312
+ function fail(code) {
1313
+ throw new HelloLynkWorkbenchContractError(code);
1314
+ }