@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.
- package/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +92 -0
- package/README.md +101 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- 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
|
+
}
|