@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,97 @@
1
+ import crypto from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
7
+ const UI_ROOT = path.join(MODULE_DIR, "ui");
8
+
9
+ const MODULES = Object.freeze([
10
+ ["bridge", path.join(UI_ROOT, "mcp-app-client.mjs")],
11
+ ["contract", path.join(MODULE_DIR, "contract.mjs")],
12
+ ["state", path.join(MODULE_DIR, "state.mjs")],
13
+ ["i18n", path.join(MODULE_DIR, "i18n.mjs")],
14
+ ["connection", path.join(UI_ROOT, "connection-form.mjs")],
15
+ ["app", path.join(UI_ROOT, "app.mjs")],
16
+ ]);
17
+ const ASSETS = Object.freeze([
18
+ ["brand", "/assets/hellolynk-mark.png", path.join(UI_ROOT, "hellolynk-mark.png"), "image/png"],
19
+ ["arrowLeft", "/assets/icons/arrow-left.svg", path.join(UI_ROOT, "icons", "arrow-left.svg"), "image/svg+xml"],
20
+ ["close", "/assets/icons/close.svg", path.join(UI_ROOT, "icons", "close.svg"), "image/svg+xml"],
21
+ ["contacts", "/assets/icons/contacts.svg", path.join(UI_ROOT, "icons", "contacts.svg"), "image/svg+xml"],
22
+ ["inbox", "/assets/icons/inbox.svg", path.join(UI_ROOT, "icons", "inbox.svg"), "image/svg+xml"],
23
+ ["refresh", "/assets/icons/refresh.svg", path.join(UI_ROOT, "icons", "refresh.svg"), "image/svg+xml"],
24
+ ["search", "/assets/icons/search.svg", path.join(UI_ROOT, "icons", "search.svg"), "image/svg+xml"],
25
+ ["security", "/assets/icons/security.svg", path.join(UI_ROOT, "icons", "security.svg"), "image/svg+xml"],
26
+ ["settings", "/assets/icons/settings.svg", path.join(UI_ROOT, "icons", "settings.svg"), "image/svg+xml"],
27
+ ]);
28
+
29
+ // MCP Apps receive one self-contained HTML resource. This composer wraps the
30
+ // exact shared modules instead of maintaining a second Workbench implementation.
31
+ export async function composeWorkbenchMcpAppHtml(options = {}) {
32
+ const title = options.title ?? "HelloLynk · Codex";
33
+ if (!["HelloLynk · Codex", "HelloLynk · Claude"].includes(title)) throw new Error("workbench_mcp_title_invalid");
34
+ const [html, css, entry, moduleSources, assetSources] = await Promise.all([
35
+ readFile(path.join(UI_ROOT, "index.html"), "utf8"),
36
+ readFile(path.join(UI_ROOT, "styles.css"), "utf8"),
37
+ readFile(path.join(UI_ROOT, "mcp-app-entry.mjs"), "utf8"),
38
+ Promise.all(MODULES.map(([, file]) => readFile(file, "utf8"))),
39
+ Promise.all(ASSETS.map(([, , file]) => readFile(file))),
40
+ ]);
41
+ const expectedEntry = '<script type="module" src="/assets/loopback-entry.mjs"></script>';
42
+ const expectedStyle = '<link rel="stylesheet" href="/assets/styles.css">';
43
+ if (!html.includes(expectedEntry) || !html.includes(expectedStyle)) throw new Error("workbench_mcp_template_invalid");
44
+
45
+ const wrapped = [];
46
+ const exportedByModule = {};
47
+ for (let index = 0; index < MODULES.length; index += 1) {
48
+ const [label] = MODULES[index];
49
+ const source = moduleSources[index].replace(/\r\n?/g, "\n");
50
+ const exports = exportedNames(source);
51
+ exportedByModule[label] = exports;
52
+ const body = stripModuleSyntax(source);
53
+ wrapped.push(`const __hellolynk_${label} = (() => {\n${body}\nreturn Object.freeze({ ${exports.join(", ")} });\n})();`);
54
+ wrapped.push(`const { ${exports.join(", ")} } = __hellolynk_${label};`);
55
+ }
56
+ const entryBody = stripModuleSyntax(entry.replace(/\r\n?/g, "\n"));
57
+ const script = `${wrapped.join("\n")}\n${entryBody}`.replaceAll("</script", "<\\/script");
58
+ let output = html
59
+ .replace(expectedStyle, `<style>${css}</style>`)
60
+ .replace(expectedEntry, `<script>${script}</script>`)
61
+ .replace("<title>HelloLynk</title>", `<title>${title}</title>`);
62
+ for (let index = 0; index < ASSETS.length; index += 1) {
63
+ const [, sourceUrl, , mime] = ASSETS[index];
64
+ output = output.replaceAll(sourceUrl, `data:${mime};base64,${assetSources[index].toString("base64")}`);
65
+ }
66
+
67
+ const manifest = Object.freeze({
68
+ contract: "hellolynk-workbench-mcp-app-bundle-v1",
69
+ sourceContract: "hellolynk-workbench-snapshot-v12",
70
+ sourceHashes: Object.freeze({
71
+ html: sha256(html),
72
+ css: sha256(css),
73
+ entry: sha256(entry),
74
+ ...Object.fromEntries(MODULES.map(([label], index) => [label, sha256(moduleSources[index])])),
75
+ ...Object.fromEntries(ASSETS.map(([label], index) => [label, sha256(assetSources[index])])),
76
+ }),
77
+ outputSha256: sha256(output),
78
+ });
79
+ if (options.withManifest === true) return Object.freeze({ html: output, manifest });
80
+ return output;
81
+ }
82
+
83
+ function exportedNames(source) {
84
+ const names = [...source.matchAll(/^export\s+(?:const|class|function)\s+([A-Za-z_$][\w$]*)/gm)].map((match) => match[1]);
85
+ if (!names.length || names.length !== new Set(names).size) throw new Error("workbench_mcp_exports_invalid");
86
+ return names;
87
+ }
88
+
89
+ function stripModuleSyntax(source) {
90
+ return source
91
+ .replace(/^import\s+[\s\S]*?\s+from\s+["'][^"']+["'];\s*$/gm, "")
92
+ .replace(/^export\s+(?=(?:const|class|function)\s+)/gm, "");
93
+ }
94
+
95
+ function sha256(value) {
96
+ return crypto.createHash("sha256").update(value).digest("hex");
97
+ }
@@ -0,0 +1,485 @@
1
+ import {
2
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES,
3
+ HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
4
+ HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
5
+ validateWorkbenchCommand,
6
+ validateWorkbenchCommandResult,
7
+ validateWorkbenchSnapshot,
8
+ workbenchCommandWindowFromKey,
9
+ } from "./contract.mjs";
10
+
11
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{1,198}$/;
12
+ const SAFE_REVISION = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,254}$/;
13
+ const REAL_TRANSPORTS = new Set(["loopback-http", "mcp-app-rpc", "cursor-webview-rpc"]);
14
+ const REVISION_OPTIONAL_COMMANDS = new Set([
15
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.openAccount,
16
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus,
17
+ ]);
18
+ // These commands name an exact request/relationship/block (or a Host-bound
19
+ // search target). Their controller/cloud endpoint validates that target and
20
+ // owns the durable outcome; rereading unrelated lists adds no authority.
21
+ const REMOTE_RELATIONSHIP_COMMANDS = new Set([
22
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.sendContactRequest,
23
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.respondContactRequest,
24
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.recoverContactRequestDecision,
25
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.cancelContactRequest,
26
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.endRelationship,
27
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.removeBlock,
28
+ ]);
29
+ // These Host-authorized reads return authority-bound results without changing
30
+ // local state. They do not need the unrelated snapshot a mutation refreshes.
31
+ // Instance rename is a local compare-and-set: the Host validates the observed
32
+ // identity and label inside the store lock, without consulting cloud lists.
33
+ const SNAPSHOT_REFRESH_EXEMPT_COMMANDS = new Set([
34
+ ...REMOTE_RELATIONSHIP_COMMANDS,
35
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContact,
36
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestSend,
37
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal,
38
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus,
39
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.listContactInstances,
40
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContactInstance,
41
+ ]);
42
+ const AUTHORITY_BOUND_RESULT_COMMANDS = new Set([
43
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContact,
44
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.setContactAuthorization,
45
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.resendDeletedReply,
46
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.configureHistory,
47
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteSent,
48
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.hideThread,
49
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.restoreThread,
50
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearThread,
51
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteThread,
52
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteArchivedThreads,
53
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContactInstance,
54
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestSend,
55
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus,
56
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.listContactInstances,
57
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal,
58
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.sendContactRequest,
59
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearResolvedContactRequests,
60
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.cancelContactRequest,
61
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.endRelationship,
62
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.removeBlock,
63
+ ]);
64
+ const OFFLINE_LOCAL_COMMANDS = new Set([
65
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContact,
66
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.markRead,
67
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.markAllRead,
68
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.setAutoRead,
69
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteRead,
70
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.setContactAuthorization,
71
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteContact,
72
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteContactInstance,
73
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContactInstance,
74
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearResolvedContactRequests,
75
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.configureHistory,
76
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteSent,
77
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.hideThread,
78
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.restoreThread,
79
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.clearThread,
80
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteThread,
81
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteArchivedThreads,
82
+ ]);
83
+ // These actions carry their own local target/boundary. The Host checks message
84
+ // IDs, receipt watermarks, conversation ownership or the name CAS under its
85
+ // store lock. Unrelated inbox/connection revisions do not change that intent.
86
+ // Permissions and broad destructive confirmations still use snapshot revision.
87
+ const TARGET_SCOPED_LOCAL_COMMANDS = new Set([
88
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.markRead,
89
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.markAllRead,
90
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.setAutoRead,
91
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteRead,
92
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.deleteSent,
93
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.hideThread,
94
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.restoreThread,
95
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContact,
96
+ HELLOLYNK_WORKBENCH_COMMAND_TYPES.renameContactInstance,
97
+ ]);
98
+ const CLAIMED_HOST_GENERATIONS = new Set();
99
+
100
+ export class HelloLynkWorkbenchServiceError extends Error {
101
+ constructor(code) {
102
+ super(code);
103
+ this.name = "HelloLynkWorkbenchServiceError";
104
+ this.code = code;
105
+ }
106
+ }
107
+
108
+ export function createHelloLynkWorkbenchService(options = {}) {
109
+ const hostGeneration = opaqueId(options.hostGeneration, "workbench_service_host_generation_invalid");
110
+ const writerRegistrationKey = opaqueId(
111
+ options.writerRegistrationKey ?? hostGeneration,
112
+ "workbench_service_writer_registration_key_invalid",
113
+ );
114
+ let currentRevision = opaqueRevision(options.revision, "workbench_service_revision_invalid");
115
+ let currentChangeCursor = opaqueRevision(options.changeCursor, "workbench_service_change_cursor_invalid");
116
+ const readSnapshotFromHost = requiredFunction(options.readSnapshot, "workbench_service_read_snapshot_required");
117
+ const executeCommandInHost = requiredFunction(options.executeCommand, "workbench_service_execute_command_required");
118
+ const runCommandOperation = options.runCommandOperation == null
119
+ ? (_command, operation) => operation()
120
+ : requiredFunction(options.runCommandOperation, "workbench_service_command_operation_invalid");
121
+ const serviceMode = options.explicitSynthetic === true ? "synthetic-preview" : "neutral-host";
122
+ const refreshBeforeCommand = options.refreshBeforeCommand === true;
123
+ const idempotencyCapacity = positiveInteger(
124
+ options.idempotencyCapacity ?? 4_096,
125
+ "workbench_service_idempotency_capacity_invalid",
126
+ );
127
+ if (CLAIMED_HOST_GENERATIONS.has(writerRegistrationKey)) {
128
+ fail("workbench_service_writer_already_registered");
129
+ }
130
+ CLAIMED_HOST_GENERATIONS.add(writerRegistrationKey);
131
+
132
+ let operationTail = Promise.resolve();
133
+ let revisionObservation = 0;
134
+ const commandInstanceId = globalThis.crypto.randomUUID().replaceAll("-", "");
135
+ let commandEpoch = 1;
136
+ const currentCommandWindow = () => `wb5.${commandInstanceId}.${commandEpoch}`;
137
+ let snapshotRequiredAfterUnknown = false;
138
+ const idempotencyJournal = new Map();
139
+
140
+ function runExclusive(operation) {
141
+ const running = operationTail.then(operation, operation);
142
+ operationTail = running.then(() => undefined, () => undefined);
143
+ return running;
144
+ }
145
+
146
+ async function readSnapshot(input = {}, context = {}) {
147
+ const source = validateSource(context.source, serviceMode);
148
+ const authority = context.authority ?? null;
149
+ return runExclusive(async () => {
150
+ const rawSnapshot = await readSnapshotFromHost(input, Object.freeze({
151
+ hostGeneration,
152
+ revision: currentRevision,
153
+ changeCursor: currentChangeCursor,
154
+ source,
155
+ authority,
156
+ }));
157
+ const snapshot = validateWorkbenchSnapshot(rawSnapshot);
158
+ if (snapshot.hostGeneration !== hostGeneration) {
159
+ fail("workbench_service_host_generation_mismatch");
160
+ }
161
+ assertSnapshotSource(snapshot.source, source);
162
+ currentRevision = snapshot.revision;
163
+ currentChangeCursor = snapshot.changeCursor;
164
+ revisionObservation += 1;
165
+ snapshotRequiredAfterUnknown = false;
166
+ // This serial read is the retirement barrier. Admissions made after this
167
+ // read was queued still reserve slots, so do not retire their window.
168
+ if (idempotencyJournal.size >= idempotencyCapacity
169
+ && [...idempotencyJournal.values()].every((entry) => entry.settled)) {
170
+ if (!Number.isSafeInteger(commandEpoch + 1)) fail("workbench_command_window_exhausted");
171
+ commandEpoch += 1;
172
+ idempotencyJournal.clear();
173
+ }
174
+ return validateWorkbenchSnapshot({ ...snapshot, commandWindow: currentCommandWindow() });
175
+ });
176
+ }
177
+
178
+ function executeCommand(value, context = {}) {
179
+ const command = validateWorkbenchCommand(value);
180
+ const source = validateSource(context.source, serviceMode);
181
+ const authority = context.authority ?? null;
182
+ const fingerprint = commandFingerprint(command);
183
+ const authorityKey = commandAuthorityKey(command.command, authority);
184
+ if (workbenchCommandWindowFromKey(command.idempotencyKey) !== currentCommandWindow()) {
185
+ // A retired/foreign service may have executed this intent. We know only
186
+ // that it cannot execute here, not its original business outcome.
187
+ return Promise.resolve(failureResult(command, {
188
+ code: "host_app_command_window_expired", currentRevision,
189
+ outcome: "unknown",
190
+ }));
191
+ }
192
+ if (AUTHORITY_BOUND_RESULT_COMMANDS.has(command.command) && authorityKey == null) {
193
+ return runExclusive(() => failureResult(command, {
194
+ code: "host_app_authorization_required",
195
+ currentRevision,
196
+ }));
197
+ }
198
+ const existing = idempotencyJournal.get(command.idempotencyKey);
199
+
200
+ if (existing) {
201
+ if (existing.fingerprint !== fingerprint || existing.authorityKey !== authorityKey) {
202
+ return runExclusive(() => failureResult(command, {
203
+ code: "host_app_idempotency_conflict",
204
+ currentRevision,
205
+ }));
206
+ }
207
+ return existing.promise.then((result) => rebindRequestId(result, command.requestId, command.command));
208
+ }
209
+
210
+ if (idempotencyJournal.size >= idempotencyCapacity) {
211
+ return Promise.resolve(failureResult(command, {
212
+ code: "host_app_command_window_full",
213
+ currentRevision,
214
+ }));
215
+ }
216
+
217
+ const entry = { fingerprint, authorityKey, promise: null, settled: false };
218
+ entry.promise = runExclusive(async () => {
219
+ try { return await runCommandOperation(command, () => executeOnce(command, source, authority)); }
220
+ catch { return unknownOutcomeResult(command); }
221
+ }).then((result) => result.deferredCompletion ?? result)
222
+ .finally(() => { entry.settled = true; });
223
+ idempotencyJournal.set(command.idempotencyKey, entry);
224
+ return entry.promise;
225
+ }
226
+
227
+ async function executeOnce(command, source, authority) {
228
+ if (snapshotRequiredAfterUnknown
229
+ && command.command !== HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus) {
230
+ return failureResult(command, {
231
+ code: "host_app_snapshot_required",
232
+ currentRevision: null,
233
+ resultRevision: null,
234
+ resultChangeCursor: null,
235
+ });
236
+ }
237
+ if (refreshBeforeCommand && !SNAPSHOT_REFRESH_EXEMPT_COMMANDS.has(command.command)
238
+ && !OFFLINE_LOCAL_COMMANDS.has(command.command)) {
239
+ let snapshot;
240
+ try {
241
+ snapshot = validateWorkbenchSnapshot(await readSnapshotFromHost({}, Object.freeze({
242
+ hostGeneration,
243
+ revision: currentRevision,
244
+ changeCursor: currentChangeCursor,
245
+ source,
246
+ authority,
247
+ beforeCommand: true,
248
+ commandType: command.command,
249
+ })));
250
+ if (snapshot.hostGeneration !== hostGeneration) {
251
+ fail("workbench_service_host_generation_mismatch");
252
+ }
253
+ assertSnapshotSource(snapshot.source, source);
254
+ } catch {
255
+ // No command has reached the Host yet. A failed preflight read cannot
256
+ // make its write outcome unknown or lock unrelated local actions.
257
+ return failureResult(command, { code: "host_app_snapshot_unavailable", currentRevision });
258
+ }
259
+ currentRevision = snapshot.revision;
260
+ currentChangeCursor = snapshot.changeCursor;
261
+ revisionObservation += 1;
262
+ if (snapshot.security.hostStatus === "offline"
263
+ && !OFFLINE_LOCAL_COMMANDS.has(command.command)) {
264
+ return failureResult(command, { code: "host_app_cloud_unavailable", currentRevision });
265
+ }
266
+ }
267
+ if (command.expectedRevision == null && !REVISION_OPTIONAL_COMMANDS.has(command.command)) {
268
+ return failureResult(command, {
269
+ code: "host_app_revision_required",
270
+ currentRevision,
271
+ });
272
+ }
273
+ if (command.expectedRevision != null && command.expectedRevision !== currentRevision
274
+ && !TARGET_SCOPED_LOCAL_COMMANDS.has(command.command)) {
275
+ return failureResult(command, {
276
+ code: "host_app_revision_conflict",
277
+ currentRevision,
278
+ });
279
+ }
280
+
281
+ let pendingResult;
282
+ try {
283
+ pendingResult = Promise.resolve(executeCommandInHost(command, Object.freeze({
284
+ hostGeneration,
285
+ revision: currentRevision,
286
+ changeCursor: currentChangeCursor,
287
+ source,
288
+ authority,
289
+ }))).catch(() => null);
290
+ } catch {
291
+ return unknownOutcomeResult(command);
292
+ }
293
+
294
+ if (command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.requestRetry
295
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.resendDeletedReply
296
+ || command.command === HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal
297
+ || REMOTE_RELATIONSHIP_COMMANDS.has(command.command)) {
298
+ // Admission, the fresh revision check and the journal reservation remain
299
+ // serial. The Host owns the durable retry claim; awaiting transport must
300
+ // not prevent this same page from reading or revoking authorization.
301
+ // Directory search only returns authority-bound targets. Its cloud wait
302
+ // must also leave local reads and actions available.
303
+ // Targeted relationship commands keep their controller's per-record
304
+ // admission and server checks, while local revocation remains usable.
305
+ const admittedObservation = revisionObservation;
306
+ return { deferredCompletion: pendingResult.then((rawResult) => runExclusive(() =>
307
+ finishCommand(command, rawResult, revisionObservation === admittedObservation))) };
308
+ }
309
+ return finishCommand(command, await pendingResult);
310
+ }
311
+
312
+ function finishCommand(command, rawResult, updateRevision = true) {
313
+ let result;
314
+ try {
315
+ result = validateWorkbenchCommandResult(rawResult, command.command);
316
+ if (result.requestId !== command.requestId) fail("workbench_service_request_id_mismatch");
317
+ if (result.ok && (result.revision == null || result.changeCursor == null)) {
318
+ fail("workbench_service_success_metadata_missing");
319
+ }
320
+ } catch {
321
+ return unknownOutcomeResult(command);
322
+ }
323
+
324
+ // A delayed retry may return a snapshot older than a read or revocation
325
+ // that completed meanwhile. Its outcome remains valid, not its authority
326
+ // to replace the service's newer observed baseline.
327
+ if (updateRevision) {
328
+ if (result.revision != null) currentRevision = result.revision;
329
+ if (result.changeCursor != null) currentChangeCursor = result.changeCursor;
330
+ if (result.revision != null || result.changeCursor != null) revisionObservation += 1;
331
+ }
332
+ if (!result.ok && result.error.outcome === "unknown"
333
+ && command.command !== HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal
334
+ && command.command !== HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus) {
335
+ snapshotRequiredAfterUnknown = true;
336
+ }
337
+ return result;
338
+ }
339
+
340
+ function unknownOutcomeResult(command) {
341
+ // Directory search can consume remote quota, but it cannot mutate local
342
+ // contacts/messages or authorize a later write. Keep its exact
343
+ // idempotency record for replay, without blocking a new human search on a
344
+ // full snapshot that may be unrelated to the directory failure.
345
+ if (command.command !== HELLOLYNK_WORKBENCH_COMMAND_TYPES.searchPrincipal
346
+ && command.command !== HELLOLYNK_WORKBENCH_COMMAND_TYPES.getSendStatus) {
347
+ snapshotRequiredAfterUnknown = true;
348
+ }
349
+ return validateWorkbenchCommandResult({
350
+ contractId: HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
351
+ schemaVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
352
+ ok: false,
353
+ requestId: command.requestId,
354
+ revision: null,
355
+ changeCursor: null,
356
+ error: {
357
+ code: "workbench_command_outcome_unknown",
358
+ retryable: false,
359
+ outcome: "unknown",
360
+ retryAfterMs: null,
361
+ currentRevision: null,
362
+ },
363
+ });
364
+ }
365
+
366
+ function failureResult(command, {
367
+ code,
368
+ currentRevision: failureRevision,
369
+ resultRevision = currentRevision,
370
+ resultChangeCursor = currentChangeCursor,
371
+ outcome = "known-failed",
372
+ }) {
373
+ return validateWorkbenchCommandResult({
374
+ contractId: HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
375
+ schemaVersion: HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
376
+ ok: false,
377
+ requestId: command.requestId,
378
+ revision: resultRevision,
379
+ changeCursor: resultChangeCursor,
380
+ error: {
381
+ code,
382
+ retryable: false,
383
+ outcome,
384
+ retryAfterMs: null,
385
+ currentRevision: failureRevision,
386
+ },
387
+ });
388
+ }
389
+
390
+ return Object.freeze({
391
+ hostGeneration,
392
+ readSnapshot,
393
+ executeCommand,
394
+ getState() {
395
+ return Object.freeze({
396
+ hostGeneration,
397
+ commandWindow: currentCommandWindow(),
398
+ revision: currentRevision,
399
+ changeCursor: currentChangeCursor,
400
+ snapshotRequired: snapshotRequiredAfterUnknown,
401
+ idempotencyEntries: idempotencyJournal.size,
402
+ });
403
+ },
404
+ });
405
+ }
406
+
407
+ function commandFingerprint(command) {
408
+ return JSON.stringify({
409
+ contractId: command.contractId,
410
+ schemaVersion: command.schemaVersion,
411
+ command: command.command,
412
+ expectedRevision: command.expectedRevision,
413
+ payload: command.payload,
414
+ });
415
+ }
416
+
417
+ function commandAuthorityKey(commandType, authority) {
418
+ if (!AUTHORITY_BOUND_RESULT_COMMANDS.has(commandType)) return null;
419
+ const fields = ["hostGeneration", "delegateId", "capabilityId", "adapterInstanceId"];
420
+ if (!authority || fields.some((field) => typeof authority[field] !== "string" || !SAFE_ID.test(authority[field]))) {
421
+ return null;
422
+ }
423
+ return fields.map((field) => authority[field]).join("\u001f");
424
+ }
425
+
426
+ function rebindRequestId(result, requestId, expectedCommand) {
427
+ if (result.requestId === requestId) return result;
428
+ return validateWorkbenchCommandResult({ ...result, requestId }, expectedCommand);
429
+ }
430
+
431
+ function validateSource(value, serviceMode) {
432
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
433
+ fail("workbench_service_source_required");
434
+ }
435
+ const source = Object.freeze({
436
+ mode: value.mode,
437
+ transport: value.transport,
438
+ clientId: opaqueId(value.clientId, "workbench_service_source_invalid"),
439
+ clientLabel: boundedText(value.clientLabel, 80, "workbench_service_source_invalid"),
440
+ });
441
+ const synthetic = source.mode === "synthetic-preview" && source.transport === "synthetic-memory";
442
+ const real = source.mode === "neutral-host" && REAL_TRANSPORTS.has(source.transport);
443
+ if (serviceMode === "synthetic-preview" ? !synthetic : !real) {
444
+ fail(synthetic ? "workbench_service_synthetic_not_enabled" : "workbench_service_source_invalid");
445
+ }
446
+ return source;
447
+ }
448
+
449
+ function assertSnapshotSource(actual, expected) {
450
+ if (actual.mode !== expected.mode
451
+ || actual.transport !== expected.transport
452
+ || actual.clientId !== expected.clientId
453
+ || actual.clientLabel !== expected.clientLabel) {
454
+ fail("workbench_service_snapshot_source_mismatch");
455
+ }
456
+ }
457
+
458
+ function requiredFunction(value, code) {
459
+ if (typeof value !== "function") fail(code);
460
+ return value;
461
+ }
462
+
463
+ function opaqueId(value, code) {
464
+ if (typeof value !== "string" || !SAFE_ID.test(value)) fail(code);
465
+ return value;
466
+ }
467
+
468
+ function opaqueRevision(value, code) {
469
+ if (typeof value !== "string" || !SAFE_REVISION.test(value)) fail(code);
470
+ return value;
471
+ }
472
+
473
+ function boundedText(value, maximum, code) {
474
+ if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\u0000")) fail(code);
475
+ return value;
476
+ }
477
+
478
+ function positiveInteger(value, code) {
479
+ if (!Number.isSafeInteger(value) || value < 1) fail(code);
480
+ return value;
481
+ }
482
+
483
+ function fail(code) {
484
+ throw new HelloLynkWorkbenchServiceError(code);
485
+ }